From 92cd908fb54de951097efea8ad97ee4dc1b97c37 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 19 Mar 2026 21:35:07 -0400 Subject: [PATCH 1/6] feat: add Node.js entry point and build script (#18324) --- packages/opencode/script/build-node.ts | 54 ++++++++++++++++++++++++++ packages/opencode/src/node.ts | 1 + 2 files changed, 55 insertions(+) create mode 100644 packages/opencode/script/build-node.ts create mode 100644 packages/opencode/src/node.ts diff --git a/packages/opencode/script/build-node.ts b/packages/opencode/script/build-node.ts new file mode 100644 index 0000000000..17bc86307a --- /dev/null +++ b/packages/opencode/script/build-node.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env bun + +import fs from "fs" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const dir = path.resolve(__dirname, "..") + +process.chdir(dir) + +// Load migrations from migration directories +const migrationDirs = ( + await fs.promises.readdir(path.join(dir, "migration"), { + withFileTypes: true, + }) +) + .filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name)) + .map((entry) => entry.name) + .sort() + +const migrations = await Promise.all( + migrationDirs.map(async (name) => { + const file = path.join(dir, "migration", name, "migration.sql") + const sql = await Bun.file(file).text() + const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name) + const timestamp = match + ? Date.UTC( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + Number(match[4]), + Number(match[5]), + Number(match[6]), + ) + : 0 + return { sql, timestamp, name } + }), +) +console.log(`Loaded ${migrations.length} migrations`) + +await Bun.build({ + target: "node", + entrypoints: ["./src/node.ts"], + outdir: "./dist", + format: "esm", + external: ["jsonc-parser"], + define: { + OPENCODE_MIGRATIONS: JSON.stringify(migrations), + }, +}) + +console.log("Build complete") diff --git a/packages/opencode/src/node.ts b/packages/opencode/src/node.ts new file mode 100644 index 0000000000..b0e653d6cc --- /dev/null +++ b/packages/opencode/src/node.ts @@ -0,0 +1 @@ +export { Server } from "./server/server" From 949191ab74424dc0da8d2c2c180b6c192e8132d9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 20 Mar 2026 01:36:22 +0000 Subject: [PATCH 2/6] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index c03eeddcd2..d1c1fcecf0 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-yfA50QKqylmaioxi+6d++W8Xv4Wix1hl3hEF6Zz7Ue0=", - "aarch64-linux": "sha256-b5sO7V+/zzJClHHKjkSz+9AUBYC8cb7S3m5ab1kpAyk=", - "aarch64-darwin": "sha256-V66nmRX6kAjrc41ARVeuTElWK7KD8qG/DVk9K7Fu+J8=", - "x86_64-darwin": "sha256-cFyh60WESiqZ5XWZi1+g3F/beSDL1+UPG8KhRivhK8w=" + "x86_64-linux": "sha256-Z5RYwq5ambJ5K5O6Iqfw+xQTR8U64pmtq/nedxrvvkU=", + "aarch64-linux": "sha256-nk6+PAAh2vy+0fSrWIMKwi/2+3YCVeDTved+2GZxksk=", + "aarch64-darwin": "sha256-3n3GMmrhn24d7lXnUNByZSXvfwcn+kDZfyRcldQiXlk=", + "x86_64-darwin": "sha256-vtxQdSC3VVXt/d3TVCIpZ7WDSRquwd/yPTQqapJWeT4=" } } From b3d0446d13504f63c6c26dfd040779a3ccd056cc Mon Sep 17 00:00:00 2001 From: Jaaneek <25470423+Jaaneek@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:09:49 +0000 Subject: [PATCH 3/6] feat: switch xai provider to responses API (#18175) Co-authored-by: Jaaneek --- bun.lock | 1 + package.json | 3 +- packages/opencode/src/provider/provider.ts | 9 ++ patches/@ai-sdk%2Fxai@2.0.51.patch | 108 +++++++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 patches/@ai-sdk%2Fxai@2.0.51.patch diff --git a/bun.lock b/bun.lock index 91c007da30..6d82378458 100644 --- a/bun.lock +++ b/bun.lock @@ -586,6 +586,7 @@ ], "patchedDependencies": { "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", + "@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", }, "overrides": { diff --git a/package.json b/package.json index 4d89daffd7..2875b9daa2 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ }, "patchedDependencies": { "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch" + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", + "@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.patch" } } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index f7667fc2cb..9c9c8e8343 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -184,6 +184,15 @@ export namespace Provider { options: {}, } }, + xai: async () => { + return { + autoload: false, + async getModel(sdk: any, modelID: string, _options?: Record) { + return sdk.responses(modelID) + }, + options: {}, + } + }, "github-copilot": async () => { return { autoload: false, diff --git a/patches/@ai-sdk%2Fxai@2.0.51.patch b/patches/@ai-sdk%2Fxai@2.0.51.patch new file mode 100644 index 0000000000..8776cab483 --- /dev/null +++ b/patches/@ai-sdk%2Fxai@2.0.51.patch @@ -0,0 +1,108 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -959,7 +959,7 @@ + model: z4.string().nullish(), + object: z4.literal("response"), + output: z4.array(outputItemSchema), +- usage: xaiResponsesUsageSchema, ++ usage: xaiResponsesUsageSchema.nullish(), + status: z4.string() + }); + var xaiResponsesChunkSchema = z4.union([ +\ No newline at end of file +@@ -1143,6 +1143,18 @@ + z4.object({ + type: z4.literal("response.completed"), + response: xaiResponsesResponseSchema ++ }), ++ z4.object({ ++ type: z4.literal("response.function_call_arguments.delta"), ++ item_id: z4.string(), ++ output_index: z4.number(), ++ delta: z4.string() ++ }), ++ z4.object({ ++ type: z4.literal("response.function_call_arguments.done"), ++ item_id: z4.string(), ++ output_index: z4.number(), ++ arguments: z4.string() + }) + ]); + +\ No newline at end of file +@@ -1940,6 +1952,9 @@ + if (response2.status) { + finishReason = mapXaiResponsesFinishReason(response2.status); + } ++ if (seenToolCalls.size > 0 && finishReason !== "tool-calls") { ++ finishReason = "tool-calls"; ++ } + return; + } + if (event.type === "response.output_item.added" || event.type === "response.output_item.done") { +\ No newline at end of file +@@ -2024,7 +2039,7 @@ + } + } + } else if (part.type === "function_call") { +- if (!seenToolCalls.has(part.call_id)) { ++ if (event.type === "response.output_item.done" && !seenToolCalls.has(part.call_id)) { + seenToolCalls.add(part.call_id); + controller.enqueue({ + type: "tool-input-start", +\ No newline at end of file +diff --git a/dist/index.js b/dist/index.js +--- a/dist/index.js ++++ b/dist/index.js +@@ -964,7 +964,7 @@ + model: import_v44.z.string().nullish(), + object: import_v44.z.literal("response"), + output: import_v44.z.array(outputItemSchema), +- usage: xaiResponsesUsageSchema, ++ usage: xaiResponsesUsageSchema.nullish(), + status: import_v44.z.string() + }); + var xaiResponsesChunkSchema = import_v44.z.union([ +\ No newline at end of file +@@ -1148,6 +1148,18 @@ + import_v44.z.object({ + type: import_v44.z.literal("response.completed"), + response: xaiResponsesResponseSchema ++ }), ++ import_v44.z.object({ ++ type: import_v44.z.literal("response.function_call_arguments.delta"), ++ item_id: import_v44.z.string(), ++ output_index: import_v44.z.number(), ++ delta: import_v44.z.string() ++ }), ++ import_v44.z.object({ ++ type: import_v44.z.literal("response.function_call_arguments.done"), ++ item_id: import_v44.z.string(), ++ output_index: import_v44.z.number(), ++ arguments: import_v44.z.string() + }) + ]); + +\ No newline at end of file +@@ -1935,6 +1947,9 @@ + if (response2.status) { + finishReason = mapXaiResponsesFinishReason(response2.status); + } ++ if (seenToolCalls.size > 0 && finishReason !== "tool-calls") { ++ finishReason = "tool-calls"; ++ } + return; + } + if (event.type === "response.output_item.added" || event.type === "response.output_item.done") { +\ No newline at end of file +@@ -2019,7 +2034,7 @@ + } + } + } else if (part.type === "function_call") { +- if (!seenToolCalls.has(part.call_id)) { ++ if (event.type === "response.output_item.done" && !seenToolCalls.has(part.call_id)) { + seenToolCalls.add(part.call_id); + controller.enqueue({ + type: "tool-input-start", +\ No newline at end of file From 1071aca91fa69044f281c1e54107dfde9dce7c75 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 19 Mar 2026 22:20:29 -0400 Subject: [PATCH 4/6] fix: miscellaneous small fixes (#18328) --- packages/opencode/src/server/routes/project.ts | 2 +- packages/opencode/src/session/message-v2.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/util/process.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/server/routes/project.ts b/packages/opencode/src/server/routes/project.ts index 994d58b0ca..6cd51ac958 100644 --- a/packages/opencode/src/server/routes/project.ts +++ b/packages/opencode/src/server/routes/project.ts @@ -29,7 +29,7 @@ export const ProjectRoutes = lazy(() => }, }), async (c) => { - const projects = await Project.list() + const projects = Project.list() return c.json(projects) }, ) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 41e2d4efc7..f1335f6f21 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -13,7 +13,7 @@ import { STATUS_CODES } from "http" import { Storage } from "@/storage/storage" import { ProviderError } from "@/provider/error" import { iife } from "@/util/iife" -import { type SystemError } from "bun" +import type { SystemError } from "bun" import type { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "@/provider/schema" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index da9a897905..6d648a097a 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -46,7 +46,7 @@ export namespace ToolRegistry { if (matches.length) await Config.waitForDependencies() for (const match of matches) { const namespace = path.basename(match, path.extname(match)) - const mod = await import(pathToFileURL(match).href) + const mod = await import(process.platform === "win32" ? match : pathToFileURL(match).href) for (const [id, def] of Object.entries(mod)) { custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def)) } diff --git a/packages/opencode/src/util/process.ts b/packages/opencode/src/util/process.ts index 7e6be0e20d..22dce37cb0 100644 --- a/packages/opencode/src/util/process.ts +++ b/packages/opencode/src/util/process.ts @@ -61,9 +61,9 @@ export namespace Process { const proc = launch(cmd[0], cmd.slice(1), { cwd: opts.cwd, + shell: opts.shell, env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined, stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"], - shell: opts.shell, windowsHide: process.platform === "win32", }) From e71a21e0a8ac721677ffdfd67fc0301a6d0a3716 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 20 Mar 2026 02:21:29 +0000 Subject: [PATCH 5/6] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index d1c1fcecf0..3db81fb5a7 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Z5RYwq5ambJ5K5O6Iqfw+xQTR8U64pmtq/nedxrvvkU=", - "aarch64-linux": "sha256-nk6+PAAh2vy+0fSrWIMKwi/2+3YCVeDTved+2GZxksk=", - "aarch64-darwin": "sha256-3n3GMmrhn24d7lXnUNByZSXvfwcn+kDZfyRcldQiXlk=", - "x86_64-darwin": "sha256-vtxQdSC3VVXt/d3TVCIpZ7WDSRquwd/yPTQqapJWeT4=" + "x86_64-linux": "sha256-xq0W2Ym0AzANLXnLyAL+IUwrFm0MKXwkJVdENowoPyY=", + "aarch64-linux": "sha256-RtpiGZXk+BboD9MjBetq5sInIbH/OPkLVNSFgN/0ehY=", + "aarch64-darwin": "sha256-cX6y262OzqRicH4m0/u1DCsMkpJfzCUOOBFQqtQLvls=", + "x86_64-darwin": "sha256-K4UmRKiEfKkvVeKUB85XjHJ1jf0ZUnjL0dWvx9TD4pI=" } } From 7866dbcfcc36a60d22ad466eddf54c54b21fabe3 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:52:04 +1000 Subject: [PATCH 6/6] fix: avoid truncate permission import cycle (#18292) --- packages/opencode/src/permission/evaluate.ts | 15 +++++++++++++++ packages/opencode/src/permission/index.ts | 9 +++------ packages/opencode/src/tool/truncate-effect.ts | 4 ++-- packages/opencode/test/tool/truncation.test.ts | 10 ++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/src/permission/evaluate.ts diff --git a/packages/opencode/src/permission/evaluate.ts b/packages/opencode/src/permission/evaluate.ts new file mode 100644 index 0000000000..2b0604f4ba --- /dev/null +++ b/packages/opencode/src/permission/evaluate.ts @@ -0,0 +1,15 @@ +import { Wildcard } from "@/util/wildcard" + +type Rule = { + permission: string + pattern: string + action: "allow" | "deny" | "ask" +} + +export function evaluate(permission: string, pattern: string, ...rulesets: Rule[][]): Rule { + const rules = rulesets.flat() + const match = rules.findLast( + (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), + ) + return match ?? { action: "ask", permission, pattern: "*" } +} diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 93a8c49b65..321c5c374e 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -13,6 +13,7 @@ import { Wildcard } from "@/util/wildcard" import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect" import os from "os" import z from "zod" +import { evaluate as evalRule } from "./evaluate" import { PermissionID } from "./schema" export namespace PermissionNext { @@ -125,12 +126,8 @@ export namespace PermissionNext { } export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { - const rules = rulesets.flat() - log.info("evaluate", { permission, pattern, ruleset: rules }) - const match = rules.findLast( - (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), - ) - return match ?? { action: "ask", permission, pattern: "*" } + log.info("evaluate", { permission, pattern, ruleset: rulesets.flat() }) + return evalRule(permission, pattern, ...rulesets) } export class Service extends ServiceMap.Service()("@opencode/PermissionNext") {} diff --git a/packages/opencode/src/tool/truncate-effect.ts b/packages/opencode/src/tool/truncate-effect.ts index 4431c18f83..a263cd2943 100644 --- a/packages/opencode/src/tool/truncate-effect.ts +++ b/packages/opencode/src/tool/truncate-effect.ts @@ -3,7 +3,7 @@ import { Cause, Duration, Effect, Layer, Schedule, ServiceMap } from "effect" import path from "path" import type { Agent } from "../agent/agent" import { AppFileSystem } from "@/filesystem" -import { PermissionNext } from "../permission" +import { evaluate } from "@/permission/evaluate" import { Identifier } from "../id/id" import { Log } from "../util/log" import { ToolID } from "./schema" @@ -28,7 +28,7 @@ export namespace TruncateEffect { function hasTaskTool(agent?: Agent.Info) { if (!agent?.permission) return false - return PermissionNext.evaluate("task", "*", agent.permission).action !== "deny" + return evaluate("task", "*", agent.permission).action !== "deny" } export interface Interface { diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index 71439f7604..a00e07e692 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -4,12 +4,14 @@ import { Effect, FileSystem, Layer } from "effect" import { Truncate } from "../../src/tool/truncate" import { TruncateEffect } from "../../src/tool/truncate-effect" import { Identifier } from "../../src/id/id" +import { Process } from "../../src/util/process" import { Filesystem } from "../../src/util/filesystem" import path from "path" import { testEffect } from "../lib/effect" import { writeFileStringScoped } from "../lib/filesystem" const FIXTURES_DIR = path.join(import.meta.dir, "fixtures") +const ROOT = path.resolve(import.meta.dir, "..", "..") describe("Truncate", () => { describe("output", () => { @@ -125,6 +127,14 @@ describe("Truncate", () => { if (result.truncated) throw new Error("expected not truncated") expect("outputPath" in result).toBe(false) }) + + test("loads truncate effect in a fresh process", async () => { + const out = await Process.run([process.execPath, "run", path.join(ROOT, "src", "tool", "truncate-effect.ts")], { + cwd: ROOT, + }) + + expect(out.code).toBe(0) + }, 20000) }) describe("cleanup", () => {