feat(plugin): align hooks with client APIs

This commit is contained in:
Dax Raad
2026-07-03 18:12:22 -04:00
parent 650d774372
commit 6ae2fa5196
65 changed files with 981 additions and 667 deletions
+2
View File
@@ -122,6 +122,7 @@
}, },
"packages/client": { "packages/client": {
"name": "@opencode-ai/client", "name": "@opencode-ai/client",
"version": "1.17.13",
"dependencies": { "dependencies": {
"@opencode-ai/protocol": "workspace:*", "@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
@@ -695,6 +696,7 @@
"version": "1.17.13", "version": "1.17.13",
"dependencies": { "dependencies": {
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/protocol": "workspace:*", "@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
+17 -3
View File
@@ -1,16 +1,30 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/client", "name": "@opencode-ai/client",
"private": true, "version": "1.17.13",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/opencode.git",
"directory": "packages/client"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"exports": { "exports": {
"./promise": "./src/promise/index.ts", "./promise": "./src/promise/index.ts",
"./effect": "./src/effect/index.ts" "./promise/api": "./src/promise/api.ts",
"./effect": "./src/effect/index.ts",
"./effect/api": "./src/effect/api.ts"
}, },
"scripts": { "scripts": {
"build": "bun run script/build-package.ts",
"generate": "bun run script/build.ts", "generate": "bun run script/build.ts",
"check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated", "check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated src/effect/api",
"test": "bun test --timeout 5000", "test": "bun test --timeout 5000",
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
}, },
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await $`rm -rf dist`
await $`bun tsc -p tsconfig.build.json`
+2 -2
View File
@@ -32,8 +32,8 @@ await Effect.runPromise(
fileURLToPath(new URL("../src/effect/generated", import.meta.url)), fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
), ),
write( write(
emitEffectShape(effectContract, { module: "@opencode-ai/protocol/client", api: "ClientApi" }), emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }),
fileURLToPath(new URL("../../plugin/src/v2/effect/generated", import.meta.url)), fileURLToPath(new URL("../src/effect/api", import.meta.url)),
), ),
], ],
{ concurrency: 3, discard: true }, { concurrency: 3, discard: true },
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string | { import: string; types: string }>
}
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
console.log(`already published ${pkg.name}@${pkg.version}`)
process.exit(0)
}
try {
await $`bun run typecheck`
await $`bun run build`
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [
key,
{
import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"),
types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"),
},
]
}),
)
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
await rm(tarball, { force: true })
await $`bun pm pack`
await $`npm publish ${tarball} --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
await rm(tarball, { force: true })
}
+8
View File
@@ -0,0 +1,8 @@
import type { ModelApi, ProviderApi } from "./api/api.js"
export type * from "./api/api.js"
export interface CatalogApi<E = never> {
readonly provider: ProviderApi<E>
readonly model: ModelApi<E>
}
@@ -1,7 +1,7 @@
// Generated by @opencode-ai/httpapi-codegen. Do not edit. // Generated by @opencode-ai/httpapi-codegen. Do not edit.
import type { Effect, Stream } from "effect" import type { Effect, Stream } from "effect"
import type { HttpApiClient } from "effect/unstable/httpapi" import type { HttpApiClient } from "effect/unstable/httpapi"
import type { ClientApi } from "@opencode-ai/protocol/client" import type { ClientApi } from "../../contract"
type RawClient = HttpApiClient.ForApi<typeof ClientApi> type RawClient = HttpApiClient.ForApi<typeof ClientApi>
type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success : never type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success : never
+16
View File
@@ -1,6 +1,21 @@
// TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import // TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations. // Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
import type { Effect } from "effect"
export * from "./generated/index" export * from "./generated/index"
export type {
AgentApi,
AppApi,
CatalogApi,
CommandApi,
IntegrationApi,
ModelApi,
PluginApi,
ProviderApi,
ReferenceApi,
SessionApi,
SkillApi,
} from "./api.js"
export { Service } from "./service.js" export { Service } from "./service.js"
export { Agent } from "@opencode-ai/schema/agent" export { Agent } from "@opencode-ai/schema/agent"
export { Command } from "@opencode-ai/schema/command" export { Command } from "@opencode-ai/schema/command"
@@ -27,3 +42,4 @@ export { SessionMessage } from "@opencode-ai/schema/session-message"
export { Skill } from "@opencode-ai/schema/skill" export { Skill } from "@opencode-ai/schema/skill"
export { Prompt } from "@opencode-ai/schema/prompt" export { Prompt } from "@opencode-ai/schema/prompt"
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
export type OpenCodeClient = Effect.Success<ReturnType<typeof import("./generated/client").make>>
+39
View File
@@ -0,0 +1,39 @@
import type {
AgentApi as EffectAgentApi,
CommandApi as EffectCommandApi,
IntegrationApi as EffectIntegrationApi,
ModelApi as EffectModelApi,
PluginApi as EffectPluginApi,
ProviderApi as EffectProviderApi,
ReferenceApi as EffectReferenceApi,
SessionApi as EffectSessionApi,
SkillApi as EffectSkillApi,
} from "../effect/api/api.js"
import type { Effect, Stream } from "effect"
type PromisifyOperation<Operation> = Operation extends (
...args: infer Args
) => Effect.Effect<infer Success, unknown, unknown>
? (...args: Args) => Promise<Success>
: Operation extends (...args: infer Args) => Stream.Stream<infer Success, unknown, unknown>
? (...args: Args) => AsyncIterable<Success>
: Operation
type PromisifyApi<Api> = {
readonly [Name in keyof Api]: PromisifyOperation<Api[Name]>
}
export type AgentApi = PromisifyApi<EffectAgentApi<unknown>>
export type CommandApi = PromisifyApi<EffectCommandApi<unknown>>
export type IntegrationApi = PromisifyApi<EffectIntegrationApi<unknown>>
export type ModelApi = PromisifyApi<EffectModelApi<unknown>>
export type PluginApi = PromisifyApi<EffectPluginApi<unknown>>
export type ProviderApi = PromisifyApi<EffectProviderApi<unknown>>
export type ReferenceApi = PromisifyApi<EffectReferenceApi<unknown>>
export type SessionApi = PromisifyApi<EffectSessionApi<unknown>>
export type SkillApi = PromisifyApi<EffectSkillApi<unknown>>
export interface CatalogApi {
readonly provider: ProviderApi
readonly model: ModelApi
}
+12
View File
@@ -1,3 +1,15 @@
export * from "./generated/index" export * from "./generated/index"
export type {
AgentApi,
CatalogApi,
CommandApi,
IntegrationApi,
ModelApi,
PluginApi,
ProviderApi,
ReferenceApi,
SessionApi,
SkillApi,
} from "./api.js"
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types" export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make> export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
+10
View File
@@ -0,0 +1,10 @@
import { Effect } from "effect"
import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect"
type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>>
declare const effectClient: EffectClient
const effectApi: EffectApi<unknown> = effectClient
void effectApi
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"noEmit": false,
"declaration": true
},
"include": ["src"]
}
+2
View File
@@ -3,6 +3,8 @@
"extends": "@tsconfig/bun/tsconfig.json", "extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"], "lib": ["ESNext", "DOM", "DOM.Iterable"],
"allowImportingTsExtensions": false,
"allowJs": false,
"noUncheckedIndexedAccess": false "noUncheckedIndexedAccess": false
}, },
"include": ["src"] "include": ["src"]
+54 -56
View File
@@ -38,65 +38,63 @@ export const Plugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service const config = yield* Config.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
yield* ctx.agent.transform( const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
Effect.fn(function* (draft) { if (entry.type === "document") return Effect.succeed([entry])
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { return Effect.gen(function* () {
if (entry.type === "document") return Effect.succeed([entry]) const files = yield* discover(fs, entry.path)
return Effect.gen(function* () { return yield* Effect.forEach(files, (file) =>
const files = yield* discover(fs, entry.path) fs.readFileStringSafe(file.filepath).pipe(
return yield* Effect.forEach(files, (file) => Effect.map((content) => content && decode(file, content)),
fs.readFileStringSafe(file.filepath).pipe( Effect.catch(() => Effect.succeed(undefined)),
Effect.map((content) => content && decode(file, content)), ),
Effect.catch(() => Effect.succeed(undefined)), ).pipe(
), Effect.map((documents) =>
).pipe( documents.filter((document): document is Config.Document => document !== undefined),
Effect.map((documents) => ),
documents.filter((document): document is Config.Document => document !== undefined), )
), })
) }).pipe(Effect.map((documents) => documents.flat()))
}) const global = documents.flatMap((document) => document.info.permissions ?? [])
}).pipe(Effect.map((documents) => documents.flat())) const configuredDefault = Config.latest(documents, "default_agent")
const global = documents.flatMap((document) => document.info.permissions ?? []) yield* ctx.agent.transform((draft) => {
const configuredDefault = Config.latest(documents, "default_agent") if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault)) for (const current of draft.list()) {
for (const current of draft.list()) { draft.update(current.id, (agent) => agent.permissions.push(...global))
draft.update(current.id, (agent) => agent.permissions.push(...global)) }
}
for (const document of documents) { for (const document of documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) { for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = AgentV2.ID.make(id) const agentID = AgentV2.ID.make(id)
if (item.disabled) { if (item.disabled) {
draft.remove(agentID) draft.remove(agentID)
continue continue
}
const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) {
const model = ModelV2.parse(item.model)
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
}
if (item.variant !== undefined && agent.model !== undefined) {
agent.model.variant = ModelV2.VariantID.make(item.variant)
}
if (item.request !== undefined) {
Object.assign(agent.request.headers, item.request.headers ?? {})
Object.assign(agent.request.body, item.request.body ?? {})
}
if (item.system !== undefined) agent.system = item.system
if (item.description !== undefined) agent.description = item.description
if (item.mode !== undefined) agent.mode = item.mode
if (item.hidden !== undefined) agent.hidden = item.hidden
if (item.color !== undefined) agent.color = item.color
if (item.steps !== undefined) agent.steps = item.steps
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
})
} }
const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) {
const model = ModelV2.parse(item.model)
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
}
if (item.variant !== undefined && agent.model !== undefined) {
agent.model.variant = ModelV2.VariantID.make(item.variant)
}
if (item.request !== undefined) {
Object.assign(agent.request.headers, item.request.headers ?? {})
Object.assign(agent.request.body, item.request.body ?? {})
}
if (item.system !== undefined) agent.system = item.system
if (item.description !== undefined) agent.description = item.description
if (item.mode !== undefined) agent.mode = item.mode
if (item.hidden !== undefined) agent.hidden = item.hidden
if (item.color !== undefined) agent.color = item.color
if (item.steps !== undefined) agent.steps = item.steps
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
})
} }
}), }
) })
}), }),
}) })
+26 -28
View File
@@ -17,35 +17,33 @@ export const Plugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service const config = yield* Config.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
yield* ctx.command.transform( const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
Effect.fn(function* (draft) { if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { return loadDirectory(fs, entry.path).pipe(
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }]) Effect.map((commands) => [
return loadDirectory(fs, entry.path).pipe( { commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
Effect.map((commands) => [ ]),
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) }, )
]), }).pipe(Effect.map((documents) => documents.flat()))
) yield* ctx.command.transform((draft) => {
}).pipe(Effect.map((documents) => documents.flat())) for (const document of documents) {
for (const document of documents) { for (const [name, command] of Object.entries(document.commands ?? {})) {
for (const [name, command] of Object.entries(document.commands ?? {})) { draft.update(name, (item) => {
draft.update(name, (item) => { item.template = command.template
item.template = command.template if (command.description !== undefined) item.description = command.description
if (command.description !== undefined) item.description = command.description if (command.agent !== undefined) item.agent = command.agent
if (command.agent !== undefined) item.agent = command.agent if (command.model !== undefined) {
if (command.model !== undefined) { const model = ModelV2.parse(command.model)
const model = ModelV2.parse(command.model) item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant } }
} if (command.variant !== undefined && item.model !== undefined) {
if (command.variant !== undefined && item.model !== undefined) { item.model.variant = ModelV2.VariantID.make(command.variant)
item.model.variant = ModelV2.VariantID.make(command.variant) }
} if (command.subtask !== undefined) item.subtask = command.subtask
if (command.subtask !== undefined) item.subtask = command.subtask })
})
}
} }
}), }
) })
}), }),
}) })
+89 -96
View File
@@ -9,108 +9,101 @@ export const Plugin = define({
id: "config-provider", id: "config-provider",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service const config = yield* Config.Service
yield* ctx.integration.transform( const entries = yield* config.entries()
Effect.fn(function* (integrations) { const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document") const configuredIntegrations = new Set(
const configuredIntegrations = new Set( files.flatMap((file) =>
files.flatMap((file) => Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => (provider.env === undefined ? [] : [id])),
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => ),
provider.env === undefined ? [] : [id], )
), yield* ctx.integration.transform((integrations) => {
), for (const file of files) {
) for (const [id, item] of Object.entries(file.info.providers ?? {})) {
for (const file of files) { const integrationID = id
for (const [id, item] of Object.entries(file.info.providers ?? {})) { if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
const integrationID = id integrations.update(integrationID, (integration) => {
if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue integration.name = item.name ?? integration.name
integrations.update(integrationID, (integration) => { })
integration.name = item.name ?? integration.name if (item.env !== undefined) {
integrations.method.update({
integrationID,
method: { type: "env", names: [...item.env] },
}) })
if (item.env !== undefined) {
integrations.method.update({
integrationID,
method: { type: "env", names: [...item.env] },
})
}
} }
} }
}), }
) })
yield* ctx.catalog.transform( const configuredDefault = Config.latest(entries, "model")
Effect.fn(function* (catalog) { yield* ctx.catalog.transform((catalog) => {
const entries = yield* config.entries() if (configuredDefault !== undefined) {
const files = entries.filter((entry): entry is Config.Document => entry.type === "document") const model = ModelV2.parse(configuredDefault)
const configuredDefault = Config.latest(entries, "model") catalog.model.default.set(model.providerID, model.modelID)
if (configuredDefault !== undefined) { }
const model = ModelV2.parse(configuredDefault) for (const file of files) {
catalog.model.default.set(model.providerID, model.modelID) for (const [id, item] of Object.entries(file.info.providers ?? {})) {
} const providerID = id
for (const file of files) { catalog.provider.update(providerID, (provider) => {
for (const [id, item] of Object.entries(file.info.providers ?? {})) { if (item.name !== undefined) provider.name = item.name
const providerID = id if (item.api !== undefined) provider.api = { ...item.api }
catalog.provider.update(providerID, (provider) => { if (item.request !== undefined) {
if (item.name !== undefined) provider.name = item.name Object.assign(provider.request.settings, item.request.settings)
if (item.api !== undefined) provider.api = { ...item.api } Object.assign(provider.request.headers, item.request.headers)
if (item.request !== undefined) { Object.assign(provider.request.body, item.request.body)
Object.assign(provider.request.settings, item.request.settings)
Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body)
}
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
input: [...config.capabilities.input],
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
Object.assign(model.request.settings, config.request.settings)
Object.assign(model.request.headers, config.request.headers)
Object.assign(model.request.body, config.request.body)
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) {
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
settings: {},
headers: {},
body: {},
}
model.variants.push(existing)
}
Object.assign(existing.settings, variant.settings)
Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body)
}
}
if (config.cost !== undefined) {
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
tier: cost.tier && { ...cost.tier },
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
if (config.disabled !== undefined) model.enabled = !config.disabled
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
})
} }
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
input: [...config.capabilities.input],
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
Object.assign(model.request.settings, config.request.settings)
Object.assign(model.request.headers, config.request.headers)
Object.assign(model.request.body, config.request.body)
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) {
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
settings: {},
headers: {},
body: {},
}
model.variants.push(existing)
}
Object.assign(existing.settings, variant.settings)
Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body)
}
}
if (config.cost !== undefined) {
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
tier: cost.tier && { ...cost.tier },
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
if (config.disabled !== undefined) model.enabled = !config.disabled
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
})
} }
} }
}), }
) })
}), }),
}) })
+29 -35
View File
@@ -16,41 +16,35 @@ export const Plugin = define({
const config = yield* Config.Service const config = yield* Config.Service
const location = yield* Location.Service const location = yield* Location.Service
const global = yield* Global.Service const global = yield* Global.Service
yield* ctx.reference.transform( const entries = new Map<string, Reference.Source>()
Effect.fn(function* (draft) { for (const doc of (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")) {
const entries = new Map<string, Reference.Source>() const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const doc of (yield* config.entries()).filter( for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
(entry): entry is Config.Document => entry.type === "document", if (!validAlias(name)) continue
)) { const description = typeof entry === "string" ? undefined : entry.description
const directory = doc.path ? path.dirname(doc.path) : location.directory const hidden = typeof entry === "string" ? undefined : entry.hidden
for (const [name, entry] of Object.entries(doc.info.references ?? {})) { entries.set(
if (!validAlias(name)) continue name,
const description = typeof entry === "string" ? undefined : entry.description local(entry)
const hidden = typeof entry === "string" ? undefined : entry.hidden ? Reference.LocalSource.make({
entries.set( type: "local",
name, path: AbsolutePath.make(localPath(directory, global.home, typeof entry === "string" ? entry : entry.path)),
local(entry) ...(description === undefined ? {} : { description }),
? Reference.LocalSource.make({ ...(hidden === undefined ? {} : { hidden }),
type: "local", })
path: AbsolutePath.make( : Reference.GitSource.make({
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path), type: "git",
), repository: typeof entry === "string" ? entry : entry.repository,
...(description === undefined ? {} : { description }), ...(entry.branch === undefined ? {} : { branch: entry.branch }),
...(hidden === undefined ? {} : { hidden }), ...(description === undefined ? {} : { description }),
}) ...(hidden === undefined ? {} : { hidden }),
: Reference.GitSource.make({ }),
type: "git", )
repository: typeof entry === "string" ? entry : entry.repository, }
...(entry.branch === undefined ? {} : { branch: entry.branch }), }
...(description === undefined ? {} : { description }), yield* ctx.reference.transform((draft) => {
...(hidden === undefined ? {} : { hidden }), for (const [name, source] of entries) draft.add(name, source)
}), })
)
}
}
for (const [name, source] of entries) draft.add(name, source)
}),
)
}), }),
}) })
+28 -30
View File
@@ -15,36 +15,34 @@ export const Plugin = define({
const config = yield* Config.Service const config = yield* Config.Service
const global = yield* Global.Service const global = yield* Global.Service
const location = yield* Location.Service const location = yield* Location.Service
yield* ctx.skill.transform( const entries = yield* config.entries()
Effect.fn(function* (draft) { const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const entries = yield* config.entries() const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : [])) yield* ctx.skill.transform((draft) => {
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) for (const directory of directories) {
for (const directory of directories) { draft.source(
draft.source( SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), )
) draft.source(
draft.source( SkillV2.DirectorySource.make({
SkillV2.DirectorySource.make({ type: "directory",
type: "directory", path: AbsolutePath.make(path.join(directory, "skills")),
path: AbsolutePath.make(path.join(directory, "skills")), }),
}), )
) }
for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
draft.source(SkillV2.UrlSource.make({ type: "url", url: item }))
continue
} }
for (const item of items) { const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { draft.source(
draft.source(SkillV2.UrlSource.make({ type: "url", url: item })) SkillV2.DirectorySource.make({
continue type: "directory",
} path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item }),
draft.source( )
SkillV2.DirectorySource.make({ }
type: "directory", })
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}),
)
}
}),
)
}), }),
}) })
+63 -16
View File
@@ -52,6 +52,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}) })
const isCurrentLocation = (ref: Location.Ref) => const isCurrentLocation = (ref: Location.Ref) =>
ref.directory === location.directory && ref.workspaceID === location.workspaceID ref.directory === location.directory && ref.workspaceID === location.workspaceID
const response = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(Effect.map((data) => ({ location: locationInfo(), data })))
return { return {
options: {}, options: {},
@@ -63,15 +65,15 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}, },
reload: agents.reload, reload: agents.reload,
transform: (callback) => transform: (callback) =>
agents.transform((draft) => agents.transform((draft) => {
callback({ callback({
list: () => mutable(draft.list()), list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(AgentV2.ID.make(id))), get: (id) => mutable(draft.get(AgentV2.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) => draft.update(AgentV2.ID.make(id), update), update: (id, update) => draft.update(AgentV2.ID.make(id), update),
remove: (id) => draft.remove(AgentV2.ID.make(id)), remove: (id) => draft.remove(AgentV2.ID.make(id)),
}), })
), }),
}, },
aisdk: { aisdk: {
sdk: (callback) => sdk: (callback) =>
@@ -102,9 +104,26 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}), }),
}, },
catalog: { catalog: {
provider: {
list: () => response(catalog.provider.available()),
get: (input) =>
catalog.provider
.get(ProviderV2.ID.make(input.providerID))
.pipe(
Effect.flatMap((provider) =>
provider === undefined
? Effect.fail(new Error(`Provider not found: ${input.providerID}`))
: response(Effect.succeed(provider)),
),
),
},
model: {
list: () => response(catalog.model.available()),
default: () => response(catalog.model.default()),
},
reload: catalog.reload, reload: catalog.reload,
transform: (callback) => transform: (callback) =>
catalog.transform((draft) => catalog.transform((draft) => {
callback({ callback({
provider: { provider: {
list: () => mutable(draft.provider.list()), list: () => mutable(draft.provider.list()),
@@ -125,14 +144,39 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
}, },
}, },
}), })
), }),
}, },
command: { command: {
list: () => response(commands.list()),
reload: commands.reload, reload: commands.reload,
transform: commands.transform, transform: (callback) =>
commands.transform((draft) => {
callback(draft)
}),
}, },
integration: { integration: {
list: () => response(integration.list()),
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
connectKey: (input) =>
integration.connection.key({
integrationID: Integration.ID.make(input.integrationID),
key: input.key,
label: input.label,
}),
connectOauth: (input) =>
response(
integration.connection.oauth({
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
inputs: input.inputs,
label: input.label,
}),
),
attemptStatus: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
attemptComplete: (input) =>
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
attemptCancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
reload: integration.reload, reload: integration.reload,
connection: { connection: {
active: (id) => integration.connection.active(Integration.ID.make(id)), active: (id) => integration.connection.active(Integration.ID.make(id)),
@@ -142,7 +186,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
), ),
}, },
transform: (callback) => transform: (callback) =>
integration.transform((draft) => integration.transform((draft) => {
callback({ callback({
list: () => mutable(draft.list()), list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(Integration.ID.make(id))), get: (id) => mutable(draft.get(Integration.ID.make(id))),
@@ -219,33 +263,36 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
remove: (id, method) => remove: (id, method) =>
draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)), draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
}, },
}), })
), }),
}, },
plugin: { plugin: {
list: () => response(plugin.list()),
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect), add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
remove: (id) => plugin.remove(PluginV2.ID.make(id)), remove: (id) => plugin.remove(PluginV2.ID.make(id)),
}, },
reference: { reference: {
list: () => response(reference.list()),
reload: reference.reload, reload: reference.reload,
transform: (callback) => transform: (callback) =>
reference.transform((draft) => reference.transform((draft) => {
callback({ callback({
add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)), add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
remove: draft.remove, remove: draft.remove,
list: draft.list, list: draft.list,
}), })
), }),
}, },
skill: { skill: {
list: () => response(skill.list()),
reload: skill.reload, reload: skill.reload,
transform: (callback) => transform: (callback) =>
skill.transform((draft) => skill.transform((draft) => {
callback({ callback({
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)), source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
list: draft.list, list: draft.list,
}), })
), }),
}, },
tool: { tool: {
register: (input) => tools.register(input), register: (input) => tools.register(input),
+55 -54
View File
@@ -201,64 +201,65 @@ export const ModelsDevPlugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
yield* ctx.integration.transform( const loaded = { data: yield* modelsDev.get() }
Effect.fn(function* (integrations) { yield* ctx.integration.transform((integrations) => {
const data = yield* modelsDev.get() for (const item of Object.values(loaded.data)) {
for (const item of Object.values(data)) { if (item.env.length === 0) continue
if (item.env.length === 0) continue const integrationID = item.id
const integrationID = item.id integrations.update(integrationID, (integration) => (integration.name = item.name))
integrations.update(integrationID, (integration) => (integration.name = item.name)) integrations.method.update({
integrations.method.update({ integrationID,
integrationID, method: { type: "key" },
method: { type: "key" }, })
}) integrations.method.update({
integrations.method.update({ integrationID,
integrationID, method: { type: "env", names: [...item.env] },
method: { type: "env", names: [...item.env] }, })
}) }
} })
}), yield* ctx.catalog.transform((catalog) => {
) for (const item of Object.values(loaded.data)) {
yield* ctx.catalog.transform( const providerID = ProviderV2.ID.make(item.id)
Effect.fn(function* (catalog) { catalog.provider.update(providerID, (provider) => {
const data = yield* modelsDev.get() provider.name = item.name
for (const item of Object.values(data)) { provider.api = item.npm
const providerID = ProviderV2.ID.make(item.id) ? {
catalog.provider.update(providerID, (provider) => { type: "aisdk",
provider.name = item.name package: item.npm,
provider.api = item.npm url: item.api,
? { }
type: "aisdk", : {
package: item.npm, type: "native",
url: item.api, url: item.api,
} settings: {},
: { }
type: "native", })
url: item.api,
settings: {},
}
})
for (const model of Object.values(item.models)) { for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost) const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model) const variants = reasoningVariants(item, model)
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants })) catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) => catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
applyModel(draft, model, { applyModel(draft, model, {
name: modeName(model, mode), name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost), cost: mergeCost(baseCost, options.cost),
request: options.provider, request: options.provider,
variants, variants,
}), }),
) )
}
} }
} }
}), }
) })
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))), Stream.runForEach(() =>
modelsDev.get().pipe(
Effect.tap((data) => Effect.sync(() => (loaded.data = data))),
Effect.andThen(ctx.integration.reload()),
Effect.andThen(ctx.catalog.reload()),
),
),
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
}), }),
+37 -9
View File
@@ -1,12 +1,11 @@
export * as PluginPromise from "./promise" export * as PluginPromise from "./promise"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "@opencode-ai/plugin/v2/effect"
import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise" import type { Plugin, PluginContext } from "@opencode-ai/plugin/v2/promise"
import { Effect, Scope } from "effect" import { Effect, Scope } from "effect"
// The Effect host hands back this registration shape; mirror it structurally so
// we do not have to alias the Effect package's `Registration` against the Promise one.
type HostRegistration = { readonly dispose: Effect.Effect<void> } type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> }
/** /**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only * Adapts a Promise plugin into an Effect plugin so the existing Effect-only
@@ -31,20 +30,23 @@ export function fromPromise(plugin: Plugin) {
dispose: () => Effect.runPromiseWith(context)(registration.dispose), dispose: () => Effect.runPromiseWith(context)(registration.dispose),
})) }))
const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect) const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(context)(effect)
const transform = const transform =
<Draft>(domain: { <Draft>(domain: {
transform: ( transform: (callback: (draft: Draft) => void) => Effect.Effect<HostRegistration, never, Scope.Scope>
callback: (draft: Draft) => Effect.Effect<void> | void,
) => Effect.Effect<HostRegistration, never, Scope.Scope>
}) => }) =>
(callback: (draft: Draft) => Promise<void> | void) => (callback: (draft: Draft) => void) =>
register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft))))) register(
domain.transform((draft) => {
callback(draft)
}),
)
const context2: PluginContext = { const context2: PluginContext = {
options: host.options, options: host.options,
agent: { agent: {
list: (input) => run(host.agent.list(input)),
transform: transform(host.agent), transform: transform(host.agent),
reload: () => run(host.agent.reload()), reload: () => run(host.agent.reload()),
}, },
@@ -55,14 +57,30 @@ export function fromPromise(plugin: Plugin) {
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))), register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
}, },
catalog: { catalog: {
provider: {
list: (input) => run(host.catalog.provider.list(input)),
get: (input) => run(host.catalog.provider.get(input)),
},
model: {
list: (input) => run(host.catalog.model.list(input)),
default: (input) => run(host.catalog.model.default(input)),
},
transform: transform(host.catalog), transform: transform(host.catalog),
reload: () => run(host.catalog.reload()), reload: () => run(host.catalog.reload()),
}, },
command: { command: {
list: (input) => run(host.command.list(input)),
transform: transform(host.command), transform: transform(host.command),
reload: () => run(host.command.reload()), reload: () => run(host.command.reload()),
}, },
integration: { integration: {
list: (input) => run(host.integration.list(input)),
get: (input) => run(host.integration.get(input)),
connectKey: (input) => run(host.integration.connectKey(input)),
connectOauth: (input) => run(host.integration.connectOauth(input)),
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
attemptComplete: (input) => run(host.integration.attemptComplete(input)),
attemptCancel: (input) => run(host.integration.attemptCancel(input)),
transform: transform(host.integration), transform: transform(host.integration),
reload: () => run(host.integration.reload()), reload: () => run(host.integration.reload()),
connection: { connection: {
@@ -71,6 +89,7 @@ export function fromPromise(plugin: Plugin) {
}, },
}, },
plugin: { plugin: {
list: (input) => run(host.plugin.list(input)),
add: (input) => { add: (input) => {
const child = fromPromise(input) const child = fromPromise(input)
return run(host.plugin.add(child)) return run(host.plugin.add(child))
@@ -78,13 +97,22 @@ export function fromPromise(plugin: Plugin) {
remove: (id) => run(host.plugin.remove(id)), remove: (id) => run(host.plugin.remove(id)),
}, },
reference: { reference: {
list: (input) => run(host.reference.list(input)),
transform: transform(host.reference), transform: transform(host.reference),
reload: () => run(host.reference.reload()), reload: () => run(host.reference.reload()),
}, },
skill: { skill: {
list: (input) => run(host.skill.list(input)),
transform: transform(host.skill), transform: transform(host.skill),
reload: () => run(host.skill.reload()), reload: () => run(host.skill.reload()),
}, },
session: {
create: (input) => run(host.session.create(input)),
get: (input) => run(host.session.get(input)),
prompt: (input) => run(host.session.prompt(input)),
command: (input) => run(host.session.command(input)),
interrupt: (input) => run(host.session.interrupt(input)),
},
} }
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2))) yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
@@ -62,22 +62,20 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
export const AmazonBedrockPlugin = define({ export const AmazonBedrockPlugin = define({
id: "amazon-bedrock", id: "amazon-bedrock",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { if (provider.api.type !== "aisdk") return
if (provider.api.type !== "aisdk") return if (typeof provider.request.body.endpoint !== "string") return
if (typeof provider.request.body.endpoint !== "string") return // The AI SDK expects a base URL, but users configure Bedrock private/VPC
// The AI SDK expects a base URL, but users configure Bedrock private/VPC // endpoints as `endpoint`; move it into the catalog endpoint URL once.
// endpoints as `endpoint`; move it into the catalog endpoint URL once. provider.api.url = provider.request.body.endpoint
provider.api.url = provider.request.body.endpoint delete provider.request.body.endpoint
delete provider.request.body.endpoint })
}) }
} })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
+10 -12
View File
@@ -4,18 +4,16 @@ import { define } from "../internal"
export const AnthropicPlugin = define({ export const AnthropicPlugin = define({
id: "anthropic", id: "anthropic",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/anthropic") continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.headers["anthropic-beta"] =
provider.request.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" })
}) }
} })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/anthropic") return if (evt.package !== "@ai-sdk/anthropic") return
+25 -29
View File
@@ -13,21 +13,19 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
export const AzurePlugin = define({ export const AzurePlugin = define({
id: "azure", id: "azure",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/azure") continue
if (item.provider.api.package !== "@ai-sdk/azure") continue const configured = item.provider.request.body.resourceName
const configured = item.provider.request.body.resourceName const resourceName =
const resourceName = typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME if (!resourceName) continue
if (!resourceName) continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.body.resourceName = resourceName
provider.request.body.resourceName = resourceName })
}) }
} })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return if (evt.package !== "@ai-sdk/azure") return
@@ -58,20 +56,18 @@ export const AzurePlugin = define({
export const AzureCognitiveServicesPlugin = define({ export const AzureCognitiveServicesPlugin = define({
id: "azure-cognitive-services", id: "azure-cognitive-services",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME if (!resourceName) return
if (!resourceName) return for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (!item.provider.id.includes("azure-cognitive-services")) continue
if (!item.provider.id.includes("azure-cognitive-services")) continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` })
}) }
} })
}),
)
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
+9 -11
View File
@@ -4,17 +4,15 @@ import { define } from "../internal"
export const CerebrasPlugin = define({ export const CerebrasPlugin = define({
id: "cerebras", id: "cerebras",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/cerebras") continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" })
}) }
} })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cerebras") return if (evt.package !== "@ai-sdk/cerebras") return
@@ -9,18 +9,16 @@ const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
export const CloudflareWorkersAIPlugin = define({ export const CloudflareWorkersAIPlugin = define({
id: "cloudflare-workers-ai", id: "cloudflare-workers-ai",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { const item = evt.provider.get(providerID)
const item = evt.provider.get(providerID) if (!item) return
if (!item) return evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { if (provider.api.type !== "aisdk") return
if (provider.api.type !== "aisdk") return if (provider.api.url) return
if (provider.api.url) return const accountId = resolveAccountId(provider.request.body)
const accountId = resolveAccountId(provider.request.body) if (accountId) provider.api.url = workersEndpoint(accountId)
if (accountId) provider.api.url = workersEndpoint(accountId) })
}) })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
@@ -14,17 +14,15 @@ function shouldUseResponses(modelID: string) {
export const GithubCopilotPlugin = define({ export const GithubCopilotPlugin = define({
id: "github-copilot", id: "github-copilot",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { const item = evt.provider.get(ProviderV2.ID.githubCopilot)
const item = evt.provider.get(ProviderV2.ID.githubCopilot) if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { // This chat-only alias conflicts with the Copilot GPT-5 Responses route,
// This chat-only alias conflicts with the Copilot GPT-5 Responses route, // so hide it only for Copilot rather than for every provider catalog.
// so hide it only for Copilot rather than for every provider catalog. model.enabled = false
model.enabled = false })
}) })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/github-copilot") return if (evt.package !== "@ai-sdk/github-copilot") return
@@ -57,33 +57,31 @@ function authFetch(fetchWithRuntimeOptions?: unknown) {
export const GoogleVertexPlugin = define({ export const GoogleVertexPlugin = define({
id: "google-vertex", id: "google-vertex",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (
if ( item.provider.api.package !== "@ai-sdk/google-vertex" &&
item.provider.api.package !== "@ai-sdk/google-vertex" && !(
!( item.provider.id === ProviderV2.ID.googleVertex &&
item.provider.id === ProviderV2.ID.googleVertex && item.provider.api.package.includes("@ai-sdk/openai-compatible")
item.provider.api.package.includes("@ai-sdk/openai-compatible")
)
) )
continue )
const project = resolveProject(item.provider.request.body) continue
const location = String(resolveLocation(item.provider.request.body)) const project = resolveProject(item.provider.request.body)
evt.provider.update(item.provider.id, (provider) => { const location = String(resolveLocation(item.provider.request.body))
if (project) provider.request.body.project = project evt.provider.update(item.provider.id, (provider) => {
provider.request.body.location = location if (project) provider.request.body.project = project
if (provider.api.type === "aisdk" && provider.api.url) { provider.request.body.location = location
provider.api.url = replaceVertexVars(provider.api.url, project, location) if (provider.api.type === "aisdk" && provider.api.url) {
} provider.api.url = replaceVertexVars(provider.api.url, project, location)
if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) { }
provider.request.body.fetch = authFetch(provider.request.body.fetch) if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) {
} provider.request.body.fetch = authFetch(provider.request.body.fetch)
}) }
} })
}), }
) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
@@ -115,28 +113,26 @@ export const GoogleVertexPlugin = define({
export const GoogleVertexAnthropicPlugin = define({ export const GoogleVertexAnthropicPlugin = define({
id: "google-vertex-anthropic", id: "google-vertex-anthropic",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue const project =
const project = item.provider.request.body.project ??
item.provider.request.body.project ?? process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ??
process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT
process.env.GCLOUD_PROJECT const location =
const location = item.provider.request.body.location ??
item.provider.request.body.location ?? process.env.GOOGLE_CLOUD_LOCATION ??
process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ??
process.env.VERTEX_LOCATION ?? "global"
"global" evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { if (project) provider.request.body.project = project
if (project) provider.request.body.project = project provider.request.body.location = location
provider.request.body.location = location })
}) }
} })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
+11 -13
View File
@@ -4,18 +4,16 @@ import { define } from "../internal"
export const KiloPlugin = define({ export const KiloPlugin = define({
id: "kilo", id: "kilo",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue
if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-Title"] = "opencode" })
}) }
} })
}),
)
}), }),
}) })
+15 -16
View File
@@ -6,21 +6,20 @@ export const LLMGatewayPlugin = define({
id: "llmgateway", id: "llmgateway",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
yield* ctx.catalog.transform( const configured = new Set((yield* integrations.list()).map((integration) => integration.id))
Effect.fn(function* (evt) { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.disabled) continue if (item.provider.disabled) continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue if (!configured.has(Integration.ID.make(item.provider.id))) continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode" provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-Source"] = "opencode" provider.request.headers["X-Source"] = "opencode"
}) })
} }
}), })
)
}), }),
}) })
+12 -14
View File
@@ -4,19 +4,17 @@ import { define } from "../internal"
export const NvidiaPlugin = define({ export const NvidiaPlugin = define({
id: "nvidia", id: "nvidia",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue
if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-Title"] = "opencode" provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode"
provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode" })
}) }
} })
}),
)
}), }),
}) })
+26 -28
View File
@@ -177,34 +177,32 @@ export const OpenAIPlugin = define({
draft.method.update(browser) draft.method.update(browser)
draft.method.update(headless) draft.method.update(headless)
}) })
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/openai") continue
if (item.provider.api.package !== "@ai-sdk/openai") continue if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { // OpenAIPlugin sends OpenAI models through Responses; this alias is a
// OpenAIPlugin sends OpenAI models through Responses; this alias is a // chat-completions-only model, so hide it only from OpenAI's catalog.
// chat-completions-only model, so hide it only from OpenAI's catalog. model.enabled = false
model.enabled = false })
}) }
} if (!chatgpt) return
if (!chatgpt) return const item = evt.provider.get(ProviderV2.ID.openai)
const item = evt.provider.get(ProviderV2.ID.openai) if (!item) return
if (!item) return for (const model of item.models.values()) {
for (const model of item.models.values()) { // ChatGPT-plan tokens only authorize codex-eligible models, and the
// ChatGPT-plan tokens only authorize codex-eligible models, and the // subscription covers usage, so hide the rest and zero the cost.
// subscription covers usage, so hide the rest and zero the cost. evt.model.update(item.provider.id, model.id, (draft) => {
evt.model.update(item.provider.id, model.id, (draft) => { if (!OpenAICodex.eligible(draft.api.id)) {
if (!OpenAICodex.eligible(draft.api.id)) { draft.enabled = false
draft.enabled = false return
return }
} draft.cost = []
draft.cost = [] })
}) }
} })
}),
)
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
+16 -18
View File
@@ -5,26 +5,24 @@ import { define } from "../internal"
export const OpenRouterPlugin = define({ export const OpenRouterPlugin = define({
id: "openrouter", id: "openrouter",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-Title"] = "opencode" })
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
if (!item.models.has(modelID)) continue
evt.model.update(item.provider.id, modelID, (model) => {
// These are OpenRouter-specific OpenAI chat aliases that do not work
// on the generic path. Keep custom providers with matching IDs untouched.
model.enabled = false
}) })
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
if (!item.models.has(modelID)) continue
evt.model.update(item.provider.id, modelID, (model) => {
// These are OpenRouter-specific OpenAI chat aliases that do not work
// on the generic path. Keep custom providers with matching IDs untouched.
model.enabled = false
})
}
} }
}), }
) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@openrouter/ai-sdk-provider") return if (evt.package !== "@openrouter/ai-sdk-provider") return
+10 -12
View File
@@ -4,18 +4,16 @@ import { define } from "../internal"
export const VercelPlugin = define({ export const VercelPlugin = define({
id: "vercel", id: "vercel",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/vercel") continue
if (item.provider.api.package !== "@ai-sdk/vercel") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.headers["http-referer"] = "https://opencode.ai/"
provider.request.headers["http-referer"] = "https://opencode.ai/" provider.request.headers["x-title"] = "opencode"
provider.request.headers["x-title"] = "opencode" })
}) }
} })
}),
)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/vercel") return if (evt.package !== "@ai-sdk/vercel") return
+11 -13
View File
@@ -4,18 +4,16 @@ import { define } from "../internal"
export const ZenmuxPlugin = define({ export const ZenmuxPlugin = define({
id: "zenmux", id: "zenmux",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform( yield* ctx.catalog.transform((evt) => {
Effect.fn(function* (evt) { for (const item of evt.provider.list()) {
for (const item of evt.provider.list()) { if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue
if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue evt.provider.update(item.provider.id, (provider) => {
evt.provider.update(item.provider.id, (provider) => { provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/"
provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/" provider.request.headers["X-Title"] ??= "opencode"
provider.request.headers["X-Title"] ??= "opencode" })
}) }
} })
}),
)
}), }),
}) })
+9 -1
View File
@@ -53,7 +53,15 @@ Review files`,
}) })
const command = yield* CommandV2.Service const command = yield* CommandV2.Service
yield* ConfigCommandPlugin.Plugin.effect(host({ command: { ...command, reload: command.reload } })).pipe( yield* ConfigCommandPlugin.Plugin.effect(
host({
command: {
list: () => Effect.die("unused command.list"),
transform: command.transform,
reload: command.reload,
},
}),
).pipe(
Effect.provideService( Effect.provideService(
Config.Service, Config.Service,
Config.Service.of({ Config.Service.of({
+1 -1
View File
@@ -36,7 +36,7 @@ describe("ConfigSkillPlugin.Plugin", () => {
yield* ConfigSkillPlugin.Plugin.effect( yield* ConfigSkillPlugin.Plugin.effect(
host({ host({
skill: { transform, reload: () => Effect.void }, skill: { list: () => Effect.die("unused skill.list"), transform, reload: () => Effect.void },
}), }),
).pipe( ).pipe(
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home: "/home/test" })), Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home: "/home/test" })),
+5 -1
View File
@@ -23,7 +23,11 @@ describe("CommandPlugin.Plugin", () => {
const command = yield* CommandV2.Service const command = yield* CommandV2.Service
yield* CommandPlugin.Plugin.effect( yield* CommandPlugin.Plugin.effect(
host({ host({
command: { transform: command.transform, reload: command.reload }, command: {
list: () => Effect.die("unused command.list"),
transform: command.transform,
reload: command.reload,
},
}), }),
).pipe( ).pipe(
Effect.provideService( Effect.provideService(
+34
View File
@@ -23,14 +23,30 @@ export function host(overrides: Overrides = {}): PluginContext {
language: () => Effect.die("unused aisdk.language"), language: () => Effect.die("unused aisdk.language"),
}, },
catalog: overrides.catalog ?? { catalog: overrides.catalog ?? {
provider: {
list: () => Effect.die("unused catalog.provider.list"),
get: () => Effect.die("unused catalog.provider.get"),
},
model: {
list: () => Effect.die("unused catalog.model.list"),
default: () => Effect.die("unused catalog.model.default"),
},
transform: () => Effect.die("unused catalog.transform"), transform: () => Effect.die("unused catalog.transform"),
reload: () => Effect.die("unused catalog.reload"), reload: () => Effect.die("unused catalog.reload"),
}, },
command: overrides.command ?? { command: overrides.command ?? {
list: () => Effect.die("unused command.list"),
transform: () => Effect.die("unused command.transform"), transform: () => Effect.die("unused command.transform"),
reload: () => Effect.die("unused command.reload"), reload: () => Effect.die("unused command.reload"),
}, },
integration: overrides.integration ?? { integration: overrides.integration ?? {
list: () => Effect.die("unused integration.list"),
get: () => Effect.die("unused integration.get"),
connectKey: () => Effect.die("unused integration.connectKey"),
connectOauth: () => Effect.die("unused integration.connectOauth"),
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
attemptComplete: () => Effect.die("unused integration.attemptComplete"),
attemptCancel: () => Effect.die("unused integration.attemptCancel"),
transform: () => Effect.die("unused integration.transform"), transform: () => Effect.die("unused integration.transform"),
reload: () => Effect.die("unused integration.reload"), reload: () => Effect.die("unused integration.reload"),
connection: { connection: {
@@ -39,14 +55,17 @@ export function host(overrides: Overrides = {}): PluginContext {
}, },
}, },
plugin: overrides.plugin ?? { plugin: overrides.plugin ?? {
list: () => Effect.die("unused plugin.list"),
add: () => Effect.die("unused plugin.add"), add: () => Effect.die("unused plugin.add"),
remove: () => Effect.die("unused plugin.remove"), remove: () => Effect.die("unused plugin.remove"),
}, },
reference: overrides.reference ?? { reference: overrides.reference ?? {
list: () => Effect.die("unused reference.list"),
transform: () => Effect.die("unused reference.transform"), transform: () => Effect.die("unused reference.transform"),
reload: () => Effect.die("unused reference.reload"), reload: () => Effect.die("unused reference.reload"),
}, },
skill: overrides.skill ?? { skill: overrides.skill ?? {
list: () => Effect.die("unused skill.list"),
transform: () => Effect.die("unused skill.transform"), transform: () => Effect.die("unused skill.transform"),
reload: () => Effect.die("unused skill.reload"), reload: () => Effect.die("unused skill.reload"),
}, },
@@ -94,6 +113,14 @@ export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"] { export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"] {
return { return {
provider: {
list: () => Effect.die("unused catalog.provider.list"),
get: () => Effect.die("unused catalog.provider.get"),
},
model: {
list: () => Effect.die("unused catalog.model.list"),
default: () => Effect.die("unused catalog.model.default"),
},
reload: catalog.reload, reload: catalog.reload,
transform: (callback) => transform: (callback) =>
catalog.transform((draft) => catalog.transform((draft) =>
@@ -158,6 +185,13 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
export function integrationHost(integration: Integration.Interface): PluginContext["integration"] { export function integrationHost(integration: Integration.Interface): PluginContext["integration"] {
return { return {
list: () => Effect.die("unused integration.list"),
get: () => Effect.die("unused integration.get"),
connectKey: () => Effect.die("unused integration.connectKey"),
connectOauth: () => Effect.die("unused integration.connectOauth"),
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
attemptComplete: () => Effect.die("unused integration.attemptComplete"),
attemptCancel: () => Effect.die("unused integration.attemptCancel"),
reload: integration.reload, reload: integration.reload,
connection: { connection: {
active: (id) => integration.connection.active(Integration.ID.make(id)), active: (id) => integration.connection.active(Integration.ID.make(id)),
+29
View File
@@ -11,6 +11,35 @@ import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer) const it = testEffect(PluginTestLayer)
describe("fromPromise", () => { describe("fromPromise", () => {
it.effect("forwards standard client reads", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const seen: string[] = []
const promisePlugin = define({
id: "promise-client-reads",
setup: async (ctx) => {
const results = await Promise.all([
ctx.agent.list(),
ctx.catalog.provider.list(),
ctx.catalog.model.list(),
ctx.command.list(),
ctx.integration.list(),
ctx.plugin.list(),
ctx.reference.list(),
ctx.skill.list(),
])
seen.push(...results.map((result) => result.location.directory))
},
})
yield* PluginPromise.fromPromise(promisePlugin).effect(host)
expect(seen).toHaveLength(8)
expect(new Set(seen).size).toBe(1)
}),
)
it.effect("loads a promise plugin and registers a transform hook", () => it.effect("loads a promise plugin and registers a transform hook", () =>
Effect.gen(function* () { Effect.gen(function* () {
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
+9 -1
View File
@@ -19,7 +19,15 @@ describe("SkillPlugin.Plugin", () => {
it.effect("registers built-in skills", () => it.effect("registers built-in skills", () =>
Effect.gen(function* () { Effect.gen(function* () {
const skill = yield* SkillV2.Service const skill = yield* SkillV2.Service
yield* SkillPlugin.Plugin.effect(host({ skill: { ...skill, reload: skill.reload } })).pipe( yield* SkillPlugin.Plugin.effect(
host({
skill: {
list: () => Effect.die("unused skill.list"),
transform: skill.transform,
reload: skill.reload,
},
}),
).pipe(
Effect.provideService(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) })), Effect.provideService(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) })),
Effect.provideService( Effect.provideService(
Location.Service, Location.Service,
+1
View File
@@ -23,6 +23,7 @@
], ],
"dependencies": { "dependencies": {
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/protocol": "workspace:*", "@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
+7 -8
View File
@@ -1,6 +1,7 @@
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types" import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
import type { AgentApi } from "./generated/api.js" import type { AgentApi } from "@opencode-ai/client/effect/api"
import type { Hooks } from "./registration.js" import type { Effect } from "effect"
import type { TransformHook } from "./registration.js"
export interface AgentDraft { export interface AgentDraft {
list(): readonly AgentV2Info[] list(): readonly AgentV2Info[]
@@ -10,9 +11,7 @@ export interface AgentDraft {
remove(id: string): void remove(id: string): void
} }
export type AgentHooks = Hooks<{ export interface AgentHooks extends AgentApi<unknown> {
transform: AgentDraft readonly transform: TransformHook<AgentDraft>
}> readonly reload: () => Effect.Effect<void>
}
export type AgentPluginApi = AgentHooks
export type AgentDomain = AgentApi & AgentPluginApi
+7 -4
View File
@@ -1,5 +1,7 @@
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types" import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types"
import type { Hooks } from "./registration.js" import type { CatalogApi } from "@opencode-ai/client/effect/api"
import type { Effect } from "effect"
import type { TransformHook } from "./registration.js"
export interface CatalogProviderRecord { export interface CatalogProviderRecord {
readonly provider: ProviderV2Info readonly provider: ProviderV2Info
@@ -24,6 +26,7 @@ export interface CatalogDraft {
} }
} }
export type CatalogHooks = Hooks<{ export interface CatalogHooks extends CatalogApi<unknown> {
transform: CatalogDraft readonly transform: TransformHook<CatalogDraft>
}> readonly reload: () => Effect.Effect<void>
}
+7 -4
View File
@@ -1,5 +1,7 @@
import type { CommandV2Info } from "@opencode-ai/sdk/v2/types" import type { CommandV2Info } from "@opencode-ai/sdk/v2/types"
import type { Hooks } from "./registration.js" import type { CommandApi } from "@opencode-ai/client/effect/api"
import type { Effect } from "effect"
import type { TransformHook } from "./registration.js"
export interface CommandDraft { export interface CommandDraft {
list(): readonly CommandV2Info[] list(): readonly CommandV2Info[]
@@ -8,6 +10,7 @@ export interface CommandDraft {
remove(name: string): void remove(name: string): void
} }
export type CommandHooks = Hooks<{ export interface CommandHooks extends CommandApi<unknown> {
transform: CommandDraft readonly transform: TransformHook<CommandDraft>
}> readonly reload: () => Effect.Effect<void>
}
+9 -10
View File
@@ -1,5 +1,5 @@
import type { PluginOptions } from "../options.js" import type { PluginOptions } from "../options.js"
import type { AgentDomain } from "./agent.js" import type { AgentHooks } from "./agent.js"
import type { AISDKHooks } from "./aisdk.js" import type { AISDKHooks } from "./aisdk.js"
import type { CatalogHooks } from "./catalog.js" import type { CatalogHooks } from "./catalog.js"
import type { CommandHooks } from "./command.js" import type { CommandHooks } from "./command.js"
@@ -7,20 +7,19 @@ import type { IntegrationHooks } from "./integration.js"
import type { PluginDomain } from "./plugin.js" import type { PluginDomain } from "./plugin.js"
import type { ReferenceHooks } from "./reference.js" import type { ReferenceHooks } from "./reference.js"
import type { SkillHooks } from "./skill.js" import type { SkillHooks } from "./skill.js"
import type { Reload } from "./registration.js"
import type { ToolDomain } from "./tool.js" import type { ToolDomain } from "./tool.js"
import type { SessionDomain } from "./runtime.js" import type { SessionHooks } from "./runtime.js"
export interface PluginContext { export interface PluginContext {
readonly options: PluginOptions readonly options: PluginOptions
readonly agent: AgentDomain & Reload readonly agent: AgentHooks
readonly aisdk: AISDKHooks readonly aisdk: AISDKHooks
readonly catalog: CatalogHooks & Reload readonly catalog: CatalogHooks
readonly command: CommandHooks & Reload readonly command: CommandHooks
readonly integration: IntegrationHooks & Reload readonly integration: IntegrationHooks
readonly plugin: PluginDomain readonly plugin: PluginDomain
readonly reference: ReferenceHooks & Reload readonly reference: ReferenceHooks
readonly skill: SkillHooks & Reload readonly skill: SkillHooks
readonly tool: ToolDomain readonly tool: ToolDomain
readonly session: SessionDomain readonly session: SessionHooks
} }
+9 -2
View File
@@ -1,6 +1,13 @@
export type { PluginContext } from "./context.js" export type { PluginContext } from "./context.js"
export { define } from "./plugin.js" export { define } from "./plugin.js"
export type { Plugin } from "./plugin.js" export type { Plugin, PluginDomain } from "./plugin.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, IntegrationMethodRegistration } from "./integration.js"
export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
export type { SkillDraft, SkillHooks } from "./skill.js"
export * as Tool from "./tool.js" export * as Tool from "./tool.js"
export type { ToolDomain, ToolExecuteBeforeEvent, ToolExecuteAfterEvent } from "./tool.js" export type { ToolDomain, ToolExecuteBeforeEvent, ToolExecuteAfterEvent } from "./tool.js"
export type { SessionDomain } from "./runtime.js" export type { SessionHooks } from "./runtime.js"
+5 -2
View File
@@ -9,8 +9,9 @@ import type {
IntegrationOAuthMethod, IntegrationOAuthMethod,
IntegrationRef, IntegrationRef,
} from "@opencode-ai/sdk/v2/types" } from "@opencode-ai/sdk/v2/types"
import type { IntegrationApi } from "@opencode-ai/client/effect/api"
import type { Effect, Scope } from "effect" import type { Effect, Scope } from "effect"
import type { Hooks } from "./registration.js" import type { TransformHook } from "./registration.js"
export type IntegrationOAuthAuthorization = { export type IntegrationOAuthAuthorization = {
readonly url: string readonly url: string
@@ -55,7 +56,9 @@ export interface IntegrationDraft {
} }
} }
export interface IntegrationHooks extends Hooks<{ transform: IntegrationDraft }> { export interface IntegrationHooks extends IntegrationApi<unknown> {
readonly transform: TransformHook<IntegrationDraft>
readonly reload: () => Effect.Effect<void>
readonly connection: { readonly connection: {
readonly active: (integrationID: string) => Effect.Effect<ConnectionInfo | undefined> readonly active: (integrationID: string) => Effect.Effect<ConnectionInfo | undefined>
readonly resolve: (connection: ConnectionInfo) => Effect.Effect<CredentialValue | undefined, unknown> readonly resolve: (connection: ConnectionInfo) => Effect.Effect<CredentialValue | undefined, unknown>
+2 -1
View File
@@ -1,3 +1,4 @@
import type { PluginApi } from "@opencode-ai/client/effect/api"
import type { Effect, Scope } from "effect" import type { Effect, Scope } from "effect"
import type { PluginContext } from "./context.js" import type { PluginContext } from "./context.js"
@@ -10,7 +11,7 @@ export function define<R = Scope.Scope>(plugin: Plugin<R>) {
return plugin return plugin
} }
export interface PluginDomain { export interface PluginDomain extends PluginApi<unknown> {
readonly add: (plugin: Plugin) => Effect.Effect<void> readonly add: (plugin: Plugin) => Effect.Effect<void>
readonly remove: (id: string) => Effect.Effect<void> readonly remove: (id: string) => Effect.Effect<void>
} }
+7 -4
View File
@@ -1,5 +1,7 @@
import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types" import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types"
import type { Hooks } from "./registration.js" import type { ReferenceApi } from "@opencode-ai/client/effect/api"
import type { Effect } from "effect"
import type { TransformHook } from "./registration.js"
export interface ReferenceDraft { export interface ReferenceDraft {
add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void
@@ -7,6 +9,7 @@ export interface ReferenceDraft {
list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[] list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[]
} }
export type ReferenceHooks = Hooks<{ export interface ReferenceHooks extends ReferenceApi<unknown> {
transform: ReferenceDraft readonly transform: TransformHook<ReferenceDraft>
}> readonly reload: () => Effect.Effect<void>
}
@@ -4,12 +4,10 @@ export interface Registration {
readonly dispose: Effect.Effect<void> readonly dispose: Effect.Effect<void>
} }
export interface Reload {
readonly reload: () => Effect.Effect<void>
}
export type Hooks<Spec> = { export type Hooks<Spec> = {
readonly [Name in keyof Spec]: ( readonly [Name in keyof Spec]: (
callback: (input: Spec[Name]) => Effect.Effect<void> | void, callback: (input: Spec[Name]) => Effect.Effect<void> | void,
) => Effect.Effect<Registration, never, Scope.Scope> ) => Effect.Effect<Registration, never, Scope.Scope>
} }
export type TransformHook<Input> = (callback: (input: Input) => void) => Effect.Effect<Registration, never, Scope.Scope>
+3 -2
View File
@@ -1,3 +1,4 @@
import type { SessionApi } from "./generated/api.js" import type { SessionApi } from "@opencode-ai/client/effect/api"
export type SessionDomain = Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt"> export interface SessionHooks
extends Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt"> {}
+7 -4
View File
@@ -1,11 +1,14 @@
import type { SkillV2Source } from "@opencode-ai/sdk/v2/types" import type { SkillV2Source } from "@opencode-ai/sdk/v2/types"
import type { Hooks } from "./registration.js" import type { SkillApi } from "@opencode-ai/client/effect/api"
import type { Effect } from "effect"
import type { TransformHook } from "./registration.js"
export interface SkillDraft { export interface SkillDraft {
source(source: SkillV2Source): void source(source: SkillV2Source): void
list(): readonly SkillV2Source[] list(): readonly SkillV2Source[]
} }
export type SkillHooks = Hooks<{ export interface SkillHooks extends SkillApi<unknown> {
transform: SkillDraft readonly transform: TransformHook<SkillDraft>
}> readonly reload: () => Effect.Effect<void>
}
+6 -4
View File
@@ -1,8 +1,10 @@
import type { AgentApi } from "@opencode-ai/client/promise/api"
import type { AgentDraft } from "../effect/agent.js" import type { AgentDraft } from "../effect/agent.js"
import type { Hooks } from "./registration.js" import type { TransformHook } from "./registration.js"
export type { AgentDraft } export type { AgentDraft }
export type AgentHooks = Hooks<{ export interface AgentHooks extends AgentApi {
transform: AgentDraft readonly transform: TransformHook<AgentDraft>
}> readonly reload: () => Promise<void>
}
+6 -4
View File
@@ -1,8 +1,10 @@
import type { CatalogApi } from "@opencode-ai/client/promise/api"
import type { CatalogDraft, CatalogProviderRecord } from "../effect/catalog.js" import type { CatalogDraft, CatalogProviderRecord } from "../effect/catalog.js"
import type { Hooks } from "./registration.js" import type { TransformHook } from "./registration.js"
export type { CatalogDraft, CatalogProviderRecord } export type { CatalogDraft, CatalogProviderRecord }
export type CatalogHooks = Hooks<{ export interface CatalogHooks extends CatalogApi {
transform: CatalogDraft readonly transform: TransformHook<CatalogDraft>
}> readonly reload: () => Promise<void>
}
+6 -4
View File
@@ -1,8 +1,10 @@
import type { CommandApi } from "@opencode-ai/client/promise/api"
import type { CommandDraft } from "../effect/command.js" import type { CommandDraft } from "../effect/command.js"
import type { Hooks } from "./registration.js" import type { TransformHook } from "./registration.js"
export type { CommandDraft } export type { CommandDraft }
export type CommandHooks = Hooks<{ export interface CommandHooks extends CommandApi {
transform: CommandDraft readonly transform: TransformHook<CommandDraft>
}> readonly reload: () => Promise<void>
}
+8 -7
View File
@@ -6,17 +6,18 @@ import type { CommandHooks } from "./command.js"
import type { IntegrationHooks } from "./integration.js" import type { IntegrationHooks } from "./integration.js"
import type { PluginDomain } from "./plugin.js" import type { PluginDomain } from "./plugin.js"
import type { ReferenceHooks } from "./reference.js" import type { ReferenceHooks } from "./reference.js"
import type { SessionHooks } from "./runtime.js"
import type { SkillHooks } from "./skill.js" import type { SkillHooks } from "./skill.js"
import type { Reload } from "./registration.js"
export interface PluginContext { export interface PluginContext {
readonly options: PluginOptions readonly options: PluginOptions
readonly agent: AgentHooks & Reload readonly agent: AgentHooks
readonly aisdk: AISDKHooks readonly aisdk: AISDKHooks
readonly catalog: CatalogHooks & Reload readonly catalog: CatalogHooks
readonly command: CommandHooks & Reload readonly command: CommandHooks
readonly integration: IntegrationHooks & Reload readonly integration: IntegrationHooks
readonly plugin: PluginDomain readonly plugin: PluginDomain
readonly reference: ReferenceHooks & Reload readonly reference: ReferenceHooks
readonly skill: SkillHooks & Reload readonly session: SessionHooks
readonly skill: SkillHooks
} }
+1 -1
View File
@@ -2,11 +2,11 @@ export type { PluginContext } from "./context.js"
export type { PluginOptions } from "../options.js" export type { PluginOptions } from "../options.js"
export { define } from "./plugin.js" export { define } from "./plugin.js"
export type { Plugin, PluginDomain } from "./plugin.js" export type { Plugin, PluginDomain } from "./plugin.js"
export type { Registration, Reload } from "./registration.js"
export type { AgentDraft, AgentHooks } from "./agent.js" export type { AgentDraft, AgentHooks } from "./agent.js"
export type { AISDKHooks } from "./aisdk.js" export type { AISDKHooks } from "./aisdk.js"
export type { CatalogDraft, CatalogHooks, CatalogProviderRecord } from "./catalog.js" export type { CatalogDraft, CatalogHooks, CatalogProviderRecord } from "./catalog.js"
export type { CommandDraft, CommandHooks } from "./command.js" export type { CommandDraft, CommandHooks } from "./command.js"
export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration } from "./integration.js" export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration } from "./integration.js"
export type { ReferenceDraft, ReferenceHooks } from "./reference.js" export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
export type { SessionHooks } from "./runtime.js"
export type { SkillDraft, SkillHooks } from "./skill.js" export type { SkillDraft, SkillHooks } from "./skill.js"
@@ -1,10 +1,13 @@
import type { IntegrationApi } from "@opencode-ai/client/promise/api"
import type { IntegrationDraft, IntegrationMethodRegistration } from "../effect/integration.js" import type { IntegrationDraft, IntegrationMethodRegistration } from "../effect/integration.js"
import type { CredentialValue } from "@opencode-ai/sdk/v2/types" import type { CredentialValue } from "@opencode-ai/sdk/v2/types"
import type { Hooks } from "./registration.js" import type { TransformHook } from "./registration.js"
export type { IntegrationDraft, IntegrationMethodRegistration } export type { IntegrationDraft, IntegrationMethodRegistration }
export interface IntegrationHooks extends Hooks<{ transform: IntegrationDraft }> { export interface IntegrationHooks extends IntegrationApi {
readonly transform: TransformHook<IntegrationDraft>
readonly reload: () => Promise<void>
readonly connection: { readonly connection: {
readonly active: (integrationID: string) => Promise<import("@opencode-ai/sdk/v2/types").ConnectionInfo | undefined> readonly active: (integrationID: string) => Promise<import("@opencode-ai/sdk/v2/types").ConnectionInfo | undefined>
readonly resolve: ( readonly resolve: (
+2 -1
View File
@@ -1,3 +1,4 @@
import type { PluginApi } from "@opencode-ai/client/promise/api"
import type { PluginContext } from "./context.js" import type { PluginContext } from "./context.js"
export interface Plugin { export interface Plugin {
@@ -9,7 +10,7 @@ export function define(plugin: Plugin) {
return plugin return plugin
} }
export interface PluginDomain { export interface PluginDomain extends PluginApi {
readonly add: (plugin: Plugin) => Promise<void> readonly add: (plugin: Plugin) => Promise<void>
readonly remove: (id: string) => Promise<void> readonly remove: (id: string) => Promise<void>
} }
+6 -4
View File
@@ -1,8 +1,10 @@
import type { ReferenceApi } from "@opencode-ai/client/promise/api"
import type { ReferenceDraft } from "../effect/reference.js" import type { ReferenceDraft } from "../effect/reference.js"
import type { Hooks } from "./registration.js" import type { TransformHook } from "./registration.js"
export type { ReferenceDraft } export type { ReferenceDraft }
export type ReferenceHooks = Hooks<{ export interface ReferenceHooks extends ReferenceApi {
transform: ReferenceDraft readonly transform: TransformHook<ReferenceDraft>
}> readonly reload: () => Promise<void>
}
@@ -2,10 +2,8 @@ export interface Registration {
readonly dispose: () => Promise<void> readonly dispose: () => Promise<void>
} }
export interface Reload {
readonly reload: () => Promise<void>
}
export type Hooks<Spec> = { export type Hooks<Spec> = {
readonly [Name in keyof Spec]: (callback: (input: Spec[Name]) => Promise<void> | void) => Promise<Registration> readonly [Name in keyof Spec]: (callback: (input: Spec[Name]) => Promise<void> | void) => Promise<Registration>
} }
export type TransformHook<Input> = (callback: (input: Input) => void) => Promise<Registration>
@@ -0,0 +1,3 @@
import type { SessionApi } from "@opencode-ai/client/promise/api"
export interface SessionHooks extends Pick<SessionApi, "create" | "get" | "prompt" | "command" | "interrupt"> {}
+6 -4
View File
@@ -1,8 +1,10 @@
import type { SkillApi } from "@opencode-ai/client/promise/api"
import type { SkillDraft } from "../effect/skill.js" import type { SkillDraft } from "../effect/skill.js"
import type { Hooks } from "./registration.js" import type { TransformHook } from "./registration.js"
export type { SkillDraft } export type { SkillDraft }
export type SkillHooks = Hooks<{ export interface SkillHooks extends SkillApi {
transform: SkillDraft readonly transform: TransformHook<SkillDraft>
}> readonly reload: () => Promise<void>
}