feat(plugin): add namespaced hook API (#33416)
This commit is contained in:
@@ -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()
|
||||
```
|
||||
@@ -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
|
||||
}>
|
||||
@@ -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
|
||||
}>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}>
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}>
|
||||
Reference in New Issue
Block a user