opencode/packages/core/test/plugin/host.ts
Shoubhit Dash fbe6e5c0d9 Merge remote-tracking branch 'origin/v2' into search-integration
# Conflicts:
#	packages/client/src/promise/api.ts
#	packages/client/src/promise/generated/types.ts
#	packages/core/src/form.ts
#	packages/core/src/plugin/promise.ts
#	packages/core/test/plugin/promise.test.ts
#	packages/plugin/test/contract-identity.test.ts
#	packages/sdk-next/test/contract-identity.test.ts
#	packages/tui/src/component/dialog-integration.tsx
#	packages/tui/src/routes/session/form.tsx
#	packages/tui/test/fixture/tui-client.ts
2026-07-14 18:40:07 +05:30

379 lines
14 KiB
TypeScript

import { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { IntegrationDefinition, IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { WebSearch } from "@opencode-ai/core/websearch"
import type {
CredentialOAuth,
IntegrationEnvMethod,
IntegrationKeyMethod,
IntegrationOAuthMethod,
} from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
type Overrides = Partial<Omit<Plugin.Context, "options">>
export function host(overrides: Overrides = {}): Plugin.Context {
return {
options: {},
agent: overrides.agent ?? {
list: () => Effect.die("unused agent.list"),
transform: () => Effect.die("unused agent.transform"),
reload: () => Effect.die("unused agent.reload"),
},
aisdk: overrides.aisdk ?? {
hook: () => Effect.die("unused aisdk.hook"),
},
catalog: overrides.catalog ?? {
provider: {
list: () => Effect.die("unused catalog.provider.list"),
get: () => Effect.die("unused catalog.provider.get"),
},
model: {
list: () => Effect.die("unused catalog.model.list"),
default: () => Effect.die("unused catalog.model.default"),
},
transform: () => Effect.die("unused catalog.transform"),
reload: () => Effect.die("unused catalog.reload"),
},
command: overrides.command ?? {
list: () => Effect.die("unused command.list"),
transform: () => Effect.die("unused command.transform"),
reload: () => Effect.die("unused command.reload"),
},
event: overrides.event ?? {
subscribe: () => Stream.empty,
},
integration: overrides.integration ?? {
list: () => Effect.die("unused integration.list"),
get: () => Effect.die("unused integration.get"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
},
register: () => Effect.die("unused integration.register"),
transform: () => Effect.die("unused integration.transform"),
reload: () => Effect.die("unused integration.reload"),
connection: {
active: () => Effect.die("unused integration.connection.active"),
resolve: () => Effect.die("unused integration.connection.resolve"),
},
},
plugin: overrides.plugin ?? {
list: () => Effect.die("unused plugin.list"),
},
reference: overrides.reference ?? {
list: () => Effect.die("unused reference.list"),
transform: () => Effect.die("unused reference.transform"),
reload: () => Effect.die("unused reference.reload"),
},
skill: overrides.skill ?? {
list: () => Effect.die("unused skill.list"),
transform: () => Effect.die("unused skill.transform"),
reload: () => Effect.die("unused skill.reload"),
},
tool: overrides.tool ?? {
transform: () => Effect.die("unused tool.transform"),
hook: () => Effect.die("unused tool.hook"),
},
websearch: overrides.websearch ?? {
register: () => Effect.die("unused websearch.register"),
},
session: overrides.session ?? {
create: () => Effect.die("unused session.create"),
get: () => Effect.die("unused session.get"),
prompt: () => Effect.die("unused session.prompt"),
command: () => Effect.die("unused session.command"),
interrupt: () => Effect.die("unused session.interrupt"),
},
}
}
export function agentHost(agent: AgentV2.Interface): Plugin.Context["agent"] {
return {
list: () => Effect.die("unused agent.list"),
reload: agent.reload,
transform: (callback) =>
agent.transform((draft) =>
callback({
list: () => draft.list().map(agentInfo),
get: (id) => {
const value = draft.get(AgentV2.ID.make(id))
return value && agentInfo(value)
},
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) =>
draft.update(AgentV2.ID.make(id), (value) => {
const current = agentInfo(value)
update(current)
Object.assign(value, current, { id: AgentV2.ID.make(current.id) })
}),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
}),
),
}
}
export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog"] {
return {
provider: {
list: () => Effect.die("unused catalog.provider.list"),
get: () => Effect.die("unused catalog.provider.get"),
},
model: {
list: () => Effect.die("unused catalog.model.list"),
default: () => Effect.die("unused catalog.model.default"),
},
reload: catalog.reload,
transform: (callback) =>
catalog.transform((draft) =>
callback({
provider: {
list: () =>
draft.provider.list().map((value) => ({
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
})),
get: (id) => {
const value = draft.provider.get(ProviderV2.ID.make(id))
return (
value && {
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
}
)
},
update: (id, update) =>
draft.provider.update(ProviderV2.ID.make(id), (value) => {
const current = providerInfo(value)
update(current)
Object.assign(value, current, { id: ProviderV2.ID.make(current.id) })
}),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
},
model: {
get: (providerID, modelID) => {
const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))
return value && modelInfo(value)
},
update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => {
const current = modelInfo(value)
update(current)
Object.assign(value, current, {
id: ModelV2.ID.make(current.id),
providerID: ProviderV2.ID.make(current.providerID),
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
variants: current.variants?.map((variant) => ({
...variant,
id: ModelV2.VariantID.make(variant.id),
})),
})
}),
remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
default: {
get: () => {
const value = draft.model.default.get()
return value && { providerID: value.providerID, modelID: value.modelID }
},
set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
},
},
}),
),
}
}
export function integrationHost(integration: Integration.Interface): Plugin.Context["integration"] {
return {
list: () => Effect.die("unused integration.list"),
get: () => Effect.die("unused integration.get"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
},
reload: integration.reload,
connection: {
active: (id) => integration.connection.active(Integration.ID.make(id)),
resolve: (connection) =>
integration.connection.resolve(
connection.type === "credential" ? { ...connection, id: Credential.ID.make(connection.id) } : connection,
),
},
register: (definition) => integration.transform((draft) => registerIntegration(draft, definition)),
transform: (callback) =>
integration.transform((draft) =>
callback({
list: () => draft.list().map((value) => ({ id: value.id, name: value.name })),
get: (id) => {
const value = draft.get(Integration.ID.make(id))
return value && { id: value.id, name: value.name }
},
update: (id, update) => draft.update(Integration.ID.make(id), update),
remove: (id) => draft.remove(Integration.ID.make(id)),
method: {
list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
update: (input) => draft.method.update(methodImplementation(input)),
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
},
}),
),
}
}
export function webSearchHost(websearch: WebSearch.Interface): Plugin.Context["websearch"] {
return {
register: (definition) =>
websearch.register({
id: WebSearch.ID.make(definition.id),
name: definition.name,
execute: definition.execute,
}),
}
}
function registerIntegration(draft: Integration.Draft, definition: IntegrationDefinition) {
const integrationID = Integration.ID.make(definition.id)
draft.update(integrationID, (integration) => (integration.name = definition.name))
for (const item of definition.methods ?? []) {
if (item.type === "env") {
draft.method.update(methodImplementation({ integrationID: definition.id, method: item }))
continue
}
if (item.type === "key") {
draft.method.update(methodImplementation({ integrationID: definition.id, method: item }))
continue
}
const { authorize, refresh, credentialLabel, ...method } = item
draft.method.update(
methodImplementation({
integrationID: definition.id,
method,
authorize,
...(refresh ? { refresh } : {}),
...(credentialLabel ? { label: credentialLabel } : {}),
}),
)
}
}
function methodImplementation(input: IntegrationMethodRegistration): Integration.Implementation {
if ("authorize" in input) {
const refresh = input.refresh
return {
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, id: Integration.MethodID.make(input.method.id) },
authorize: (inputs) =>
input.authorize(inputs).pipe(
Effect.map((authorization) => {
if (authorization.mode === "auto") {
return { ...authorization, callback: authorization.callback.pipe(Effect.map(oauthCredential)) }
}
return {
...authorization,
callback: (code: string) => authorization.callback(code).pipe(Effect.map(oauthCredential)),
}
}),
),
...(refresh ? { refresh: (value: Credential.OAuth) => refresh(value).pipe(Effect.map(oauthCredential)) } : {}),
...(input.label ? { label: input.label } : {}),
}
}
if (input.method.type === "env") {
return {
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, names: [...input.method.names] },
}
}
return { integrationID: Integration.ID.make(input.integrationID), method: input.method }
}
function oauthCredential(value: CredentialOAuth) {
return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) })
}
function method(value: Integration.Method) {
if (value.type === "env") return { type: value.type, names: [...value.names] }
if (value.type === "key") return { type: value.type, label: value.label }
return {
type: value.type,
id: value.id,
label: value.label,
prompts: value.prompts?.map((prompt) => {
if (prompt.type === "text") return { ...prompt }
return { ...prompt, options: prompt.options.map((option) => ({ ...option })) }
}),
}
}
function internalMethod(
value: IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod,
): Integration.Method {
if (value.type === "env") return value
if (value.type === "key") return value
return {
...value,
id: Integration.MethodID.make(value.id),
}
}
function agentInfo(value: AgentV2.Info) {
return {
...value,
model: value.model && { ...value.model },
request: {
settings: { ...value.request.settings },
headers: { ...value.request.headers },
body: { ...value.request.body },
},
permissions: value.permissions.map((permission) => ({ ...permission })),
}
}
function providerInfo(value: ProviderV2.MutableInfo) {
return {
...value,
settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
}
}
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return {
...value,
settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
capabilities: {
...value.capabilities,
input: [...value.capabilities.input],
output: [...value.capabilities.output],
},
variants: value.variants?.map((variant) => ({
...variant,
settings: variant.settings && { ...variant.settings },
headers: variant.headers && { ...variant.headers },
body: variant.body && { ...variant.body },
})),
time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
limit: { ...value.limit },
}
}