refactor(search): persist provider in global config
This commit is contained in:
parent
48f8b5de98
commit
ccad83549d
38 changed files with 736 additions and 589 deletions
|
|
@ -370,77 +370,63 @@ export type Endpoint9_1Input = {
|
|||
export type Endpoint9_1Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.get"]>>
|
||||
export type IntegrationGetOperation<E = never> = (input: Endpoint9_1Input) => Effect.Effect<Endpoint9_1Output, E>
|
||||
|
||||
type Endpoint9_2Request = Parameters<RawClient["server.integration"]["integration.capability.select"]>[0]
|
||||
type Endpoint9_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||
export type Endpoint9_2Input = {
|
||||
readonly integrationID: Endpoint9_2Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint9_2Request["query"]["location"]
|
||||
readonly capability: Endpoint9_2Request["payload"]["capability"]
|
||||
readonly key: Endpoint9_2Request["payload"]["key"]
|
||||
readonly label?: Endpoint9_2Request["payload"]["label"]
|
||||
}
|
||||
export type Endpoint9_2Output = EffectValue<
|
||||
ReturnType<RawClient["server.integration"]["integration.capability.select"]>
|
||||
>
|
||||
export type IntegrationSelectCapabilityOperation<E = never> = (
|
||||
input: Endpoint9_2Input,
|
||||
) => Effect.Effect<Endpoint9_2Output, E>
|
||||
export type Endpoint9_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
|
||||
export type IntegrationConnectKeyOperation<E = never> = (input: Endpoint9_2Input) => Effect.Effect<Endpoint9_2Output, E>
|
||||
|
||||
type Endpoint9_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||
type Endpoint9_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||
export type Endpoint9_3Input = {
|
||||
readonly integrationID: Endpoint9_3Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint9_3Request["query"]["location"]
|
||||
readonly key: Endpoint9_3Request["payload"]["key"]
|
||||
readonly methodID: Endpoint9_3Request["payload"]["methodID"]
|
||||
readonly inputs: Endpoint9_3Request["payload"]["inputs"]
|
||||
readonly label?: Endpoint9_3Request["payload"]["label"]
|
||||
}
|
||||
export type Endpoint9_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
|
||||
export type IntegrationConnectKeyOperation<E = never> = (input: Endpoint9_3Input) => Effect.Effect<Endpoint9_3Output, E>
|
||||
|
||||
type Endpoint9_4Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||
export type Endpoint9_4Input = {
|
||||
readonly integrationID: Endpoint9_4Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint9_4Request["query"]["location"]
|
||||
readonly methodID: Endpoint9_4Request["payload"]["methodID"]
|
||||
readonly inputs: Endpoint9_4Request["payload"]["inputs"]
|
||||
readonly label?: Endpoint9_4Request["payload"]["label"]
|
||||
}
|
||||
export type Endpoint9_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.oauth"]>>
|
||||
export type Endpoint9_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.oauth"]>>
|
||||
export type IntegrationConnectOauthOperation<E = never> = (
|
||||
input: Endpoint9_3Input,
|
||||
) => Effect.Effect<Endpoint9_3Output, E>
|
||||
|
||||
type Endpoint9_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||
export type Endpoint9_4Input = {
|
||||
readonly attemptID: Endpoint9_4Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_4Request["query"]["location"]
|
||||
}
|
||||
export type Endpoint9_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.status"]>>
|
||||
export type IntegrationAttemptStatusOperation<E = never> = (
|
||||
input: Endpoint9_4Input,
|
||||
) => Effect.Effect<Endpoint9_4Output, E>
|
||||
|
||||
type Endpoint9_5Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||
type Endpoint9_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||
export type Endpoint9_5Input = {
|
||||
readonly attemptID: Endpoint9_5Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_5Request["query"]["location"]
|
||||
readonly code?: Endpoint9_5Request["payload"]["code"]
|
||||
}
|
||||
export type Endpoint9_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.status"]>>
|
||||
export type IntegrationAttemptStatusOperation<E = never> = (
|
||||
export type Endpoint9_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.complete"]>>
|
||||
export type IntegrationAttemptCompleteOperation<E = never> = (
|
||||
input: Endpoint9_5Input,
|
||||
) => Effect.Effect<Endpoint9_5Output, E>
|
||||
|
||||
type Endpoint9_6Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||
type Endpoint9_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||
export type Endpoint9_6Input = {
|
||||
readonly attemptID: Endpoint9_6Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_6Request["query"]["location"]
|
||||
readonly code?: Endpoint9_6Request["payload"]["code"]
|
||||
}
|
||||
export type Endpoint9_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.complete"]>>
|
||||
export type IntegrationAttemptCompleteOperation<E = never> = (
|
||||
export type Endpoint9_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.cancel"]>>
|
||||
export type IntegrationAttemptCancelOperation<E = never> = (
|
||||
input: Endpoint9_6Input,
|
||||
) => Effect.Effect<Endpoint9_6Output, E>
|
||||
|
||||
type Endpoint9_7Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||
export type Endpoint9_7Input = {
|
||||
readonly attemptID: Endpoint9_7Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_7Request["query"]["location"]
|
||||
}
|
||||
export type Endpoint9_7Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.cancel"]>>
|
||||
export type IntegrationAttemptCancelOperation<E = never> = (
|
||||
input: Endpoint9_7Input,
|
||||
) => Effect.Effect<Endpoint9_7Output, E>
|
||||
|
||||
export interface IntegrationApi<E = never> {
|
||||
readonly list: IntegrationListOperation<E>
|
||||
readonly get: IntegrationGetOperation<E>
|
||||
readonly selectCapability: IntegrationSelectCapabilityOperation<E>
|
||||
readonly connectKey: IntegrationConnectKeyOperation<E>
|
||||
readonly connectOauth: IntegrationConnectOauthOperation<E>
|
||||
readonly attemptStatus: IntegrationAttemptStatusOperation<E>
|
||||
|
|
@ -908,20 +894,37 @@ export interface DebugApi<E = never> {
|
|||
readonly location: DebugLocationOperation<E>
|
||||
}
|
||||
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.search"]["search.query"]>[0]
|
||||
export type Endpoint26_0Input = {
|
||||
readonly location?: Endpoint26_0Request["query"]["location"]
|
||||
readonly query: Endpoint26_0Request["payload"]["query"]
|
||||
readonly providerID?: Endpoint26_0Request["payload"]["providerID"]
|
||||
readonly numResults?: Endpoint26_0Request["payload"]["numResults"]
|
||||
readonly livecrawl?: Endpoint26_0Request["payload"]["livecrawl"]
|
||||
readonly type?: Endpoint26_0Request["payload"]["type"]
|
||||
readonly contextMaxCharacters?: Endpoint26_0Request["payload"]["contextMaxCharacters"]
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.search"]["search.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 SearchProviderOperation<E = never> = (input?: Endpoint26_0Input) => Effect.Effect<Endpoint26_0Output, E>
|
||||
|
||||
type Endpoint26_1Request = Parameters<RawClient["server.search"]["search.provider.select"]>[0]
|
||||
export type Endpoint26_1Input = {
|
||||
readonly location?: Endpoint26_1Request["query"]["location"]
|
||||
readonly providerID: Endpoint26_1Request["payload"]["providerID"]
|
||||
}
|
||||
export type Endpoint26_0Output = EffectValue<ReturnType<RawClient["server.search"]["search.query"]>>
|
||||
export type SearchQueryOperation<E = never> = (input: Endpoint26_0Input) => Effect.Effect<Endpoint26_0Output, E>
|
||||
export type Endpoint26_1Output = EffectValue<ReturnType<RawClient["server.search"]["search.provider.select"]>>
|
||||
export type SearchSelectProviderOperation<E = never> = (
|
||||
input: Endpoint26_1Input,
|
||||
) => Effect.Effect<Endpoint26_1Output, E>
|
||||
|
||||
type Endpoint26_2Request = Parameters<RawClient["server.search"]["search.query"]>[0]
|
||||
export type Endpoint26_2Input = {
|
||||
readonly location?: Endpoint26_2Request["query"]["location"]
|
||||
readonly query: Endpoint26_2Request["payload"]["query"]
|
||||
readonly providerID?: Endpoint26_2Request["payload"]["providerID"]
|
||||
readonly numResults?: Endpoint26_2Request["payload"]["numResults"]
|
||||
readonly livecrawl?: Endpoint26_2Request["payload"]["livecrawl"]
|
||||
readonly type?: Endpoint26_2Request["payload"]["type"]
|
||||
readonly contextMaxCharacters?: Endpoint26_2Request["payload"]["contextMaxCharacters"]
|
||||
}
|
||||
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 interface SearchApi<E = never> {
|
||||
readonly provider: SearchProviderOperation<E>
|
||||
readonly selectProvider: SearchSelectProviderOperation<E>
|
||||
readonly query: SearchQueryOperation<E>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -450,78 +450,65 @@ const Endpoint9_1 = (raw: RawClient["server.integration"]) => (input: Endpoint9_
|
|||
query: { location: input["location"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_2Request = Parameters<RawClient["server.integration"]["integration.capability.select"]>[0]
|
||||
type Endpoint9_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||
type Endpoint9_2Input = {
|
||||
readonly integrationID: Endpoint9_2Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint9_2Request["query"]["location"]
|
||||
readonly capability: Endpoint9_2Request["payload"]["capability"]
|
||||
readonly key: Endpoint9_2Request["payload"]["key"]
|
||||
readonly label?: Endpoint9_2Request["payload"]["label"]
|
||||
}
|
||||
const Endpoint9_2 = (raw: RawClient["server.integration"]) => (input: Endpoint9_2Input) =>
|
||||
raw["integration.capability.select"]({
|
||||
params: { integrationID: input["integrationID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { capability: input["capability"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||
type Endpoint9_3Input = {
|
||||
readonly integrationID: Endpoint9_3Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint9_3Request["query"]["location"]
|
||||
readonly key: Endpoint9_3Request["payload"]["key"]
|
||||
readonly label?: Endpoint9_3Request["payload"]["label"]
|
||||
}
|
||||
const Endpoint9_3 = (raw: RawClient["server.integration"]) => (input: Endpoint9_3Input) =>
|
||||
raw["integration.connect.key"]({
|
||||
params: { integrationID: input["integrationID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { key: input["key"], label: input["label"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_4Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||
type Endpoint9_4Input = {
|
||||
readonly integrationID: Endpoint9_4Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint9_4Request["query"]["location"]
|
||||
readonly methodID: Endpoint9_4Request["payload"]["methodID"]
|
||||
readonly inputs: Endpoint9_4Request["payload"]["inputs"]
|
||||
readonly label?: Endpoint9_4Request["payload"]["label"]
|
||||
type Endpoint9_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||
type Endpoint9_3Input = {
|
||||
readonly integrationID: Endpoint9_3Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint9_3Request["query"]["location"]
|
||||
readonly methodID: Endpoint9_3Request["payload"]["methodID"]
|
||||
readonly inputs: Endpoint9_3Request["payload"]["inputs"]
|
||||
readonly label?: Endpoint9_3Request["payload"]["label"]
|
||||
}
|
||||
const Endpoint9_4 = (raw: RawClient["server.integration"]) => (input: Endpoint9_4Input) =>
|
||||
const Endpoint9_3 = (raw: RawClient["server.integration"]) => (input: Endpoint9_3Input) =>
|
||||
raw["integration.connect.oauth"]({
|
||||
params: { integrationID: input["integrationID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_5Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||
type Endpoint9_5Input = {
|
||||
readonly attemptID: Endpoint9_5Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_5Request["query"]["location"]
|
||||
type Endpoint9_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||
type Endpoint9_4Input = {
|
||||
readonly attemptID: Endpoint9_4Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_4Request["query"]["location"]
|
||||
}
|
||||
const Endpoint9_5 = (raw: RawClient["server.integration"]) => (input: Endpoint9_5Input) =>
|
||||
const Endpoint9_4 = (raw: RawClient["server.integration"]) => (input: Endpoint9_4Input) =>
|
||||
raw["integration.attempt.status"]({
|
||||
params: { attemptID: input["attemptID"] },
|
||||
query: { location: input["location"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_6Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||
type Endpoint9_6Input = {
|
||||
readonly attemptID: Endpoint9_6Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_6Request["query"]["location"]
|
||||
readonly code?: Endpoint9_6Request["payload"]["code"]
|
||||
type Endpoint9_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||
type Endpoint9_5Input = {
|
||||
readonly attemptID: Endpoint9_5Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_5Request["query"]["location"]
|
||||
readonly code?: Endpoint9_5Request["payload"]["code"]
|
||||
}
|
||||
const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_6Input) =>
|
||||
const Endpoint9_5 = (raw: RawClient["server.integration"]) => (input: Endpoint9_5Input) =>
|
||||
raw["integration.attempt.complete"]({
|
||||
params: { attemptID: input["attemptID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { code: input["code"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_7Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||
type Endpoint9_7Input = {
|
||||
readonly attemptID: Endpoint9_7Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_7Request["query"]["location"]
|
||||
type Endpoint9_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||
type Endpoint9_6Input = {
|
||||
readonly attemptID: Endpoint9_6Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint9_6Request["query"]["location"]
|
||||
}
|
||||
const Endpoint9_7 = (raw: RawClient["server.integration"]) => (input: Endpoint9_7Input) =>
|
||||
const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_6Input) =>
|
||||
raw["integration.attempt.cancel"]({
|
||||
params: { attemptID: input["attemptID"] },
|
||||
query: { location: input["location"] },
|
||||
|
|
@ -530,12 +517,11 @@ const Endpoint9_7 = (raw: RawClient["server.integration"]) => (input: Endpoint9_
|
|||
const adaptGroup9 = (raw: RawClient["server.integration"]) => ({
|
||||
list: Endpoint9_0(raw),
|
||||
get: Endpoint9_1(raw),
|
||||
selectCapability: Endpoint9_2(raw),
|
||||
connectKey: Endpoint9_3(raw),
|
||||
connectOauth: Endpoint9_4(raw),
|
||||
attemptStatus: Endpoint9_5(raw),
|
||||
attemptComplete: Endpoint9_6(raw),
|
||||
attemptCancel: Endpoint9_7(raw),
|
||||
connectKey: Endpoint9_2(raw),
|
||||
connectOauth: Endpoint9_3(raw),
|
||||
attemptStatus: Endpoint9_4(raw),
|
||||
attemptComplete: Endpoint9_5(raw),
|
||||
attemptCancel: Endpoint9_6(raw),
|
||||
})
|
||||
|
||||
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
||||
|
|
@ -1085,17 +1071,33 @@ const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
|||
|
||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
|
||||
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.search"]["search.query"]>[0]
|
||||
type Endpoint26_0Input = {
|
||||
readonly location?: Endpoint26_0Request["query"]["location"]
|
||||
readonly query: Endpoint26_0Request["payload"]["query"]
|
||||
readonly providerID?: Endpoint26_0Request["payload"]["providerID"]
|
||||
readonly numResults?: Endpoint26_0Request["payload"]["numResults"]
|
||||
readonly livecrawl?: Endpoint26_0Request["payload"]["livecrawl"]
|
||||
readonly type?: Endpoint26_0Request["payload"]["type"]
|
||||
readonly contextMaxCharacters?: Endpoint26_0Request["payload"]["contextMaxCharacters"]
|
||||
type Endpoint26_0Request = Parameters<RawClient["server.search"]["search.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))
|
||||
|
||||
type Endpoint26_1Request = Parameters<RawClient["server.search"]["search.provider.select"]>[0]
|
||||
type Endpoint26_1Input = {
|
||||
readonly location?: Endpoint26_1Request["query"]["location"]
|
||||
readonly providerID: Endpoint26_1Request["payload"]["providerID"]
|
||||
}
|
||||
const Endpoint26_0 = (raw: RawClient["server.search"]) => (input: Endpoint26_0Input) =>
|
||||
const Endpoint26_1 = (raw: RawClient["server.search"]) => (input: Endpoint26_1Input) =>
|
||||
raw["search.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_2Input = {
|
||||
readonly location?: Endpoint26_2Request["query"]["location"]
|
||||
readonly query: Endpoint26_2Request["payload"]["query"]
|
||||
readonly providerID?: Endpoint26_2Request["payload"]["providerID"]
|
||||
readonly numResults?: Endpoint26_2Request["payload"]["numResults"]
|
||||
readonly livecrawl?: Endpoint26_2Request["payload"]["livecrawl"]
|
||||
readonly type?: Endpoint26_2Request["payload"]["type"]
|
||||
readonly contextMaxCharacters?: Endpoint26_2Request["payload"]["contextMaxCharacters"]
|
||||
}
|
||||
const Endpoint26_2 = (raw: RawClient["server.search"]) => (input: Endpoint26_2Input) =>
|
||||
raw["search.query"]({
|
||||
query: { location: input["location"] },
|
||||
payload: {
|
||||
|
|
@ -1108,7 +1110,11 @@ const Endpoint26_0 = (raw: RawClient["server.search"]) => (input: Endpoint26_0In
|
|||
},
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup26 = (raw: RawClient["server.search"]) => ({ query: Endpoint26_0(raw) })
|
||||
const adaptGroup26 = (raw: RawClient["server.search"]) => ({
|
||||
provider: Endpoint26_0(raw),
|
||||
selectProvider: Endpoint26_1(raw),
|
||||
query: Endpoint26_2(raw),
|
||||
})
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
health: adaptGroup0(raw["server.health"]),
|
||||
|
|
|
|||
|
|
@ -75,8 +75,6 @@ import type {
|
|||
IntegrationListOutput,
|
||||
IntegrationGetInput,
|
||||
IntegrationGetOutput,
|
||||
IntegrationSelectCapabilityInput,
|
||||
IntegrationSelectCapabilityOutput,
|
||||
IntegrationConnectKeyInput,
|
||||
IntegrationConnectKeyOutput,
|
||||
IntegrationConnectOauthInput,
|
||||
|
|
@ -180,6 +178,10 @@ import type {
|
|||
VcsDiffInput,
|
||||
VcsDiffOutput,
|
||||
DebugLocationOutput,
|
||||
SearchProviderInput,
|
||||
SearchProviderOutput,
|
||||
SearchSelectProviderInput,
|
||||
SearchSelectProviderOutput,
|
||||
SearchQueryInput,
|
||||
SearchQueryOutput,
|
||||
} from "./types"
|
||||
|
|
@ -817,19 +819,6 @@ export function make(options: ClientOptions) {
|
|||
},
|
||||
requestOptions,
|
||||
),
|
||||
selectCapability: (input: IntegrationSelectCapabilityInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationSelectCapabilityOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/capability`,
|
||||
query: { location: input["location"] },
|
||||
body: { capability: input["capability"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connectKey: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationConnectKeyOutput>(
|
||||
{
|
||||
|
|
@ -1513,6 +1502,31 @@ export function make(options: ClientOptions) {
|
|||
),
|
||||
},
|
||||
search: {
|
||||
provider: (input?: SearchProviderInput, requestOptions?: RequestOptions) =>
|
||||
request<SearchProviderOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/search/provider`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
selectProvider: (input: SearchSelectProviderInput, requestOptions?: RequestOptions) =>
|
||||
request<SearchSelectProviderOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/search/provider`,
|
||||
query: { location: input["location"] },
|
||||
body: { providerID: input["providerID"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 503, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
query: (input: SearchQueryInput, requestOptions?: RequestOptions) =>
|
||||
request<SearchQueryOutput>(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2279,11 +2279,7 @@ export type IntegrationListOutput = {
|
|||
| { readonly type: "key"; readonly label?: string }
|
||||
| { readonly type: "env"; readonly names: ReadonlyArray<string> }
|
||||
>
|
||||
readonly capabilities: ReadonlyArray<{
|
||||
readonly type: "search"
|
||||
readonly connection: "optional" | "required"
|
||||
readonly selected: boolean
|
||||
}>
|
||||
readonly search?: { readonly connection: "optional" | "required" }
|
||||
readonly connections: ReadonlyArray<
|
||||
| { readonly type: "credential"; readonly id: string; readonly label: string }
|
||||
| { readonly type: "env"; readonly name: string }
|
||||
|
|
@ -2336,11 +2332,7 @@ export type IntegrationGetOutput = {
|
|||
| { readonly type: "key"; readonly label?: string }
|
||||
| { readonly type: "env"; readonly names: ReadonlyArray<string> }
|
||||
>
|
||||
readonly capabilities: ReadonlyArray<{
|
||||
readonly type: "search"
|
||||
readonly connection: "optional" | "required"
|
||||
readonly selected: boolean
|
||||
}>
|
||||
readonly search?: { readonly connection: "optional" | "required" }
|
||||
readonly connections: ReadonlyArray<
|
||||
| { readonly type: "credential"; readonly id: string; readonly label: string }
|
||||
| { readonly type: "env"; readonly name: string }
|
||||
|
|
@ -2348,16 +2340,6 @@ export type IntegrationGetOutput = {
|
|||
} | null
|
||||
}
|
||||
|
||||
export type IntegrationSelectCapabilityInput = {
|
||||
readonly integrationID: { readonly integrationID: string }["integrationID"]
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
readonly capability: { readonly capability: "search" }["capability"]
|
||||
}
|
||||
|
||||
export type IntegrationSelectCapabilityOutput = void
|
||||
|
||||
export type IntegrationConnectKeyInput = {
|
||||
readonly integrationID: { readonly integrationID: string }["integrationID"]
|
||||
readonly location?: {
|
||||
|
|
@ -6191,6 +6173,30 @@ export type VcsDiffOutput = {
|
|||
|
||||
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
|
||||
|
||||
export type SearchProviderInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type SearchProviderOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly project: { readonly id: string; readonly directory: string }
|
||||
}
|
||||
readonly data: string | null
|
||||
}
|
||||
|
||||
export type SearchSelectProviderInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
readonly providerID: { readonly providerID: string }["providerID"]
|
||||
}
|
||||
|
||||
export type SearchSelectProviderOutput = void
|
||||
|
||||
export type SearchQueryInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
|
|
|
|||
|
|
@ -38,14 +38,13 @@ test("exposes every standard HTTP API group", () => {
|
|||
expect(Object.keys(client.integration)).toEqual([
|
||||
"list",
|
||||
"get",
|
||||
"selectCapability",
|
||||
"connectKey",
|
||||
"connectOauth",
|
||||
"attemptStatus",
|
||||
"attemptComplete",
|
||||
"attemptCancel",
|
||||
])
|
||||
expect(Object.keys(client.search)).toEqual(["query"])
|
||||
expect(Object.keys(client.search)).toEqual(["provider", "selectProvider", "query"])
|
||||
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"])
|
||||
|
|
@ -79,6 +78,31 @@ test("search.query uses the public HTTP contract", async () => {
|
|||
expect(await request?.json()).toEqual({ query: "opencode", providerID: "exa", numResults: 5 })
|
||||
})
|
||||
|
||||
test("search provider methods use the public HTTP contract", async () => {
|
||||
const requests: Request[] = []
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async (input, init) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
requests.push(request)
|
||||
if (request.method === "POST") return new Response(null, { status: 204 })
|
||||
return Response.json({
|
||||
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
|
||||
data: "exa",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
expect(await client.search.provider({ location: { directory: "/tmp/project" } })).toMatchObject({ data: "exa" })
|
||||
await client.search.selectProvider({ 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"],
|
||||
])
|
||||
expect(await requests[1]?.json()).toEqual({ providerID: "parallel" })
|
||||
})
|
||||
|
||||
test("file.read returns binary content from the public HTTP contract", async () => {
|
||||
let request: Request | undefined
|
||||
const client = OpenCode.make({
|
||||
|
|
|
|||
|
|
@ -36,10 +36,6 @@
|
|||
"name": "event",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "integration_capability",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
|
|
@ -558,46 +554,6 @@
|
|||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "capability",
|
||||
"entityType": "columns",
|
||||
"table": "integration_capability"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "integration_id",
|
||||
"entityType": "columns",
|
||||
"table": "integration_capability"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "integration_capability"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "integration_capability"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
|
|
@ -1849,13 +1805,6 @@
|
|||
"table": "event",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["capability"],
|
||||
"nameExplicit": false,
|
||||
"name": "integration_capability_pk",
|
||||
"table": "integration_capability",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * as Config from "./config"
|
||||
export * as ConfigGlobal from "./config/global"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import path from "path"
|
||||
|
|
|
|||
50
packages/core/src/config/global.ts
Normal file
50
packages/core/src/config/global.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export * as ConfigGlobal from "./global"
|
||||
|
||||
import { randomUUID } from "node:crypto"
|
||||
import path from "node:path"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { applyEdits, modify, type JSONPath } from "jsonc-parser"
|
||||
import { makeGlobalNode } from "../effect/app-node"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { Global } from "../global"
|
||||
import { EffectFlock } from "../util/effect-flock"
|
||||
|
||||
export interface Interface {
|
||||
readonly update: (path: JSONPath, value: unknown) => Effect.Effect<void, FSUtil.Error | EffectFlock.LockError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ConfigGlobal") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const global = yield* Global.Service
|
||||
const flock = yield* EffectFlock.Service
|
||||
|
||||
return Service.of({
|
||||
update: Effect.fn("ConfigGlobal.update")(function* (jsonPath, value) {
|
||||
yield* flock.withLock(
|
||||
Effect.gen(function* () {
|
||||
const existing = yield* Effect.filter(
|
||||
["opencode.jsonc", "opencode.json"].map((name) => path.join(global.config, name)),
|
||||
fs.existsSafe,
|
||||
)
|
||||
const filepath = existing[0] ?? path.join(global.config, "opencode.json")
|
||||
const text = (yield* fs.readFileStringSafe(filepath)) ?? "{}"
|
||||
const next = applyEdits(
|
||||
text,
|
||||
modify(text, jsonPath, value, { formattingOptions: { tabSize: 2, insertSpaces: true } }),
|
||||
)
|
||||
const temp = `${filepath}.${randomUUID()}.tmp`
|
||||
yield* fs.writeWithDirs(temp, next)
|
||||
yield* fs.rename(temp, filepath).pipe(Effect.ensuring(fs.remove(temp).pipe(Effect.ignore)))
|
||||
}),
|
||||
"global-config",
|
||||
)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [EffectFlock.node, FSUtil.node, Global.node] })
|
||||
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -46,7 +46,6 @@ export const migrations = (
|
|||
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
|
||||
import("./migration/20260703200000_reset_v2_session_events"),
|
||||
import("./migration/20260705180000_rename_instructions"),
|
||||
import("./migration/20260706133920_integration-search"),
|
||||
import("./migration/20260706223930_add-session-fork"),
|
||||
import("./migration/20260707010146_durable_session_inbox"),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260706133920_integration-search",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`integration_capability\` (
|
||||
\`capability\` text PRIMARY KEY,
|
||||
\`integration_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -87,14 +87,6 @@ export default {
|
|||
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`integration_capability\` (
|
||||
\`capability\` text PRIMARY KEY,
|
||||
\`integration_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`permission\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
export * as Integration from "./integration"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { eq } from "drizzle-orm"
|
||||
import {
|
||||
Cause,
|
||||
Clock,
|
||||
|
|
@ -19,11 +18,9 @@ import {
|
|||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Search } from "@opencode-ai/schema/search"
|
||||
import { Credential } from "./credential"
|
||||
import { Database } from "./database/database"
|
||||
import { State } from "./state"
|
||||
import { EventV2 } from "./event"
|
||||
import { IntegrationConnection } from "./integration/connection"
|
||||
import { IntegrationCapabilityTable } from "./integration/sql"
|
||||
|
||||
export const ID = Integration.ID
|
||||
export type ID = Integration.ID
|
||||
|
|
@ -64,12 +61,6 @@ export type Info = Integration.Info
|
|||
export const Inputs = Integration.Inputs
|
||||
export type Inputs = Integration.Inputs
|
||||
|
||||
export const SearchCapability = Integration.SearchCapability
|
||||
export type SearchCapability = Omit<Integration.SearchCapability, "selected">
|
||||
|
||||
export const Capability = Integration.Capability
|
||||
export type Capability = Integration.Capability
|
||||
|
||||
export type OAuthAuthorization = {
|
||||
readonly url: string
|
||||
readonly instructions: string
|
||||
|
|
@ -106,7 +97,7 @@ export type Implementation = OAuthImplementation | KeyImplementation | EnvImplem
|
|||
|
||||
export interface SearchImplementation {
|
||||
readonly integrationID: ID
|
||||
readonly capability: SearchCapability
|
||||
readonly connection: Integration.Search["connection"]
|
||||
readonly execute: (
|
||||
input: Search.Input,
|
||||
context: { readonly credential?: Credential.Value; readonly sessionID?: string },
|
||||
|
|
@ -155,12 +146,10 @@ export type Draft = {
|
|||
update: (implementation: Implementation) => void
|
||||
remove: (integrationID: ID, method: Method) => void
|
||||
}
|
||||
capability: {
|
||||
search: {
|
||||
list: () => readonly SearchImplementation[]
|
||||
update: (implementation: SearchImplementation) => void
|
||||
remove: (integrationID: ID) => void
|
||||
}
|
||||
search: {
|
||||
list: () => readonly SearchImplementation[]
|
||||
update: (implementation: SearchImplementation) => void
|
||||
remove: (integrationID: ID) => void
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,13 +207,9 @@ export interface Interface extends State.Transformable<Draft> {
|
|||
/** Cancels an attempt and releases its resources. */
|
||||
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
|
||||
}
|
||||
readonly capability: {
|
||||
readonly search: {
|
||||
readonly list: () => Effect.Effect<readonly SearchImplementation[]>
|
||||
readonly get: (integrationID: ID) => Effect.Effect<SearchImplementation | undefined>
|
||||
readonly selected: () => Effect.Effect<ID | undefined>
|
||||
readonly select: (integrationID: ID) => Effect.Effect<void>
|
||||
}
|
||||
readonly search: {
|
||||
readonly list: () => Effect.Effect<readonly SearchImplementation[]>
|
||||
readonly get: (integrationID: ID) => Effect.Effect<SearchImplementation | undefined>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +242,6 @@ const layer = Layer.effect(
|
|||
Service,
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
const { db } = yield* Database.Service
|
||||
const events = yield* EventV2.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
|
||||
|
|
@ -317,30 +301,28 @@ const layer = Layer.effect(
|
|||
if (method.type === "oauth") current.implementations.delete(method.id)
|
||||
},
|
||||
},
|
||||
capability: {
|
||||
search: {
|
||||
list: () =>
|
||||
Array.from(draft.integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
),
|
||||
update: (implementation) => {
|
||||
const current = draft.integrations.get(implementation.integrationID) ?? {
|
||||
ref: {
|
||||
id: implementation.integrationID,
|
||||
name: implementation.integrationID,
|
||||
},
|
||||
methods: [],
|
||||
implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
|
||||
}
|
||||
if (!draft.integrations.has(implementation.integrationID)) {
|
||||
draft.integrations.set(implementation.integrationID, current)
|
||||
}
|
||||
current.search = implementation as Types.DeepMutable<SearchImplementation>
|
||||
},
|
||||
remove: (integrationID) => {
|
||||
const current = draft.integrations.get(integrationID)
|
||||
if (current) delete current.search
|
||||
},
|
||||
search: {
|
||||
list: () =>
|
||||
Array.from(draft.integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
),
|
||||
update: (implementation) => {
|
||||
const current = draft.integrations.get(implementation.integrationID) ?? {
|
||||
ref: {
|
||||
id: implementation.integrationID,
|
||||
name: implementation.integrationID,
|
||||
},
|
||||
methods: [],
|
||||
implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
|
||||
}
|
||||
if (!draft.integrations.has(implementation.integrationID)) {
|
||||
draft.integrations.set(implementation.integrationID, current)
|
||||
}
|
||||
current.search = implementation as Types.DeepMutable<SearchImplementation>
|
||||
},
|
||||
remove: (integrationID) => {
|
||||
const current = draft.integrations.get(integrationID)
|
||||
if (current) delete current.search
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
@ -362,24 +344,15 @@ const layer = Layer.effect(
|
|||
return [...credentials, ...env]
|
||||
}
|
||||
|
||||
const project = (entry: Entry, connections: IntegrationConnection.Info[], selectedSearch: ID | undefined) =>
|
||||
const project = (entry: Entry, connections: IntegrationConnection.Info[]) =>
|
||||
new Info({
|
||||
id: entry.ref.id,
|
||||
name: entry.ref.name,
|
||||
methods: entry.methods,
|
||||
capabilities: entry.search ? [{ ...entry.search.capability, selected: entry.ref.id === selectedSearch }] : [],
|
||||
search: entry.search ? { connection: entry.search.connection } : undefined,
|
||||
connections,
|
||||
})
|
||||
|
||||
const selectedSearch = Effect.fn("Integration.capability.search.selected")(function* () {
|
||||
return (yield* db
|
||||
.select({ integrationID: IntegrationCapabilityTable.integration_id })
|
||||
.from(IntegrationCapabilityTable)
|
||||
.where(eq(IntegrationCapabilityTable.capability, "search"))
|
||||
.get()
|
||||
.pipe(Effect.orDie))?.integrationID
|
||||
})
|
||||
|
||||
const authorize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause })))
|
||||
|
||||
|
|
@ -441,13 +414,12 @@ const layer = Layer.effect(
|
|||
get: Effect.fn("Integration.get")(function* (id) {
|
||||
const entry = state.get().integrations.get(id)
|
||||
if (!entry) return undefined
|
||||
return project(entry, resolveConnections(entry, yield* credentials.list(id)), yield* selectedSearch())
|
||||
return project(entry, resolveConnections(entry, yield* credentials.list(id)))
|
||||
}),
|
||||
list: Effect.fn("Integration.list")(function* () {
|
||||
const saved = Map.groupBy(yield* credentials.all(), (credential) => credential.integrationID)
|
||||
const selected = yield* selectedSearch()
|
||||
return Array.from(state.get().integrations.values(), (entry) =>
|
||||
project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? []), selected),
|
||||
project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? [])),
|
||||
).toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
}),
|
||||
connection: {
|
||||
|
|
@ -587,36 +559,18 @@ const layer = Layer.effect(
|
|||
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
|
||||
}),
|
||||
},
|
||||
capability: {
|
||||
search: {
|
||||
list: Effect.fn("Integration.capability.search.list")(function* () {
|
||||
return Array.from(state.get().integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
)
|
||||
}),
|
||||
get: Effect.fn("Integration.capability.search.get")(function* (integrationID) {
|
||||
return state.get().integrations.get(integrationID)?.search as SearchImplementation | undefined
|
||||
}),
|
||||
selected: selectedSearch,
|
||||
select: Effect.fn("Integration.capability.search.select")(function* (integrationID) {
|
||||
if (!state.get().integrations.get(integrationID)?.search) {
|
||||
return yield* Effect.die(new Error(`Search capability not found: ${integrationID}`))
|
||||
}
|
||||
yield* db
|
||||
.insert(IntegrationCapabilityTable)
|
||||
.values({ capability: "search", integration_id: integrationID })
|
||||
.onConflictDoUpdate({
|
||||
target: IntegrationCapabilityTable.capability,
|
||||
set: { integration_id: integrationID },
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* events.publish(Event.Updated, {})
|
||||
}),
|
||||
},
|
||||
search: {
|
||||
list: Effect.fn("Integration.search.list")(function* () {
|
||||
return Array.from(state.get().integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
)
|
||||
}),
|
||||
get: Effect.fn("Integration.search.get")(function* (integrationID) {
|
||||
return state.get().integrations.get(integrationID)?.search as SearchImplementation | undefined
|
||||
}),
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Credential.node, Database.node, EventV2.node] })
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Credential.node, EventV2.node] })
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import type { Integration } from "../integration"
|
||||
|
||||
export const IntegrationCapabilityTable = sqliteTable("integration_capability", {
|
||||
capability: text().$type<Integration.Capability["type"]>().primaryKey(),
|
||||
integration_id: text().$type<Integration.ID>().notNull(),
|
||||
...Timestamps,
|
||||
})
|
||||
|
|
@ -165,7 +165,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||
integration: {
|
||||
list: () => response(integration.list()),
|
||||
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
|
||||
selectCapability: (input) => integration.capability.search.select(Integration.ID.make(input.integrationID)),
|
||||
connectKey: (input) =>
|
||||
integration.connection.key({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
|
|
@ -343,9 +342,9 @@ function registerIntegration(draft: Integration.Draft, definition: IntegrationDe
|
|||
)
|
||||
}
|
||||
if (!definition.search) return
|
||||
draft.capability.search.update({
|
||||
draft.search.update({
|
||||
integrationID,
|
||||
capability: { type: "search", connection: definition.search.connection },
|
||||
connection: definition.search.connection,
|
||||
execute: definition.search.execute,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ export function fromPromise(plugin: Plugin) {
|
|||
integration: {
|
||||
list: (input) => run(host.integration.list(input)),
|
||||
get: (input) => run(host.integration.get(input)),
|
||||
selectCapability: (input) => run(host.integration.selectCapability(input)),
|
||||
connectKey: (input) => run(host.integration.connectKey(input)),
|
||||
connectOauth: (input) => run(host.integration.connectOauth(input)),
|
||||
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
export * as Search from "./search"
|
||||
|
||||
import { Search } from "@opencode-ai/schema/search"
|
||||
import { Context, Effect, Layer, Schema, Semaphore } from "effect"
|
||||
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 { makeLocationNode } from "./effect/app-node"
|
||||
import { EventV2 } from "./event"
|
||||
import { Form } from "./form"
|
||||
import { Global } from "./global"
|
||||
import { Integration } from "./integration"
|
||||
import { truthy } from "./flag/flag"
|
||||
|
||||
|
|
@ -50,6 +56,8 @@ export interface QueryInput extends Input {
|
|||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly selected: () => Effect.Effect<Integration.ID | undefined>
|
||||
readonly select: (providerID: Integration.ID) => Effect.Effect<void, ProviderNotFoundError>
|
||||
readonly query: (input: QueryInput) => Effect.Effect<Result, Error>
|
||||
}
|
||||
|
||||
|
|
@ -59,10 +67,14 @@ const layer = Layer.effect(
|
|||
Service,
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const configGlobal = yield* ConfigGlobal.Service
|
||||
const events = yield* EventV2.Service
|
||||
const forms = yield* Form.Service
|
||||
const global = yield* Global.Service
|
||||
const integrations = yield* Integration.Service
|
||||
const onboarding = Semaphore.makeUnsafe(1)
|
||||
const decodeOutput = Schema.decodeUnknownEffect(ProviderOutput)
|
||||
let pending: Integration.ID | undefined
|
||||
|
||||
const requireProvider = (
|
||||
providers: Map<Integration.ID, Integration.SearchImplementation>,
|
||||
|
|
@ -86,6 +98,39 @@ const layer = Layer.effect(
|
|||
}
|
||||
})
|
||||
|
||||
const globalProvider = Effect.fn("Search.globalProvider")(function* () {
|
||||
const entries = (yield* config.entries()).filter(
|
||||
(entry) => entry.type === "document" && entry.path && path.dirname(entry.path) === path.resolve(global.config),
|
||||
)
|
||||
return Config.latest(entries, "search")?.provider
|
||||
})
|
||||
|
||||
const selected = Effect.fn("Search.selected")(function* () {
|
||||
return pending ?? (yield* globalProvider())
|
||||
})
|
||||
|
||||
const save = Effect.fn("Search.save")(function* (providerID: Integration.ID) {
|
||||
pending = providerID
|
||||
yield* configGlobal.update(["search"], new ConfigSearch.Info({ provider: providerID })).pipe(
|
||||
Effect.tapError(() => Effect.sync(() => (pending = undefined))),
|
||||
Effect.orDie,
|
||||
)
|
||||
})
|
||||
|
||||
yield* events.subscribe(ConfigSchema.Event.Updated).pipe(
|
||||
Stream.runForEach(() =>
|
||||
globalProvider().pipe(
|
||||
Effect.tap((providerID) =>
|
||||
Effect.sync(() => {
|
||||
if (providerID === pending) pending = undefined
|
||||
}),
|
||||
),
|
||||
Effect.ignore,
|
||||
),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
const ask = Effect.fn("Search.ask")(function* (
|
||||
providers: Map<Integration.ID, Integration.SearchImplementation>,
|
||||
sessionID: string,
|
||||
|
|
@ -110,8 +155,7 @@ const layer = Layer.effect(
|
|||
.flatMap((provider) => {
|
||||
const info = infos.get(provider.integrationID)
|
||||
if (!info) return []
|
||||
const disconnected =
|
||||
provider.capability.connection === "optional" ? "Keyless available" : "Connection required"
|
||||
const disconnected = provider.connection === "optional" ? "Keyless available" : "Connection required"
|
||||
return [{ info, description: info.connections.length ? "Connected" : disconnected }]
|
||||
})
|
||||
.toSorted((a, b) => a.info.name.localeCompare(b.info.name))
|
||||
|
|
@ -135,7 +179,7 @@ const layer = Layer.effect(
|
|||
sessionID?: string,
|
||||
) {
|
||||
const active = yield* integrations.connection.active(provider.integrationID)
|
||||
if (active || provider.capability.connection === "optional") return active
|
||||
if (active || provider.connection === "optional") return active
|
||||
if (!sessionID) return yield* new ConnectionRequiredError({ providerID: provider.integrationID })
|
||||
const state = yield* forms
|
||||
.ask({
|
||||
|
|
@ -152,34 +196,40 @@ const layer = Layer.effect(
|
|||
return connected
|
||||
})
|
||||
|
||||
const select = Effect.fn("Search.select")(function* (input: QueryInput) {
|
||||
const resolve = Effect.fn("Search.resolve")(function* (input: QueryInput) {
|
||||
const providers = new Map(
|
||||
(yield* integrations.capability.search.list()).map((provider) => [provider.integrationID, provider]),
|
||||
(yield* integrations.search.list()).map((provider) => [provider.integrationID, provider]),
|
||||
)
|
||||
if (input.providerID) return yield* requireProvider(providers, input.providerID)
|
||||
const override = yield* configuredProvider()
|
||||
if (override) return yield* requireProvider(providers, override)
|
||||
const selected = yield* integrations.capability.search.selected()
|
||||
const provider = selected ? providers.get(selected) : undefined
|
||||
const providerID = yield* selected()
|
||||
const provider = providerID ? providers.get(providerID) : undefined
|
||||
if (provider) return provider
|
||||
const sessionID = input.sessionID
|
||||
if (!sessionID) return yield* new ProviderRequiredError()
|
||||
return yield* onboarding.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const current = yield* integrations.capability.search.selected()
|
||||
const selected = current ? providers.get(current) : undefined
|
||||
if (selected) return selected
|
||||
const current = yield* selected()
|
||||
const selectedProvider = current ? providers.get(current) : undefined
|
||||
if (selectedProvider) return selectedProvider
|
||||
const provider = yield* ask(providers, sessionID)
|
||||
yield* connect(provider, sessionID)
|
||||
yield* integrations.capability.search.select(provider.integrationID)
|
||||
yield* save(provider.integrationID)
|
||||
return provider
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
selected,
|
||||
select: Effect.fn("Search.select")(function* (providerID) {
|
||||
const provider = yield* integrations.search.get(providerID)
|
||||
if (!provider) return yield* new ProviderNotFoundError({ providerID })
|
||||
yield* save(providerID)
|
||||
}),
|
||||
query: Effect.fn("Search.query")(function* (input) {
|
||||
const provider = yield* select(input)
|
||||
const provider = yield* resolve(input)
|
||||
const connection = yield* connect(provider, input.sessionID)
|
||||
const credential = connection
|
||||
? yield* integrations.connection
|
||||
|
|
@ -196,4 +246,8 @@ const layer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Config.node, Form.node, Integration.node] })
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [Config.node, ConfigGlobal.node, EventV2.node, Form.node, Global.node, Integration.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { describe, expect } from "bun:test"
|
|||
import { Effect, Fiber, Layer, PubSub, Schema, Stream } from "effect"
|
||||
import { FastCheck } from "effect/testing"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigGlobal } from "@opencode-ai/core/config/global"
|
||||
import { ConfigModel } from "@opencode-ai/core/config/model"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
||||
|
|
@ -22,6 +23,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||
import { location } from "../fixture/location"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { parse } from "jsonc-parser"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
const selection = Schema.decodeUnknownSync(ConfigModel.Selection)
|
||||
|
|
@ -58,6 +60,37 @@ const provider = {
|
|||
}
|
||||
|
||||
describe("Config", () => {
|
||||
it.live("updates the global JSONC config without removing comments", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const global = path.join(tmp.path, "global")
|
||||
const file = path.join(global, "opencode.jsonc")
|
||||
yield* Effect.promise(async () => {
|
||||
await fs.mkdir(global, { recursive: true })
|
||||
await fs.writeFile(file, `// user config\n{\n "username": "tester"\n}\n`)
|
||||
})
|
||||
|
||||
const config = yield* ConfigGlobal.Service
|
||||
yield* config.update(["search"], { 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" } })
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
AppNodeBuilder.build(LayerNode.group([ConfigGlobal.node]), [
|
||||
[Global.node, Global.layerWith({ config: path.join(tmp.path, "global") })],
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("reloads external config and publishes directory updates", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe("Integration", () => {
|
|||
.transform((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
|
||||
.pipe(Scope.provide(scope))
|
||||
expect(yield* integrations.get(openai)).toEqual(
|
||||
new Integration.Info({ id: openai, name: "OpenAI", methods: [], capabilities: [], connections: [] }),
|
||||
new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
|
||||
)
|
||||
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ export function host(overrides: Overrides = {}): PluginContext {
|
|||
integration: overrides.integration ?? {
|
||||
list: () => Effect.die("unused integration.list"),
|
||||
get: () => Effect.die("unused integration.get"),
|
||||
selectCapability: () => Effect.die("unused integration.selectCapability"),
|
||||
connectKey: () => Effect.die("unused integration.connectKey"),
|
||||
connectOauth: () => Effect.die("unused integration.connectOauth"),
|
||||
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
|
||||
|
|
@ -195,7 +194,6 @@ export function integrationHost(integration: Integration.Interface): PluginConte
|
|||
return {
|
||||
list: () => Effect.die("unused integration.list"),
|
||||
get: () => Effect.die("unused integration.get"),
|
||||
selectCapability: () => Effect.die("unused integration.selectCapability"),
|
||||
connectKey: () => Effect.die("unused integration.connectKey"),
|
||||
connectOauth: () => Effect.die("unused integration.connectOauth"),
|
||||
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
|
||||
|
|
@ -257,9 +255,9 @@ function registerIntegration(draft: Integration.Draft, definition: IntegrationDe
|
|||
)
|
||||
}
|
||||
if (!definition.search) return
|
||||
draft.capability.search.update({
|
||||
draft.search.update({
|
||||
integrationID,
|
||||
capability: { type: "search", connection: definition.search.connection },
|
||||
connection: definition.search.connection,
|
||||
execute: definition.search.execute,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,6 @@ describe("ModelsDevPlugin", () => {
|
|||
names: ["ACME_API_KEY"],
|
||||
},
|
||||
],
|
||||
capabilities: [],
|
||||
connections: [],
|
||||
}),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ describe("fromPromise", () => {
|
|||
name: "Promise Search",
|
||||
methods: [{ type: "env", names: ["PROMISE_SEARCH_KEY"] }],
|
||||
})
|
||||
const provider = yield* integrations.capability.search.get(Integration.ID.make("promise-search"))
|
||||
const provider = yield* integrations.search.get(Integration.ID.make("promise-search"))
|
||||
if (!provider) return yield* Effect.die("Expected promise search provider")
|
||||
expect(yield* provider.execute({ query: "effect" }, {})).toEqual({ text: "promise: effect" })
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ describe("built-in search integrations", () => {
|
|||
expect(yield* integrations.get(Integration.ID.make("test-search"))).toMatchObject({
|
||||
name: "Test Search",
|
||||
methods: [{ type: "key", label: "API key" }],
|
||||
capabilities: [{ type: "search", connection: "required" }],
|
||||
search: { connection: "required" },
|
||||
})
|
||||
yield* registration.dispose
|
||||
expect(yield* integrations.get(Integration.ID.make("test-search"))).toBeUndefined()
|
||||
|
|
@ -53,9 +53,9 @@ describe("built-in search integrations", () => {
|
|||
id: "exa",
|
||||
name: "Exa",
|
||||
methods: [{ type: "key" }, { type: "env", names: ["EXA_API_KEY"] }],
|
||||
capabilities: [{ type: "search", connection: "optional", selected: false }],
|
||||
search: { connection: "optional" },
|
||||
})
|
||||
const provider = yield* integrations.capability.search.get(Integration.ID.make("exa"))
|
||||
const provider = yield* integrations.search.get(Integration.ID.make("exa"))
|
||||
if (!provider) return yield* Effect.die("Expected Exa search provider")
|
||||
expect(
|
||||
yield* provider.execute(
|
||||
|
|
@ -97,7 +97,7 @@ describe("built-in search integrations", () => {
|
|||
Effect.gen(function* () {
|
||||
const integrations = yield* Integration.Service
|
||||
yield* SearchParallel.Plugin.effect(host({ integration: integrationHost(integrations) }))
|
||||
const provider = yield* integrations.capability.search.get(Integration.ID.make("parallel"))
|
||||
const provider = yield* integrations.search.get(Integration.ID.make("parallel"))
|
||||
if (!provider) return yield* Effect.die("Expected Parallel search provider")
|
||||
|
||||
const output = yield* provider.execute(
|
||||
|
|
|
|||
|
|
@ -1,22 +1,34 @@
|
|||
import { beforeEach, describe, expect } from "bun:test"
|
||||
import { Effect, Exit, Fiber, Layer, Scope } from "effect"
|
||||
import path from "node:path"
|
||||
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 { 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 { testEffect } from "./lib/effect"
|
||||
|
||||
let entries: Config.Entry[] = []
|
||||
const writes: { path: readonly (string | number)[]; value: unknown }[] = []
|
||||
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed(entries) }))
|
||||
const configGlobal = Layer.succeed(
|
||||
ConfigGlobal.Service,
|
||||
ConfigGlobal.Service.of({ update: (path, value) => Effect.sync(() => writes.push({ path, value })) }),
|
||||
)
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(LayerNode.group([Search.node, Integration.node, Credential.node, EventV2.node, Form.node]), [
|
||||
[Config.node, config],
|
||||
]),
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([Search.node, Integration.node, Credential.node, EventV2.node, Form.node, ConfigGlobal.node]),
|
||||
[
|
||||
[Config.node, config],
|
||||
[ConfigGlobal.node, configGlobal],
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
const register = (id: string, connection: "optional" | "required" = "optional") =>
|
||||
|
|
@ -26,9 +38,9 @@ const register = (id: string, connection: "optional" | "required" = "optional")
|
|||
const calls: { input: Search.Input; credential?: Credential.Value; sessionID?: string }[] = []
|
||||
yield* integrations.transform((draft) => {
|
||||
draft.update(integrationID, (integration) => (integration.name = id.toUpperCase()))
|
||||
draft.capability.search.update({
|
||||
draft.search.update({
|
||||
integrationID,
|
||||
capability: { type: "search", connection },
|
||||
connection,
|
||||
execute: (input, context) =>
|
||||
Effect.sync(() => {
|
||||
calls.push({ input, ...context })
|
||||
|
|
@ -41,6 +53,7 @@ const register = (id: string, connection: "optional" | "required" = "optional")
|
|||
|
||||
beforeEach(() => {
|
||||
entries = []
|
||||
writes.length = 0
|
||||
})
|
||||
|
||||
describe("Search", () => {
|
||||
|
|
@ -48,7 +61,6 @@ describe("Search", () => {
|
|||
Effect.gen(function* () {
|
||||
const provider = yield* register("exa")
|
||||
const search = yield* Search.Service
|
||||
const integrations = yield* Integration.Service
|
||||
|
||||
expect(yield* search.query({ query: "effect", providerID: provider.integrationID })).toEqual(
|
||||
new Search.Result({
|
||||
|
|
@ -57,7 +69,7 @@ describe("Search", () => {
|
|||
metadata: { id: "exa" },
|
||||
}),
|
||||
)
|
||||
expect(yield* integrations.capability.search.selected()).toBeUndefined()
|
||||
expect(yield* search.selected()).toBeUndefined()
|
||||
expect(provider.calls).toEqual([
|
||||
{
|
||||
input: { query: "effect", providerID: provider.integrationID },
|
||||
|
|
@ -68,18 +80,33 @@ describe("Search", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the persisted integration capability selection", () =>
|
||||
it.effect("uses and persists the global provider selection", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* register("exa")
|
||||
const parallel = yield* register("parallel")
|
||||
const integrations = yield* Integration.Service
|
||||
const search = yield* Search.Service
|
||||
yield* integrations.capability.search.select(parallel.integrationID)
|
||||
yield* search.select(parallel.integrationID)
|
||||
|
||||
expect((yield* search.query({ query: "layers" })).providerID).toBe(parallel.integrationID)
|
||||
expect((yield* integrations.get(parallel.integrationID))?.capabilities).toEqual([
|
||||
{ type: "search", connection: "optional", selected: true },
|
||||
])
|
||||
expect(yield* search.selected()).toBe(parallel.integrationID)
|
||||
expect(writes).toEqual([{ path: ["search"], value: new ConfigSearch.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
|
||||
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 }) }),
|
||||
}),
|
||||
]
|
||||
|
||||
expect(yield* search.selected()).toBe(provider.integrationID)
|
||||
expect((yield* search.query({ query: "configured" })).providerID).toBe(provider.integrationID)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -87,9 +114,8 @@ describe("Search", () => {
|
|||
Effect.gen(function* () {
|
||||
const exa = yield* register("exa")
|
||||
const parallel = yield* register("parallel")
|
||||
const integrations = yield* Integration.Service
|
||||
const search = yield* Search.Service
|
||||
yield* integrations.capability.search.select(exa.integrationID)
|
||||
yield* search.select(exa.integrationID)
|
||||
entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
|
|
@ -106,7 +132,6 @@ describe("Search", () => {
|
|||
const provider = yield* register("exa")
|
||||
const search = yield* Search.Service
|
||||
const forms = yield* Form.Service
|
||||
const integrations = yield* Integration.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)
|
||||
yield* Effect.yieldNow
|
||||
|
|
@ -119,7 +144,7 @@ describe("Search", () => {
|
|||
|
||||
expect((yield* Fiber.join(first)).providerID).toBe(provider.integrationID)
|
||||
expect((yield* Fiber.join(second)).providerID).toBe(provider.integrationID)
|
||||
expect(yield* integrations.capability.search.selected()).toBe(provider.integrationID)
|
||||
expect(yield* search.selected()).toBe(provider.integrationID)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -140,9 +165,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.capability.search.get(provider.integrationID)).toBeDefined()
|
||||
expect(yield* integrations.search.get(provider.integrationID)).toBeDefined()
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
expect(yield* integrations.capability.search.get(provider.integrationID)).toBeUndefined()
|
||||
expect(yield* integrations.search.get(provider.integrationID)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ const permission = Layer.succeed(
|
|||
const search = Layer.succeed(
|
||||
Search.Service,
|
||||
Search.Service.of({
|
||||
selected: () => Effect.succeed(undefined),
|
||||
select: () => Effect.die("unused"),
|
||||
query: (input) =>
|
||||
Effect.sync(() => {
|
||||
queries.push(input)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ export const groupNames = {
|
|||
export const endpointNames = {
|
||||
"session.messages": "list",
|
||||
"integration.connect.key": "connectKey",
|
||||
"integration.capability.select": "selectCapability",
|
||||
"search.provider.get": "provider",
|
||||
"search.provider.select": "selectProvider",
|
||||
"integration.connect.oauth": "connectOauth",
|
||||
"integration.attempt.status": "attemptStatus",
|
||||
"integration.attempt.complete": "attemptComplete",
|
||||
|
|
|
|||
|
|
@ -37,23 +37,6 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
|||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("integration.capability.select", "/api/integration/:integrationID/capability", {
|
||||
params: { integrationID: Integration.ID },
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({ capability: Schema.Literal("search") }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: InvalidRequestError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.capability.select",
|
||||
summary: "Select integration capability",
|
||||
description: "Set the default integration for a capability.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("integration.connect.key", "/api/integration/:integrationID/connect/key", {
|
||||
params: { integrationID: Integration.ID },
|
||||
|
|
|
|||
|
|
@ -1,11 +1,42 @@
|
|||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Location } from "@opencode-ai/schema/location"
|
||||
import { Search } from "@opencode-ai/schema/search"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
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")
|
||||
.add(
|
||||
HttpApiEndpoint.get("search.provider.get", "/api/search/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",
|
||||
description: "Return the globally selected web search provider.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("search.provider.select", "/api/search/provider", {
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({ providerID: Integration.ID }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [InvalidRequestError, ServiceUnavailableError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.search.provider.select",
|
||||
summary: "Select default search provider",
|
||||
description: "Persist the global web search provider in the user configuration.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("search.query", "/api/search", {
|
||||
query: LocationQuery,
|
||||
|
|
|
|||
|
|
@ -76,15 +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 SearchCapability extends Schema.Schema.Type<typeof SearchCapability> {}
|
||||
export const SearchCapability = Schema.Struct({
|
||||
type: Schema.Literal("search"),
|
||||
export interface Search extends Schema.Schema.Type<typeof Search> {}
|
||||
export const Search = Schema.Struct({
|
||||
connection: Schema.Literals(["optional", "required"]),
|
||||
selected: Schema.Boolean,
|
||||
}).annotate({ identifier: "Integration.SearchCapability" })
|
||||
|
||||
export const Capability = SearchCapability
|
||||
export type Capability = SearchCapability
|
||||
}).annotate({ identifier: "Integration.Search" })
|
||||
|
||||
const Updated = ephemeral({
|
||||
type: "integration.updated",
|
||||
|
|
@ -106,7 +101,7 @@ export class Info extends Schema.Class<Info>("Integration.Info")({
|
|||
id: ID,
|
||||
name: Schema.String,
|
||||
methods: Schema.Array(Method),
|
||||
capabilities: Schema.Array(Capability),
|
||||
search: optional(Search),
|
||||
connections: Schema.Array(Connection.Info),
|
||||
}) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -298,8 +298,6 @@ import type {
|
|||
V2IntegrationAttemptCompleteResponses,
|
||||
V2IntegrationAttemptStatusErrors,
|
||||
V2IntegrationAttemptStatusResponses,
|
||||
V2IntegrationCapabilitySelectErrors,
|
||||
V2IntegrationCapabilitySelectResponses,
|
||||
V2IntegrationConnectKeyErrors,
|
||||
V2IntegrationConnectKeyResponses,
|
||||
V2IntegrationConnectOauthErrors,
|
||||
|
|
@ -358,6 +356,10 @@ import type {
|
|||
V2QuestionRequestListResponses,
|
||||
V2ReferenceListErrors,
|
||||
V2ReferenceListResponses,
|
||||
V2SearchProviderGetErrors,
|
||||
V2SearchProviderGetResponses,
|
||||
V2SearchProviderSelectErrors,
|
||||
V2SearchProviderSelectResponses,
|
||||
V2SearchQueryErrors,
|
||||
V2SearchQueryResponses,
|
||||
V2SessionActiveErrors,
|
||||
|
|
@ -6688,52 +6690,6 @@ export class Provider2 extends HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
export class Capability extends HeyApiClient {
|
||||
/**
|
||||
* Select integration capability
|
||||
*
|
||||
* Set the default integration for a capability.
|
||||
*/
|
||||
public select<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
integrationID: string
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
capability?: "search"
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "integrationID" },
|
||||
{ in: "query", key: "location" },
|
||||
{ in: "body", key: "capability" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<
|
||||
V2IntegrationCapabilitySelectResponses,
|
||||
V2IntegrationCapabilitySelectErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/integration/{integrationID}/capability",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Connect extends HeyApiClient {
|
||||
/**
|
||||
* Connect with key
|
||||
|
|
@ -7008,11 +6964,6 @@ export class Integration extends HeyApiClient {
|
|||
})
|
||||
}
|
||||
|
||||
private _capability?: Capability
|
||||
get capability(): Capability {
|
||||
return (this._capability ??= new Capability({ client: this.client }))
|
||||
}
|
||||
|
||||
private _connect?: Connect
|
||||
get connect(): Connect {
|
||||
return (this._connect ??= new Connect({ client: this.client }))
|
||||
|
|
@ -8186,6 +8137,72 @@ export class Debug extends HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
export class Provider3 extends HeyApiClient {
|
||||
/**
|
||||
* Get default search provider
|
||||
*
|
||||
* Return the globally selected web search provider.
|
||||
*/
|
||||
public get<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
},
|
||||
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",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Select default search provider
|
||||
*
|
||||
* Persist the global web search provider in the user configuration.
|
||||
*/
|
||||
public select<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
providerID?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "location" },
|
||||
{ in: "body", key: "providerID" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<
|
||||
V2SearchProviderSelectResponses,
|
||||
V2SearchProviderSelectErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/search/provider",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Search extends HeyApiClient {
|
||||
/**
|
||||
* Search the web
|
||||
|
|
@ -8234,6 +8251,11 @@ export class Search extends HeyApiClient {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
private _provider?: Provider3
|
||||
get provider(): Provider3 {
|
||||
return (this._provider ??= new Provider3({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class V2 extends HeyApiClient {
|
||||
|
|
|
|||
|
|
@ -5611,10 +5611,8 @@ export type IntegrationEnvMethod = {
|
|||
names: Array<string>
|
||||
}
|
||||
|
||||
export type IntegrationSearchCapability = {
|
||||
type: "search"
|
||||
export type IntegrationSearch = {
|
||||
connection: "optional" | "required"
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export type ConnectionCredentialInfo = {
|
||||
|
|
@ -5634,7 +5632,7 @@ export type IntegrationInfo = {
|
|||
id: string
|
||||
name: string
|
||||
methods: Array<IntegrationMethod>
|
||||
capabilities: Array<IntegrationSearchCapability>
|
||||
search?: IntegrationSearch
|
||||
connections: Array<ConnectionInfo>
|
||||
}
|
||||
|
||||
|
|
@ -16486,46 +16484,6 @@ export type V2IntegrationGetResponses = {
|
|||
|
||||
export type V2IntegrationGetResponse = V2IntegrationGetResponses[keyof V2IntegrationGetResponses]
|
||||
|
||||
export type V2IntegrationCapabilitySelectData = {
|
||||
body: {
|
||||
capability: "search"
|
||||
}
|
||||
path: {
|
||||
integrationID: string
|
||||
}
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
}
|
||||
url: "/api/integration/{integrationID}/capability"
|
||||
}
|
||||
|
||||
export type V2IntegrationCapabilitySelectErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError1 | InvalidRequestErrorV2
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
}
|
||||
|
||||
export type V2IntegrationCapabilitySelectError =
|
||||
V2IntegrationCapabilitySelectErrors[keyof V2IntegrationCapabilitySelectErrors]
|
||||
|
||||
export type V2IntegrationCapabilitySelectResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2IntegrationCapabilitySelectResponse =
|
||||
V2IntegrationCapabilitySelectResponses[keyof V2IntegrationCapabilitySelectResponses]
|
||||
|
||||
export type V2IntegrationConnectKeyData = {
|
||||
body: {
|
||||
key: string
|
||||
|
|
@ -18658,6 +18616,83 @@ export type V2DebugLocationResponses = {
|
|||
|
||||
export type V2DebugLocationResponse = V2DebugLocationResponses[keyof V2DebugLocationResponses]
|
||||
|
||||
export type V2SearchProviderGetData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
}
|
||||
url: "/api/search/provider"
|
||||
}
|
||||
|
||||
export type V2SearchProviderGetErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestErrorV2
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
}
|
||||
|
||||
export type V2SearchProviderGetError = V2SearchProviderGetErrors[keyof V2SearchProviderGetErrors]
|
||||
|
||||
export type V2SearchProviderGetResponses = {
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
200: {
|
||||
location: LocationInfoV2
|
||||
data: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export type V2SearchProviderGetResponse = V2SearchProviderGetResponses[keyof V2SearchProviderGetResponses]
|
||||
|
||||
export type V2SearchProviderSelectData = {
|
||||
body: {
|
||||
providerID: string
|
||||
}
|
||||
path?: never
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
}
|
||||
url: "/api/search/provider"
|
||||
}
|
||||
|
||||
export type V2SearchProviderSelectErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError1 | InvalidRequestErrorV2
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
/**
|
||||
* ServiceUnavailableError
|
||||
*/
|
||||
503: ServiceUnavailableErrorV2
|
||||
}
|
||||
|
||||
export type V2SearchProviderSelectError = V2SearchProviderSelectErrors[keyof V2SearchProviderSelectErrors]
|
||||
|
||||
export type V2SearchProviderSelectResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2SearchProviderSelectResponse = V2SearchProviderSelectResponses[keyof V2SearchProviderSelectResponses]
|
||||
|
||||
export type V2SearchQueryData = {
|
||||
body: {
|
||||
query: string
|
||||
|
|
|
|||
|
|
@ -33,21 +33,6 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
|||
return yield* response(service.get(ctx.params.integrationID))
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"integration.capability.select",
|
||||
Effect.fn(function* (ctx) {
|
||||
const service = yield* Integration.Service
|
||||
const integration = yield* service.get(ctx.params.integrationID)
|
||||
if (!integration?.capabilities.some((capability) => capability.type === ctx.payload.capability)) {
|
||||
return yield* new InvalidRequestError({
|
||||
message: `Capability not found: ${ctx.payload.capability}`,
|
||||
kind: "integration_capability_not_found",
|
||||
})
|
||||
}
|
||||
yield* service.capability.search.select(ctx.params.integrationID)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"integration.connect.key",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
|
|
|||
|
|
@ -2,61 +2,90 @@ 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 } from "effect/unstable/httpapi"
|
||||
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* () {
|
||||
return handlers.handle(
|
||||
"search.query",
|
||||
Effect.fn("server.search.query")(function* (request) {
|
||||
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",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
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) =>
|
||||
const ready = Effect.fn("server.search.ready")(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* ready()
|
||||
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",
|
||||
}),
|
||||
"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,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"search.query",
|
||||
Effect.fn("server.search.query")(function* (request) {
|
||||
yield* ready()
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ export function DialogIntegration(
|
|||
.map((integration) => {
|
||||
const methods = connectMethods(integration)
|
||||
const connected = integration.connections.length > 0
|
||||
const search = integration.capabilities.find((capability) => capability.type === "search")
|
||||
const search = integration.search
|
||||
const selected = data.location.search.provider() === integration.id
|
||||
const credentials = credentialConnections(integration)
|
||||
const description = search?.connection === "optional" ? "API key optional" : undefined
|
||||
const category = search ? "Web search" : undefined
|
||||
|
|
@ -74,7 +75,7 @@ export function DialogIntegration(
|
|||
value: integration.id,
|
||||
description: description ?? (methods.length === 0 ? "Environment only" : undefined),
|
||||
footer:
|
||||
[connectionSummary(integration), search?.selected ? "Web search default" : undefined]
|
||||
[connectionSummary(integration), selected ? "Web search default" : undefined]
|
||||
.filter((value) => value !== undefined && value.length > 0)
|
||||
.join(" · ") || undefined,
|
||||
category: category ?? (integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services"),
|
||||
|
|
@ -111,7 +112,7 @@ export function DialogIntegration(
|
|||
function manageIntegration(
|
||||
integration: IntegrationInfo,
|
||||
methods: ConnectMethod[],
|
||||
search: IntegrationInfo["capabilities"][number],
|
||||
search: NonNullable<IntegrationInfo["search"]>,
|
||||
dialog: ReturnType<typeof useDialog>,
|
||||
) {
|
||||
const connected = integration.connections.length > 0
|
||||
|
|
@ -120,15 +121,15 @@ function manageIntegration(
|
|||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const credentials = credentialConnections(integration)
|
||||
const selected = () => data.location.search.provider() === integration.id
|
||||
const selectSearch = () => {
|
||||
void sdk.api.integration
|
||||
.selectCapability({
|
||||
integrationID: integration.id,
|
||||
capability: "search",
|
||||
void sdk.api.search
|
||||
.selectProvider({
|
||||
providerID: integration.id,
|
||||
location: location(data),
|
||||
})
|
||||
.then(async () => {
|
||||
await data.location.integration.refresh()
|
||||
await Promise.all([data.location.integration.refresh(), data.location.search.refresh()])
|
||||
toast.show({ variant: "success", message: `${integration.name} is now the web search default` })
|
||||
dialog.clear()
|
||||
})
|
||||
|
|
@ -140,9 +141,9 @@ function manageIntegration(
|
|||
title={integration.name}
|
||||
options={[
|
||||
{
|
||||
title: search.selected ? "Web search default" : "Use for web search",
|
||||
title: selected() ? "Web search default" : "Use for web search",
|
||||
value: "search",
|
||||
disabled: search.selected,
|
||||
disabled: selected(),
|
||||
onSelect:
|
||||
search.connection === "required" && !connected
|
||||
? () => selectMethod(integration, methods, dialog, selectSearch)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ type LocationData = {
|
|||
model?: ModelV2Info[]
|
||||
provider?: ProviderV2Info[]
|
||||
reference?: ReferenceInfo[]
|
||||
searchProvider?: 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>
|
||||
|
|
@ -752,6 +753,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
result.location.provider.refresh(event.location),
|
||||
])
|
||||
break
|
||||
case "config.updated":
|
||||
void result.location.search.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.
|
||||
case "mcp.status.changed":
|
||||
|
|
@ -974,6 +978,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
setStore("location", key, { ...store.location[key], reference: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
search: {
|
||||
provider(location?: LocationRef) {
|
||||
return store.location[locationKey(location ?? defaultLocation())]?.searchProvider ?? undefined
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.api.search.provider({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], searchProvider: result.data ?? null })
|
||||
},
|
||||
},
|
||||
skill: {
|
||||
list(location?: LocationRef) {
|
||||
return store.location[locationKey(location ?? defaultLocation())]?.skill
|
||||
|
|
@ -1014,6 +1028,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.command.refresh(),
|
||||
result.location.skill.refresh(),
|
||||
result.shell.refresh(),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import type {
|
|||
} from "@opencode-ai/sdk/v2"
|
||||
import { useLocal } from "../../context/local"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { selectedWebSearchProvider, webSearchProviderLabel } from "../../util/tool-display"
|
||||
import { webSearchProviderLabel } from "../../util/tool-display"
|
||||
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { useEditorContext } from "../../context/editor"
|
||||
|
|
@ -2346,12 +2346,10 @@ function WebFetch(props: ToolProps) {
|
|||
|
||||
function WebSearch(props: ToolProps) {
|
||||
const data = useData()
|
||||
const [provider, setProvider] = createSignal(
|
||||
selectedWebSearchProvider(data.location.integration.list() ?? []) ?? stringValue(props.metadata.provider),
|
||||
)
|
||||
const [provider, setProvider] = createSignal(data.location.search.provider() ?? stringValue(props.metadata.provider))
|
||||
createEffect(() => {
|
||||
if (provider()) return
|
||||
const next = selectedWebSearchProvider(data.location.integration.list() ?? [])
|
||||
const next = data.location.search.provider()
|
||||
if (next) setProvider(next)
|
||||
})
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,17 +1,9 @@
|
|||
import type { IntegrationInfo } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export function webSearchProviderLabel(provider: unknown) {
|
||||
if (provider === "parallel") return "Parallel Web Search"
|
||||
if (provider === "exa") return "Exa Web Search"
|
||||
return "Web Search"
|
||||
}
|
||||
|
||||
export function selectedWebSearchProvider(integrations: readonly Pick<IntegrationInfo, "id" | "capabilities">[]) {
|
||||
return integrations.find((integration) =>
|
||||
integration.capabilities.some((capability) => capability.type === "search" && capability.selected),
|
||||
)?.id
|
||||
}
|
||||
|
||||
export function toolDisplayMetadata(state: unknown): Record<string, unknown> {
|
||||
if (!state || typeof state !== "object" || Array.isArray(state)) return {}
|
||||
if (!("status" in state) || state.status === "pending") return {}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
|
||||
const integration = (value: Partial<IntegrationInfo> & Pick<IntegrationInfo, "id" | "name">): IntegrationInfo => ({
|
||||
methods: [],
|
||||
capabilities: [],
|
||||
connections: [],
|
||||
...value,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { selectedWebSearchProvider, toolDisplayMetadata, webSearchProviderLabel } from "../../src/util/tool-display"
|
||||
import { toolDisplayMetadata, webSearchProviderLabel } from "../../src/util/tool-display"
|
||||
|
||||
describe("webSearchProviderLabel", () => {
|
||||
test("labels known providers", () => {
|
||||
|
|
@ -21,25 +21,6 @@ describe("webSearchProviderLabel", () => {
|
|||
}
|
||||
})
|
||||
|
||||
describe("selectedWebSearchProvider", () => {
|
||||
test("returns the selected search integration", () => {
|
||||
expect(
|
||||
selectedWebSearchProvider([
|
||||
{ id: "exa", capabilities: [{ type: "search", connection: "optional", selected: false }] },
|
||||
{ id: "parallel", capabilities: [{ type: "search", connection: "optional", selected: true }] },
|
||||
]),
|
||||
).toBe("parallel")
|
||||
})
|
||||
|
||||
test("ignores unselected and unrelated capabilities", () => {
|
||||
expect(
|
||||
selectedWebSearchProvider([
|
||||
{ id: "exa", capabilities: [{ type: "search", connection: "optional", selected: false }] },
|
||||
]),
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("toolDisplayMetadata", () => {
|
||||
test("returns structured metadata for non-pending states", () => {
|
||||
const structured = { provider: "parallel", numResults: 3 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue