feat(plugin): add namespaced hook API (#33416)

This commit is contained in:
Dax
2026-06-22 19:06:57 -04:00
committed by GitHub
parent dc468bdcfd
commit 909a1a6d78
150 changed files with 3286 additions and 3916 deletions
+103
View File
@@ -0,0 +1,103 @@
# OpenCode V2 Promise Plugin API
The Promise plugin API is the async/await equivalent of `@opencode-ai/plugin/v2/effect`. It grants plugins the same two in-process capabilities:
- `hook` installs behavior at an OpenCode extension point.
- `reload` reruns every transform hook for a stateful domain.
The only difference from the Effect API is the async boundary: hook callbacks, hook registration, `reload`, and `Registration.dispose` use Promises instead of Effects.
## Defining A Plugin
```ts
import { define } from "@opencode-ai/plugin/v2/promise"
export const Plugin = define({
id: "example",
setup: async (ctx) => {
await ctx.catalog.transform((catalog) => {
catalog.provider.update("example", (provider) => {
provider.name = "Example"
})
})
},
})
```
Plugin setup registers hooks imperatively. It does not return a hook object.
Configuration supplied for the plugin is available as `ctx.options`.
A registration may be removed early through `dispose`:
```ts
const registration = await ctx.catalog.transform(applyCatalog)
await registration.dispose()
```
## Transform Hooks
Transform hooks contribute to stateful domains. The draft editor is synchronous; the callback may be `async` when it needs to await other work:
```ts
await ctx.agent.transform((agent) => {
agent.update("reviewer", (item) => {
item.description = "Reviews code for regressions"
item.mode = "subagent"
})
})
```
Available transform hooks are namespaced by domain:
```ts
ctx.agent.transform
ctx.catalog.transform
ctx.command.transform
ctx.integration.transform
ctx.reference.transform
ctx.skill.transform
```
## Runtime Hooks
Runtime hooks intercept live operations:
```ts
await ctx.aisdk.sdk(async (event) => {
if (event.package !== "@ai-sdk/xai") return
const mod = await import("@ai-sdk/xai")
event.sdk = mod.createXai(event.options)
})
await ctx.aisdk.language((event) => {
if (event.model.providerID !== "xai") return
event.language = event.sdk.responses(event.model.api.id)
})
```
## Reloading A Domain
When data captured by a transform changes, reload the affected domain:
```ts
let data = await loadCatalog()
await ctx.catalog.transform((catalog) => {
applyCatalog(data, catalog)
})
data = await loadCatalog()
await ctx.catalog.reload()
```
Available reload operations are:
```ts
ctx.agent.reload()
ctx.catalog.reload()
ctx.command.reload()
ctx.integration.reload()
ctx.reference.reload()
ctx.skill.reload()
```
+8
View File
@@ -0,0 +1,8 @@
import type { AgentDraft } from "../effect/agent.js"
import type { Hooks } from "./registration.js"
export type { AgentDraft }
export type AgentHooks = Hooks<{
transform: AgentDraft
}>
+18
View File
@@ -0,0 +1,18 @@
import type { LanguageModelV3 } from "@ai-sdk/provider"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import type { Hooks } from "./registration.js"
export type AISDKHooks = Hooks<{
sdk: {
readonly model: ModelV2Info
readonly package: string
readonly options: Record<string, any>
sdk?: any
}
language: {
readonly model: ModelV2Info
readonly sdk: any
readonly options: Record<string, any>
language?: LanguageModelV3
}
}>
@@ -0,0 +1,8 @@
import type { CatalogDraft, CatalogProviderRecord } from "../effect/catalog.js"
import type { Hooks } from "./registration.js"
export type { CatalogDraft, CatalogProviderRecord }
export type CatalogHooks = Hooks<{
transform: CatalogDraft
}>
@@ -0,0 +1,8 @@
import type { CommandDraft } from "../effect/command.js"
import type { Hooks } from "./registration.js"
export type { CommandDraft }
export type CommandHooks = Hooks<{
transform: CommandDraft
}>
+22
View File
@@ -0,0 +1,22 @@
import type { PluginOptions } from "../options.js"
import type { AgentHooks } from "./agent.js"
import type { AISDKHooks } from "./aisdk.js"
import type { CatalogHooks } from "./catalog.js"
import type { CommandHooks } from "./command.js"
import type { IntegrationHooks } from "./integration.js"
import type { PluginHooks } from "./plugin.js"
import type { ReferenceHooks } from "./reference.js"
import type { SkillHooks } from "./skill.js"
import type { Reload } from "./registration.js"
export interface PluginContext {
readonly options: PluginOptions
readonly agent: AgentHooks & Reload
readonly aisdk: AISDKHooks
readonly catalog: CatalogHooks & Reload
readonly command: CommandHooks & Reload
readonly integration: IntegrationHooks & Reload
readonly plugin: PluginHooks & Reload
readonly reference: ReferenceHooks & Reload
readonly skill: SkillHooks & Reload
}
+17
View File
@@ -0,0 +1,17 @@
export type { PluginContext } from "./context.js"
export type { PluginOptions } from "../options.js"
export { define } from "./plugin.js"
export type { Plugin, PluginDraft, PluginHooks, PluginRef } from "./plugin.js"
export type { Registration, Reload } from "./registration.js"
export type { AgentDraft, AgentHooks } from "./agent.js"
export type { AISDKHooks } from "./aisdk.js"
export type { CatalogDraft, CatalogHooks, CatalogProviderRecord } from "./catalog.js"
export type { CommandDraft, CommandHooks } from "./command.js"
export type {
IntegrationDraft,
IntegrationHooks,
IntegrationMethod,
IntegrationMethodRegistration,
} from "./integration.js"
export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
export type { SkillDraft, SkillHooks, SkillSource } from "./skill.js"
@@ -0,0 +1,8 @@
import type { IntegrationDraft, IntegrationMethod, IntegrationMethodRegistration } from "../effect/integration.js"
import type { Hooks } from "./registration.js"
export type { IntegrationDraft, IntegrationMethod, IntegrationMethodRegistration }
export type IntegrationHooks = Hooks<{
transform: IntegrationDraft
}>
+18
View File
@@ -0,0 +1,18 @@
import type { PluginContext } from "./context.js"
import type { PluginDraft, PluginRef } from "../effect/plugin.js"
import type { Hooks } from "./registration.js"
export interface Plugin {
readonly id: string
readonly setup: (context: PluginContext) => Promise<void> | void
}
export function define(plugin: Plugin) {
return plugin
}
export type { PluginDraft, PluginRef }
export type PluginHooks = Hooks<{
transform: PluginDraft
}>
@@ -0,0 +1,8 @@
import type { ReferenceDraft } from "../effect/reference.js"
import type { Hooks } from "./registration.js"
export type { ReferenceDraft }
export type ReferenceHooks = Hooks<{
transform: ReferenceDraft
}>
@@ -0,0 +1,11 @@
export interface Registration {
readonly dispose: () => Promise<void>
}
export interface Reload {
readonly reload: () => Promise<void>
}
export type Hooks<Spec> = {
readonly [Name in keyof Spec]: (callback: (input: Spec[Name]) => Promise<void> | void) => Promise<Registration>
}
+8
View File
@@ -0,0 +1,8 @@
import type { SkillDraft, SkillSource } from "../effect/skill.js"
import type { Hooks } from "./registration.js"
export type { SkillDraft, SkillSource }
export type SkillHooks = Hooks<{
transform: SkillDraft
}>