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,282 +0,0 @@
1
- import {
2
- DESC,
3
- autoSyntaxCheck,
4
- resolveInCwd,
5
- normalizeEOL,
6
- detectFileEol,
7
- majorityEol
8
- } from "./shared.mjs";
9
- import { markDirty } from "./file.mjs";
10
- import { execFileSync } from "node:child_process";
11
- import { mkdir, readFile, lstat, writeFile, unlink } from "node:fs/promises";
12
- import { existsSync } from "node:fs";
13
- import { relative, dirname } from "node:path";
14
-
15
- /**
16
- * Parse a unified diff: returns [{ path, isNew, hunks: [{ ops: [{type:" "|"-"|"+", text}] }] }]
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.
19
- * APPLY-PATCH.md §3: 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.
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
-
35
- function parsePatch(patch) {
36
- // Patch text often comes from CRLF terminals/model output; trailing \r mixed into hunk content breaks context matching, strip uniformly
37
- const lines = patch.replace(/\r(?=\n|$)/g, "").split("\n")
38
- const files = []
39
- let cur = null
40
- let i = 0
41
- const stripPrefix = (p) => p.replace(/^[ab]\//, "")
42
- while (i < lines.length) {
43
- const line = lines[i]
44
- if (line.startsWith("--- ")) {
45
- const oldPath = line.slice(4).trim()
46
- const plus = lines[i + 1]
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}"`)
66
- }
67
- if (line.startsWith("@@")) {
68
- if (!cur) throw new Error("Malformed patch: hunk header before any file header")
69
- const m = line.match(/^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/)
70
- if (!m) {
71
- // APPLY-PATCH.md §3:坐标裸 "@@" 头——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
- // §3 (APPLY-PATCH.md——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
- }
110
- let oldNeed = m[1] == null ? 1 : Number(m[1])
111
- let newNeed = m[2] == null ? 1 : Number(m[2])
112
- const hunk = { ops: [] }
113
- i++
114
- while (oldNeed > 0 || newNeed > 0) {
115
- if (i >= lines.length) throw new Error("Malformed patch: hunk truncated (line counts in @@ header not satisfied)")
116
- const hl = lines[i]
117
- if (hl.startsWith("\\")) { i++; continue } // ""
118
- const tag = hl === "" ? " " : hl[0] // pure blank line: treat leniently as context line
119
- const text = hl === "" ? "" : hl.slice(1)
120
- if (tag === " ") { hunk.ops.push({ type: " ", text }); oldNeed--; newNeed-- }
121
- else if (tag === "-") { hunk.ops.push({ type: "-", text }); oldNeed-- }
122
- else if (tag === "+") { hunk.ops.push({ type: "+", text }); newNeed-- }
123
- else throw new Error(`Malformed patch: unexpected line "${hl.slice(0, 60)}" inside hunk`)
124
- i++
125
- }
126
- cur.hunks.push(hunk)
127
- continue
128
- }
129
- i++ // skip diff --git / index / blank lines and other metadata
130
- }
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
135
- }
136
-
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 */
138
- function applyHunks(fileLines, hunks, eol, path) {
139
- const cr = eol === "\r\n" ? "\r" : ""
140
- for (let h = 0; h < hunks.length; h++) {
141
- const oldSeq = hunks[h].ops.filter((o) => o.type !== "+").map((o) => o.text)
142
- if (oldSeq.length === 0) throw new Error(`Hunk ${h + 1} in ${path} has no context/removed lines to locate`)
143
- const matches = []
144
- for (let pos = 0; pos + oldSeq.length <= fileLines.length; pos++) {
145
- let ok = true
146
- for (let j = 0; j < oldSeq.length; j++) {
147
- if (fileLines[pos + j].replace(/\r$/, "") !== oldSeq[j]) { ok = false; break }
148
- }
149
- if (ok) matches.push(pos)
150
- }
151
- if (matches.length === 0) {
152
- const preview = oldSeq.slice(0, 3).join(" ⏎ ")
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.`)
154
- }
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
- }
159
- const pos = matches[0]
160
- const out = []
161
- let src = pos
162
- for (const op of hunks[h].ops) {
163
- if (op.type === " ") out.push(fileLines[src++]) // preserve original context line (trailing whitespace as-is)
164
- else if (op.type === "-") src++
165
- else out.push(op.text + cr)
166
- }
167
- fileLines.splice(pos, oldSeq.length, ...out)
168
- }
169
- }
170
-
171
- export const applyPatchTool = {
172
- name: "apply_patch",
173
- description: DESC("apply_patch"),
174
- parameters: {
175
- type: "object",
176
- properties: {
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>)." },
178
- },
179
- required: ["patch"],
180
- },
181
- readonly: false,
182
- /** For the agent layer to track touched files (multi-path, replaces single path parameter) */
183
- touchedPaths(args) {
184
- try { return parsePatch(args.patch ?? "").map((f) => f.path) } catch { return [] }
185
- },
186
- async execute(args, ctx) {
187
- const files = parsePatch(args.patch ?? "")
188
- // Read all into memory first for trial: if any hunk fails, abort entirely — never write a partial patch (atomicity)
189
- const planned = []
190
- for (const f of files) {
191
- const abs = resolveInCwd(ctx, f.path)
192
- if (f.isNew) {
193
- if (existsSync(abs)) throw new Error(`Cannot create ${f.path}: file already exists`)
194
- // New file: follow the directory's majority EOL style (default LF).
195
- const eol = majorityEol(dirname(abs))
196
- const content = f.hunks.flatMap((h) => h.ops.filter((o) => o.type === "+").map((o) => o.text)).join(eol) + eol
197
- planned.push({ abs, path: f.path, content, isNew: true })
198
- } else {
199
- const original = await readFile(abs, "utf8").catch(() => { throw new Error(`File not found: ${f.path}`) })
200
- const eol = detectFileEol(original)
201
- // Apply hunks in the normalized LF domain, then write back joined with the
202
- // file's ORIGINAL EOL style — join("\n") here used to rewrite CRLF files as LF.
203
- const lines = normalizeEOL(original).split("\n")
204
- applyHunks(lines, f.hunks, "\n", f.path)
205
- planned.push({ abs, path: f.path, content: lines.join(eol), isNew: false })
206
- }
207
- }
208
- // Multi-file write: write all to .tmp first, rename only after all succeed — failure cleans up written .tmp without affecting committed files
209
- const { rename } = await import("node:fs/promises") // unlink already statically imported
210
- const written = []
211
- try {
212
- for (const p of planned) {
213
- await mkdir(dirname(p.abs), { recursive: true })
214
- await writeFile(p.abs + ".thincoder-tmp", p.content, "utf8")
215
- written.push(p.abs)
216
- }
217
- for (const p of planned) {
218
- await rename(p.abs + ".thincoder-tmp", p.abs)
219
- }
220
- } catch (renameError) {
221
- // rename phase failed: clean up leftover .tmp files
222
- for (const abs of written) {
223
- try { await unlink(abs + ".thincoder-tmp") } catch {}
224
- }
225
- throw renameError
226
- }
227
- const summary = planned.map((p) => ` ${p.isNew ? "created " : "modified"} ${p.path}`).join("\n")
228
- // Mark every touched file dirty — insert_after must not run on stale line numbers.
229
- for (const p of planned) markDirty(p.abs)
230
- const syntaxChecks = await Promise.all(planned.map(async (p) => {
231
- const r = await autoSyntaxCheck(p.abs)
232
- return r ? `${p.path}:${r.replace("Syntax: ", "")}` : ""
233
- }))
234
- const syntaxResults = syntaxChecks.filter(Boolean).join("\n")
235
- return `Applied patch to ${planned.length} file(s):\n${summary}${syntaxResults ? "\n\nSyntax checks:\n" + syntaxResults : ""}`
236
- },
237
- }
238
-
239
-
240
-
241
- // ---------------------------------------------------------------- bash
242
-
243
- export const deleteTool = {
244
- name: "delete",
245
- description: DESC("delete"),
246
- parameters: {
247
- type: "object",
248
- properties: {
249
- path: { type: "string", description: "File path (relative to cwd or absolute)" },
250
- force: { type: "boolean", description: "Allow deleting git-tracked files (default false)" },
251
- },
252
- required: ["path"],
253
- },
254
- readonly: false,
255
- touchedPaths(args) { return args.path ? [args.path] : [] },
256
- async execute(args, ctx) {
257
- const abs = resolveInCwd(ctx, args.path)
258
- let s
259
- try {
260
- s = await lstat(abs)
261
- } catch {
262
- throw new Error(`File not found: ${args.path}`)
263
- }
264
- if (s.isDirectory()) throw new Error(`"${args.path}" is a directory — use bash to remove directories`)
265
- // git-tracked files: refuse direct deletion (safety net); untracked: allow
266
- // Use resolved relative path (normalized forward slashes) to prevent backslash/unusual paths from bypassing ls-files matching
267
- const rel = relative(ctx.cwd, abs).replace(/\\/g, "/")
268
- let tracked = false
269
- try {
270
- execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: ctx.cwd, stdio: "ignore" })
271
- tracked = true
272
- } catch {
273
- // untracked / non-git repo
274
- }
275
- if (tracked && !args.force) throw new Error(`"${args.path}" is git-tracked. Set force=true to delete anyway.`)
276
- await unlink(abs)
277
- markDirty(abs)
278
- return `Deleted ${args.path}`
279
- },
280
- }
281
-
282
- // ---------------------------------------------------------------- git_diff
@@ -1,10 +0,0 @@
1
- List running processes, optionally filtered by name. Returns process name / PID / memory.
2
-
3
- **Route to process instead of bash:**
4
- - `tasklist` (Windows) / `ps aux` (POSIX) → process
5
-
6
- Parameters:
7
- - name (optional): substring filter (case-insensitive), e.g. "node", "python"
8
-
9
- Notes:
10
- - List-only. To kill a process, use `bash taskkill /PID <pid> /F` (Windows) or `bash kill <pid>` — and confirm with the user first.
@@ -1,16 +0,0 @@
1
- Ask the user a question and wait for their response. Use when the task is ambiguous, you need a design decision, or you're stuck and need human judgment.
2
-
3
- Parameters:
4
- - question (required): The question to ask the user
5
- - options: Array of single-choice options for the user to pick from (optional). MUST be plain strings, e.g. ["A", "B", "C"] — never objects.
6
-
7
- Notes:
8
- - The agent loop pauses until the user answers
9
- - The answer is injected as the next user message
10
- - Returns the user's answer — the chosen option or free text — as the next message; the loop resumes when it arrives.
11
- - Availability: this tool needs an interactive UI — in contexts without one (headless runs, subagent children) it returns an error instead of asking; put the question in your reply text instead.
12
- - Use sparingly — prefer making reasonable decisions when possible
13
- - Ask ONE question per call — never bundle multiple sub-questions into one question string; ask the next one after the answer arrives.
14
- - Keep the question text short — one or two sentences. Background, context, and analysis belong in your normal reply text, NOT in the question.
15
- - Routine confirmations (confirm gates) belong in your plain reply text — the user answers in their next message. Use this tool ONLY when you need the user's decision or input to proceed.
16
- - After receiving an answer about a design convention, tool preference, or recurring pattern: save it with the memory tool (action: put). This prevents asking the same question in future sessions — the user shouldn't have to repeat their preferences.
@@ -1,26 +0,0 @@
1
- import { DESC } from "./shared.mjs";
2
-
3
- export const questionTool = {
4
- name: "question",
5
- description: DESC("question"),
6
- parameters: {
7
- type: "object",
8
- properties: {
9
- question: { type: "string", description: "The question to ask the user" },
10
- options: {
11
- type: "array",
12
- items: { type: "string" },
13
- description: "Single-choice options for the user to pick from (optional)",
14
- },
15
- },
16
- required: ["question"],
17
- },
18
- readonly: true,
19
- async execute(args, ctx) {
20
- if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
21
- // §22 D-Q3 机械限制(2026-09-06 用户裁定:100 字符 / 4 条——超限拒绝回模型,不弹卡、不调 onQuestion)
22
- if (args.question.length > 100) return "(error: question too long (>100 chars) — ask ONE short question; background belongs in your normal reply text)"
23
- if (Array.isArray(args.options) && args.options.length > 4) return "(error: too many options (>4) — offer at most 4 plain-string options)"
24
- return ctx.onQuestion(args.question, args.options ?? [])
25
- },
26
- }
package/src/tools/read.md DELETED
@@ -1,20 +0,0 @@
1
- Read a text file. Returns numbered lines. Use offset/limit to page large files.
2
-
3
- **Route to read instead of bash:** `cat file` / `type file` / `node -e "fs.readFileSync(...)"` → read. Reading a file is a read — never shell out for it.
4
-
5
- **Routing:**
6
- - Don't know which file? → `repo_outline` / `code_search` / `glob` first
7
- - Know the symbol but not the location? → `code_search` or `lsp definition`
8
- - Know the file but not the lines? → `grep` to find line numbers, then read that range with offset/limit
9
- - Reading an image? → `read_image` instead
10
-
11
- Parameters:
12
- - path (required): File path, relative to cwd or absolute (alias: filePath)
13
- - offset: 1-based line number to start reading from
14
- - limit: Max lines to return (default 2000) — windows over files beyond that return head + `…(truncated: K lines in middle, use offset to continue)` + the file's real tail lines, so the file end is never hidden
15
- - hashes: Include SHA256 content hashes per line (for hashline_edit). Set true before using hashline_edit.
16
-
17
- Notes:
18
- - Always prefer this over `cat` or shell-based reading — it caps output and avoids large dumps
19
- - Use offset for pagination when the file is large
20
- - When you plan to edit the file, set hashes=true to get line hashes for hashline_edit — hash-based editing avoids whitespace/encoding matching failures
@@ -1,8 +0,0 @@
1
- Read an image file and return it as multimodal content visible to the model. Use this to view screenshots, UI mockups, diagrams, or any visual content. The model only sees images through this tool — it cannot "see" files directly. Supports png, jpg, gif, webp. The image is base64-encoded and included in the response. Large images (>15MB) are rejected.
2
-
3
- Parameters:
4
- - path (required): Path to image file (relative to cwd or absolute). Supports png, jpg, gif, webp; svg is returned as text source.
5
-
6
- Notes:
7
- - Raster formats only (png/jpg/gif/webp): no mainstream vision API (Kimi, Anthropic, OpenAI, Gemini) accepts svg or bmp, and an unsupported image part in history makes every subsequent request fail with 400. svg files are returned as text source instead (readable by any model); bmp is rejected — convert to PNG first.
8
- - This tool only works with models that support vision/image input (capability is spec-driven — the model spec decides, not a hardcoded list). Models without vision support will receive an error — except svg, which needs no vision support since it is read as text.