feat(plugin): add namespaced hook API (#33416)
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
".": "./src/index.ts",
|
||||
"./tool": "./src/tool.ts",
|
||||
"./tui": "./src/tui.ts",
|
||||
"./v2/effect": "./src/v2/effect/index.ts"
|
||||
"./v2/effect": "./src/v2/effect/index.ts",
|
||||
"./v2/promise": "./src/v2/promise/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,585 +1,111 @@
|
||||
# OpenCode V2 Plugin API
|
||||
# OpenCode V2 Effect Plugin API
|
||||
|
||||
> Design proposal. The API shown here is the intended V2 model and is not fully implemented yet.
|
||||
The Effect plugin API grants plugins two in-process capabilities:
|
||||
|
||||
This document explains how OpenCode V2 plugins contribute agents, commands, skills, integrations, providers, and models without importing `@opencode-ai/core`.
|
||||
- `hook` installs behavior at an OpenCode extension point.
|
||||
- `reload` reruns every transform hook for a stateful domain.
|
||||
|
||||
The design has four goals:
|
||||
The public server client will be exposed separately. It is intentionally not part of `PluginContext` yet.
|
||||
|
||||
- Internal and external plugins use the same API.
|
||||
- Plugin values use generated `@opencode-ai/sdk` types.
|
||||
- Core may keep richer internal representations such as branded IDs and decoded Effect schemas.
|
||||
- Plugins can react to changing data without reloading an entire Location.
|
||||
|
||||
## Mental Model
|
||||
|
||||
A plugin has two parts:
|
||||
|
||||
1. A setup effect that loads data, starts scoped subscriptions, and returns hooks.
|
||||
2. Singular transform hooks that describe the plugin's current contribution to a domain.
|
||||
## Defining A Plugin
|
||||
|
||||
```ts
|
||||
export default defineEffectPlugin({
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "example",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
return {
|
||||
"agent.transform": (agent) => {
|
||||
// Describe this plugin's agent contribution.
|
||||
},
|
||||
}
|
||||
}),
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
catalog.provider.update("example", (provider) => {
|
||||
provider.name = "Example"
|
||||
})
|
||||
})
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
A transform is not a one-time mutation. It is a replayable declaration.
|
||||
Plugin setup registers hooks imperatively. It does not return a hook object.
|
||||
|
||||
OpenCode may run it when:
|
||||
Configuration supplied for the plugin is available as `ctx.options`.
|
||||
|
||||
- The plugin is added.
|
||||
- The plugin is removed or replaced.
|
||||
- Another plugin affecting the same domain changes.
|
||||
- The plugin explicitly invalidates the domain.
|
||||
Registrations are owned by the plugin scope. Closing the scope removes them automatically; a registration may also be removed early through `dispose`.
|
||||
|
||||
Transforms must therefore be synchronous, deterministic, and safe to rerun.
|
||||
## Transform Hooks
|
||||
|
||||
## Why Hooks Are Returned
|
||||
|
||||
Each transform is a singular property of the plugin definition:
|
||||
Transform hooks contribute to stateful domains:
|
||||
|
||||
```ts
|
||||
return {
|
||||
"catalog.transform": applyCatalog,
|
||||
}
|
||||
```
|
||||
|
||||
This makes it structurally clear that one plugin has at most one transform per domain. There is no ambiguous behavior from calling `transform()` multiple times during setup.
|
||||
|
||||
Transforms from different plugins compose in plugin order.
|
||||
|
||||
```text
|
||||
models.dev catalog transform
|
||||
→ config catalog transform
|
||||
→ provider catalog transforms
|
||||
→ user catalog transforms
|
||||
→ core catalog finalizer
|
||||
```
|
||||
|
||||
## Your First Plugin
|
||||
|
||||
This plugin adds a reviewer agent.
|
||||
|
||||
```ts
|
||||
import { defineEffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default defineEffectPlugin({
|
||||
id: "reviewer",
|
||||
effect: () =>
|
||||
Effect.succeed({
|
||||
"agent.transform": (agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code for correctness and regressions"
|
||||
item.system = "Review the requested code. Prioritize bugs and behavioral regressions."
|
||||
item.mode = "subagent"
|
||||
item.hidden = false
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
The editor supplies a complete default agent when `reviewer` does not exist. The callback modifies that value using the generated SDK agent shape.
|
||||
|
||||
When the plugin unloads, OpenCode rebuilds the agent registry without this transform. The reviewer disappears automatically.
|
||||
|
||||
## Transform Editors
|
||||
|
||||
Editors support ordered reads and writes while a domain is being rebuilt.
|
||||
|
||||
```ts
|
||||
"agent.transform": (agent) => {
|
||||
const existing = agent.get("reviewer")
|
||||
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description ??= existing?.description ?? "Reviews code"
|
||||
yield *
|
||||
ctx.agent.transform((agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code for regressions"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
An editor is valid only during the transform call. Do not retain it in plugin state.
|
||||
OpenCode rebuilds the domain when a transform is registered or disposed. A rebuild starts from fresh domain state and runs every active transform in registration order.
|
||||
|
||||
Later plugins see mutations made by earlier plugins in the same rebuild.
|
||||
|
||||
## Adding A Provider And Model
|
||||
|
||||
This plugin contributes one provider and one model.
|
||||
Available transform hooks are namespaced by domain:
|
||||
|
||||
```ts
|
||||
import { defineEffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default defineEffectPlugin({
|
||||
id: "acme",
|
||||
effect: () =>
|
||||
Effect.succeed({
|
||||
"catalog.transform": (catalog) => {
|
||||
catalog.provider.update("acme", (provider) => {
|
||||
provider.name = "Acme AI"
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.acme.example/v1",
|
||||
}
|
||||
})
|
||||
|
||||
catalog.model.update("acme", "acme-chat", (model) => {
|
||||
model.name = "Acme Chat"
|
||||
model.family = "acme"
|
||||
model.api = {
|
||||
id: "acme-chat",
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.acme.example/v1",
|
||||
}
|
||||
model.capabilities = {
|
||||
tools: true,
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
}
|
||||
model.time.released = Date.now()
|
||||
model.status = "active"
|
||||
model.enabled = true
|
||||
model.limit = {
|
||||
context: 128_000,
|
||||
output: 16_384,
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
ctx.agent.transform
|
||||
ctx.catalog.transform
|
||||
ctx.command.transform
|
||||
ctx.integration.transform
|
||||
ctx.reference.transform
|
||||
ctx.skill.transform
|
||||
```
|
||||
|
||||
The provider and model values use generated SDK types. Core may encode and decode richer internal schema values at the plugin boundary.
|
||||
|
||||
## Dynamic Data And Invalidation
|
||||
|
||||
Some plugins depend on data that changes after setup. Examples include:
|
||||
|
||||
- models.dev refreshes
|
||||
- config file watchers
|
||||
- skill directory watchers
|
||||
- authentication state changes
|
||||
|
||||
The plugin keeps the current data in its own scoped state. When that data changes, it invalidates each affected domain.
|
||||
|
||||
```ts
|
||||
let data = yield * loadData()
|
||||
|
||||
return {
|
||||
"catalog.transform": (catalog) => {
|
||||
applyCatalog(data, catalog)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
After changing `data`:
|
||||
|
||||
```ts
|
||||
data = yield * loadData()
|
||||
yield * ctx.catalog.invalidate()
|
||||
```
|
||||
|
||||
Invalidation does not mutate the current catalog in place. It requests a rebuild:
|
||||
|
||||
```text
|
||||
create fresh catalog state
|
||||
→ replay every catalog transform in plugin order
|
||||
→ run the core catalog finalizer
|
||||
→ commit the new catalog
|
||||
→ publish catalog.updated
|
||||
```
|
||||
|
||||
Repeated invalidations are serialized and may be coalesced.
|
||||
|
||||
## Models.dev Example
|
||||
|
||||
Models.dev is the main example of a dynamic plugin. It projects one changing source into the integration and catalog domains.
|
||||
|
||||
```ts
|
||||
import { defineEffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect, Stream } from "effect"
|
||||
|
||||
export default defineEffectPlugin({
|
||||
id: "models-dev",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
const events = yield* EventV2.Service
|
||||
let data = yield* modelsDev.get()
|
||||
|
||||
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
data = yield* modelsDev.get()
|
||||
yield* ctx.integration.invalidate()
|
||||
yield* ctx.catalog.invalidate()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
|
||||
return {
|
||||
"integration.transform": (integration) => {
|
||||
for (const provider of Object.values(data)) {
|
||||
if (provider.env.length === 0) continue
|
||||
|
||||
integration.update(provider.id, (item) => {
|
||||
item.name = provider.name
|
||||
})
|
||||
|
||||
integration.method.update({
|
||||
integrationID: provider.id,
|
||||
method: { type: "key" },
|
||||
})
|
||||
|
||||
integration.method.update({
|
||||
integrationID: provider.id,
|
||||
method: {
|
||||
type: "env",
|
||||
names: [...provider.env],
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
"catalog.transform": (catalog) => {
|
||||
for (const provider of Object.values(data)) {
|
||||
applyProvider(provider, catalog)
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
`ModelsDev.Service` and `ModelsDev.Event` are privileged internal dependencies in this example. The integration and catalog contributions still use the same hooks available to external plugins.
|
||||
|
||||
This design intentionally does not require a special multi-domain transform. The two domains rebuild independently. If strict cross-domain atomic publication becomes a requirement, it should be designed separately rather than making every transform combinatorial.
|
||||
|
||||
## Config File Watching
|
||||
|
||||
A config plugin can project one parsed config snapshot into several independent domains.
|
||||
|
||||
```ts
|
||||
export default defineEffectPlugin({
|
||||
id: "config",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
let config = yield* loadConfig()
|
||||
|
||||
yield* watchConfig.pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
config = yield* loadConfig()
|
||||
yield* ctx.agent.invalidate()
|
||||
yield* ctx.command.invalidate()
|
||||
yield* ctx.catalog.invalidate()
|
||||
yield* ctx.integration.invalidate()
|
||||
yield* ctx.reference.invalidate()
|
||||
yield* ctx.skill.invalidate()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return {
|
||||
"agent.transform": (agent) => applyAgentConfig(config, agent),
|
||||
"command.transform": (command) => applyCommandConfig(config, command),
|
||||
"catalog.transform": (catalog) => applyProviderConfig(config, catalog),
|
||||
"integration.transform": (integration) => applyIntegrationConfig(config, integration),
|
||||
"reference.transform": (reference) => applyReferenceConfig(config, reference),
|
||||
"skill.transform": (skill) => applySkillConfig(config, skill),
|
||||
}
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
The watcher performs I/O. The transforms only project the latest in-memory snapshot.
|
||||
|
||||
## Skill Directory Watching
|
||||
|
||||
A skill plugin follows the same pattern.
|
||||
|
||||
```ts
|
||||
export default defineEffectPlugin({
|
||||
id: "workspace-skills",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
let sources = yield* discoverSkills()
|
||||
|
||||
yield* watchSkillDirectories.pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
sources = yield* discoverSkills()
|
||||
yield* ctx.skill.invalidate()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return {
|
||||
"skill.transform": (skill) => {
|
||||
for (const source of sources) skill.source(source)
|
||||
},
|
||||
}
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Rebuilding the source registry may not be enough if discovered skill contents are cached separately. Domain invalidation must include all materialized state owned by that domain.
|
||||
|
||||
## Runtime Hooks
|
||||
|
||||
Transform hooks build registry state. Runtime hooks intercept live operations.
|
||||
Runtime hooks intercept live operations rather than rebuilding domain state:
|
||||
|
||||
```ts
|
||||
return {
|
||||
"catalog.transform": (catalog) => {
|
||||
// Synchronous and replayable.
|
||||
},
|
||||
|
||||
"aisdk.sdk": Effect.fn(function* (event) {
|
||||
// Runs when OpenCode needs an AI SDK provider.
|
||||
}),
|
||||
|
||||
"aisdk.language": Effect.fn(function* (event) {
|
||||
// Runs when OpenCode selects a language model implementation.
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Runtime hooks may perform Effects appropriate to the operation. Transform hooks must remain replay-safe.
|
||||
|
||||
## Integration Authentication
|
||||
|
||||
Executable registrations may be installed during an integration transform.
|
||||
|
||||
```ts
|
||||
return {
|
||||
"integration.transform": (integration) => {
|
||||
integration.update("openai", (item) => {
|
||||
item.name = "OpenAI"
|
||||
})
|
||||
|
||||
integration.method.update({
|
||||
integrationID: "openai",
|
||||
method: {
|
||||
id: "chatgpt-browser",
|
||||
type: "oauth",
|
||||
label: "ChatGPT Pro/Plus (browser)",
|
||||
},
|
||||
authorize: browserAuthorize,
|
||||
refresh: refreshCredential,
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Replay installs callback values. It must not start OAuth, open a server, or refresh credentials. Those effects run later when core invokes the stored implementation.
|
||||
|
||||
## Reading Other Domains
|
||||
|
||||
A transform may need information from another committed domain.
|
||||
|
||||
```ts
|
||||
"agent.transform": (agent) => {
|
||||
if (!anthropicAvailable) return
|
||||
|
||||
agent.update("anthropic-reviewer", (item) => {
|
||||
item.model = {
|
||||
providerID: "anthropic",
|
||||
id: "claude-sonnet",
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Load or subscribe to the dependency during setup, keep a local snapshot, and invalidate the dependent domain when the snapshot changes.
|
||||
|
||||
```ts
|
||||
let anthropicAvailable = yield * readAnthropicAvailability()
|
||||
yield *
|
||||
ctx.aisdk.sdk(
|
||||
Effect.fn(function* (event) {
|
||||
if (event.package !== "@ai-sdk/xai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||
event.sdk = mod.createXai(event.options)
|
||||
}),
|
||||
)
|
||||
|
||||
yield *
|
||||
catalogChanges.pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
anthropicAvailable = yield* readAnthropicAvailability()
|
||||
yield* ctx.agent.invalidate()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
```
|
||||
|
||||
This keeps transform callbacks synchronous and avoids hidden dependency tracking.
|
||||
|
||||
## Plugin Order
|
||||
|
||||
OpenCode's default distribution uses an opinionated order.
|
||||
|
||||
```text
|
||||
1. Built-in agents, commands, and skills
|
||||
2. Base data sources such as models.dev
|
||||
3. Configuration projections
|
||||
4. Provider-specific normalization and authentication
|
||||
5. External user plugins
|
||||
6. Core domain finalization
|
||||
```
|
||||
|
||||
For the catalog:
|
||||
|
||||
```text
|
||||
models.dev
|
||||
→ config provider overrides
|
||||
→ built-in provider normalization
|
||||
→ user catalog transforms
|
||||
→ policy and validation
|
||||
→ commit
|
||||
→ catalog.updated
|
||||
```
|
||||
|
||||
Ordering is observable behavior. Later transforms see and may override earlier transforms.
|
||||
|
||||
## Core Finalization
|
||||
|
||||
Plugin transforms and core finalization are different concepts.
|
||||
|
||||
Transforms describe configurable plugin contributions. Core finalization enforces domain invariants.
|
||||
|
||||
Catalog finalization may:
|
||||
|
||||
- Validate the materialized catalog.
|
||||
- Apply provider-use policy.
|
||||
- Build indexes.
|
||||
- Commit the new snapshot.
|
||||
- Publish `catalog.updated` after the new snapshot is visible.
|
||||
|
||||
Reference finalization may materialize Git-backed references. Integration finalization may update connection projections and publish events.
|
||||
|
||||
Core finalizers always run after plugin transforms for that domain.
|
||||
|
||||
## Add, Remove, And Replace
|
||||
|
||||
When a plugin is added, OpenCode invalidates every domain for which it returned a transform.
|
||||
|
||||
When a plugin is removed, OpenCode removes its hooks and invalidates those domains. Rebuilding from base state automatically removes the plugin's prior mutations.
|
||||
|
||||
When a plugin is replaced, OpenCode swaps its hooks, preserves the intended plugin order, and invalidates the affected domains.
|
||||
|
||||
No plugin-specific undo callback is required.
|
||||
|
||||
## Effect API
|
||||
|
||||
The Effect API exposes Effect-native setup, runtime hooks, scopes, interruption, and typed failures.
|
||||
|
||||
```ts
|
||||
export type EffectPlugin = (ctx: EffectPluginContext) => Effect.Effect<PluginHooks | void, PluginError, Scope.Scope>
|
||||
```
|
||||
|
||||
The setup scope owns:
|
||||
|
||||
- Event subscriptions
|
||||
- Watchers
|
||||
- Background fibers
|
||||
- Plugin hooks
|
||||
|
||||
Closing the scope unloads the plugin and invalidates its transformed domains.
|
||||
|
||||
## Promise API
|
||||
|
||||
The Promise API uses the same SDK values, hook names, editors, and lifecycle semantics.
|
||||
|
||||
```ts
|
||||
export default definePlugin({
|
||||
id: "reviewer",
|
||||
plugin: async () => ({
|
||||
"agent.transform": (agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code"
|
||||
item.mode = "subagent"
|
||||
item.hidden = false
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Promise plugins receive Promise-returning host capabilities:
|
||||
|
||||
```ts
|
||||
await ctx.catalog.invalidate()
|
||||
```
|
||||
|
||||
Core implements the Promise API by running the canonical Effect capabilities. It manages the plugin scope automatically.
|
||||
|
||||
## Rules For Transform Hooks
|
||||
|
||||
Transform hooks must:
|
||||
|
||||
- Be synchronous.
|
||||
- Be deterministic for their captured snapshot.
|
||||
- Avoid network, filesystem, process, and database I/O.
|
||||
- Avoid publishing events.
|
||||
- Avoid invalidating a domain while that domain is rebuilding.
|
||||
- Avoid retaining the editor after returning.
|
||||
|
||||
Transform hooks may:
|
||||
|
||||
- Read the editor's current materialized state.
|
||||
- Add, update, and remove domain entries.
|
||||
- Install executable callback values for later use.
|
||||
- Read immutable or plugin-owned captured data.
|
||||
|
||||
## Runtime Requirements
|
||||
|
||||
The plugin runtime must provide these guarantees:
|
||||
|
||||
- Hooks replay in deterministic plugin order.
|
||||
- Only one rebuild per domain runs at a time.
|
||||
- Repeated invalidations may be coalesced.
|
||||
- Rebuilds use fresh temporary state.
|
||||
- Failed rebuilds leave the previous committed state intact.
|
||||
- Core finalization runs after all plugin transforms.
|
||||
- Update events publish only after the new state is visible.
|
||||
- Plugin add, remove, and replacement invalidate affected domains automatically.
|
||||
- A transform cannot invalidate the domain currently running it.
|
||||
|
||||
## Summary
|
||||
|
||||
Use setup for effects and transforms for declarations.
|
||||
|
||||
```ts
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
let data = yield* loadData()
|
||||
|
||||
yield* watchData.pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
data = yield* loadData()
|
||||
yield* ctx.catalog.invalidate()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return {
|
||||
"catalog.transform": (catalog) => {
|
||||
applyCatalog(data, catalog)
|
||||
},
|
||||
}
|
||||
ctx.aisdk.language((event) => {
|
||||
if (event.model.providerID !== "xai") return
|
||||
event.language = event.sdk.responses(event.model.api.id)
|
||||
})
|
||||
```
|
||||
|
||||
The plugin owns changing source data. The runtime owns hook ordering, replay, invalidation, cleanup, and commit. Core services own their state and finalization.
|
||||
Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.
|
||||
|
||||
## Reloading A Domain
|
||||
|
||||
When data captured by a transform changes, reload the affected domain:
|
||||
|
||||
```ts
|
||||
let data = yield * loadCatalog()
|
||||
|
||||
yield *
|
||||
ctx.catalog.transform((catalog) => {
|
||||
applyCatalog(data, catalog)
|
||||
})
|
||||
|
||||
data = yield * loadCatalog()
|
||||
yield * ctx.catalog.reload()
|
||||
```
|
||||
|
||||
Reload belongs to the domain, not an individual registration. `ctx.catalog.reload()` reruns every active catalog transform and publishes the rebuilt catalog.
|
||||
|
||||
Available reload operations are:
|
||||
|
||||
```ts
|
||||
ctx.agent.reload()
|
||||
ctx.catalog.reload()
|
||||
ctx.command.reload()
|
||||
ctx.integration.reload()
|
||||
ctx.reference.reload()
|
||||
ctx.skill.reload()
|
||||
```
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface AgentDraft {
|
||||
list(): readonly AgentV2Info[]
|
||||
@@ -10,8 +9,6 @@ export interface AgentDraft {
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
export interface Agent extends Transformable<AgentDraft> {
|
||||
get(id: string): Effect.Effect<AgentV2Info | undefined>
|
||||
default(): Effect.Effect<AgentV2Info | undefined>
|
||||
list(): Effect.Effect<AgentV2Info[]>
|
||||
}
|
||||
export type AgentHooks = Hooks<{
|
||||
transform: AgentDraft
|
||||
}>
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Hookable } from "./registration.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface AISDKHooks {
|
||||
readonly sdk: (event: {
|
||||
export type AISDKHooks = Hooks<{
|
||||
sdk: {
|
||||
readonly model: ModelV2Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: any
|
||||
}) => Effect.Effect<void> | void
|
||||
readonly language: (event: {
|
||||
}
|
||||
language: {
|
||||
readonly model: ModelV2Info
|
||||
readonly sdk: any
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}) => Effect.Effect<void> | void
|
||||
}
|
||||
|
||||
export interface AISDK extends Hookable<AISDKHooks> {}
|
||||
}
|
||||
}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface CatalogProviderRecord {
|
||||
readonly provider: ProviderV2Info
|
||||
@@ -25,17 +24,6 @@ export interface CatalogDraft {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Catalog extends Transformable<CatalogDraft> {
|
||||
readonly provider: {
|
||||
get(id: string): Effect.Effect<ProviderV2Info | undefined>
|
||||
list(): Effect.Effect<ProviderV2Info[]>
|
||||
available(): Effect.Effect<ProviderV2Info[]>
|
||||
}
|
||||
readonly model: {
|
||||
get(providerID: string, modelID: string): Effect.Effect<ModelV2Info | undefined>
|
||||
list(): Effect.Effect<ModelV2Info[]>
|
||||
available(): Effect.Effect<ModelV2Info[]>
|
||||
default(): Effect.Effect<ModelV2Info | undefined>
|
||||
small(providerID: string): Effect.Effect<ModelV2Info | undefined>
|
||||
}
|
||||
}
|
||||
export type CatalogHooks = Hooks<{
|
||||
transform: CatalogDraft
|
||||
}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CommandV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface CommandDraft {
|
||||
list(): readonly CommandV2Info[]
|
||||
@@ -9,7 +8,6 @@ export interface CommandDraft {
|
||||
remove(name: string): void
|
||||
}
|
||||
|
||||
export interface Command extends Transformable<CommandDraft> {
|
||||
get(name: string): Effect.Effect<CommandV2Info | undefined>
|
||||
list(): Effect.Effect<CommandV2Info[]>
|
||||
}
|
||||
export type CommandHooks = Hooks<{
|
||||
transform: CommandDraft
|
||||
}>
|
||||
|
||||
22
packages/plugin/src/v2/effect/context.ts
Normal file
22
packages/plugin/src/v2/effect/context.ts
Normal 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
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Agent } from "./agent.js"
|
||||
import type { AISDK } from "./aisdk.js"
|
||||
import type { Catalog } from "./catalog.js"
|
||||
import type { Command } from "./command.js"
|
||||
import type { Event } from "./event.js"
|
||||
import type { FileSystem } from "./filesystem.js"
|
||||
import type { Integration } from "./integration.js"
|
||||
import type { Location } from "./location.js"
|
||||
import type { Npm } from "./npm.js"
|
||||
import type { Path } from "./path.js"
|
||||
import type { Reference } from "./reference.js"
|
||||
import type { Skill } from "./skill.js"
|
||||
|
||||
export interface PluginHost {
|
||||
readonly agent: Agent
|
||||
readonly aisdk: AISDK
|
||||
readonly catalog: Catalog
|
||||
readonly command: Command
|
||||
readonly event: Event
|
||||
readonly filesystem: FileSystem
|
||||
readonly integration: Integration
|
||||
readonly location: Location
|
||||
readonly npm: Npm
|
||||
readonly path: Path
|
||||
readonly reference: Reference
|
||||
readonly skill: Skill
|
||||
}
|
||||
@@ -1,17 +1,3 @@
|
||||
export type { PluginHost } from "./host.js"
|
||||
export type { PluginContext } from "./context.js"
|
||||
export { define } from "./plugin.js"
|
||||
export type { Plugin } from "./plugin.js"
|
||||
export type { Registration } from "./registration.js"
|
||||
export type { Agent, AgentDraft } from "./agent.js"
|
||||
export type { AISDK, AISDKHooks } from "./aisdk.js"
|
||||
export type { Catalog, CatalogDraft, CatalogProviderRecord } from "./catalog.js"
|
||||
export type { Command, CommandDraft } from "./command.js"
|
||||
export type { Event, EventMap } from "./event.js"
|
||||
export type { FileSystem } from "./filesystem.js"
|
||||
export type { Integration, IntegrationDraft, IntegrationMethod, IntegrationMethodRegistration } from "./integration.js"
|
||||
export type { Location } from "./location.js"
|
||||
export type { Npm } from "./npm.js"
|
||||
export type { Path } from "./path.js"
|
||||
export type { Reference, ReferenceDraft } from "./reference.js"
|
||||
export type { Hookable, Transform, Transformable } from "./registration.js"
|
||||
export type { Skill, SkillDraft, SkillSource } from "./skill.js"
|
||||
export type { Plugin, PluginDraft } from "./plugin.js"
|
||||
|
||||
@@ -4,8 +4,7 @@ import type {
|
||||
IntegrationKeyMethod,
|
||||
IntegrationOAuthMethod,
|
||||
} from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod
|
||||
export type IntegrationMethodRegistration =
|
||||
@@ -30,7 +29,6 @@ export interface IntegrationDraft {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Integration extends Transformable<IntegrationDraft> {
|
||||
get(id: string): Effect.Effect<IntegrationInfo | undefined>
|
||||
list(): Effect.Effect<IntegrationInfo[]>
|
||||
}
|
||||
export type IntegrationHooks = Hooks<{
|
||||
transform: IntegrationDraft
|
||||
}>
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import type { Effect, Scope } from "effect"
|
||||
import type { PluginHost } from "./host.js"
|
||||
import type { PluginContext } from "./context.js"
|
||||
import type { PluginOptions } from "../options.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface Plugin<R = never> {
|
||||
export interface Plugin {
|
||||
readonly id: string
|
||||
readonly effect: (host: PluginHost) => Effect.Effect<void, never, R | Scope.Scope>
|
||||
readonly effect: (context: PluginContext) => Effect.Effect<void, never, Scope.Scope>
|
||||
}
|
||||
|
||||
export function define<R>(plugin: Plugin<R>) {
|
||||
export function define(plugin: Plugin) {
|
||||
return plugin
|
||||
}
|
||||
|
||||
export interface PluginRef {
|
||||
readonly package: string
|
||||
readonly options?: PluginOptions
|
||||
}
|
||||
|
||||
export interface PluginDraft {
|
||||
list(): readonly Plugin[]
|
||||
add(plugin: Plugin): void
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
export type PluginHooks = Hooks<{
|
||||
transform: PluginDraft
|
||||
}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ReferenceGitSource, ReferenceInfo, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface ReferenceDraft {
|
||||
add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void
|
||||
@@ -8,6 +7,6 @@ export interface ReferenceDraft {
|
||||
list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[]
|
||||
}
|
||||
|
||||
export interface Reference extends Transformable<ReferenceDraft> {
|
||||
list(): Effect.Effect<ReferenceInfo[]>
|
||||
}
|
||||
export type ReferenceHooks = Hooks<{
|
||||
transform: ReferenceDraft
|
||||
}>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import type { Effect, Scope } from "effect"
|
||||
|
||||
export type Transform<Draft> = (draft: Draft) => Effect.Effect<void> | void
|
||||
|
||||
export interface Registration {
|
||||
readonly dispose: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface Transformable<Draft> {
|
||||
transform(callback: Transform<Draft>): Effect.Effect<Registration, never, Scope.Scope>
|
||||
rebuild(): Effect.Effect<void>
|
||||
export interface Reload {
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface Hookable<Hooks> {
|
||||
hook<Name extends keyof Hooks>(name: Name, callback: Hooks[Name]): Effect.Effect<Registration, never, Scope.Scope>
|
||||
export type Hooks<Spec> = {
|
||||
readonly [Name in keyof Spec]: (
|
||||
callback: (input: Spec[Name]) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { SkillV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export type SkillSource =
|
||||
| { readonly type: "directory"; readonly path: string }
|
||||
@@ -12,7 +11,6 @@ export interface SkillDraft {
|
||||
list(): readonly SkillSource[]
|
||||
}
|
||||
|
||||
export interface Skill extends Transformable<SkillDraft> {
|
||||
sources(): Effect.Effect<SkillSource[]>
|
||||
list(): Effect.Effect<SkillV2Info[]>
|
||||
}
|
||||
export type SkillHooks = Hooks<{
|
||||
transform: SkillDraft
|
||||
}>
|
||||
|
||||
1
packages/plugin/src/v2/options.ts
Normal file
1
packages/plugin/src/v2/options.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type PluginOptions = Readonly<Record<string, any>>
|
||||
103
packages/plugin/src/v2/promise/README.md
Normal file
103
packages/plugin/src/v2/promise/README.md
Normal 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
packages/plugin/src/v2/promise/agent.ts
Normal file
8
packages/plugin/src/v2/promise/agent.ts
Normal 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
packages/plugin/src/v2/promise/aisdk.ts
Normal file
18
packages/plugin/src/v2/promise/aisdk.ts
Normal 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
|
||||
}
|
||||
}>
|
||||
8
packages/plugin/src/v2/promise/catalog.ts
Normal file
8
packages/plugin/src/v2/promise/catalog.ts
Normal file
@@ -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
|
||||
}>
|
||||
8
packages/plugin/src/v2/promise/command.ts
Normal file
8
packages/plugin/src/v2/promise/command.ts
Normal file
@@ -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
packages/plugin/src/v2/promise/context.ts
Normal file
22
packages/plugin/src/v2/promise/context.ts
Normal 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
packages/plugin/src/v2/promise/index.ts
Normal file
17
packages/plugin/src/v2/promise/index.ts
Normal 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"
|
||||
8
packages/plugin/src/v2/promise/integration.ts
Normal file
8
packages/plugin/src/v2/promise/integration.ts
Normal file
@@ -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
packages/plugin/src/v2/promise/plugin.ts
Normal file
18
packages/plugin/src/v2/promise/plugin.ts
Normal 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
|
||||
}>
|
||||
8
packages/plugin/src/v2/promise/reference.ts
Normal file
8
packages/plugin/src/v2/promise/reference.ts
Normal file
@@ -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
|
||||
}>
|
||||
11
packages/plugin/src/v2/promise/registration.ts
Normal file
11
packages/plugin/src/v2/promise/registration.ts
Normal file
@@ -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
packages/plugin/src/v2/promise/skill.ts
Normal file
8
packages/plugin/src/v2/promise/skill.ts
Normal 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
|
||||
}>
|
||||
Reference in New Issue
Block a user