thincoder 0.12.62 → 0.12.63

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.
Files changed (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +25 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. package/src/traces/trace-store.mjs +0 -355
@@ -1,191 +0,0 @@
1
- /**
2
- * edit-batch.mjs — edit 工具的数组形态(edits: [...]):一次多文件原子替换。
3
- * (2026-08-31 工具顺手度 §9 ②;2026-09-01 缺陷修复"同文件多条串行累积";
4
- * 2026-09-04 EDIT.md §6——条目级判定+应用迁至 edit-diff.mjs——批量调共用。)
5
- *
6
- * 语义:同一 path 的多条编辑按序**串行累积应用**——第 n 条基于前 n-1 条已应用后的
7
- * 累积内容做匹配与替换;跨 path 条目互不影响(并行原子语义);任一条失败 →
8
- * 全不写(原子性保留)。每条目独立按判定序(EDIT.md §4——分支 0 单行替换 / 零重叠→替换即删 /
9
- * LCS / 空 new 显式报错;EDIT.md §5(约束——删行形态):省略 new_string = 删行。顶层 path
10
- * (args.path)为无自带 path 条目的默认(2026-09-05 用户裁定——条目自带 path 优先——见
11
- * EDIT.md §5 修订注)。
12
- */
13
- import { readFile, writeFile } from "node:fs/promises"
14
- import { resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck } from "./shared.mjs"
15
- // file.mjs ↔ edit-batch.mjs 循环引用:两侧导入的都是函数声明(提升初始化),
16
- // 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
17
- import { recordWrite, appendWriteContext } from "./file.mjs"
18
- // EDIT.md §6:批量条目判定+应用共用 edit-diff(EDIT.md §4 分支 0 单行替换 + 行级 LCS——零重叠→替换即删);
19
- // EDIT.md §5:edits 互斥错误文本随前置校验分支迁出至 edit-diff.mjs。
20
- import { assertEditArgsExclusive, validateEditEntry, computeEditEntry, splitLines, EMPTY_NEW_STRING_LINE, deleteTarget } from "./edit-diff.mjs"
21
-
22
- /**
23
- * Apply the `edits` array form: multi-file atomic replacement. Throws on any
24
- * failure (atomic — nothing written). Returns the per-entry result text (joined).
25
- */
26
- export async function applyEditBatch(args, ctx) {
27
- if (!Array.isArray(args.edits) || args.edits.length === 0) {
28
- throw new Error("edits must be a non-empty array of {path, old_string | line/startLine+endLine, new_string}")
29
- }
30
- assertEditArgsExclusive(args)
31
- // 原子:先全量检查(所有文件的替换都可执行)——任一失败全不写。
32
- // 2026-09-01 缺陷修复(TOOLS.md §9 ②"同文件多条规则"):同一 path 的多条编辑
33
- // 按序**串行累积应用**——第 n 条基于前 n-1 条已应用后的累积内容做匹配与替换
34
- // (原实现每条都基于盘上原始内容计算、写盘循环后置,同文件后者覆盖前者 →
35
- // 除最后一条外全部静默丢失);跨 path 条目互不影响(并行原子语义不变)。
36
- const groups = new Map() // abs → 每文件一条流水线
37
- for (const e of args.edits) {
38
- // 2026-09-05 用户裁定:顶层 path + edits 并存合法化——`e.path || args.path`(条目优先;
39
- // 缺省入口仅在此兜底);两者皆无 → 原错误文本(措辞补顶层选项)。
40
- const p = e.path || args.path
41
- if (!p) throw new Error("each edit must have a path — give each entry its own path or pass a top-level path")
42
- // 前置校验(空 old / 非字符串 new——文本同 edit-diff 批量形态;读盘前校验)
43
- validateEditEntry(e, { label: `edit for ${p}: `, rich: false })
44
- const abs = resolveInCwd(ctx, p)
45
- let g = groups.get(abs)
46
- if (!g) {
47
- const raw = await readFile(abs, "utf8")
48
- g = { abs, path: p, raw, content: normalizeEOL(raw), edits: [] }
49
- groups.set(abs, g)
50
- }
51
- g.edits.push(e)
52
- }
53
- const prepared = [] // 每条目一条回显;同文件内按 args 序,跨文件按首次出现分组序(应用/回显语义均正确)
54
- for (const g of groups.values()) {
55
- g.netShift = 0 // 组内行数差累积(合并快照的 shift = 全组净漂移)
56
- for (const e of g.edits) {
57
- // 条目级判定+应用(edit-diff——EDIT.md §4 分支 0 单行替换 + 判定序 1/2/3 + 空 new 显式报错 +
58
- // >1000 行报错(diff 形态——D1 行号条目走 applyLineEdit,不经 LCS,无此上限))
59
- const out = computeEditEntry(g.content, e, {
60
- path: g.path,
61
- absPath: g.abs,
62
- abortPrefix: "edit aborted (atomic — no files written): ",
63
- })
64
- prepared.push({
65
- g,
66
- editStartLine: out.editStartLine, // 基于累积内容计算——已天然计入前面条目的行偏移,不再累加
67
- lineShift: out.lineShift,
68
- occurrences: out.occurrences,
69
- note: out.note ?? null, // P15.11——空白差异自动落点标记(成功消息追加)
70
- deleted: out.deleted ?? false, // 删行形态(EDIT.md §5)——结果文本用 Deleted 前缀
71
- })
72
- g.content = out.updated // 串行累积:下一条基于本条应用后的内容
73
- g.netShift += out.lineShift
74
- }
75
- }
76
- // 全部检查通过——每文件一次写盘(同文件多条:写入串行累积后的最终内容);
77
- // recordWrite 每组一条合并快照:startLine = 组内**所有**编辑受影响行的最小值
78
- // (#2,2026-09-01 交付评审尾巴——原实现取首条 = 调用序第一条,逆序条目时
79
- // 护栏下界过高、受影响区内的 insert_after 被放行),shift = 全组行数差累积
80
- // ——受影响区护栏覆盖组内所有编辑
81
- for (const g of groups.values()) {
82
- const startLine = Math.min(...prepared.filter((p) => p.g === g).map((p) => p.editStartLine))
83
- await writeFile(g.abs, joinWithEol(normalizeEOL(g.content).split("\n"), g.raw), "utf8")
84
- recordWrite(g.abs, { type: "edit", startLine, shift: g.netShift })
85
- }
86
- const results = []
87
- for (const p of prepared) {
88
- // #4(2026-09-01 交付评审尾巴):与单文件路径对齐——每条结果附 git diff +
89
- // autoSyntaxCheck(同文件多条会重复 diff/检查,换取格式一致、实现零分支)
90
- const diff = gitDiffOne(ctx.cwd, p.g.abs)
91
- const base = p.deleted
92
- ? `Deleted ${deleteTarget(p)} of ${p.g.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(p.g.abs)}`
93
- : `Edited ${p.g.path}: replaced ${p.occurrences} occurrence(s)${p.note ? ` — ${p.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(p.g.abs)}`
94
- results.push(await appendWriteContext(p.g.abs, p.editStartLine, base))
95
- }
96
- return results.join("\n")
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // 2026-09-08 edit 语义升级(EDIT.md §2/§3——D1 按行号改 / D2 模糊匹配,落点定死本模块;
101
- // 条目判定接线在 edit-diff.mjs computeEditEntry——单形态/批量/ACP 桥三通道自动继承;
102
- // 阶段 2 删行形态见 EDIT.md §5(约束)。以下均为纯函数(无 IO)——edit-diff.mjs 调用期导入
103
- // (ESM 循环引用安全:两侧仅函数声明,提升初始化——同 file.mjs ↔ edit-diff.mjs 先例)。
104
-
105
- /**
106
- * D2 行级 normalize(EDIT.md §3——匹配档位;阶段 2 normalize 统一基准——双端同算法):
107
- * ① 去首尾空白 + 去行尾空格;② tab → 2 空格;③ 引号**单遍逐字符映射**——ASCII 单引号 /
108
- * 弯引号 ‘ ’ “ ” / 反引号 → 直双引号 "(评审 #2 定稿目标字符 = "——与 ASCII 单引号规则
109
- * 合并为单遍映射,无顺序依赖——防两端分叉)。**不做行内 \s+ 折叠**——缩进/对齐是结构信息,
110
- * 折叠会误匹配文字相似但结构不同的行(统一基准 5——CLI 从未折叠,防误配回归)。仅用于匹配
111
- * 比较——替换永远用文件原文窗口。
112
- */
113
- const QUOTE_TO_DOUBLE = {
114
- "'": '"', "\u2018": '"', "\u2019": '"', "\u201c": '"', "\u201d": '"', "`": '"',
115
- }
116
- export function normalizeEditLine(line) {
117
- let out = ""
118
- for (const ch of line) {
119
- if (ch === "\t") out += " "
120
- else out += QUOTE_TO_DOUBLE[ch] ?? ch
121
- }
122
- return out.replace(/\s+$/g, "").trim()
123
- }
124
-
125
- /** D2 模糊匹配阈值:行级 normalize 后逐行相等比例 ≥0.9 即匹配(评审 #4 定稿)。 */
126
- export const FUZZY_MATCH_THRESHOLD = 0.9
127
- /** D2 成功消息追加 note(双端同句——同 WHITESPACE_VARIANT_NOTE 机制)。 */
128
- export const FUZZY_MATCH_NOTE = "applied via fuzzy match (≥90% of lines identical after whitespace/indent/quote normalization)"
129
-
130
- /**
131
- * D2 模糊匹配(P15.11 空白自动落点的推广——逐字/trim 等价都失败后的最后一档):
132
- * 找文件中**唯一**窗口——行数与 old 相同、normalizeEditLine 后逐行相等比例 ≥90% →
133
- * 返回 { actual }(actual = 文件窗口原文);多窗口达标 → null(歧义不猜——走 not-found
134
- * 报错引导);old 含尾换行 → null(终止符语义边界——同 P15.11)。比例向上取整行数:
135
- * 需要相等行数 = ceil(m × 0.9)(m=1 即 normalize 后全等——单行细微差异由此命中)。
136
- */
137
- export function findFuzzyWindow(content, old) {
138
- if (old.endsWith("\n")) return null
139
- const oldLines = old.split("\n")
140
- const m = oldLines.length
141
- const fileLines = content.split("\n")
142
- if (m === 0 || fileLines.length < m) return null
143
- const norm = oldLines.map(normalizeEditLine)
144
- const need = Math.ceil(m * FUZZY_MATCH_THRESHOLD)
145
- let hit = null
146
- for (let i = 0; i + m <= fileLines.length; i++) {
147
- let eq = 0
148
- for (let j = 0; j < m; j++) {
149
- if (normalizeEditLine(fileLines[i + j]) === norm[j]) eq++
150
- }
151
- if (eq < need) continue
152
- const actual = fileLines.slice(i, i + m).join("\n")
153
- if (actual === old) continue // 逐字已匹配——occurrences=0 前提下不会发生
154
- if (hit) return null // 多窗口达标 → 歧义 → 不猜
155
- hit = { actual }
156
- }
157
- return hit
158
- }
159
-
160
- /**
161
- * D1 按行号改(EDIT.md §2/§5——F1):entry 带 line(单行)或 startLine/endLine(1-based
162
- * 闭区间)→ 直接按行号替换该行/行范围为 new_string(无需 old_string——互斥校验在
163
- * validateEditEntry)。阶段 2 删行形态(EDIT.md §5——裁定 A):**省略 new_string =
164
- * 删除该行/范围**(意图有界——删哪行是显式声明);显式空串 `new_string: ""` →
165
- * EMPTY_NEW_STRING_LINE 显式报错(模板生成 new_string 但落空 ≠ 删行意图——防误删)。
166
- * 内容域 = normalizeEOL 后 LF(与 computeEditEntry 同域);尾随换行随原文件保持(删到
167
- * 文件为空 → ""——无剩余行即无终止符)。行号越界 → 明确报错。原子性由调用方保证
168
- * (本函数纯计算)。
169
- * 返回 { updated, editStartLine, lineShift, occurrences, note, deleted? }(同 computeEditEntry 形态)。
170
- */
171
- export function applyLineEdit(content, entry, opts = {}) {
172
- const prefix = opts.abortPrefix ?? ""
173
- if (entry.new_string === "") throw new Error(prefix + EMPTY_NEW_STRING_LINE)
174
- const lines = splitLines(content)
175
- const start = entry.line ?? entry.startLine
176
- const end = entry.line ?? entry.endLine
177
- if (start > lines.length || end > lines.length) {
178
- throw new Error(prefix + `line ${end > lines.length ? end : start} out of range — ${opts.path ?? "file"} has ${lines.length} line(s)`)
179
- }
180
- const removed = end - start + 1
181
- if (entry.new_string === undefined) {
182
- // 删行/删范围(EDIT.md §5)——省略 new_string:受影响行整体移除
183
- const remaining = [...lines.slice(0, start - 1), ...lines.slice(end)]
184
- const updated = remaining.join("\n") + (remaining.length > 0 && content.endsWith("\n") ? "\n" : "")
185
- return { updated, editStartLine: start, lineShift: -removed, occurrences: 1, note: null, deleted: true }
186
- }
187
- const newLines = splitLines(entry.new_string)
188
- const updated = [...lines.slice(0, start - 1), ...newLines, ...lines.slice(end)].join("\n") +
189
- (content.endsWith("\n") ? "\n" : "")
190
- return { updated, editStartLine: start, lineShift: newLines.length - removed, occurrences: 1, note: null }
191
- }
@@ -1,348 +0,0 @@
1
- /**
2
- * edit-diff.mjs — edit 工具的行级 diff 内核(权威语义 = EDIT.md——2026-09-04 拆出)。
3
- *
4
- * edit 语义(权威 = EDIT.md §2-§5——2026-09-08 语义升级 D1/D2/D3 已并入):两种定位形态(互斥)——
5
- * ① 按行号改(D1/F1):line(单行)/ startLine+endLine(1-based 闭区间)→ 直接替换该
6
- * 行/行范围为 new_string(无需 old_string——实现落点 edit-batch.mjs applyLineEdit);
7
- * 阶段 2 删行形态(EDIT.md §5——约束):省略 new_string = 删除该行/范围(有界意图);
8
- * ② 内容定位:old_string = 变化区**当前内容**(单次匹配——匹配档位:逐字 → P15.11 唯一
9
- * 空白差异窗口 → D2/F2 模糊匹配(行级 normalize 后 ≥90% 行相等——edit-batch.mjs
10
- * findFuzzyWindow));new_string = 该区的**期望结果**。判定序:
11
- * 0. 分支 0(EDIT.md §4——单行精确替换):old 单行 && new 单行 && old 全文唯一 &&
12
- * new 非空 → **就地替换该行**(行数不变——EOL 由调用方 joinWithEol 恢复);
13
- * 1. 零重叠(old 每一行都不出现在 new 行集中)→ **替换即删**(D3/F3,2026-09-08——
14
- * breaking:old 行整体删除、new 取而代之——旧行不再保留;新增行用 insert_after);
15
- * 2. 有公共行(≥1)→ 行级 LCS——公共行保留、差异行增删;
16
- * 3. new 与 old 行级完全一致 → 原样替换(no-op 语义——仍报成功)。
17
- * 内容形态空 new_string(纯删除意图)→ 显式报错(不静默——先于分支 0——单行替换永不
18
- * 成删除);行号形态显式空串 → 同样显式错(防误删——模板落空 ≠ 删行意图;EDIT.md
19
- * §5 空串 vs 省略矩阵);**省略 new_string = 删行/范围**(阶段 2 命名删行形态);
20
- * 分支 0 只在 computeEditEntry 条目判定层(壳/桥/批量自动继承)——
21
- * applyPatchLines(纯 diff 层)语义不动。old/new 行数各上限 1000(超限报错)。
22
- *
23
- * 行尾权威 = EDIT-TOOL-EOL-DESIGN.md:判定/应用在 normalizeEOL 后的 LF 域计算,
24
- * 写回由调用方 joinWithEol(原文) 恢复原行尾。
25
- * 模块拆分(file.mjs ≤500 硬限):file.mjs 只留工具壳与转发——单形态执行体与前置校验
26
- * 分支整段迁出至本模块(模块拆分写优先纪律:先迁后删、逻辑体不变、wiring 导入)。
27
- * edit-diff.mjs ↔ edit-batch.mjs 循环引用(2026-09-08——D1/D2 实现落点 edit-batch):
28
- * 两侧仅函数声明(提升初始化)、仅调用期使用——ESM 循环下安全(同 file.mjs 先例)。
29
- */
30
- import { readFile, writeFile } from "node:fs/promises"
31
- import {
32
- resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck, findCandidates,
33
- } from "./shared.mjs"
34
- // file.mjs ↔ edit-diff.mjs 循环引用:两侧导入的都是函数声明(提升初始化),
35
- // 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
36
- import { recordWrite, appendWriteContext, lastWriteOf, isDirty } from "./file.mjs"
37
- // D1/D2 落点(EDIT.md §6——edit-batch.mjs):按行号改 + 模糊匹配纯函数。
38
- import { applyLineEdit, findFuzzyWindow, FUZZY_MATCH_NOTE } from "./edit-batch.mjs"
39
-
40
- export const MAX_DIFF_LINES = 1000
41
- export const REGION_TOO_LARGE = "edit region too large — narrow the change"
42
- 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"
43
- export const EMPTY_NEW_STRING_LINE = "empty new_string with line-based targeting is an explicit error — OMIT new_string to delete the line/range (deleting by number is explicit intent; an empty replacement is a mistake, not a delete)"
44
- export const EDIT_ARGS_MUTEX = "edits array is mutually exclusive with top-level old_string/new_string/line/startLine/endLine — a top-level path is allowed (default for entries without their own path); provide each change's targeting (old_string or line range) and new_string inside its edits entry"
45
-
46
- /** D1(2026-09-08):条目带按行号改参数(line / startLine / endLine 任一)。 */
47
- export function hasLineParams(entry) {
48
- return entry.line !== undefined || entry.startLine !== undefined || entry.endLine !== undefined
49
- }
50
-
51
- /** 删行结果文本(EDIT.md——删行返回文本;out.deleted 才调用):单行 "line N" / 范围 "lines N-M"。
52
- * 删行 lineShift = -(范围行数)——end = start - 1 - lineShift。 */
53
- export function deleteTarget(out) {
54
- const start = out.editStartLine
55
- const end = start - 1 - out.lineShift
56
- return start === end ? `line ${start}` : `lines ${start}-${end}`
57
- }
58
-
59
- /** 行切分(尾随换行终止最后一行——非额外空行):"a\nb\n" → ["a","b"]。
60
- * 2026-09-08 导出(D1 落点 edit-batch.mjs applyLineEdit 复用同语义)。 */
61
- export function splitLines(text) {
62
- const lines = text.split("\n")
63
- if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop()
64
- return lines
65
- }
66
-
67
- /**
68
- * 行级 LCS(整行相等判定)合并:公共行保留(LCS 序)、old 独有行删除、new 独有行按其在
69
- * new 中相对公共行的位置插入;**零重叠 → 替换即删**(D3,2026-09-08 语义升级——
70
- * breaking:old 行整体删除、new 取而代之,旧行不再保留——原"零重叠→插入保留旧行"
71
- * 语义废止;新增行用 insert_after)。结果在 LF 域;区域尾随换行随 oldText(区域边界保持)。
72
- * 返回 { ok: true, resultText } 或 { ok: false, reason }。
73
- */
74
- export function applyPatchLines(oldText, newText) {
75
- if (newText === "") return { ok: false, reason: EMPTY_NEW_STRING }
76
- const oldLines = splitLines(oldText)
77
- const newLines = splitLines(newText)
78
- if (oldLines.length > MAX_DIFF_LINES || newLines.length > MAX_DIFF_LINES) {
79
- return { ok: false, reason: REGION_TOO_LARGE }
80
- }
81
- // 判定 1(D3——替换即删):零重叠 → old 行删、new 整体取代(旧行不再保留)
82
- const newSet = new Set(newLines)
83
- const merged = oldLines.some((l) => newSet.has(l))
84
- ? lcsMerge(oldLines, newLines) // 判定 2/3——公共行保留、差异行增删
85
- : [...newLines]
86
- return { ok: true, resultText: merged.join("\n") + (oldText.endsWith("\n") ? "\n" : "") }
87
- }
88
-
89
- /** LCS 推导(DP + 回溯)——整行相等判定;结果 = 公共行 + 差异行增删的合并序列。 */
90
- function lcsMerge(a, b) {
91
- const n = a.length
92
- const m = b.length
93
- const width = m + 1
94
- const dp = new Uint16Array((n + 1) * width)
95
- for (let i = 1; i <= n; i++) {
96
- for (let j = 1; j <= m; j++) {
97
- dp[i * width + j] = a[i - 1] === b[j - 1]
98
- ? dp[(i - 1) * width + (j - 1)] + 1
99
- : Math.max(dp[(i - 1) * width + j], dp[i * width + (j - 1)])
100
- }
101
- }
102
- const merged = []
103
- let i = n
104
- let j = m
105
- while (i > 0 && j > 0) {
106
- if (a[i - 1] === b[j - 1]) {
107
- merged.push(a[i - 1])
108
- i--
109
- j--
110
- } else if (dp[(i - 1) * width + j] >= dp[i * width + (j - 1)]) {
111
- i-- // old 独有行——删除
112
- } else {
113
- merged.push(b[j - 1]) // new 独有行——按 LCS 相对位置插入
114
- j--
115
- }
116
- }
117
- while (j > 0) { merged.push(b[j - 1]); j-- }
118
- return merged.reverse()
119
- }
120
-
121
- /**
122
- * (2026-09-05 用户裁定——EDIT.md §5——顶层 path + edits 并存合法化:顶层 path = 无自带
123
- * path 条目的默认——模型直觉形态「顶层 path + 数组条目」不再拒绝;条目自带 path 优先)。
124
- * 互斥收窄为只对顶层 old_string/new_string(+ 2026-09-08 D1 的 line/startLine/endLine)——
125
- * edits 下它们无批语义可解释——顶层 path 不再触发。
126
- */
127
- export function assertEditArgsExclusive(args) {
128
- if (args.old_string !== undefined || args.new_string !== undefined || hasLineParams(args)) {
129
- throw new Error(EDIT_ARGS_MUTEX)
130
- }
131
- }
132
-
133
- /**
134
- * 前置校验——内容形态:空 old / 非字符串 new;按行号改形态(D1,2026-09-08;阶段 2
135
- * 删行——EDIT.md §5):与 old_string 互斥 / line 与 startLine|endLine 互斥 /
136
- * startLine+endLine 须成对 / 正整数 / endLine ≥ startLine / replace_all 不适用 /
137
- * new_string:省略(undefined)= 删行 / 显式空串 = 显式错(防误删)/ 其他类型报错。
138
- * error 文本按调用形态(单形态 rich / 批量 label)。
139
- * opts: { label = ""(批量前缀 "edit for <path>: "), rich = true(单形态完整文本) }
140
- */
141
- export function validateEditEntry(entry, opts = {}) {
142
- const label = opts.label ?? ""
143
- if (hasLineParams(entry)) {
144
- if (entry.old_string !== undefined) {
145
- throw new Error(label + "line/startLine/endLine are mutually exclusive with old_string — target by line number OR by content, not both")
146
- }
147
- if (entry.line !== undefined && (entry.startLine !== undefined || entry.endLine !== undefined)) {
148
- throw new Error(label + "line is mutually exclusive with startLine/endLine — single line or a range, not both")
149
- }
150
- if ((entry.startLine === undefined) !== (entry.endLine === undefined)) {
151
- throw new Error(label + "startLine and endLine must be given together (1-based, inclusive)")
152
- }
153
- for (const k of ["line", "startLine", "endLine"]) {
154
- if (entry[k] !== undefined && (!Number.isInteger(entry[k]) || entry[k] < 1)) {
155
- throw new Error(label + `${k} must be a positive integer (1-based), got ${JSON.stringify(entry[k])}`)
156
- }
157
- }
158
- if (entry.startLine !== undefined && entry.endLine < entry.startLine) {
159
- throw new Error(label + `endLine (${entry.endLine}) is before startLine (${entry.startLine})`)
160
- }
161
- if (entry.replace_all) {
162
- throw new Error(label + "replace_all does not apply to line-based edits (a line range is already a single explicit target)")
163
- }
164
- if (entry.new_string === "") {
165
- throw new Error(label + EMPTY_NEW_STRING_LINE + (opts.rich === false ? "" : " — nothing written"))
166
- }
167
- if (entry.new_string !== undefined && typeof entry.new_string !== "string") {
168
- throw new Error(
169
- label + `new_string must be a string (got ${typeof entry.new_string})` +
170
- (opts.rich === false ? "" : " — nothing written")
171
- )
172
- }
173
- return
174
- }
175
- if (!entry.old_string) {
176
- throw new Error(
177
- label + "old_string must not be empty" + (opts.rich === false ? "" : " (empty string matches everywhere and would corrupt the file)")
178
- )
179
- }
180
- if (entry.new_string === "") {
181
- // 空串 vs 省略矩阵(EDIT.md §5/§8.1):内容形态空串 = 显式错(无界意图防静默)——
182
- // 先于 replace_all 分支(否则 replace_all + 空串会静默删除全部 occurrences——绕过保护)
183
- throw new Error(label + EMPTY_NEW_STRING + (opts.rich === false ? "" : " — nothing written"))
184
- }
185
- if (typeof entry.new_string !== "string") {
186
- throw new Error(
187
- label + `new_string must be a string${entry.new_string === undefined ? " (missing)" : ` (got ${typeof entry.new_string})`}` +
188
- (opts.rich === false ? "" : " — nothing written")
189
- )
190
- }
191
- }
192
-
193
- /**
194
- * 分支 0 形态判定(EDIT.md §4):old/new 各为单行——无内部换行(splitLines
195
- * 语义:尾随单个换行符是行终止而非新行——old/new 行内容均不含 \n)——且 new 非空
196
- * ("" 经 splitLines 切分为 []——由下方 !== "" 守卫先拦截——空 new 不落本分支——落
197
- * applyPatchLines 的空 new 显式错误——单行替换永不成删除)。多匹配与 replace_all 不落
198
- * 本分支(occurrences 错误 / 字面全部先行)。
199
- */
200
- function isSingleLineReplace(entry) {
201
- return (
202
- entry.new_string !== "" &&
203
- splitLines(entry.old_string).length === 1 &&
204
- splitLines(entry.new_string).length === 1
205
- )
206
- }
207
-
208
- /**
209
- * 条目级判定+应用(单形态与批量共用——EDIT.md §6"批量条目判定+应用调用 edit-diff"):
210
- * 匹配校验(精确存在——非 replace_all 单次)→ 按判定序应用(分支 0 单行替换 / 零重叠替换即删 /
211
- * LCS 替换 / replace_all 字面)→
212
- * 元数据(受影响区首行/行数差/次数——recordWrite 与结果回显用)。
213
- * 返回 { updated, editStartLine, lineShift, occurrences, note?, deleted? };失败抛错(含路径/引导)。
214
- * opts: { path, abortPrefix(批量 "edit aborted (atomic — no files written): ") }
215
- */
216
- /**
217
- * P15.11(2026-09-05 用户裁定——edit 连续失败分析):old_string 逐字 not-found 时——
218
- * 若文件中存在**唯一**窗口:行数与 old 相同、逐行 trim() 相等(内容零差异——差异仅前导/
219
- * 尾随空白——EOL 已在 normalize 域消除)→ 返回 { actual }(actual = 文件窗口原文);
220
- * 多窗口(两处 trim 同内容不同空白)→ null(歧义不猜——报错引导);old 含尾换行 → null
221
- * (终止符语义边界——不做窗口猜测)。模型从 read 记忆拷贝 old_string 时丢/加前导空格是
222
- * 纯机械损耗(本轮 12 次 edit 失败中 8 次空白差异)——内容零差异时自动落点 + 结果明示。
223
- */
224
- function findWhitespaceVariant(content, old) {
225
- if (old.endsWith("\n")) return null
226
- const oldLines = old.split("\n")
227
- const fileLines = content.split("\n")
228
- const m = oldLines.length
229
- if (m === 0 || fileLines.length < m) return null
230
- const trimmed = oldLines.map((l) => l.trim())
231
- let hit = null
232
- for (let i = 0; i + m <= fileLines.length; i++) {
233
- let same = true
234
- for (let j = 0; j < m; j++) {
235
- if (fileLines[i + j].trim() !== trimmed[j]) { same = false; break }
236
- }
237
- if (!same) continue
238
- const actual = fileLines.slice(i, i + m).join("\n")
239
- if (actual === old) continue // 逐字已匹配——occurrences=0 前提下不会发生
240
- if (hit) return null // 多窗口空白差异 → 歧义 → 不猜
241
- hit = { actual }
242
- }
243
- return hit
244
- }
245
-
246
- /**
247
- * P15.11 note 文案(各端成功消息追加——双端同句)
248
- */
249
- export const WHITESPACE_VARIANT_NOTE = "applied to the unique whitespace-only match (content identical, leading/trailing whitespace differs from your old_string)"
250
-
251
- export function computeEditEntry(content, entry, opts = {}) {
252
- validateEditEntry(entry, { ...opts, rich: !opts.abortPrefix })
253
- // D1 按行号改(EDIT.md §2/§5):行号定位条目直接按行替换(applyLineEdit——落点
254
- // edit-batch.mjs),不走 old_string 匹配/判定序;省略 new_string = 删行(EDIT.md §5);
255
- // 互斥/越界/显式空串均在 validateEditEntry/applyLineEdit 显式报错。
256
- if (hasLineParams(entry)) return applyLineEdit(content, entry, opts)
257
- // P15.11:not-found 时先查唯一空白差异窗口——命中则以其原文为实际 old 继续(内容零差异
258
- // ——自动落点 + note 明示);实质差异/歧义仍走下方 not-found 报错(不猜内容)。
259
- let old = entry.old_string
260
- let occurrences = content.split(old).length - 1
261
- let note = null
262
- if (occurrences === 0) {
263
- const variant = findWhitespaceVariant(content, old)
264
- if (variant) { old = variant.actual; occurrences = 1; note = WHITESPACE_VARIANT_NOTE }
265
- }
266
- // D2 模糊匹配(2026-09-08——P15.11 的推广):逐字/trim 等价都失败后,找唯一
267
- // normalize 后 ≥90% 行相等的窗口——细微差异(缩进/引号/行尾空格/少量行差异)自动
268
- // 落点 + note 明示;多窗口歧义/不足阈值仍走 not-found 报错(不猜内容)。
269
- if (occurrences === 0) {
270
- const fuzzy = findFuzzyWindow(content, old)
271
- if (fuzzy) { old = fuzzy.actual; occurrences = 1; note = FUZZY_MATCH_NOTE }
272
- }
273
- if (occurrences === 0) {
274
- const preview = entry.old_string.slice(0, 100).split("\n")[0]
275
- const cands = findCandidates(content.split("\n"), entry.old_string)
276
- let candText = ""
277
- if (cands.length > 0) {
278
- const header = entry.old_string.includes("\n")
279
- ? ` similar lines (old_string line 1: "${entry.old_string.split("\n")[0].slice(0, 80)}"):`
280
- : " similar lines:"
281
- candText = "\n" + header + "\n" + cands.map((c) => ` L${c.line}: ${c.preview} (${Math.round(c.score * 100)}%)`).join("\n")
282
- }
283
- throw new Error(
284
- `${opts.abortPrefix ?? ""}old_string not found in ${opts.path ?? "file"}\n` +
285
- ` searched: "${preview}${entry.old_string.length > 100 ? "…" : ""}" — use grep to locate the actual content\n` +
286
- (opts.absPath && lastWriteOf(opts.absPath)?.type === "write"
287
- ? ` hints: this file was modified since your last read (write 全文重写后内容全变) — re-read it to refresh your copy of the content, then retry\n`
288
- : opts.absPath && isDirty(opts.absPath)
289
- ? ` 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`
290
- : ` hints: whitespace mismatch? file already changed? try reading the file first\n`) +
291
- candText
292
- )
293
- }
294
- if (occurrences > 1 && !entry.replace_all) {
295
- throw new Error(
296
- `${opts.abortPrefix ?? ""}old_string matches ${occurrences} times in ${opts.path ?? "file"}; provide more context or set replace_all`
297
- )
298
- }
299
- const matchIdx = content.indexOf(old)
300
- const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
301
- let updated
302
- let resultForShift
303
- if (entry.replace_all) {
304
- // 字面替换——不做插入规则(old 多处时"插到哪处"无定义)——不落分支 0(EDIT.md §4)
305
- updated = content.split(old).join(entry.new_string)
306
- resultForShift = entry.new_string
307
- } else if (isSingleLineReplace(entry)) {
308
- // 分支 0(EDIT.md §4——单行精确替换):old 单行 && new 单行 && old 唯一匹配
309
- // (occ==1 既有校验保证)&& new 非空(空 new 显式错误先于本分支——防删除)→ **就地
310
- // 替换该行**——不再零重叠插入(P15.8——行尾段编辑反复踩的插入坑)。只替换行内容段:
311
- // old/new 的尾随换行符属文件结构而非行内容——留在原位——行数不变——EOL 由调用方
312
- // joinWithEol 恢复(normalizeEOL 后本域为 LF)。边界声明(评审 2026-09-04):new 为
313
- // 行内容空串(如 new="\n"——置空行意图)时,常规带终止换行的行变空行(行数不变);
314
- // 唯一例外是**无终止换行的末行**被置空——该行随文件结束自然消失(行数 −1——文件格式
315
- // 固有边界——空行无法表达)——不落 EMPTY_NEW_STRING 错误路径(new 本身非空串)。
316
- const oldLine = splitLines(old)[0]
317
- const newLine = splitLines(entry.new_string)[0]
318
- updated = content.slice(0, matchIdx) + newLine + content.slice(matchIdx + oldLine.length)
319
- resultForShift = newLine
320
- } else {
321
- const r = applyPatchLines(old, entry.new_string)
322
- if (!r.ok) throw new Error(r.reason)
323
- updated = content.slice(0, matchIdx) + r.resultText + content.slice(matchIdx + old.length)
324
- resultForShift = r.resultText
325
- }
326
- // 行数差 = 应用后区域行数 − 旧区域行数(分支 0 单行替换:0;零重叠替换即删:new−old;LCS:new−old)
327
- const lineShift = splitLines(resultForShift).length - splitLines(old).length
328
- return { updated, editStartLine, lineShift, occurrences: entry.replace_all ? occurrences : 1, note }
329
- }
330
-
331
- /**
332
- * 单形态 edit 执行体(自 file.mjs 整段迁出——模块拆分写优先纪律:逻辑体不变)——
333
- * file.mjs 只留工具壳与转发。
334
- */
335
- export async function runSingleEdit(args, ctx) {
336
- const abs = resolveInCwd(ctx, args.path)
337
- const raw = await readFile(abs, "utf8")
338
- const content = normalizeEOL(raw)
339
- const out = computeEditEntry(content, args, { path: args.path, absPath: abs })
340
- // 写回按原文行尾(joinWithEol)——normalizeEOL 先行避免 \r\n 污染 split/join
341
- await writeFile(abs, joinWithEol(normalizeEOL(out.updated).split("\n"), raw), "utf8")
342
- recordWrite(abs, { type: "edit", startLine: out.editStartLine, shift: out.lineShift })
343
- const diff = gitDiffOne(ctx.cwd, abs)
344
- const baseResult = out.deleted
345
- ? `Deleted ${deleteTarget(out)} of ${args.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
346
- : `Edited ${args.path}: replaced ${out.occurrences} occurrence(s)${out.note ? ` — ${out.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
347
- return await appendWriteContext(abs, out.editStartLine, baseResult)
348
- }
package/src/tools/edit.md DELETED
@@ -1,30 +0,0 @@
1
- Edit a file as a patch — targeted by content or by line number. Two targeting forms (mutually exclusive — use one): ① line-based: `line: N` replaces that single line, `startLine: N, endLine: M` replaces the inclusive 1-based line range — no old_string needed when you know the line number; give new_string to replace, or OMIT new_string to delete the line/range; ② content-based: old_string is the current content of the region to change (must match exactly once); new_string is the desired result of that region. old_string matching is tolerant: exact match first, then a unique whitespace-only variant, then fuzzy match (unique window with ≥90% of lines identical after trimming, tab→space indent and quote normalization — ASCII single, curly single/double and backtick quotes all unify to straight double quotes). Lines shared by old/new are kept; lines only in new_string take their position relative to the shared lines (LCS order); lines only in old_string are deleted — a replacement never leaves old lines behind (zero overlap → old lines are replaced by new_string, not kept). A unique single-line old_string paired with a single-line new_string replaces that exact line in place (line count unchanged). To add a new line without removing anything use insert_after. replace_all keeps literal replacement of every occurrence — the diff rules do not apply.
2
-
3
- **Routing — pick the right edit tool:**
4
- - Delete a line/range by number → omit new_string: edit with `line: N` / `startLine: N, endLine: M`
5
- - Line numbers fresh (just read) → `line`/`startLine`/`endLine` targeting — precise, no content copy needed
6
- - Line numbers may have drifted / content has whitespace-encoding noise → `hashline_edit` (content-hash addressing — position-independent)
7
- - Add a line/entry after a known line → insert_after
8
- - Same change across multiple files → apply_patch
9
- - Rewrite an entire file → write
10
- - Rename a symbol project-wide → `lsp` or `grep` first to map every caller
11
-
12
- **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).
13
-
14
- Parameters:
15
- - path: File path (single form: required; with the edits array: optional — the default for entries without their own path)
16
- - old_string: Current content of the region to change (must match exactly once — tolerant matching: exact → whitespace-only variant → fuzzy ≥90% line-equal). Mutually exclusive with line/startLine/endLine — required unless line targeting is used
17
- - new_string: Desired result of the region — diffed against old_string (shared lines kept; old-only lines deleted — a replacement never leaves old lines behind). Content-based edits: required — an explicit empty string is an error (protects against forgetting it). Line-based edits (line/startLine/endLine): give it to replace the line/range, or OMIT it to delete — an explicit empty string is NOT deletion (error — omit instead)
18
- - line: 1-based line number — replace that single line with new_string, or OMIT new_string to DELETE it (mutually exclusive with old_string and startLine/endLine)
19
- - startLine / endLine: 1-based inclusive line range to replace with new_string (given together; mutually exclusive with old_string) — OMIT new_string to DELETE the range
20
- - replace_all: Replace all occurrences instead of just one (default false; content-based targeting only)
21
- - edits: Array of {path?, old_string?, new_string?, line?, startLine?, endLine?, replace_all?} entries — batch form; mutually exclusive with top-level old_string/new_string/line/startLine/endLine — a top-level path is allowed and applies to entries without their own path
22
-
23
- Notes:
24
- - Prefer this over write for targeted edits — it's safer and keeps changes targeted
25
- - If old_string matches zero times (even fuzzy): error. If it matches multiple times without replace_all: error — add more surrounding context to make it unique
26
- - Content-based edits: new_string empty is an explicit error (protects against forgetting it).
27
- - Line-based edits: OMIT new_string to delete the line/range — deleting by number is an explicit, bounded intent.
28
- - Returns `Edited <path>: replaced N occurrence(s)` (delete-mode: `Deleted line N of <path>` / `Deleted lines N-M of <path>`) + git diff + syntax-check note + context block (L..-L..).
29
- - Never fabricate the old_string — copy it verbatim from the actual file using read first
30
- - use the most recent read of the file as the source of old_string / line numbers / hashes — re-read after the file changed
@@ -1,21 +0,0 @@
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
-
3
- **Route to execute instead of bash:**
4
- - `node -e "…"` → execute (inline code; top-level await + import() + console all work)
5
- - `node <script.mjs>` → execute with scriptFile (runs the file in a child node process)
6
- - `node --test <file>` / `node --check <file>` → execute with scriptFile + nodeArgs
7
-
8
- Parameters:
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
- - nodeArgs: (scriptFile) extra node flags before the script, e.g. ["--test"], ["--check"]. Eval-like flags (--eval/--input-type/--inspect) are rejected.
12
- - workdir: run in this directory (relative to cwd — no directory restriction; default cwd)
13
- - filter: optional — only return output lines matching this regex (case-insensitive)
14
- - timeoutMs: Timeout in milliseconds (default 30000, max 600000 — covers `node --test` suites and package scripts)
15
-
16
- Notes:
17
- - `console.log(...)` prints to the result; objects are JSON-stringified where needed.
18
- - A non-zero exit / thrown exception returns the stderr (error + stack) as the result.
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.
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.