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