thincoder 0.12.58 → 0.12.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -1
- package/README.md +1 -1
- package/bin/thincoder.mjs +8 -0
- package/package.json +1 -1
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +34 -1
- package/src/advisor/run.mjs +89 -51
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +91 -14
- package/src/agent/helpers.mjs +35 -4
- package/src/agent/setup.mjs +90 -19
- package/src/agent/spawn-child.mjs +25 -0
- package/src/agent-tools/advisor.mjs +24 -2
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +2 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +467 -193
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +161 -125
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +5 -0
- package/src/context.mjs +87 -27
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +6 -4
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +126 -0
- package/src/memory/docs.mjs +196 -87
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +15 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +10 -5
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +106 -56
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -6
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +4 -2
- package/src/provider/core.mjs +56 -2
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +31 -43
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +10 -8
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +24 -20
- package/src/tools/file.mjs +18 -68
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +4 -12
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +385 -22
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +9 -0
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +29 -95
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +40 -0
- package/src/tui/pickers.mjs +11 -3
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/startup.mjs +36 -0
- package/src/tui/subagent-blocks.mjs +322 -144
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +132 -100
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/tools/execute.mjs
CHANGED
|
@@ -10,30 +10,34 @@
|
|
|
10
10
|
* killable timeout (an in-process infinite loop would freeze the CLI; a child
|
|
11
11
|
* process is killed like bash).
|
|
12
12
|
*
|
|
13
|
-
* The child
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* The child runs PURE node ESM — no helpers are injected (exec-prelude.mjs
|
|
14
|
+
* retired 2026-09-03, TOOLS.md §12: preloaded readFile/writeFile/glob/grep/log
|
|
15
|
+
* helpers made execute look like a file tool, bypassing the dedicated
|
|
16
|
+
* read/ls/glob/grep/write/edit tools). Scripts that need fs/path import the
|
|
17
|
+
* node: modules themselves — same boundary as bash, no fake sandbox.
|
|
17
18
|
*
|
|
18
19
|
* Parameters:
|
|
19
20
|
* code — JS to run inline (top-level await and import() supported). Use this OR scriptFile.
|
|
20
|
-
* scriptFile — run a .mjs/.js file with node (self-contained,
|
|
21
|
+
* scriptFile — run a .mjs/.js file with node (self-contained, imports what it needs). Use this OR code.
|
|
21
22
|
* nodeArgs — (scriptFile) extra node flags before the script (e.g. --test, --check); eval-like flags rejected
|
|
22
23
|
* workdir — run in this sub-directory (no directory restriction)
|
|
23
24
|
* filter — return only output lines matching this regex (case-insensitive)
|
|
24
|
-
* timeoutMs — timeout (default 30s, max
|
|
25
|
+
* timeoutMs — timeout (default 30s, max 600000ms)
|
|
25
26
|
*/
|
|
26
27
|
import { spawn } from "node:child_process"
|
|
27
|
-
import {
|
|
28
|
-
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
28
|
+
import { resolve } from "node:path"
|
|
29
29
|
import { DESC } from "./shared.mjs"
|
|
30
30
|
|
|
31
31
|
const MAX_SCRIPT = 50_000
|
|
32
32
|
const MAX_OUTPUT = 50_000
|
|
33
33
|
const DEFAULT_TIMEOUT = 30_000
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* 超时错误文本——带重试引导(TOOLS.md §14.1 D14.1.2——"下一跳"):数字 = 实际生效的
|
|
37
|
+
* timeoutMs(Math.min(t, 600_000) 或默认 30s)——上限 600000 与 schema/头注/execute.md 一致。
|
|
38
|
+
*/
|
|
39
|
+
const timeoutErrorText = (timeoutMs) =>
|
|
40
|
+
`Error: script timed out after ${timeoutMs}ms — retry with a larger timeoutMs (up to 600000) for long scripts, or use bash (default 120s) for shell commands`
|
|
37
41
|
|
|
38
42
|
/** Resolve workdir relative to cwd — no boundary assertion
|
|
39
43
|
* (§10.1 2026-09-02: workspace confinement removed; the child node process is
|
|
@@ -56,11 +60,10 @@ function applyFilter(output, filter) {
|
|
|
56
60
|
|
|
57
61
|
/** Spawn node with the given args, capture stdout/stderr, enforce timeout/abort.
|
|
58
62
|
* Resolves { text, ok } — ok=false on non-zero exit / timeout / abort. */
|
|
59
|
-
function runNode(childArgs, baseDir,
|
|
63
|
+
function runNode(childArgs, baseDir, timeoutMs, signal) {
|
|
60
64
|
return new Promise((resolvePromise) => {
|
|
61
65
|
const child = spawn(process.execPath, childArgs, {
|
|
62
66
|
cwd: baseDir,
|
|
63
|
-
env: { ...process.env, THINCODER_EXEC_ROOT: root },
|
|
64
67
|
stdio: ["ignore", "pipe", "pipe"],
|
|
65
68
|
windowsHide: true,
|
|
66
69
|
})
|
|
@@ -80,7 +83,7 @@ function runNode(childArgs, baseDir, root, timeoutMs, signal) {
|
|
|
80
83
|
const kill = () => { try { child.kill("SIGKILL") } catch { /* already gone */ } }
|
|
81
84
|
// After kill, wait for "close" (child fully reaped) before settling — settling
|
|
82
85
|
// early races the caller deleting the cwd dir while the child still holds it.
|
|
83
|
-
const armKick = () => { kickTimer = setTimeout(() => settle(mode === "abort" ? "(stopped)" :
|
|
86
|
+
const armKick = () => { kickTimer = setTimeout(() => settle(mode === "abort" ? "(stopped)" : timeoutErrorText(timeoutMs), false), 3000) }
|
|
84
87
|
const onAbort = () => { if (mode) return; mode = "abort"; kill(); armKick() }
|
|
85
88
|
|
|
86
89
|
timer = setTimeout(() => { if (!mode) { mode = "timeout"; kill(); armKick() } }, timeoutMs)
|
|
@@ -100,7 +103,7 @@ function runNode(childArgs, baseDir, root, timeoutMs, signal) {
|
|
|
100
103
|
child.on("error", (e) => settle(`Error: failed to start node: ${e.message}`, false))
|
|
101
104
|
child.on("close", (code) => {
|
|
102
105
|
if (mode === "abort") return settle("(stopped)", false)
|
|
103
|
-
if (mode === "timeout") return settle(
|
|
106
|
+
if (mode === "timeout") return settle(timeoutErrorText(timeoutMs), false)
|
|
104
107
|
const out = outBuf.trimEnd()
|
|
105
108
|
const err = errBuf.trim()
|
|
106
109
|
if (code === 0) {
|
|
@@ -132,11 +135,11 @@ export const executeTool = {
|
|
|
132
135
|
properties: {
|
|
133
136
|
code: {
|
|
134
137
|
type: "string",
|
|
135
|
-
description: "JavaScript code to execute (top-level await and dynamic import() supported).
|
|
138
|
+
description: "JavaScript code to execute (top-level await and dynamic import() supported). Pure node ESM — no preloaded globals; import node: modules (fs/path) yourself when needed. File reads/writes go through the dedicated read/ls/glob/grep/write/edit tools. Use this OR scriptFile.",
|
|
136
139
|
},
|
|
137
140
|
scriptFile: {
|
|
138
141
|
type: "string",
|
|
139
|
-
description: "Run a .mjs/.js file with node (self-contained
|
|
142
|
+
description: "Run a .mjs/.js file with node (self-contained — the file imports what it needs). Path relative to workdir — no directory restriction. Use this OR code. For `node <script>` / `node --test <file>` / `node --check <file>`.",
|
|
140
143
|
},
|
|
141
144
|
nodeArgs: {
|
|
142
145
|
type: "array",
|
|
@@ -174,7 +177,7 @@ export const executeTool = {
|
|
|
174
177
|
if (args.scriptFile) {
|
|
175
178
|
if (args.code?.trim()) return "Error: pass code OR scriptFile, not both"
|
|
176
179
|
// scriptFile mode: run a .mjs/.js file with node [nodeArgs...]. Self-contained —
|
|
177
|
-
//
|
|
180
|
+
// a real node process imports what it needs. No directory restriction.
|
|
178
181
|
const scriptAbs = resolve(baseDir, args.scriptFile)
|
|
179
182
|
let nodeArgs
|
|
180
183
|
try { nodeArgs = validateNodeArgs(args.nodeArgs) }
|
|
@@ -186,12 +189,13 @@ export const executeTool = {
|
|
|
186
189
|
if (code.length > MAX_SCRIPT) {
|
|
187
190
|
return `Error: script too large (${code.length} > ${MAX_SCRIPT} bytes). Split into smaller scripts or use individual tools.`
|
|
188
191
|
}
|
|
189
|
-
|
|
192
|
+
// inline mode: pure node ESM — no prelude, nothing injected (TOOLS.md §12).
|
|
193
|
+
childArgs = ["--input-type=module", "--eval", code]
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
const { text, ok } = await runNode(childArgs, baseDir,
|
|
196
|
+
const { text, ok } = await runNode(childArgs, baseDir, timeoutMs, ctx.signal)
|
|
193
197
|
// Only filter successful output — never swallow an error report behind a filter.
|
|
194
198
|
if (!ok) return text
|
|
195
199
|
return args.filter ? applyFilter(text, args.filter) : text
|
|
196
200
|
},
|
|
197
|
-
}
|
|
201
|
+
}
|
package/src/tools/file.mjs
CHANGED
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
detectFileEol,
|
|
11
11
|
joinWithEol,
|
|
12
12
|
majorityEol,
|
|
13
|
-
findCandidates,
|
|
14
13
|
FFFD_WARNING,
|
|
15
14
|
} from "./shared.mjs";
|
|
16
15
|
import { applyEditBatch } from "./edit-batch.mjs";
|
|
16
|
+
import { runSingleEdit } from "./edit-diff.mjs";
|
|
17
17
|
import { specForModel } from "../config.mjs";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { mkdir, readFile, stat, writeFile, unlink } from "node:fs/promises";
|
|
@@ -73,6 +73,7 @@ export const readTool = {
|
|
|
73
73
|
type: "object",
|
|
74
74
|
properties: {
|
|
75
75
|
path: { type: "string", description: "File path (relative to cwd or absolute)" },
|
|
76
|
+
filePath: { type: "string", description: "Alias for path (supported for API compatibility)" },
|
|
76
77
|
offset: { type: "number", description: "1-based line number to start from" },
|
|
77
78
|
limit: { type: "number", description: `Max lines to return (default ${MAX_READ_LINES})` },
|
|
78
79
|
allowExternal: { type: "boolean", description: "No-op retained for API compatibility — path resolution no longer asserts a working-directory boundary (all paths resolve relative to cwd)." },
|
|
@@ -82,7 +83,8 @@ export const readTool = {
|
|
|
82
83
|
},
|
|
83
84
|
readonly: true,
|
|
84
85
|
async execute(args, ctx) {
|
|
85
|
-
const
|
|
86
|
+
const p = args.path ?? args.filePath
|
|
87
|
+
const abs = args.allowExternal ? resolveExternal(ctx, p) : resolveInCwd(ctx, p)
|
|
86
88
|
// Large file guard: check size first, reject reading entire file if >10MB (offset/limit only affect the returned slice, not buffering)
|
|
87
89
|
const st = await stat(abs).catch(() => null)
|
|
88
90
|
if (st && st.size > MAX_FILE_READ_BYTES) throw new Error(`File too large (${Math.round(st.size / 1_000_000)}MB > 10MB limit). Use bash with head/tail or grep for targeted extraction.`)
|
|
@@ -214,13 +216,13 @@ export const editTool = {
|
|
|
214
216
|
parameters: {
|
|
215
217
|
type: "object",
|
|
216
218
|
properties: {
|
|
217
|
-
path: { type: "string", description: "File path" },
|
|
219
|
+
path: { type: "string", description: "File path — single form: required; with the edits array: optional top-level default for entries without their own path" },
|
|
218
220
|
old_string: { type: "string", description: "Exact text to replace" },
|
|
219
221
|
new_string: { type: "string", description: "Replacement text" },
|
|
220
222
|
replace_all: { type: "boolean", description: "Replace all occurrences (default false)" },
|
|
221
223
|
edits: {
|
|
222
224
|
type: "array",
|
|
223
|
-
description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing; same-file entries apply serially, each based on the previous result). Use it for multiple changes to the same file AND for independent changes across multiple files — prefer one batched call over N single edits. Mutually exclusive with
|
|
225
|
+
description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing; same-file entries apply serially, each based on the previous result). Use it for multiple changes to the same file AND for independent changes across multiple files — prefer one batched call over N single edits. A top-level path is allowed — it defaults entries without their own path (entry paths win). Mutually exclusive with top-level old_string/new_string — provide each change's old/new inside its edits entry.",
|
|
224
226
|
items: {
|
|
225
227
|
type: "object",
|
|
226
228
|
properties: {
|
|
@@ -229,7 +231,7 @@ export const editTool = {
|
|
|
229
231
|
new_string: { type: "string" },
|
|
230
232
|
replace_all: { type: "boolean" },
|
|
231
233
|
},
|
|
232
|
-
required: ["
|
|
234
|
+
required: ["old_string", "new_string"],
|
|
233
235
|
},
|
|
234
236
|
},
|
|
235
237
|
},
|
|
@@ -237,7 +239,13 @@ export const editTool = {
|
|
|
237
239
|
},
|
|
238
240
|
readonly: false,
|
|
239
241
|
touchedPaths(args) {
|
|
240
|
-
|
|
242
|
+
// 2026-09-05 用户裁定:顶层 path = 批默认——仅当有条目缺 path 时计入(条目全带 path
|
|
243
|
+
// 时顶层 path 不实际使用——不虚报进 _touchedFiles)
|
|
244
|
+
if (args.edits) {
|
|
245
|
+
const out = args.edits.map((e) => e.path).filter(Boolean)
|
|
246
|
+
if (args.path && args.edits.some((e) => !e.path)) out.push(args.path)
|
|
247
|
+
return out
|
|
248
|
+
}
|
|
241
249
|
return args.path ? [args.path] : []
|
|
242
250
|
},
|
|
243
251
|
async execute(args, ctx) {
|
|
@@ -245,67 +253,9 @@ export const editTool = {
|
|
|
245
253
|
// (应用逻辑在 edit-batch.mjs——2026-09-01 拆出,500 行硬限,先例 git-ext.mjs)
|
|
246
254
|
if (args.edits) return applyEditBatch(args, ctx)
|
|
247
255
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
throw new Error("old_string must not be empty (empty string matches everywhere and would corrupt the file)")
|
|
252
|
-
}
|
|
253
|
-
// #5(2026-09-01 交付评审尾巴):new_string 非字符串(含 undefined)在写盘前拒绝——
|
|
254
|
-
// 原缺陷:replace 回调返回 undefined 被字符串化成 "undefined" 写入盘,随后
|
|
255
|
-
// args.new_string.split 才 TypeError——文件已损坏 + 错误信息不知所云。
|
|
256
|
-
if (typeof args.new_string !== "string") {
|
|
257
|
-
throw new Error(
|
|
258
|
-
`new_string must be a string${args.new_string === undefined ? " (missing)" : ` (got ${typeof args.new_string})`} — nothing written`,
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
const raw = await readFile(abs, "utf8")
|
|
262
|
-
const content = normalizeEOL(raw)
|
|
263
|
-
const occurrences = content.split(args.old_string).length - 1
|
|
264
|
-
if (occurrences === 0) {
|
|
265
|
-
// Give clues to help the model locate: first-line preview + common causes
|
|
266
|
-
const preview = args.old_string.slice(0, 100).split("\n")[0]
|
|
267
|
-
// Similarity candidates (LCS, line-level, top 3, score ≥ 0.5) — turns the
|
|
268
|
-
// "not found" black box into a pointer at the most likely intended line.
|
|
269
|
-
// Multi-line old_string: only its first line is scored (marked accordingly).
|
|
270
|
-
const cands = findCandidates(content.split("\n"), args.old_string)
|
|
271
|
-
let candText = ""
|
|
272
|
-
if (cands.length > 0) {
|
|
273
|
-
const header = args.old_string.includes("\n")
|
|
274
|
-
? ` similar lines (old_string line 1: "${args.old_string.split("\n")[0].slice(0, 80)}"):`
|
|
275
|
-
: " similar lines:"
|
|
276
|
-
candText = "\n" + header + "\n" + cands.map((c) => ` L${c.line}: ${c.preview} (${Math.round(c.score * 100)}%)`).join("\n")
|
|
277
|
-
}
|
|
278
|
-
throw new Error(
|
|
279
|
-
`old_string not found in ${args.path}\n` +
|
|
280
|
-
` searched: "${preview}${args.old_string.length > 100 ? "…" : ""}"\n` +
|
|
281
|
-
(lastWriteOf(abs)?.type === "write"
|
|
282
|
-
? ` hints: this file was modified since your last read (write 全文重写后内容全变) — re-read it to refresh your copy of the content, then retry\n`
|
|
283
|
-
: isDirty(abs)
|
|
284
|
-
? ` hints: this file was modified since your last read (a prior write marked it dirty) — re-read it to refresh your copy of the content, then retry\n`
|
|
285
|
-
: ` hints: whitespace mismatch? file already changed? try reading the file first\n`) +
|
|
286
|
-
candText
|
|
287
|
-
)
|
|
288
|
-
}
|
|
289
|
-
if (occurrences > 1 && !args.replace_all) {
|
|
290
|
-
throw new Error(`old_string matches ${occurrences} times in ${args.path}; provide more context or set replace_all`)
|
|
291
|
-
}
|
|
292
|
-
const updated = args.replace_all
|
|
293
|
-
? content.split(args.old_string).join(args.new_string)
|
|
294
|
-
// Functional replacement: avoid $-substitution patterns in new_string (match string / backreference) being expanded
|
|
295
|
-
: content.replace(args.old_string, () => args.new_string)
|
|
296
|
-
// Write back in the file's ORIGINAL EOL style (first-newline rule) — a CRLF
|
|
297
|
-
// file must not come back as LF (that rewrites every line in the diff).
|
|
298
|
-
// normalizeEOL first: new_string may carry \r\n (e.g. pasted from a raw CRLF
|
|
299
|
-
// read); without normalizing, split leaves stray \r and CRLF join makes \r\r\n.
|
|
300
|
-
await writeFile(abs, joinWithEol(normalizeEOL(updated).split("\n"), raw), "utf8")
|
|
301
|
-
// 2026-08-31 工具顺手度:记录受影响区(替换首行 + 行数差)——insert_after 精确判定
|
|
302
|
-
const matchIdx = content.indexOf(args.old_string)
|
|
303
|
-
const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
|
|
304
|
-
const lineShift = args.new_string.split("\n").length - args.old_string.split("\n").length
|
|
305
|
-
recordWrite(abs, { type: "edit", startLine: editStartLine, shift: lineShift })
|
|
306
|
-
const diff = gitDiffOne(ctx.cwd, abs)
|
|
307
|
-
const baseResult = `Edited ${args.path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s)${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
|
|
308
|
-
return await appendWriteContext(abs, editStartLine, baseResult)
|
|
256
|
+
// 单文件(现状路径)——执行体整段迁出至 edit-diff.mjs(TOOLS.md §15 D15.1——
|
|
257
|
+
// 行级 LCS 判定:零重叠→插入 / 一般 diff→LCS / 空 new→显式报错)
|
|
258
|
+
return runSingleEdit(args, ctx)
|
|
309
259
|
},
|
|
310
260
|
}
|
|
311
261
|
|
|
@@ -453,7 +403,7 @@ export const hashlineEditTool = {
|
|
|
453
403
|
const hashDump = fileHashes.slice(0, maxShow).map((h, i) => `${h} L${i + 1}: ${lines[i].slice(0, 80)}`).join("\n")
|
|
454
404
|
const preview = target.join(" ")
|
|
455
405
|
throw new Error(
|
|
456
|
-
`Hash sequence not found in ${args.path}: ${preview}\n` +
|
|
406
|
+
`Hash sequence not found in ${args.path}: ${preview} — for fresh hashes, re-read the file with hashes=true\n` +
|
|
457
407
|
`The file may have been modified since you last read it. Current hashes (first ${maxShow} lines):\n${hashDump}` +
|
|
458
408
|
(corrupted ? `\n${FFFD_WARNING}` : "")
|
|
459
409
|
)
|
package/src/tools/file_ops.md
CHANGED
|
@@ -13,4 +13,5 @@ Parameters:
|
|
|
13
13
|
Notes:
|
|
14
14
|
- Paths resolve relative to cwd — no directory restriction (same boundary as bash; the approval gate is the guard).
|
|
15
15
|
- `dest` is overwritten if it already exists. `copy` is recursive for directories.
|
|
16
|
-
- To create a directory, use `write` (creates parent dirs) or `bash mkdir`.
|
|
16
|
+
- To create a directory, use `write` (creates parent dirs) or `bash mkdir`.
|
|
17
|
+
- Returns `Moved|Copied|Renamed <source> -> <dest>` or `Error: ...` (same-path and missing-path errors).
|
|
@@ -3,4 +3,6 @@ Get the current date, time, weekday, and timezone.
|
|
|
3
3
|
**Route to get_current_time instead of bash:**
|
|
4
4
|
- `date` / `time` → get_current_time
|
|
5
5
|
|
|
6
|
-
Use it whenever a task depends on the current time or date (deadlines, freshness, timestamps) rather than shelling out.
|
|
6
|
+
Use it whenever a task depends on the current time or date (deadlines, freshness, timestamps) rather than shelling out.
|
|
7
|
+
|
|
8
|
+
Returns `Date` / `Timezone` / `Weekday` / `Local` lines (UTC + local timezone).
|
|
@@ -10,3 +10,5 @@ Notes:
|
|
|
10
10
|
- Hashes are position-independent: they identify lines by content, not by line number (which changes after edits)
|
|
11
11
|
- If the hash sequence isn't found, the error will include the current file's hashes so you can retry with corrected values
|
|
12
12
|
- Prefer this over edit when: 1) the file may have mixed whitespace/encoding, 2) you want to edit a block of lines with a single call
|
|
13
|
+
- Replacement text replaces the lines identified by the hashes — content not present in new_content is deleted. For a new line after a known line, use insert_after. For a single simple string swap, use edit.
|
|
14
|
+
- use the most recent read of the file as the source of old_string / line numbers / hashes — re-read after the file changed
|
package/src/tools/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { toOpenAISchema } from "./shared.mjs";
|
|
3
3
|
|
|
4
4
|
import { readTool, writeTool, editTool, insertAfterTool, readImageTool, hashlineEditTool } from "./file.mjs";
|
|
5
|
+
import { readPdfTool } from "./pdf.mjs";
|
|
5
6
|
import { applyPatchTool, deleteTool } from "./patch.mjs";
|
|
6
7
|
import { bashTool, globTool, grepTool, lsTool } from "./system.mjs";
|
|
7
8
|
import { websearchTool, fetchTool } from "./web.mjs";
|
|
@@ -15,7 +16,7 @@ import { treeTool } from "./tree.mjs";
|
|
|
15
16
|
|
|
16
17
|
export const builtinTools = [
|
|
17
18
|
readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, applyPatchTool,
|
|
18
|
-
readImageTool, bashTool, globTool, grepTool,
|
|
19
|
+
readImageTool, readPdfTool, bashTool, globTool, grepTool,
|
|
19
20
|
websearchTool, lsTool, fetchTool, deleteTool,
|
|
20
21
|
gitTool, questionTool,
|
|
21
22
|
checklistTool, lintTool, lspTool, executeTool,
|
|
@@ -25,7 +26,7 @@ export const builtinTools = [
|
|
|
25
26
|
|
|
26
27
|
export {
|
|
27
28
|
readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, applyPatchTool,
|
|
28
|
-
readImageTool, bashTool, globTool, grepTool,
|
|
29
|
+
readImageTool, readPdfTool, bashTool, globTool, grepTool,
|
|
29
30
|
websearchTool, lsTool, fetchTool, deleteTool,
|
|
30
31
|
gitTool, questionTool,
|
|
31
32
|
checklistTool, lintTool, lspTool, executeTool,
|
|
@@ -8,7 +8,8 @@ Parameters:
|
|
|
8
8
|
|
|
9
9
|
Notes:
|
|
10
10
|
- Either after_line or after_regex is required; if both are given, after_line wins.
|
|
11
|
-
- Use this instead of
|
|
11
|
+
- Use this instead of edit when you're adding a new line — a checklist item, a doc heading, a line of prose, a function, an import, or a block — no need to fabricate surrounding context for exact matching.
|
|
12
12
|
- The inserted content becomes its own line; it's equivalent to `lines.splice(targetLine, 0, content)`.
|
|
13
13
|
- Returns a diff of the change.
|
|
14
14
|
- **Read-before-insert guard**: if the file was modified by any write tool (write/edit/insert_after/hashline_edit/apply_patch/delete) since your last `read`, this tool REFUSES with an error — line numbers may be stale. Read the file again, then retry. This prevents after_line from silently landing at a drifted position.
|
|
15
|
+
- use the most recent read of the file as the source of old_string / line numbers / hashes — re-read after the file changed
|
package/src/tools/lint.md
CHANGED
|
@@ -3,6 +3,8 @@ Without 'full', runs a fast node --check (JS/TS syntax only, catches parse error
|
|
|
3
3
|
With 'full', runs the language-aware cascade: tsc –noEmit (TS); ruff (Python); cargo check (Rust); go vet (Go). JS/JSX files fall back to node --check; TS uses tsc --noEmit (requires tsconfig.json).
|
|
4
4
|
Use the fast default after every write/edit; use 'full' before declaring a task complete.
|
|
5
5
|
|
|
6
|
+
Returns the check result: `Syntax OK: <path>` / `Syntax error in <path>: <message>` (or the language checker's output — `✓ no issues` or the failure text).
|
|
7
|
+
|
|
6
8
|
Parameters:
|
|
7
9
|
- path: File to check (default: most recently modified file)
|
|
8
10
|
- full: Run the full language-aware cascade instead of just node --check (default false)
|
package/src/tools/lsp.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. Use this to understand code structure without grep-guessing function locations or type shapes.
|
|
1
|
+
LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. Use this to understand code structure without grep-guessing function locations or type shapes. Find files with glob / repo_outline — use lsp for definition / references / diagnostics
|
|
2
2
|
|
|
3
3
|
Parameters:
|
|
4
4
|
- subcommand (required): LSP operation — "definition" | "references" | "hover" | "symbols" | "diagnostics"
|
|
5
5
|
- uri (required): Target file path (relative to project root)
|
|
6
6
|
- line: 1-based line number (for definition/references/hover)
|
|
7
7
|
- character: 1-based character offset (for definition/references/hover)
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- Returns the requested LSP result: definition location, references, hover info, document symbols, or diagnostics — or an error message.
|
package/src/tools/patch.mjs
CHANGED
|
@@ -15,8 +15,23 @@ import { relative, dirname } from "node:path";
|
|
|
15
15
|
/**
|
|
16
16
|
* Parse a unified diff: returns [{ path, isNew, hunks: [{ ops: [{type:" "|"-"|"+", text}] }] }]
|
|
17
17
|
* Consume hunk lines by the line counts in the @@ header — LLMs often strip context blank lines to pure empty lines,
|
|
18
|
-
* so we use counts rather than first characters to determine hunk boundaries
|
|
18
|
+
* so we use counts rather than first characters to determine hunk boundaries.
|
|
19
|
+
* D15.6: a bare "@@" header (no coordinates) is accepted — the hunk body runs until the next hunk/file header
|
|
20
|
+
* ("@@" / "--- " / "+++ " / "diff " / "index "), purely located by its ops.
|
|
19
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* P15.10(2026-09-05 用户裁定——「符合模型直觉」):文件头判定——
|
|
24
|
+
* 完整头(`--- x` 后随 `+++ `)任意老路径形态均认(git 规范);
|
|
25
|
+
* 容缺头(`+++ b/<path>` 配对行省略——模型单文件补丁自然形态)仅认 a//b/ 前缀——
|
|
26
|
+
* newPath 推导 = oldPath;`/dev/null` 容缺仍拒(新文件名从 --- 侧不可推导——parsePatch 内特报);
|
|
27
|
+
* 其他 `--- x` = 普通删行内容(行首标记 - + 内容 `-- x`)——不是文件头——hunk 体不得误断。
|
|
28
|
+
*/
|
|
29
|
+
function isFileHeader(line, nextLine) {
|
|
30
|
+
if (!line.startsWith("--- ")) return false
|
|
31
|
+
if (nextLine?.startsWith("+++ ")) return true
|
|
32
|
+
return /^[ab]\//.test(line.slice(4).trim())
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function parsePatch(patch) {
|
|
21
36
|
// Patch text often comes from CRLF terminals/model output; trailing \r mixed into hunk content breaks context matching, strip uniformly
|
|
22
37
|
const lines = patch.replace(/\r(?=\n|$)/g, "").split("\n")
|
|
@@ -29,18 +44,69 @@ function parsePatch(patch) {
|
|
|
29
44
|
if (line.startsWith("--- ")) {
|
|
30
45
|
const oldPath = line.slice(4).trim()
|
|
31
46
|
const plus = lines[i + 1]
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
if (plus?.startsWith("+++ ")) {
|
|
48
|
+
const newPath = plus.slice(4).trim()
|
|
49
|
+
if (newPath === "/dev/null") throw new Error("Deleting files via patch is not supported — use the delete tool")
|
|
50
|
+
cur = { path: stripPrefix(newPath), isNew: oldPath === "/dev/null", hunks: [] }
|
|
51
|
+
files.push(cur)
|
|
52
|
+
i += 2
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
// P15.10:容缺头——`--- a/<path>`(或 b/ 前缀)后直接跟 hunk = 对同路径的修改。
|
|
56
|
+
if (/^[ab]\//.test(oldPath)) {
|
|
57
|
+
cur = { path: stripPrefix(oldPath), isNew: false, hunks: [] }
|
|
58
|
+
files.push(cur)
|
|
59
|
+
i += 1
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (oldPath === "/dev/null") {
|
|
63
|
+
throw new Error(`"--- /dev/null" needs a "+++ b/<path>" line naming the new file — the --- side does not carry the file name`)
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Malformed patch: expected "+++" line after "${line}"`)
|
|
39
66
|
}
|
|
40
67
|
if (line.startsWith("@@")) {
|
|
41
68
|
if (!cur) throw new Error("Malformed patch: hunk header before any file header")
|
|
42
69
|
const m = line.match(/^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/)
|
|
43
|
-
if (!m)
|
|
70
|
+
if (!m) {
|
|
71
|
+
// D15.6 (TOOLS.md §15):坐标裸 "@@" 头——hunk 完全靠操作行定位。
|
|
72
|
+
// 无行数可用:操作行以 " " / "-" / "+" 开头(空行宽容为上下文行),
|
|
73
|
+
// 直到下一个 hunk 头 / 文件头 "@@"/"--- "/"+++ "/"diff "/"index " 为止。
|
|
74
|
+
if (!/^@@(?: @@)?\s*$/.test(line)) {
|
|
75
|
+
throw new Error(`Malformed patch: bad hunk header "${line}" (need @@ -old,count +new,count @@ or bare @@)`)
|
|
76
|
+
}
|
|
77
|
+
const hunk = { ops: [] }
|
|
78
|
+
i++
|
|
79
|
+
while (i < lines.length) {
|
|
80
|
+
const hl = lines[i]
|
|
81
|
+
if (hl.startsWith("@") || isFileHeader(hl, lines[i + 1]) || hl.startsWith("+++ ") || hl.startsWith("diff ") || hl.startsWith("index ")) break
|
|
82
|
+
if (hl.startsWith("\\")) { i++; continue } // ""
|
|
83
|
+
// 宽容空行=上下文行——但 patch 文本末尾(或 hunk 之间/文件头之前)的 "" 是
|
|
84
|
+
// 分隔产物而非内容:仅当后继仍是操作行时才当作上下文消费。
|
|
85
|
+
// 复评 #1(2026-09-04):文件头 "--- "/"+++ " 以 -/+ 开头会被 op 前缀判定误收——
|
|
86
|
+
// 文件头前的分隔空行不得吞成幽灵上下文行(会把 - 锚序列尾部拼上 ""——跨文件
|
|
87
|
+
// 零上下文 hunk 因此误报 not-found)。
|
|
88
|
+
const next = lines[i + 1]
|
|
89
|
+
if (hl === "" && (next == null || !/^[ +\-\\]/.test(next) || isFileHeader(next, lines[i + 2]) || next.startsWith("+++ "))) break
|
|
90
|
+
const tag = hl === "" ? " " : hl[0]
|
|
91
|
+
if (tag !== " " && tag !== "-" && tag !== "+") break // metadata / file section end
|
|
92
|
+
hunk.ops.push({ type: tag, text: hl === "" ? "" : hl.slice(1) })
|
|
93
|
+
i++
|
|
94
|
+
}
|
|
95
|
+
if (hunk.ops.length === 0) throw new Error(`Malformed patch: empty coordinate-less hunk "${line.trim()}"`)
|
|
96
|
+
const ctxCount = hunk.ops.filter((o) => o.type === " ").length
|
|
97
|
+
// §15.3 (TOOLS.md D15.10.1——2026-09-04):context<2 且含 ≥1 个 - 行 → 接受——定位锚 = hunk 内
|
|
98
|
+
// 匹配行序列(空格上下文行 + - 行——按出现序)连续——唯一匹配即应用(applyHunks 既有锚匹配域——
|
|
99
|
+
// 多匹配 / not-found 语义不变)。0 上下文与 1 上下文同待遇(评审 #4a)。
|
|
100
|
+
// 纯 +(无 - 锚)且 context<2 仍拒——插入位置不可判——报错引导加锚(NF15.8c)。
|
|
101
|
+
const removedCount = hunk.ops.filter((o) => o.type === "-").length
|
|
102
|
+
if (ctxCount < 2 && removedCount === 0) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Coordinate-less hunk ${cur.hunks.length + 1} in ${cur.path} has ${ctxCount} context line(s) — add more context lines`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
cur.hunks.push(hunk)
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
44
110
|
let oldNeed = m[1] == null ? 1 : Number(m[1])
|
|
45
111
|
let newNeed = m[2] == null ? 1 : Number(m[2])
|
|
46
112
|
const hunk = { ops: [] }
|
|
@@ -62,8 +128,10 @@ function parsePatch(patch) {
|
|
|
62
128
|
}
|
|
63
129
|
i++ // skip diff --git / index / blank lines and other metadata
|
|
64
130
|
}
|
|
65
|
-
|
|
66
|
-
|
|
131
|
+
// P15.10:容缺/完整空段头(头后无任何 hunk)过滤——不虚报 touchedPaths、不触发无谓 read+write
|
|
132
|
+
const withHunks = files.filter((f) => f.hunks.length > 0)
|
|
133
|
+
if (withHunks.length === 0) throw new Error("No file changes found in patch (need --- / +++ headers)")
|
|
134
|
+
return withHunks
|
|
67
135
|
}
|
|
68
136
|
|
|
69
137
|
/** Apply hunks sequentially onto an in-memory line array; any failure throws (caller guarantees nothing is written to disk). Ignores trailing \r when comparing; context lines retain original bytes */
|
|
@@ -84,7 +152,10 @@ function applyHunks(fileLines, hunks, eol, path) {
|
|
|
84
152
|
const preview = oldSeq.slice(0, 3).join(" ⏎ ")
|
|
85
153
|
throw new Error(`Hunk ${h + 1} in ${path} does not apply — context not found: "${preview}${oldSeq.length > 3 ? "…" : ""}". Read the file first and regenerate the patch from actual content.`)
|
|
86
154
|
}
|
|
87
|
-
if (matches.length > 1)
|
|
155
|
+
if (matches.length > 1) {
|
|
156
|
+
const preview = oldSeq.slice(0, 3).join(" ⏎ ")
|
|
157
|
+
throw new Error(`Hunk ${h + 1} in ${path} matches ${matches.length} locations — add more context lines to make it unique. Anchor: "${preview}${oldSeq.length > 3 ? "…" : ""}"`)
|
|
158
|
+
}
|
|
88
159
|
const pos = matches[0]
|
|
89
160
|
const out = []
|
|
90
161
|
let src = pos
|
|
@@ -103,7 +174,7 @@ export const applyPatchTool = {
|
|
|
103
174
|
parameters: {
|
|
104
175
|
type: "object",
|
|
105
176
|
properties: {
|
|
106
|
-
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks." },
|
|
177
|
+
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks (a bare @@ header is also accepted — coordinate-less hunks are located by their anchor lines: context lines plus the removed (-) lines, matched as a contiguous sequence — a unique match applies; a zero/one-context hunk is accepted only when it removes (-) at least one line and that anchor sequence is unique, while anchor-free pure-+ (insert) hunks need at least 2 context lines). The +++ b/<path> pair may be omitted for existing files — a lone --- a/<path> (or --- b/<path>) header followed directly by hunks applies to that path (new files still need --- /dev/null + +++ b/<path>)." },
|
|
107
178
|
},
|
|
108
179
|
required: ["patch"],
|
|
109
180
|
},
|