diff --git a/.opencode/command/spellcheck.md b/.opencode/command/spellcheck.md new file mode 100644 index 000000000..afa1970b7 --- /dev/null +++ b/.opencode/command/spellcheck.md @@ -0,0 +1,5 @@ +--- +description: Spellcheck all markdown file changes +--- + +Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors. diff --git a/STATS.md b/STATS.md index b1c90d402..f35acc6cd 100644 --- a/STATS.md +++ b/STATS.md @@ -103,3 +103,4 @@ | 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) | | 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) | | 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) | +| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) | diff --git a/bun.lock b/bun.lock index 980d70b55..8a9aebdab 100644 --- a/bun.lock +++ b/bun.lock @@ -154,8 +154,12 @@ "@openauthjs/openauth": "catalog:", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/sdk": "workspace:*", +<<<<<<< HEAD "@opentui/core": "0.1.26", "@opentui/solid": "0.1.26", +======= + "@parcel/watcher": "2.5.1", +>>>>>>> dev "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", @@ -190,6 +194,7 @@ "@ai-sdk/amazon-bedrock": "2.2.10", "@ai-sdk/google-vertex": "3.0.16", "@octokit/webhooks-types": "7.6.1", + "@parcel/watcher-win32-x64": "2.5.1", "@standard-schema/spec": "1.0.0", "@tsconfig/bun": "1.0.7", "@types/bun": "catalog:", @@ -248,7 +253,7 @@ "sharp": "0.32.5", "shiki": "3.4.2", "solid-js": "catalog:", - "toolbeam-docs-theme": "0.4.7", + "toolbeam-docs-theme": "0.4.8", }, "devDependencies": { "@types/node": "catalog:", @@ -2958,9 +2963,13 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], +<<<<<<< HEAD "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.7", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-oVA/V4M4s4vtLljfnZrOSuCNomek5h9jIYkr92l4QgAQvB3ht+D7xAJIy27IGVJzYA5scUE1OK84ZZqeajoeWw=="], +======= + "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="], +>>>>>>> dev "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 635741d7f..434227c65 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -21,6 +21,7 @@ "@ai-sdk/amazon-bedrock": "2.2.10", "@ai-sdk/google-vertex": "3.0.16", "@octokit/webhooks-types": "7.6.1", + "@parcel/watcher-win32-x64": "2.5.1", "@standard-schema/spec": "1.0.0", "@tsconfig/bun": "1.0.7", "@types/bun": "catalog:", @@ -41,6 +42,7 @@ "@opencode-ai/sdk": "workspace:*", "@opentui/core": "0.1.26", "@opentui/solid": "0.1.26", + "@parcel/watcher": "2.5.1", "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 4175d40dd..c7241bb3b 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -1,7 +1,11 @@ #!/usr/bin/env bun +<<<<<<< HEAD import solidPlugin from "../../../node_modules/@opentui/solid/scripts/solid-plugin" +======= +import path from "path" +>>>>>>> dev const dir = new URL("..", import.meta.url).pathname process.chdir(dir) import { $ } from "bun" @@ -41,6 +45,11 @@ for (const [os, arch] of targets) { await $`npm pack npm pack ${opentui}`.cwd(path.join(dir, "../../node_modules")).quiet() await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1` + const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}` + await $`mkdir -p ../../node_modules/${watcher}` + await $`npm pack npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet() + await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1` + await Bun.build({ conditions: ["browser"], tsconfig: "./tsconfig.json", @@ -54,7 +63,6 @@ for (const [os, arch] of targets) { entrypoints: ["./src/index.ts", path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")], define: { OPENCODE_VERSION: `'${version}'`, - OPENCODE_TUI_PATH: `'../../../dist/${name}/bin/tui'`, OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/../../node_modules/@opentui/core/parser.worker.js", }, }) diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts deleted file mode 100644 index 119a8c789..000000000 --- a/packages/opencode/src/cli/cmd/tui.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { Global } from "../../global" -import { Provider } from "../../provider/provider" -import { Server } from "../../server/server" -import { UI } from "../ui" -import { cmd } from "./cmd" -import path from "path" -import fs from "fs/promises" -import { Installation } from "../../installation" -import { Config } from "../../config/config" -import { Bus } from "../../bus" -import { Log } from "../../util/log" -import { Ide } from "../../ide" - -import { Flag } from "../../flag/flag" -import { Session } from "../../session" -import { $ } from "bun" -import { bootstrap } from "../bootstrap" - -declare global { - const OPENCODE_TUI_PATH: string -} - -if (typeof OPENCODE_TUI_PATH !== "undefined") { - await import(OPENCODE_TUI_PATH as string, { - with: { type: "file" }, - }) -} - -export const TuiCommand = cmd({ - command: "$0 [project]", - describe: "start opencode tui", - builder: (yargs) => - yargs - .positional("project", { - type: "string", - describe: "path to start opencode in", - }) - .option("model", { - type: "string", - alias: ["m"], - describe: "model to use in the format of provider/model", - }) - .option("continue", { - alias: ["c"], - describe: "continue the last session", - type: "boolean", - }) - .option("session", { - alias: ["s"], - describe: "session id to continue", - type: "string", - }) - .option("prompt", { - alias: ["p"], - type: "string", - describe: "prompt to use", - }) - .option("agent", { - type: "string", - describe: "agent to use", - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - alias: ["h"], - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), - handler: async (args) => { - while (true) { - const cwd = args.project ? path.resolve(args.project) : process.cwd() - try { - process.chdir(cwd) - } catch (e) { - UI.error("Failed to change directory to " + cwd) - return - } - const result = await bootstrap(cwd, async () => { - const sessionID = await (async () => { - if (args.continue) { - const it = Session.list() - try { - for await (const s of it) { - if (s.parentID === undefined) { - return s.id - } - } - return - } finally { - await it.return() - } - } - if (args.session) { - return args.session - } - return undefined - })() - const providers = await Provider.list() - if (Object.keys(providers).length === 0) { - return "needs_provider" - } - - const server = Server.listen({ - port: args.port, - hostname: args.hostname, - }) - - let cmd = [] as string[] - const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File - if (tui) { - let binaryName = tui.name - if (process.platform === "win32" && !binaryName.endsWith(".exe")) { - binaryName += ".exe" - } - const binary = path.join(Global.Path.cache, "tui", binaryName) - const file = Bun.file(binary) - if (!(await file.exists())) { - await Bun.write(file, tui, { mode: 0o755 }) - if (process.platform !== "win32") await fs.chmod(binary, 0o755) - } - cmd = [binary] - } - if (!tui) { - const dir = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url)) - let binaryName = `./dist/tui${process.platform === "win32" ? ".exe" : ""}` - await $`go build -o ${binaryName} ./main.go`.cwd(dir) - cmd = [path.join(dir, binaryName)] - } - Log.Default.info("tui", { - cmd, - }) - const proc = Bun.spawn({ - cmd: [ - ...cmd, - ...(args.model ? ["--model", args.model] : []), - ...(args.prompt ? ["--prompt", args.prompt] : []), - ...(args.agent ? ["--agent", args.agent] : []), - ...(sessionID ? ["--session", sessionID] : []), - ], - cwd, - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - env: { - ...process.env, - CGO_ENABLED: "0", - OPENCODE_SERVER: server.url.toString(), - }, - onExit: () => { - server.stop() - }, - }) - - ;(async () => { - if (Installation.isDev()) return - if (Installation.isSnapshot()) return - const config = await Config.global() - if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return - const latest = await Installation.latest().catch(() => {}) - if (!latest) return - if (Installation.VERSION === latest) return - const method = await Installation.method() - if (method === "unknown") return - await Installation.upgrade(method, latest) - .then(() => Bus.publish(Installation.Event.Updated, { version: latest })) - .catch(() => {}) - })() - ;(async () => { - if (Ide.alreadyInstalled()) return - const ide = Ide.ide() - if (ide === "unknown") return - await Ide.install(ide) - .then(() => Bus.publish(Ide.Event.Installed, { ide })) - .catch(() => {}) - })() - - await proc.exited - server.stop() - - return "done" - }) - if (result === "done") break - if (result === "needs_provider") { - UI.empty() - UI.println(UI.logo(" ")) - const result = await Bun.spawn({ - cmd: [...getOpencodeCommand(), "auth", "login"], - cwd: process.cwd(), - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - }).exited - if (result !== 0) return - UI.empty() - } - } - }, -}) - -/** - * Get the correct command to run opencode CLI - * In development: ["bun", "run", "packages/opencode/src/index.ts"] - * In production: ["/path/to/opencode"] - */ -function getOpencodeCommand(): string[] { - // Check if OPENCODE_BIN_PATH is set (used by shell wrapper scripts) - if (process.env["OPENCODE_BIN_PATH"]) { - return [process.env["OPENCODE_BIN_PATH"]] - } - - const execPath = process.execPath.toLowerCase() - - if (Installation.isDev()) { - // In development, use bun to run the TypeScript entry point - return [execPath, "run", process.argv[1]] - } - - // In production, use the current executable path - return [process.execPath] -} diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 912f2159e..2e1d1428f 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,28 +1,30 @@ +import { sep } from "node:path" + export namespace FileIgnore { - const DEFAULT_PATTERNS = [ - // Dependencies - "**/node_modules/**", - "**/bower_components/**", - "**/.pnpm-store/**", - "**/vendor/**", + const FOLDERS = new Set([ + "node_modules", + "bower_components", + ".pnpm-store", + "vendor", + "dist", + "build", + "out", + ".next", + "target", + "bin", + "obj", + ".git", + ".svn", + ".hg", + ".vscode", + ".idea", + ".turbo", + ".output", + "desktop", + ".sst", + ]) - // Build outputs - "**/dist/**", - "**/build/**", - "**/out/**", - "**/.next/**", - "**/target/**", // Rust - "**/bin/**", - "**/obj/**", // .NET - - // Version control - "**/.git/**", - "**/.svn/**", - "**/.hg/**", - - // IDE/Editor - "**/.vscode/**", - "**/.idea/**", + const FILES = [ "**/*.swp", "**/*.swo", @@ -41,22 +43,31 @@ export namespace FileIgnore { "**/.nyc_output/**", ] - const GLOBS = DEFAULT_PATTERNS.map((p) => new Bun.Glob(p)) + const FILE_GLOBS = FILES.map((p) => new Bun.Glob(p)) + + export const PATTERNS = [...FILES, ...FOLDERS] export function match( filepath: string, - opts: { + opts?: { extra?: Bun.Glob[] whitelist?: Bun.Glob[] }, ) { - for (const glob of opts.whitelist || []) { + for (const glob of opts?.whitelist || []) { if (glob.match(filepath)) return false } - const extra = opts.extra || [] - for (const glob of [...GLOBS, ...extra]) { + + const parts = filepath.split(sep) + for (let i = 0; i < parts.length; i++) { + if (FOLDERS.has(parts[i])) return true + } + + const extra = opts?.extra || [] + for (const glob of [...FILE_GLOBS, ...extra]) { if (glob.match(filepath)) return true } + return false } } diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 652cb7853..1bae71cfb 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -1,11 +1,13 @@ import z from "zod/v4" import { Bus } from "../bus" -import chokidar from "chokidar" import { Flag } from "../flag/flag" import { Instance } from "../project/instance" import { Log } from "../util/log" import { FileIgnore } from "./ignore" import { Config } from "../config/config" +// @ts-ignore +import { createWrapper } from "@parcel/watcher/wrapper" +import { lazy } from "@/util/lazy" export namespace FileWatcher { const log = Log.create({ service: "file.watcher" }) @@ -20,37 +22,48 @@ export namespace FileWatcher { ), } + const watcher = lazy(() => { + const binding = require( + `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? "-glibc" : ""}`, + ) + return createWrapper(binding) as typeof import("@parcel/watcher") + }) + const state = Instance.state( async () => { if (Instance.project.vcs !== "git") return {} log.info("init") const cfg = await Config.get() - const ignore = (cfg.watcher?.ignore ?? []).map((v) => new Bun.Glob(v)) - const watcher = chokidar.watch(Instance.directory, { - ignoreInitial: true, - ignored: (filepath) => { - return FileIgnore.match(filepath, { - whitelist: [new Bun.Glob("**/.git/{index,logs/HEAD}")], - extra: ignore, - }) + const backend = (() => { + if (process.platform === "win32") return "windows" + if (process.platform === "darwin") return "fs-events" + if (process.platform === "linux") return "inotify" + })() + if (!backend) { + log.error("watcher backend not supported", { platform: process.platform }) + return {} + } + log.info("watcher backend", { platform: process.platform, backend }) + const sub = await watcher().subscribe( + Instance.directory, + (err, evts) => { + if (err) return + for (const evt of evts) { + log.info("event", evt) + if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) + if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" }) + if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) + } }, - }) - watcher.on("change", (file) => { - Bus.publish(Event.Updated, { file, event: "change" }) - }) - watcher.on("add", (file) => { - Bus.publish(Event.Updated, { file, event: "add" }) - }) - watcher.on("unlink", (file) => { - Bus.publish(Event.Updated, { file, event: "unlink" }) - }) - watcher.on("ready", () => { - log.info("ready") - }) - return { watcher } + { + ignore: [...FileIgnore.PATTERNS, ...(cfg.watcher?.ignore ?? [])], + backend, + }, + ) + return { sub } }, async (state) => { - state.watcher?.close() + state.sub?.unsubscribe() }, ) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 501279dd4..0437c4c69 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -12,6 +12,7 @@ export namespace Flag { // Experimental export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER") + export const OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP = truthy("OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP") function truthy(key: string) { const value = process.env[key]?.toLowerCase() diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index b52573137..01ffd1c00 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -12,13 +12,13 @@ import { Installation } from "./installation" import { NamedError } from "./util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" -import { TuiCommand } from "./cli/cmd/tui/tui" import { AttachCommand } from "./cli/cmd/tui/attach" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" import { GithubCommand } from "./cli/cmd/github" import { ExportCommand } from "./cli/cmd/export" +import { TuiCommand } from "./cli/cmd/tui/tui" const cancel = new AbortController() diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index a3037528f..b636cb3ea 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -5,8 +5,10 @@ import { LSP } from "../lsp" import { Snapshot } from "../snapshot" import { FileWatcher } from "../file/watcher" import { File } from "../file" +import { Flag } from "../flag/flag" export async function InstanceBootstrap() { + if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return await Plugin.init() Share.init() Format.init() diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 514203e91..97310dd19 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -28,6 +28,12 @@ export namespace ModelsDev { context: z.number(), output: z.number(), }), + modalities: z + .object({ + input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + }) + .optional(), experimental: z.boolean().optional(), options: z.record(z.string(), z.any()), provider: z.object({ npm: z.string() }).optional(), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c18bc4898..e0fe4be23 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -279,6 +279,11 @@ export namespace Provider { context: 0, output: 0, }, + modalities: model.modalities ?? + existing?.modalities ?? { + input: ["text"], + output: ["text"], + }, provider: model.provider ?? existing?.provider, } parsed.models[modelID] = parsedModel diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 538200567..8dc059ca1 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -17,73 +17,6 @@ export namespace MessageV2 { }), ) - export const ToolStatePending = z - .object({ - status: z.literal("pending"), - raw: z.string(), - input: z.record(z.string(), z.any()), - }) - .meta({ - ref: "ToolStatePending", - }) - - export type ToolStatePending = z.infer - - export const ToolStateRunning = z - .object({ - status: z.literal("running"), - input: z.record(z.string(), z.any()), - title: z.string().optional(), - metadata: z.record(z.string(), z.any()).optional(), - time: z.object({ - start: z.number(), - }), - }) - .meta({ - ref: "ToolStateRunning", - }) - export type ToolStateRunning = z.infer - - export const ToolStateCompleted = z - .object({ - status: z.literal("completed"), - input: z.record(z.string(), z.any()), - output: z.string(), - title: z.string(), - metadata: z.record(z.string(), z.any()), - time: z.object({ - start: z.number(), - end: z.number(), - compacted: z.number().optional(), - }), - }) - .meta({ - ref: "ToolStateCompleted", - }) - export type ToolStateCompleted = z.infer - - export const ToolStateError = z - .object({ - status: z.literal("error"), - input: z.record(z.string(), z.any()), - error: z.string(), - metadata: z.record(z.string(), z.any()).optional(), - time: z.object({ - start: z.number(), - end: z.number(), - }), - }) - .meta({ - ref: "ToolStateError", - }) - export type ToolStateError = z.infer - - export const ToolState = z - .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) - .meta({ - ref: "ToolState", - }) - const PartBase = z.object({ id: z.string(), sessionID: z.string(), @@ -136,17 +69,6 @@ export namespace MessageV2 { }) export type ReasoningPart = z.infer - export const ToolPart = PartBase.extend({ - type: z.literal("tool"), - callID: z.string(), - tool: z.string(), - state: ToolState, - metadata: z.record(z.string(), z.any()).optional(), - }).meta({ - ref: "ToolPart", - }) - export type ToolPart = z.infer - const FilePartSourceBase = z.object({ text: z .object({ @@ -230,6 +152,83 @@ export namespace MessageV2 { }) export type StepFinishPart = z.infer + export const ToolStatePending = z + .object({ + status: z.literal("pending"), + }) + .meta({ + ref: "ToolStatePending", + }) + + export type ToolStatePending = z.infer + + export const ToolStateRunning = z + .object({ + status: z.literal("running"), + input: z.any(), + title: z.string().optional(), + metadata: z.record(z.string(), z.any()).optional(), + time: z.object({ + start: z.number(), + }), + }) + .meta({ + ref: "ToolStateRunning", + }) + export type ToolStateRunning = z.infer + + export const ToolStateCompleted = z + .object({ + status: z.literal("completed"), + input: z.record(z.string(), z.any()), + output: z.string(), + title: z.string(), + metadata: z.record(z.string(), z.any()), + time: z.object({ + start: z.number(), + end: z.number(), + compacted: z.number().optional(), + }), + attachments: FilePart.array().optional(), + }) + .meta({ + ref: "ToolStateCompleted", + }) + export type ToolStateCompleted = z.infer + + export const ToolStateError = z + .object({ + status: z.literal("error"), + input: z.record(z.string(), z.any()), + error: z.string(), + metadata: z.record(z.string(), z.any()).optional(), + time: z.object({ + start: z.number(), + end: z.number(), + }), + }) + .meta({ + ref: "ToolStateError", + }) + export type ToolStateError = z.infer + + export const ToolState = z + .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) + .meta({ + ref: "ToolState", + }) + + export const ToolPart = PartBase.extend({ + type: z.literal("tool"), + callID: z.string(), + tool: z.string(), + state: ToolState, + metadata: z.record(z.string(), z.any()).optional(), + }).meta({ + ref: "ToolPart", + }) + export type ToolPart = z.infer + const Base = z.object({ id: z.string(), sessionID: z.string(), @@ -284,7 +283,6 @@ export namespace MessageV2 { cwd: z.string(), root: z.string(), }), - finish: z.string().optional(), summary: z.boolean().optional(), cost: z.number(), tokens: z.object({ @@ -396,8 +394,6 @@ export namespace MessageV2 { if (part.toolInvocation.state === "partial-call") { return { status: "pending", - input: {}, - raw: "", } } @@ -536,7 +532,25 @@ export namespace MessageV2 { }, ] if (part.type === "tool") { - if (part.state.status === "completed") + if (part.state.status === "completed") { + if (part.state.attachments?.length) { + result.push({ + id: Identifier.ascending("message"), + role: "user", + parts: [ + { + type: "text", + text: `Tool ${part.tool} returned an attachment:`, + }, + ...part.state.attachments.map((attachment) => ({ + type: "file" as const, + url: attachment.url, + mediaType: attachment.mime, + filename: attachment.filename, + })), + ], + }) + } return [ { type: ("tool-" + part.tool) as `tool-${string}`, @@ -547,6 +561,7 @@ export namespace MessageV2 { callProviderMetadata: part.metadata, }, ] + } if (part.state.status === "error") return [ { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 36d9f6274..4a6ff8ee9 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -457,6 +457,10 @@ export namespace SessionPrompt { abort: options.abortSignal!, messageID: input.processor.message.id, callID: options.toolCallId, + extra: { + modelID: input.modelID, + providerID: input.providerID, + }, agent: input.agent.name, metadata: async (val) => { const match = input.processor.partFromToolCall(options.toolCallId) @@ -991,6 +995,7 @@ export namespace SessionPrompt { start: match.state.time.start, end: Date.now(), }, + attachments: value.output.attachments, }, }) delete toolcalls[value.toolCallId] diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 2ed3accbd..5e8cecaf2 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -7,6 +7,8 @@ import { FileTime } from "../file/time" import DESCRIPTION from "./read.txt" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" +import { Provider } from "../provider/provider" +import { Identifier } from "../id/id" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -23,6 +25,8 @@ export const ReadTool = Tool.define("read", { if (!path.isAbsolute(filepath)) { filepath = path.join(process.cwd(), filepath) } + const title = path.relative(Instance.worktree, filepath) + if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) { throw new Error(`File ${filepath} is not in the current working directory`) } @@ -48,12 +52,45 @@ export const ReadTool = Tool.define("read", { throw new Error(`File not found: ${filepath}`) } - const limit = params.limit ?? DEFAULT_READ_LIMIT - const offset = params.offset || 0 const isImage = isImageFile(filepath) - if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`) + const supportsImages = await (async () => { + if (!ctx.extra?.["providerID"] || !ctx.extra?.["modelID"]) return false + const providerID = ctx.extra["providerID"] as string + const modelID = ctx.extra["modelID"] as string + const model = await Provider.getModel(providerID, modelID).catch(() => undefined) + if (!model) return false + return model.info.modalities?.input?.includes("image") ?? false + })() + if (isImage) { + if (!supportsImages) { + throw new Error(`Failed to read image: ${filepath}, model may not be able to read images`) + } + const mime = file.type + const msg = "Image read successfully" + return { + title, + output: msg, + metadata: { + preview: msg, + }, + attachments: [ + { + id: Identifier.ascending("part"), + sessionID: ctx.sessionID, + messageID: ctx.messageID, + type: "file", + mime, + url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`, + }, + ], + } + } + const isBinary = await isBinaryFile(filepath, file) if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`) + + const limit = params.limit ?? DEFAULT_READ_LIMIT + const offset = params.offset || 0 const lines = await file.text().then((text) => text.split("\n")) const raw = lines.slice(offset, offset + limit).map((line) => { return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line @@ -76,7 +113,7 @@ export const ReadTool = Tool.define("read", { FileTime.read(ctx.sessionID, filepath) return { - title: path.relative(Instance.worktree, filepath), + title, output, metadata: { preview, diff --git a/packages/opencode/src/tool/read.txt b/packages/opencode/src/tool/read.txt index 3904c0939..b5bffee26 100644 --- a/packages/opencode/src/tool/read.txt +++ b/packages/opencode/src/tool/read.txt @@ -7,6 +7,6 @@ Usage: - You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters - Any lines longer than 2000 characters will be truncated - Results are returned using cat -n format, with line numbers starting at 1 -- This tool cannot read binary files, including images -- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. +- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents. +- You can read image files using this tool. diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 2a0c9eef1..e978e39d1 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -1,9 +1,11 @@ import z from "zod/v4" +import type { MessageV2 } from "../session/message-v2" export namespace Tool { interface Metadata { [key: string]: any } + export type Context = { sessionID: string messageID: string @@ -25,6 +27,7 @@ export namespace Tool { title: string metadata: M output: string + attachments?: MessageV2.FilePart[] }> }> } diff --git a/packages/opencode/test/file/ignore.test.ts b/packages/opencode/test/file/ignore.test.ts new file mode 100644 index 000000000..6387ff63e --- /dev/null +++ b/packages/opencode/test/file/ignore.test.ts @@ -0,0 +1,10 @@ +import { test, expect } from "bun:test" +import { FileIgnore } from "../../src/file/ignore" + +test("match nested and non-nested", () => { + expect(FileIgnore.match("node_modules/index.js")).toBe(true) + expect(FileIgnore.match("node_modules")).toBe(true) + expect(FileIgnore.match("node_modules/")).toBe(true) + expect(FileIgnore.match("node_modules/bar")).toBe(true) + expect(FileIgnore.match("node_modules/bar/")).toBe(true) +}) diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index fb7bd75b5..3bcd98cb7 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -74,6 +74,7 @@ export default defineConfig({ { label: "Configure", items: [ + "tools", "rules", "agents", "models", diff --git a/packages/web/package.json b/packages/web/package.json index b01a51e2a..e97855c65 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -31,7 +31,7 @@ "sharp": "0.32.5", "shiki": "3.4.2", "solid-js": "catalog:", - "toolbeam-docs-theme": "0.4.7" + "toolbeam-docs-theme": "0.4.8" }, "devDependencies": { "opencode": "workspace:*", diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 7016d6576..e15ba6cb1 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -303,27 +303,33 @@ Use the `model` config to override the default model for this agent. Useful for Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`. -```json title="opencode.json" +```json title="opencode.json" {3-6,9-12} { + "$schema": "https://opencode.ai/config.json", + "tools": { + "write": true, + "bash": true + }, "agent": { - "readonly": { + "plan": { "tools": { "write": false, - "edit": false, - "bash": false, - "read": true, - "grep": true, - "glob": true + "bash": false } } } } ``` +:::note +The agent-specific config overrides the global config. +::: + You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server: ```json title="opencode.json" { + "$schema": "https://opencode.ai/config.json", "agent": { "readonly": { "tools": { @@ -336,27 +342,7 @@ You can also use wildcards to control multiple tools at once. For example, to di } ``` -If no tools are specified, all tools are enabled by default. - ---- - -#### Available tools - -Here are all the tools can be controlled through the agent config. - -| Tool | Description | -| ----------- | ----------------------- | -| `bash` | Execute shell commands | -| `edit` | Modify existing files | -| `write` | Create new files | -| `read` | Read file contents | -| `grep` | Search file contents | -| `glob` | Find files by pattern | -| `list` | List directory contents | -| `patch` | Apply patches to files | -| `todowrite` | Manage todo lists | -| `todoread` | Read todo lists | -| `webfetch` | Fetch web content | +[Learn more about tools](/docs/tools). --- diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index d996cb94b..f3b2a05a0 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -86,6 +86,24 @@ You can configure TUI-specific settings through the `tui` option. --- +### Tools + +You can manage the tools an LLM can use through the `tools` option. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "write": false, + "bash": false + } +} +``` + +[Learn more about tools here](/docs/tools). + +--- + ### Models You can configure the providers and models you want to use in your OpenCode config through the `provider`, `model` and `small_model` options. diff --git a/packages/web/src/content/docs/custom-tools.mdx b/packages/web/src/content/docs/custom-tools.mdx index d982475ef..3f4cf06bc 100644 --- a/packages/web/src/content/docs/custom-tools.mdx +++ b/packages/web/src/content/docs/custom-tools.mdx @@ -3,7 +3,7 @@ title: Custom Tools description: Create tools the LLM can call in opencode. --- -Custom tools are functions you create that the LLM can call during conversations. They work alongside opencode's built-in tools like `read`, `write`, and `bash`. +Custom tools are functions you create that the LLM can call during conversations. They work alongside opencode's [built-in tools](/docs/tools) like `read`, `write`, and `bash`. --- diff --git a/packages/web/src/content/docs/mcp-servers.mdx b/packages/web/src/content/docs/mcp-servers.mdx index 0ceeb47a3..535e22839 100644 --- a/packages/web/src/content/docs/mcp-servers.mdx +++ b/packages/web/src/content/docs/mcp-servers.mdx @@ -3,10 +3,10 @@ title: MCP servers description: Add local and remote MCP tools. --- -You can add external tools to opencode using the _Model Context Protocol_, or MCP. opencode supports both: +You can add external tools to OpenCode using the _Model Context Protocol_, or MCP. OpenCode supports both: - Local servers -- And remote servers +- Remote servers Once added, MCP tools are automatically available to the LLM alongside built-in tools. @@ -14,15 +14,22 @@ Once added, MCP tools are automatically available to the LLM alongside built-in ## Configure -You can define MCP servers in your opencode config under `mcp`. +You can define MCP servers in your OpenCode config under `mcp`. --- ### Local -Add local MCP servers using `"type": "local"` within the MCP object. Multiple MCP servers can be added. The key string for each server can be any arbitrary name. +Add local MCP servers using `"type": "local"` within the MCP object. Multiple MCP servers can be added. -```json title="opencode.json" +:::tip +MCP servers add to your context, so you want to be careful with which +ones you enable. +::: + +The key string for each server can be any arbitrary name. + +```json title="opencode.json" {15} { "$schema": "https://opencode.ai/config.json", "mcp": { @@ -95,16 +102,70 @@ Local and remote servers can be used together within the same `mcp` config objec --- -## Per agent +## Manage + +Your MCPs are available as tools in OpenCode, alongside built-in tools. So you +can manage them through the OpenCode config like any other tool. + +--- + +### Global + +This means that you can enable or disable them globally. + +```json title="opencode.json" {14} +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "my-mcp-foo": { + "type": "local", + "command": ["bun", "x", "my-mcp-command-foo"] + }, + "my-mcp-bar": { + "type": "local", + "command": ["bun", "x", "my-mcp-command-bar"] + } + }, + "tools": { + "my-mcp-foo": false + } +} +``` + +We can also use a glob pattern to disable all matching MCPs. + +```json title="opencode.json" {14} +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "my-mcp-foo": { + "type": "local", + "command": ["bun", "x", "my-mcp-command-foo"] + }, + "my-mcp-bar": { + "type": "local", + "command": ["bun", "x", "my-mcp-command-bar"] + } + }, + "tools": { + "my-mcp*": false + } +} +``` + +Here we are using the glob pattern `my-mcp*` to disable all MCPs. + +--- + +### Per agent If you have a large number of MCP servers you may want to only enable them per agent and disable them globally. To do this: -1. Configure the MCP server. -2. Disable it as a tool globally. -3. In your [agent config](/docs/agents#tools) enable the MCP server as a tool. +1. Disable it as a tool globally. +2. In your [agent config](/docs/agents#tools) enable the MCP server as a tool. -```json title="opencode.json" {11, 14-17} +```json title="opencode.json" {11, 14-18} { "$schema": "https://opencode.ai/config.json", "mcp": { @@ -126,3 +187,13 @@ agent and disable them globally. To do this: } } ``` + +--- + +#### Glob patterns + +The glob pattern uses simple regex globbing patterns. + +- `*` matches zero or more of any character +- `?` matches exactly one character +- All other characters match literally diff --git a/packages/web/src/content/docs/plugins.mdx b/packages/web/src/content/docs/plugins.mdx index bfcf1ed87..fe1ae9bfd 100644 --- a/packages/web/src/content/docs/plugins.mdx +++ b/packages/web/src/content/docs/plugins.mdx @@ -111,7 +111,7 @@ export const EnvProtection = async ({ project, client, $, directory, worktree }) Plugins can also add custom tools to opencode: ```ts title=".opencode/plugin/custom-tools.ts" -import type { Plugin, tool } from "@opencode-ai/plugin" +import { type Plugin, tool } from "@opencode-ai/plugin" export const CustomToolsPlugin: Plugin = async (ctx) => { return { diff --git a/packages/web/src/content/docs/tools.mdx b/packages/web/src/content/docs/tools.mdx new file mode 100644 index 000000000..11761d4c8 --- /dev/null +++ b/packages/web/src/content/docs/tools.mdx @@ -0,0 +1,316 @@ +--- +title: Tools +description: Manage the tools an LLM can use. +--- + +Tools allow the LLM to perform actions in your codebase. OpenCode comes with a set of built-in tools, but you can extend it with [custom tools](/docs/custom-tools) or [MCP servers](/docs/mcp-servers). + +By default, all tools are **enabled** and don't need permission to run. But you can configure this and control the [permissions](/docs/permissions) through your config. + +--- + +## Configure + +You can configure tools globally or per agent. Agent-specific configs override global settings. + +By default, all tools are set to `true`. To disable a tool, set it to `false`. + +--- + +### Global + +Disable or enable tools globally using the `tools` option. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "write": false, + "bash": false, + "webfetch": true + } +} +``` + +You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server: + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "mymcp_*": false + } +} +``` + +--- + +### Per agent + +Override global tool settings for specific agents using the `tools` config in the agent definition. + +```json title="opencode.json" {3-6,9-12} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "write": true, + "bash": true + }, + "agent": { + "plan": { + "tools": { + "write": false, + "bash": false + } + } + } +} +``` + +For example, here the `plan` agent overrides the global config to disable `write` and `bash` tools. + +You can also configure tools for agents in Markdown. + +```markdown title="~/.config/opencode/agent/readonly.md" +--- +description: Read-only analysis agent +mode: subagent +tools: + write: false + edit: false + bash: false +--- + +Analyze code without making any modifications. +``` + +[Learn more](/docs/agents#tools) about configuring tools per agent. + +--- + +## Built-in + +Here are all the built-in tools available in OpenCode. + +--- + +### bash + +Execute shell commands in your project environment. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "bash": true + } +} +``` + +This tool allows the LLM to run terminal commands like `npm install`, `git status`, or any other shell command. + +--- + +### edit + +Modify existing files using exact string replacements. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "edit": true + } +} +``` + +This tool performs precise edits to files by replacing exact text matches. It's the primary way the LLM modifies code. + +--- + +### write + +Create new files or overwrite existing ones. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "write": true + } +} +``` + +Use this to allow the LLM to create new files. It will overwrite existing files if they already exist. + +--- + +### read + +Read file contents from your codebase. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "read": true + } +} +``` + +This tool reads files and returns their contents. It supports reading specific line ranges for large files. + +--- + +### grep + +Search file contents using regular expressions. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "grep": true + } +} +``` + +Fast content search across your codebase. Supports full regex syntax and file pattern filtering. + +--- + +### glob + +Find files by pattern matching. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "glob": true + } +} +``` + +Search for files using glob patterns like `**/*.js` or `src/**/*.ts`. Returns matching file paths sorted by modification time. + +--- + +### list + +List files and directories in a given path. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "list": true + } +} +``` + +This tool lists directory contents. It accepts glob patterns to filter results. + +--- + +### patch + +Apply patches to files. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "patch": true + } +} +``` + +This tool applies patch files to your codebase. Useful for applying diffs and patches from various sources. + +--- + +### todowrite + +Manage todo lists during coding sessions. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "todowrite": true + } +} +``` + +Creates and updates task lists to track progress during complex operations. The LLM uses this to organize multi-step tasks. + +--- + +### todoread + +Read existing todo lists. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "todoread": true + } +} +``` + +Reads the current todo list state. Used by the LLM to track what tasks are pending or completed. + +--- + +### webfetch + +Fetch web content. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "tools": { + "webfetch": true + } +} +``` + +Allows the LLM to fetch and read web pages. Useful for looking up documentation or researching online resources. + +--- + +## Custom tools + +Custom tools let you define your own functions that the LLM can call. These are defined in your config file and can execute arbitrary code. + +[Learn more](/docs/custom-tools) about creating custom tools. + +--- + +## MCP servers + +MCP (Model Context Protocol) servers allow you to integrate external tools and services. This includes database access, API integrations, and third-party services. + +[Learn more](/docs/mcp-servers) about configuring MCP servers. + +--- + +## Internals + +Internally, tools like `grep`, `glob`, and `list` use [ripgrep](https://github.com/BurntSushi/ripgrep) under the hood. By default, ripgrep respects `.gitignore` patterns, which means files and directories listed in your `.gitignore` will be excluded from searches and listings. + +--- + +### Ignore patterns + +To include files that would normally be ignored, create a `.ignore` file in your project root. This file can explicitly allow certain paths. + +```text title=".ignore" +!node_modules/ +!dist/ +!build/ +``` + +For example, this `.ignore` file allows ripgrep to search within `node_modules/`, `dist/`, and `build/` directories even if they're listed in `.gitignore`. diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 6bbe34a84..f1c2b3338 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -66,6 +66,7 @@ You can also access our models through the following API endpoints. | ---------------- | ---------------- | --------------------------------------------- | --------------------------- | | GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |