diff --git a/packages/client/src/effect/api.ts b/packages/client/src/effect/api.ts index e7cb2012f5..d0b2170345 100644 --- a/packages/client/src/effect/api.ts +++ b/packages/client/src/effect/api.ts @@ -1,7 +1,9 @@ -import type { ModelApi, ProviderApi } from "./api/api.js" +import type { ModelApi, ProviderApi, WebsearchApi } from "./api/api.js" export type * from "./api/api.js" +export type WebSearchApi = WebsearchApi + export interface CatalogApi { readonly provider: ProviderApi readonly model: ModelApi diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 11dcf4cbde..647b85a982 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -923,33 +923,38 @@ export interface DebugApi { readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } } -type Endpoint26_0Request = Parameters[0] +type Endpoint26_0Request = Parameters[0] export type Endpoint26_0Input = { readonly location?: Endpoint26_0Request["query"]["location"] } -export type Endpoint26_0Output = EffectValue> -export type SearchProviderGetOperation = (input?: Endpoint26_0Input) => Effect.Effect +export type Endpoint26_0Output = EffectValue> +export type WebsearchProviderGetOperation = ( + input?: Endpoint26_0Input, +) => Effect.Effect -type Endpoint26_1Request = Parameters[0] +type Endpoint26_1Request = Parameters[0] export type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] readonly providerID: Endpoint26_1Request["payload"]["providerID"] } -export type Endpoint26_1Output = EffectValue> -export type SearchProviderSelectOperation = ( +export type Endpoint26_1Output = EffectValue> +export type WebsearchProviderSelectOperation = ( input: Endpoint26_1Input, ) => Effect.Effect -type Endpoint26_2Request = Parameters[0] +type Endpoint26_2Request = Parameters[0] export type Endpoint26_2Input = { readonly location?: Endpoint26_2Request["query"]["location"] readonly query: Endpoint26_2Request["payload"]["query"] readonly providerID?: Endpoint26_2Request["payload"]["providerID"] } -export type Endpoint26_2Output = EffectValue> -export type SearchQueryOperation = (input: Endpoint26_2Input) => Effect.Effect +export type Endpoint26_2Output = EffectValue> +export type WebsearchQueryOperation = (input: Endpoint26_2Input) => Effect.Effect -export interface SearchApi { - readonly provider: { readonly get: SearchProviderGetOperation; readonly select: SearchProviderSelectOperation } - readonly query: SearchQueryOperation +export interface WebsearchApi { + readonly provider: { + readonly get: WebsearchProviderGetOperation + readonly select: WebsearchProviderSelectOperation + } + readonly query: WebsearchQueryOperation } export interface AppApi { @@ -979,5 +984,5 @@ export interface AppApi { readonly projectCopy: ProjectCopyApi readonly vcs: VcsApi readonly debug: DebugApi - readonly search: SearchApi + readonly websearch: WebsearchApi } diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 54f12eeb0b..63f50c2bd5 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -1099,35 +1099,35 @@ const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) }, }) -type Endpoint26_0Request = Parameters[0] +type Endpoint26_0Request = Parameters[0] type Endpoint26_0Input = { readonly location?: Endpoint26_0Request["query"]["location"] } -const Endpoint26_0 = (raw: RawClient["server.search"]) => (input?: Endpoint26_0Input) => - raw["search.provider.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) +const Endpoint26_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint26_0Input) => + raw["websearch.provider.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint26_1Request = Parameters[0] +type Endpoint26_1Request = Parameters[0] type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] readonly providerID: Endpoint26_1Request["payload"]["providerID"] } -const Endpoint26_1 = (raw: RawClient["server.search"]) => (input: Endpoint26_1Input) => - raw["search.provider.select"]({ +const Endpoint26_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint26_1Input) => + raw["websearch.provider.select"]({ query: { location: input["location"] }, payload: { providerID: input["providerID"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint26_2Request = Parameters[0] +type Endpoint26_2Request = Parameters[0] type Endpoint26_2Input = { readonly location?: Endpoint26_2Request["query"]["location"] readonly query: Endpoint26_2Request["payload"]["query"] readonly providerID?: Endpoint26_2Request["payload"]["providerID"] } -const Endpoint26_2 = (raw: RawClient["server.search"]) => (input: Endpoint26_2Input) => - raw["search.query"]({ +const Endpoint26_2 = (raw: RawClient["server.websearch"]) => (input: Endpoint26_2Input) => + raw["websearch.query"]({ query: { location: input["location"] }, payload: { query: input["query"], providerID: input["providerID"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup26 = (raw: RawClient["server.search"]) => ({ +const adaptGroup26 = (raw: RawClient["server.websearch"]) => ({ provider: { get: Endpoint26_0(raw), select: Endpoint26_1(raw) }, query: Endpoint26_2(raw), }) @@ -1159,7 +1159,7 @@ const adaptClient = (raw: RawClient) => ({ projectCopy: adaptGroup23(raw["server.projectCopy"]), vcs: adaptGroup24(raw["server.vcs"]), debug: adaptGroup25(raw["server.debug"]), - search: adaptGroup26(raw["server.search"]), + websearch: adaptGroup26(raw["server.websearch"]), }) export const make = (options?: { readonly baseUrl?: URL | string }) => diff --git a/packages/client/src/effect/index.ts b/packages/client/src/effect/index.ts index 109901fc12..42ed490fb3 100644 --- a/packages/client/src/effect/index.ts +++ b/packages/client/src/effect/index.ts @@ -14,7 +14,7 @@ export type { PluginApi, ProviderApi, ReferenceApi, - SearchApi, + WebSearchApi, SessionApi, SkillApi, } from "./api.js" @@ -37,7 +37,7 @@ export { Provider } from "@opencode-ai/schema/provider" export { Pty } from "@opencode-ai/schema/pty" export { Question } from "@opencode-ai/schema/question" export { Reference } from "@opencode-ai/schema/reference" -export { Search } from "@opencode-ai/schema/search" +export { WebSearch } from "@opencode-ai/schema/websearch" export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema" export { Session } from "@opencode-ai/schema/session" export { SessionInput } from "@opencode-ai/schema/session-input" diff --git a/packages/client/src/promise/api.ts b/packages/client/src/promise/api.ts index 86ca65e9b1..c0104123ca 100644 --- a/packages/client/src/promise/api.ts +++ b/packages/client/src/promise/api.ts @@ -7,7 +7,7 @@ import type { PluginApi as EffectPluginApi, ProviderApi as EffectProviderApi, ReferenceApi as EffectReferenceApi, - SearchApi as EffectSearchApi, + WebsearchApi, SessionApi as EffectSessionApi, SkillApi as EffectSkillApi, } from "../effect/api/api.js" @@ -37,7 +37,7 @@ export type ModelApi = PromisifyApi> export type PluginApi = PromisifyApi> export type ProviderApi = PromisifyApi> export type ReferenceApi = PromisifyApi> -export type SearchApi = PromisifyApi> +export type WebSearchApi = PromisifyApi> export type SessionApi = PromisifyApi> export type SkillApi = PromisifyApi> diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index fd18840692..99c88df2d4 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -184,12 +184,12 @@ import type { DebugLocationListOutput, DebugLocationEvictInput, DebugLocationEvictOutput, - SearchProviderGetInput, - SearchProviderGetOutput, - SearchProviderSelectInput, - SearchProviderSelectOutput, - SearchQueryInput, - SearchQueryOutput, + WebsearchProviderGetInput, + WebsearchProviderGetOutput, + WebsearchProviderSelectInput, + WebsearchProviderSelectOutput, + WebsearchQueryInput, + WebsearchQueryOutput, } from "./types" import { ClientError } from "./client-error" @@ -1566,13 +1566,13 @@ export function make(options: ClientOptions) { ), }, }, - search: { + websearch: { provider: { - get: (input?: SearchProviderGetInput, requestOptions?: RequestOptions) => - request( + get: (input?: WebsearchProviderGetInput, requestOptions?: RequestOptions) => + request( { method: "GET", - path: `/api/search/provider`, + path: `/api/websearch/provider`, query: { location: input?.["location"] }, successStatus: 200, declaredStatuses: [401, 400], @@ -1580,11 +1580,11 @@ export function make(options: ClientOptions) { }, requestOptions, ), - select: (input: SearchProviderSelectInput, requestOptions?: RequestOptions) => - request( + select: (input: WebsearchProviderSelectInput, requestOptions?: RequestOptions) => + request( { method: "POST", - path: `/api/search/provider`, + path: `/api/websearch/provider`, query: { location: input["location"] }, body: { providerID: input["providerID"] }, successStatus: 204, @@ -1594,11 +1594,11 @@ export function make(options: ClientOptions) { requestOptions, ), }, - query: (input: SearchQueryInput, requestOptions?: RequestOptions) => - request( + query: (input: WebsearchQueryInput, requestOptions?: RequestOptions) => + request( { method: "POST", - path: `/api/search`, + path: `/api/websearch`, query: { location: input["location"] }, body: { query: input["query"], providerID: input["providerID"] }, successStatus: 200, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 42926da1e2..d218f41ca0 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -2306,7 +2306,7 @@ export type IntegrationListOutput = { | { readonly type: "key"; readonly label?: string } | { readonly type: "env"; readonly names: ReadonlyArray } > - readonly search?: { readonly connection: "optional" | "required" } + readonly websearch?: { readonly connection: "optional" | "required" } readonly connections: ReadonlyArray< | { readonly type: "credential"; readonly id: string; readonly label: string } | { readonly type: "env"; readonly name: string } @@ -2359,7 +2359,7 @@ export type IntegrationGetOutput = { | { readonly type: "key"; readonly label?: string } | { readonly type: "env"; readonly names: ReadonlyArray } > - readonly search?: { readonly connection: "optional" | "required" } + readonly websearch?: { readonly connection: "optional" | "required" } readonly connections: ReadonlyArray< | { readonly type: "credential"; readonly id: string; readonly label: string } | { readonly type: "env"; readonly name: string } @@ -6279,13 +6279,13 @@ export type DebugLocationEvictInput = { export type DebugLocationEvictOutput = void -export type SearchProviderGetInput = { +export type WebsearchProviderGetInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined }["location"] } -export type SearchProviderGetOutput = { +export type WebsearchProviderGetOutput = { readonly location: { readonly directory: string readonly workspaceID?: string @@ -6294,16 +6294,16 @@ export type SearchProviderGetOutput = { readonly data: string | null } -export type SearchProviderSelectInput = { +export type WebsearchProviderSelectInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined }["location"] readonly providerID: { readonly providerID: string }["providerID"] } -export type SearchProviderSelectOutput = void +export type WebsearchProviderSelectOutput = void -export type SearchQueryInput = { +export type WebsearchQueryInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined }["location"] @@ -6311,7 +6311,7 @@ export type SearchQueryInput = { readonly providerID?: { readonly query: string; readonly providerID?: string }["providerID"] } -export type SearchQueryOutput = { +export type WebsearchQueryOutput = { readonly location: { readonly directory: string readonly workspaceID?: string diff --git a/packages/client/src/promise/index.ts b/packages/client/src/promise/index.ts index 75b59c5d4b..dd64831e01 100644 --- a/packages/client/src/promise/index.ts +++ b/packages/client/src/promise/index.ts @@ -9,7 +9,7 @@ export type { PluginApi, ProviderApi, ReferenceApi, - SearchApi, + WebSearchApi, SessionApi, SkillApi, } from "./api.js" diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 3406491fb4..684a74a195 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -31,7 +31,7 @@ test("exposes every standard HTTP API group", () => { "projectCopy", "vcs", "debug", - "search", + "websearch", ]) expect(Object.keys(client.debug)).toEqual(["location"]) expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) @@ -39,8 +39,8 @@ test("exposes every standard HTTP API group", () => { expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"]) expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"]) expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"]) - expect(Object.keys(client.search)).toEqual(["provider", "query"]) - expect(Object.keys(client.search.provider)).toEqual(["get", "select"]) + expect(Object.keys(client.websearch)).toEqual(["provider", "query"]) + expect(Object.keys(client.websearch.provider)).toEqual(["get", "select"]) expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) expect(Object.keys(client.vcs)).toEqual(["status", "diff"]) expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"]) @@ -48,7 +48,7 @@ test("exposes every standard HTTP API group", () => { expect(Object.keys(client.project)).toEqual(["list", "current", "directories"]) }) -test("search.query uses the public HTTP contract", async () => { +test("websearch.query uses the public HTTP contract", async () => { let request: Request | undefined const client = OpenCode.make({ baseUrl: "http://localhost:3000", @@ -61,7 +61,7 @@ test("search.query uses the public HTTP contract", async () => { }, }) - const result = await client.search.query({ + const result = await client.websearch.query({ query: "opencode", providerID: "exa", location: { directory: "/tmp/project" }, @@ -69,11 +69,11 @@ test("search.query uses the public HTTP contract", async () => { expect(result.data).toEqual({ providerID: "exa", text: "result", metadata: { requestID: "req_test" } }) expect(request?.method).toBe("POST") - expect(request?.url).toBe("http://localhost:3000/api/search?location%5Bdirectory%5D=%2Ftmp%2Fproject") + expect(request?.url).toBe("http://localhost:3000/api/websearch?location%5Bdirectory%5D=%2Ftmp%2Fproject") expect(await request?.json()).toEqual({ query: "opencode", providerID: "exa" }) }) -test("search provider methods use the public HTTP contract", async () => { +test("websearch provider methods use the public HTTP contract", async () => { const requests: Request[] = [] const client = OpenCode.make({ baseUrl: "http://localhost:3000", @@ -88,12 +88,12 @@ test("search provider methods use the public HTTP contract", async () => { }, }) - expect(await client.search.provider.get({ location: { directory: "/tmp/project" } })).toMatchObject({ data: "exa" }) - await client.search.provider.select({ providerID: "parallel", location: { directory: "/tmp/project" } }) + expect(await client.websearch.provider.get({ location: { directory: "/tmp/project" } })).toMatchObject({ data: "exa" }) + await client.websearch.provider.select({ providerID: "parallel", location: { directory: "/tmp/project" } }) expect(requests.map((request) => [request.method, request.url])).toEqual([ - ["GET", "http://localhost:3000/api/search/provider?location%5Bdirectory%5D=%2Ftmp%2Fproject"], - ["POST", "http://localhost:3000/api/search/provider?location%5Bdirectory%5D=%2Ftmp%2Fproject"], + ["GET", "http://localhost:3000/api/websearch/provider?location%5Bdirectory%5D=%2Ftmp%2Fproject"], + ["POST", "http://localhost:3000/api/websearch/provider?location%5Bdirectory%5D=%2Ftmp%2Fproject"], ]) expect(await requests[1]?.json()).toEqual({ providerID: "parallel" }) }) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 9713921841..2867b710ee 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -24,7 +24,7 @@ import { ConfigModel } from "./config/model" import { ConfigPlugin } from "./config/plugin" import { ConfigProvider } from "./config/provider" import { ConfigReference } from "./config/reference" -import { ConfigSearch } from "./config/search" +import { ConfigWebSearch } from "./config/websearch" import { ConfigToolOutput } from "./config/tool-output" import { ConfigVariable } from "./config/variable" import { ConfigWatcher } from "./config/watcher" @@ -104,7 +104,7 @@ export class Info extends Schema.Class("Config.Info")({ references: ConfigReference.Info.pipe(Schema.optional).annotate({ description: "Named local directories or Git repositories available as external context", }), - search: ConfigSearch.Info.pipe(Schema.optional).annotate({ + websearch: ConfigWebSearch.Info.pipe(Schema.optional).annotate({ description: "Web search provider selection", }), plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ diff --git a/packages/core/src/config/search.ts b/packages/core/src/config/websearch.ts similarity index 52% rename from packages/core/src/config/search.ts rename to packages/core/src/config/websearch.ts index 7173479339..c4ab0252c6 100644 --- a/packages/core/src/config/search.ts +++ b/packages/core/src/config/websearch.ts @@ -1,8 +1,8 @@ -export * as ConfigSearch from "./search" +export * as ConfigWebSearch from "./websearch" import { Integration } from "@opencode-ai/schema/integration" import { Schema } from "effect" -export class Info extends Schema.Class("ConfigSearch.Info")({ +export class Info extends Schema.Class("ConfigWebSearch.Info")({ provider: Integration.ID, }) {} diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 5990d93fa7..906e477747 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -16,7 +16,7 @@ import { Types, } from "effect" import { Integration } from "@opencode-ai/schema/integration" -import { Search } from "@opencode-ai/schema/search" +import { WebSearch } from "@opencode-ai/schema/websearch" import { Credential } from "./credential" import { State } from "./state" import { EventV2 } from "./event" @@ -95,13 +95,13 @@ export interface EnvImplementation { export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation -export interface SearchImplementation { +export interface WebSearchImplementation { readonly integrationID: ID - readonly connection: Integration.Search["connection"] + readonly connection: Integration.WebSearch["connection"] readonly execute: ( - input: Search.Input, + input: WebSearch.Input, context: { readonly credential?: Credential.Value; readonly sessionID?: string }, - ) => Effect.Effect + ) => Effect.Effect } export const Attempt = Integration.Attempt @@ -129,7 +129,7 @@ type Entry = { ref: Types.DeepMutable methods: Types.DeepMutable[] implementations: Map> - search?: Types.DeepMutable + websearch?: Types.DeepMutable } type Data = { @@ -146,9 +146,9 @@ export type Draft = { update: (implementation: Implementation) => void remove: (integrationID: ID, method: Method) => void } - search: { - list: () => readonly SearchImplementation[] - update: (implementation: SearchImplementation) => void + websearch: { + list: () => readonly WebSearchImplementation[] + update: (implementation: WebSearchImplementation) => void remove: (integrationID: ID) => void } } @@ -207,9 +207,9 @@ export interface Interface extends State.Transformable { /** Cancels an attempt and releases its resources. */ readonly cancel: (attemptID: AttemptID) => Effect.Effect } - readonly search: { - readonly list: () => Effect.Effect - readonly get: (integrationID: ID) => Effect.Effect + readonly websearch: { + readonly list: () => Effect.Effect + readonly get: (integrationID: ID) => Effect.Effect } } @@ -301,10 +301,10 @@ const layer = Layer.effect( if (method.type === "oauth") current.implementations.delete(method.id) }, }, - search: { + websearch: { list: () => Array.from(draft.integrations.values()).flatMap((entry) => - entry.search ? [entry.search as SearchImplementation] : [], + entry.websearch ? [entry.websearch as WebSearchImplementation] : [], ), update: (implementation) => { const current = draft.integrations.get(implementation.integrationID) ?? { @@ -318,11 +318,11 @@ const layer = Layer.effect( if (!draft.integrations.has(implementation.integrationID)) { draft.integrations.set(implementation.integrationID, current) } - current.search = implementation as Types.DeepMutable + current.websearch = implementation as Types.DeepMutable }, remove: (integrationID) => { const current = draft.integrations.get(integrationID) - if (current) delete current.search + if (current) delete current.websearch }, }, }), @@ -349,7 +349,7 @@ const layer = Layer.effect( id: entry.ref.id, name: entry.ref.name, methods: entry.methods, - search: entry.search ? { connection: entry.search.connection } : undefined, + websearch: entry.websearch ? { connection: entry.websearch.connection } : undefined, connections, }) @@ -559,14 +559,14 @@ const layer = Layer.effect( if (attempt) yield* Scope.close(attempt.scope, Exit.void) }), }, - search: { - list: Effect.fn("Integration.search.list")(function* () { + websearch: { + list: Effect.fn("Integration.websearch.list")(function* () { return Array.from(state.get().integrations.values()).flatMap((entry) => - entry.search ? [entry.search as SearchImplementation] : [], + entry.websearch ? [entry.websearch as WebSearchImplementation] : [], ) }), - get: Effect.fn("Integration.search.get")(function* (integrationID) { - return state.get().integrations.get(integrationID)?.search as SearchImplementation | undefined + get: Effect.fn("Integration.websearch.get")(function* (integrationID) { + return state.get().integrations.get(integrationID)?.websearch as WebSearchImplementation | undefined }), }, }) diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 943b0b34cf..a38033e747 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -34,7 +34,7 @@ import { Pty } from "./pty" import { QuestionV2 } from "./question" import { Shell } from "./shell" import { Reference } from "./reference" -import { Search } from "./search" +import { WebSearch } from "./websearch" import { ReferenceGuidance } from "./reference/guidance" import { Ripgrep } from "./ripgrep" import { SessionRunnerLLM } from "./session/runner/llm" @@ -84,7 +84,7 @@ const pluginSupervisorNode = makeLocationNode({ Form.node, ReadToolFileSystem.node, Reference.node, - Search.node, + WebSearch.node, Ripgrep.node, SessionInstructions.node, SessionTodo.node, @@ -100,7 +100,7 @@ const locationServiceNodes = [ AgentV2.node, CommandV2.node, Reference.node, - Search.node, + WebSearch.node, Integration.node, Catalog.node, AISDK.node, diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index dbb987a0b2..e04a05f72f 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -345,11 +345,11 @@ function registerIntegration(draft: Integration.Draft, definition: IntegrationDe }), ) } - if (!definition.search) return - draft.search.update({ + if (!definition.websearch) return + draft.websearch.update({ integrationID, - connection: definition.search.connection, - execute: definition.search.execute, + connection: definition.websearch.connection, + execute: definition.websearch.execute, }) } diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 4356d7b57e..c7cfefeb50 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -26,7 +26,7 @@ import { ModelsDev } from "../models-dev" import { Npm } from "../npm" import { PermissionV2 } from "../permission" import { Reference } from "../reference" -import { Search } from "../search" +import { WebSearch } from "../websearch" import { Ripgrep } from "../ripgrep" import { SessionInstructions } from "../session/instructions" import { SessionTodo } from "../session/todo" @@ -51,7 +51,7 @@ import { AgentPlugin } from "./agent" import { CommandPlugin } from "./command" import { ModelsDevPlugin } from "./models-dev" import { ProviderPlugins } from "./provider" -import { SearchPlugins } from "./search" +import { WebSearchPlugins } from "./websearch" import { PluginRuntime } from "./runtime" import { SkillPlugin } from "./skill" import { VariantPlugin } from "./variant" @@ -78,7 +78,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { const form = yield* Form.Service const read = yield* ReadToolFileSystem.Service const reference = yield* Reference.Service - const search = yield* Search.Service + const websearch = yield* WebSearch.Service const ripgrep = yield* Ripgrep.Service const instructions = yield* SessionInstructions.Service const todo = yield* SessionTodo.Service @@ -107,7 +107,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { Context.make(Form.Service, form), Context.make(ReadToolFileSystem.Service, read), Context.make(Reference.Service, reference), - Context.make(Search.Service, search), + Context.make(WebSearch.Service, websearch), Context.make(Ripgrep.Service, ripgrep), Context.make(SessionInstructions.Service, instructions), Context.make(SessionTodo.Service, todo), @@ -129,7 +129,7 @@ const pre = [ SkillPlugin.Plugin, ModelsDevPlugin, ...ProviderPlugins, - ...SearchPlugins, + ...WebSearchPlugins, PatchTool.Plugin, EditTool.Plugin, GlobTool.Plugin, diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index a0f152ebb6..0953c0cd4d 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -131,7 +131,7 @@ export function fromPromise(plugin: PromisePlugin) { } function adaptIntegration(definition: IntegrationDefinition) { - const { methods, search, ...definitionInfo } = definition + const { methods, websearch, ...definitionInfo } = definition return { ...definitionInfo, methods: methods?.map((method) => { @@ -163,16 +163,16 @@ function adaptIntegration(definition: IntegrationDefinition) { : {}), } }), - ...(search + ...(websearch ? { - search: { - connection: search.connection, + websearch: { + connection: websearch.connection, execute: ( - input: Parameters[0], - execution: Omit[1], "signal">, + input: Parameters[0], + execution: Omit[1], "signal">, ) => Effect.tryPromise({ - try: (signal) => search.execute(input, { ...execution, signal }), + try: (signal) => websearch.execute(input, { ...execution, signal }), catch: (cause) => cause, }), }, diff --git a/packages/core/src/plugin/search/index.ts b/packages/core/src/plugin/search/index.ts deleted file mode 100644 index b816effe9b..0000000000 --- a/packages/core/src/plugin/search/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { SearchExa } from "./exa" -import { SearchParallel } from "./parallel" - -export const SearchPlugins = [SearchExa.Plugin, SearchParallel.Plugin] as const diff --git a/packages/core/src/plugin/search/exa.ts b/packages/core/src/plugin/websearch/exa.ts similarity index 85% rename from packages/core/src/plugin/search/exa.ts rename to packages/core/src/plugin/websearch/exa.ts index 73f1e0e787..6d962b642c 100644 --- a/packages/core/src/plugin/search/exa.ts +++ b/packages/core/src/plugin/websearch/exa.ts @@ -1,9 +1,9 @@ -export * as SearchExa from "./exa" +export * as WebSearchExa from "./exa" import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { Effect, Schema, Scope } from "effect" import { HttpClient } from "effect/unstable/http" -import { SearchMcp } from "./mcp" +import { WebSearchMcp } from "./mcp" export const endpoint = "https://mcp.exa.ai/mcp" @@ -23,8 +23,8 @@ const Output = Schema.Struct({ }) export const Plugin = define({ - id: "opencode.search.exa", - effect: Effect.fn("SearchExa.Plugin")(function* (ctx) { + id: "opencode.websearch.exa", + effect: Effect.fn("WebSearchExa.Plugin")(function* (ctx) { const http = yield* HttpClient.HttpClient yield* ctx.integration.register({ id: "exa", @@ -33,17 +33,17 @@ export const Plugin = define({ { type: "key", label: "API key (optional)" }, { type: "env", names: ["EXA_API_KEY"] }, ], - search: { + websearch: { connection: "optional", execute: (input, context) => { const url = new URL(endpoint) if (context.credential?.type === "key") url.searchParams.set("exaApiKey", context.credential.key) - return SearchMcp.call( + return WebSearchMcp.call( http, url.toString(), "web_search_exa", { input: Input, output: Output }, - { query: input.query }, + { query: input.query, numResults: 8 }, ).pipe( Effect.map((result) => { const content = result?.content.find((item) => item.text) diff --git a/packages/core/src/plugin/websearch/index.ts b/packages/core/src/plugin/websearch/index.ts new file mode 100644 index 0000000000..8f8b5fec6d --- /dev/null +++ b/packages/core/src/plugin/websearch/index.ts @@ -0,0 +1,4 @@ +import { WebSearchExa } from "./exa" +import { WebSearchParallel } from "./parallel" + +export const WebSearchPlugins = [WebSearchExa.Plugin, WebSearchParallel.Plugin] as const diff --git a/packages/core/src/plugin/search/mcp.ts b/packages/core/src/plugin/websearch/mcp.ts similarity index 98% rename from packages/core/src/plugin/search/mcp.ts rename to packages/core/src/plugin/websearch/mcp.ts index 1a65ffccb3..085a930b3c 100644 --- a/packages/core/src/plugin/search/mcp.ts +++ b/packages/core/src/plugin/websearch/mcp.ts @@ -1,4 +1,4 @@ -export * as SearchMcp from "./mcp" +export * as WebSearchMcp from "./mcp" import { Duration, Effect, Schema } from "effect" import { HttpClient, HttpClientRequest } from "effect/unstable/http" diff --git a/packages/core/src/plugin/search/parallel.ts b/packages/core/src/plugin/websearch/parallel.ts similarity index 92% rename from packages/core/src/plugin/search/parallel.ts rename to packages/core/src/plugin/websearch/parallel.ts index d71382cc66..31936a7d7f 100644 --- a/packages/core/src/plugin/search/parallel.ts +++ b/packages/core/src/plugin/websearch/parallel.ts @@ -1,10 +1,10 @@ -export * as SearchParallel from "./parallel" +export * as WebSearchParallel from "./parallel" import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { Effect, Schema, Scope } from "effect" import { HttpClient } from "effect/unstable/http" import { InstallationVersion } from "../../installation/version" -import { SearchMcp } from "./mcp" +import { WebSearchMcp } from "./mcp" export const endpoint = "https://search.parallel.ai/mcp" @@ -50,8 +50,8 @@ const Output = Schema.Struct({ }) export const Plugin = define({ - id: "opencode.search.parallel", - effect: Effect.fn("SearchParallel.Plugin")(function* (ctx) { + id: "opencode.websearch.parallel", + effect: Effect.fn("WebSearchParallel.Plugin")(function* (ctx) { const http = yield* HttpClient.HttpClient yield* ctx.integration.register({ id: "parallel", @@ -60,10 +60,10 @@ export const Plugin = define({ { type: "key", label: "API key (optional)" }, { type: "env", names: ["PARALLEL_API_KEY"] }, ], - search: { + websearch: { connection: "optional", execute: (input, context) => - SearchMcp.call( + WebSearchMcp.call( http, endpoint, "web_search", diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/websearch.ts index e685b24cb5..ff5665b1b5 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/websearch.ts @@ -5,7 +5,7 @@ import { ToolFailure } from "@opencode-ai/llm" import { Effect, Schema } from "effect" import { Integration } from "../integration" import { PermissionV2 } from "../permission" -import { Search } from "../search" +import { WebSearch } from "../websearch" import { Tool } from "./tool" export const name = "websearch" @@ -29,7 +29,7 @@ export const Plugin = { id: "opencode.tool.websearch", effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) { const permission = yield* PermissionV2.Service - const search = yield* Search.Service + const websearch = yield* WebSearch.Service yield* ctx.tool .transform((draft) => @@ -51,7 +51,7 @@ export const Plugin = { agent: context.agent, source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, }) - const result = yield* search.query({ ...input, sessionID: context.sessionID }) + const result = yield* websearch.query({ ...input, sessionID: context.sessionID }) return { provider: result.providerID, text: result.text || NO_RESULTS, diff --git a/packages/core/src/search.ts b/packages/core/src/websearch.ts similarity index 81% rename from packages/core/src/search.ts rename to packages/core/src/websearch.ts index 8ac18db54f..8c5505d7de 100644 --- a/packages/core/src/search.ts +++ b/packages/core/src/websearch.ts @@ -1,12 +1,12 @@ -export * as Search from "./search" +export * as WebSearch from "./websearch" -import { Search } from "@opencode-ai/schema/search" +import { WebSearch } from "@opencode-ai/schema/websearch" import { Config as ConfigSchema } from "@opencode-ai/schema/config" import { Context, Effect, Layer, Schema, Semaphore, Stream } from "effect" import path from "node:path" import { Config } from "./config" import { ConfigGlobal } from "./config/global" -import { ConfigSearch } from "./config/search" +import { ConfigWebSearch } from "./config/websearch" import { makeLocationNode } from "./effect/app-node" import { EventV2 } from "./event" import { Form } from "./form" @@ -14,32 +14,32 @@ import { Global } from "./global" import { Integration } from "./integration" import { truthy } from "./flag/flag" -export const Input = Search.Input -export type Input = Search.Input +export const Input = WebSearch.Input +export type Input = WebSearch.Input -export const ProviderOutput = Search.ProviderOutput -export type ProviderOutput = Search.ProviderOutput +export const ProviderOutput = WebSearch.ProviderOutput +export type ProviderOutput = WebSearch.ProviderOutput -export const Result = Search.Result -export type Result = Search.Result +export const Result = WebSearch.Result +export type Result = WebSearch.Result export class ProviderRequiredError extends Schema.TaggedErrorClass()( - "Search.ProviderRequired", + "WebSearch.ProviderRequired", {}, ) {} -export class ProviderNotFoundError extends Schema.TaggedErrorClass()("Search.ProviderNotFound", { +export class ProviderNotFoundError extends Schema.TaggedErrorClass()("WebSearch.ProviderNotFound", { providerID: Integration.ID, }) {} export class ConnectionRequiredError extends Schema.TaggedErrorClass()( - "Search.ConnectionRequired", + "WebSearch.ConnectionRequired", { providerID: Integration.ID }, ) {} -export class CancelledError extends Schema.TaggedErrorClass()("Search.Cancelled", {}) {} +export class CancelledError extends Schema.TaggedErrorClass()("WebSearch.Cancelled", {}) {} -export class RequestError extends Schema.TaggedErrorClass()("Search.Request", { +export class RequestError extends Schema.TaggedErrorClass()("WebSearch.Request", { providerID: Integration.ID, cause: Schema.Defect(), }) {} @@ -61,7 +61,7 @@ export interface Interface { readonly query: (input: QueryInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Search") {} +export class Service extends Context.Service()("@opencode/v2/WebSearch") {} const layer = Layer.effect( Service, @@ -78,27 +78,27 @@ const layer = Layer.effect( let pendingProviderID: Integration.ID | undefined const requireProvider = ( - providers: Map, + providers: Map, providerID: Integration.ID, ) => { const provider = providers.get(providerID) return provider ? Effect.succeed(provider) : Effect.fail(new ProviderNotFoundError({ providerID })) } - const globalProviderID = Effect.fn("Search.globalProviderID")(function* () { + const globalProviderID = Effect.fn("WebSearch.globalProviderID")(function* () { const entries = (yield* config.entries()).filter( (entry) => entry.type === "document" && entry.path && path.dirname(entry.path) === globalConfigPath, ) - return Config.latest(entries, "search")?.provider + return Config.latest(entries, "websearch")?.provider }) - const selected = Effect.fn("Search.selected")(function* () { + const selected = Effect.fn("WebSearch.selected")(function* () { return pendingProviderID ?? (yield* globalProviderID()) }) - const saveProvider = Effect.fn("Search.saveProvider")(function* (providerID: Integration.ID) { + const saveProvider = Effect.fn("WebSearch.saveProvider")(function* (providerID: Integration.ID) { pendingProviderID = providerID - yield* configGlobal.update(["search"], new ConfigSearch.Info({ provider: providerID })).pipe( + yield* configGlobal.update(["websearch"], new ConfigWebSearch.Info({ provider: providerID })).pipe( Effect.tapError(() => Effect.sync(() => (pendingProviderID = undefined))), Effect.orDie, ) @@ -118,8 +118,8 @@ const layer = Layer.effect( Effect.forkScoped, ) - const ask = Effect.fn("Search.ask")(function* ( - providers: Map, + const ask = Effect.fn("WebSearch.ask")(function* ( + providers: Map, sessionID: string, ) { if (providers.size === 0) return yield* new ProviderRequiredError() @@ -128,7 +128,7 @@ const layer = Layer.effect( .ask({ sessionID, title: "Choose a web search provider", - metadata: { kind: "search.provider" }, + metadata: { kind: "websearch.provider" }, mode: "form", fields: [ { @@ -161,8 +161,8 @@ const layer = Layer.effect( return yield* requireProvider(providers, Integration.ID.make(answer)) }) - const connect = Effect.fn("Search.connect")(function* ( - provider: Integration.SearchImplementation, + const connect = Effect.fn("WebSearch.connect")(function* ( + provider: Integration.WebSearchImplementation, sessionID?: string, ) { const active = yield* integrations.connection.active(provider.integrationID) @@ -183,12 +183,12 @@ const layer = Layer.effect( return connected }) - const resolve = Effect.fn("Search.resolve")(function* (input: QueryInput) { + const resolve = Effect.fn("WebSearch.resolve")(function* (input: QueryInput) { const providers = new Map( - (yield* integrations.search.list()).map((provider) => [provider.integrationID, provider]), + (yield* integrations.websearch.list()).map((provider) => [provider.integrationID, provider]), ) if (input.providerID) return yield* requireProvider(providers, input.providerID) - const configuredProviderID = Config.latest(yield* config.entries(), "search")?.provider + const configuredProviderID = Config.latest(yield* config.entries(), "websearch")?.provider if (configuredProviderID) return yield* requireProvider(providers, configuredProviderID) if (process.env.OPENCODE_WEBSEARCH_PROVIDER) { return yield* requireProvider(providers, Integration.ID.make(process.env.OPENCODE_WEBSEARCH_PROVIDER)) @@ -219,12 +219,12 @@ const layer = Layer.effect( return Service.of({ selected, - select: Effect.fn("Search.select")(function* (providerID) { - const provider = yield* integrations.search.get(providerID) + select: Effect.fn("WebSearch.select")(function* (providerID) { + const provider = yield* integrations.websearch.get(providerID) if (!provider) return yield* new ProviderNotFoundError({ providerID }) yield* saveProvider(providerID) }), - query: Effect.fn("Search.query")(function* (input) { + query: Effect.fn("WebSearch.query")(function* (input) { const provider = yield* resolve(input) const connection = yield* connect(provider, input.sessionID) const credential = connection diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 496805d038..87c7cb5a53 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -75,11 +75,11 @@ describe("Config", () => { }) const config = yield* ConfigGlobal.Service - yield* config.update(["search"], { provider: "exa" }) + yield* config.update(["websearch"], { provider: "exa" }) const text = yield* Effect.promise(() => Bun.file(file).text()) expect(text).toContain("// user config") - expect(parse(text)).toEqual({ username: "tester", search: { provider: "exa" } }) + expect(parse(text)).toEqual({ username: "tester", websearch: { provider: "exa" } }) }).pipe( Effect.provide( AppNodeBuilder.build(LayerNode.group([ConfigGlobal.node]), [ diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index fbbbb97259..10ff4025b1 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -155,7 +155,7 @@ function resourceMcpLayer(url: string) { complete: unusedIntegration, cancel: unusedIntegration, }, - search: { + websearch: { list: unusedIntegration, get: unusedIntegration, }, diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 2a025c814c..5f706d7dc0 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -259,11 +259,11 @@ function registerIntegration(draft: Integration.Draft, definition: IntegrationDe }), ) } - if (!definition.search) return - draft.search.update({ + if (!definition.websearch) return + draft.websearch.update({ integrationID, - connection: definition.search.connection, - execute: definition.search.execute, + connection: definition.websearch.connection, + execute: definition.websearch.execute, }) } diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index ac2093ee28..07454a7b9d 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -95,19 +95,19 @@ describe("fromPromise", () => { }), ) - it.effect("adapts promise search capability execution", () => + it.effect("adapts promise web search capability execution", () => Effect.gen(function* () { const integrations = yield* Integration.Service const plugin = yield* PluginV2.Service const host = yield* PluginHost.make(plugin) const promisePlugin = Plugin.define({ - id: "promise-search", + id: "promise-websearch", setup: async (ctx) => { await ctx.integration.register({ - id: "promise-search", - name: "Promise Search", - methods: [{ type: "env", names: ["PROMISE_SEARCH_KEY"] }], - search: { + id: "promise-websearch", + name: "Promise Web Search", + methods: [{ type: "env", names: ["PROMISE_WEBSEARCH_KEY"] }], + websearch: { connection: "optional", execute: async (input) => ({ text: `promise: ${input.query}` }), }, @@ -116,12 +116,12 @@ describe("fromPromise", () => { }) yield* PluginPromise.fromPromise(promisePlugin).effect(host) - expect(yield* integrations.get(Integration.ID.make("promise-search"))).toMatchObject({ - name: "Promise Search", - methods: [{ type: "env", names: ["PROMISE_SEARCH_KEY"] }], + expect(yield* integrations.get(Integration.ID.make("promise-websearch"))).toMatchObject({ + name: "Promise Web Search", + methods: [{ type: "env", names: ["PROMISE_WEBSEARCH_KEY"] }], }) - const provider = yield* integrations.search.get(Integration.ID.make("promise-search")) - if (!provider) return yield* Effect.die("Expected promise search provider") + const provider = yield* integrations.websearch.get(Integration.ID.make("promise-websearch")) + if (!provider) return yield* Effect.die("Expected promise web search provider") expect(yield* provider.execute({ query: "effect" }, {})).toEqual({ text: "promise: effect" }) }), ) diff --git a/packages/core/test/plugin/search-fixture.ts b/packages/core/test/plugin/websearch-fixture.ts similarity index 86% rename from packages/core/test/plugin/search-fixture.ts rename to packages/core/test/plugin/websearch-fixture.ts index 7472d57962..24c5a5ca22 100644 --- a/packages/core/test/plugin/search-fixture.ts +++ b/packages/core/test/plugin/websearch-fixture.ts @@ -7,16 +7,16 @@ import { EventV2 } from "@opencode-ai/core/event" import { Integration } from "@opencode-ai/core/integration" import { testEffect } from "../lib/effect" -export interface SearchRequest { +export interface WebSearchRequest { readonly url: string readonly headers: Record readonly body: unknown } -export const requests: SearchRequest[] = [] +export const requests: WebSearchRequest[] = [] export const response = { body: "" } -export function resetSearchFixture(body: string) { +export function resetWebSearchFixture(body: string) { requests.length = 0 response.body = body } @@ -36,6 +36,6 @@ const http = Layer.succeed( ), ) -export const searchIntegrationTest = testEffect( +export const webSearchIntegrationTest = testEffect( Layer.merge(AppNodeBuilder.build(LayerNode.group([Integration.node, Credential.node, EventV2.node])), http), ) diff --git a/packages/core/test/plugin/search.test.ts b/packages/core/test/plugin/websearch.test.ts similarity index 72% rename from packages/core/test/plugin/search.test.ts rename to packages/core/test/plugin/websearch.test.ts index 8a2f3eb77a..a417759162 100644 --- a/packages/core/test/plugin/search.test.ts +++ b/packages/core/test/plugin/websearch.test.ts @@ -2,13 +2,13 @@ import { beforeEach, describe, expect } from "bun:test" import { Effect } from "effect" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" -import { SearchExa } from "@opencode-ai/core/plugin/search/exa" -import { SearchParallel } from "@opencode-ai/core/plugin/search/parallel" +import { WebSearchExa } from "@opencode-ai/core/plugin/websearch/exa" +import { WebSearchParallel } from "@opencode-ai/core/plugin/websearch/parallel" import { host, integrationHost } from "./host" -import { requests, resetSearchFixture, searchIntegrationTest } from "./search-fixture" +import { requests, resetWebSearchFixture, webSearchIntegrationTest } from "./websearch-fixture" beforeEach(() => { - resetSearchFixture( + resetWebSearchFixture( `event: message\ndata: ${JSON.stringify({ jsonrpc: "2.0", id: 1, @@ -17,46 +17,46 @@ beforeEach(() => { ) }) -const it = searchIntegrationTest +const it = webSearchIntegrationTest -describe("built-in search integrations", () => { - it.effect("registers and disposes an atomic search integration", () => +describe("built-in web search integrations", () => { + it.effect("registers and disposes an atomic web search integration", () => Effect.gen(function* () { const integrations = yield* Integration.Service const registration = yield* integrationHost(integrations).register({ - id: "test-search", - name: "Test Search", + id: "test-websearch", + name: "Test Web Search", methods: [{ type: "key", label: "API key" }], - search: { + websearch: { connection: "required", execute: (input) => Effect.succeed({ text: input.query }), }, }) - expect(yield* integrations.get(Integration.ID.make("test-search"))).toMatchObject({ - name: "Test Search", + expect(yield* integrations.get(Integration.ID.make("test-websearch"))).toMatchObject({ + name: "Test Web Search", methods: [{ type: "key", label: "API key" }], - search: { connection: "required" }, + websearch: { connection: "required" }, }) yield* registration.dispose - expect(yield* integrations.get(Integration.ID.make("test-search"))).toBeUndefined() + expect(yield* integrations.get(Integration.ID.make("test-websearch"))).toBeUndefined() }), ) it.effect("registers Exa with its MCP schema", () => Effect.gen(function* () { const integrations = yield* Integration.Service - yield* SearchExa.Plugin.effect(host({ integration: integrationHost(integrations) })) + yield* WebSearchExa.Plugin.effect(host({ integration: integrationHost(integrations) })) const info = yield* integrations.get(Integration.ID.make("exa")) expect(info).toMatchObject({ id: "exa", name: "Exa", methods: [{ type: "key" }, { type: "env", names: ["EXA_API_KEY"] }], - search: { connection: "optional" }, + websearch: { connection: "optional" }, }) - const provider = yield* integrations.search.get(Integration.ID.make("exa")) - if (!provider) return yield* Effect.die("Expected Exa search provider") + const provider = yield* integrations.websearch.get(Integration.ID.make("exa")) + if (!provider) return yield* Effect.die("Expected Exa web search provider") expect( yield* provider.execute( { query: "effect typescript" }, @@ -65,7 +65,7 @@ describe("built-in search integrations", () => { ).toEqual({ text: "search results", metadata: { searchTime: 123 } }) expect(requests).toEqual([ { - url: `${SearchExa.endpoint}?exaApiKey=exa+secret`, + url: `${WebSearchExa.endpoint}?exaApiKey=exa+secret`, headers: expect.any(Object), body: { jsonrpc: "2.0", @@ -73,7 +73,7 @@ describe("built-in search integrations", () => { method: "tools/call", params: { name: "web_search_exa", - arguments: { query: "effect typescript" }, + arguments: { query: "effect typescript", numResults: 8 }, }, }, }, @@ -83,7 +83,7 @@ describe("built-in search integrations", () => { it.effect("registers Parallel and keeps its credential in the authorization header", () => Effect.gen(function* () { - resetSearchFixture( + resetWebSearchFixture( JSON.stringify({ jsonrpc: "2.0", id: 1, @@ -107,9 +107,9 @@ describe("built-in search integrations", () => { }), ) const integrations = yield* Integration.Service - yield* SearchParallel.Plugin.effect(host({ integration: integrationHost(integrations) })) - const provider = yield* integrations.search.get(Integration.ID.make("parallel")) - if (!provider) return yield* Effect.die("Expected Parallel search provider") + yield* WebSearchParallel.Plugin.effect(host({ integration: integrationHost(integrations) })) + const provider = yield* integrations.websearch.get(Integration.ID.make("parallel")) + if (!provider) return yield* Effect.die("Expected Parallel web search provider") const output = yield* provider.execute( { query: "effect layers" }, @@ -136,7 +136,7 @@ describe("built-in search integrations", () => { }, }) expect(requests[0]).toMatchObject({ - url: SearchParallel.endpoint, + url: WebSearchParallel.endpoint, headers: { authorization: "Bearer parallel-secret" }, body: { jsonrpc: "2.0", diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 85f155981d..cb6dc445b2 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { PermissionV2 } from "@opencode-ai/core/permission" import { Integration } from "@opencode-ai/core/integration" -import { Search } from "@opencode-ai/core/search" +import { WebSearch } from "@opencode-ai/core/websearch" import { SessionV2 } from "@opencode-ai/core/session" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { WebSearchTool } from "@opencode-ai/core/tool/websearch" @@ -16,18 +16,18 @@ import { executeTool, registerToolPlugin, settleTool, toolDefinitions, toolIdent const webSearchToolNode = makeLocationNode({ name: "test/websearch-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(WebSearchTool.Plugin)), - deps: [ToolRegistry.toolsNode, PermissionV2.node, Search.node], + deps: [ToolRegistry.toolsNode, PermissionV2.node, WebSearch.node], }) const sessionID = SessionV2.ID.make("ses_websearch_test") const assertions: PermissionV2.AssertInput[] = [] -const queries: Search.QueryInput[] = [] -let result = new Search.Result({ providerID: Integration.ID.make("exa"), text: "search results" }) +const queries: WebSearch.QueryInput[] = [] +let result = new WebSearch.Result({ providerID: Integration.ID.make("exa"), text: "search results" }) beforeEach(() => { assertions.length = 0 queries.length = 0 - result = new Search.Result({ providerID: Integration.ID.make("exa"), text: "search results" }) + result = new WebSearch.Result({ providerID: Integration.ID.make("exa"), text: "search results" }) }) const permission = Layer.succeed( @@ -41,9 +41,9 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) -const search = Layer.succeed( - Search.Service, - Search.Service.of({ +const websearch = Layer.succeed( + WebSearch.Service, + WebSearch.Service.of({ selected: () => Effect.succeed(undefined), select: () => Effect.die("unused"), query: (input) => @@ -54,15 +54,15 @@ const search = Layer.succeed( }), ) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, Search.node, webSearchToolNode]), [ + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearch.node, webSearchToolNode]), [ [PermissionV2.node, permission], - [Search.node, search], + [WebSearch.node, websearch], [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ]), ) describe("WebSearchTool registration", () => { - it.effect("asserts permission before delegating to Search", () => + it.effect("asserts permission before delegating to WebSearch", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service @@ -99,7 +99,7 @@ describe("WebSearchTool registration", () => { it.effect("keeps provider metadata in structured output", () => Effect.gen(function* () { - result = new Search.Result({ + result = new WebSearch.Result({ providerID: Integration.ID.make("parallel"), text: "parallel results", metadata: { requestID: "req_1" }, @@ -124,7 +124,7 @@ describe("WebSearchTool registration", () => { it.effect("uses the concise no-results fallback", () => Effect.gen(function* () { - result = new Search.Result({ providerID: Integration.ID.make("exa"), text: "" }) + result = new WebSearch.Result({ providerID: Integration.ID.make("exa"), text: "" }) const registry = yield* ToolRegistry.Service expect( diff --git a/packages/core/test/search.test.ts b/packages/core/test/websearch.test.ts similarity index 63% rename from packages/core/test/search.test.ts rename to packages/core/test/websearch.test.ts index 60347acc2f..39d93c2e0a 100644 --- a/packages/core/test/search.test.ts +++ b/packages/core/test/websearch.test.ts @@ -5,13 +5,13 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Config } from "@opencode-ai/core/config" import { ConfigGlobal } from "@opencode-ai/core/config/global" -import { ConfigSearch } from "@opencode-ai/core/config/search" +import { ConfigWebSearch } from "@opencode-ai/core/config/websearch" import { Credential } from "@opencode-ai/core/credential" import { EventV2 } from "@opencode-ai/core/event" import { Form } from "@opencode-ai/core/form" import { Global } from "@opencode-ai/core/global" import { Integration } from "@opencode-ai/core/integration" -import { Search } from "@opencode-ai/core/search" +import { WebSearch } from "@opencode-ai/core/websearch" import { testEffect } from "./lib/effect" let entries: Config.Entry[] = [] @@ -23,7 +23,7 @@ const configGlobal = Layer.succeed( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Search.node, Integration.node, Credential.node, EventV2.node, Form.node, ConfigGlobal.node]), + LayerNode.group([WebSearch.node, Integration.node, Credential.node, EventV2.node, Form.node, ConfigGlobal.node]), [ [Config.node, config], [ConfigGlobal.node, configGlobal], @@ -35,10 +35,10 @@ const register = (id: string, connection: "optional" | "required" = "optional") Effect.gen(function* () { const integrations = yield* Integration.Service const integrationID = Integration.ID.make(id) - const calls: { input: Search.Input; credential?: Credential.Value; sessionID?: string }[] = [] + const calls: { input: WebSearch.Input; credential?: Credential.Value; sessionID?: string }[] = [] yield* integrations.transform((draft) => { draft.update(integrationID, (integration) => (integration.name = id.toUpperCase())) - draft.search.update({ + draft.websearch.update({ integrationID, connection, execute: (input, context) => @@ -56,20 +56,20 @@ beforeEach(() => { writes.length = 0 }) -describe("Search", () => { +describe("WebSearch", () => { it.effect("executes an explicit provider without changing the default", () => Effect.gen(function* () { const provider = yield* register("exa") - const search = yield* Search.Service + const websearch = yield* WebSearch.Service - expect(yield* search.query({ query: "effect", providerID: provider.integrationID })).toEqual( - new Search.Result({ + expect(yield* websearch.query({ query: "effect", providerID: provider.integrationID })).toEqual( + new WebSearch.Result({ providerID: provider.integrationID, text: "exa: effect", metadata: { id: "exa" }, }), ) - expect(yield* search.selected()).toBeUndefined() + expect(yield* websearch.selected()).toBeUndefined() expect(provider.calls).toEqual([ { input: { query: "effect", providerID: provider.integrationID }, @@ -84,29 +84,31 @@ describe("Search", () => { Effect.gen(function* () { yield* register("exa") const parallel = yield* register("parallel") - const search = yield* Search.Service - yield* search.select(parallel.integrationID) + const websearch = yield* WebSearch.Service + yield* websearch.select(parallel.integrationID) - expect((yield* search.query({ query: "layers" })).providerID).toBe(parallel.integrationID) - expect(yield* search.selected()).toBe(parallel.integrationID) - expect(writes).toEqual([{ path: ["search"], value: new ConfigSearch.Info({ provider: parallel.integrationID }) }]) + expect((yield* websearch.query({ query: "layers" })).providerID).toBe(parallel.integrationID) + expect(yield* websearch.selected()).toBe(parallel.integrationID) + expect(writes).toEqual([ + { path: ["websearch"], value: new ConfigWebSearch.Info({ provider: parallel.integrationID }) }, + ]) }), ) it.effect("reads the selected provider from global config", () => Effect.gen(function* () { const provider = yield* register("exa") - const search = yield* Search.Service + const websearch = yield* WebSearch.Service entries = [ new Config.Document({ type: "document", path: path.join(Global.Path.config, "opencode.json"), - info: new Config.Info({ search: new ConfigSearch.Info({ provider: provider.integrationID }) }), + info: new Config.Info({ websearch: new ConfigWebSearch.Info({ provider: provider.integrationID }) }), }), ] - expect(yield* search.selected()).toBe(provider.integrationID) - expect((yield* search.query({ query: "configured" })).providerID).toBe(provider.integrationID) + expect(yield* websearch.selected()).toBe(provider.integrationID) + expect((yield* websearch.query({ query: "configured" })).providerID).toBe(provider.integrationID) }), ) @@ -114,29 +116,29 @@ describe("Search", () => { Effect.gen(function* () { const exa = yield* register("exa") const parallel = yield* register("parallel") - const search = yield* Search.Service - yield* search.select(exa.integrationID) + const websearch = yield* WebSearch.Service + yield* websearch.select(exa.integrationID) entries = [ new Config.Document({ type: "document", - info: new Config.Info({ search: new ConfigSearch.Info({ provider: parallel.integrationID }) }), + info: new Config.Info({ websearch: new ConfigWebSearch.Info({ provider: parallel.integrationID }) }), }), ] - expect((yield* search.query({ query: "configured" })).providerID).toBe(parallel.integrationID) + expect((yield* websearch.query({ query: "configured" })).providerID).toBe(parallel.integrationID) }), ) it.effect("serializes concurrent first-use onboarding and persists the answer", () => Effect.gen(function* () { const provider = yield* register("exa") - const search = yield* Search.Service + const websearch = yield* WebSearch.Service const forms = yield* Form.Service - const first = yield* search.query({ query: "one", sessionID: "ses_search" }).pipe(Effect.forkChild) - const second = yield* search.query({ query: "two", sessionID: "ses_search" }).pipe(Effect.forkChild) + const first = yield* websearch.query({ query: "one", sessionID: "ses_websearch" }).pipe(Effect.forkChild) + const second = yield* websearch.query({ query: "two", sessionID: "ses_websearch" }).pipe(Effect.forkChild) yield* Effect.yieldNow - const pending = yield* forms.list({ sessionID: "ses_search" }) + const pending = yield* forms.list({ sessionID: "ses_websearch" }) expect(pending).toHaveLength(1) const form = pending[0] if (!form) return yield* Effect.die("Expected an onboarding form") @@ -144,18 +146,18 @@ describe("Search", () => { expect((yield* Fiber.join(first)).providerID).toBe(provider.integrationID) expect((yield* Fiber.join(second)).providerID).toBe(provider.integrationID) - expect(yield* search.selected()).toBe(provider.integrationID) + expect(yield* websearch.selected()).toBe(provider.integrationID) }), ) it.effect("requires a connection before invoking a required provider", () => Effect.gen(function* () { const provider = yield* register("private", "required") - const search = yield* Search.Service + const websearch = yield* WebSearch.Service expect( - yield* search.query({ query: "secret", providerID: provider.integrationID }).pipe(Effect.flip), - ).toBeInstanceOf(Search.ConnectionRequiredError) + yield* websearch.query({ query: "secret", providerID: provider.integrationID }).pipe(Effect.flip), + ).toBeInstanceOf(WebSearch.ConnectionRequiredError) expect(provider.calls).toEqual([]) }), ) @@ -165,9 +167,9 @@ describe("Search", () => { const integrations = yield* Integration.Service const scope = yield* Scope.fork(yield* Scope.Scope) const provider = yield* register("temporary").pipe(Scope.provide(scope)) - expect(yield* integrations.search.get(provider.integrationID)).toBeDefined() + expect(yield* integrations.websearch.get(provider.integrationID)).toBeDefined() yield* Scope.close(scope, Exit.void) - expect(yield* integrations.search.get(provider.integrationID)).toBeUndefined() + expect(yield* integrations.websearch.get(provider.integrationID)).toBeUndefined() }), ) }) diff --git a/packages/plugin/src/v2/effect/integration.ts b/packages/plugin/src/v2/effect/integration.ts index f1658a7fbc..b0bb1c64e5 100644 --- a/packages/plugin/src/v2/effect/integration.ts +++ b/packages/plugin/src/v2/effect/integration.ts @@ -10,7 +10,7 @@ import type { IntegrationRef, } from "@opencode-ai/sdk/v2/types" import type { IntegrationApi } from "@opencode-ai/client/effect/api" -import type { Search } from "@opencode-ai/schema/search" +import type { WebSearch } from "@opencode-ai/schema/websearch" import type { Effect, Scope } from "effect" import type { Registration, Transform } from "./registration.js" @@ -53,19 +53,19 @@ export type IntegrationOAuthMethodDefinition = IntegrationOAuthMethod & { export type IntegrationMethodDefinition = IntegrationOAuthMethodDefinition | IntegrationKeyMethod | IntegrationEnvMethod -export interface IntegrationSearchDefinition { +export interface IntegrationWebSearchDefinition { readonly connection: "optional" | "required" readonly execute: ( - input: Search.Input, + input: WebSearch.Input, context: { readonly credential?: CredentialValue; readonly sessionID?: string }, - ) => Effect.Effect + ) => Effect.Effect } export interface IntegrationDefinition { readonly id: string readonly name: string readonly methods?: readonly IntegrationMethodDefinition[] - readonly search?: IntegrationSearchDefinition + readonly websearch?: IntegrationWebSearchDefinition } export interface IntegrationDraft { diff --git a/packages/plugin/src/v2/promise/integration.ts b/packages/plugin/src/v2/promise/integration.ts index fc126ec9b0..e5eb38db6e 100644 --- a/packages/plugin/src/v2/promise/integration.ts +++ b/packages/plugin/src/v2/promise/integration.ts @@ -8,7 +8,7 @@ import type { IntegrationKeyMethod, IntegrationOAuthMethod, } from "@opencode-ai/sdk/v2/types" -import type { Search } from "@opencode-ai/schema/search" +import type { WebSearch } from "@opencode-ai/schema/websearch" import type { Registration, Transform } from "./registration.js" export type { IntegrationDraft, IntegrationMethodRegistration } @@ -35,19 +35,19 @@ export type IntegrationOAuthMethodDefinition = IntegrationOAuthMethod & { export type IntegrationMethodDefinition = IntegrationOAuthMethodDefinition | IntegrationKeyMethod | IntegrationEnvMethod -export interface IntegrationSearchDefinition { +export interface IntegrationWebSearchDefinition { readonly connection: "optional" | "required" readonly execute: ( - input: Search.Input, + input: WebSearch.Input, context: { readonly credential?: CredentialValue; readonly sessionID?: string; readonly signal: AbortSignal }, - ) => Promise + ) => Promise } export interface IntegrationDefinition { readonly id: string readonly name: string readonly methods?: readonly IntegrationMethodDefinition[] - readonly search?: IntegrationSearchDefinition + readonly websearch?: IntegrationWebSearchDefinition } export interface IntegrationDomain extends IntegrationApi { diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index d2937fade0..d9d3f7f41e 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -24,7 +24,7 @@ import { ReferenceGroup } from "./groups/reference.js" import { Authorization } from "./middleware/authorization.js" import { LocationGroup } from "./groups/location.js" import { IntegrationGroup } from "./groups/integration.js" -import { SearchGroup } from "./groups/search.js" +import { WebSearchGroup } from "./groups/websearch.js" import { McpGroup } from "./groups/mcp.js" import { CredentialGroup } from "./groups/credential.js" import { ProjectGroup } from "./groups/project.js" @@ -39,7 +39,7 @@ type LocationGroups = | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware - | HttpApiGroup.AddMiddleware + | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware @@ -170,7 +170,7 @@ const makeApiFromGroup = < .add(ProjectCopyGroup.middleware(locationMiddleware)) .add(VcsGroup.middleware(locationMiddleware)) .add(DebugGroup) - .add(SearchGroup.middleware(locationMiddleware)) + .add(WebSearchGroup.middleware(locationMiddleware)) .annotateMerge( OpenApi.annotations({ title: "opencode HttpApi", diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index 74ef8e0ed5..461fe86844 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -44,7 +44,7 @@ export const groupNames = { "server.generate": "generate", "server.provider": "provider", "server.integration": "integration", - "server.search": "search", + "server.websearch": "websearch", "server.credential": "credential", "server.form": "form", "server.permission": "permission", diff --git a/packages/protocol/src/groups/search.ts b/packages/protocol/src/groups/websearch.ts similarity index 68% rename from packages/protocol/src/groups/search.ts rename to packages/protocol/src/groups/websearch.ts index 40085d1252..3ef77f19eb 100644 --- a/packages/protocol/src/groups/search.ts +++ b/packages/protocol/src/groups/websearch.ts @@ -1,28 +1,28 @@ import { Integration } from "@opencode-ai/schema/integration" import { Location } from "@opencode-ai/schema/location" -import { Search } from "@opencode-ai/schema/search" +import { WebSearch } from "@opencode-ai/schema/websearch" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { InvalidRequestError, ServiceUnavailableError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" -export const SearchGroup = HttpApiGroup.make("server.search") +export const WebSearchGroup = HttpApiGroup.make("server.websearch") .add( - HttpApiEndpoint.get("search.provider.get", "/api/search/provider", { + HttpApiEndpoint.get("websearch.provider.get", "/api/websearch/provider", { query: LocationQuery, success: Location.response(Schema.UndefinedOr(Integration.ID)), }) .annotateMerge(locationQueryOpenApi) .annotateMerge( OpenApi.annotations({ - identifier: "v2.search.provider.get", - summary: "Get default search provider", + identifier: "v2.websearch.provider.get", + summary: "Get default web search provider", description: "Return the globally selected web search provider.", }), ), ) .add( - HttpApiEndpoint.post("search.provider.select", "/api/search/provider", { + HttpApiEndpoint.post("websearch.provider.select", "/api/websearch/provider", { query: LocationQuery, payload: Schema.Struct({ providerID: Integration.ID }), success: HttpApiSchema.NoContent, @@ -31,23 +31,23 @@ export const SearchGroup = HttpApiGroup.make("server.search") .annotateMerge(locationQueryOpenApi) .annotateMerge( OpenApi.annotations({ - identifier: "v2.search.provider.select", - summary: "Select default search provider", + identifier: "v2.websearch.provider.select", + summary: "Select default web search provider", description: "Persist the global web search provider in the user configuration.", }), ), ) .add( - HttpApiEndpoint.post("search.query", "/api/search", { + HttpApiEndpoint.post("websearch.query", "/api/websearch", { query: LocationQuery, - payload: Schema.Struct(Search.Input.fields), - success: Location.response(Search.Result), + payload: Schema.Struct(WebSearch.Input.fields), + success: Location.response(WebSearch.Result), error: [InvalidRequestError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( OpenApi.annotations({ - identifier: "v2.search.query", + identifier: "v2.websearch.query", summary: "Search the web", description: "Run one web search through the selected integration. Specify a provider to override the configured default.", @@ -56,7 +56,7 @@ export const SearchGroup = HttpApiGroup.make("server.search") ) .annotateMerge( OpenApi.annotations({ - title: "search", + title: "websearch", description: "Location-scoped web search routes.", }), ) diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 8f1bc0183c..4130ccaac1 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -18,7 +18,7 @@ export { Project } from "./project.js" export { ProjectCopy } from "./project-copy.js" export { Provider } from "./provider.js" export { Reference } from "./reference.js" -export { Search } from "./search.js" +export { WebSearch } from "./websearch.js" export { Session } from "./session.js" export { Vcs } from "./vcs.js" export { SessionInput } from "./session-input.js" diff --git a/packages/schema/src/integration.ts b/packages/schema/src/integration.ts index ce5fa6661e..6013227a83 100644 --- a/packages/schema/src/integration.ts +++ b/packages/schema/src/integration.ts @@ -76,10 +76,10 @@ export type Method = typeof Method.Type export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" }) export type Inputs = typeof Inputs.Type -export interface Search extends Schema.Schema.Type {} -export const Search = Schema.Struct({ +export interface WebSearch extends Schema.Schema.Type {} +export const WebSearch = Schema.Struct({ connection: Schema.Literals(["optional", "required"]), -}).annotate({ identifier: "Integration.Search" }) +}).annotate({ identifier: "Integration.WebSearch" }) const Updated = ephemeral({ type: "integration.updated", @@ -101,7 +101,7 @@ export class Info extends Schema.Class("Integration.Info")({ id: ID, name: Schema.String, methods: Schema.Array(Method), - search: optional(Search), + websearch: optional(WebSearch), connections: Schema.Array(Connection.Info), }) {} diff --git a/packages/schema/src/search.ts b/packages/schema/src/websearch.ts similarity index 71% rename from packages/schema/src/search.ts rename to packages/schema/src/websearch.ts index fd178f35ba..82eef0b7b7 100644 --- a/packages/schema/src/search.ts +++ b/packages/schema/src/websearch.ts @@ -1,4 +1,4 @@ -export * as Search from "./search.js" +export * as WebSearch from "./websearch.js" import { Schema } from "effect" import { IntegrationID } from "./integration-id.js" @@ -8,15 +8,15 @@ export interface Input extends Schema.Schema.Type {} export const Input = Schema.Struct({ query: Schema.String, providerID: IntegrationID.pipe(optional), -}).annotate({ identifier: "Search.Input" }) +}).annotate({ identifier: "WebSearch.Input" }) export interface ProviderOutput extends Schema.Schema.Type {} export const ProviderOutput = Schema.Struct({ text: Schema.String, metadata: Schema.Json.pipe(optional), -}).annotate({ identifier: "Search.ProviderOutput" }) +}).annotate({ identifier: "WebSearch.ProviderOutput" }) -export class Result extends Schema.Class("Search.Result")({ +export class Result extends Schema.Class("WebSearch.Result")({ providerID: IntegrationID, ...ProviderOutput.fields, }) {} diff --git a/packages/sdk-next/src/index.ts b/packages/sdk-next/src/index.ts index 4af002d5d8..a22445522b 100644 --- a/packages/sdk-next/src/index.ts +++ b/packages/sdk-next/src/index.ts @@ -20,7 +20,7 @@ export { Provider } from "@opencode-ai/schema/provider" export { Pty } from "@opencode-ai/schema/pty" export { Question } from "@opencode-ai/schema/question" export { Reference } from "@opencode-ai/schema/reference" -export { Search } from "@opencode-ai/schema/search" +export { WebSearch } from "@opencode-ai/schema/websearch" export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema" export { Session } from "@opencode-ai/schema/session" export { SessionInput } from "@opencode-ai/schema/session-input" diff --git a/packages/sdk-next/test/contract-identity.test.ts b/packages/sdk-next/test/contract-identity.test.ts index 169042ef75..46182ff667 100644 --- a/packages/sdk-next/test/contract-identity.test.ts +++ b/packages/sdk-next/test/contract-identity.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test" import { Agent } from "@opencode-ai/schema/agent" import { Model } from "@opencode-ai/schema/model" -import { Search } from "@opencode-ai/schema/search" +import { WebSearch } from "@opencode-ai/schema/websearch" import { Session } from "@opencode-ai/schema/session" const SDK = await import("../src/index") @@ -9,7 +9,7 @@ const SDK = await import("../src/index") test("re-exports canonical contracts directly from Schema", () => { expect(SDK.Agent).toBe(Agent) expect(SDK.Model).toBe(Model) - expect(SDK.Search).toBe(Search) + expect(SDK.WebSearch).toBe(WebSearch) expect(SDK.Session).toBe(Session) expect(Object.keys(SDK).sort()).toEqual([ "AbsolutePath", @@ -33,11 +33,11 @@ test("re-exports canonical contracts directly from Schema", () => { "Question", "Reference", "RelativePath", - "Search", "Session", "SessionInput", "SessionMessage", "Skill", "Tool", + "WebSearch", ]) }) diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts index 9622324726..468bdd1be1 100644 --- a/packages/sdk-next/test/embedded.test.ts +++ b/packages/sdk-next/test/embedded.test.ts @@ -302,25 +302,25 @@ it.live( 10_000, ) -it.live("embedded client exposes integration-backed search", () => - withEmbedded("opencode-embedded-search-", (fixture) => +it.live("embedded client exposes integration-backed web search", () => + withEmbedded("opencode-embedded-websearch-", (fixture) => Effect.gen(function* () { const opencode = yield* fixture.sdk.OpenCode.create() - const providerID = fixture.sdk.Integration.ID.make("embedded-search") + const providerID = fixture.sdk.Integration.ID.make("embedded-websearch") yield* opencode.plugin({ - id: `embedded-search-${crypto.randomUUID()}`, + id: `embedded-websearch-${crypto.randomUUID()}`, effect: (ctx) => ctx.integration.register({ id: providerID, - name: "Embedded search", - search: { + name: "Embedded web search", + websearch: { connection: "optional", execute: (input) => Effect.succeed({ text: `Found ${input.query}`, metadata: { source: "embedded" } }), }, }), }) - const result = yield* opencode.search.query({ + const result = yield* opencode.websearch.query({ query: "opencode", providerID, location: location(fixture), diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index d14867f426..aad9de0ed6 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -362,12 +362,6 @@ import type { V2QuestionRequestListResponses, V2ReferenceListErrors, V2ReferenceListResponses, - V2SearchProviderGetErrors, - V2SearchProviderGetResponses, - V2SearchProviderSelectErrors, - V2SearchProviderSelectResponses, - V2SearchQueryErrors, - V2SearchQueryResponses, V2SessionActiveErrors, V2SessionActiveResponses, V2SessionBackgroundErrors, @@ -468,6 +462,12 @@ import type { V2VcsDiffResponses, V2VcsStatusErrors, V2VcsStatusResponses, + V2WebsearchProviderGetErrors, + V2WebsearchProviderGetResponses, + V2WebsearchProviderSelectErrors, + V2WebsearchProviderSelectResponses, + V2WebsearchQueryErrors, + V2WebsearchQueryResponses, VcsApplyErrors, VcsApplyResponses, VcsDiffErrors, @@ -8261,7 +8261,7 @@ export class Debug extends HeyApiClient { export class Provider3 extends HeyApiClient { /** - * Get default search provider + * Get default web search provider * * Return the globally selected web search provider. */ @@ -8275,15 +8275,19 @@ export class Provider3 extends HeyApiClient { options?: Options, ) { const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/search/provider", + return (options?.client ?? this.client).get< + V2WebsearchProviderGetResponses, + V2WebsearchProviderGetErrors, + ThrowOnError + >({ + url: "/api/websearch/provider", ...options, ...params, }) } /** - * Select default search provider + * Select default web search provider * * Persist the global web search provider in the user configuration. */ @@ -8309,11 +8313,11 @@ export class Provider3 extends HeyApiClient { ], ) return (options?.client ?? this.client).post< - V2SearchProviderSelectResponses, - V2SearchProviderSelectErrors, + V2WebsearchProviderSelectResponses, + V2WebsearchProviderSelectErrors, ThrowOnError >({ - url: "/api/search/provider", + url: "/api/websearch/provider", ...options, ...params, headers: { @@ -8325,7 +8329,7 @@ export class Provider3 extends HeyApiClient { } } -export class Search extends HeyApiClient { +export class Websearch extends HeyApiClient { /** * Search the web * @@ -8354,8 +8358,8 @@ export class Search extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ - url: "/api/search", + return (options?.client ?? this.client).post({ + url: "/api/websearch", ...options, ...params, headers: { @@ -8503,9 +8507,9 @@ export class V2 extends HeyApiClient { return (this._debug ??= new Debug({ client: this.client })) } - private _search?: Search - get search(): Search { - return (this._search ??= new Search({ client: this.client })) + private _websearch?: Websearch + get websearch(): Websearch { + return (this._websearch ??= new Websearch({ client: this.client })) } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 558685e664..4de893543b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -5721,7 +5721,7 @@ export type IntegrationEnvMethod = { names: Array } -export type IntegrationSearch = { +export type IntegrationWebSearch = { connection: "optional" | "required" } @@ -5742,7 +5742,7 @@ export type IntegrationInfo = { id: string name: string methods: Array - search?: IntegrationSearch + websearch?: IntegrationWebSearch connections: Array } @@ -7035,7 +7035,7 @@ export type ProjectCopyCopy = { export type VcsMode = "working" | "branch" -export type SearchResult = { +export type WebSearchResult = { providerID: string text: string metadata?: unknown @@ -18955,7 +18955,7 @@ export type V2DebugLocationListResponses = { export type V2DebugLocationListResponse = V2DebugLocationListResponses[keyof V2DebugLocationListResponses] -export type V2SearchProviderGetData = { +export type V2WebsearchProviderGetData = { body?: never path?: never query?: { @@ -18964,10 +18964,10 @@ export type V2SearchProviderGetData = { workspace?: string | null } | null } - url: "/api/search/provider" + url: "/api/websearch/provider" } -export type V2SearchProviderGetErrors = { +export type V2WebsearchProviderGetErrors = { /** * InvalidRequestError */ @@ -18978,9 +18978,9 @@ export type V2SearchProviderGetErrors = { 401: UnauthorizedError } -export type V2SearchProviderGetError = V2SearchProviderGetErrors[keyof V2SearchProviderGetErrors] +export type V2WebsearchProviderGetError = V2WebsearchProviderGetErrors[keyof V2WebsearchProviderGetErrors] -export type V2SearchProviderGetResponses = { +export type V2WebsearchProviderGetResponses = { /** * Success */ @@ -18990,9 +18990,9 @@ export type V2SearchProviderGetResponses = { } } -export type V2SearchProviderGetResponse = V2SearchProviderGetResponses[keyof V2SearchProviderGetResponses] +export type V2WebsearchProviderGetResponse = V2WebsearchProviderGetResponses[keyof V2WebsearchProviderGetResponses] -export type V2SearchProviderSelectData = { +export type V2WebsearchProviderSelectData = { body: { providerID: string } @@ -19003,10 +19003,10 @@ export type V2SearchProviderSelectData = { workspace?: string | null } | null } - url: "/api/search/provider" + url: "/api/websearch/provider" } -export type V2SearchProviderSelectErrors = { +export type V2WebsearchProviderSelectErrors = { /** * InvalidRequestError */ @@ -19021,18 +19021,19 @@ export type V2SearchProviderSelectErrors = { 503: ServiceUnavailableErrorV2 } -export type V2SearchProviderSelectError = V2SearchProviderSelectErrors[keyof V2SearchProviderSelectErrors] +export type V2WebsearchProviderSelectError = V2WebsearchProviderSelectErrors[keyof V2WebsearchProviderSelectErrors] -export type V2SearchProviderSelectResponses = { +export type V2WebsearchProviderSelectResponses = { /** * */ 204: void } -export type V2SearchProviderSelectResponse = V2SearchProviderSelectResponses[keyof V2SearchProviderSelectResponses] +export type V2WebsearchProviderSelectResponse = + V2WebsearchProviderSelectResponses[keyof V2WebsearchProviderSelectResponses] -export type V2SearchQueryData = { +export type V2WebsearchQueryData = { body: { query: string providerID?: string @@ -19044,10 +19045,10 @@ export type V2SearchQueryData = { workspace?: string | null } | null } - url: "/api/search" + url: "/api/websearch" } -export type V2SearchQueryErrors = { +export type V2WebsearchQueryErrors = { /** * InvalidRequestError */ @@ -19062,19 +19063,19 @@ export type V2SearchQueryErrors = { 503: ServiceUnavailableErrorV2 } -export type V2SearchQueryError = V2SearchQueryErrors[keyof V2SearchQueryErrors] +export type V2WebsearchQueryError = V2WebsearchQueryErrors[keyof V2WebsearchQueryErrors] -export type V2SearchQueryResponses = { +export type V2WebsearchQueryResponses = { /** * Success */ 200: { location: LocationInfoV2 - data: SearchResult + data: WebSearchResult } } -export type V2SearchQueryResponse = V2SearchQueryResponses[keyof V2SearchQueryResponses] +export type V2WebsearchQueryResponse = V2WebsearchQueryResponses[keyof V2WebsearchQueryResponses] export type PtyConnectData = { body?: never diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index f6612faf0a..2a99842006 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -20,7 +20,7 @@ import { QuestionHandler } from "./handlers/question" import { ReferenceHandler } from "./handlers/reference" import { LocationHandler } from "./handlers/location" import { IntegrationHandler } from "./handlers/integration" -import { SearchHandler } from "./handlers/search" +import { WebSearchHandler } from "./handlers/websearch" import { McpHandler } from "./handlers/mcp" import { CredentialHandler } from "./handlers/credential" import { ProjectHandler } from "./handlers/project" @@ -39,7 +39,7 @@ export const handlers = Layer.mergeAll( GenerateHandler, ProviderHandler, IntegrationHandler, - SearchHandler, + WebSearchHandler, McpHandler, CredentialHandler, ProjectHandler, diff --git a/packages/server/src/handlers/search.ts b/packages/server/src/handlers/search.ts deleted file mode 100644 index 1e6d85e1f5..0000000000 --- a/packages/server/src/handlers/search.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { Search } from "@opencode-ai/core/search" -import { InvalidRequestError, ServiceUnavailableError } from "@opencode-ai/protocol/errors" -import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" -import { Api } from "../api" -import { response } from "../location" - -export const SearchHandler = HttpApiBuilder.group(Api, "server.search", (handlers) => - Effect.gen(function* () { - const awaitPlugins = Effect.fn("server.search.awaitPlugins")(function* () { - const plugins = yield* PluginSupervisor.Service - yield* plugins.ready.pipe( - Effect.timeoutOrElse({ - duration: "5 seconds", - orElse: () => - Effect.fail( - new ServiceUnavailableError({ - message: "Search integration initialization timed out", - service: "search", - }), - ), - }), - ) - }) - return handlers - .handle( - "search.provider.get", - Effect.fn("server.search.provider.get")(function* () { - const search = yield* Search.Service - return yield* response(search.selected()) - }), - ) - .handle( - "search.provider.select", - Effect.fn("server.search.provider.select")(function* (request) { - yield* awaitPlugins() - const search = yield* Search.Service - yield* search.select(request.payload.providerID).pipe( - Effect.mapError( - (error) => - new InvalidRequestError({ - message: `Search provider not found: ${error.providerID}`, - kind: "search_provider_not_found", - field: "providerID", - }), - ), - ) - return HttpApiSchema.NoContent.make() - }), - ) - .handle( - "search.query", - Effect.fn("server.search.query")(function* (request) { - yield* awaitPlugins() - const search = yield* Search.Service - return yield* response( - search.query(request.payload).pipe( - Effect.catchTags({ - "Search.ProviderRequired": () => - new InvalidRequestError({ - message: "Search provider is required", - kind: "search_provider_required", - field: "providerID", - }), - "Search.ProviderNotFound": (error) => - new InvalidRequestError({ - message: `Search provider not found: ${error.providerID}`, - kind: "search_provider_not_found", - field: "providerID", - }), - "Search.ConnectionRequired": (error) => - new InvalidRequestError({ - message: `Search provider requires a connection: ${error.providerID}`, - kind: "search_connection_required", - field: "providerID", - }), - "Search.Cancelled": () => - new InvalidRequestError({ message: "Search cancelled", kind: "search_cancelled" }), - "Search.Request": (error) => - new ServiceUnavailableError({ - message: `Search request failed: ${error.providerID}`, - service: error.providerID, - }), - }), - ), - ) - }), - ) - }), -) diff --git a/packages/server/src/handlers/websearch.ts b/packages/server/src/handlers/websearch.ts new file mode 100644 index 0000000000..98c9f6278f --- /dev/null +++ b/packages/server/src/handlers/websearch.ts @@ -0,0 +1,91 @@ +import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" +import { WebSearch } from "@opencode-ai/core/websearch" +import { InvalidRequestError, ServiceUnavailableError } from "@opencode-ai/protocol/errors" +import { Effect } from "effect" +import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { Api } from "../api" +import { response } from "../location" + +export const WebSearchHandler = HttpApiBuilder.group(Api, "server.websearch", (handlers) => + Effect.gen(function* () { + const awaitPlugins = Effect.fn("server.websearch.awaitPlugins")(function* () { + const plugins = yield* PluginSupervisor.Service + yield* plugins.ready.pipe( + Effect.timeoutOrElse({ + duration: "5 seconds", + orElse: () => + Effect.fail( + new ServiceUnavailableError({ + message: "Web search integration initialization timed out", + service: "websearch", + }), + ), + }), + ) + }) + return handlers + .handle( + "websearch.provider.get", + Effect.fn("server.websearch.provider.get")(function* () { + const websearch = yield* WebSearch.Service + return yield* response(websearch.selected()) + }), + ) + .handle( + "websearch.provider.select", + Effect.fn("server.websearch.provider.select")(function* (request) { + yield* awaitPlugins() + const websearch = yield* WebSearch.Service + yield* websearch.select(request.payload.providerID).pipe( + Effect.mapError( + (error) => + new InvalidRequestError({ + message: `Web search provider not found: ${error.providerID}`, + kind: "websearch_provider_not_found", + field: "providerID", + }), + ), + ) + return HttpApiSchema.NoContent.make() + }), + ) + .handle( + "websearch.query", + Effect.fn("server.websearch.query")(function* (request) { + yield* awaitPlugins() + const websearch = yield* WebSearch.Service + return yield* response( + websearch.query(request.payload).pipe( + Effect.catchTags({ + "WebSearch.ProviderRequired": () => + new InvalidRequestError({ + message: "Web search provider is required", + kind: "websearch_provider_required", + field: "providerID", + }), + "WebSearch.ProviderNotFound": (error) => + new InvalidRequestError({ + message: `Web search provider not found: ${error.providerID}`, + kind: "websearch_provider_not_found", + field: "providerID", + }), + "WebSearch.ConnectionRequired": (error) => + new InvalidRequestError({ + message: `Web search provider requires a connection: ${error.providerID}`, + kind: "websearch_connection_required", + field: "providerID", + }), + "WebSearch.Cancelled": () => + new InvalidRequestError({ message: "Web search cancelled", kind: "websearch_cancelled" }), + "WebSearch.Request": (error) => + new ServiceUnavailableError({ + message: `Web search request failed: ${error.providerID}`, + service: error.providerID, + }), + }), + ), + ) + }), + ) + }), +) diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index d231b110fa..51ba469c0e 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -65,11 +65,11 @@ export function DialogIntegration( .map((integration) => { const methods = connectMethods(integration) const connected = integration.connections.length > 0 - const search = integration.search - const selected = data.location.search.provider() === integration.id + const websearch = integration.websearch + const selected = data.location.websearch.provider() === integration.id const credentials = credentialConnections(integration) - const description = search?.connection === "optional" ? "API key optional" : undefined - const category = search ? "Web search" : undefined + const description = websearch?.connection === "optional" ? "API key optional" : undefined + const category = websearch ? "Web search" : undefined return { title: integration.name, value: integration.id, @@ -79,7 +79,7 @@ export function DialogIntegration( .filter((value) => value !== undefined && value.length > 0) .join(" · ") || undefined, category: category ?? (integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services"), - disabled: methods.length === 0 && !search, + disabled: methods.length === 0 && !websearch, gutter: connected ? () => : undefined, onSelect: () => { if (props.connectionOnly) { @@ -87,7 +87,7 @@ export function DialogIntegration( ? manageConnections(integration, methods, dialog, props.onConnected) : selectMethod(integration, methods, dialog, props.onConnected) } - if (search) return manageIntegration(integration, methods, search, dialog) + if (websearch) return manageIntegration(integration, methods, websearch, dialog) return credentials.length ? manageConnections(integration, methods, dialog, props.onConnected) : selectMethod(integration, methods, dialog, props.onConnected) @@ -112,7 +112,7 @@ export function DialogIntegration( function manageIntegration( integration: IntegrationInfo, methods: ConnectMethod[], - search: NonNullable, + websearch: NonNullable, dialog: ReturnType, ) { const connected = integration.connections.length > 0 @@ -121,15 +121,15 @@ function manageIntegration( const sdk = useSDK() const toast = useToast() const credentials = credentialConnections(integration) - const selected = createMemo(() => data.location.search.provider() === integration.id) - const selectSearch = () => { - void sdk.api.search.provider + const selected = createMemo(() => data.location.websearch.provider() === integration.id) + const selectWebSearch = () => { + void sdk.api.websearch.provider .select({ providerID: integration.id, location: location(data), }) .then(async () => { - await Promise.all([data.location.integration.refresh(), data.location.search.refresh()]) + await Promise.all([data.location.integration.refresh(), data.location.websearch.refresh()]) toast.show({ variant: "success", message: `${integration.name} is now the web search default` }) dialog.clear() }) @@ -142,12 +142,12 @@ function manageIntegration( options={[ { title: selected() ? "Web search default" : "Use for web search", - value: "search", + value: "websearch", disabled: selected(), onSelect: - search.connection === "required" && !connected - ? () => selectMethod(integration, methods, dialog, selectSearch) - : selectSearch, + websearch.connection === "required" && !connected + ? () => selectMethod(integration, methods, dialog, selectWebSearch) + : selectWebSearch, }, ...(methods.length ? [ diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 53b1d68450..13e736986f 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -56,7 +56,7 @@ type LocationData = { model?: ModelInfo[] provider?: ProviderV2Info[] reference?: ReferenceInfo[] - searchProvider?: string | null + websearchProvider?: string | null // Currently running shell commands for this location, keyed by shell id. Entries are removed // once the command exits or is deleted, so this only ever holds in-flight shells. shell?: Record @@ -803,7 +803,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ]) break case "config.updated": - void result.location.search.refresh(event.location) + void result.location.websearch.refresh(event.location) break // Authenticating an MCP integration reconnects its server, which emits mcp.status.changed, // so the mcp list refreshes here rather than off integration.updated. @@ -1049,14 +1049,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ setStore("location", key, { ...store.location[key], reference: mutable(result.data) }) }, }, - search: { + websearch: { provider(location?: LocationRef) { - return store.location[locationKey(location ?? defaultLocation())]?.searchProvider ?? undefined + return store.location[locationKey(location ?? defaultLocation())]?.websearchProvider ?? undefined }, async refresh(ref?: LocationRef) { - const result = await sdk.api.search.provider.get({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await sdk.api.websearch.provider.get({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) - setStore("location", key, { ...store.location[key], searchProvider: result.data ?? null }) + setStore("location", key, { ...store.location[key], websearchProvider: result.data ?? null }) }, }, skill: { @@ -1119,7 +1119,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ result.location.model.refresh(), result.location.provider.refresh(), result.location.reference.refresh(), - result.location.search.refresh(), + result.location.websearch.refresh(), result.location.command.refresh(), result.location.skill.refresh(), result.shell.refresh(), diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 62507b44d3..97cc1819c5 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -2382,10 +2382,12 @@ function WebFetch(props: ToolProps) { function WebSearch(props: ToolProps) { const data = useData() - const [provider, setProvider] = createSignal(data.location.search.provider() ?? stringValue(props.metadata.provider)) + const [provider, setProvider] = createSignal( + data.location.websearch.provider() ?? stringValue(props.metadata.provider), + ) createEffect(() => { if (provider()) return - const next = data.location.search.provider() + const next = data.location.websearch.provider() if (next) setProvider(next) }) return ( diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index 557b561b88..9e56ca00dc 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -118,7 +118,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType