thincoder 0.12.62 → 0.12.64

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 (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  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 +257 -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 +60 -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 +35 -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-plan.mjs +9 -0
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +24 -1
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. 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.