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/provider/core.mjs
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { providerSpec, resolveEnableThinking } from "../config.mjs"
|
|
8
8
|
import { proxyFetch } from "../proxy.mjs"
|
|
9
|
-
import { escapeMessages } from "../escape.mjs"
|
|
9
|
+
import { escapeMessages, stripLocalMessageFields } from "../escape.mjs"
|
|
10
|
+
import { logEvent, errText, classifyErr, headText } from "../log.mjs"
|
|
11
|
+
import { recordChatTrace } from "../traces/trace-store.mjs"
|
|
10
12
|
import { readSSE } from "./sse.mjs"
|
|
11
13
|
export { readSSE } from "./sse.mjs"
|
|
12
14
|
import {
|
|
@@ -71,13 +73,60 @@ export function effectiveFetchTimeoutMs(provider) {
|
|
|
71
73
|
return Number.isFinite(provider?.fetchTimeoutMs) && provider.fetchTimeoutMs > 0 ? provider.fetchTimeoutMs : 600_000
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
export async function chat(provider,
|
|
76
|
+
export async function chat(provider, opts = {}) {
|
|
77
|
+
// LOGGING(docs/design/LOGGING.md):llm:* 事件统一在此落点——所有 chat 调用
|
|
78
|
+
// (主回合/消化轮/compress/distill/advisor/子代理/auto-think/consult)都经本函数,
|
|
79
|
+
// 格式分派(anthropic/google/responses)在内部——单点覆盖即 llm:* 全覆盖。
|
|
80
|
+
// 续写/重试各自为独立 HTTP 请求——续写递归(下方 chatImpl 内)会再包一层(嵌套
|
|
81
|
+
// llm:start/done 对——每请求一事件);重试在 requestWithRetry 内部不可见。
|
|
82
|
+
// §18.6 完整轨迹存档(AGENT-LOOP.md §18.6 N-TR2——权威句 D-TR1):采集点唯一=
|
|
83
|
+
// 本函数出口——所有 chat 调用(主回合/消化轮/compress/distill/advisor/子代理/
|
|
84
|
+
// auto-think/consult)都经本函数;续写/重试在出口已合并——reasoning 全量才完整。
|
|
85
|
+
const logCtx = opts.logCtx ?? {}
|
|
86
|
+
const t0 = Date.now()
|
|
87
|
+
const pname = provider?.name ?? provider?.model ?? "unknown"
|
|
88
|
+
logEvent("llm:start", { provider: pname, model: provider?.model ?? "", stage: logCtx.stage, turn: logCtx.turn, auto: logCtx.auto === true, child: logCtx.child })
|
|
89
|
+
try {
|
|
90
|
+
const result = await chatImpl(provider, opts)
|
|
91
|
+
logEvent("llm:done", {
|
|
92
|
+
provider: pname, model: provider?.model ?? "",
|
|
93
|
+
ms: Date.now() - t0,
|
|
94
|
+
stage: logCtx.stage, turn: logCtx.turn, auto: logCtx.auto === true, child: logCtx.child,
|
|
95
|
+
head: headText(result?.content ?? "", 300, { paragraph: true }),
|
|
96
|
+
len: String(result?.content ?? "").length,
|
|
97
|
+
finish: result?.finishReason ?? null,
|
|
98
|
+
tools: Array.isArray(result?.toolCalls) ? result.toolCalls.length : 0,
|
|
99
|
+
})
|
|
100
|
+
// §18.6 D-TR1/D-TR5:出口收集——成功路径轨迹(含 content/reasoning 全文/toolCalls)
|
|
101
|
+
recordChatTrace(provider, opts, result, null)
|
|
102
|
+
return result
|
|
103
|
+
} catch (e) {
|
|
104
|
+
logEvent("llm:error", {
|
|
105
|
+
provider: pname, model: provider?.model ?? "",
|
|
106
|
+
ms: Date.now() - t0,
|
|
107
|
+
stage: logCtx.stage, turn: logCtx.turn, auto: logCtx.auto === true, child: logCtx.child,
|
|
108
|
+
err: errText(e, 200),
|
|
109
|
+
kind: classifyErr(e, opts.signal),
|
|
110
|
+
})
|
|
111
|
+
// §18.6 D-TR5:失败路径也落盘——error(errText 截断 + 类别)+ finishReason:null
|
|
112
|
+
recordChatTrace(provider, opts, null, e)
|
|
113
|
+
throw e
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** chat 本体(LOG(LLM) 事件包装之外——见上方 chat 包装器)。 */
|
|
118
|
+
async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWait, signal, streamRules, firedPatterns, toolChoice, parallelToolCalls, logCtx }) {
|
|
75
119
|
// Sanitize BEFORE format dispatch — image poisoning bricks anthropic/google sessions
|
|
76
120
|
// the same way it bricks OpenAI-format ones (all raster-only).
|
|
77
121
|
// providerSpec: spec with the provider-level context override (PROVIDER.md §15) — the
|
|
78
122
|
// window/clamping logic below reads the overridden value where it matters.
|
|
79
123
|
const spec = providerSpec(provider)
|
|
80
124
|
messages = stripImagesForTextModel(messages, spec)
|
|
125
|
+
// SESSION.md §9 T-S3: local-only message fields (ts/transient) never reach the wire.
|
|
126
|
+
// Stripped BEFORE format dispatch — anthropic/responses transports pass whole message
|
|
127
|
+
// objects through verbatim (only the OpenAI path ran escapeMessages). Copy-on-write:
|
|
128
|
+
// history keeps the fields, the request never sees them.
|
|
129
|
+
messages = stripLocalMessageFields(messages)
|
|
81
130
|
const _debugBeforeLen = process.env.THIN_DEBUG_BODY ? JSON.stringify(messages).length : 0
|
|
82
131
|
|
|
83
132
|
// Format dispatch: delegate to non-OpenAI transports
|
|
@@ -211,6 +260,11 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
211
260
|
onReasoning,
|
|
212
261
|
onWait,
|
|
213
262
|
signal,
|
|
263
|
+
// §18.6:续写是同一逻辑调用的子请求——logCtx 原样透传(元数据与门控
|
|
264
|
+
// traces.enabled 对续写调用同样生效,不在出口静默越过开关)
|
|
265
|
+
// fix round1(D-TR1):续写子请求标记 isContinuation:true(T-TR14——true =
|
|
266
|
+
// 该调用是续写链的一环;外层新调用 false)——分析"纠结"时区分续写/重试链。
|
|
267
|
+
logCtx: { ...logCtx, isContinuation: true },
|
|
214
268
|
})
|
|
215
269
|
} catch (error) {
|
|
216
270
|
// §14.3 失败可见性:续写失败注入 _warnings(agent 机读线可见)不整轮飞出;AbortError 用户中断透传
|
package/src/tools/apply_patch.md
CHANGED
|
@@ -3,9 +3,11 @@ Apply a unified diff to one or more files, atomically: if any hunk fails to appl
|
|
|
3
3
|
**Use it for whole-file and multi-file changes:** creating MULTIPLE new files at once (`--- /dev/null` header per file), whole-file replacement, and cross-file refactors — one unified-diff call covers the whole change. A batched call is one permission ask and one turn instead of N separate calls.
|
|
4
4
|
|
|
5
5
|
Parameters:
|
|
6
|
-
- patch (required): Unified diff text. One `--- a/path` / `+++ b/path` header pair per file, then `@@ -old,count +new,count @@` hunks. Use `--- /dev/null` to create a new file.
|
|
6
|
+
- patch (required): Unified diff text. One `--- a/path` / `+++ b/path` header pair per file, then `@@ -old,count +new,count @@` hunks. Use `--- /dev/null` to create a new file. 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`).
|
|
7
7
|
|
|
8
8
|
Notes:
|
|
9
|
+
- Hunk header "@@" without coordinates is 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. The anchor-free forms require context: a hunk with no removed (-) lines (pure additions) needs at least 2 context lines for a unique match; a zero/one-context hunk with at least one removed (-) line is located by its anchor sequence (context + removed lines, in order) and applies on a unique match.
|
|
10
|
+
- Returns the applied-file summary (`Applied patch to N file(s)` plus per-file change lines and syntax-check notes for changed .mjs files)
|
|
9
11
|
- Use this for multi-file changes (e.g. rename an interface + update all callers) — one call, all-or-nothing
|
|
10
12
|
- Hunks are located by their context/removed lines, not line numbers — but the context must match the file EXACTLY. Read the files first and generate the patch from actual content
|
|
11
13
|
- If a hunk's context matches multiple locations it is rejected — add more surrounding context lines
|
package/src/tools/bash.md
CHANGED
|
@@ -5,7 +5,7 @@ Execute a shell command and return stdout+stderr. Use for running commands, buil
|
|
|
5
5
|
- `ls` / `dir` → `ls`
|
|
6
6
|
- `find` / glob search → `glob`
|
|
7
7
|
- `grep` / `rg` → `grep`
|
|
8
|
-
- `echo >` / `sed` / `printf >` / `cat << EOF` → `write` / `edit` / `hashline_edit` / `apply_patch` (
|
|
8
|
+
- `echo >` / `sed` / `printf >` / `cat << EOF` → `write` / `edit` / `hashline_edit` / `apply_patch` (preferred: write tools handle encoding/EOL correctly)
|
|
9
9
|
- `git diff` / `git status` / `git log` → `git` tool
|
|
10
10
|
|
|
11
11
|
Parameters:
|
package/src/tools/delete.md
CHANGED
package/src/tools/edit-batch.mjs
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* edit-batch.mjs — edit 工具的数组形态(edits: [...]):一次多文件原子替换。
|
|
3
|
-
* (2026-08-31 工具顺手度 §9 ②;2026-09-01 缺陷修复"同文件多条串行累积"
|
|
4
|
-
*
|
|
3
|
+
* (2026-08-31 工具顺手度 §9 ②;2026-09-01 缺陷修复"同文件多条串行累积";
|
|
4
|
+
* 2026-09-04 TOOLS.md §15 D15.1——条目级判定+应用迁至 edit-diff.mjs——批量调共用。)
|
|
5
5
|
*
|
|
6
6
|
* 语义:同一 path 的多条编辑按序**串行累积应用**——第 n 条基于前 n-1 条已应用后的
|
|
7
7
|
* 累积内容做匹配与替换;跨 path 条目互不影响(并行原子语义);任一条失败 →
|
|
8
|
-
*
|
|
8
|
+
* 全不写(原子性保留)。每条目独立按判定序(§15.2 分支 0 单行替换 / 零重叠→插入 / LCS /
|
|
9
|
+
* 空 new 显式报错)。顶层 path(args.path)为无自带 path 条目的默认(2026-09-05 用户裁定
|
|
10
|
+
* ——条目自带 path 优先——见 TOOLS.md D15.3#9 修订注)。
|
|
9
11
|
*/
|
|
10
12
|
import { readFile, writeFile } from "node:fs/promises"
|
|
11
13
|
import { resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck } from "./shared.mjs"
|
|
12
14
|
// file.mjs ↔ edit-batch.mjs 循环引用:两侧导入的都是函数声明(提升初始化),
|
|
13
15
|
// 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
|
|
14
16
|
import { recordWrite, appendWriteContext } from "./file.mjs"
|
|
17
|
+
// TOOLS.md §15 D15.1:批量条目判定+应用共用 edit-diff(§15.2 分支 0 单行替换 + 行级 LCS——零重叠→插入);
|
|
18
|
+
// D15.3#9:edits 互斥错误文本随前置校验分支迁出至 edit-diff.mjs。
|
|
19
|
+
import { assertEditArgsExclusive, validateEditEntry, computeEditEntry } from "./edit-diff.mjs"
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Apply the `edits` array form: multi-file atomic replacement. Throws on any
|
|
@@ -21,9 +26,7 @@ export async function applyEditBatch(args, ctx) {
|
|
|
21
26
|
if (!Array.isArray(args.edits) || args.edits.length === 0) {
|
|
22
27
|
throw new Error("edits must be a non-empty array of {path, old_string, new_string}")
|
|
23
28
|
}
|
|
24
|
-
|
|
25
|
-
throw new Error("edits array is mutually exclusive with path/old_string/new_string")
|
|
26
|
-
}
|
|
29
|
+
assertEditArgsExclusive(args)
|
|
27
30
|
// 原子:先全量检查(所有文件的替换都可执行)——任一失败全不写。
|
|
28
31
|
// 2026-09-01 缺陷修复(TOOLS.md §9 ②"同文件多条规则"):同一 path 的多条编辑
|
|
29
32
|
// 按序**串行累积应用**——第 n 条基于前 n-1 条已应用后的累积内容做匹配与替换
|
|
@@ -31,55 +34,40 @@ export async function applyEditBatch(args, ctx) {
|
|
|
31
34
|
// 除最后一条外全部静默丢失);跨 path 条目互不影响(并行原子语义不变)。
|
|
32
35
|
const groups = new Map() // abs → 每文件一条流水线
|
|
33
36
|
for (const e of args.edits) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
const abs = resolveInCwd(ctx, e.path)
|
|
37
|
+
// 2026-09-05 用户裁定:顶层 path + edits 并存合法化——`e.path || args.path`(条目优先;
|
|
38
|
+
// 缺省入口仅在此兜底);两者皆无 → 原错误文本(措辞补顶层选项)。
|
|
39
|
+
const p = e.path || args.path
|
|
40
|
+
if (!p) throw new Error("each edit must have a path — give each entry its own path or pass a top-level path")
|
|
41
|
+
// 前置校验(空 old / 非字符串 new——文本同 edit-diff 批量形态;读盘前校验)
|
|
42
|
+
validateEditEntry(e, { label: `edit for ${p}: `, rich: false })
|
|
43
|
+
const abs = resolveInCwd(ctx, p)
|
|
43
44
|
let g = groups.get(abs)
|
|
44
45
|
if (!g) {
|
|
45
46
|
const raw = await readFile(abs, "utf8")
|
|
46
|
-
g = { abs, path:
|
|
47
|
+
g = { abs, path: p, raw, content: normalizeEOL(raw), edits: [] }
|
|
47
48
|
groups.set(abs, g)
|
|
48
49
|
}
|
|
49
50
|
g.edits.push(e)
|
|
50
51
|
}
|
|
51
|
-
const prepared = [] //
|
|
52
|
+
const prepared = [] // 每条目一条回显;同文件内按 args 序,跨文件按首次出现分组序(应用/回显语义均正确)
|
|
52
53
|
for (const g of groups.values()) {
|
|
53
54
|
g.netShift = 0 // 组内行数差累积(合并快照的 shift = 全组净漂移)
|
|
54
55
|
for (const e of g.edits) {
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
if (occurrences > 1 && !e.replace_all) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`edit aborted (atomic — no files written): old_string matches ${occurrences} times in ${g.path}; ` +
|
|
66
|
-
`provide more context or set replace_all`
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
const matchIdx = content.indexOf(e.old_string)
|
|
70
|
-
const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
|
|
71
|
-
const lineShift = e.new_string.split("\n").length - e.old_string.split("\n").length
|
|
72
|
-
const updated = e.replace_all
|
|
73
|
-
? content.split(e.old_string).join(e.new_string)
|
|
74
|
-
: content.replace(e.old_string, () => e.new_string)
|
|
56
|
+
// 条目级判定+应用(edit-diff——§15.2 分支 0 单行替换 + 判定序 1/2/3 + 空 new 显式报错 + >1000 行报错)
|
|
57
|
+
const out = computeEditEntry(g.content, e, {
|
|
58
|
+
path: g.path,
|
|
59
|
+
absPath: g.abs,
|
|
60
|
+
abortPrefix: "edit aborted (atomic — no files written): ",
|
|
61
|
+
})
|
|
75
62
|
prepared.push({
|
|
76
63
|
g,
|
|
77
|
-
editStartLine, // 基于累积内容计算——已天然计入前面条目的行偏移,不再累加
|
|
78
|
-
lineShift,
|
|
79
|
-
occurrences:
|
|
64
|
+
editStartLine: out.editStartLine, // 基于累积内容计算——已天然计入前面条目的行偏移,不再累加
|
|
65
|
+
lineShift: out.lineShift,
|
|
66
|
+
occurrences: out.occurrences,
|
|
67
|
+
note: out.note ?? null, // P15.11——空白差异自动落点标记(成功消息追加)
|
|
80
68
|
})
|
|
81
|
-
g.content = updated // 串行累积:下一条基于本条应用后的内容
|
|
82
|
-
g.netShift += lineShift
|
|
69
|
+
g.content = out.updated // 串行累积:下一条基于本条应用后的内容
|
|
70
|
+
g.netShift += out.lineShift
|
|
83
71
|
}
|
|
84
72
|
}
|
|
85
73
|
// 全部检查通过——每文件一次写盘(同文件多条:写入串行累积后的最终内容);
|
|
@@ -97,7 +85,7 @@ export async function applyEditBatch(args, ctx) {
|
|
|
97
85
|
// #4(2026-09-01 交付评审尾巴):与单文件路径对齐——每条结果附 git diff +
|
|
98
86
|
// autoSyntaxCheck(同文件多条会重复 diff/检查,换取格式一致、实现零分支)
|
|
99
87
|
const diff = gitDiffOne(ctx.cwd, p.g.abs)
|
|
100
|
-
const base = `Edited ${p.g.path}: replaced ${p.occurrences} occurrence(s)${diff ? "\n" + diff : ""}${await autoSyntaxCheck(p.g.abs)}`
|
|
88
|
+
const base = `Edited ${p.g.path}: replaced ${p.occurrences} occurrence(s)${p.note ? ` — ${p.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(p.g.abs)}`
|
|
101
89
|
results.push(await appendWriteContext(p.g.abs, p.editStartLine, base))
|
|
102
90
|
}
|
|
103
91
|
return results.join("\n")
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edit-diff.mjs — edit 工具的行级 diff 内核(TOOLS.md §15 D15.1,2026-09-04)。
|
|
3
|
+
*
|
|
4
|
+
* edit 新语义(用户裁定——breaking——不承诺旧行为兼容):old_string = 变化区**当前内容**
|
|
5
|
+
* (必须精确存在、单次匹配——不变);new_string = 该区的**期望结果**。判定序:
|
|
6
|
+
* 0. 分支 0(TOOLS.md §15.2——单行精确替换):old 单行 && new 单行 && old 全文唯一 &&
|
|
7
|
+
* new 非空 → **就地替换该行**(行数不变——EOL 由调用方 joinWithEol 恢复);
|
|
8
|
+
* 1. 零重叠(old 每一行都不出现在 new 行集中)→ 按**插入**——new 整体插入在 old
|
|
9
|
+
* 最后一行之后(旧内容保留——数据零丢失);
|
|
10
|
+
* 2. 有公共行(≥1)→ 行级 LCS——公共行保留、差异行增删;
|
|
11
|
+
* 3. new 与 old 行级完全一致 → 原样替换(no-op 语义——仍报成功)。
|
|
12
|
+
* 空 new_string(纯删除意图)→ 显式报错(不静默——先于分支 0——单行替换永不成删除);
|
|
13
|
+
* 分支 0 只在 computeEditEntry 条目判定层(壳/桥/批量自动继承)——applyPatchLines
|
|
14
|
+
* (纯 diff 层)语义不动。old/new 行数各上限 1000(超限报错)。
|
|
15
|
+
*
|
|
16
|
+
* 行尾权威 = EDIT-TOOL-EOL-DESIGN.md:判定/应用在 normalizeEOL 后的 LF 域计算,
|
|
17
|
+
* 写回由调用方 joinWithEol(原文) 恢复原行尾。
|
|
18
|
+
* 模块拆分(file.mjs ≤500 硬限):file.mjs 只留工具壳与转发——单形态执行体与前置校验
|
|
19
|
+
* 分支整段迁出至本模块(模块拆分写优先纪律:先迁后删、逻辑体不变、wiring 导入)。
|
|
20
|
+
*/
|
|
21
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
22
|
+
import {
|
|
23
|
+
resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck, findCandidates,
|
|
24
|
+
} from "./shared.mjs"
|
|
25
|
+
// file.mjs ↔ edit-diff.mjs 循环引用:两侧导入的都是函数声明(提升初始化),
|
|
26
|
+
// 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
|
|
27
|
+
import { recordWrite, appendWriteContext, lastWriteOf, isDirty } from "./file.mjs"
|
|
28
|
+
|
|
29
|
+
export const MAX_DIFF_LINES = 1000
|
|
30
|
+
export const REGION_TOO_LARGE = "edit region too large — narrow the change"
|
|
31
|
+
export const EMPTY_NEW_STRING = "empty new_string — for deletion, keep the context lines you want to preserve in both old_string and new_string"
|
|
32
|
+
export const EDIT_ARGS_MUTEX = "edits array is mutually exclusive with top-level old_string/new_string — a top-level path is allowed (default for entries without their own path); provide each change's old_string/new_string inside its edits entry"
|
|
33
|
+
|
|
34
|
+
/** 行切分(尾随换行终止最后一行——非额外空行):"a\nb\n" → ["a","b"]。 */
|
|
35
|
+
function splitLines(text) {
|
|
36
|
+
const lines = text.split("\n")
|
|
37
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop()
|
|
38
|
+
return lines
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 行级 LCS(整行相等判定)合并:公共行保留(LCS 序)、old 独有行删除、new 独有行按其在
|
|
43
|
+
* new 中相对公共行的位置插入。结果在 LF 域;区域尾随换行随 oldText(区域边界保持)。
|
|
44
|
+
* 返回 { ok: true, resultText } 或 { ok: false, reason }。
|
|
45
|
+
*/
|
|
46
|
+
export function applyPatchLines(oldText, newText) {
|
|
47
|
+
if (newText === "") return { ok: false, reason: EMPTY_NEW_STRING }
|
|
48
|
+
const oldLines = splitLines(oldText)
|
|
49
|
+
const newLines = splitLines(newText)
|
|
50
|
+
if (oldLines.length > MAX_DIFF_LINES || newLines.length > MAX_DIFF_LINES) {
|
|
51
|
+
return { ok: false, reason: REGION_TOO_LARGE }
|
|
52
|
+
}
|
|
53
|
+
// 判定 1(F15.2):零重叠 → 插入——new 整体插在 old 最后一行之后(旧内容保留)
|
|
54
|
+
const newSet = new Set(newLines)
|
|
55
|
+
const merged = oldLines.some((l) => newSet.has(l))
|
|
56
|
+
? lcsMerge(oldLines, newLines) // 判定 2/3——公共行保留、差异行增删
|
|
57
|
+
: [...oldLines, ...newLines]
|
|
58
|
+
return { ok: true, resultText: merged.join("\n") + (oldText.endsWith("\n") ? "\n" : "") }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** LCS 推导(DP + 回溯)——整行相等判定;结果 = 公共行 + 差异行增删的合并序列。 */
|
|
62
|
+
function lcsMerge(a, b) {
|
|
63
|
+
const n = a.length
|
|
64
|
+
const m = b.length
|
|
65
|
+
const width = m + 1
|
|
66
|
+
const dp = new Uint16Array((n + 1) * width)
|
|
67
|
+
for (let i = 1; i <= n; i++) {
|
|
68
|
+
for (let j = 1; j <= m; j++) {
|
|
69
|
+
dp[i * width + j] = a[i - 1] === b[j - 1]
|
|
70
|
+
? dp[(i - 1) * width + (j - 1)] + 1
|
|
71
|
+
: Math.max(dp[(i - 1) * width + j], dp[i * width + (j - 1)])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const merged = []
|
|
75
|
+
let i = n
|
|
76
|
+
let j = m
|
|
77
|
+
while (i > 0 && j > 0) {
|
|
78
|
+
if (a[i - 1] === b[j - 1]) {
|
|
79
|
+
merged.push(a[i - 1])
|
|
80
|
+
i--
|
|
81
|
+
j--
|
|
82
|
+
} else if (dp[(i - 1) * width + j] >= dp[i * width + (j - 1)]) {
|
|
83
|
+
i-- // old 独有行——删除
|
|
84
|
+
} else {
|
|
85
|
+
merged.push(b[j - 1]) // new 独有行——按 LCS 相对位置插入
|
|
86
|
+
j--
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
while (j > 0) { merged.push(b[j - 1]); j-- }
|
|
90
|
+
return merged.reverse()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* D15.3#9 修订(2026-09-05 用户裁定——顶层 path + edits 并存合法化:顶层 path = 无自带
|
|
95
|
+
* path 条目的默认——模型直觉形态「顶层 path + 数组条目」不再拒绝;条目自带 path 优先)。
|
|
96
|
+
* 互斥收窄为只对顶层 old_string/new_string——edits 下它们无批语义可解释——顶层 path 不再触发。
|
|
97
|
+
*/
|
|
98
|
+
export function assertEditArgsExclusive(args) {
|
|
99
|
+
if (args.old_string !== undefined || args.new_string !== undefined) {
|
|
100
|
+
throw new Error(EDIT_ARGS_MUTEX)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 前置校验(空 old / 非字符串 new)——error 文本按调用形态(单形态 rich / 批量 label)。
|
|
106
|
+
* opts: { label = ""(批量前缀 "edit for <path>: "), rich = true(单形态完整文本) }
|
|
107
|
+
*/
|
|
108
|
+
export function validateEditEntry(entry, opts = {}) {
|
|
109
|
+
const label = opts.label ?? ""
|
|
110
|
+
if (!entry.old_string) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
label + "old_string must not be empty" + (opts.rich === false ? "" : " (empty string matches everywhere and would corrupt the file)")
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
if (typeof entry.new_string !== "string") {
|
|
116
|
+
throw new Error(
|
|
117
|
+
label + `new_string must be a string${entry.new_string === undefined ? " (missing)" : ` (got ${typeof entry.new_string})`}` +
|
|
118
|
+
(opts.rich === false ? "" : " — nothing written")
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 分支 0 形态判定(TOOLS.md §15.2 D15.9.1):old/new 各为单行——无内部换行(splitLines
|
|
125
|
+
* 语义:尾随单个换行符是行终止而非新行——old/new 行内容均不含 \n)——且 new 非空
|
|
126
|
+
* ("" 经 splitLines 切分为 []——由下方 !== "" 守卫先拦截——空 new 不落本分支——落
|
|
127
|
+
* applyPatchLines 的空 new 显式错误——单行替换永不成删除)。多匹配与 replace_all 不落
|
|
128
|
+
* 本分支(occurrences 错误 / 字面全部先行)。
|
|
129
|
+
*/
|
|
130
|
+
function isSingleLineReplace(entry) {
|
|
131
|
+
return (
|
|
132
|
+
entry.new_string !== "" &&
|
|
133
|
+
splitLines(entry.old_string).length === 1 &&
|
|
134
|
+
splitLines(entry.new_string).length === 1
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 条目级判定+应用(单形态与批量共用——D15.1"批量条目判定+应用调用 edit-diff"):
|
|
140
|
+
* 匹配校验(精确存在——非 replace_all 单次)→ 按判定序应用(分支 0 单行替换 / 零重叠插入 /
|
|
141
|
+
* LCS 替换 / replace_all 字面)→
|
|
142
|
+
* 元数据(受影响区首行/行数差/次数——recordWrite 与结果回显用)。
|
|
143
|
+
* 返回 { updated, editStartLine, lineShift, occurrences };失败抛错(含路径/引导)。
|
|
144
|
+
* opts: { path, abortPrefix(批量 "edit aborted (atomic — no files written): ") }
|
|
145
|
+
*/
|
|
146
|
+
/**
|
|
147
|
+
* P15.11(2026-09-05 用户裁定——edit 连续失败分析):old_string 逐字 not-found 时——
|
|
148
|
+
* 若文件中存在**唯一**窗口:行数与 old 相同、逐行 trim() 相等(内容零差异——差异仅前导/
|
|
149
|
+
* 尾随空白——EOL 已在 normalize 域消除)→ 返回 { actual }(actual = 文件窗口原文);
|
|
150
|
+
* 多窗口(两处 trim 同内容不同空白)→ null(歧义不猜——报错引导);old 含尾换行 → null
|
|
151
|
+
* (终止符语义边界——不做窗口猜测)。模型从 read 记忆拷贝 old_string 时丢/加前导空格是
|
|
152
|
+
* 纯机械损耗(本轮 12 次 edit 失败中 8 次空白差异)——内容零差异时自动落点 + 结果明示。
|
|
153
|
+
*/
|
|
154
|
+
function findWhitespaceVariant(content, old) {
|
|
155
|
+
if (old.endsWith("\n")) return null
|
|
156
|
+
const oldLines = old.split("\n")
|
|
157
|
+
const fileLines = content.split("\n")
|
|
158
|
+
const m = oldLines.length
|
|
159
|
+
if (m === 0 || fileLines.length < m) return null
|
|
160
|
+
const trimmed = oldLines.map((l) => l.trim())
|
|
161
|
+
let hit = null
|
|
162
|
+
for (let i = 0; i + m <= fileLines.length; i++) {
|
|
163
|
+
let same = true
|
|
164
|
+
for (let j = 0; j < m; j++) {
|
|
165
|
+
if (fileLines[i + j].trim() !== trimmed[j]) { same = false; break }
|
|
166
|
+
}
|
|
167
|
+
if (!same) continue
|
|
168
|
+
const actual = fileLines.slice(i, i + m).join("\n")
|
|
169
|
+
if (actual === old) continue // 逐字已匹配——occurrences=0 前提下不会发生
|
|
170
|
+
if (hit) return null // 多窗口空白差异 → 歧义 → 不猜
|
|
171
|
+
hit = { actual }
|
|
172
|
+
}
|
|
173
|
+
return hit
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* P15.11 note 文案(各端成功消息追加——双端同句)
|
|
178
|
+
*/
|
|
179
|
+
export const WHITESPACE_VARIANT_NOTE = "applied to the unique whitespace-only match (content identical, leading/trailing whitespace differs from your old_string)"
|
|
180
|
+
|
|
181
|
+
export function computeEditEntry(content, entry, opts = {}) {
|
|
182
|
+
validateEditEntry(entry, { ...opts, rich: !opts.abortPrefix })
|
|
183
|
+
// P15.11:not-found 时先查唯一空白差异窗口——命中则以其原文为实际 old 继续(内容零差异
|
|
184
|
+
// ——自动落点 + note 明示);实质差异/歧义仍走下方 not-found 报错(不猜内容)。
|
|
185
|
+
let old = entry.old_string
|
|
186
|
+
let occurrences = content.split(old).length - 1
|
|
187
|
+
let note = null
|
|
188
|
+
if (occurrences === 0) {
|
|
189
|
+
const variant = findWhitespaceVariant(content, old)
|
|
190
|
+
if (variant) { old = variant.actual; occurrences = 1; note = WHITESPACE_VARIANT_NOTE }
|
|
191
|
+
}
|
|
192
|
+
if (occurrences === 0) {
|
|
193
|
+
const preview = entry.old_string.slice(0, 100).split("\n")[0]
|
|
194
|
+
const cands = findCandidates(content.split("\n"), entry.old_string)
|
|
195
|
+
let candText = ""
|
|
196
|
+
if (cands.length > 0) {
|
|
197
|
+
const header = entry.old_string.includes("\n")
|
|
198
|
+
? ` similar lines (old_string line 1: "${entry.old_string.split("\n")[0].slice(0, 80)}"):`
|
|
199
|
+
: " similar lines:"
|
|
200
|
+
candText = "\n" + header + "\n" + cands.map((c) => ` L${c.line}: ${c.preview} (${Math.round(c.score * 100)}%)`).join("\n")
|
|
201
|
+
}
|
|
202
|
+
throw new Error(
|
|
203
|
+
`${opts.abortPrefix ?? ""}old_string not found in ${opts.path ?? "file"}\n` +
|
|
204
|
+
` searched: "${preview}${entry.old_string.length > 100 ? "…" : ""}" — use grep to locate the actual content\n` +
|
|
205
|
+
(opts.absPath && lastWriteOf(opts.absPath)?.type === "write"
|
|
206
|
+
? ` hints: this file was modified since your last read (write 全文重写后内容全变) — re-read it to refresh your copy of the content, then retry\n`
|
|
207
|
+
: opts.absPath && isDirty(opts.absPath)
|
|
208
|
+
? ` 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`
|
|
209
|
+
: ` hints: whitespace mismatch? file already changed? try reading the file first\n`) +
|
|
210
|
+
candText
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
if (occurrences > 1 && !entry.replace_all) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`${opts.abortPrefix ?? ""}old_string matches ${occurrences} times in ${opts.path ?? "file"}; provide more context or set replace_all`
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
const matchIdx = content.indexOf(old)
|
|
219
|
+
const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
|
|
220
|
+
let updated
|
|
221
|
+
let resultForShift
|
|
222
|
+
if (entry.replace_all) {
|
|
223
|
+
// 字面替换——不做插入规则(old 多处时"插到哪处"无定义)——不落分支 0(§15.2 D15.9.2)
|
|
224
|
+
updated = content.split(old).join(entry.new_string)
|
|
225
|
+
resultForShift = entry.new_string
|
|
226
|
+
} else if (isSingleLineReplace(entry)) {
|
|
227
|
+
// 分支 0(§15.2 D15.9.1——单行精确替换):old 单行 && new 单行 && old 唯一匹配
|
|
228
|
+
// (occ==1 既有校验保证)&& new 非空(空 new 显式错误先于本分支——防删除)→ **就地
|
|
229
|
+
// 替换该行**——不再零重叠插入(P15.8——行尾段编辑反复踩的插入坑)。只替换行内容段:
|
|
230
|
+
// old/new 的尾随换行符属文件结构而非行内容——留在原位——行数不变——EOL 由调用方
|
|
231
|
+
// joinWithEol 恢复(normalizeEOL 后本域为 LF)。边界声明(评审 2026-09-04):new 为
|
|
232
|
+
// 行内容空串(如 new="\n"——置空行意图)时,常规带终止换行的行变空行(行数不变);
|
|
233
|
+
// 唯一例外是**无终止换行的末行**被置空——该行随文件结束自然消失(行数 −1——文件格式
|
|
234
|
+
// 固有边界——空行无法表达)——不落 EMPTY_NEW_STRING 错误路径(new 本身非空串)。
|
|
235
|
+
const oldLine = splitLines(old)[0]
|
|
236
|
+
const newLine = splitLines(entry.new_string)[0]
|
|
237
|
+
updated = content.slice(0, matchIdx) + newLine + content.slice(matchIdx + oldLine.length)
|
|
238
|
+
resultForShift = newLine
|
|
239
|
+
} else {
|
|
240
|
+
const r = applyPatchLines(old, entry.new_string)
|
|
241
|
+
if (!r.ok) throw new Error(r.reason)
|
|
242
|
+
updated = content.slice(0, matchIdx) + r.resultText + content.slice(matchIdx + old.length)
|
|
243
|
+
resultForShift = r.resultText
|
|
244
|
+
}
|
|
245
|
+
// 行数差 = 应用后区域行数 − 旧区域行数(分支 0 单行替换:0;插入:new 行数;LCS:new−old)
|
|
246
|
+
const lineShift = splitLines(resultForShift).length - splitLines(old).length
|
|
247
|
+
return { updated, editStartLine, lineShift, occurrences: entry.replace_all ? occurrences : 1, note }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 单形态 edit 执行体(自 file.mjs 整段迁出——模块拆分写优先纪律:逻辑体不变)——
|
|
252
|
+
* file.mjs 只留工具壳与转发。
|
|
253
|
+
*/
|
|
254
|
+
export async function runSingleEdit(args, ctx) {
|
|
255
|
+
const abs = resolveInCwd(ctx, args.path)
|
|
256
|
+
const raw = await readFile(abs, "utf8")
|
|
257
|
+
const content = normalizeEOL(raw)
|
|
258
|
+
const out = computeEditEntry(content, args, { path: args.path, absPath: abs })
|
|
259
|
+
// 写回按原文行尾(joinWithEol)——normalizeEOL 先行避免 \r\n 污染 split/join
|
|
260
|
+
await writeFile(abs, joinWithEol(normalizeEOL(out.updated).split("\n"), raw), "utf8")
|
|
261
|
+
recordWrite(abs, { type: "edit", startLine: out.editStartLine, shift: out.lineShift })
|
|
262
|
+
const diff = gitDiffOne(ctx.cwd, abs)
|
|
263
|
+
const baseResult = `Edited ${args.path}: replaced ${out.occurrences} occurrence(s)${out.note ? ` — ${out.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
|
|
264
|
+
return await appendWriteContext(abs, out.editStartLine, baseResult)
|
|
265
|
+
}
|
package/src/tools/edit.md
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
Edit a file
|
|
1
|
+
Edit a file as a patch. old_string is the current content of the region to change (must match exactly once); new_string is the desired result of that region. Lines shared by both are kept; lines only in new_string take their position relative to the shared lines (LCS order) — when no line overlaps, new_string is inserted after old_string (old content stays) — except a unique single-line old_string paired with a single-line new_string: that exact line is replaced in place (line count unchanged); for a multi-line replacement, include a shared context line — for adding a new line use insert_after: a unique single-line old/new pair replaces the line in place; multi-line zero-overlap pairs still insert per the diff rules above. replace_all keeps literal replacement of every occurrence — the insert rule does not apply.
|
|
2
2
|
|
|
3
3
|
**Routing — pick the right edit tool:**
|
|
4
4
|
- Precise line-targeted change → `hashline_edit` (hash-based, immune to whitespace/encoding drift — preferred)
|
|
5
|
-
-
|
|
6
|
-
- Add a function/block after a known line → `insert_after`
|
|
5
|
+
- Add a line/entry after a known line → insert_after — includes checklist items and doc lines.
|
|
7
6
|
- Same change across multiple files or many spots → `apply_patch`
|
|
8
7
|
- Rewrite an entire file → `write`
|
|
9
8
|
- Rename a symbol project-wide → `lsp` or `grep` first to map every caller
|
|
10
9
|
|
|
11
|
-
**Batch multiple edits into ONE call via the `edits` array** (preferred over N single edit calls): multiple changes to the SAME file go into one `edits` array (entries are applied serially, each based on the previous one's result); independent changes across MULTIPLE files also go into the same `edits` array — one call, atomic (any failure writes nothing). A batched call is one permission ask, one undo unit, and one turn instead of N.
|
|
10
|
+
**Batch multiple edits into ONE call via the `edits` array** (preferred over N single edit calls): multiple changes to the SAME file go into one `edits` array (entries are applied serially, each based on the previous one's result); independent changes across MULTIPLE files also go into the same `edits` array — one call, atomic (any failure writes nothing). A batched call is one permission ask, one undo unit, and one turn instead of N. A top-level path may accompany the array — entries without their own path inherit it (entry paths override).
|
|
12
11
|
|
|
13
12
|
Parameters:
|
|
14
|
-
- path (required
|
|
15
|
-
- old_string (required):
|
|
16
|
-
- new_string (required):
|
|
13
|
+
- path: File path (single form: required; with the edits array: optional — the default for entries without their own path)
|
|
14
|
+
- old_string (required): Current content of the region to change (must match exactly once)
|
|
15
|
+
- new_string (required): Desired result of the region — diffed against old_string (shared lines kept; zero overlap → new_string inserted after old_string — a unique single-line old/new pair replaces the line in place)
|
|
17
16
|
- replace_all: Replace all occurrences instead of just one (default false)
|
|
18
|
-
- edits: Array of {path
|
|
17
|
+
- edits: Array of {path?, old_string, new_string, replace_all?} entries — batch form; mutually exclusive with top-level old_string/new_string — a top-level path is allowed and applies to entries without their own path
|
|
19
18
|
|
|
20
19
|
Notes:
|
|
21
20
|
- Prefer this over write for targeted edits — it's safer and keeps changes targeted
|
|
22
21
|
- If old_string matches zero times: error. If it matches multiple times without replace_all: error — add more surrounding context to make it unique
|
|
22
|
+
- new_string empty (deletion) is an explicit error — keep the context lines you want to preserve in both old_string and new_string
|
|
23
|
+
- Returns `Edited <path>: replaced N occurrence(s)` + git diff + syntax-check note + context block (L..-L..).
|
|
23
24
|
- Never fabricate the old_string — copy it verbatim from the actual file using read first
|
|
25
|
+
- 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/execute.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Execute JavaScript — either inline `code` or a `scriptFile`. Runs in a real `node` process
|
|
1
|
+
Execute JavaScript — either inline `code` or a `scriptFile`. Runs in a real child `node` process — a pure node ESM environment: top-level `await` and dynamic `import()` are available, no globals are injected. File reads/writes/searches belong to the dedicated read/ls/glob/grep/write/edit tools — not to execute. If a script genuinely needs fs/path, `import` the `node:` module inside the code (one explicit import line).
|
|
2
2
|
|
|
3
3
|
**Route to execute instead of bash:**
|
|
4
4
|
- `node -e "…"` → execute (inline code; top-level await + import() + console all work)
|
|
@@ -6,16 +6,16 @@ Execute JavaScript — either inline `code` or a `scriptFile`. Runs in a real `n
|
|
|
6
6
|
- `node --test <file>` / `node --check <file>` → execute with scriptFile + nodeArgs
|
|
7
7
|
|
|
8
8
|
Parameters:
|
|
9
|
-
- code: JavaScript to run inline. Top-level `await` and `import('./x.mjs')` are supported.
|
|
10
|
-
- scriptFile: run a .mjs/.js file with node (self-contained —
|
|
9
|
+
- code: JavaScript to run inline. Top-level `await` and `import('./x.mjs')` are supported. Pure node ESM — no preloaded helpers; import `node:fs`/`node:path` etc. yourself when needed. File reads/writes go through the dedicated read/ls/glob/grep/write/edit tools. Use this OR scriptFile.
|
|
10
|
+
- scriptFile: 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.
|
|
11
11
|
- nodeArgs: (scriptFile) extra node flags before the script, e.g. ["--test"], ["--check"]. Eval-like flags (--eval/--input-type/--inspect) are rejected.
|
|
12
12
|
- workdir: run in this directory (relative to cwd — no directory restriction; default cwd)
|
|
13
13
|
- filter: optional — only return output lines matching this regex (case-insensitive)
|
|
14
|
-
- timeoutMs: Timeout in milliseconds (default 30000, max 600000 — covers
|
|
14
|
+
- timeoutMs: Timeout in milliseconds (default 30000, max 600000 — covers `node --test` suites and package scripts)
|
|
15
15
|
|
|
16
16
|
Notes:
|
|
17
|
-
- `console.log(...)`
|
|
18
|
-
- The prelude's readFile/writeFile/glob/grep helpers resolve paths against the working directory (helper-only guard — `require`/`process`/`import()` are full Node, same boundary as bash).
|
|
17
|
+
- `console.log(...)` prints to the result; objects are JSON-stringified where needed.
|
|
19
18
|
- A non-zero exit / thrown exception returns the stderr (error + stack) as the result.
|
|
20
|
-
- Output capped at ~50KB;
|
|
19
|
+
- Output is capped at ~50KB; when a script overruns it, an explicit `[output truncated]` marker is appended — print large results in chunks, or have the script write them to a file (node:fs) and read that file back with the `read` tool.
|
|
21
20
|
- Use `write`/`edit`/`apply_patch` for source edits. Still use `bash` for package-manager/CLI subprocesses (`npm test`/`npm publish`/`vsce`), servers, and interactive/TTY programs — execute covers in-process JS and `node <script>`/`node --test`/`node --check`, not arbitrary CLI or long-running programs.
|
|
21
|
+
- Irreversibility: it runs with full filesystem access and no automatic undo — script side effects are permanent; checkpoint (git) before risky bulk operations.
|