2025-10-26 19:50:41 +00:00
|
|
|
import z from "zod"
|
2025-06-04 17:33:25 +00:00
|
|
|
import * as path from "path"
|
2026-04-10 20:57:12 +00:00
|
|
|
import { Effect } from "effect"
|
2026-04-16 03:56:54 +00:00
|
|
|
import * as Tool from "./tool"
|
2025-06-04 17:33:25 +00:00
|
|
|
import { LSP } from "../lsp"
|
2026-01-01 22:54:11 +00:00
|
|
|
import { createTwoFilesPatch } from "diff"
|
2025-06-04 17:33:25 +00:00
|
|
|
import DESCRIPTION from "./write.txt"
|
2025-06-27 15:29:20 +00:00
|
|
|
import { Bus } from "../bus"
|
2026-03-21 04:51:35 +00:00
|
|
|
import { File } from "../file"
|
2026-01-26 22:46:01 +00:00
|
|
|
import { FileWatcher } from "../file/watcher"
|
2026-03-26 00:19:24 +00:00
|
|
|
import { Format } from "../format"
|
2026-04-15 14:26:20 +00:00
|
|
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
2025-09-01 21:15:49 +00:00
|
|
|
import { Instance } from "../project/instance"
|
2026-01-01 22:54:11 +00:00
|
|
|
import { trimDiff } from "./edit"
|
2026-04-10 20:57:12 +00:00
|
|
|
import { assertExternalDirectoryEffect } from "./external-directory"
|
2025-06-04 17:33:25 +00:00
|
|
|
|
2025-12-14 01:56:26 +00:00
|
|
|
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
|
|
|
|
|
|
test(tool): pin every tool's parameters schema before migration
Pre-migration safety net for the upcoming tool-by-tool zod\u2192Schema
conversion. Every tool's parameters schema now has:
1. A JSON Schema snapshot (`z.toJSONSchema` with `io: "input"`) \u2014 this
captures exactly what the LLM sees at tool registration time, so any
drift caused by a future migration fails the snapshot.
2. Parse-accept/parse-reject assertions per tool pinning the
user-visible behavioural contract (required fields, refinement
bounds, enum membership, default values).
To make the snapshots possible without standing up each tool's full
Effect runtime, every tool file now exports its parameters schema as
`Parameters` at module scope:
- 9 tools already had a module-level const \u2014 just added `export`, and
standardised the name to `Parameters` (uppercase) where it was
previously `parameters`.
- 9 tools had their schema inline inside `Tool.define` \u2014 hoisted to
module scope under the same `Parameters` name and wired back through.
Zero behaviour change: Tool.define still sees the same schema, runtime
validation path is identical, SDK (types.gen.ts + openapi.json) is
byte-identical, and the full 2054-test suite passes.
18 JSON Schema snapshots and 43 explicit parse/reject assertions for the
18 built-in tools (apply_patch, bash, codesearch, edit, glob, grep,
invalid, lsp, multiedit, plan, question, read, skill, task, todo,
webfetch, websearch, write).
2026-04-18 04:01:50 +00:00
|
|
|
export const Parameters = z.object({
|
|
|
|
|
content: z.string().describe("The content to write to the file"),
|
|
|
|
|
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-11 02:36:02 +00:00
|
|
|
export const WriteTool = Tool.define(
|
2026-04-10 20:57:12 +00:00
|
|
|
"write",
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const lsp = yield* LSP.Service
|
|
|
|
|
const fs = yield* AppFileSystem.Service
|
2026-04-11 02:36:02 +00:00
|
|
|
const bus = yield* Bus.Service
|
|
|
|
|
const format = yield* Format.Service
|
2025-06-04 17:33:25 +00:00
|
|
|
|
2026-04-10 20:57:12 +00:00
|
|
|
return {
|
|
|
|
|
description: DESCRIPTION,
|
test(tool): pin every tool's parameters schema before migration
Pre-migration safety net for the upcoming tool-by-tool zod\u2192Schema
conversion. Every tool's parameters schema now has:
1. A JSON Schema snapshot (`z.toJSONSchema` with `io: "input"`) \u2014 this
captures exactly what the LLM sees at tool registration time, so any
drift caused by a future migration fails the snapshot.
2. Parse-accept/parse-reject assertions per tool pinning the
user-visible behavioural contract (required fields, refinement
bounds, enum membership, default values).
To make the snapshots possible without standing up each tool's full
Effect runtime, every tool file now exports its parameters schema as
`Parameters` at module scope:
- 9 tools already had a module-level const \u2014 just added `export`, and
standardised the name to `Parameters` (uppercase) where it was
previously `parameters`.
- 9 tools had their schema inline inside `Tool.define` \u2014 hoisted to
module scope under the same `Parameters` name and wired back through.
Zero behaviour change: Tool.define still sees the same schema, runtime
validation path is identical, SDK (types.gen.ts + openapi.json) is
byte-identical, and the full 2054-test suite passes.
18 JSON Schema snapshots and 43 explicit parse/reject assertions for the
18 built-in tools (apply_patch, bash, codesearch, edit, glob, grep,
invalid, lsp, multiedit, plan, question, read, skill, task, todo,
webfetch, websearch, write).
2026-04-18 04:01:50 +00:00
|
|
|
parameters: Parameters,
|
2026-04-10 20:57:12 +00:00
|
|
|
execute: (params: { content: string; filePath: string }, ctx: Tool.Context) =>
|
|
|
|
|
Effect.gen(function* () {
|
2026-04-10 21:01:33 +00:00
|
|
|
const filepath = path.isAbsolute(params.filePath)
|
|
|
|
|
? params.filePath
|
|
|
|
|
: path.join(Instance.directory, params.filePath)
|
2026-04-10 20:57:12 +00:00
|
|
|
yield* assertExternalDirectoryEffect(ctx, filepath)
|
2025-06-04 17:33:25 +00:00
|
|
|
|
2026-04-10 20:57:12 +00:00
|
|
|
const exists = yield* fs.existsSafe(filepath)
|
|
|
|
|
const contentOld = exists ? yield* fs.readFileString(filepath) : ""
|
2025-06-04 17:33:25 +00:00
|
|
|
|
2026-04-10 20:57:12 +00:00
|
|
|
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
|
2026-04-11 02:36:02 +00:00
|
|
|
yield* ctx.ask({
|
|
|
|
|
permission: "edit",
|
|
|
|
|
patterns: [path.relative(Instance.worktree, filepath)],
|
|
|
|
|
always: ["*"],
|
|
|
|
|
metadata: {
|
|
|
|
|
filepath,
|
|
|
|
|
diff,
|
|
|
|
|
},
|
|
|
|
|
})
|
2025-06-04 17:33:25 +00:00
|
|
|
|
2026-04-10 20:57:12 +00:00
|
|
|
yield* fs.writeWithDirs(filepath, params.content)
|
2026-04-11 02:36:02 +00:00
|
|
|
yield* format.file(filepath)
|
|
|
|
|
yield* bus.publish(File.Event.Edited, { file: filepath })
|
|
|
|
|
yield* bus.publish(FileWatcher.Event.Updated, {
|
|
|
|
|
file: filepath,
|
|
|
|
|
event: exists ? "change" : "add",
|
|
|
|
|
})
|
2025-06-04 17:33:25 +00:00
|
|
|
|
2026-04-10 20:57:12 +00:00
|
|
|
let output = "Wrote file successfully."
|
|
|
|
|
yield* lsp.touchFile(filepath, true)
|
|
|
|
|
const diagnostics = yield* lsp.diagnostics()
|
|
|
|
|
const normalizedFilepath = AppFileSystem.normalizePath(filepath)
|
|
|
|
|
let projectDiagnosticsCount = 0
|
|
|
|
|
for (const [file, issues] of Object.entries(diagnostics)) {
|
2026-04-11 02:00:56 +00:00
|
|
|
const current = file === normalizedFilepath
|
|
|
|
|
if (!current && projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
|
|
|
|
|
const block = LSP.Diagnostic.report(current ? filepath : file, issues)
|
|
|
|
|
if (!block) continue
|
|
|
|
|
if (current) {
|
|
|
|
|
output += `\n\nLSP errors detected in this file, please fix:\n${block}`
|
2026-04-10 20:57:12 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
projectDiagnosticsCount++
|
2026-04-11 02:00:56 +00:00
|
|
|
output += `\n\nLSP errors detected in other files:\n${block}`
|
2026-04-10 20:57:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
title: path.relative(Instance.worktree, filepath),
|
|
|
|
|
metadata: {
|
|
|
|
|
diagnostics,
|
|
|
|
|
filepath,
|
|
|
|
|
exists: exists,
|
|
|
|
|
},
|
|
|
|
|
output,
|
|
|
|
|
}
|
2026-04-11 02:36:02 +00:00
|
|
|
}).pipe(Effect.orDie),
|
2025-06-04 17:33:25 +00:00
|
|
|
}
|
2026-04-10 20:57:12 +00:00
|
|
|
}),
|
|
|
|
|
)
|