feat(plugin): add v2 effect host (#33111)
This commit is contained in:
parent
7a9337da8a
commit
c780d7cee7
139 changed files with 3740 additions and 1943 deletions
515
packages/plugin/src/v2/effect/PLAN.md
Normal file
515
packages/plugin/src/v2/effect/PLAN.md
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
# V2 Plugin System Implementation Plan
|
||||
|
||||
## Status
|
||||
|
||||
This document describes the agreed target design for the V2 plugin system. It is an implementation plan, not documentation for the current API.
|
||||
|
||||
## Goals
|
||||
|
||||
- Internal and external plugins use the same public plugin API.
|
||||
- Effect plugins import `@opencode-ai/plugin/v2/effect`, not `@opencode-ai/core`.
|
||||
- Public domain values use generated `@opencode-ai/sdk` types.
|
||||
- Core may retain branded IDs, decoded Effect schemas, and internal service types.
|
||||
- Plugins may register replayable domain transforms and runtime hooks imperatively during setup.
|
||||
- Registrations are scoped, independently disposable, ordered, and removable.
|
||||
- Dynamic sources such as models.dev, config files, and skill directories can rebuild one domain without reloading the entire Location.
|
||||
- The initial implementation covers the Effect API. A Promise API will be designed afterward as a wrapper over the same capabilities.
|
||||
|
||||
## Authoring Model
|
||||
|
||||
A plugin setup effect receives `PluginHost` and imperatively registers transforms and hooks.
|
||||
|
||||
```ts
|
||||
export const Plugin = define({
|
||||
id: "example",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.agent.transform(
|
||||
Effect.fn(function* (agent) {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code for regressions"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.tool.hook(
|
||||
"execute.before",
|
||||
Effect.fn(function* (event) {
|
||||
event.args.update(sanitizeArgs)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Plugin setup does not return hooks.
|
||||
|
||||
## Public Naming
|
||||
|
||||
Settled names:
|
||||
|
||||
- Replayable domain registration: `transform`
|
||||
- Explicit domain replay: `rebuild`
|
||||
- Runtime callback registration: `hook`
|
||||
- Registration cleanup: `dispose`
|
||||
- Event domain: singular `event`
|
||||
- Other domains are singular: `agent`, `command`, `integration`, `reference`, `session`, `skill`, and `tool`; `catalog` remains `catalog`
|
||||
- Hook names use dotted lifecycle names such as `"execute.before"` and `"execute.after"`
|
||||
|
||||
## Transform API
|
||||
|
||||
Each transformable domain exposes:
|
||||
|
||||
```ts
|
||||
interface TransformDomain<Editor> {
|
||||
transform(callback: (editor: Editor) => Effect.Effect<void>): Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
rebuild(): Effect.Effect<void>
|
||||
}
|
||||
```
|
||||
|
||||
The actual callback may be represented with the project's normal `Effect.fn` style.
|
||||
|
||||
```ts
|
||||
const registration =
|
||||
yield *
|
||||
ctx.catalog.transform(
|
||||
Effect.fn(function* (catalog) {
|
||||
const integration = yield* ctx.integration.get("anthropic")
|
||||
if (!integration) return
|
||||
|
||||
catalog.provider.update("anthropic", (provider) => {
|
||||
provider.name = "Anthropic"
|
||||
})
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Transforms may perform arbitrary Effects, including reads from other PluginHost services, filesystem I/O, and network I/O. Reads from another domain observe that domain's latest committed state.
|
||||
|
||||
Transforms have no typed error channel. Unexpected failures are defects.
|
||||
|
||||
## Transform Semantics
|
||||
|
||||
- Every call to `transform()` creates an independent registration.
|
||||
- Multiple transforms from one plugin and domain are allowed.
|
||||
- Transform order is plugin registration order, then transform registration order within the plugin.
|
||||
- A transform is automatically removed when its registration scope closes.
|
||||
- `Registration.dispose` removes it early and is idempotent.
|
||||
- Registering or disposing a transform automatically rebuilds its domain.
|
||||
- During bulk plugin boot, automatic rebuilds are deferred and each affected domain is rebuilt once after the batch.
|
||||
- `rebuild()` waits until replay and finalization complete.
|
||||
- `rebuild()` always replays every active transform for the domain.
|
||||
- Rebuilds are serialized and coalesced. Calls arriving during an active rebuild schedule at most one additional rebuild.
|
||||
- A rebuild captures its registration list at the start. Concurrent registration changes affect the next rebuild.
|
||||
- Transforms may not register or dispose transforms while replaying. Such changes are rejected or deferred by the runtime.
|
||||
- Calling `rebuild()` for the currently rebuilding domain from one of its transforms is rejected.
|
||||
- Rebuilding another domain from a transform is deferred until the current transform finishes.
|
||||
|
||||
## Registration API
|
||||
|
||||
Transforms and runtime hooks return the same Effect registration type.
|
||||
|
||||
```ts
|
||||
interface Registration {
|
||||
readonly dispose: Effect.Effect<void>
|
||||
}
|
||||
```
|
||||
|
||||
Registration behavior:
|
||||
|
||||
- Automatically attached to the current `Scope.Scope`
|
||||
- Explicitly disposable before scope closure
|
||||
- Disposal affects future replays or invocations
|
||||
- An in-flight rebuild or hook invocation uses the registration snapshot captured when it started and is allowed to finish
|
||||
|
||||
## Runtime Hook API
|
||||
|
||||
Domains expose runtime interception through `hook()`.
|
||||
|
||||
```ts
|
||||
const registration =
|
||||
yield *
|
||||
ctx.tool.hook(
|
||||
"execute.before",
|
||||
Effect.fn(function* (event) {
|
||||
event.args.update(sanitizeArgs)
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Runtime hook behavior:
|
||||
|
||||
- Multiple registrations for the same hook are allowed.
|
||||
- Hooks run sequentially in plugin and registration order.
|
||||
- Later hooks observe mutations made by earlier hooks.
|
||||
- Hook registration is scope-owned and independently disposable.
|
||||
- Disposal affects future invocations; an in-flight invocation finishes using its captured registration snapshot.
|
||||
- Runtime hooks are not replayed during domain rebuilds.
|
||||
- Runtime hook callbacks have no typed error channel.
|
||||
|
||||
## Hook Contexts
|
||||
|
||||
Each hook receives one purpose-built context object rather than separate input/output parameters.
|
||||
|
||||
```ts
|
||||
ctx.tool.hook("execute.before", (event) => {
|
||||
event.args.update((args) => ({
|
||||
...args,
|
||||
timeout: 30,
|
||||
}))
|
||||
})
|
||||
```
|
||||
|
||||
Hook context objects may contain:
|
||||
|
||||
- Readonly SDK-typed operation data
|
||||
- Purpose-built methods for allowed mutations
|
||||
- Capability methods where the operation requires more than field assignment
|
||||
|
||||
They must not expose core drafts or unrestricted internal objects.
|
||||
|
||||
## Domain Transforms Versus Runtime Hooks
|
||||
|
||||
Both use the same low-level scoped registration registry, but consumers invoke them differently.
|
||||
|
||||
```ts
|
||||
ctx.tool.transform(...) // replayed to build effective tool registry state
|
||||
ctx.tool.hook(...) // invoked at a live tool operation boundary
|
||||
```
|
||||
|
||||
The shared low-level machinery owns registration order, scope cleanup, disposal, and snapshots. Each domain owns when its transforms or runtime hooks execute.
|
||||
|
||||
## Event API
|
||||
|
||||
The Effect API exposes the existing event system as typed streams using generated SDK event discriminants.
|
||||
|
||||
```ts
|
||||
ctx.event.subscribe("catalog.updated")
|
||||
// Stream.Stream<EventCatalogUpdated>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
ctx.event.subscribe("catalog.updated").pipe(
|
||||
Stream.runForEach(() => ctx.agent.rebuild()),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
```
|
||||
|
||||
The plugin package derives event payload types from the generated SDK `Event` union:
|
||||
|
||||
```ts
|
||||
type EventMap = {
|
||||
[Item in Event as Item["type"]]: Item
|
||||
}
|
||||
```
|
||||
|
||||
Core resolves the public event type string to its internal event definition and delegates to `EventV2.Service.subscribe`.
|
||||
|
||||
## Domain State Model
|
||||
|
||||
Each transformable core service continues to own:
|
||||
|
||||
- Base state
|
||||
- Effective committed state
|
||||
- Editor creation
|
||||
- Ordered transform registrations for that domain
|
||||
- Rebuild serialization and coalescing
|
||||
- Core finalization
|
||||
- Commit and post-commit events
|
||||
|
||||
The initial implementation should evolve the existing generic `State` helper rather than create a central cross-domain state manager.
|
||||
|
||||
```text
|
||||
base state
|
||||
→ replay active transforms in order
|
||||
→ core domain finalization
|
||||
→ commit effective state
|
||||
→ publish updated event
|
||||
```
|
||||
|
||||
No cross-domain transform or transaction API is included.
|
||||
|
||||
## Finalization
|
||||
|
||||
Each domain has one plugin transform phase followed by core finalization.
|
||||
|
||||
Core finalization is for invariants and materialization, not plugin extension behavior.
|
||||
|
||||
Examples:
|
||||
|
||||
- Catalog policy filtering and validation
|
||||
- Reference repository materialization
|
||||
- Integration connection projection
|
||||
- Index construction
|
||||
- Post-commit update events
|
||||
|
||||
Finalizers should distinguish pre-commit work from post-commit notification. Update events should publish after the new state is visible.
|
||||
|
||||
## Plugin Order
|
||||
|
||||
The default distribution uses an opinionated internal 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 catalog transforms:
|
||||
|
||||
```text
|
||||
models.dev
|
||||
→ config provider overrides
|
||||
→ built-in provider normalization
|
||||
→ user catalog transforms
|
||||
→ catalog finalization
|
||||
```
|
||||
|
||||
This replaces the current distinction between setup-installed State transforms and catalog hooks invoked from the catalog finalizer.
|
||||
|
||||
Replacing a plugin with the same ID retains its existing order position. The old plugin is disabled before the replacement setup starts.
|
||||
|
||||
## Boot Batching
|
||||
|
||||
Plugin boot runs in an internal registration batch.
|
||||
|
||||
```text
|
||||
begin batch
|
||||
→ initialize plugins sequentially
|
||||
→ register transforms and hooks
|
||||
→ collect affected domains
|
||||
→ rebuild each affected domain once
|
||||
→ end batch
|
||||
```
|
||||
|
||||
Registration itself is not staged per plugin. If setup fails, closing the plugin's child scope removes every registration made before the failure.
|
||||
|
||||
Outside a batch, transform registration and disposal rebuild immediately.
|
||||
|
||||
## Models.dev Example
|
||||
|
||||
Models.dev performs effectful reads directly from its transforms and rebuilds affected domains after refresh.
|
||||
|
||||
```ts
|
||||
export const ModelsDevPlugin = define({
|
||||
id: "models-dev",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
const event = yield* EventV2.Service
|
||||
|
||||
yield* ctx.integration.transform(
|
||||
Effect.fn(function* (integration) {
|
||||
const data = yield* modelsDev.get()
|
||||
applyIntegrations(data, integration)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (catalog) {
|
||||
const data = yield* modelsDev.get()
|
||||
applyCatalog(data, catalog)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* event.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
yield* ctx.integration.rebuild()
|
||||
yield* ctx.catalog.rebuild()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
The two domains rebuild sequentially. This plan does not add a cross-domain atomic transaction.
|
||||
|
||||
## Config Watcher Example
|
||||
|
||||
```ts
|
||||
export const ConfigPlugin = define({
|
||||
id: "config",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* ConfigSource.Service
|
||||
|
||||
yield* ctx.agent.transform(
|
||||
Effect.fn(function* (agent) {
|
||||
applyAgentConfig(yield* config.get(), agent)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.command.transform(
|
||||
Effect.fn(function* (command) {
|
||||
applyCommandConfig(yield* config.get(), command)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* config.changes.pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
yield* ctx.agent.rebuild()
|
||||
yield* ctx.command.rebuild()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
## Cross-Domain Read Example
|
||||
|
||||
A transform may read another committed service. It must still arrange for its own domain to rebuild when that dependency changes.
|
||||
|
||||
```ts
|
||||
export const AnthropicAgentPlugin = define({
|
||||
id: "anthropic-agent",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.agent.transform(
|
||||
Effect.fn(function* (agent) {
|
||||
const providers = yield* ctx.catalog.provider.list()
|
||||
if (!providers.some((provider) => provider.id === "anthropic")) return
|
||||
|
||||
agent.update("anthropic-reviewer", (item) => {
|
||||
item.description = "Reviews code using Anthropic"
|
||||
item.mode = "subagent"
|
||||
item.model = {
|
||||
providerID: "anthropic",
|
||||
id: "claude-sonnet",
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.event.subscribe("catalog.updated").pipe(
|
||||
Stream.runForEach(() => ctx.agent.rebuild()),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
The runtime does not infer cross-domain dependencies.
|
||||
|
||||
## Embedding API Compatibility
|
||||
|
||||
The imperative registration model maps naturally to a future application embedding API:
|
||||
|
||||
```ts
|
||||
const registration = oc.agent.transform((agent) => {
|
||||
agent.update("reviewer", configureReviewer)
|
||||
})
|
||||
|
||||
registration.dispose()
|
||||
```
|
||||
|
||||
An application registration is stored as an application-level plugin registration. It attaches to every current Location and is installed during future Location boot. Disposal removes all current attachments and prevents future attachment.
|
||||
|
||||
The Effect implementation remains the canonical runtime. Promise and embedding wrappers are deferred until after the Effect API is stable.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 1. Define Public Contracts
|
||||
|
||||
- Define `PluginHost` domain capabilities in `@opencode-ai/plugin/v2/effect`.
|
||||
- Define SDK-typed editors for agent, catalog, command, integration, reference, skill, and tool.
|
||||
- Define typed runtime hook maps per domain.
|
||||
- Define `Registration`.
|
||||
- Define typed `event.subscribe(type)`.
|
||||
|
||||
### 2. Generalize Registration Machinery
|
||||
|
||||
- Add one low-level scoped registration registry used by transforms and runtime hooks.
|
||||
- Preserve plugin order and registration order.
|
||||
- Support idempotent disposal and registration snapshots.
|
||||
- Retain plugin position during same-ID replacement.
|
||||
|
||||
### 3. Evolve State
|
||||
|
||||
- Replace the current returned transform-slot updater with direct `transform(callback)` registration.
|
||||
- Support Effectful callbacks.
|
||||
- Add public `rebuild()`.
|
||||
- Add rebuild serialization and coalescing.
|
||||
- Add boot batching that defers automatic rebuilds.
|
||||
- Move update event publication after commit.
|
||||
|
||||
### 4. Expand Domain Transform Hooks
|
||||
|
||||
- Agent
|
||||
- Catalog
|
||||
- Command
|
||||
- Integration
|
||||
- Reference
|
||||
- Skill
|
||||
- Tool
|
||||
|
||||
### 5. Migrate Existing Plugins
|
||||
|
||||
- Built-in agent transform
|
||||
- Built-in command transform
|
||||
- Built-in skill transform
|
||||
- Models.dev catalog and integration transforms
|
||||
- Config transforms
|
||||
- OpenAI integration transform
|
||||
- Provider catalog transforms
|
||||
|
||||
### 6. Migrate Runtime Hooks
|
||||
|
||||
- AI SDK resolution
|
||||
- Language model resolution
|
||||
- Tool execution hooks
|
||||
- Session prompt/context hooks as required
|
||||
|
||||
### 7. Remove Returned Hooks
|
||||
|
||||
- Remove `HookFunctions` as the plugin setup return value.
|
||||
- Remove catalog's special finalizer-triggered plugin hook path.
|
||||
- Remove `plugin.added` catalog mutation handling.
|
||||
- Make add/remove/replacement rely on scoped registration and domain rebuilds.
|
||||
|
||||
### 8. Add Event Adapter
|
||||
|
||||
- Build the SDK event discriminant map.
|
||||
- Resolve public type strings to internal EventV2 definitions.
|
||||
- Return typed Effect streams.
|
||||
|
||||
### 9. Verification
|
||||
|
||||
- Transform order is deterministic.
|
||||
- Multiple transforms per plugin/domain compose.
|
||||
- Registration and disposal rebuild automatically outside boot batches.
|
||||
- Boot performs one rebuild per affected domain.
|
||||
- Plugin setup failure removes prior registrations.
|
||||
- Same-ID replacement retains order and disables the old plugin first.
|
||||
- Rebuilds serialize and coalesce.
|
||||
- Registration changes during replay affect the next rebuild.
|
||||
- Same-domain recursive rebuild is rejected.
|
||||
- Cross-domain rebuild requests from transforms are deferred.
|
||||
- Hook execution is sequential and snapshot-based.
|
||||
- Models.dev refresh replays config and provider transforms.
|
||||
- Config and skill watcher refreshes remove stale entries.
|
||||
- Plugin removal restores prior effective state.
|
||||
- Events observe newly committed state.
|
||||
|
||||
## Deferred Decisions
|
||||
|
||||
- Promise API shape
|
||||
- Typed error model
|
||||
- Transform timeouts
|
||||
- Cross-domain atomic rebuilds
|
||||
- Automatic dependency tracking
|
||||
- Whole-Location generation reload
|
||||
- Exact editors and runtime hooks not required by current plugins
|
||||
585
packages/plugin/src/v2/effect/README.md
Normal file
585
packages/plugin/src/v2/effect/README.md
Normal file
|
|
@ -0,0 +1,585 @@
|
|||
# OpenCode V2 Plugin API
|
||||
|
||||
> Design proposal. The API shown here is the intended V2 model and is not fully implemented yet.
|
||||
|
||||
This document explains how OpenCode V2 plugins contribute agents, commands, skills, integrations, providers, and models without importing `@opencode-ai/core`.
|
||||
|
||||
The design has four goals:
|
||||
|
||||
- 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.
|
||||
|
||||
```ts
|
||||
export default defineEffectPlugin({
|
||||
id: "example",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
return {
|
||||
"agent.transform": (agent) => {
|
||||
// Describe this plugin's agent contribution.
|
||||
},
|
||||
}
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
A transform is not a one-time mutation. It is a replayable declaration.
|
||||
|
||||
OpenCode may run it when:
|
||||
|
||||
- The plugin is added.
|
||||
- The plugin is removed or replaced.
|
||||
- Another plugin affecting the same domain changes.
|
||||
- The plugin explicitly invalidates the domain.
|
||||
|
||||
Transforms must therefore be synchronous, deterministic, and safe to rerun.
|
||||
|
||||
## Why Hooks Are Returned
|
||||
|
||||
Each transform is a singular property of the plugin definition:
|
||||
|
||||
```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"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
An editor is valid only during the transform call. Do not retain it in plugin state.
|
||||
|
||||
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.
|
||||
|
||||
```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,
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```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 *
|
||||
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)
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The plugin owns changing source data. The runtime owns hook ordering, replay, invalidation, cleanup, and commit. Core services own their state and finalization.
|
||||
17
packages/plugin/src/v2/effect/agent.ts
Normal file
17
packages/plugin/src/v2/effect/agent.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
|
||||
export interface AgentDraft {
|
||||
list(): readonly AgentV2Info[]
|
||||
get(id: string): AgentV2Info | undefined
|
||||
default(id: string | undefined): void
|
||||
update(id: string, update: (agent: AgentV2Info) => void): void
|
||||
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[]>
|
||||
}
|
||||
21
packages/plugin/src/v2/effect/aisdk.ts
Normal file
21
packages/plugin/src/v2/effect/aisdk.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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"
|
||||
|
||||
export interface AISDKHooks {
|
||||
readonly sdk: (event: {
|
||||
readonly model: ModelV2Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: any
|
||||
}) => Effect.Effect<void> | void
|
||||
readonly language: (event: {
|
||||
readonly model: ModelV2Info
|
||||
readonly sdk: any
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}) => Effect.Effect<void> | void
|
||||
}
|
||||
|
||||
export interface AISDK extends Hookable<AISDKHooks> {}
|
||||
41
packages/plugin/src/v2/effect/catalog.ts
Normal file
41
packages/plugin/src/v2/effect/catalog.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
|
||||
export interface CatalogProviderRecord {
|
||||
readonly provider: ProviderV2Info
|
||||
readonly models: ReadonlyMap<string, ModelV2Info>
|
||||
}
|
||||
|
||||
export interface CatalogDraft {
|
||||
readonly provider: {
|
||||
list(): readonly CatalogProviderRecord[]
|
||||
get(providerID: string): CatalogProviderRecord | undefined
|
||||
update(providerID: string, update: (provider: ProviderV2Info) => void): void
|
||||
remove(providerID: string): void
|
||||
}
|
||||
readonly model: {
|
||||
get(providerID: string, modelID: string): ModelV2Info | undefined
|
||||
update(providerID: string, modelID: string, update: (model: ModelV2Info) => void): void
|
||||
remove(providerID: string, modelID: string): void
|
||||
readonly default: {
|
||||
get(): { providerID: string; modelID: string } | undefined
|
||||
set(providerID: string, modelID: string): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
15
packages/plugin/src/v2/effect/command.ts
Normal file
15
packages/plugin/src/v2/effect/command.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { CommandV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
|
||||
export interface CommandDraft {
|
||||
list(): readonly CommandV2Info[]
|
||||
get(name: string): CommandV2Info | undefined
|
||||
update(name: string, update: (command: CommandV2Info) => void): void
|
||||
remove(name: string): void
|
||||
}
|
||||
|
||||
export interface Command extends Transformable<CommandDraft> {
|
||||
get(name: string): Effect.Effect<CommandV2Info | undefined>
|
||||
list(): Effect.Effect<CommandV2Info[]>
|
||||
}
|
||||
10
packages/plugin/src/v2/effect/event.ts
Normal file
10
packages/plugin/src/v2/effect/event.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Event as SDKEvent } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Stream } from "effect"
|
||||
|
||||
export type EventMap = {
|
||||
[Item in SDKEvent as Item["type"]]: Item
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
subscribe<Type extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]>
|
||||
}
|
||||
17
packages/plugin/src/v2/effect/filesystem.ts
Normal file
17
packages/plugin/src/v2/effect/filesystem.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { FileSystemEntry } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
|
||||
export interface FileSystem {
|
||||
read(input: { readonly path: string }): Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
|
||||
list(input?: { readonly path?: string }): Effect.Effect<FileSystemEntry[]>
|
||||
find(input: {
|
||||
readonly query: string
|
||||
readonly type?: "file" | "directory"
|
||||
readonly limit?: number
|
||||
}): Effect.Effect<FileSystemEntry[]>
|
||||
glob(input: {
|
||||
readonly pattern: string
|
||||
readonly path?: string
|
||||
readonly limit?: number
|
||||
}): Effect.Effect<readonly FileSystemEntry[]>
|
||||
}
|
||||
27
packages/plugin/src/v2/effect/host.ts
Normal file
27
packages/plugin/src/v2/effect/host.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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
|
||||
}
|
||||
17
packages/plugin/src/v2/effect/index.ts
Normal file
17
packages/plugin/src/v2/effect/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export type { PluginHost } from "./host.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"
|
||||
36
packages/plugin/src/v2/effect/integration.ts
Normal file
36
packages/plugin/src/v2/effect/integration.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type {
|
||||
IntegrationEnvMethod,
|
||||
IntegrationInfo,
|
||||
IntegrationKeyMethod,
|
||||
IntegrationOAuthMethod,
|
||||
} from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
|
||||
export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod
|
||||
export type IntegrationMethodRegistration =
|
||||
| {
|
||||
readonly integrationID: string
|
||||
readonly method: IntegrationKeyMethod
|
||||
}
|
||||
| {
|
||||
readonly integrationID: string
|
||||
readonly method: IntegrationEnvMethod
|
||||
}
|
||||
|
||||
export interface IntegrationDraft {
|
||||
list(): readonly Pick<IntegrationInfo, "id" | "name">[]
|
||||
get(id: string): Pick<IntegrationInfo, "id" | "name"> | undefined
|
||||
update(id: string, update: (integration: Pick<IntegrationInfo, "id" | "name">) => void): void
|
||||
remove(id: string): void
|
||||
readonly method: {
|
||||
list(integrationID: string): readonly IntegrationMethod[]
|
||||
update(input: IntegrationMethodRegistration): void
|
||||
remove(integrationID: string, method: IntegrationMethod): void
|
||||
}
|
||||
}
|
||||
|
||||
export interface Integration extends Transformable<IntegrationDraft> {
|
||||
get(id: string): Effect.Effect<IntegrationInfo | undefined>
|
||||
list(): Effect.Effect<IntegrationInfo[]>
|
||||
}
|
||||
6
packages/plugin/src/v2/effect/location.ts
Normal file
6
packages/plugin/src/v2/effect/location.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface Location {
|
||||
readonly directory: string
|
||||
readonly project: {
|
||||
readonly directory: string
|
||||
}
|
||||
}
|
||||
11
packages/plugin/src/v2/effect/npm.ts
Normal file
11
packages/plugin/src/v2/effect/npm.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { Effect } from "effect"
|
||||
|
||||
export interface Npm {
|
||||
add(pkg: string): Effect.Effect<
|
||||
{
|
||||
readonly directory: string
|
||||
readonly entrypoint?: string
|
||||
},
|
||||
unknown
|
||||
>
|
||||
}
|
||||
8
packages/plugin/src/v2/effect/path.ts
Normal file
8
packages/plugin/src/v2/effect/path.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface Path {
|
||||
readonly home: string
|
||||
readonly data: string
|
||||
readonly cache: string
|
||||
readonly config: string
|
||||
readonly state: string
|
||||
readonly temp: string
|
||||
}
|
||||
11
packages/plugin/src/v2/effect/plugin.ts
Normal file
11
packages/plugin/src/v2/effect/plugin.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { Effect, Scope } from "effect"
|
||||
import type { PluginHost } from "./host.js"
|
||||
|
||||
export interface Plugin<R = never> {
|
||||
readonly id: string
|
||||
readonly effect: (host: PluginHost) => Effect.Effect<void, never, R | Scope.Scope>
|
||||
}
|
||||
|
||||
export function define<R>(plugin: Plugin<R>) {
|
||||
return plugin
|
||||
}
|
||||
13
packages/plugin/src/v2/effect/reference.ts
Normal file
13
packages/plugin/src/v2/effect/reference.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { ReferenceGitSource, ReferenceInfo, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
|
||||
export interface ReferenceDraft {
|
||||
add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void
|
||||
remove(name: string): void
|
||||
list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[]
|
||||
}
|
||||
|
||||
export interface Reference extends Transformable<ReferenceDraft> {
|
||||
list(): Effect.Effect<ReferenceInfo[]>
|
||||
}
|
||||
16
packages/plugin/src/v2/effect/registration.ts
Normal file
16
packages/plugin/src/v2/effect/registration.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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 Hookable<Hooks> {
|
||||
hook<Name extends keyof Hooks>(name: Name, callback: Hooks[Name]): Effect.Effect<Registration, never, Scope.Scope>
|
||||
}
|
||||
18
packages/plugin/src/v2/effect/skill.ts
Normal file
18
packages/plugin/src/v2/effect/skill.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { SkillV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transformable } from "./registration.js"
|
||||
|
||||
export type SkillSource =
|
||||
| { readonly type: "directory"; readonly path: string }
|
||||
| { readonly type: "url"; readonly url: string }
|
||||
| { readonly type: "embedded"; readonly skill: SkillV2Info }
|
||||
|
||||
export interface SkillDraft {
|
||||
source(source: SkillSource): void
|
||||
list(): readonly SkillSource[]
|
||||
}
|
||||
|
||||
export interface Skill extends Transformable<SkillDraft> {
|
||||
sources(): Effect.Effect<SkillSource[]>
|
||||
list(): Effect.Effect<SkillV2Info[]>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue