export * as PluginV2 from "./plugin" import { createDraft, finishDraft, type Draft } from "immer" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Context, Effect, Exit, Layer, PubSub, Schema, Scope, Stream } from "effect" import type { ModelV2 } from "./model" import type { AgentV2 } from "./agent" import type { Catalog } from "./catalog" export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export type ID = typeof ID.Type type HookSpec = { "catalog.transform": { input: Catalog.Context output: {} } "account.switched": { input: { serviceID: import("./account").AccountV2.ServiceID from?: import("./account").AccountV2.ID to?: import("./account").AccountV2.ID } output: {} } "aisdk.language": { input: { model: ModelV2.Info sdk: any options: Record } output: { language?: LanguageModelV3 } } "aisdk.sdk": { input: { model: ModelV2.Info package: string options: Record } output: { sdk?: any } } "agent.update": { input: {} output: { agent: AgentV2.Info cancel: boolean } } "agent.remove": { input: { agent: AgentV2.Info } output: { cancel: boolean } } "agent.default": { input: {} output: { agent?: AgentV2.ID } } } export type Hooks = { [Name in keyof HookSpec]: Readonly & { -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object ? Draft : HookSpec[Name]["output"][Field] } } export type HookFunctions = { [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect } export type HookInput = HookSpec[Name]["input"] export type HookOutput = HookSpec[Name]["output"] export type Effect = Effect.Effect export function define(input: { id: ID; effect: Effect.Effect }) { return input } export interface Interface { readonly add: (input: { id: ID effect: Effect.Effect }) => Effect.Effect readonly remove: (id: ID) => Effect.Effect readonly added: () => Stream.Stream readonly triggerFor: ( id: ID, name: Name, input: HookInput, output: HookOutput, ) => Effect.Effect & HookOutput> readonly trigger: ( name: Name, input: HookInput, output: HookOutput, ) => Effect.Effect & HookOutput> } export class Service extends Context.Service()("@opencode/v2/Plugin") {} export const layer = Layer.effect( Service, Effect.gen(function* () { let hooks: { id: ID hooks: HookFunctions scope: Scope.Closeable }[] = [] const added = yield* PubSub.unbounded() yield* Effect.addFinalizer(() => PubSub.shutdown(added)) const svc = Service.of({ add: Effect.fn("Plugin.add")(function* (input) { const existing = hooks.find((item) => item.id === input.id) if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) const scope = yield* Scope.make() const result = yield* input.effect.pipe(Scope.provide(scope)) hooks = [ ...hooks.filter((item) => item.id !== input.id), { id: input.id, hooks: result ?? {}, scope, }, ] yield* PubSub.publish(added, input.id) }), added: () => Stream.fromPubSub(added), trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) { return yield* svc.triggerFor(ID.make("*"), name, input, output) }), triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) { const draftEntries = new Map>() const event = { ...input, ...output, } as Record for (const [field, value] of Object.entries(output)) { if (value && typeof value === "object") { draftEntries.set(field, createDraft(value)) event[field] = draftEntries.get(field) } } for (const item of hooks) { if (id !== ID.make("*") && item.id !== id) continue const match = item.hooks[name] if (!match) continue yield* match(event as any).pipe( Effect.withSpan(`Plugin.hook.${name}`, { attributes: { plugin: item.id, hook: name, }, }), ) } for (const [field, draft] of draftEntries) { event[field] = finishDraft(draft) } return event as any }), remove: Effect.fn("Plugin.remove")(function* (id) { const existing = hooks.find((item) => item.id === id) hooks = hooks.filter((item) => item.id !== id) if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) }), }) return svc }), ) export const defaultLayer = layer // opencode // sdcok