mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 18:46:26 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bc9f6ea09 |
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@opencode-ai/plugin": patch
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Compile Effect-authored plugins to the runtime-neutral Promise and Standard Schema plugin contract.
|
||||
@@ -579,7 +579,6 @@
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -590,6 +589,7 @@
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
@@ -597,12 +597,14 @@
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opentui/core": ">=0.5.8",
|
||||
"@opentui/solid": ">=0.5.8",
|
||||
"effect": "catalog:",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opencode-ai/theme",
|
||||
"@opentui/core",
|
||||
"@opentui/solid",
|
||||
"effect",
|
||||
"solid-js",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "./plugin-runtime.promise"
|
||||
import "./plugin-runtime.effect"
|
||||
|
||||
process.stdout.on("error", (error) => {
|
||||
if ("code" in error && error.code === "EPIPE") return
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
} from "@opencode-ai/plugin/effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
|
||||
const key = Symbol.for("opencode.plugin.v2.effect")
|
||||
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
Tool: { Error: Tool.Error },
|
||||
}
|
||||
@@ -132,20 +132,10 @@ export const Plugin = sdk.Plugin
|
||||
export const Provider = sdk.Provider
|
||||
export const Reference = sdk.Reference
|
||||
export const Skill = sdk.Skill`
|
||||
const effectModule = promiseModule
|
||||
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||
.replace("Promise plugin", "Effect plugin")
|
||||
const promisePluginModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
|
||||
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
|
||||
export const define = sdk.Plugin.define`
|
||||
const effectPluginModule = promisePluginModule
|
||||
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||
.replace("Promise plugin", "Effect plugin")
|
||||
const promiseToolModule = `export {}`
|
||||
const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
|
||||
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
|
||||
export const Error = sdk.Tool.Error
|
||||
`
|
||||
return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")}
|
||||
import __cjs_mod__ from "node:module"
|
||||
import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs"
|
||||
@@ -160,17 +150,11 @@ const __ocPluginModules = ${JSON.stringify({
|
||||
"@opencode-ai/plugin": "opencode:plugin-v2",
|
||||
"@opencode-ai/plugin/promise/plugin": "opencode:plugin-promise-plugin",
|
||||
"@opencode-ai/plugin/promise/tool": "opencode:plugin-promise-tool",
|
||||
"@opencode-ai/plugin/effect": "opencode:plugin-v2-effect",
|
||||
"@opencode-ai/plugin/effect/plugin": "opencode:plugin-v2-effect-plugin",
|
||||
"@opencode-ai/plugin/effect/tool": "opencode:plugin-v2-effect-tool",
|
||||
})}
|
||||
const __ocPluginSources = ${JSON.stringify({
|
||||
"opencode:plugin-v2": promiseModule,
|
||||
"opencode:plugin-promise-plugin": promisePluginModule,
|
||||
"opencode:plugin-promise-tool": promiseToolModule,
|
||||
"opencode:plugin-v2-effect": effectModule,
|
||||
"opencode:plugin-v2-effect-plugin": effectPluginModule,
|
||||
"opencode:plugin-v2-effect-tool": effectToolModule,
|
||||
})}
|
||||
__cjs_mod__.registerHooks({
|
||||
resolve(__ocSpecifier, __ocContext, __ocNextResolve) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigAgentPlugin from "./agent.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document, Info, type Entry } from "@opencode-ai/schema/config"
|
||||
import { ConfigAgent } from "@opencode-ai/schema/config/agent"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigCommandPlugin from "./command.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Info, type Entry } from "@opencode-ai/schema/config"
|
||||
import { ConfigCommand } from "@opencode-ai/schema/config/command"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigCompactionPlugin from "./compaction.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { SessionCompaction } from "../../session/compaction.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigFormatterPlugin from "./formatter.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigImagePlugin from "./image.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Image } from "../../image.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigInstructionPlugin from "./instruction.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { dirname, join } from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigLocationWatcherPlugin from "./location-watcher.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { LocationWatcherPolicy } from "../../filesystem/location-watcher-policy.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigMCPPlugin from "./mcp.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { Effect, Stream } from "effect"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigPolicyPlugin from "./policy.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigProviderPlugin from "./provider.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Effect } from "effect"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigReferencePlugin from "./reference.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import { ConfigReference } from "@opencode-ai/schema/config/reference"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigShellPlugin from "./shell.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { ShellSelect } from "../../shell/select.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigSkillPlugin from "./skill.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import type { Entry } from "@opencode-ai/schema/config"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigSnapshotPlugin from "./snapshot.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Snapshot } from "../../snapshot.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigToolOutputPlugin from "./tool-output.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { ToolOutput } from "../../tool-output.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigWebSearchPlugin from "./websearch.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as Plugin from "./plugin.js"
|
||||
export { Event, ID, Info, Source } from "@opencode-ai/schema/plugin"
|
||||
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Plugin as PluginDefinition } from "@opencode-ai/core/plugin/definition"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { App } from "./app.js"
|
||||
import { Cause, Context, Effect, Exit, Layer, Logger, References, Scope, Semaphore } from "effect"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as AgentPlugin from "./agent.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Agent } from "../agent.js"
|
||||
import { Permission } from "../permission.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as CommandPlugin from "./command.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { Location } from "../location.js"
|
||||
import PROMPT_INITIALIZE from "./command/initialize.txt"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Context as PluginContext, Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Scope } from "effect"
|
||||
|
||||
export type Context = PluginContext
|
||||
export type Plugin<R = Scope.Scope> = PluginDefinition<R>
|
||||
|
||||
export function define<R>(plugin: PluginDefinition<R>) {
|
||||
return plugin
|
||||
}
|
||||
|
||||
export namespace Plugin {
|
||||
export type Context = PluginContext
|
||||
export type Plugin<R = Scope.Scope> = PluginDefinition<R>
|
||||
export const define = <R>(plugin: PluginDefinition<R>) => plugin
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PluginHost from "./host.js"
|
||||
|
||||
import { Plugin } from "@opencode-ai/plugin/effect"
|
||||
import { Plugin } from "@opencode-ai/core/plugin/definition"
|
||||
import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration"
|
||||
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PluginInternal from "./internal.js"
|
||||
|
||||
import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Plugin } from "@opencode-ai/core/plugin/definition"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as MCPCodeModeExclusionPlugin from "./mcp-codemode-exclusion.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
|
||||
// These servers provide Code Mode, so expose them directly instead of nesting them inside OpenCode Code Mode.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Bus } from "../bus.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as PlanPlugin from "./plan.js"
|
||||
|
||||
import { Message, ToolFailure } from "@opencode-ai/ai"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Effect, Stream } from "effect"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
type MantleSDK = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
export const AnthropicPlugin = define({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Form } from "@opencode-ai/schema/form"
|
||||
import { Model } from "../../model.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
export const CerebrasPlugin = define({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os from "os"
|
||||
import { App } from "../../app.js"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Form } from "@opencode-ai/schema/form"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { iife } from "../../util/iife.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os from "os"
|
||||
import { App } from "../../app.js"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Form } from "@opencode-ai/schema/form"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { iife } from "../../util/iife.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { importModule } from "@opencode-ai/util/runtime-import"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import type { AISDKHooks } from "@opencode-ai/plugin/effect/aisdk"
|
||||
import { Effect } from "effect"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { App } from "../../app.js"
|
||||
import { Agent } from "../../agent.js"
|
||||
import { Integration } from "../../integration.js"
|
||||
import { Model } from "../../model.js"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
import type { PluginInternal } from "../internal.js"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os from "os"
|
||||
import { App } from "../../app.js"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
export const GitLabPlugin = define({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
function resolveProject(options: Record<string, any>) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
export const KiloPlugin = define({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Integration } from "../../integration.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Duration, Effect, Schedule, Schema, Semaphore, Stream } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
export const NvidiaPlugin = define({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Duration, Effect, Schedule, Schema, Semaphore, Stream } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
|
||||
export const OpenAICompatiblePlugin = define({
|
||||
id: "opencode.provider.openai.compatible",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
|
||||
import { App } from "../../app.js"
|
||||
import { Credential } from "../../credential.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Duration, Effect, Schema, Semaphore, Stream } from "effect"
|
||||
import type { Scope } from "effect"
|
||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { Bus } from "../../bus.js"
|
||||
import { Credential } from "../../credential.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { Model } from "../../model.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
|
||||
export const OpenRouterPlugin = define({
|
||||
id: "opencode.provider.openrouter",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { importModule } from "@opencode-ai/util/runtime-import"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
export const VercelPlugin = define({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Duration, Effect, Schedule, Schema, Semaphore, Stream } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Clock, Effect, Option, Schema } from "effect"
|
||||
import { App } from "../../app.js"
|
||||
import { Credential } from "../../credential.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Provider } from "../../provider.js"
|
||||
|
||||
export const ZenmuxPlugin = define({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SdkPlugins from "./sdk.js"
|
||||
|
||||
import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Plugin } from "@opencode-ai/core/plugin/definition"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Bus } from "../bus.js"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export * as SkillPlugin from "./skill.js"
|
||||
|
||||
import { define, type Context } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define, type Context } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
import { AbsolutePath } from "../schema.js"
|
||||
import { Skill } from "../skill.js"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
export * as PluginSupervisor from "./supervisor.js"
|
||||
export { Service, type Interface } from "./supervisor-service.js"
|
||||
|
||||
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Event } from "@opencode-ai/schema/config"
|
||||
import { Cause, Effect, Latch, Layer, Schema, Stream } from "effect"
|
||||
import { Cause, Effect, Latch, Layer, Predicate, Schema, Stream } from "effect"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { ConfigPluginSource } from "../config/plugin/source.js"
|
||||
@@ -18,22 +17,13 @@ import { importModule } from "@opencode-ai/util/runtime-import"
|
||||
import { Service } from "./supervisor-service.js"
|
||||
|
||||
const PluginModule = Schema.Struct({
|
||||
default: Schema.Union([
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
tui: Schema.optional(Schema.Boolean),
|
||||
effect: Schema.declare<PluginDefinition["effect"]>(
|
||||
(input): input is PluginDefinition["effect"] => typeof input === "function",
|
||||
),
|
||||
}),
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
tui: Schema.optional(Schema.Boolean),
|
||||
setup: Schema.declare<Parameters<typeof PluginPromise.fromPromise>[0]["setup"]>(
|
||||
(input): input is Parameters<typeof PluginPromise.fromPromise>[0]["setup"] => typeof input === "function",
|
||||
),
|
||||
}),
|
||||
]),
|
||||
default: Schema.Struct({
|
||||
id: Schema.String,
|
||||
tui: Schema.optional(Schema.Boolean),
|
||||
setup: Schema.declare<Parameters<typeof PluginPromise.fromPromise>[0]["setup"]>(
|
||||
(input): input is Parameters<typeof PluginPromise.fromPromise>[0]["setup"] => typeof input === "function",
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
|
||||
@@ -119,8 +109,18 @@ const load = Effect.fn("PluginSupervisor.load")(function* (
|
||||
: `${entrypoint}?mtime=${operation.mtime}`
|
||||
yield* Effect.log({ msg: "loading plugin", id: operation.target, entrypoint: source })
|
||||
const mod = yield* Effect.promise(() => importModule(source))
|
||||
if (
|
||||
Predicate.isObject(mod) &&
|
||||
Predicate.isObject(mod.default) &&
|
||||
"effect" in mod.default &&
|
||||
!("setup" in mod.default)
|
||||
) {
|
||||
return yield* Effect.fail(
|
||||
new Error("Effect plugins must be exported through Plugin.define from @opencode-ai/plugin/effect"),
|
||||
)
|
||||
}
|
||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||
const plugin = PluginPromise.fromPromise(value)
|
||||
return {
|
||||
id: plugin.id,
|
||||
tui: plugin.tui,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SystemPromptPlugin from "./system-prompt.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect } from "effect"
|
||||
|
||||
import PROMPT_ANTHROPIC from "./system-prompt/anthropic.txt"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as VariantPlugin from "./variant.js"
|
||||
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Model } from "../model.js"
|
||||
import { Provider } from "../provider.js"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WarmingPlugin from "./warming.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Clock, Duration, Effect, Scope } from "effect"
|
||||
import { Config } from "../config.js"
|
||||
import { SessionSchema } from "../session/schema.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebSearchExa from "./exa.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect, Schema, Scope } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import { WebSearchMcp } from "./mcp.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebSearchFirecrawl from "./firecrawl.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect, Option, Schema, Scope } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import { App } from "../../app.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebSearchParallel from "./parallel.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect, Schema, Scope } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import { App } from "../../app.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebSearchTavily from "./tavily.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Duration, Effect, Schema, Scope } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { App } from "../../app.js"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
export * as EditTool from "./edit.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { Bom } from "@opencode-ai/util/bom"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as GlobTool from "./glob.js"
|
||||
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { Environment } from "../../environment/index.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as GrepTool from "./grep.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PatchTool from "./patch.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { Effect, Result, Schema } from "effect"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as QuestionTool from "./question.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Form } from "../../form.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ReadTool from "./read.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { basename, dirname, join } from "path"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as ShellTool from "./shell.js"
|
||||
|
||||
import path from "path"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { Deferred, Effect, Schema, Scope } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Environment } from "../../environment/index.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SkillTool from "./skill.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import path from "path"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as SubagentTool from "./subagent.js"
|
||||
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect, Schema, Scope } from "effect"
|
||||
import { Agent } from "../../agent.js"
|
||||
import { Config } from "../../config.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebFetchTool from "./webfetch.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebSearchTool from "./websearch.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema, Semaphore } from "effect"
|
||||
import { HttpClientError } from "effect/unstable/http"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
export * as WriteTool from "./write.js"
|
||||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Bom } from "@opencode-ai/util/bom"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WellKnownPlugin from "./plugin.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Bus } from "../bus.js"
|
||||
import { WellKnown } from "../wellknown.js"
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/core/plugin/definition"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { ConfigPluginSource } from "@opencode-ai/core/config/plugin/source"
|
||||
@@ -146,6 +146,7 @@ describe("PluginSupervisor config", () => {
|
||||
"-*",
|
||||
path.join(import.meta.dir, "../plugin/fixtures/missing-plugin.ts"),
|
||||
path.join(import.meta.dir, "../plugin/fixtures/invalid-plugin.ts"),
|
||||
path.join(import.meta.dir, "../plugin/fixtures/raw-effect-plugin.ts"),
|
||||
{
|
||||
package: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
|
||||
options: { description: "Loaded after invalid plugins" },
|
||||
@@ -162,12 +163,14 @@ describe("PluginSupervisor config", () => {
|
||||
expect(output).toEqual([
|
||||
path.join(import.meta.dir, "../plugin/fixtures/missing-plugin.ts"),
|
||||
path.join(import.meta.dir, "../plugin/fixtures/invalid-plugin.ts"),
|
||||
path.join(import.meta.dir, "../plugin/fixtures/raw-effect-plugin.ts"),
|
||||
])
|
||||
expect(
|
||||
(yield* plugins.list()).filter((plugin) => plugin.status === "failed").map((plugin) => plugin.source),
|
||||
).toEqual([
|
||||
{ type: "local", path: path.join(import.meta.dir, "../plugin/fixtures/missing-plugin.ts") },
|
||||
{ type: "local", path: path.join(import.meta.dir, "../plugin/fixtures/invalid-plugin.ts") },
|
||||
{ type: "local", path: path.join(import.meta.dir, "../plugin/fixtures/raw-effect-plugin.ts") },
|
||||
])
|
||||
}),
|
||||
).pipe(Effect.provide(Logger.layer([logger])))
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
|
||||
import type { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Tool } from "@opencode-ai/core/tool"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context as PluginContext } from "@opencode-ai/core/plugin/definition"
|
||||
import { Effect, type Scope } from "effect"
|
||||
import { host } from "../plugin/host"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Stream,
|
||||
} from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/core/plugin/definition"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/core/plugin/definition"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default {
|
||||
id: "raw-effect-plugin",
|
||||
effect: () => Effect.void,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Message, SystemPart } from "@opencode-ai/ai"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { DateTime, Effect, Fiber, Schema } from "effect"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
@@ -25,6 +25,7 @@ import { PluginTestLayer } from "./fixture"
|
||||
import { host as testHost } from "./host"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
const EffectPlugin = await import("@opencode-ai/plugin/effect")
|
||||
|
||||
describe("fromPromise", () => {
|
||||
it.effect("adapts plugin storage methods", () =>
|
||||
@@ -469,11 +470,11 @@ describe("fromPromise", () => {
|
||||
const events: string[] = []
|
||||
const promisePlugin = define({
|
||||
id: "promise-cleanup",
|
||||
setup: async () => {
|
||||
setup: async (context) => {
|
||||
events.push("setup")
|
||||
return async () => {
|
||||
await Promise.resolve()
|
||||
events.push("cleanup")
|
||||
events.push(context.signal.aborted ? "cleanup-aborted" : "cleanup-active")
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -485,7 +486,7 @@ describe("fromPromise", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
expect(events).toEqual(["setup", "cleanup"])
|
||||
expect(events).toEqual(["setup", "cleanup-aborted"])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -534,6 +535,77 @@ describe("fromPromise", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("runs Effect-authored tool codecs through the Promise boundary", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
const registry = yield* Tool.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
const interrupted: string[] = []
|
||||
const effectPlugin = EffectPlugin.Plugin.define({
|
||||
id: "effect-tool",
|
||||
effect: (context) =>
|
||||
context.tool.transform((tools) =>
|
||||
tools.add({
|
||||
name: "increment",
|
||||
options: { codemode: false },
|
||||
description: "Increment",
|
||||
input: Schema.FiniteFromString,
|
||||
output: Schema.FiniteFromString,
|
||||
execute: (input) => {
|
||||
if (input === 42) {
|
||||
return Effect.fail(
|
||||
new Tool.Error({ message: "cannot increment 42", metadata: { reason: "reserved" } }),
|
||||
)
|
||||
}
|
||||
if (input === 43) {
|
||||
return Effect.sync(() => interrupted.push("started")).pipe(
|
||||
Effect.andThen(Effect.never),
|
||||
Effect.ensuring(Effect.sync(() => interrupted.push("finalized"))),
|
||||
)
|
||||
}
|
||||
return Effect.succeed({ output: input + 1 })
|
||||
},
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
yield* PluginPromise.fromPromise(effectPlugin).effect(host)
|
||||
|
||||
expect(
|
||||
yield* (yield* registry.snapshot()).execute({
|
||||
sessionID: Session.ID.make("ses_effect_tool"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_effect_tool"),
|
||||
progress: () => Effect.void,
|
||||
call: { type: "tool-call", id: "call_effect_tool", name: "increment", input: "41" },
|
||||
}),
|
||||
).toMatchObject({ output: "42" })
|
||||
expect(
|
||||
yield* Effect.flip(
|
||||
(yield* registry.snapshot()).execute({
|
||||
sessionID: Session.ID.make("ses_effect_tool"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_effect_tool_error"),
|
||||
progress: () => Effect.void,
|
||||
call: { type: "tool-call", id: "call_effect_tool_error", name: "increment", input: "42" },
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ _tag: "Tool.Error", message: "cannot increment 42", metadata: { reason: "reserved" } })
|
||||
const execution = yield* (yield* registry.snapshot())
|
||||
.execute({
|
||||
sessionID: Session.ID.make("ses_effect_tool"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_effect_tool_interrupt"),
|
||||
progress: () => Effect.void,
|
||||
call: { type: "tool-call", id: "call_effect_tool_interrupt", name: "increment", input: "43" },
|
||||
})
|
||||
.pipe(Effect.forkScoped({ startImmediately: true }))
|
||||
while (interrupted.length === 0) yield* Effect.yieldNow
|
||||
yield* Fiber.interrupt(execution)
|
||||
expect(interrupted).toEqual(["started", "finalized"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("returns content-only plugin results through Code Mode", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opentui/core": ">=0.5.8",
|
||||
"@opentui/solid": ">=0.5.8",
|
||||
"effect": "catalog:",
|
||||
"solid-js": ">=1.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -44,6 +44,9 @@
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
},
|
||||
"effect": {
|
||||
"optional": true
|
||||
},
|
||||
"solid-js": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -55,6 +58,7 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# OpenCode V2 Promise Plugin API
|
||||
|
||||
The Promise plugin API at `@opencode-ai/plugin` is the async/await equivalent of `@opencode-ai/plugin/effect`. It grants plugins the same two in-process capabilities:
|
||||
The Promise plugin API at `@opencode-ai/plugin` is OpenCode's runtime-neutral
|
||||
plugin contract. The Effect entrypoint compiles its authoring API to this same
|
||||
contract before the plugin reaches OpenCode. It grants plugins two in-process
|
||||
capabilities:
|
||||
|
||||
- `hook` installs behavior at an OpenCode extension point.
|
||||
- `reload` reruns every transform hook for a stateful domain.
|
||||
|
||||
The only difference from the Effect API is the async boundary: hook callbacks, hook registration, `reload`, and `Registration.dispose` use Promises instead of Effects.
|
||||
Hook callbacks, hook registration, `reload`, and `Registration.dispose` use
|
||||
Promises. No Effect runtime values are part of this boundary.
|
||||
|
||||
## Defining A Plugin
|
||||
|
||||
@@ -57,6 +61,11 @@ await ctx.agent.transform((agent) => {
|
||||
})
|
||||
```
|
||||
|
||||
`ctx.signal` is aborted when setup is interrupted or the plugin unloads. Runtime
|
||||
hook callbacks receive the invocation signal as their second argument, and tool
|
||||
executors receive it on their context, so asynchronous work can stop with the
|
||||
host operation.
|
||||
|
||||
Available transform hooks are namespaced by domain:
|
||||
|
||||
```ts
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Status
|
||||
|
||||
This document describes the agreed target design for the V2 plugin system. It is an implementation plan, not documentation for the current API.
|
||||
This is a historical implementation plan. It was superseded by the
|
||||
Promise/Standard Schema host boundary documented in `../README.md` and
|
||||
`README.md`; do not treat the direction or API names below as current design.
|
||||
|
||||
## Goals
|
||||
|
||||
|
||||
@@ -23,12 +23,22 @@ export default Plugin.define({
|
||||
})
|
||||
```
|
||||
|
||||
`Plugin.define` compiles the definition to OpenCode's Promise plugin contract.
|
||||
The adapter runs every Effect, schema conversion, callback, and finalizer with
|
||||
the plugin's installed `effect` peer; OpenCode never executes a plugin-created
|
||||
Effect with the host runtime.
|
||||
|
||||
Plugin setup registers hooks imperatively through each domain's `hook` method.
|
||||
|
||||
Configuration supplied for the plugin is available as `ctx.options`.
|
||||
|
||||
Registrations are owned by the plugin scope. Closing the scope removes them automatically; a registration may also be removed early through `dispose`.
|
||||
|
||||
Tool schemas are converted to detached Standard Schema capabilities before
|
||||
registration. Inputs decode from model JSON into the executor type, while
|
||||
outputs encode from the executor type back to JSON. The host does not inspect
|
||||
or transfer Effect schema ASTs.
|
||||
|
||||
## Transform Hooks
|
||||
|
||||
Transform hooks contribute to stateful domains:
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Effect, Exit, Schema, Scope, Stream } from "effect"
|
||||
import { HttpApiEndpoint } from "effect/unstable/httpapi"
|
||||
import type { Context as PromiseContext, Plugin as PromisePlugin } from "../promise/plugin.js"
|
||||
import type { Info as PromiseTool } from "../promise/tool.js"
|
||||
import type { Context, Plugin } from "./plugin.js"
|
||||
import { toStandardSchema } from "./tool-schema.js"
|
||||
import { endpointSchemas } from "./endpoint-schema.js"
|
||||
|
||||
export type { Context, Plugin } from "./plugin.js"
|
||||
|
||||
type PromiseRegistration = { readonly dispose: () => Promise<void> }
|
||||
type EffectRegistration = { readonly dispose: Effect.Effect<void> }
|
||||
type PromiseEvent = ReturnType<PromiseContext["event"]["subscribe"]> extends AsyncIterable<infer Event> ? Event : never
|
||||
|
||||
interface CompiledEndpoint {
|
||||
readonly encode: ReadonlyArray<(input: unknown) => Effect.Effect<unknown, Schema.SchemaError>>
|
||||
readonly decode: (output: unknown) => Effect.Effect<unknown, Schema.SchemaError>
|
||||
readonly noContent: boolean
|
||||
}
|
||||
|
||||
const compiledEndpoints = new WeakMap<object, CompiledEndpoint>()
|
||||
|
||||
export function define(plugin: Plugin<Scope.Scope>): PromisePlugin {
|
||||
return {
|
||||
id: plugin.id,
|
||||
tui: plugin.tui,
|
||||
setup: (host) =>
|
||||
Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const [{ ClientApi }, { OpenCodeEvent }] = yield* Effect.promise(() =>
|
||||
Promise.all([import("@opencode-ai/protocol/client"), import("@opencode-ai/protocol/groups/event")]),
|
||||
)
|
||||
const scope = yield* Scope.make()
|
||||
const AgentEndpoints = ClientApi.groups["server.agent"].endpoints
|
||||
const CommandEndpoints = ClientApi.groups["server.command"].endpoints
|
||||
const IntegrationEndpoints = ClientApi.groups["server.integration"].endpoints
|
||||
const McpEndpoints = ClientApi.groups["server.mcp"].endpoints
|
||||
const ModelEndpoints = ClientApi.groups["server.model"].endpoints
|
||||
const PluginEndpoints = ClientApi.groups["server.plugin"].endpoints
|
||||
const ProviderEndpoints = ClientApi.groups["server.provider"].endpoints
|
||||
const ReferenceEndpoints = ClientApi.groups["server.reference"].endpoints
|
||||
const SessionEndpoints = ClientApi.groups["server.session"].endpoints
|
||||
const SkillEndpoints = ClientApi.groups["server.skill"].endpoints
|
||||
const WebSearchEndpoints = ClientApi.groups["server.websearch"].endpoints
|
||||
|
||||
const run = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>, signal?: AbortSignal) =>
|
||||
Effect.runPromise(Scope.provide(scope)(effect), { signal })
|
||||
const runFailure = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>, signal?: AbortSignal) =>
|
||||
run(
|
||||
Effect.match(effect, {
|
||||
onFailure: (error) => ({ _tag: "Failure" as const, error }),
|
||||
onSuccess: (value) => ({ _tag: "Success" as const, value }),
|
||||
}),
|
||||
signal,
|
||||
).then((result) => (result._tag === "Success" ? result.value : Promise.reject(result.error)))
|
||||
const register = (acquire: () => Promise<PromiseRegistration>) =>
|
||||
Effect.acquireRelease(Effect.promise(acquire), (registration) =>
|
||||
Effect.promise(() => registration.dispose()),
|
||||
).pipe(
|
||||
Effect.map(
|
||||
(registration): EffectRegistration => ({
|
||||
dispose: Effect.promise(() => registration.dispose()),
|
||||
}),
|
||||
),
|
||||
)
|
||||
const adaptApiMethod = <EffectMethod>(
|
||||
endpoint: HttpApiEndpoint.Top,
|
||||
method: (input: never, options?: never) => Promise<unknown>,
|
||||
) => {
|
||||
const compiled = compileEndpoint(endpoint)
|
||||
return ((input?: unknown) =>
|
||||
Effect.gen(function* () {
|
||||
const encoded = yield* Effect.forEach(compiled.encode, (encode) => encode(input ?? {}))
|
||||
const output = yield* Effect.tryPromise({
|
||||
try: (signal) => method(Object.assign({}, ...encoded) as never, { signal } as never),
|
||||
catch: (cause) => cause,
|
||||
})
|
||||
if (compiled.noContent) return undefined
|
||||
return yield* compiled.decode(output)
|
||||
})) as EffectMethod
|
||||
}
|
||||
const transform = <EffectTransform>(domain: {
|
||||
transform: (callback: never) => Promise<PromiseRegistration>
|
||||
}) => ((callback: never) => register(() => domain.transform(callback))) as EffectTransform
|
||||
const context: Context = {
|
||||
app: host.app,
|
||||
options: host.options,
|
||||
agent: {
|
||||
get: adaptApiMethod(AgentEndpoints["agent.get"], host.agent.get),
|
||||
list: adaptApiMethod(AgentEndpoints["agent.list"], host.agent.list),
|
||||
transform: transform<Context["agent"]["transform"]>(host.agent),
|
||||
reload: () => Effect.promise(() => host.agent.reload()),
|
||||
},
|
||||
aisdk: {
|
||||
hook: (name, callback, options) =>
|
||||
register(() =>
|
||||
host.aisdk.hook(name, (event, invocation) => run(callback(event), invocation.signal), options),
|
||||
),
|
||||
},
|
||||
catalog: {
|
||||
provider: {
|
||||
list: adaptApiMethod(ProviderEndpoints["provider.list"], host.catalog.provider.list),
|
||||
get: adaptApiMethod(ProviderEndpoints["provider.get"], host.catalog.provider.get),
|
||||
},
|
||||
model: {
|
||||
list: adaptApiMethod(ModelEndpoints["model.list"], host.catalog.model.list),
|
||||
default: adaptApiMethod(ModelEndpoints["model.default"], host.catalog.model.default),
|
||||
},
|
||||
transform: transform<Context["catalog"]["transform"]>(host.catalog),
|
||||
reload: () => Effect.promise(() => host.catalog.reload()),
|
||||
},
|
||||
command: {
|
||||
list: adaptApiMethod(CommandEndpoints["command.list"], host.command.list),
|
||||
transform: transform<Context["command"]["transform"]>(host.command),
|
||||
reload: () => Effect.promise(() => host.command.reload()),
|
||||
},
|
||||
event: {
|
||||
subscribe: () =>
|
||||
Stream.fromAsyncIterable(host.event.subscribe(), (cause) => cause).pipe(
|
||||
Stream.mapEffect((event) => Schema.decodeUnknownEffect(OpenCodeEvent)(event as PromiseEvent)),
|
||||
),
|
||||
},
|
||||
integration: {
|
||||
list: adaptApiMethod(IntegrationEndpoints["integration.list"], host.integration.list),
|
||||
get: adaptApiMethod(IntegrationEndpoints["integration.get"], host.integration.get),
|
||||
connect: {
|
||||
key: adaptApiMethod(IntegrationEndpoints["integration.connect.key"], host.integration.connect.key),
|
||||
},
|
||||
oauth: {
|
||||
connect: adaptApiMethod(
|
||||
IntegrationEndpoints["integration.oauth.connect"],
|
||||
host.integration.oauth.connect,
|
||||
),
|
||||
status: adaptApiMethod(IntegrationEndpoints["integration.oauth.status"], host.integration.oauth.status),
|
||||
complete: adaptApiMethod(
|
||||
IntegrationEndpoints["integration.oauth.complete"],
|
||||
host.integration.oauth.complete,
|
||||
),
|
||||
cancel: adaptApiMethod(IntegrationEndpoints["integration.oauth.cancel"], host.integration.oauth.cancel),
|
||||
},
|
||||
command: {
|
||||
connect: adaptApiMethod(
|
||||
IntegrationEndpoints["integration.command.connect"],
|
||||
host.integration.command.connect,
|
||||
),
|
||||
status: adaptApiMethod(
|
||||
IntegrationEndpoints["integration.command.status"],
|
||||
host.integration.command.status,
|
||||
),
|
||||
cancel: adaptApiMethod(
|
||||
IntegrationEndpoints["integration.command.cancel"],
|
||||
host.integration.command.cancel,
|
||||
),
|
||||
},
|
||||
transform: (callback) =>
|
||||
register(() =>
|
||||
host.integration.transform((draft) =>
|
||||
callback({
|
||||
list: draft.list,
|
||||
get: draft.get,
|
||||
update: draft.update,
|
||||
remove: draft.remove,
|
||||
method: {
|
||||
list: draft.method.list,
|
||||
update: (input) => {
|
||||
if (!("authorize" in input)) return draft.method.update(input)
|
||||
const refresh = input.refresh
|
||||
draft.method.update({
|
||||
...input,
|
||||
authorize: (answer) =>
|
||||
run(input.authorize(answer)).then((authorization) =>
|
||||
authorization.mode === "auto"
|
||||
? {
|
||||
...authorization,
|
||||
callback: run(authorization.callback),
|
||||
}
|
||||
: {
|
||||
...authorization,
|
||||
callback: (code) => run(authorization.callback(code)),
|
||||
},
|
||||
),
|
||||
refresh: refresh === undefined ? undefined : (credential) => run(refresh(credential)),
|
||||
})
|
||||
},
|
||||
remove: draft.method.remove,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
reload: () => Effect.promise(() => host.integration.reload()),
|
||||
connection: {
|
||||
active: (id) => Effect.promise(() => host.integration.connection.active(id)),
|
||||
resolve: (connection) =>
|
||||
Effect.tryPromise({
|
||||
try: () => host.integration.connection.resolve(connection),
|
||||
catch: (cause) => cause,
|
||||
}),
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
list: adaptApiMethod(McpEndpoints["mcp.list"], host.mcp.list),
|
||||
add: adaptApiMethod(McpEndpoints["mcp.add"], host.mcp.add),
|
||||
remove: adaptApiMethod(McpEndpoints["mcp.remove"], host.mcp.remove),
|
||||
connect: adaptApiMethod(McpEndpoints["mcp.connect"], host.mcp.connect),
|
||||
disconnect: adaptApiMethod(McpEndpoints["mcp.disconnect"], host.mcp.disconnect),
|
||||
transform: transform<Context["mcp"]["transform"]>(host.mcp),
|
||||
reload: () => Effect.promise(() => host.mcp.reload()),
|
||||
},
|
||||
plugin: {
|
||||
list: adaptApiMethod(PluginEndpoints["plugin.list"], host.plugin.list),
|
||||
},
|
||||
reference: {
|
||||
list: adaptApiMethod(ReferenceEndpoints["reference.list"], host.reference.list),
|
||||
transform: transform<Context["reference"]["transform"]>(host.reference),
|
||||
reload: () => Effect.promise(() => host.reference.reload()),
|
||||
},
|
||||
skill: {
|
||||
list: adaptApiMethod(SkillEndpoints["skill.list"], host.skill.list),
|
||||
transform: transform<Context["skill"]["transform"]>(host.skill),
|
||||
reload: () => Effect.promise(() => host.skill.reload()),
|
||||
},
|
||||
storage: {
|
||||
get: (key) => Effect.promise(() => host.storage.get(key)),
|
||||
set: (key, value) => Effect.promise(() => host.storage.set(key, value)),
|
||||
remove: (key) => Effect.promise(() => host.storage.remove(key)),
|
||||
scan: (options) => Effect.promise(() => host.storage.scan(options)),
|
||||
},
|
||||
tool: {
|
||||
transform: (callback) =>
|
||||
register(() =>
|
||||
host.tool.transform((draft) =>
|
||||
callback({
|
||||
add: (tool) => draft.add(adaptTool(tool, runFailure)),
|
||||
}),
|
||||
),
|
||||
),
|
||||
hook: (name, callback) =>
|
||||
register(() =>
|
||||
host.tool.hook(name, (event, invocation) => runFailure(callback(event), invocation.signal)),
|
||||
),
|
||||
},
|
||||
websearch: {
|
||||
providers: adaptApiMethod(WebSearchEndpoints["websearch.providers"], host.websearch.providers),
|
||||
query: adaptApiMethod(WebSearchEndpoints["websearch.query"], host.websearch.query),
|
||||
reload: () => Effect.promise(() => host.websearch.reload()),
|
||||
transform: (callback) =>
|
||||
register(() =>
|
||||
host.websearch.transform((draft) =>
|
||||
callback({
|
||||
add: (definition) =>
|
||||
draft.add({
|
||||
id: definition.id,
|
||||
name: definition.name,
|
||||
execute: (input, context) => run(definition.execute(input), context.signal),
|
||||
}),
|
||||
default: draft.default,
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
session: {
|
||||
hook: (name, callback, options) =>
|
||||
register(() =>
|
||||
host.session.hook(name, (event, invocation) => run(callback(event), invocation.signal), options),
|
||||
),
|
||||
create: adaptApiMethod(SessionEndpoints["session.create"], host.session.create),
|
||||
get: adaptApiMethod(SessionEndpoints["session.get"], host.session.get),
|
||||
switchAgent: adaptApiMethod(SessionEndpoints["session.switchAgent"], host.session.switchAgent),
|
||||
switchModel: adaptApiMethod(SessionEndpoints["session.switchModel"], host.session.switchModel),
|
||||
prompt: adaptApiMethod(SessionEndpoints["session.prompt"], host.session.prompt),
|
||||
generate: adaptApiMethod(SessionEndpoints["session.generate"], host.session.generate),
|
||||
command: adaptApiMethod(SessionEndpoints["session.command"], host.session.command),
|
||||
synthetic: adaptApiMethod(SessionEndpoints["session.synthetic"], host.session.synthetic),
|
||||
interrupt: adaptApiMethod(SessionEndpoints["session.interrupt"], host.session.interrupt),
|
||||
rename: adaptApiMethod(SessionEndpoints["session.rename"], host.session.rename),
|
||||
wait: adaptApiMethod(SessionEndpoints["session.wait"], host.session.wait),
|
||||
},
|
||||
shell: {
|
||||
hook: (name, callback) =>
|
||||
register(() => host.shell.hook(name, (event, invocation) => run(callback(event), invocation.signal))),
|
||||
},
|
||||
}
|
||||
const setup = yield* Effect.exit(Scope.provide(scope)(plugin.effect(context)))
|
||||
if (Exit.isFailure(setup)) {
|
||||
yield* Scope.close(scope, setup)
|
||||
return yield* Effect.failCause(setup.cause)
|
||||
}
|
||||
return () => Effect.runPromise(Scope.close(scope, Exit.void))
|
||||
}),
|
||||
{ signal: host.signal },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function compileEndpoint(endpoint: HttpApiEndpoint.Top) {
|
||||
const cached = compiledEndpoints.get(endpoint)
|
||||
if (cached) return cached
|
||||
const schemas = endpointSchemas(endpoint)
|
||||
const compiled = {
|
||||
encode: schemas.inputs.map((schema) => Schema.encodeUnknownEffect(schema)),
|
||||
decode: Schema.decodeUnknownEffect(schemas.output),
|
||||
noContent: schemas.noContent,
|
||||
} satisfies CompiledEndpoint
|
||||
compiledEndpoints.set(endpoint, compiled)
|
||||
return compiled
|
||||
}
|
||||
|
||||
function adaptTool(
|
||||
tool: Tool.Info<any, any>,
|
||||
runFailure: <A, E>(effect: Effect.Effect<A, E, Scope.Scope>, signal?: AbortSignal) => Promise<A>,
|
||||
): PromiseTool {
|
||||
const input = toStandardSchema(tool.input, "input")
|
||||
const output = tool.output === undefined ? undefined : toStandardSchema(tool.output, "output")
|
||||
return {
|
||||
...tool,
|
||||
input,
|
||||
...(output === undefined ? {} : { output }),
|
||||
execute: (value, context) =>
|
||||
runFailure(
|
||||
tool.execute(value, {
|
||||
...context,
|
||||
progress: (update) => Effect.promise(() => context.progress(update)),
|
||||
}),
|
||||
context.signal,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Schema, SchemaAST } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
|
||||
type RuntimeSchema = Schema.Codec<unknown, unknown>
|
||||
|
||||
export function endpointSchemas(endpoint: HttpApiEndpoint.Top) {
|
||||
const payload = Array.from(endpoint.payload.values()).flatMap(({ schemas }) => schemas)
|
||||
const success = Array.from(endpoint.success)
|
||||
if (payload.length > 1 || success.length > 1) {
|
||||
throw new Error(`Unsupported API schema cardinality: ${endpoint.identifier}`)
|
||||
}
|
||||
const inputs = [
|
||||
endpoint.params,
|
||||
endpoint.query === undefined ? undefined : Schema.toType(endpoint.query),
|
||||
endpoint.headers,
|
||||
...payload,
|
||||
].filter((schema): schema is Schema.Top => schema !== undefined) as Array<RuntimeSchema>
|
||||
const output = (success[0] ?? HttpApiSchema.NoContent) as RuntimeSchema
|
||||
const type = Schema.toType(output).ast
|
||||
const data = SchemaAST.isObjects(output.ast)
|
||||
? output.ast.propertySignatures.find((property) => property.name === "data")
|
||||
: undefined
|
||||
return {
|
||||
inputs,
|
||||
output:
|
||||
!HttpApiSchema.isNoContent(output.ast) &&
|
||||
SchemaAST.isObjects(type) &&
|
||||
type.indexSignatures.length === 0 &&
|
||||
type.propertySignatures.length === 1 &&
|
||||
type.propertySignatures[0]?.name === "data" &&
|
||||
data !== undefined
|
||||
? (Schema.make<Schema.Top>(data.type) as RuntimeSchema)
|
||||
: output,
|
||||
noContent: HttpApiSchema.isNoContent(output.ast),
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as Plugin from "./plugin.js"
|
||||
export * as Plugin from "./adapter.js"
|
||||
export type { StorageEntry, StorageScanOptions, StorageScanResult } from "../storage.js"
|
||||
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
|
||||
@@ -43,6 +43,4 @@ export interface Plugin<R = Scope.Scope> {
|
||||
readonly effect: (context: Context) => Effect.Effect<void, never, R>
|
||||
}
|
||||
|
||||
export function define<R = Scope.Scope>(plugin: Plugin<R>) {
|
||||
return plugin
|
||||
}
|
||||
export { define } from "./adapter.js"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Schema, SchemaAST } from "effect"
|
||||
|
||||
export function toStandardSchema(schema: Tool.ValueSchema<any>, direction: "input" | "output"): Tool.ValueSchema<any> {
|
||||
if (typeof schema === "object" && schema !== null && "~standard" in schema) {
|
||||
return { "~standard": schema["~standard"] } as Tool.ValueSchema<any>
|
||||
}
|
||||
if (!Schema.isSchema(schema)) return schema
|
||||
if (!(schema.ast instanceof SchemaAST.Base)) {
|
||||
throw new Error(
|
||||
"Effect tool schemas must use the plugin's Effect peer or be converted to Standard Schema by their authoring runtime",
|
||||
)
|
||||
}
|
||||
const codec = schema as Schema.Codec<unknown, unknown>
|
||||
const oriented = direction === "input" ? codec : Schema.flip(codec)
|
||||
const standard = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(oriented))
|
||||
return { "~standard": standard["~standard"] } as Tool.ValueSchema<any>
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Effect, Schema, SchemaAST, Scope, Stream } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { define } from "../effect/plugin.js"
|
||||
import { Effect, Option, Schema, Scope, Stream } from "effect"
|
||||
import { HttpApiEndpoint } from "effect/unstable/httpapi"
|
||||
import type { Plugin as EffectPlugin } from "../effect/plugin.js"
|
||||
import { endpointSchemas } from "../effect/endpoint-schema.js"
|
||||
import type { Context, Plugin } from "./plugin.js"
|
||||
import type { Info } from "./tool.js"
|
||||
import type { BeforeHook, Info } from "./tool.js"
|
||||
|
||||
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||
type Registration = { readonly dispose: () => Promise<void> }
|
||||
@@ -20,52 +21,27 @@ const compiledEndpoints = new WeakMap<object, CompiledEndpoint>()
|
||||
function compileEndpoint(endpoint: HttpApiEndpoint.Top) {
|
||||
const cached = compiledEndpoints.get(endpoint)
|
||||
if (cached) return cached
|
||||
const payloadSchemas = Array.from(endpoint.payload.values()).flatMap(({ schemas }) => schemas)
|
||||
const successSchemas = Array.from(endpoint.success)
|
||||
if (payloadSchemas.length > 1 || successSchemas.length > 1) {
|
||||
throw new Error(`Unsupported API schema cardinality: ${endpoint.identifier}`)
|
||||
}
|
||||
const inputs = [
|
||||
endpoint.params,
|
||||
endpoint.query === undefined ? undefined : Schema.toType(endpoint.query),
|
||||
endpoint.headers,
|
||||
...payloadSchemas,
|
||||
].filter((schema): schema is Schema.Top => schema !== undefined) as Array<RuntimeSchema>
|
||||
const success = (successSchemas[0] ?? HttpApiSchema.NoContent) as RuntimeSchema
|
||||
const noContent = HttpApiSchema.isNoContent(success.ast)
|
||||
const type = Schema.toType(success).ast
|
||||
const data = SchemaAST.isObjects(success.ast)
|
||||
? success.ast.propertySignatures.find((property) => property.name === "data")
|
||||
: undefined
|
||||
const output =
|
||||
!noContent &&
|
||||
SchemaAST.isObjects(type) &&
|
||||
type.indexSignatures.length === 0 &&
|
||||
type.propertySignatures.length === 1 &&
|
||||
type.propertySignatures[0]?.name === "data" &&
|
||||
data !== undefined
|
||||
? (Schema.make<Schema.Top>(data.type) as RuntimeSchema)
|
||||
: success
|
||||
const schemas = endpointSchemas(endpoint)
|
||||
const compiled = {
|
||||
decode: inputs.map((schema) => Schema.decodeUnknownEffect(schema)),
|
||||
encode: Schema.encodeUnknownEffect(output),
|
||||
noContent,
|
||||
decode: schemas.inputs.map((schema) => Schema.decodeUnknownEffect(schema)),
|
||||
encode: Schema.encodeUnknownEffect(schemas.output),
|
||||
noContent: schemas.noContent,
|
||||
} satisfies CompiledEndpoint
|
||||
compiledEndpoints.set(endpoint, compiled)
|
||||
return compiled
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||
* loader (`Plugin` / `PluginSupervisor`) can run it unchanged.
|
||||
* Adapts the runtime-neutral Promise contract into Core's internal Effect
|
||||
* plugin representation.
|
||||
*
|
||||
* Hook registrations created during the async `setup` attach to the plugin's
|
||||
* scope, so unloading the plugin disposes them. The captured fiber context
|
||||
* preserves boot-time batching, so Promise-plugin transforms still coalesce
|
||||
* into one reload per domain.
|
||||
*/
|
||||
export function fromPromise(plugin: Plugin) {
|
||||
return define({
|
||||
export function fromPromise(plugin: Plugin): EffectPlugin {
|
||||
return {
|
||||
id: plugin.id,
|
||||
tui: plugin.tui,
|
||||
effect: (host) =>
|
||||
@@ -100,13 +76,15 @@ export function fromPromise(plugin: Plugin) {
|
||||
method: (input: never) => Effect.Effect<unknown, unknown>,
|
||||
) => {
|
||||
const compiled = compileEndpoint(endpoint)
|
||||
return ((input?: unknown) =>
|
||||
return ((input?: unknown, requestOptions?: { readonly signal?: AbortSignal }) =>
|
||||
Effect.gen(function* () {
|
||||
const decoded = yield* Effect.forEach(compiled.decode, (decode) => decode(input ?? {}))
|
||||
const result = yield* method(Object.assign({}, ...decoded) as never)
|
||||
if (compiled.noContent) return undefined
|
||||
return yield* compiled.encode(result)
|
||||
}).pipe(Effect.runPromiseWith(context))) as PromiseMethod
|
||||
}).pipe((effect) =>
|
||||
Effect.runPromiseWith(context)(effect, { signal: requestOptions?.signal }),
|
||||
)) as PromiseMethod
|
||||
}
|
||||
|
||||
const transform =
|
||||
@@ -120,7 +98,7 @@ export function fromPromise(plugin: Plugin) {
|
||||
}),
|
||||
)
|
||||
|
||||
const context2: Context = {
|
||||
const context2: Omit<Context, "signal"> = {
|
||||
app: host.app,
|
||||
options: host.options,
|
||||
agent: {
|
||||
@@ -132,7 +110,11 @@ export function fromPromise(plugin: Plugin) {
|
||||
aisdk: {
|
||||
hook: (name, callback, options) =>
|
||||
register(
|
||||
host.aisdk.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))), options),
|
||||
host.aisdk.hook(
|
||||
name,
|
||||
(event) => attempt((signal) => Promise.resolve(callback(event, { signal }))).pipe(Effect.orDie),
|
||||
options,
|
||||
),
|
||||
),
|
||||
},
|
||||
catalog: {
|
||||
@@ -280,8 +262,21 @@ export function fromPromise(plugin: Plugin) {
|
||||
}),
|
||||
),
|
||||
),
|
||||
hook: (name, callback) =>
|
||||
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
hook: (name, callback) => {
|
||||
if (name === "execute.before") {
|
||||
const before = callback as BeforeHook
|
||||
return register(
|
||||
host.tool.hook("execute.before", (event) =>
|
||||
attemptTool((signal) => Promise.resolve(before(event, { signal }))),
|
||||
),
|
||||
)
|
||||
}
|
||||
return register(
|
||||
host.tool.hook(name, (event) =>
|
||||
attempt((signal) => Promise.resolve(callback(event, { signal }))).pipe(Effect.orDie),
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
websearch: {
|
||||
providers: adaptApiMethod(WebSearchEndpoints["websearch.providers"], host.websearch.providers),
|
||||
@@ -305,7 +300,11 @@ export function fromPromise(plugin: Plugin) {
|
||||
session: {
|
||||
hook: (name, callback, options) =>
|
||||
register(
|
||||
host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))), options),
|
||||
host.session.hook(
|
||||
name,
|
||||
(event) => attempt((signal) => Promise.resolve(callback(event, { signal }))).pipe(Effect.orDie),
|
||||
options,
|
||||
),
|
||||
),
|
||||
create: adaptApiMethod(SessionEndpoints["session.create"], host.session.create),
|
||||
get: adaptApiMethod(SessionEndpoints["session.get"], host.session.get),
|
||||
@@ -321,27 +320,45 @@ export function fromPromise(plugin: Plugin) {
|
||||
},
|
||||
shell: {
|
||||
hook: (name, callback) =>
|
||||
register(host.shell.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
register(
|
||||
host.shell.hook(name, (event) =>
|
||||
attempt((signal) => Promise.resolve(callback(event, { signal }))).pipe(Effect.orDie),
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const cleanup = yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
|
||||
if (!cleanup) return
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => Promise.resolve(cleanup())))
|
||||
const controller = new AbortController()
|
||||
const cleanup = yield* attempt((signal) => {
|
||||
signal.addEventListener("abort", () => controller.abort(), { once: true })
|
||||
return Promise.resolve(plugin.setup({ ...context2, signal: controller.signal }))
|
||||
}).pipe(Effect.orDie)
|
||||
if (cleanup) yield* Effect.addFinalizer(() => Effect.promise(() => Promise.resolve(cleanup())))
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => controller.abort()))
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function attempt<A>(evaluate: (signal: AbortSignal) => PromiseLike<A>) {
|
||||
return Effect.tryPromise({ try: evaluate, catch: (cause) => cause })
|
||||
}
|
||||
|
||||
type RuntimeSchema = Schema.Codec<unknown, unknown>
|
||||
function attemptTool<A>(evaluate: (signal: AbortSignal) => PromiseLike<A>) {
|
||||
return Effect.tryPromise({
|
||||
try: evaluate,
|
||||
catch: (cause) =>
|
||||
Option.getOrElse(
|
||||
Schema.decodeUnknownOption(Tool.Error)(cause),
|
||||
() => new Tool.Error({ message: cause instanceof Error ? cause.message : String(cause), error: cause }),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
const executePromiseTool = (tool: Info, input: any, context: Tool.Context) =>
|
||||
Effect.promise(() =>
|
||||
attemptTool((signal) =>
|
||||
tool.execute(input, {
|
||||
...context,
|
||||
signal,
|
||||
progress: (update) => Effect.runPromise(context.progress(update)),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { ToolDomain } from "./tool.js"
|
||||
import type { WebSearchDomain } from "./websearch.js"
|
||||
|
||||
export interface Context {
|
||||
readonly signal: AbortSignal
|
||||
readonly app: App
|
||||
readonly options: PluginOptions
|
||||
readonly agent: AgentDomain
|
||||
|
||||
@@ -7,14 +7,18 @@ export interface ModelHookOptions {
|
||||
readonly providerID?: string
|
||||
}
|
||||
|
||||
export interface InvocationContext {
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
|
||||
export type Hooks<Spec> = <Name extends keyof Spec>(
|
||||
name: Name,
|
||||
callback: (input: Spec[Name]) => Promise<void> | void,
|
||||
callback: (input: Spec[Name], context: InvocationContext) => Promise<void> | void,
|
||||
) => Promise<Registration>
|
||||
|
||||
export type ModelHooks<Spec> = <Name extends keyof Spec>(
|
||||
name: Name,
|
||||
callback: (input: Spec[Name]) => Promise<void> | void,
|
||||
callback: (input: Spec[Name], context: InvocationContext) => Promise<void> | void,
|
||||
options?: ModelHookOptions,
|
||||
) => Promise<Registration>
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ import type { Agent } from "@opencode-ai/schema/agent"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import type { JsonSchema } from "effect"
|
||||
import type { Hooks, Transform } from "./registration.js"
|
||||
import type { Hooks, InvocationContext, Transform } from "./registration.js"
|
||||
|
||||
export interface ToolContext extends Omit<Tool.Context, "progress"> {
|
||||
readonly signal: AbortSignal
|
||||
readonly progress: (update: Tool.Metadata) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ interface ToolDraft {
|
||||
): void
|
||||
}
|
||||
|
||||
interface ToolHooks {
|
||||
export interface ToolHooks {
|
||||
readonly "execute.before": {
|
||||
readonly tool: string
|
||||
readonly inputSchema: JsonSchema.JsonSchema
|
||||
@@ -57,6 +58,8 @@ interface ToolHooks {
|
||||
)
|
||||
}
|
||||
|
||||
export type BeforeHook = (input: ToolHooks["execute.before"], context: InvocationContext) => Promise<void> | void
|
||||
|
||||
export interface ToolDomain {
|
||||
readonly transform: Transform<ToolDraft>
|
||||
readonly hook: Hooks<ToolHooks>
|
||||
|
||||
@@ -10,8 +10,10 @@ import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Reference } from "@opencode-ai/schema/reference"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
import { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
import { Effect } from "effect"
|
||||
|
||||
const Plugin = await import("../src/effect/index")
|
||||
const EffectPlugin = await import("../src/effect/plugin")
|
||||
const PromisePlugin = await import("../src/promise/index")
|
||||
const TuiPlugin = await import("../src/tui/index")
|
||||
|
||||
@@ -46,6 +48,15 @@ test.each([
|
||||
])
|
||||
})
|
||||
|
||||
test("effect definitions compile to the Promise plugin ABI", () => {
|
||||
const plugin = Plugin.Plugin.define({ id: "effect", effect: () => Effect.void })
|
||||
const subpath = EffectPlugin.define({ id: "effect-subpath", effect: () => Effect.void })
|
||||
expect(plugin).toHaveProperty("setup")
|
||||
expect(plugin).not.toHaveProperty("effect")
|
||||
expect(subpath).toHaveProperty("setup")
|
||||
expect(subpath).not.toHaveProperty("effect")
|
||||
})
|
||||
|
||||
test("tui entrypoint exposes the plugin definition", () => {
|
||||
const plugin = TuiPlugin.Plugin.define({ id: "demo", setup() {} })
|
||||
expect(plugin.id).toBe("demo")
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { afterAll, expect, test } from "bun:test"
|
||||
import { cp, mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
import type { Tool } from "@opencode-ai/schema/tool"
|
||||
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Plugin } from "../src/effect/index.js"
|
||||
import type { Context } from "../src/promise/plugin.js"
|
||||
|
||||
const fixture = prepareAdapter()
|
||||
|
||||
afterAll(async () => rm((await fixture).directory, { recursive: true, force: true }))
|
||||
|
||||
test("owns Effect callbacks and finalizers inside the Promise plugin lifecycle", async () => {
|
||||
const events: string[] = []
|
||||
const host = {
|
||||
signal: new AbortController().signal,
|
||||
app: {},
|
||||
options: {},
|
||||
agent: {},
|
||||
aisdk: {
|
||||
hook: async (_name: string, callback: () => Promise<void>) => {
|
||||
events.push("registered")
|
||||
await callback(undefined, { signal: new AbortController().signal })
|
||||
return { dispose: async () => void events.push("disposed") }
|
||||
},
|
||||
},
|
||||
catalog: { provider: {}, model: {} },
|
||||
command: {},
|
||||
event: {},
|
||||
integration: { connect: {}, oauth: {}, command: {}, connection: {} },
|
||||
mcp: {},
|
||||
plugin: {},
|
||||
reference: {},
|
||||
session: {},
|
||||
shell: {},
|
||||
skill: {},
|
||||
storage: {},
|
||||
tool: {},
|
||||
websearch: {},
|
||||
} as unknown as Context
|
||||
const plugin = Plugin.define({
|
||||
id: "lifecycle",
|
||||
effect: (context) =>
|
||||
Effect.gen(function* () {
|
||||
yield* context.aisdk.hook("sdk", () => Effect.sync(() => events.push("callback")))
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => events.push("finalized")))
|
||||
}),
|
||||
})
|
||||
|
||||
const cleanup = await plugin.setup(host)
|
||||
expect(events).toEqual(["registered", "callback"])
|
||||
expect(cleanup).toBeFunction()
|
||||
if (cleanup) await cleanup()
|
||||
expect(events).toEqual(["registered", "callback", "finalized", "disposed"])
|
||||
})
|
||||
|
||||
test("interrupts Effect setup through the Promise setup signal", async () => {
|
||||
const controller = new AbortController()
|
||||
const events: string[] = []
|
||||
const plugin = Plugin.define({
|
||||
id: "interrupt-setup",
|
||||
effect: () =>
|
||||
Effect.sync(() => events.push("started")).pipe(
|
||||
Effect.andThen(Effect.never),
|
||||
Effect.ensuring(Effect.sync(() => events.push("finalized"))),
|
||||
),
|
||||
})
|
||||
const setup = plugin.setup({
|
||||
signal: controller.signal,
|
||||
app: {},
|
||||
options: {},
|
||||
agent: {},
|
||||
aisdk: {},
|
||||
catalog: { provider: {}, model: {} },
|
||||
command: {},
|
||||
event: {},
|
||||
integration: { connect: {}, oauth: {}, command: {}, connection: {} },
|
||||
mcp: {},
|
||||
plugin: {},
|
||||
reference: {},
|
||||
session: {},
|
||||
shell: {},
|
||||
skill: {},
|
||||
storage: {},
|
||||
tool: {},
|
||||
websearch: {},
|
||||
} as unknown as Context)
|
||||
while (events.length === 0) await Bun.sleep(0)
|
||||
controller.abort()
|
||||
|
||||
await expect(setup).rejects.toBeDefined()
|
||||
expect(events).toEqual(["started", "finalized"])
|
||||
})
|
||||
|
||||
test("converts schemas entirely inside the plugin Effect runtime", async () => {
|
||||
const prepared = await fixture
|
||||
const input = prepared.toStandardSchema(prepared.Schema.FiniteFromString, "input") as StandardSchemaV1 &
|
||||
StandardJSONSchemaV1
|
||||
const output = prepared.toStandardSchema(prepared.Schema.FiniteFromString, "output") as StandardSchemaV1 &
|
||||
StandardJSONSchemaV1
|
||||
|
||||
expect(Schema.isSchema(input)).toBe(false)
|
||||
expect(await input["~standard"].validate("42")).toEqual({ value: 42 })
|
||||
expect(input["~standard"].jsonSchema.input({ target: "draft-2020-12" })).toMatchObject({ type: "string" })
|
||||
expect(await output["~standard"].validate(42)).toEqual({ value: "42" })
|
||||
expect(output["~standard"].jsonSchema.output({ target: "draft-2020-12" })).toMatchObject({ type: "string" })
|
||||
})
|
||||
|
||||
test("rejects schemas from a different plugin Effect runtime", async () => {
|
||||
const prepared = await fixture
|
||||
expect(() => prepared.toStandardSchema(Schema.String, "input")).toThrow("must use the plugin's Effect peer")
|
||||
})
|
||||
|
||||
async function prepareAdapter() {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "opencode-effect-adapter-"))
|
||||
await cp(
|
||||
path.dirname(fileURLToPath(import.meta.resolve("effect/package.json"))),
|
||||
path.join(directory, "node_modules", "effect"),
|
||||
{ recursive: true },
|
||||
)
|
||||
await cp(new URL("../src/effect/tool-schema.ts", import.meta.url), path.join(directory, "tool-schema.ts"))
|
||||
const adapter = (await import(pathToFileURL(path.join(directory, "tool-schema.ts")).href)) as {
|
||||
toStandardSchema: (schema: Tool.ValueSchema, direction: "input" | "output") => Tool.ValueSchema
|
||||
}
|
||||
const runtime = (await import(
|
||||
pathToFileURL(path.join(directory, "node_modules", "effect", "dist", "index.js")).href
|
||||
)) as {
|
||||
Schema: typeof Schema
|
||||
}
|
||||
return { directory, toStandardSchema: adapter.toStandardSchema, Schema: runtime.Schema }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user