Compare commits

..
Author SHA1 Message Date
Kit Langton b7d0582a1f fix(plugin): own effect plugin runtime 2026-08-24 09:17:45 -04:00
46 changed files with 319 additions and 419 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@opencode-ai/plugin": patch
---
Export the Effect runtime used by Effect plugins and safely adapt its tool schemas across host module instances.
-6
View File
@@ -89,12 +89,6 @@ jobs:
working-directory: packages/codemode
run: bun run script/publish.ts --dry-run
- name: Verify packed workerd SDK
if: runner.os == 'Linux'
timeout-minutes: 15
working-directory: packages/sdk
run: bun run verify:package
- name: Verify compiled service lifecycle
if: always()
timeout-minutes: 10
+16 -16
View File
@@ -595,8 +595,8 @@
},
"peerDependencies": {
"@opencode-ai/theme": "workspace:*",
"@opentui/core": ">=0.5.8",
"@opentui/solid": ">=0.5.8",
"@opentui/core": ">=0.5.7",
"@opentui/solid": ">=0.5.7",
"solid-js": ">=1.9.0",
},
"optionalPeers": [
@@ -1090,9 +1090,9 @@
"@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.5.8",
"@opentui/keymap": "0.5.8",
"@opentui/solid": "0.5.8",
"@opentui/core": "0.5.7",
"@opentui/keymap": "0.5.7",
"@opentui/solid": "0.5.7",
"@pierre/diffs": "1.2.10",
"@playwright/test": "1.59.1",
"@sentry/solid": "10.36.0",
@@ -2218,27 +2218,27 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
"@opentui/core": ["@opentui/core@0.5.8", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.8", "@opentui/core-darwin-x64": "0.5.8", "@opentui/core-linux-arm64": "0.5.8", "@opentui/core-linux-arm64-musl": "0.5.8", "@opentui/core-linux-x64": "0.5.8", "@opentui/core-linux-x64-musl": "0.5.8", "@opentui/core-win32-arm64": "0.5.8", "@opentui/core-win32-x64": "0.5.8" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-GbZ+nSLYZqxj2Z5TU19Mx6IsAvVsn2+7WEXz+6OlMGoootvt3TxP3vWwfGI0jWk9qp1ftRlML1JPNEkzy+9I8g=="],
"@opentui/core": ["@opentui/core@0.5.7", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.7", "@opentui/core-darwin-x64": "0.5.7", "@opentui/core-linux-arm64": "0.5.7", "@opentui/core-linux-arm64-musl": "0.5.7", "@opentui/core-linux-x64": "0.5.7", "@opentui/core-linux-x64-musl": "0.5.7", "@opentui/core-win32-arm64": "0.5.7", "@opentui/core-win32-x64": "0.5.7" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-/XDabTkfBs2Wy2FhlC4jvzbpphYAs4SlnCRKYEvi+metlXNTSAshSs43wqZ9O4IPd0E4EcQqdfeQ0sdc3yPpYw=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c9Y1FBrSnA4sKUCMETsrLYOmsMTyJae8mU9cE6M4o9rXcr3ZLPA7o9AkPA3S0+kH+kZ0o1Fn0wLejgNxvEp5mg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-75TDJgFD6hDoCElIX35Yg3TIRF/jhrtVQO/9snhjGuTsZ7bd0W88jUlvSXSNhqB3CX431rwgi47B+asWPDI1lQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-oZ/6Iz1KN+4volMFKmmvziYJhMgyyJ99LfK1S+uPRyIRqzT3CESoJ58D4h04M1g0dHeHE4PvkU1p3uQKrXiP3g=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-j+Dwu2yV8zahBFjnVIssYdPrHz/+pkd7xWxI+nHjV41V3c+scvH6vJY1U85VCVaoQs/zCH5xAJbmAAn7Sgeh8g=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-N6i/ocrsTjIq9aUQyrfJkqUo+tc4P5ZS6xz38Cm1MhtDzwBE+CrNIJdHfoJfmRLhJkYHlw6Z0ToICn8ZNj4Bpw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-gWO9NWaivXRPc0XhEbxehj3ApyC1SW8Bf1cnHnME1zhw/EZNCNE3TQHgKwOnnNKtvlbhzLQjOI4OqyPYV8UcEQ=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-eFMB41AWODaYf8PsCx3vtTMX33tFgtSpR9tKNnUErCXlcnZ+WCRHVaJ9F6DrVNX3ir6HmgbwAmaWXvaVF+kLtg=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RumSHTasIAWU7jPBKiTuEZG/m+prfQKCHhL3JAQnBLgp7eAB20XZGfSLVWDBgxkyKq1rHCUgLtZktC5ZAyKfTA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.8", "", { "os": "linux", "cpu": "x64" }, "sha512-/2QM7/wMnML/sxchzbwgoU5tUu/7k836/kSOKMti8opjuecv1K+WWNKGXufhTNeRcXFZaba5rsCdYrf3VqnVsQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-fXDmrIlfp9xaoVkk3BNn3yUO/b7plBSOfUh2oWOezRA6E/g5z+hTO1alGFQCy+VV+1kfA6iuYadAww0xNtfACg=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YXo+qUHYmep2uvv3ECvTeqr10aD7+lBsavYmsTLBzS5hHabbzlQ10oX/99nIRPC3Au1BMWD6d6zQP5czPx23eg=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-1opDV+W7C1F4iWivCNKuYl5Hen+IeFXAuUPm3hVyN0UtdcP8LESpFJTr8QRwHpGSE9Jz1K2g2S2ipzI3u5mhgw=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7qBdhEAlh4tLFzW7nWLPlREtNiF6NZMDMi+4uDpUlAMhRavLr6wjcIcgfhNAF/puq06DjuZlPTwaMCgB3qOuwA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-gbiZUyttjq8s+bsnsaopygWN7LTS/RyzD6GOmpgOB2TVitvo0P6mxM+AebBGuu/7xri17Rqaxf3uqrgdKiJAeg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.8", "", { "os": "win32", "cpu": "x64" }, "sha512-Z76YaTKnmRDSHKdKa7iTBCXBQdInQLq7UG3qIE84nvyHCDtkhYtPWOaCzDisSgXC8Y6hJt6NiFbaNjQyxPkfpQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-bDwon45lUxbV3rMcekTCg4mXcBu9uhdG6HLeLK5TVZf05/h+Ibkf2UeZ8A2jjQt+MK6jpYmxWwDCVCTzZ7EU7Q=="],
"@opentui/keymap": ["@opentui/keymap@0.5.8", "", { "dependencies": { "@opentui/core": "0.5.8" }, "peerDependencies": { "@opentui/react": "0.5.8", "@opentui/solid": "0.5.8", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-KQHKRnLZroSZIbHmGmSeDPsXi7Yykdu9x9ACmDzBFkgPGx0EIuXHqRcmrqmUaX485MQEZuCD9t6FdDsOMGyIFg=="],
"@opentui/keymap": ["@opentui/keymap@0.5.7", "", { "dependencies": { "@opentui/core": "0.5.7" }, "peerDependencies": { "@opentui/react": "0.5.7", "@opentui/solid": "0.5.7", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-BqfSbjlLuctnew2aoPPdK3wpzJfEPp7ttLBkTu1wJTkps9AcfUgXqwloqvY4DUKjUBNFYYEpU/0CPxI4Blj4MA=="],
"@opentui/solid": ["@opentui/solid@0.5.8", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.8", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-L0NxuAU8XT+jlE5G90oA3kspqkof48b0hmzi5XLw+1gxnkxrkTb+YfKys+GzVK4UqhgwY9aW+TeDfrKbnfwCMw=="],
"@opentui/solid": ["@opentui/solid@0.5.7", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.7", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-qrKAZd9xt4D67LXZUARg1Aw0dwEWi29GyOxpM6abqmyExGPMS7jih5Z9oMQJ9EXsouDKpW7Mejjq7WbX2AaecQ=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-phyTF0/jQZ3L0B66PSLdpH//kyPc1M6j5a40wCSx7TA=",
"aarch64-linux": "sha256-1Zb/Is0ujIslCbPPusAVhcuzAPyIauQyeIIRRGtzpAk=",
"aarch64-darwin": "sha256-DDsVm7z+PSDry6QqrwVDFSmEnq6jIKb709Y4ymAv9f8=",
"x86_64-darwin": "sha256-S+5LI2J+WRhRP7jp2PAv6AesXk238wEYoyIO1oKdF3w="
"x86_64-linux": "sha256-LvDHCOm8OAZfvb0I0L6AbdOevRoQmEJnnqrSgAiNHv8=",
"aarch64-linux": "sha256-O0L0iHjb4cwl9xWHIna8VFHyQzoAKzfY8oMpVNayMOg=",
"aarch64-darwin": "sha256-ETP8FE71NqufYDUbR7tBdsMEOVQ44wLmsZBeZiSRBRY=",
"x86_64-darwin": "sha256-WUcoLldDriT3QxcdlnBQhuPrxDNub0EDvvZXk/pDMpY="
}
}
+3 -3
View File
@@ -49,9 +49,9 @@
"@octokit/rest": "22.0.0",
"@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.5.8",
"@opentui/keymap": "0.5.8",
"@opentui/solid": "0.5.8",
"@opentui/core": "0.5.7",
"@opentui/keymap": "0.5.7",
"@opentui/solid": "0.5.7",
"@tanstack/solid-virtual": "3.13.37",
"@shikijs/stream": "4.2.0",
"@standard-schema/spec": "1.1.0",
+1 -8
View File
@@ -1,14 +1,7 @@
import { Argument, Flag, GlobalFlag } from "effect/unstable/cli"
import { Argument, Flag } from "effect/unstable/cli"
import { Schema } from "effect"
import { Spec } from "../framework/spec"
export const PrintLogs = GlobalFlag.setting("print-logs")({
flag: Flag.boolean("print-logs").pipe(
Flag.withDescription("Print logs to stderr (server logs require --standalone)"),
Flag.withDefault(false),
),
})
declare const OPENCODE_CLI_NAME: string | undefined
const ServerParams = {
+1 -7
View File
@@ -1,6 +1,5 @@
import { Effect, FileSystem, Scope } from "effect"
import { Command } from "effect/unstable/cli"
import { PrintLogs } from "../commands/commands"
import { Spec } from "./spec"
import { Global } from "@opencode-ai/util/global"
import { Updater } from "../services/updater"
@@ -78,11 +77,7 @@ export function handlers<const Root extends Spec.Any>(root: Root, handlers: Hand
}
export function run(commands: Spec.Any, handlers: ReadonlyArray<LazyHandler>, options: { readonly version: string }) {
return Command.run(provide(commands, handlers).pipe(Command.withGlobalFlags([PrintLogs])), options) as Effect.Effect<
void,
unknown,
Command.Environment
>
return Command.run(provide(commands, handlers), options) as Effect.Effect<void, unknown, Command.Environment>
}
function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
@@ -91,7 +86,6 @@ function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): Provided
? node.spec.pipe(
Command.withHandler((input) =>
Effect.gen(function* () {
if (yield* PrintLogs) process.env.OPENCODE_PRINT_LOGS = "1"
const module = yield* Effect.promise(handler.load)
return yield* module.default(input)
}),
@@ -3,12 +3,16 @@ import {
Command,
Connection,
Credential,
Effect,
Integration,
Mcp,
Model,
Plugin,
Provider,
Reference,
Schema,
Skill,
WebSearch,
} from "@opencode-ai/plugin/effect"
import { Tool } from "@opencode-ai/schema/tool"
@@ -18,11 +22,15 @@ const key = Symbol.for("opencode.plugin.v2.effect")
Command,
Connection,
Credential,
Effect,
Integration,
Mcp,
Model,
Plugin,
Provider,
Reference,
Schema,
Skill,
WebSearch,
Tool: { Error: Tool.Error },
}
@@ -4,11 +4,13 @@ import {
Connection,
Credential,
Integration,
Mcp,
Model,
Plugin,
Provider,
Reference,
Skill,
WebSearch,
} from "@opencode-ai/plugin"
const key = Symbol.for("opencode.plugin.v2.promise")
@@ -18,9 +20,11 @@ const key = Symbol.for("opencode.plugin.v2.promise")
Connection,
Credential,
Integration,
Mcp,
Model,
Plugin,
Provider,
Reference,
Skill,
WebSearch,
}
+1 -1
View File
@@ -27,7 +27,7 @@ function command(password: string, options: Options) {
// The server treats EOF on this pipe as the end of its ownership lease.
// The OS closes it even when the TUI is killed before Effect finalizers run.
stdin: "pipe",
stderr: process.env.OPENCODE_PRINT_LOGS === "1" ? "inherit" : "ignore",
stderr: "ignore",
killSignal: "SIGTERM",
forceKillAfter: "3 seconds",
})
+4 -1
View File
@@ -127,14 +127,17 @@ export const Command = sdk.Command
export const Connection = sdk.Connection
export const Credential = sdk.Credential
export const Integration = sdk.Integration
export const Mcp = sdk.Mcp
export const Model = sdk.Model
export const Plugin = sdk.Plugin
export const Provider = sdk.Provider
export const Reference = sdk.Reference
export const Skill = sdk.Skill`
export const Skill = sdk.Skill
export const WebSearch = sdk.WebSearch`
const effectModule = promiseModule
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
.replace("Promise plugin", "Effect plugin")
.concat("\nexport const Effect = sdk.Effect\nexport const Schema = sdk.Schema")
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`
+1 -1
View File
@@ -95,7 +95,7 @@ await Effect.runPromise(
),
write(
emitEffectImported(effectContract, {
module: "../../contract.js",
module: "../../contract",
api: "ClientApi",
shapeModule: "../api/api.js",
}),
@@ -3,7 +3,7 @@ import { Effect, Stream, Schema } from "effect"
import { Sse } from "effect/unstable/encoding"
import { HttpClientError } from "effect/unstable/http"
import { HttpApiClient } from "effect/unstable/httpapi"
import { ClientApi } from "../../contract.js"
import { ClientApi } from "../../contract"
import type {
HealthGetOutput,
ServerGetOutput,
+2 -2
View File
@@ -2,7 +2,7 @@
// 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.js"
export * from "./generated/index"
export type {
AgentApi,
AppApi,
@@ -47,4 +47,4 @@ export { Skill } from "@opencode-ai/schema/skill"
export { Prompt } from "@opencode-ai/schema/prompt"
export { PromptInput } from "@opencode-ai/schema/prompt-input"
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
export type OpenCodeClient = Effect.Success<ReturnType<typeof import("./generated/client.js").make>>
export type OpenCodeClient = Effect.Success<ReturnType<typeof import("./generated/client").make>>
-35
View File
@@ -32,38 +32,3 @@ const result = await Bun.build({
},
})
if (!result.success) throw new AggregateError(result.logs, "Failed to build Core")
// Bun's Node target eagerly creates its shared require helper, so every split
// entry evaluates import.meta.url even when it never requires a module. Keep
// the helper lazy until Bun stops hoisting it into workerd-reachable chunks.
// https://github.com/oven-sh/bun/issues/12615
const eagerRequire = "var __require = /* @__PURE__ */ createRequire(import.meta.url);"
const lazyRequire = `var __require = (specifier) => createRequire(import.meta.url ?? "file:///worker.js")(specifier);
__require.resolve = (specifier, options) => createRequire(import.meta.url ?? "file:///worker.js").resolve(specifier, options);`
const rewritten = await Promise.all(
result.outputs.map(async (output) => {
if (!output.path.endsWith(".js")) return false
const source = await output.text()
const generatedUses = source
.replace(/import\s*\{[^}]*\b__require\b[^}]*\}\s*from\s*["'][^"']+["'];/g, "")
.replace(/export\s*\{[^}]*\b__require\b[^}]*\};/g, "")
.replace(eagerRequire, "")
if (/\bnew\s+__require\s*\(/.test(generatedUses))
throw new Error(`Unsupported generated require constructor in ${output.path}`)
const unsupported = generatedUses
.replace(/\b__require\.resolve\s*\(/g, "")
.replace(/\b__require\s*\(/g, "")
if (/\b__require\b/.test(unsupported)) throw new Error(`Unsupported generated require usage in ${output.path}`)
if (!source.includes(eagerRequire)) return false
if (source.indexOf(eagerRequire) !== source.lastIndexOf(eagerRequire))
throw new Error(`Multiple eager require helpers in ${output.path}`)
const rewrittenSource = source.replace(eagerRequire, lazyRequire)
if (rewrittenSource.includes(eagerRequire)) throw new Error(`Failed to rewrite eager require helper in ${output.path}`)
await Bun.write(output.path, rewrittenSource)
return true
}),
)
if (rewritten.filter(Boolean).length !== 1)
throw new Error("Expected exactly one eager require helper; Bun may have fixed #12615 and made this shim removable")
+1 -1
View File
@@ -1,7 +1,7 @@
export * as Watcher from "./watcher.js"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper.js"
import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
+1 -1
View File
@@ -572,7 +572,7 @@ function cacheKey(source: string) {
}
export function bodyDigest(text: string) {
return Hash.sha256(text)
return new Bun.CryptoHasher("sha256").update(text).digest("hex")
}
export const layer = (options?: Options) =>
+2 -1
View File
@@ -2,6 +2,7 @@ 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 { instanceSafeContext } from "@opencode-ai/plugin/effect/tool-schema"
import { Event } from "@opencode-ai/schema/config"
import { Cause, Effect, Latch, Layer, Schema, Stream } from "effect"
import path from "path"
@@ -126,7 +127,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (
tui: plugin.tui,
version: JSON.stringify(operation),
source: pluginSource(operation.target),
effect: (host) => plugin.effect({ ...host, options: operation.options }),
effect: (host) => plugin.effect(instanceSafeContext({ ...host, options: operation.options })),
} satisfies Plugin.Versioned
})
+1 -1
View File
@@ -1,2 +1,2 @@
export * as SessionMessage from "@opencode-ai/schema/session-message"
export * as SessionMessage from "./message.js"
export * from "@opencode-ai/schema/session-message"
+1 -1
View File
@@ -1,4 +1,4 @@
export * as SessionSchema from "@opencode-ai/schema/session"
export * as SessionSchema from "./schema.js"
import { Session } from "@opencode-ai/schema/session"
+15 -33
View File
@@ -49,12 +49,7 @@ type ToolRow = {
duration: number | null
}
type ToolSummaryRow = {
calls: number
succeeded: number
failed: number
unfinished: number
}
type ToolSummaryRow = { status: string | null; count: number }
type ModelAggregate = {
model: Model.Ref
@@ -193,38 +188,25 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
(range) => {
if (toolMode === "summary")
return db
.get<ToolSummaryRow>(
.all<ToolSummaryRow>(
sql`
WITH calls AS MATERIALIZED (
SELECT json_extract(content.value, '$.state.status') AS status
FROM ${SessionMessageTable} AS message
JOIN ${SessionTable} AS session ON session.id = message.session_id,
json_each(message.data, '$.content') AS content
WHERE message.type = 'assistant'
AND message.time_created >= ${range.from}
AND message.time_created < ${range.to}
AND (session.fork_session_id IS NULL OR message.time_created >= session.time_created)
AND json_extract(content.value, '$.type') = 'tool'
${project}
)
SELECT
count(*) AS calls,
count(*) FILTER (WHERE status = 'completed') AS succeeded,
count(*) FILTER (WHERE status = 'error') AS failed,
count(*) FILTER (WHERE status IS NULL OR status NOT IN ('completed', 'error')) AS unfinished
FROM calls
SELECT json_extract(content.value, '$.state.status') AS status, count(*) AS count
FROM ${SessionMessageTable} AS message
JOIN ${SessionTable} AS session ON session.id = message.session_id,
json_each(message.data, '$.content') AS content
WHERE message.type = 'assistant'
AND message.time_created >= ${range.from}
AND message.time_created < ${range.to}
AND (session.fork_session_id IS NULL OR message.time_created >= session.time_created)
AND json_extract(content.value, '$.type') = 'tool'
${project}
GROUP BY status
`,
)
.pipe(
Effect.orDie,
Effect.tap((row) =>
Effect.sync(() => {
if (!row) return
toolTotals.calls += row.calls
toolTotals.succeeded += row.succeeded
toolTotals.failed += row.failed
toolTotals.unfinished += row.unfinished
}),
Effect.tap((rows) =>
Effect.sync(() => rows.forEach((row) => addToolStatus(toolTotals, row.status, row.count))),
),
Effect.asVoid,
)
@@ -1,5 +1,4 @@
import { Plugin } from "@opencode-ai/plugin/effect"
import { Effect } from "effect"
import { Effect, Plugin } from "@opencode-ai/plugin/effect"
export default Plugin.define({
id: "config-effect-plugin",
+2 -10
View File
@@ -113,13 +113,6 @@ describe("SessionStats", () => {
completed: DateTime.makeUnsafe(Date.UTC(2026, 0, 2, 10, 0, 2)),
},
}),
SessionMessage.AssistantTool.make({
type: "tool",
id: "call_pending",
name: "pending",
state: SessionMessage.ToolStateRunning.make({ status: "running", input: {}, metadata: {} }),
time: { created: DateTime.makeUnsafe(Date.UTC(2026, 0, 2, 10)) },
}),
]),
),
messageRow(childID, 1, assistant("msg_stats_child", Date.UTC(2026, 0, 3, 10), [], "large", 2)),
@@ -217,7 +210,7 @@ describe("SessionStats", () => {
expect(stats.cost).toBe(Money.USD.make(6.25))
expect(stats.tools).toMatchObject({
mode: "detail",
totals: { calls: 3, succeeded: 1, failed: 1, unfinished: 1 },
totals: { calls: 2, succeeded: 1, failed: 1, unfinished: 0 },
})
expect(stats.activity).toEqual([
{ date: "2026-01-02", steps: 1 },
@@ -231,7 +224,6 @@ describe("SessionStats", () => {
expect(stats.tools.usage).toMatchObject([
{ name: "read", calls: 1, succeeded: 1, failed: 0, durationP50: 250 },
{ name: "edit", calls: 1, succeeded: 0, failed: 1, durationP50: 2_000 },
{ name: "pending", calls: 1, succeeded: 0, failed: 0, unfinished: 1 },
])
const summary = yield* SessionStats.get({
@@ -242,7 +234,7 @@ describe("SessionStats", () => {
expect(summary.models.map((model) => String(model.model.id))).toEqual(["large", "sonnet", "fork-new"])
expect(summary.tools).toEqual({
mode: "summary",
totals: { calls: 3, succeeded: 1, failed: 1, unfinished: 1 },
totals: { calls: 2, succeeded: 1, failed: 1, unfinished: 0 },
})
const withoutTools = yield* SessionStats.get({
+2 -2
View File
@@ -30,8 +30,8 @@
},
"peerDependencies": {
"@opencode-ai/theme": "workspace:*",
"@opentui/core": ">=0.5.8",
"@opentui/solid": ">=0.5.8",
"@opentui/core": ">=0.5.7",
"@opentui/solid": ">=0.5.7",
"solid-js": ">=1.9.0"
},
"peerDependenciesMeta": {
+1 -2
View File
@@ -8,8 +8,7 @@ The Effect plugin API grants plugins two in-process capabilities:
## Defining A Plugin
```ts
import { Plugin } from "@opencode-ai/plugin/effect"
import { Effect } from "effect"
import { Effect, Plugin } from "@opencode-ai/plugin/effect"
export default Plugin.define({
id: "example",
+1
View File
@@ -1,5 +1,6 @@
export * as Plugin from "./plugin.js"
export type { StorageEntry, StorageScanOptions, StorageScanResult } from "../storage.js"
export { Effect, Schema } from "effect"
export { Agent } from "@opencode-ai/schema/agent"
export { Command } from "@opencode-ai/schema/command"
+1 -1
View File
@@ -43,6 +43,6 @@ export interface Plugin<R = Scope.Scope> {
readonly effect: (context: Context) => Effect.Effect<void, never, R>
}
export function define<R = Scope.Scope>(plugin: Plugin<R>) {
export function define<R = Scope.Scope>(plugin: Plugin<R>): Plugin<R> {
return plugin
}
+48
View File
@@ -0,0 +1,48 @@
import type { Tool } from "@opencode-ai/schema/tool"
import { Schema, SchemaAST } from "effect"
import type { Context } from "./plugin.js"
export function instanceSafeContext(context: Context): Context {
return {
...context,
tool: {
...context.tool,
transform: (callback) =>
context.tool.transform((draft) => callback({ add: (tool) => draft.add(instanceSafeTool(tool)) })),
},
}
}
export function instanceSafeTool(tool: Tool.Info<any, any>): Tool.Info<any, any> {
const input = instanceSafeValueSchema(tool.input, "input")
const output = tool.output === undefined ? undefined : instanceSafeValueSchema(tool.output, "output")
if (input === tool.input && output === tool.output) return tool
return { ...tool, input, ...(output === undefined ? {} : { output }) }
}
function instanceSafeValueSchema(schema: Tool.ValueSchema<any>, direction: "input" | "output"): Tool.ValueSchema<any> {
if (typeof schema === "object" && schema !== null && "~standard" in schema) {
const standard = schema["~standard"] as Record<string, any>
if (
typeof standard.validate !== "function" ||
typeof standard.jsonSchema?.input !== "function" ||
typeof standard.jsonSchema?.output !== "function"
)
throw new Error("Tool schemas must implement Standard Schema validation and JSON Schema generation")
return { "~standard": standard } as Tool.ValueSchema<any>
}
if (!Schema.isSchema(schema)) return schema
// Native codecs can only be compiled by the Effect instance that authored their AST.
if (!(schema.ast instanceof SchemaAST.Base)) {
throw new Error(
"Effect tool schemas must use Schema from @opencode-ai/plugin/effect or be converted to Standard Schema by their authoring Effect instance",
)
}
const codec = schema as Schema.Codec<unknown, unknown>
// Standard Schema validates Encoded -> Type, so flip outputs to run Type -> Encoded.
const oriented = direction === "input" ? codec : Schema.flip(codec)
const augmented = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(oriented)) as unknown as {
readonly "~standard": Record<string, unknown>
}
return { "~standard": augmented["~standard"] } as Tool.ValueSchema<any>
}
+31 -1
View File
@@ -1,4 +1,5 @@
import { expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { Agent } from "@opencode-ai/schema/agent"
import { Command } from "@opencode-ai/schema/command"
import { Connection } from "@opencode-ai/schema/connection"
@@ -30,7 +31,10 @@ test.each([
expect(entrypoint.Reference).toBe(Reference)
expect(entrypoint.Skill).toBe(Skill)
expect(entrypoint.WebSearch).toBe(WebSearch)
expect(Object.keys(entrypoint).sort()).toEqual([
})
test("promise entrypoint exposes its public contract", () => {
expect(Object.keys(PromisePlugin).sort()).toEqual([
"Agent",
"Command",
"Connection",
@@ -46,6 +50,32 @@ test.each([
])
})
test("effect entrypoint owns its Effect runtime", () => {
expect(Plugin.Effect).toBe(Effect)
expect(Plugin.Schema).toBe(Schema)
expect(Object.keys(Plugin).sort()).toEqual([
"Agent",
"Command",
"Connection",
"Credential",
"Effect",
"Integration",
"Mcp",
"Model",
"Plugin",
"Provider",
"Reference",
"Schema",
"Skill",
"WebSearch",
])
})
test("effect plugin definition preserves identity", () => {
const definition = { id: "demo", effect: () => Effect.void }
expect(Plugin.Plugin.define(definition)).toBe(definition)
})
test("tui entrypoint exposes the plugin definition", () => {
const plugin = TuiPlugin.Plugin.define({ id: "demo", setup() {} })
expect(plugin.id).toBe("demo")
@@ -0,0 +1,121 @@
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 { instanceSafeContext } from "../src/effect/tool-schema.js"
const fixture = prepareAdapter()
afterAll(async () => rm((await fixture).directory, { recursive: true, force: true }))
test("prepares tools at the host plugin boundary", async () => {
const registered: Array<Tool.Info> = []
const context = {
tool: {
transform: (callback: (draft: { add: (tool: Tool.Info) => void }) => void) => {
callback({ add: (tool) => registered.push(tool) })
return Effect.succeed({ dispose: Effect.void })
},
},
} as unknown as Parameters<typeof instanceSafeContext>[0]
await Effect.runPromise(
Effect.scoped(
instanceSafeContext(context).tool.transform((draft) =>
draft.add({
name: "create",
description: "Create",
input: Schema.Struct({ title: Schema.String }),
execute: () => Effect.succeed({ content: "ok" }),
}),
),
),
)
expect(registered).toHaveLength(1)
expect(Schema.isSchema(registered[0]?.input)).toBe(false)
})
test("converts schemas authored by the plugin Effect runtime", async () => {
const prepared = await fixture
const tool = prepared.instanceSafeTool({
name: "create",
description: "Create",
input: prepared.Schema.FiniteFromString,
output: prepared.Schema.FiniteFromString,
execute: () => prepared.Effect.succeed({ output: 42 }),
})
expect(Schema.isSchema(tool.input)).toBe(false)
const input = tool.input as StandardSchemaV1
expect(await input["~standard"].validate("42")).toEqual({ value: 42 })
expect(await input["~standard"].validate(42)).toHaveProperty("issues")
expect((input as StandardJSONSchemaV1)["~standard"].jsonSchema.input({ target: "draft-2020-12" })).toMatchObject({
type: "string",
})
const output = tool.output as StandardSchemaV1 & StandardJSONSchemaV1
expect(await output["~standard"].validate(42)).toEqual({ value: "42" })
expect(output["~standard"].jsonSchema.output({ target: "draft-2020-12" })).toMatchObject({ type: "string" })
})
test("rejects an unprepared schema from another Effect runtime", async () => {
const prepared = await fixture
expect(() =>
prepared.instanceSafeTool({
name: "create",
description: "Create",
input: Schema.Struct({ title: Schema.String }),
execute: () => prepared.Effect.succeed({ content: "ok" }),
}),
).toThrow("must use Schema from @opencode-ai/plugin/effect")
})
test("accepts a foreign schema prepared by its authoring runtime", async () => {
const prepared = await fixture
const schema = Schema.Struct({ title: Schema.String })
const augmented = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))
const detached = { "~standard": augmented["~standard"] }
const tool = prepared.instanceSafeTool({
name: "create",
description: "Create",
input: detached,
execute: () => prepared.Effect.succeed({ content: "ok" }),
})
expect(Schema.isSchema(tool.input)).toBe(false)
const input = tool.input as StandardSchemaV1 & StandardJSONSchemaV1
expect(await input["~standard"].validate({ title: "probe" })).toEqual({ value: { title: "probe" } })
expect(input["~standard"].jsonSchema.input({ target: "draft-2020-12" })).toMatchObject({ type: "object" })
})
test("rejects incomplete Standard Schema implementations", async () => {
const prepared = await fixture
expect(() =>
prepared.instanceSafeTool({
name: "create",
description: "Create",
input: { "~standard": { version: 1, vendor: "test", validate: () => ({ value: {} }) } } as Tool.ValueSchema,
execute: () => prepared.Effect.succeed({ content: "ok" }),
}),
).toThrow("must implement Standard Schema validation and JSON Schema generation")
})
async function prepareAdapter() {
const directory = await mkdtemp(path.join(tmpdir(), "opencode-plugin-effect-"))
const source = path.dirname(fileURLToPath(import.meta.resolve("effect/package.json")))
await cp(source, 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 {
instanceSafeTool: (tool: Tool.Info) => Tool.Info
}
const runtime = (await import(
pathToFileURL(path.join(directory, "node_modules", "effect", "dist", "index.js")).href
)) as {
Effect: typeof Effect
Schema: typeof Schema
}
return { directory, instanceSafeTool: adapter.instanceSafeTool, Effect: runtime.Effect, Schema: runtime.Schema }
}
+1 -2
View File
@@ -22,8 +22,7 @@
"scripts": {
"build": "bun run script/build.ts",
"test": "bun test --timeout 5000",
"typecheck": "tsgo -b",
"verify:package": "bun run script/verify-package.ts"
"typecheck": "tsgo -b"
},
"dependencies": {
"@opencode-ai/client": "workspace:*",
-210
View File
@@ -1,210 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { fileURLToPath } from "node:url"
const root = fileURLToPath(new URL("../../..", import.meta.url))
const names = ["schema", "codemode", "ai", "util", "protocol", "client", "plugin", "core", "simulation", "server", "sdk"]
const temporary = await mkdtemp(join(tmpdir(), "opencode-sdk-package-"))
const archives = new Map<string, string>()
try {
for (const name of names) {
const directory = join(root, "packages", name)
await $`bun run build`.cwd(directory)
const original = await Bun.file(join(directory, "package.json")).text()
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- package manifests are validated by their package builds.
const pkg = JSON.parse(original) as {
name: string
dependencies?: Record<string, string>
exports?: Record<string, string | { import: string; types: string }>
imports?: Record<string, Record<string, string>>
}
const archive = join(temporary, `${name}.tgz`)
if (pkg.dependencies) {
const unpacked = Object.keys(pkg.dependencies).filter(
(dependency) => dependency.startsWith("@opencode-ai/") && !archives.has(dependency),
)
if (unpacked.length > 0) throw new Error(`${pkg.name} has unpacked workspace dependencies: ${unpacked.join(", ")}`)
pkg.dependencies = Object.fromEntries(
Object.entries(pkg.dependencies).map(([dependency, version]) => {
const local = archives.get(dependency)
return [dependency, local ? `file:${local}` : version]
}),
)
}
if (pkg.exports) {
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [key, { import: output(name, value), types: output(name, value, true) }]
}),
)
}
if (pkg.imports) {
pkg.imports = Object.fromEntries(
Object.entries(pkg.imports).map(([key, conditions]) => [
key,
Object.fromEntries(
Object.entries(conditions).map(([condition, value]) => [condition, output(name, value, condition === "types")]),
),
]),
)
}
await Bun.write(join(directory, "package.json"), JSON.stringify(pkg, null, 2) + "\n")
try {
await $`bun pm pack --filename ${archive} --ignore-scripts --quiet`.cwd(directory)
} finally {
await Bun.write(join(directory, "package.json"), original)
}
archives.set(pkg.name, archive)
}
const consumer = join(temporary, "consumer")
await Bun.write(
join(consumer, "package.json"),
JSON.stringify({ name: "opencode-sdk-consumer", private: true, type: "module" }),
)
await Promise.all([
Bun.write(
join(consumer, "node-import.mjs"),
'await import("@opencode-ai/sdk")\nawait import("@opencode-ai/simulation/backend")\n',
),
Bun.write(
join(consumer, "consumer.ts"),
`import { OpenCode, Tool } from "@opencode-ai/sdk"
import { OpenCodeWorkerd } from "@opencode-ai/sdk/workerd"
OpenCode.create satisfies Function
OpenCodeWorkerd.create satisfies Function
Tool.Error satisfies Function
`,
),
Bun.write(
join(consumer, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
target: "ES2022",
module: "NodeNext",
moduleResolution: "NodeNext",
strict: true,
noEmit: true,
lib: ["ES2022", "DOM", "ESNext.Disposable"],
},
include: ["consumer.ts"],
}),
),
Bun.write(
join(consumer, "wrangler.jsonc"),
JSON.stringify({
name: "opencode-sdk-packed-consumer",
main: "worker.js",
compatibility_date: "2026-07-15",
compatibility_flags: ["nodejs_compat"],
durable_objects: { bindings: [{ name: "OPENCODE", class_name: "OpenCodeDO" }] },
migrations: [{ tag: "v1", new_sqlite_classes: ["OpenCodeDO"] }],
}),
),
Bun.write(
join(consumer, "worker.js"),
`import { bodyDigest } from "@opencode-ai/core/models-dev"
import { OpenCodeWorkerd } from "@opencode-ai/sdk/workerd"
import { Effect } from "effect"
export class OpenCodeDO {
constructor(state) {
this.state = state
}
fetch() {
if (bodyDigest("packed-workerd") !== "5fc174bf63e8dd108ebb6c53d85e7bbc4525b2f4c1c43280364cdbfd9b37aaf5") {
throw new Error("Packed workerd SHA-256 mismatch")
}
const storage = this.state.storage
return Effect.runPromise(
Effect.gen(function* () {
const sdk = yield* OpenCodeWorkerd.create({
storage,
app: { version: "packed-workerd" },
config: { content: "{}" },
})
return Response.json(yield* sdk.health.get())
}).pipe(Effect.scoped),
)
}
}
export default {
fetch(request, env) {
return env.OPENCODE.get(env.OPENCODE.idFromName("packed-consumer")).fetch(request)
},
}
`,
),
Bun.write(
join(consumer, "boot.mjs"),
`import { Miniflare } from "miniflare"
const miniflare = new Miniflare({
compatibilityDate: "2026-07-15",
compatibilityFlags: ["nodejs_compat"],
modules: true,
scriptPath: new URL("./dist/worker.js", import.meta.url).pathname,
durableObjects: { OPENCODE: { className: "OpenCodeDO", useSQLite: true } },
})
try {
const response = await miniflare.dispatchFetch("http://opencode.local/health")
if (response.status !== 200) throw new Error(
"Packed workerd health returned " + response.status + ": " + await response.text(),
)
const body = await response.json()
if (body.healthy !== true || body.version !== "packed-workerd") {
throw new Error("Unexpected packed workerd health: " + JSON.stringify(body))
}
} finally {
await miniflare.dispose()
}
`,
),
])
const sdk = archives.get("@opencode-ai/sdk")
if (!sdk) throw new Error("Packed SDK archive was not created")
await $`npm install --ignore-scripts --no-audit --no-fund --package-lock=false ${sdk} @types/json-schema@7.0.15 typescript@5.8.2 wrangler@4.110.0`.cwd(consumer)
await $`node node-import.mjs`.cwd(consumer)
await $`node_modules/.bin/tsc --noEmit`.cwd(consumer)
await $`node_modules/.bin/wrangler deploy --dry-run --config wrangler.jsonc --outdir dist`.cwd(consumer)
const transpiler = new Bun.Transpiler({ loader: "js" })
const bundled = await Bun.file(join(consumer, "dist/worker.js")).text()
if (/createRequire\s*\(\s*import\.meta\.url\s*\)/.test(bundled)) {
throw new Error("Packed workerd bundle contains Bun's eager Node require initializer")
}
const bunGlobals = Array.from(new Set(bundled.match(/\bBun\.[A-Za-z_$][\w$]*/g) ?? []))
if (bunGlobals.length > 0) throw new Error(`Packed workerd bundle references Bun globals: ${bunGlobals.join(", ")}`)
const leaked = [
...transpiler.scanImports(bundled)
.filter((imported) => imported.kind !== "dynamic-import")
.map((imported) => imported.path),
...Array.from(bundled.matchAll(/\brequire\(\s*["']([^"']+)["']\s*\)/g), (match) => match[1]),
]
.filter((specifier) => specifier === "bun" || specifier.startsWith("bun:"))
if (leaked.length > 0) throw new Error(`Packed workerd bundle statically imports Bun builtins: ${leaked.join(", ")}`)
await $`node boot.mjs`.cwd(consumer)
console.log("packed SDK consumer OK")
} finally {
await rm(temporary, { recursive: true, force: true })
}
function output(name: string, value: string, types = false) {
const root = name === "core" && types ? "./dist/types/" : "./dist/"
return value.replace("./src/", root).replace(/\.ts$/, types ? ".d.ts" : ".js")
}
+2 -2
View File
@@ -1,5 +1,5 @@
export * as OpenCode from "./opencode.js"
export * as Tool from "./tool.js"
export * as OpenCode from "./opencode"
export * as Tool from "./tool"
export { ClientError } from "@opencode-ai/client/effect"
export type { OpenCodeEvent } from "@opencode-ai/client/effect"
+3 -3
View File
@@ -9,10 +9,10 @@ import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
import type { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Config, Context, Effect, Layer, ManagedRuntime, Scope } from "effect"
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"
import * as Logging from "./logging.js"
import * as Logging from "./logging"
export type { LogEntry, LogLevel, LogOptions, LogWriter } from "./logging.js"
import type { LogOptions } from "./logging.js"
export type { LogEntry, LogLevel, LogOptions, LogWriter } from "./logging"
import type { LogOptions } from "./logging"
export interface CreateOptions {
readonly app?: {
+2 -2
View File
@@ -1,9 +1,9 @@
export * as OpenCodeWorkerd from "./workerd.js"
export * as OpenCodeWorkerd from "./workerd"
import type { DurableObjectStorage } from "@opencode-ai/core/database/sqlite.workerd"
import { ServerWorkerd } from "@opencode-ai/server/workerd"
import { Config, Effect, Layer, Scope } from "effect"
import * as OpenCode from "./opencode.js"
import * as OpenCode from "./opencode"
export interface CreateOptions extends Pick<OpenCode.CreateOptions, "log" | "workspaceProviders"> {
readonly storage: DurableObjectStorage
+5 -22
View File
@@ -2,7 +2,6 @@
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { extname } from "node:path"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
@@ -14,26 +13,10 @@ const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: "src" })
)
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" })
await Promise.all(
files
.map(async (file) =>
Bun.write(
file.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
withRelativeExtensions(await transpiler.transform(await Bun.file(file).text())),
),
)
.concat(
await Array.fromAsync(new Bun.Glob("**/*.d.ts").scan({ cwd: "dist" })).then((declarations) =>
declarations.map(async (file) =>
Bun.write(`dist/${file}`, withRelativeExtensions(await Bun.file(`dist/${file}`).text())),
),
),
files.map(async (file) =>
Bun.write(
file.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
await transpiler.transform(await Bun.file(file).text()),
),
),
)
function withRelativeExtensions(source: string) {
return source.replaceAll(
/((?:from\s*|import\s*(?:\(\s*)?)["'])(\.{1,2}\/[^"']+)(["'])/g,
(match, prefix: string, specifier: string, suffix: string) =>
extname(specifier) ? match : `${prefix}${specifier}.js${suffix}`,
)
}
+5 -5
View File
@@ -4,10 +4,10 @@ import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { Config, Effect, FileSystem, Layer } from "effect"
import { HttpClient } from "effect/unstable/http"
import { DriveManifest } from "../manifest.js"
import { SimulationNetwork } from "./network.js"
import { SimulationOpenAI } from "./openai.js"
import { SimulatedProvider } from "./simulated-provider.js"
import { DriveManifest } from "../manifest"
import { SimulationNetwork } from "./network"
import { SimulationOpenAI } from "./openai"
import { SimulatedProvider } from "./simulated-provider"
/**
* Layer replacements applied when the server is built in simulation mode.
@@ -56,4 +56,4 @@ export const simulationReplacements: (app: {
return [[httpClient, networkNode]] satisfies LayerNode.Replacements
})
export * as Simulation from "./index.js"
export * as Simulation from "./index"
+2 -2
View File
@@ -2,7 +2,7 @@ import { Clock, Effect, Layer, Ref } from "effect"
import { HttpClient, HttpClientResponse, type HttpMethod } from "effect/unstable/http"
import { HttpClientError, TransportError } from "effect/unstable/http/HttpClientError"
import type { HttpClientRequest } from "effect/unstable/http"
import { SimulationProtocol } from "../protocol/index.js"
import { SimulationProtocol } from "../protocol"
/**
* Simulated network.
@@ -82,4 +82,4 @@ export const make = Effect.fn("SimulationNetwork.make")(function* (routes: reado
export const layer = (routes: readonly Route[] = []) =>
Layer.effect(HttpClient.HttpClient, make(routes).pipe(Effect.map((run) => run.client)))
export * as SimulationNetwork from "./network.js"
export * as SimulationNetwork from "./network"
+3 -3
View File
@@ -2,8 +2,8 @@ import { Effect, Schema, Stream } from "effect"
import { HttpClientResponse } from "effect/unstable/http"
import { HttpClientError, TransportError } from "effect/unstable/http/HttpClientError"
import { OpenAIChatEvent, DEFAULT_BASE_URL, PATH } from "@opencode-ai/ai/protocols/openai-chat"
import { SimulationNetwork } from "./network.js"
import { SimulatedProvider } from "./simulated-provider.js"
import { SimulationNetwork } from "./network"
import { SimulatedProvider } from "./simulated-provider"
/**
* Driver-answered OpenAI endpoint for the simulated network.
@@ -130,4 +130,4 @@ export const route = (provider: SimulatedProvider.Interface): SimulationNetwork.
},
})
export * as SimulationOpenAI from "./openai.js"
export * as SimulationOpenAI from "./openai"
@@ -20,8 +20,8 @@ import {
Semaphore,
Stream,
} from "effect"
import { SimulationControlServer } from "../control-server.js"
import { SimulationProtocol } from "../protocol/index.js"
import { SimulationControlServer } from "../control-server"
import { SimulationProtocol } from "../protocol"
export interface ProviderRequest {
readonly url: string
@@ -916,4 +916,4 @@ function releaseController(
)
}
export * as SimulatedProvider from "./simulated-provider.js"
export * as SimulatedProvider from "./simulated-provider"
+2 -2
View File
@@ -1,5 +1,5 @@
import { Effect, Fiber, FiberSet, Queue, Stream } from "effect"
import { SimulationProtocol } from "./protocol/index.js"
import { SimulationProtocol } from "./protocol"
export interface Server {
readonly url: string
@@ -181,4 +181,4 @@ function send(socket: Socket, response: SimulationProtocol.JsonRpc.Response | un
return socket.send(JSON.stringify(response))
}
export * as SimulationControlServer from "./control-server.js"
export * as SimulationControlServer from "./control-server"
+1 -1
View File
@@ -107,4 +107,4 @@ export const resolve = Effect.fn("DriveManifest.resolve")(function* () {
)
})
export * as DriveManifest from "./manifest.js"
export * as DriveManifest from "./manifest"
+1 -1
View File
@@ -25,7 +25,7 @@
"default": "./src/global-roots.ts"
},
"#runtime-import": {
"workerd": "./src/runtime/import.workerd.ts",
"workerd": "./src/runtime/import.bun.ts",
"bun": "./src/runtime/import.bun.ts",
"node": "./src/runtime/import.node.ts",
"default": "./src/runtime/import.bun.ts"
+3 -5
View File
@@ -60,10 +60,7 @@ export function fileLogger(target = file(), id: string = runID()) {
})
}
const stderrLogger = Logger.make((options) => {
if (process.env.OPENCODE_PRINT_LOGS !== "1") return
process.stderr.write(formatter().log(options) + "\n")
})
const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n"))
export function minimumLogLevel() {
const value = process.env.OPENCODE_LOG_LEVEL?.toUpperCase()
@@ -77,7 +74,8 @@ export function minimumLogLevel() {
}
export function loggers(local = true, channel = "local") {
return [fileLogger(file(local, channel)), stderrLogger]
const logger = fileLogger(file(local, channel))
return process.env.OPENCODE_PRINT_LOGS === "1" ? [logger, stderrLogger] : [logger]
}
export * as Logging from "./logging.js"
@@ -1,9 +0,0 @@
const unavailable = () => new Error("Dynamic module loading is unavailable on workerd")
export function importModule(_specifier: string): Promise<unknown> {
return Promise.reject(unavailable())
}
export function resolveModule(_specifier: string, _directory: string): string {
throw unavailable()
}
@@ -459,16 +459,16 @@ being resolved.
## Effect
OpenCode provides a first-class Effect API for plugins through the
`@opencode-ai/plugin/effect` entrypoint. Install `effect` alongside the
plugin package and export an `effect` function instead of `setup`:
`@opencode-ai/plugin/effect` entrypoint. It exports the compatible `Effect` and
`Schema` modules alongside the plugin API. Use those exports instead of a separate
`effect` installation so values crossing the plugin boundary share one runtime:
```sh
bun add @opencode-ai/plugin@beta effect
bun add @opencode-ai/plugin@beta
```
```ts title=".opencode/plugins/reviewer-effect.ts"
import { Plugin } from "@opencode-ai/plugin/effect"
import { Effect } from "effect"
import { Effect, Plugin } from "@opencode-ai/plugin/effect"
export default Plugin.define({
id: "acme.reviewer-effect",
@@ -489,6 +489,6 @@ fibers, and registrations are released when the plugin reloads or unloads.
OpenCode does not expose its private Core services to the plugin; use the
capabilities on `ctx`.
Typed tools can use `Schema` from `effect`. Effect and Promise plugins use the
Typed tools can use `Schema` from `@opencode-ai/plugin/effect`. Effect and Promise plugins use the
same `tools.add(name, tool, options?)` registration shape. Effect executors
return an Effect and may fail with the typed tool failure channel.