summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-06-04 13:33:25 -0400
committerDax Raad <[email protected]>2025-06-04 13:33:25 -0400
commitfb88705bdcdca62c99f0d55f1510dc0a82f863ef (patch)
treec3bffba5fbb42a8f9da356b33b45bf21122ff036
parent90d85e6393c93c18a173a4e80e3bfa8afc62ab88 (diff)
downloadopencode-fb88705bdcdca62c99f0d55f1510dc0a82f863ef.tar.gz
opencode-fb88705bdcdca62c99f0d55f1510dc0a82f863ef.zip
more tools
-rw-r--r--packages/opencode/src/provider/provider.ts4
-rw-r--r--packages/opencode/src/tool/edit.ts11
-rw-r--r--packages/opencode/src/tool/ls.ts4
-rw-r--r--packages/opencode/src/tool/multiedit.ts37
-rw-r--r--packages/opencode/src/tool/patch.ts13
-rw-r--r--packages/opencode/src/tool/util/file-times.ts14
-rw-r--r--packages/opencode/src/tool/write.ts67
7 files changed, 126 insertions, 24 deletions
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index a979eaff4..af841bbd1 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -18,6 +18,8 @@ import { LspHoverTool } from "../tool/lsp-hover"
import { PatchTool } from "../tool/patch"
import { ReadTool } from "../tool/read"
import type { Tool } from "../tool/tool"
+import { MultiEditTool } from "../tool/multiedit"
+import { WriteTool } from "../tool/write"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -174,6 +176,8 @@ export namespace Provider {
PatchTool,
ReadTool,
EditTool,
+ MultiEditTool,
+ WriteTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"),
diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts
index fb420dcea..7342d6af5 100644
--- a/packages/opencode/src/tool/edit.ts
+++ b/packages/opencode/src/tool/edit.ts
@@ -52,21 +52,12 @@ export const EditTool = Tool.define({
return
}
- const read = FileTimes.get(ctx.sessionID, filepath)
- if (!read)
- throw new Error(
- `You must read the file ${filepath} before editing it. Use the View tool first`,
- )
const file = Bun.file(filepath)
if (!(await file.exists())) throw new Error(`File ${filepath} not found`)
const stats = await file.stat()
if (stats.isDirectory())
throw new Error(`Path is a directory, not a file: ${filepath}`)
- if (stats.mtime.getTime() > read.getTime())
- throw new Error(
- `File ${filepath} has been modified since it was last read.\nLast modification: ${read.toISOString()}\nLast read: ${stats.mtime.toISOString()}\n\nPlease read the file again before modifying it.`,
- )
-
+ await FileTimes.assert(ctx.sessionID, filepath)
contentOld = await file.text()
const index = contentOld.indexOf(params.oldString)
if (index === -1)
diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts
index b96e40cda..4d481a648 100644
--- a/packages/opencode/src/tool/ls.ts
+++ b/packages/opencode/src/tool/ls.ts
@@ -22,8 +22,8 @@ export const ListTool = Tool.define({
id: "opencode.list",
description: DESCRIPTION,
parameters: z.object({
- path: z.string().optional(),
- ignore: z.array(z.string()).optional(),
+ path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
+ ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
}),
async execute(params) {
const app = App.info()
diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts
new file mode 100644
index 000000000..8a3fd43db
--- /dev/null
+++ b/packages/opencode/src/tool/multiedit.ts
@@ -0,0 +1,37 @@
+import { z } from "zod"
+import { Tool } from "./tool"
+import { EditTool } from "./edit"
+import DESCRIPTION from "./multiedit.txt"
+
+export const MultiEditTool = Tool.define({
+ id: "opencode.multiedit",
+ description: DESCRIPTION,
+ parameters: z.object({
+ filePath: z.string().describe("The absolute path to the file to modify"),
+ edits: z
+ .array(EditTool.parameters)
+ .describe("Array of edit operations to perform sequentially on the file"),
+ }),
+ async execute(params, ctx) {
+ const results = []
+ for (const [, edit] of params.edits.entries()) {
+ const result = await EditTool.execute(
+ {
+ filePath: params.filePath,
+ oldString: edit.oldString,
+ newString: edit.newString,
+ replaceAll: edit.replaceAll,
+ },
+ ctx,
+ )
+ results.push(result)
+ }
+
+ return {
+ metadata: {
+ results: results.map((r) => r.metadata),
+ },
+ output: results.at(-1)!.output,
+ }
+ },
+})
diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts
index 326b27565..ae8772806 100644
--- a/packages/opencode/src/tool/patch.ts
+++ b/packages/opencode/src/tool/patch.ts
@@ -254,24 +254,13 @@ export const PatchTool = Tool.define({
absPath = path.resolve(process.cwd(), absPath)
}
- if (!FileTimes.get(ctx.sessionID, absPath)) {
- throw new Error(
- `you must read the file ${filePath} before patching it. Use the FileRead tool first`,
- )
- }
+ await FileTimes.assert(ctx.sessionID, absPath)
try {
const stats = await fs.stat(absPath)
if (stats.isDirectory()) {
throw new Error(`path is a directory, not a file: ${absPath}`)
}
-
- const lastRead = FileTimes.get(ctx.sessionID, absPath)
- if (lastRead && stats.mtime > lastRead) {
- throw new Error(
- `file ${absPath} has been modified since it was last read (mod time: ${stats.mtime.toISOString()}, last read: ${lastRead.toISOString()})`,
- )
- }
} catch (error: any) {
if (error.code === "ENOENT") {
throw new Error(`file not found: ${absPath}`)
diff --git a/packages/opencode/src/tool/util/file-times.ts b/packages/opencode/src/tool/util/file-times.ts
index 19589d549..7eb60aecf 100644
--- a/packages/opencode/src/tool/util/file-times.ts
+++ b/packages/opencode/src/tool/util/file-times.ts
@@ -21,4 +21,18 @@ export namespace FileTimes {
export function get(sessionID: string, file: string) {
return state().read[sessionID]?.[file]
}
+
+ export async function assert(sessionID: string, filepath: string) {
+ const time = get(sessionID, filepath)
+ if (!time)
+ throw new Error(
+ `You must read the file ${filepath} before overwriting it. Use the Read tool first`,
+ )
+ const stats = await Bun.file(filepath).stat()
+ if (stats.mtime.getTime() > time.getTime()) {
+ throw new Error(
+ `File ${filepath} has been modified since it was last read.\nLast modification: ${stats.mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
+ )
+ }
+ }
}
diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts
new file mode 100644
index 000000000..097185924
--- /dev/null
+++ b/packages/opencode/src/tool/write.ts
@@ -0,0 +1,67 @@
+import { z } from "zod"
+import * as path from "path"
+import { Tool } from "./tool"
+import { FileTimes } from "./util/file-times"
+import { LSP } from "../lsp"
+import { Permission } from "../permission"
+import DESCRIPTION from "./write.txt"
+
+export const WriteTool = Tool.define({
+ id: "opencode.write",
+ description: DESCRIPTION,
+ parameters: z.object({
+ filePath: z
+ .string()
+ .describe(
+ "The absolute path to the file to write (must be absolute, not relative)",
+ ),
+ content: z.string().describe("The content to write to the file"),
+ }),
+ async execute(params, ctx) {
+ const filepath = path.isAbsolute(params.filePath)
+ ? params.filePath
+ : path.join(process.cwd(), params.filePath)
+
+ const file = Bun.file(filepath)
+ const exists = await file.exists()
+ if (exists) await FileTimes.assert(ctx.sessionID, filepath)
+
+ await Permission.ask({
+ id: "opencode.write",
+ sessionID: ctx.sessionID,
+ title: exists
+ ? "Overwrite this file: " + filepath
+ : "Create new file: " + filepath,
+ metadata: {
+ filePath: filepath,
+ content: params.content,
+ exists,
+ },
+ })
+
+ await Bun.write(filepath, params.content)
+ FileTimes.read(ctx.sessionID, filepath)
+
+ let output = ""
+ await LSP.file(filepath)
+ const diagnostics = await LSP.diagnostics()
+ for (const [file, issues] of Object.entries(diagnostics)) {
+ if (issues.length === 0) continue
+ if (file === filepath) {
+ output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
+ continue
+ }
+ output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
+ }
+
+ return {
+ metadata: {
+ diagnostics,
+ filepath,
+ exists: exists,
+ },
+ output,
+ }
+ },
+})
+