thincoder 0.12.54 → 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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -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 +565 -128
- 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 +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -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 +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- 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 +3 -1
- package/src/tools/linter.mjs +9 -37
- 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 +11 -32
- 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 +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- 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/tools/exec-prelude.mjs +0 -84
package/src/tools/file.mjs
CHANGED
|
@@ -10,16 +10,13 @@ import {
|
|
|
10
10
|
detectFileEol,
|
|
11
11
|
joinWithEol,
|
|
12
12
|
majorityEol,
|
|
13
|
-
findCandidates,
|
|
14
13
|
FFFD_WARNING,
|
|
15
14
|
} from "./shared.mjs";
|
|
15
|
+
import { applyEditBatch } from "./edit-batch.mjs";
|
|
16
|
+
import { runSingleEdit } from "./edit-diff.mjs";
|
|
16
17
|
import { specForModel } from "../config.mjs";
|
|
17
18
|
import { createHash } from "node:crypto";
|
|
18
|
-
import { mkdir } from "node:fs/promises";
|
|
19
|
-
import { readFile } from "node:fs/promises";
|
|
20
|
-
import { stat } from "node:fs/promises";
|
|
21
|
-
import { writeFile } from "node:fs/promises";
|
|
22
|
-
import { unlink } from "node:fs/promises";
|
|
19
|
+
import { mkdir, readFile, stat, writeFile, unlink } from "node:fs/promises";
|
|
23
20
|
import { join, relative, dirname } from "node:path";
|
|
24
21
|
|
|
25
22
|
const MAX_FILE_READ_BYTES = 10_000_000
|
|
@@ -54,8 +51,9 @@ export function clearLastWrite(abs) { lastWrites.delete(abs) }
|
|
|
54
51
|
* 模型拿到的不只是"inserted at L395",而是"L395 这行是什么内容"——下次再操作时
|
|
55
52
|
* 能自检"我的行号 vs 实际内容"是否匹配,匹配不上 = 行号漂了,先 read——
|
|
56
53
|
* 死循环就断了(根因:模型对行号锚点的"新鲜度"没有感知——数字本身不携带语义)。
|
|
57
|
-
* write 全文重写跳过(无行号锚点——模型刚写的知道内容)。
|
|
58
|
-
|
|
54
|
+
* write 全文重写跳过(无行号锚点——模型刚写的知道内容)。
|
|
55
|
+
* edit-batch.mjs(数组形态)复用——导出仅供内部模块,非公共 API。 */
|
|
56
|
+
export async function appendWriteContext(abs, writeLine, baseResult) {
|
|
59
57
|
const content = normalizeEOL(await readFile(abs, "utf8"))
|
|
60
58
|
const lines = content.split("\n")
|
|
61
59
|
const start = Math.max(1, writeLine - 3)
|
|
@@ -75,16 +73,18 @@ export const readTool = {
|
|
|
75
73
|
type: "object",
|
|
76
74
|
properties: {
|
|
77
75
|
path: { type: "string", description: "File path (relative to cwd or absolute)" },
|
|
76
|
+
filePath: { type: "string", description: "Alias for path (supported for API compatibility)" },
|
|
78
77
|
offset: { type: "number", description: "1-based line number to start from" },
|
|
79
78
|
limit: { type: "number", description: `Max lines to return (default ${MAX_READ_LINES})` },
|
|
80
|
-
allowExternal: { type: "boolean", description: "
|
|
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)." },
|
|
81
80
|
hashes: { type: "boolean", description: "Include SHA256 line hashes for hash-based editing (default false). Use when you plan to edit the file with hashline_edit." },
|
|
82
81
|
},
|
|
83
82
|
required: ["path"],
|
|
84
83
|
},
|
|
85
84
|
readonly: true,
|
|
86
85
|
async execute(args, ctx) {
|
|
87
|
-
const
|
|
86
|
+
const p = args.path ?? args.filePath
|
|
87
|
+
const abs = args.allowExternal ? resolveExternal(ctx, p) : resolveInCwd(ctx, p)
|
|
88
88
|
// Large file guard: check size first, reject reading entire file if >10MB (offset/limit only affect the returned slice, not buffering)
|
|
89
89
|
const st = await stat(abs).catch(() => null)
|
|
90
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.`)
|
|
@@ -216,13 +216,13 @@ export const editTool = {
|
|
|
216
216
|
parameters: {
|
|
217
217
|
type: "object",
|
|
218
218
|
properties: {
|
|
219
|
-
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" },
|
|
220
220
|
old_string: { type: "string", description: "Exact text to replace" },
|
|
221
221
|
new_string: { type: "string", description: "Replacement text" },
|
|
222
222
|
replace_all: { type: "boolean", description: "Replace all occurrences (default false)" },
|
|
223
223
|
edits: {
|
|
224
224
|
type: "array",
|
|
225
|
-
description: "
|
|
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.",
|
|
226
226
|
items: {
|
|
227
227
|
type: "object",
|
|
228
228
|
properties: {
|
|
@@ -231,7 +231,7 @@ export const editTool = {
|
|
|
231
231
|
new_string: { type: "string" },
|
|
232
232
|
replace_all: { type: "boolean" },
|
|
233
233
|
},
|
|
234
|
-
required: ["
|
|
234
|
+
required: ["old_string", "new_string"],
|
|
235
235
|
},
|
|
236
236
|
},
|
|
237
237
|
},
|
|
@@ -239,111 +239,23 @@ export const editTool = {
|
|
|
239
239
|
},
|
|
240
240
|
readonly: false,
|
|
241
241
|
touchedPaths(args) {
|
|
242
|
-
|
|
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
|
+
}
|
|
243
249
|
return args.path ? [args.path] : []
|
|
244
250
|
},
|
|
245
251
|
async execute(args, ctx) {
|
|
246
252
|
// 2026-08-31 工具顺手度(用户批准):数组形态——一次多文件原子替换
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
throw new Error("edits must be a non-empty array of {path, old_string, new_string}")
|
|
250
|
-
}
|
|
251
|
-
if (args.path || args.old_string !== undefined || args.new_string !== undefined) {
|
|
252
|
-
throw new Error("edits array is mutually exclusive with path/old_string/new_string")
|
|
253
|
-
}
|
|
254
|
-
// 原子:先全量 read+match 检查(所有文件都能替换)——任一失败全不写
|
|
255
|
-
const prepared = []
|
|
256
|
-
for (const e of args.edits) {
|
|
257
|
-
if (!e.path) throw new Error("each edit must have a path")
|
|
258
|
-
if (!e.old_string) throw new Error(`edit for ${e.path}: old_string must not be empty`)
|
|
259
|
-
const abs = resolveInCwd(ctx, e.path)
|
|
260
|
-
const raw = await readFile(abs, "utf8")
|
|
261
|
-
const content = normalizeEOL(raw)
|
|
262
|
-
const occurrences = content.split(e.old_string).length - 1
|
|
263
|
-
if (occurrences === 0) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
`edit aborted (atomic — no files written): old_string not found in ${e.path}\n` +
|
|
266
|
-
` searched: "${e.old_string.slice(0, 100).split("\n")[0]}${e.old_string.length > 100 ? "…" : ""}"`
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
if (occurrences > 1 && !e.replace_all) {
|
|
270
|
-
throw new Error(
|
|
271
|
-
`edit aborted (atomic — no files written): old_string matches ${occurrences} times in ${e.path}; ` +
|
|
272
|
-
`provide more context or set replace_all`
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
const updated = e.replace_all
|
|
276
|
-
? content.split(e.old_string).join(e.new_string)
|
|
277
|
-
: content.replace(e.old_string, () => e.new_string)
|
|
278
|
-
const matchIdx = content.indexOf(e.old_string)
|
|
279
|
-
const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
|
|
280
|
-
const lineShift = e.new_string.split("\n").length - e.old_string.split("\n").length
|
|
281
|
-
prepared.push({ abs, path: e.path, raw, updated, editStartLine, lineShift, occurrences: e.replace_all ? occurrences : 1 })
|
|
282
|
-
}
|
|
283
|
-
// 全部检查通过——逐个写
|
|
284
|
-
const results = []
|
|
285
|
-
for (const p of prepared) {
|
|
286
|
-
await writeFile(p.abs, joinWithEol(normalizeEOL(p.updated).split("\n"), p.raw), "utf8")
|
|
287
|
-
recordWrite(p.abs, { type: "edit", startLine: p.editStartLine, shift: p.lineShift })
|
|
288
|
-
const withCtx = await appendWriteContext(p.abs, p.editStartLine, `Edited ${p.path}: replaced ${p.occurrences} occurrence(s)`)
|
|
289
|
-
results.push(withCtx)
|
|
290
|
-
}
|
|
291
|
-
return results.join("\n")
|
|
292
|
-
}
|
|
253
|
+
// (应用逻辑在 edit-batch.mjs——2026-09-01 拆出,500 行硬限,先例 git-ext.mjs)
|
|
254
|
+
if (args.edits) return applyEditBatch(args, ctx)
|
|
293
255
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
throw new Error("old_string must not be empty (empty string matches everywhere and would corrupt the file)")
|
|
298
|
-
}
|
|
299
|
-
const raw = await readFile(abs, "utf8")
|
|
300
|
-
const content = normalizeEOL(raw)
|
|
301
|
-
const occurrences = content.split(args.old_string).length - 1
|
|
302
|
-
if (occurrences === 0) {
|
|
303
|
-
// Give clues to help the model locate: first-line preview + common causes
|
|
304
|
-
const preview = args.old_string.slice(0, 100).split("\n")[0]
|
|
305
|
-
// Similarity candidates (LCS, line-level, top 3, score ≥ 0.5) — turns the
|
|
306
|
-
// "not found" black box into a pointer at the most likely intended line.
|
|
307
|
-
// Multi-line old_string: only its first line is scored (marked accordingly).
|
|
308
|
-
const cands = findCandidates(content.split("\n"), args.old_string)
|
|
309
|
-
let candText = ""
|
|
310
|
-
if (cands.length > 0) {
|
|
311
|
-
const header = args.old_string.includes("\n")
|
|
312
|
-
? ` similar lines (old_string line 1: "${args.old_string.split("\n")[0].slice(0, 80)}"):`
|
|
313
|
-
: " similar lines:"
|
|
314
|
-
candText = "\n" + header + "\n" + cands.map((c) => ` L${c.line}: ${c.preview} (${Math.round(c.score * 100)}%)`).join("\n")
|
|
315
|
-
}
|
|
316
|
-
throw new Error(
|
|
317
|
-
`old_string not found in ${args.path}\n` +
|
|
318
|
-
` searched: "${preview}${args.old_string.length > 100 ? "…" : ""}"\n` +
|
|
319
|
-
(lastWriteOf(abs)?.type === "write"
|
|
320
|
-
? ` hints: this file was modified since your last read (write 全文重写后内容全变) — re-read it to refresh your copy of the content, then retry\n`
|
|
321
|
-
: isDirty(abs)
|
|
322
|
-
? ` 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`
|
|
323
|
-
: ` hints: whitespace mismatch? file already changed? try reading the file first\n`) +
|
|
324
|
-
candText
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
if (occurrences > 1 && !args.replace_all) {
|
|
328
|
-
throw new Error(`old_string matches ${occurrences} times in ${args.path}; provide more context or set replace_all`)
|
|
329
|
-
}
|
|
330
|
-
const updated = args.replace_all
|
|
331
|
-
? content.split(args.old_string).join(args.new_string)
|
|
332
|
-
// Functional replacement: avoid $-substitution patterns in new_string (match string / backreference) being expanded
|
|
333
|
-
: content.replace(args.old_string, () => args.new_string)
|
|
334
|
-
// Write back in the file's ORIGINAL EOL style (first-newline rule) — a CRLF
|
|
335
|
-
// file must not come back as LF (that rewrites every line in the diff).
|
|
336
|
-
// normalizeEOL first: new_string may carry \r\n (e.g. pasted from a raw CRLF
|
|
337
|
-
// read); without normalizing, split leaves stray \r and CRLF join makes \r\r\n.
|
|
338
|
-
await writeFile(abs, joinWithEol(normalizeEOL(updated).split("\n"), raw), "utf8")
|
|
339
|
-
// 2026-08-31 工具顺手度:记录受影响区(替换首行 + 行数差)——insert_after 精确判定
|
|
340
|
-
const matchIdx = content.indexOf(args.old_string)
|
|
341
|
-
const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
|
|
342
|
-
const lineShift = args.new_string.split("\n").length - args.old_string.split("\n").length
|
|
343
|
-
recordWrite(abs, { type: "edit", startLine: editStartLine, shift: lineShift })
|
|
344
|
-
const diff = gitDiffOne(ctx.cwd, abs)
|
|
345
|
-
const baseResult = `Edited ${args.path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s)${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
|
|
346
|
-
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)
|
|
347
259
|
},
|
|
348
260
|
}
|
|
349
261
|
|
|
@@ -491,7 +403,7 @@ export const hashlineEditTool = {
|
|
|
491
403
|
const hashDump = fileHashes.slice(0, maxShow).map((h, i) => `${h} L${i + 1}: ${lines[i].slice(0, 80)}`).join("\n")
|
|
492
404
|
const preview = target.join(" ")
|
|
493
405
|
throw new Error(
|
|
494
|
-
`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` +
|
|
495
407
|
`The file may have been modified since you last read it. Current hashes (first ${maxShow} lines):\n${hashDump}` +
|
|
496
408
|
(corrupted ? `\n${FFFD_WARNING}` : "")
|
|
497
409
|
)
|
package/src/tools/file_ops.md
CHANGED
|
@@ -11,6 +11,7 @@ Parameters:
|
|
|
11
11
|
- dest (required): destination path
|
|
12
12
|
|
|
13
13
|
Notes:
|
|
14
|
-
- Paths
|
|
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).
|
package/src/tools/git.md
CHANGED
|
@@ -36,7 +36,7 @@ Run a git command. Only works inside a git repository.
|
|
|
36
36
|
|
|
37
37
|
Parameters:
|
|
38
38
|
- action (required): diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote / clone / init / rebase / remote / clean / switch / apply / worktree / archive / blame / mv
|
|
39
|
-
- workdir: run git in this
|
|
39
|
+
- workdir: run git in this subdirectory (monorepo / multi-repo). Path relative to cwd — no directory restriction. Default: cwd
|
|
40
40
|
- config: (network actions push/fetch/pull/ls-remote/clone) git -c overrides, e.g. ["http.proxy=http://10.2.2.112:3128"] for blocked remotes
|
|
41
41
|
- path: (diff/log/add/commit/checkout/restore/rm/apply/archive/blame/mv/worktree) file or directory to scope / stage / restore
|
|
42
42
|
- ref: (show/diff/checkout/reset/revert/merge/cherry-pick/tag:create/branch:create/rebase/worktree:add/archive) commit/branch/ref; (push/pull/fetch) the branch or tag (space-separated for multiple)
|
package/src/tools/git.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
runGit
|
|
5
5
|
} from "./shared.mjs";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
|
-
import { resolve
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
8
|
import { filterLines, runGitStrict, validateRef, gitConfigArgs, snapshotBefore, executeExtAction } from "./git-ext.mjs";
|
|
9
9
|
import { executeCheckpointAction } from "./git-checkpoint.mjs";
|
|
10
10
|
|
|
@@ -23,20 +23,12 @@ function runGitRaw(cwd, cmdArgs, config = []) {
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
const rel = relative(root, abs)
|
|
30
|
-
if (isAbsolute(rel)) return false
|
|
31
|
-
return rel !== ".." && !rel.startsWith(".." + sep)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Resolve workdir relative to cwd, asserting it stays within the workspace. */
|
|
26
|
+
/** Resolve workdir relative to cwd — no boundary assertion
|
|
27
|
+
* (§10.1 2026-09-02: workspace confinement removed; git itself is not
|
|
28
|
+
* directory-limited — same boundary as bash). */
|
|
35
29
|
function resolveBaseDir(cwd, workdir) {
|
|
36
30
|
if (!workdir || typeof workdir !== "string") return cwd
|
|
37
|
-
|
|
38
|
-
if (!isInside(cwd, abs)) throw new Error(`workdir escapes the workspace: ${workdir}`)
|
|
39
|
-
return abs
|
|
31
|
+
return resolve(cwd, workdir)
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
|
|
@@ -59,7 +51,7 @@ export const gitTool = {
|
|
|
59
51
|
// write-op params
|
|
60
52
|
name: { type: "string", description: "(branch/tag) The branch or tag name (create/delete/switch)" },
|
|
61
53
|
remote: { type: "string", description: "(push/fetch/pull) Remote name (e.g. origin). Default: current upstream" },
|
|
62
|
-
workdir: { type: "string", description: "Run git in this
|
|
54
|
+
workdir: { type: "string", description: "Run git in this subdirectory (monorepo / multi-repo). Path relative to cwd — no directory restriction. Default: cwd" },
|
|
63
55
|
config: { type: "array", items: { type: "string" }, description: "(network actions: push/fetch/pull/ls-remote) git -c overrides, e.g. [\"http.proxy=http://10.2.2.112:3128\"] for blocked remotes" },
|
|
64
56
|
tags: { type: "boolean", description: "(push) Also push all tags (--tags)" },
|
|
65
57
|
mode: { type: "string", enum: ["soft", "mixed", "hard"], description: "(reset) reset mode — hard snapshots the tree first + needs confirmation(操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
@@ -82,8 +74,8 @@ export const gitTool = {
|
|
|
82
74
|
},
|
|
83
75
|
readonly: false,
|
|
84
76
|
async execute(args, ctx) {
|
|
85
|
-
// workdir: run git in a
|
|
86
|
-
// every action + snapshotBefore + checkpoint resolves against the workdir
|
|
77
|
+
// workdir: run git in a subdirectory (monorepo / multi-repo). Shadow ctx.cwd so
|
|
78
|
+
// every action + snapshotBefore + checkpoint resolves against the workdir.
|
|
87
79
|
if (args.workdir) ctx = { ...ctx, cwd: resolveBaseDir(ctx.cwd, args.workdir) }
|
|
88
80
|
// git -c overrides (proxy etc.) — only network actions need them; passing to every
|
|
89
81
|
// action would be harmless but noisy. cfgArgs stays [] for local ops.
|
|
@@ -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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
Run the appropriate linter/checker for a file. Auto-detects based on file extension and project config.
|
|
2
2
|
Without 'full', runs a fast node --check (JS/TS syntax only, catches parse errors in milliseconds).
|
|
3
|
-
With 'full', runs the language-aware cascade:
|
|
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/linter.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DESC, resolveInCwd } from "./shared.mjs"
|
|
2
2
|
import { execFileSync } from "node:child_process"
|
|
3
3
|
import { existsSync } from "node:fs"
|
|
4
|
-
import { join
|
|
4
|
+
import { join } from "node:path"
|
|
5
5
|
|
|
6
6
|
export const lintTool = {
|
|
7
7
|
name: "lint",
|
|
@@ -26,7 +26,8 @@ export const lintTool = {
|
|
|
26
26
|
return nodeCheckResult(abs)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Full cascade: language-aware (
|
|
29
|
+
// Full cascade: language-aware (tsc → node --check, ruff, cargo, go vet —
|
|
30
|
+
// third-party linter cascade removed 2026-09-02, TOOLS.md §10.2: zero-dependency lint)
|
|
30
31
|
const ext = abs.split(".").pop()?.toLowerCase()
|
|
31
32
|
const checkers = LANG_CHECKERS[ext]
|
|
32
33
|
if (!checkers) return nodeCheckResult(abs) // fall back to node --check
|
|
@@ -54,33 +55,7 @@ function nodeCheckResult(abs) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
// ─── Full-check cascade checkers
|
|
58
|
-
|
|
59
|
-
async function eslintCheck(file, { cwd, existsSync, execFileSync, join, relative }) {
|
|
60
|
-
let dir = file.split(/[\\/]/).slice(0, -1).join("/") || "."
|
|
61
|
-
while (true) {
|
|
62
|
-
for (const cfg of [".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yaml", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs"]) {
|
|
63
|
-
if (existsSync(join(cwd, dir, cfg))) {
|
|
64
|
-
try {
|
|
65
|
-
const cfgDir = join(cwd, dir)
|
|
66
|
-
const relPath = relative(cfgDir, file)
|
|
67
|
-
execFileSync("npx", ["eslint", "--no-color", "--format", "compact", relPath], {
|
|
68
|
-
cwd: cfgDir, encoding: "utf8", timeout: 30000, stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
-
})
|
|
70
|
-
return "✓ eslint: no issues"
|
|
71
|
-
} catch (e) {
|
|
72
|
-
const stdout = (e.stdout || "").trim()
|
|
73
|
-
if (stdout) return stdout
|
|
74
|
-
return `✗ eslint: ${(e.stderr || e.message).slice(0, 500)}`
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const parent = dir.split("/").slice(0, -1).join("/")
|
|
79
|
-
if (!parent || parent === dir) break
|
|
80
|
-
dir = parent
|
|
81
|
-
}
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
58
|
+
// ─── Full-check cascade checkers (third-party linter branch removed 2026-09-02, TOOLS.md §10.2) ──────
|
|
84
59
|
|
|
85
60
|
async function tscCheck(file, { cwd, existsSync, execFileSync, join }) {
|
|
86
61
|
if (!existsSync(join(cwd, "tsconfig.json"))) return null
|
|
@@ -142,14 +117,11 @@ async function goVet(file, { cwd, execFileSync }) {
|
|
|
142
117
|
}
|
|
143
118
|
|
|
144
119
|
const LANG_CHECKERS = {
|
|
145
|
-
js
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
tsx: [eslintCheck, tscCheck],
|
|
151
|
-
mts: [eslintCheck, tscCheck],
|
|
152
|
-
cts: [eslintCheck, tscCheck],
|
|
120
|
+
// js/mjs/cjs/jsx fall back to node --check (no entry — third-party linter cascade removed 2026-09-02, TOOLS.md §10.2)
|
|
121
|
+
ts: [tscCheck],
|
|
122
|
+
tsx: [tscCheck],
|
|
123
|
+
mts: [tscCheck],
|
|
124
|
+
cts: [tscCheck],
|
|
153
125
|
py: [ruffCheck],
|
|
154
126
|
rs: [cargoCheck],
|
|
155
127
|
go: [goVet],
|
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; --- / +++ 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
|
},
|