refactor(websearch): rename search domain
This commit is contained in:
parent
97aa6713b5
commit
c86f4e9b9b
51 changed files with 478 additions and 462 deletions
|
|
@ -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<E = never> = WebsearchApi<E>
|
||||
|
||||
export interface CatalogApi<E = never> {
|
||||
readonly provider: ProviderApi<E>
|
||||
readonly model: ModelApi<E>
|
||||
|
|
|
|||
|
|
@ -923,33 +923,38 @@ export interface DebugApi<E = never> {
|
|||
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
|
||||
}
|
||||
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.search"]["search.provider.get"]>[0]
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.websearch"]["websearch.provider.get"]>[0]
|
||||
export type Endpoint26_0Input = { readonly location?: Endpoint26_0Request["query"]["location"] }
|
||||
export type Endpoint26_0Output = EffectValue<ReturnType<RawClient["server.search"]["search.provider.get"]>>
|
||||
export type SearchProviderGetOperation<E = never> = (input?: Endpoint26_0Input) => Effect.Effect<Endpoint26_0Output, E>
|
||||
export type Endpoint26_0Output = EffectValue<ReturnType<RawClient["server.websearch"]["websearch.provider.get"]>>
|
||||
export type WebsearchProviderGetOperation<E = never> = (
|
||||
input?: Endpoint26_0Input,
|
||||
) => Effect.Effect<Endpoint26_0Output, E>
|
||||
|
||||
type Endpoint26_1Request = Parameters<RawClient["server.search"]["search.provider.select"]>[0]
|
||||
type Endpoint26_1Request = Parameters<RawClient["server.websearch"]["websearch.provider.select"]>[0]
|
||||
export type Endpoint26_1Input = {
|
||||
readonly location?: Endpoint26_1Request["query"]["location"]
|
||||
readonly providerID: Endpoint26_1Request["payload"]["providerID"]
|
||||
}
|
||||
export type Endpoint26_1Output = EffectValue<ReturnType<RawClient["server.search"]["search.provider.select"]>>
|
||||
export type SearchProviderSelectOperation<E = never> = (
|
||||
export type Endpoint26_1Output = EffectValue<ReturnType<RawClient["server.websearch"]["websearch.provider.select"]>>
|
||||
export type WebsearchProviderSelectOperation<E = never> = (
|
||||
input: Endpoint26_1Input,
|
||||
) => Effect.Effect<Endpoint26_1Output, E>
|
||||
|
||||
type Endpoint26_2Request = Parameters<RawClient["server.search"]["search.query"]>[0]
|
||||
type Endpoint26_2Request = Parameters<RawClient["server.websearch"]["websearch.query"]>[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<ReturnType<RawClient["server.search"]["search.query"]>>
|
||||
export type SearchQueryOperation<E = never> = (input: Endpoint26_2Input) => Effect.Effect<Endpoint26_2Output, E>
|
||||
export type Endpoint26_2Output = EffectValue<ReturnType<RawClient["server.websearch"]["websearch.query"]>>
|
||||
export type WebsearchQueryOperation<E = never> = (input: Endpoint26_2Input) => Effect.Effect<Endpoint26_2Output, E>
|
||||
|
||||
export interface SearchApi<E = never> {
|
||||
readonly provider: { readonly get: SearchProviderGetOperation<E>; readonly select: SearchProviderSelectOperation<E> }
|
||||
readonly query: SearchQueryOperation<E>
|
||||
export interface WebsearchApi<E = never> {
|
||||
readonly provider: {
|
||||
readonly get: WebsearchProviderGetOperation<E>
|
||||
readonly select: WebsearchProviderSelectOperation<E>
|
||||
}
|
||||
readonly query: WebsearchQueryOperation<E>
|
||||
}
|
||||
|
||||
export interface AppApi<E = never> {
|
||||
|
|
@ -979,5 +984,5 @@ export interface AppApi<E = never> {
|
|||
readonly projectCopy: ProjectCopyApi<E>
|
||||
readonly vcs: VcsApi<E>
|
||||
readonly debug: DebugApi<E>
|
||||
readonly search: SearchApi<E>
|
||||
readonly websearch: WebsearchApi<E>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1099,35 +1099,35 @@ const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
|
|||
location: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) },
|
||||
})
|
||||
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.search"]["search.provider.get"]>[0]
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.websearch"]["websearch.provider.get"]>[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<RawClient["server.search"]["search.provider.select"]>[0]
|
||||
type Endpoint26_1Request = Parameters<RawClient["server.websearch"]["websearch.provider.select"]>[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<RawClient["server.search"]["search.query"]>[0]
|
||||
type Endpoint26_2Request = Parameters<RawClient["server.websearch"]["websearch.query"]>[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 }) =>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<EffectModelApi<unknown>>
|
|||
export type PluginApi = PromisifyApi<EffectPluginApi<unknown>>
|
||||
export type ProviderApi = PromisifyApi<EffectProviderApi<unknown>>
|
||||
export type ReferenceApi = PromisifyApi<EffectReferenceApi<unknown>>
|
||||
export type SearchApi = PromisifyApi<EffectSearchApi<unknown>>
|
||||
export type WebSearchApi = PromisifyApi<WebsearchApi<unknown>>
|
||||
export type SessionApi = PromisifyApi<EffectSessionApi<unknown>>
|
||||
export type SkillApi = PromisifyApi<EffectSkillApi<unknown>>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SearchProviderGetOutput>(
|
||||
get: (input?: WebsearchProviderGetInput, requestOptions?: RequestOptions) =>
|
||||
request<WebsearchProviderGetOutput>(
|
||||
{
|
||||
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<SearchProviderSelectOutput>(
|
||||
select: (input: WebsearchProviderSelectInput, requestOptions?: RequestOptions) =>
|
||||
request<WebsearchProviderSelectOutput>(
|
||||
{
|
||||
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<SearchQueryOutput>(
|
||||
query: (input: WebsearchQueryInput, requestOptions?: RequestOptions) =>
|
||||
request<WebsearchQueryOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/search`,
|
||||
path: `/api/websearch`,
|
||||
query: { location: input["location"] },
|
||||
body: { query: input["query"], providerID: input["providerID"] },
|
||||
successStatus: 200,
|
||||
|
|
|
|||
|
|
@ -2306,7 +2306,7 @@ export type IntegrationListOutput = {
|
|||
| { readonly type: "key"; readonly label?: string }
|
||||
| { readonly type: "env"; readonly names: ReadonlyArray<string> }
|
||||
>
|
||||
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<string> }
|
||||
>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export type {
|
|||
PluginApi,
|
||||
ProviderApi,
|
||||
ReferenceApi,
|
||||
SearchApi,
|
||||
WebSearchApi,
|
||||
SessionApi,
|
||||
SkillApi,
|
||||
} from "./api.js"
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<Info>("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({
|
||||
|
|
|
|||
|
|
@ -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<Info>("ConfigSearch.Info")({
|
||||
export class Info extends Schema.Class<Info>("ConfigWebSearch.Info")({
|
||||
provider: Integration.ID,
|
||||
}) {}
|
||||
|
|
@ -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<Search.ProviderOutput, unknown>
|
||||
) => Effect.Effect<WebSearch.ProviderOutput, unknown>
|
||||
}
|
||||
|
||||
export const Attempt = Integration.Attempt
|
||||
|
|
@ -129,7 +129,7 @@ type Entry = {
|
|||
ref: Types.DeepMutable<Ref>
|
||||
methods: Types.DeepMutable<Method>[]
|
||||
implementations: Map<MethodID, Types.DeepMutable<OAuthImplementation>>
|
||||
search?: Types.DeepMutable<SearchImplementation>
|
||||
websearch?: Types.DeepMutable<WebSearchImplementation>
|
||||
}
|
||||
|
||||
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<Draft> {
|
|||
/** Cancels an attempt and releases its resources. */
|
||||
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
|
||||
}
|
||||
readonly search: {
|
||||
readonly list: () => Effect.Effect<readonly SearchImplementation[]>
|
||||
readonly get: (integrationID: ID) => Effect.Effect<SearchImplementation | undefined>
|
||||
readonly websearch: {
|
||||
readonly list: () => Effect.Effect<readonly WebSearchImplementation[]>
|
||||
readonly get: (integrationID: ID) => Effect.Effect<WebSearchImplementation | undefined>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<SearchImplementation>
|
||||
current.websearch = implementation as Types.DeepMutable<WebSearchImplementation>
|
||||
},
|
||||
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
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<typeof search.execute>[0],
|
||||
execution: Omit<Parameters<typeof search.execute>[1], "signal">,
|
||||
input: Parameters<typeof websearch.execute>[0],
|
||||
execution: Omit<Parameters<typeof websearch.execute>[1], "signal">,
|
||||
) =>
|
||||
Effect.tryPromise({
|
||||
try: (signal) => search.execute(input, { ...execution, signal }),
|
||||
try: (signal) => websearch.execute(input, { ...execution, signal }),
|
||||
catch: (cause) => cause,
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
import { SearchExa } from "./exa"
|
||||
import { SearchParallel } from "./parallel"
|
||||
|
||||
export const SearchPlugins = [SearchExa.Plugin, SearchParallel.Plugin] as const
|
||||
|
|
@ -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<HttpClient.HttpClient | Scope.Scope>({
|
||||
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<HttpClient.HttpClient | Scope.Scope>({
|
|||
{ 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)
|
||||
4
packages/core/src/plugin/websearch/index.ts
Normal file
4
packages/core/src/plugin/websearch/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { WebSearchExa } from "./exa"
|
||||
import { WebSearchParallel } from "./parallel"
|
||||
|
||||
export const WebSearchPlugins = [WebSearchExa.Plugin, WebSearchParallel.Plugin] as const
|
||||
|
|
@ -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"
|
||||
|
|
@ -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<HttpClient.HttpClient | Scope.Scope>({
|
||||
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<HttpClient.HttpClient | Scope.Scope>({
|
|||
{ 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",
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<ProviderRequiredError>()(
|
||||
"Search.ProviderRequired",
|
||||
"WebSearch.ProviderRequired",
|
||||
{},
|
||||
) {}
|
||||
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()("Search.ProviderNotFound", {
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()("WebSearch.ProviderNotFound", {
|
||||
providerID: Integration.ID,
|
||||
}) {}
|
||||
|
||||
export class ConnectionRequiredError extends Schema.TaggedErrorClass<ConnectionRequiredError>()(
|
||||
"Search.ConnectionRequired",
|
||||
"WebSearch.ConnectionRequired",
|
||||
{ providerID: Integration.ID },
|
||||
) {}
|
||||
|
||||
export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("Search.Cancelled", {}) {}
|
||||
export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("WebSearch.Cancelled", {}) {}
|
||||
|
||||
export class RequestError extends Schema.TaggedErrorClass<RequestError>()("Search.Request", {
|
||||
export class RequestError extends Schema.TaggedErrorClass<RequestError>()("WebSearch.Request", {
|
||||
providerID: Integration.ID,
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
|
|
@ -61,7 +61,7 @@ export interface Interface {
|
|||
readonly query: (input: QueryInput) => Effect.Effect<Result, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Search") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@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<Integration.ID, Integration.SearchImplementation>,
|
||||
providers: Map<Integration.ID, Integration.WebSearchImplementation>,
|
||||
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<Integration.ID, Integration.SearchImplementation>,
|
||||
const ask = Effect.fn("WebSearch.ask")(function* (
|
||||
providers: Map<Integration.ID, Integration.WebSearchImplementation>,
|
||||
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
|
||||
|
|
@ -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]), [
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ function resourceMcpLayer(url: string) {
|
|||
complete: unusedIntegration,
|
||||
cancel: unusedIntegration,
|
||||
},
|
||||
search: {
|
||||
websearch: {
|
||||
list: unusedIntegration,
|
||||
get: unusedIntegration,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<string, string>
|
||||
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),
|
||||
)
|
||||
|
|
@ -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",
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -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<Search.ProviderOutput, unknown>
|
||||
) => Effect.Effect<WebSearch.ProviderOutput, unknown>
|
||||
}
|
||||
|
||||
export interface IntegrationDefinition {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly methods?: readonly IntegrationMethodDefinition[]
|
||||
readonly search?: IntegrationSearchDefinition
|
||||
readonly websearch?: IntegrationWebSearchDefinition
|
||||
}
|
||||
|
||||
export interface IntegrationDraft {
|
||||
|
|
|
|||
|
|
@ -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<Search.ProviderOutput>
|
||||
) => Promise<WebSearch.ProviderOutput>
|
||||
}
|
||||
|
||||
export interface IntegrationDefinition {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly methods?: readonly IntegrationMethodDefinition[]
|
||||
readonly search?: IntegrationSearchDefinition
|
||||
readonly websearch?: IntegrationWebSearchDefinition
|
||||
}
|
||||
|
||||
export interface IntegrationDomain extends IntegrationApi {
|
||||
|
|
|
|||
|
|
@ -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<LocationId extends HttpApiMiddleware.AnyId> =
|
|||
| HttpApiGroup.AddMiddleware<typeof GenerateGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof ProviderGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof IntegrationGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof SearchGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof WebSearchGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof McpGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof CredentialGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof ProjectGroup, LocationId>
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
}),
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<typeof Search> {}
|
||||
export const Search = Schema.Struct({
|
||||
export interface WebSearch extends Schema.Schema.Type<typeof WebSearch> {}
|
||||
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<Info>("Integration.Info")({
|
|||
id: ID,
|
||||
name: Schema.String,
|
||||
methods: Schema.Array(Method),
|
||||
search: optional(Search),
|
||||
websearch: optional(WebSearch),
|
||||
connections: Schema.Array(Connection.Info),
|
||||
}) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof Input> {}
|
|||
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<typeof ProviderOutput> {}
|
||||
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<Result>("Search.Result")({
|
||||
export class Result extends Schema.Class<Result>("WebSearch.Result")({
|
||||
providerID: IntegrationID,
|
||||
...ProviderOutput.fields,
|
||||
}) {}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
|
||||
return (options?.client ?? this.client).get<V2SearchProviderGetResponses, V2SearchProviderGetErrors, ThrowOnError>({
|
||||
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<V2SearchQueryResponses, V2SearchQueryErrors, ThrowOnError>({
|
||||
url: "/api/search",
|
||||
return (options?.client ?? this.client).post<V2WebsearchQueryResponses, V2WebsearchQueryErrors, ThrowOnError>({
|
||||
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 }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5721,7 +5721,7 @@ export type IntegrationEnvMethod = {
|
|||
names: Array<string>
|
||||
}
|
||||
|
||||
export type IntegrationSearch = {
|
||||
export type IntegrationWebSearch = {
|
||||
connection: "optional" | "required"
|
||||
}
|
||||
|
||||
|
|
@ -5742,7 +5742,7 @@ export type IntegrationInfo = {
|
|||
id: string
|
||||
name: string
|
||||
methods: Array<IntegrationMethod>
|
||||
search?: IntegrationSearch
|
||||
websearch?: IntegrationWebSearch
|
||||
connections: Array<ConnectionInfo>
|
||||
}
|
||||
|
||||
|
|
@ -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 = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
91
packages/server/src/handlers/websearch.ts
Normal file
91
packages/server/src/handlers/websearch.ts
Normal file
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -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 ? () => <text fg={theme.success}>✓</text> : 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<IntegrationInfo["search"]>,
|
||||
websearch: NonNullable<IntegrationInfo["websearch"]>,
|
||||
dialog: ReturnType<typeof useDialog>,
|
||||
) {
|
||||
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
|
||||
? [
|
||||
|
|
|
|||
|
|
@ -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<string, Shell>
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
|||
})
|
||||
if (url.pathname === "/api/reference")
|
||||
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
|
||||
if (url.pathname === "/api/search/provider") {
|
||||
if (url.pathname === "/api/websearch/provider") {
|
||||
if (request.method === "POST") return new Response(null, { status: 204 })
|
||||
return json({ location: { directory, project: { id: "proj_test", directory } }, data: null })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue