Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659export * as PluginHost from "./host.js"
import { Plugin } from "@opencode/plugin/effect"import type { IntegrationMethodRegistration } from "@opencode/plugin/effect/integration"import { EventManifest } from "@opencode/schema/event-manifest"import type { Event } from "@opencode/schema/event"import { ServerConfig } from "@opencode/schema/mcp"import { App } from "../app.js"import { Effect, Schema, Stream } from "effect"import { Agent } from "../agent.js"import { AISDK } from "../aisdk.js"import { Command } from "../command.js"import { Credential } from "../credential.js"import { Bus } from "../bus.js"import { Integration } from "../integration.js"import { KV } from "../kv.js"import { Location } from "../location.js"import { LocationServiceMap } from "../location-service-map.js"import { Model } from "../model.js"import { Mcp } from "../mcp/index.js"import { Session } from "../session.js"import { PersistentPty } from "../persistent-pty.js"import { Provider } from "../provider.js"import { Reference } from "../reference.js"import { Rpc } from "../rpc.js"import { AbsolutePath, type DeepMutable } from "../schema.js"import { Skill } from "../skill.js"import { Tool } from "../tool.js"import { Workspace } from "../workspace.js"import { Vcs } from "../vcs.js"import { WebSearch } from "../websearch.js"import { Worktree } from "../worktree.js"import { WorktreeStrategies } from "../worktree/strategies.js"import { Generate } from "../generate.js"import { Permission } from "../permission.js"import { PluginHooks } from "./hooks.js"import type { Interface } from "../plugin.js"import { LayerNode } from "@opencode/util/effect/layer-node"
const mutable = <T>(value: T) => value as DeepMutable<T>type RpcEvent = Event.Payload & { readonly type: `rpc.${string}` readonly location: Location.Ref readonly data: Readonly<Record<string, unknown>>}const isRpcEvent = (event: Event.Payload): event is RpcEvent => event.type.startsWith("rpc.")export const make = Effect.fn("PluginHost.make")(function* ( plugin: Pick<Interface, "list">, pluginID: string = "test",) { const app = yield* App.Metadata const agents = yield* Agent.Service const aisdk = yield* AISDK.Service const providers = yield* Provider.Service const models = yield* Model.Service const commands = yield* Command.Service const bus = yield* Bus.Service const integration = yield* Integration.Service const kv = yield* KV.Service const mcp = yield* Mcp.Service const location = yield* Location.Service const reference = yield* Reference.Service const rpc = yield* Rpc.Service const skill = yield* Skill.Service const tools = yield* Tool.Service const vcs = yield* Vcs.Service const websearch = yield* WebSearch.Service const generate = yield* Generate.Service const permission = yield* Permission.Service const hooks = yield* PluginHooks.Service const sessions = yield* Session.Service const persistentPty = yield* PersistentPty.Service const locations = yield* LocationServiceMap.Service const worktrees = yield* Worktree.Service const worktreeStrategies = yield* WorktreeStrategies.Service const currentWorktreeStrategies = location.workspaceID ? undefined : worktreeStrategies const locationInfo = () => new Location.Info({ directory: location.directory, workspaceID: location.workspaceID, project: location.project, }) const locationRef = (input?: { readonly location?: { readonly directory?: string; readonly workspace?: string } }) => input?.location === undefined ? undefined : Location.Ref.make({ directory: AbsolutePath.make(input.location.directory ?? location.directory), workspaceID: input.location.workspace === undefined ? location.workspaceID : Workspace.ID.make(input.location.workspace), }) const isCurrentLocation = (ref: Location.Ref) => ref.directory === location.directory && ref.workspaceID === location.workspaceID const response = <A, E, R>(effect: Effect.Effect<A, E, R>) => effect.pipe(Effect.map((data) => ({ location: locationInfo(), data })))
const decodeWorktree = Schema.decodeUnknownEffect(Worktree.Info) const decodeWorktrees = Schema.decodeUnknownEffect(Schema.Array(Worktree.ListEntry))
const listAgents = Effect.fn("PluginHost.listAgents")((ref: Location.Ref) => Effect.gen(function* () { const location = yield* Location.Service const agents = yield* Agent.Service return { location: new Location.Info({ directory: location.directory, workspaceID: location.workspaceID, project: location.project, }), data: yield* agents.list(), } }).pipe(Effect.provide(locations.get(ref)), Effect.orDie), )
// Keep the instance graph's inferred types independent of Session handles. const context: Plugin.Context = { app, location: locationInfo(), options: {}, rpc: Object.assign(rpc.client, { register: rpc.register }), agent: { get: (input) => { const ref = locationRef(input) const output = ref && !isCurrentLocation(ref) ? listAgents(ref).pipe( Effect.map((result) => ({ ...result, data: result.data.find((agent) => agent.id === input.agentID), })), ) : response(agents.get(input.agentID)) return output.pipe( Effect.flatMap((result) => result.data ? Effect.succeed({ ...result, data: result.data }) : Effect.fail(new Error(`Agent not found: ${input.agentID}`)), ), ) }, list: (input) => { const ref = locationRef(input) if (ref && !isCurrentLocation(ref)) return listAgents(ref) return response(agents.list()) }, reload: agents.reload, transform: (callback) => agents.transform((editor) => { callback({ list: () => mutable(editor.list()), get: (id) => mutable(editor.get(Agent.ID.make(id))), default: (id) => editor.default(id === undefined ? undefined : Agent.ID.make(id)), update: (id, update) => editor.update(Agent.ID.make(id), update), remove: (id) => editor.remove(Agent.ID.make(id)), }) }), }, aisdk: { hook: (name, callback, options) => { if (name === "sdk") { return aisdk.hook.sdk((event) => { if (options?.providerID !== undefined && options.providerID !== event.model.providerID) return Effect.void const output = { model: mutable(event.model), package: event.package, options: event.options, sdk: event.sdk, } // oxlint-disable-next-line no-restricted-globals -- The generic hook callback remains a union after narrowing by hook name. return Reflect.apply(callback, undefined, [output]).pipe( Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))), ) }) } return aisdk.hook.language((event) => { if (options?.providerID !== undefined && options.providerID !== event.model.providerID) return Effect.void const output = { model: mutable(event.model), options: event.options, sdk: event.sdk, language: event.language, } // oxlint-disable-next-line no-restricted-globals -- The generic hook callback remains a union after narrowing by hook name. return Reflect.apply(callback, undefined, [output]).pipe( Effect.tap(() => Effect.sync(() => (event.language = output.language))), ) }) }, }, provider: { list: () => response(providers.available()), get: (input) => providers .get(Provider.ID.make(input.providerID)) .pipe( Effect.flatMap((provider) => provider === undefined ? Effect.fail(new Error(`Provider not found: ${input.providerID}`)) : response(Effect.succeed(provider)), ), ), reload: providers.reload, transform: (callback) => providers.transform((editor) => { callback({ list: editor.list, get: (id) => editor.get(Provider.ID.make(id)), add: (definition) => editor.add({ ...definition, sourceConnection: definition.sourceConnection?.type === "credential" ? { ...definition.sourceConnection, id: Credential.ID.make(definition.sourceConnection.id) } : definition.sourceConnection, }), update: (id, update) => editor.update(Provider.ID.make(id), update), remove: (id) => editor.remove(Provider.ID.make(id)), models: { set: (id, models) => editor.models.set(Provider.ID.make(id), models), update: (providerID, modelID, update) => editor.models.update(Provider.ID.make(providerID), Model.ID.make(modelID), update), remove: (providerID, modelID) => editor.models.remove(Provider.ID.make(providerID), Model.ID.make(modelID)), }, }) }), }, model: { list: () => response(models.available()), default: () => response(models.default()), reload: models.reload, transform: (callback) => models.transform((editor) => callback({ list: (providerID) => editor.list(providerID === undefined ? undefined : Provider.ID.make(providerID)), get: (providerID, modelID) => editor.get(Provider.ID.make(providerID), Model.ID.make(modelID)), update: (providerID, modelID, update) => editor.update(Provider.ID.make(providerID), Model.ID.make(modelID), update), remove: (providerID, modelID) => editor.remove(Provider.ID.make(providerID), Model.ID.make(modelID)), default: { get: editor.default.get, set: (providerID, modelID) => editor.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)), }, provider: { list: editor.provider.list, get: (id) => editor.provider.get(Provider.ID.make(id)), }, }), ), }, command: { list: () => response(commands.list()), reload: commands.reload, transform: commands.transform, }, event: { subscribe: () => bus .subscribe() .pipe( Stream.filter( (event): event is EventManifest.ServerEvent | RpcEvent => EventManifest.isServer(event) || isRpcEvent(event), ), ), }, experimental: { terminal: { read: (input) => persistentPty.read(input.sessionID, input.lines), }, }, generate: { text: (input) => generate.text(input).pipe(Effect.map((text) => ({ text }))), }, integration: { list: () => response(integration.list()), get: Effect.fn(function* (input) { const item = yield* integration.get(Integration.ID.make(input.integrationID)) if (!item) return yield* Effect.fail(new Error(`Integration not found: ${input.integrationID}`)) return yield* response(Effect.succeed(item)) }), connect: { key: (input) => integration.connection.key({ integrationID: Integration.ID.make(input.integrationID), key: input.key, answer: input.answer, label: input.label, }), }, oauth: { connect: (input) => response( integration.oauth.connect({ integrationID: Integration.ID.make(input.integrationID), methodID: Integration.MethodID.make(input.methodID), answer: input.answer, label: input.label, }), ), status: (input) => response( integration.oauth.status({ integrationID: Integration.ID.make(input.integrationID), attemptID: Integration.AttemptID.make(input.attemptID), }), ), complete: (input) => integration.oauth.complete({ integrationID: Integration.ID.make(input.integrationID), attemptID: Integration.AttemptID.make(input.attemptID), code: input.code, }), cancel: (input) => integration.oauth.cancel({ integrationID: Integration.ID.make(input.integrationID), attemptID: Integration.AttemptID.make(input.attemptID), }), }, command: { connect: (input) => response( integration.command.connect({ integrationID: Integration.ID.make(input.integrationID), methodID: Integration.MethodID.make(input.methodID), label: input.label, }), ), status: (input) => response( integration.command.status({ integrationID: Integration.ID.make(input.integrationID), attemptID: Integration.AttemptID.make(input.attemptID), }), ), cancel: (input) => integration.command.cancel({ integrationID: Integration.ID.make(input.integrationID), attemptID: Integration.AttemptID.make(input.attemptID), }), }, 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, ), }, transform: (callback) => integration.transform((editor) => { callback({ list: () => mutable(editor.list()), get: (id) => mutable(editor.get(Integration.ID.make(id))), update: (id, update) => editor.update(Integration.ID.make(id), update), remove: (id) => editor.remove(Integration.ID.make(id)), method: { list: (id) => editor.method.list(Integration.ID.make(id)), update: (input) => editor.method.update(methodImplementation(input)), remove: (id, method) => editor.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)), }, }) }), }, mcp: { list: (input) => { const ref = locationRef(input) if (ref && !isCurrentLocation(ref)) return Effect.gen(function* () { const location = yield* Location.Service const mcp = yield* Mcp.Service return { location: new Location.Info({ directory: location.directory, workspaceID: location.workspaceID, project: location.project, }), data: yield* mcp.servers(), } }).pipe(Effect.provide(locations.get(ref))) return response(mcp.servers()) }, reload: mcp.reload, transform: (callback) => mcp.transform((editor) => { callback({ list: () => editor.list().map(([name, config]) => [name, mutable(config)]), get: (name) => mutable(editor.get(name)), set: (name, config) => editor.set(name, Schema.decodeUnknownSync(ServerConfig)(config)), update: editor.update, remove: editor.remove, }) }), }, permission: { hook: (name, callback) => hooks.register("permission", name, callback), list: (input) => permission.forSession(input.sessionID), get: (input) => permission .get(input.requestID) .pipe( Effect.flatMap((request) => request?.sessionID === input.sessionID ? Effect.succeed(request) : Effect.fail(new Error(`Permission request not found: ${input.requestID}`)), ), ), reply: (input) => permission .get(input.requestID) .pipe( Effect.flatMap((request) => request?.sessionID === input.sessionID ? permission.reply({ requestID: input.requestID, reply: input.decision, message: input.message }) : Effect.fail(new Error(`Permission request not found: ${input.requestID}`)), ), ), }, plugin: { list: () => response(plugin.list()), }, reference: { list: () => response(reference.list()), reload: reference.reload, transform: (callback) => reference.transform((editor) => { callback({ add: (name, source) => editor.add(name, Schema.decodeUnknownSync(Reference.Source)(source)), remove: editor.remove, list: editor.list, get: editor.get, }) }), }, skill: { list: () => response(skill.list()), reload: skill.reload, transform: (callback) => skill.transform((editor) => { callback({ list: () => mutable(editor.list()), get: editor.get, add: (value) => editor.add(Schema.decodeUnknownSync(Skill.Info)(value)), update: editor.update, remove: editor.remove, }) }), }, storage: storage(kv, pluginID), shell: { hook: (name, callback) => hooks.register("shell", name, callback), }, tool: { transform: tools.transform, reload: tools.reload, list: tools.list, hook: (name, callback) => hooks.register("tool", name, callback), }, vcs: { get: () => response(vcs.info()), base: () => response(vcs.base()), branch: { list: (input) => response(vcs.branches({ search: input?.search, limit: input?.limit })), }, status: () => response(vcs.status()), diff: (input) => response(vcs.diff(input.mode, { context: input.context, base: input.base })), transform: vcs.transform, reload: vcs.reload, }, websearch: { providers: () => response(websearch.providers()), query: (input) => response( websearch.query({ query: input.query, providerID: input.providerID === undefined ? undefined : WebSearch.ID.make(input.providerID), }), ), reload: websearch.reload, transform: (callback) => websearch.transform((editor) => { callback({ add: (definition) => editor.add({ id: WebSearch.ID.make(definition.id), name: definition.name, execute: definition.execute, }), default: { get: editor.default.get, set: (selection) => editor.default.set( selection === false || selection === "random" ? selection : WebSearch.ID.make(selection), ), }, }) }), }, worktree: { list: worktrees.list, create: (input) => worktrees.create(input, currentWorktreeStrategies), refresh: (input) => worktrees.refresh(input, currentWorktreeStrategies).pipe(Effect.asVoid), remove: (input) => worktrees.remove(input, currentWorktreeStrategies), reload: worktreeStrategies.reload, transform: (callback) => worktreeStrategies.transform((editor) => callback({ add: (definition) => editor.add({ id: Worktree.StrategyID.make(definition.id), create: (input) => definition.create(input).pipe(Effect.flatMap(decodeWorktree)), remove: (input) => definition.remove(input), list: (directory) => definition.list(directory).pipe(Effect.flatMap(decodeWorktrees)), }), }), ), }, session: { hook: (name, callback, options) => hooks.register("session", name, callback, options), create: (input) => sessions.create({ id: input?.id, title: input?.title, agent: input?.agent, model: input?.model, metadata: input?.metadata, permissions: input?.permissions, location: input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }), }), get: (input) => sessions.get(input.sessionID), switchAgent: sessions.switchAgent, switchModel: sessions.switchModel, prompt: sessions.prompt, generate: (input) => sessions.generate(input).pipe(Effect.map((text) => ({ text }))), command: (input) => sessions.command({ ...input, command: input.name }), update: Effect.fn(function* (input) { yield* sessions.get(input.sessionID) if (input.title !== undefined) yield* sessions.rename({ sessionID: input.sessionID, title: input.title }) if (input.permissions !== undefined) yield* sessions.setPermissions({ sessionID: input.sessionID, permissions: input.permissions }) }), move: sessions.move, synthetic: sessions.synthetic, interrupt: (input) => sessions .interrupt(input.sessionID, { resume: input.resume }) .pipe(Effect.map((interrupted) => ({ interrupted }))), wait: (input) => sessions.wait(input.sessionID), context: (input) => sessions.context(input.sessionID), }, } return context})
export const requirements = LayerNode.group([ App.node, Agent.node, AISDK.node, Provider.node, Model.node, Command.node, Bus.node, Integration.node, KV.node, Mcp.node, Location.node, Reference.node, Rpc.node, Skill.node, Tool.node, Vcs.node, WebSearch.node, Worktree.node, WorktreeStrategies.node, Generate.node, Permission.node, PluginHooks.node, Session.node, PersistentPty.node, LocationServiceMap.node,])
export function storage(kv: KV.Interface, pluginID: string): Plugin.Context["storage"] { const namespace = `plugin:${pluginID .split("") .map((value) => value.charCodeAt(0).toString(16).padStart(4, "0")) .join("")}:` return { get: (key) => kv.get(namespace + key), set: (key, value) => kv.set(namespace + key, value), remove: (key) => kv.remove(namespace + key), scan: (options) => kv .scan({ prefix: namespace + options.prefix, after: options.after === undefined ? undefined : namespace + options.after, limit: options.limit, }) .pipe( Effect.map((result) => { const entries = result.entries.map((entry) => ({ key: entry.key.slice(namespace.length), value: entry.value, })) if (result.next === undefined) return { entries } return { entries, next: result.next.slice(namespace.length) } }), ), }}
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: (answer) => input.authorize(answer).pipe( Effect.map((authorization) => { if (authorization.mode === "auto") { return { ...authorization, callback: authorization.callback.pipe(Effect.map(credential)), } } return { ...authorization, callback: (code: string) => authorization.callback(code).pipe(Effect.map(credential)), } }), ), ...(refresh ? { refresh: (value: Credential.OAuth) => refresh(value).pipe(Effect.map(credential)) } : {}), ...(input.label ? { label: input.label } : {}), } } if (input.method.type === "env") { return { integrationID: Integration.ID.make(input.integrationID), method: input.method, } } if (input.method.type === "command") { return { integrationID: Integration.ID.make(input.integrationID), method: { ...input.method, id: Integration.MethodID.make(input.method.id) }, } } return { integrationID: Integration.ID.make(input.integrationID), method: input.method, }}
function credential(value: Credential.OAuth) { return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) })}