@opencode-ai/plugin
Advanced tools
| import type { RpcApi } from "@opencode-ai/client/effect/api"; | ||
| export type { RpcClient } from "@opencode-ai/client/effect/api"; | ||
| import type { Rpc } from "@opencode-ai/schema/rpc"; | ||
| import type { Effect, Scope } from "effect"; | ||
| import type { Registration } from "./registration.js"; | ||
| export interface RpcCallContext<M extends Rpc.Method> { | ||
| readonly error: Rpc.ErrorFactory<M>; | ||
| } | ||
| export type RpcHandlers<D extends Rpc.Definition> = { | ||
| readonly [Name in keyof D["methods"]]: (input: Rpc.Output<D["methods"][Name]["input"]>, context: RpcCallContext<D["methods"][Name]>) => Effect.Effect<Rpc.HandlerOutput<D["methods"][Name]["output"]>, Rpc.HandlerError<D["methods"][Name]>>; | ||
| }; | ||
| export interface RpcRegistration<D extends Rpc.Definition> extends Registration { | ||
| readonly events: { | ||
| readonly emit: (...args: Rpc.EventInput<D>) => Effect.Effect<void, unknown>; | ||
| }; | ||
| } | ||
| export interface RpcDomain extends RpcApi<Rpc.SystemError, never, unknown> { | ||
| readonly register: <const D extends Rpc.Definition>(definition: D, handlers: RpcHandlers<NoInfer<D>>) => Effect.Effect<RpcRegistration<D>, unknown, Scope.Scope>; | ||
| } |
| import type { RpcApi, RpcCallOptions } from "@opencode-ai/client/promise/api"; | ||
| import type { Rpc } from "@opencode-ai/schema/rpc"; | ||
| import type { Registration } from "./registration.js"; | ||
| export type { RpcEventPayload } from "@opencode-ai/client/promise/api"; | ||
| export interface RpcCallContext<M extends Rpc.Method> { | ||
| readonly signal: AbortSignal; | ||
| readonly error: Rpc.ErrorFactory<M>; | ||
| } | ||
| export type RpcHandlers<D extends Rpc.PortableDefinition> = { | ||
| readonly [Name in keyof D["methods"]]: (input: Rpc.Output<D["methods"][Name]["input"]>, context: RpcCallContext<D["methods"][Name]>) => Promise<Rpc.HandlerOutput<D["methods"][Name]["output"]> | Rpc.HandlerError<D["methods"][Name]>>; | ||
| }; | ||
| export interface RpcRegistration<D extends Rpc.PortableDefinition> extends Registration { | ||
| readonly events: { | ||
| readonly emit: (...args: Rpc.EventInput<D>) => Promise<void>; | ||
| }; | ||
| } | ||
| export interface RpcDomain extends RpcApi<Pick<RpcCallOptions, "signal"> & { | ||
| readonly location?: never; | ||
| readonly headers?: never; | ||
| }> { | ||
| readonly register: <const D extends Rpc.PortableDefinition>(definition: D, handlers: RpcHandlers<NoInfer<D>>) => Promise<RpcRegistration<D>>; | ||
| } |
| export { Rpc } from "@opencode-ai/schema/rpc"; |
| export { Rpc } from "@opencode-ai/schema/rpc"; |
@@ -14,4 +14,5 @@ export * as Plugin from "./plugin.js"; | ||
| export { Reference } from "@opencode-ai/schema/reference"; | ||
| export { Rpc } from "@opencode-ai/schema/rpc"; | ||
| export { Skill } from "@opencode-ai/schema/skill"; | ||
| export { Vcs } from "@opencode-ai/schema/vcs"; | ||
| export { WebSearch } from "@opencode-ai/schema/websearch"; |
@@ -13,4 +13,5 @@ export * as Plugin from "./plugin.js"; | ||
| export { Reference } from "@opencode-ai/schema/reference"; | ||
| export { Rpc } from "@opencode-ai/schema/rpc"; | ||
| export { Skill } from "@opencode-ai/schema/skill"; | ||
| export { Vcs } from "@opencode-ai/schema/vcs"; | ||
| export { WebSearch } from "@opencode-ai/schema/websearch"; |
@@ -16,2 +16,3 @@ import type { ExperimentalApi, GenerateApi, PluginApi } from "@opencode-ai/client/effect/api"; | ||
| import type { ReferenceDomain } from "./reference.js"; | ||
| import type { RpcDomain } from "./rpc.js"; | ||
| import type { SessionDomain } from "./session.js"; | ||
@@ -42,2 +43,3 @@ import type { ShellDomain } from "./shell.js"; | ||
| readonly reference: ReferenceDomain; | ||
| readonly rpc: RpcDomain; | ||
| readonly session: SessionDomain; | ||
@@ -53,3 +55,2 @@ readonly shell: ShellDomain; | ||
| readonly id: string; | ||
| readonly tui?: boolean; | ||
| readonly vcs?: VcsDiscovery; | ||
@@ -56,0 +57,0 @@ readonly effect: (context: Context) => Effect.Effect<void, never, R>; |
+102
-2
@@ -6,2 +6,101 @@ import { Tool } from "@opencode-ai/schema/tool"; | ||
| const compiledEndpoints = new WeakMap(); | ||
| class ReturnedRpcError extends Error { | ||
| type; | ||
| data; | ||
| constructor(type, message, data) { | ||
| super(message); | ||
| this.type = type; | ||
| this.data = data; | ||
| } | ||
| } | ||
| const makeStreams = Effect.fn("Plugin.Event.makeStreams")(function* () { | ||
| const context = yield* Effect.context(); | ||
| const subscriptions = new Set(); | ||
| // Async iterators own separate scopes, so close them when the plugin unloads. | ||
| yield* Effect.addFinalizer(() => Effect.promise(() => Promise.all(Array.from(subscriptions, (close) => close())))); | ||
| return ((stream, options) => ({ | ||
| [Symbol.asyncIterator]() { | ||
| const iterator = Stream.toAsyncIterableWith(stream, context)[Symbol.asyncIterator](); | ||
| const close = () => { | ||
| subscriptions.delete(close); | ||
| options?.signal?.removeEventListener("abort", abort); | ||
| return iterator.return?.() ?? Promise.resolve({ done: true, value: undefined }); | ||
| }; | ||
| const abort = () => { | ||
| void close(); | ||
| }; | ||
| subscriptions.add(close); | ||
| options?.signal?.addEventListener("abort", abort, { once: true }); | ||
| if (options?.signal?.aborted) | ||
| abort(); | ||
| return { | ||
| next: () => iterator.next().then((result) => (result.done ? close().then(() => result) : result), (error) => close().then(() => Promise.reject(error))), | ||
| return: close, | ||
| }; | ||
| }, | ||
| })); | ||
| }); | ||
| const rpcFromEffect = Effect.fn("Plugin.Rpc.fromEffect")(function* (host, streams) { | ||
| const context = yield* Effect.context(); | ||
| const run = Effect.runPromiseWith(context); | ||
| const client = (definition) => { | ||
| const local = host(definition); | ||
| const subscribe = (name, options) => streams(local.events.subscribe(name), options); | ||
| return Object.assign(Object.fromEntries(Object.keys(definition.methods).map((name) => [ | ||
| name, | ||
| (input, options) => { | ||
| // SAFETY: The local client was built from this definition, so every declared key is an Effect method. | ||
| // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion | ||
| const method = local[name]; | ||
| return run(method(input), { signal: options?.signal }); | ||
| }, | ||
| ])), { | ||
| events: { | ||
| subscribe, | ||
| on: (name, handler, options) => { | ||
| const controller = new AbortController(); | ||
| const signal = options?.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal; | ||
| void (async () => { | ||
| for await (const event of subscribe(name, { signal })) | ||
| await handler(event); | ||
| })().catch((error) => run(Effect.logError(error))); | ||
| return () => controller.abort(); | ||
| }, | ||
| }, | ||
| }); | ||
| }; | ||
| const register = (definition, handlers) => run(host.register(definition, | ||
| // SAFETY: Each entry preserves its definition key; Core restores that method's erased schema and error types. | ||
| // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion | ||
| Object.fromEntries(Object.entries(handlers).map(([name, handler]) => [ | ||
| name, | ||
| (input, context) => Effect.tryPromise({ | ||
| try: (signal) => { | ||
| // SAFETY: Promise RPC handlers return Promise values before this adapter erases their concrete types. | ||
| // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion | ||
| return Reflect.apply(handler, undefined, [ | ||
| input, | ||
| { | ||
| signal, | ||
| error: (type, message, data) => new ReturnedRpcError(type, message, data), | ||
| }, | ||
| ]); | ||
| }, | ||
| catch: (error) => hostRpcError(context, error), | ||
| }).pipe(Effect.flatMap((result) => result instanceof ReturnedRpcError | ||
| ? Effect.fail(hostRpcError(context, result)) | ||
| : Effect.succeed(result))), | ||
| ])))).then((registration) => ({ | ||
| dispose: () => run(registration.dispose), | ||
| events: { emit: (...args) => run(registration.events.emit(...args)) }, | ||
| })); | ||
| // SAFETY: Client and register implement RpcDomain from the same portable definitions and schema adapters. | ||
| // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion | ||
| return Object.assign(client, { register }); | ||
| }); | ||
| function hostRpcError(context, error) { | ||
| if (!(error instanceof ReturnedRpcError)) | ||
| return error; | ||
| return context.error(error.type, error.message, error.data); | ||
| } | ||
| function compileEndpoint(endpoint) { | ||
@@ -56,3 +155,2 @@ const cached = compiledEndpoints.get(endpoint); | ||
| id: plugin.id, | ||
| tui: plugin.tui, | ||
| vcs: plugin.vcs, | ||
@@ -77,2 +175,3 @@ effect: (host) => Effect.gen(function* () { | ||
| const context = yield* Effect.context(); | ||
| const streams = yield* makeStreams(); | ||
| // Run a hook registration on the plugin scope and resolve once it is registered. | ||
@@ -136,3 +235,3 @@ const register = (effect) => Effect.runPromiseWith(context)(effect).then((registration) => ({ | ||
| event: { | ||
| subscribe: () => Stream.toAsyncIterable(host.event.subscribe().pipe(Stream.mapEffect((event) => Schema.encodeUnknownEffect(OpenCodeEvent)(event)), Stream.map((event) => event))), | ||
| subscribe: (options) => streams(host.event.subscribe().pipe(Stream.mapEffect((event) => Schema.encodeUnknownEffect(OpenCodeEvent)(event)), Stream.map((event) => event)), options), | ||
| }, | ||
@@ -219,2 +318,3 @@ experimental: { | ||
| }, | ||
| rpc: yield* rpcFromEffect(host.rpc, streams), | ||
| skill: { | ||
@@ -221,0 +321,0 @@ list: adaptApiMethod(SkillEndpoints["skill.list"], host.skill.list), |
@@ -15,4 +15,5 @@ export type { PluginOptions } from "../options.js"; | ||
| export { Reference } from "@opencode-ai/schema/reference"; | ||
| export { Rpc } from "@opencode-ai/schema/rpc"; | ||
| export { Skill } from "@opencode-ai/schema/skill"; | ||
| export { Vcs } from "@opencode-ai/schema/vcs"; | ||
| export { WebSearch } from "@opencode-ai/schema/websearch"; |
@@ -13,4 +13,5 @@ export * as Plugin from "./plugin.js"; | ||
| export { Reference } from "@opencode-ai/schema/reference"; | ||
| export { Rpc } from "@opencode-ai/schema/rpc"; | ||
| export { Skill } from "@opencode-ai/schema/skill"; | ||
| export { Vcs } from "@opencode-ai/schema/vcs"; | ||
| export { WebSearch } from "@opencode-ai/schema/websearch"; |
@@ -16,2 +16,3 @@ import type { OpenCodeClient } from "@opencode-ai/client"; | ||
| import type { ReferenceDomain } from "./reference.js"; | ||
| import type { RpcDomain } from "./rpc.js"; | ||
| import type { SessionDomain } from "./session.js"; | ||
@@ -42,2 +43,3 @@ import type { ShellDomain } from "./shell.js"; | ||
| readonly reference: ReferenceDomain; | ||
| readonly rpc: RpcDomain; | ||
| readonly session: SessionDomain; | ||
@@ -54,3 +56,2 @@ readonly shell: ShellDomain; | ||
| readonly id: string; | ||
| readonly tui?: boolean; | ||
| readonly vcs?: VcsDiscovery; | ||
@@ -57,0 +58,0 @@ readonly setup: (context: Context) => Promise<Cleanup | void> | Cleanup | void; |
@@ -28,6 +28,9 @@ import type { AgentInfo, CommandInfo, FormCancelInput, FormInfo, FormReplyInput, IntegrationInfo, LocationRef, McpResource, McpServer, ModelInfo, OpenCodeClient, OpenCodeEvent, PermissionSavedInfo, PermissionRequest, Project, ProviderInfo, ReferenceInfo, SessionInfo, SessionMessageInfo, SessionInboxInfo, ShellInfo, SkillInfo, VcsInfo } from "@opencode-ai/client"; | ||
| } | ||
| type OpenCodeEventMap = { | ||
| [Type in OpenCodeEvent["type"]]: Extract<OpenCodeEvent, { | ||
| type: Type; | ||
| }>; | ||
| }; | ||
| export interface Data { | ||
| readonly on: <Type extends OpenCodeEvent["type"]>(type: Type, handler: (event: Extract<OpenCodeEvent, { | ||
| type: Type; | ||
| }>) => void) => () => void; | ||
| readonly on: <Type extends OpenCodeEvent["type"]>(type: Type, handler: (event: OpenCodeEventMap[Type]) => void) => () => void; | ||
| readonly listen: (handler: (event: { | ||
@@ -34,0 +37,0 @@ details: OpenCodeEvent; |
+8
-8
| { | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "name": "@opencode-ai/plugin", | ||
| "version": "0.0.0-dev-18706", | ||
| "version": "0.0.0-dev-18708", | ||
| "type": "module", | ||
@@ -9,3 +9,3 @@ "license": "MIT", | ||
| "test": "bun test --timeout 5000", | ||
| "typecheck": "tsgo --noEmit", | ||
| "typecheck": "tsgo --noEmit -p tsconfig.tests.json", | ||
| "build": "tsc -p tsconfig.build.json" | ||
@@ -36,6 +36,6 @@ }, | ||
| "@ai-sdk/provider": "3.0.8", | ||
| "@opencode-ai/ai": "0.0.0-dev-18706", | ||
| "@opencode-ai/client": "0.0.0-dev-18706", | ||
| "@opencode-ai/protocol": "0.0.0-dev-18706", | ||
| "@opencode-ai/schema": "0.0.0-dev-18706", | ||
| "@opencode-ai/ai": "0.0.0-dev-18708", | ||
| "@opencode-ai/client": "0.0.0-dev-18708", | ||
| "@opencode-ai/protocol": "0.0.0-dev-18708", | ||
| "@opencode-ai/schema": "0.0.0-dev-18708", | ||
| "@standard-schema/spec": "1.1.0", | ||
@@ -46,3 +46,3 @@ "effect": "4.0.0-rc.112", | ||
| "peerDependencies": { | ||
| "@opencode-ai/theme": "0.0.0-dev-18706", | ||
| "@opencode-ai/theme": "0.0.0-dev-18708", | ||
| "@opentui/core": ">=0.5.9", | ||
@@ -67,3 +67,3 @@ "@opentui/solid": ">=0.5.9", | ||
| "devDependencies": { | ||
| "@opencode-ai/theme": "0.0.0-dev-18706", | ||
| "@opencode-ai/theme": "0.0.0-dev-18708", | ||
| "@opentui/core": "0.5.9", | ||
@@ -70,0 +70,0 @@ "@opentui/solid": "0.5.9", |
94924
8.71%103
6.19%2100
7.8%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed