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,446 +0,0 @@
1
- /**
2
- * tools/shared.mjs — shared tool utilities, constants, OpenAI schema conversion
3
- * Imported by tools/file.mjs / system.mjs / web.mjs / git.mjs
4
- */
5
-
6
- import { spawn, execFileSync, execFile } from "node:child_process"
7
- import { readFileSync, existsSync, realpathSync, readdirSync, statSync, openSync, readSync, closeSync } from "node:fs"
8
- import { dirname, join, resolve } from "node:path"
9
- import { fileURLToPath } from "node:url"
10
-
11
- const __dirname = dirname(fileURLToPath(import.meta.url))
12
- export const DESC = (name) => readFileSync(join(__dirname, "..", "tools", `${name}.md`), "utf8")
13
-
14
- export const MAX_READ_LINES = 2000
15
- export const MAX_OUTPUT_CHARS = 200_000
16
-
17
- const ENCODING_DETECT_MAX_TRIM = 3
18
- const SYNTAX_CHECK_TIMEOUT = 10000
19
- export const BASH_TIMEOUT_MS = 120_000
20
- export const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".turbo", "coverage"])
21
-
22
- /** SSRF guard: check if a hostname is private/internal. Shared by web.mjs and codemode.mjs.
23
- * Returns TRUE for private hosts — callers block them. */
24
- export function isPrivateHost(hostname) {
25
- const h = hostname.toLowerCase()
26
- // Local loopback + link-local names: BLOCK (true). These returned false
27
- // before — the guard was inverted for the most common SSRF targets
28
- // (localhost/127.x reach internal services unchecked).
29
- if (h === "localhost" || h === "0.0.0.0" || h.endsWith(".localhost")) return true
30
- if (h === "127.0.0.1" || h.startsWith("127.")) return true
31
- if (h === "169.254.169.254" || h === "metadata.google.internal") return true
32
- // IPv6 private ranges — only check if host contains ":"
33
- // fe80::/10 link-local covers fe80:…febf:… — startsWith("fe80:") is the
34
- // practical subset (fe8/fe9/fea/feb all begin fe8/feb — full range regex
35
- // would be /^fe[89ab][0-9a-f]:/; startsWith fe8 + fe9 + fea + feb covers it).
36
- if (h.includes(":")) {
37
- if (h === "::1" || h.startsWith("fc") || h.startsWith("fd")) return true
38
- if (/^fe[89ab][0-9a-f]:/.test(h)) return true
39
- }
40
- const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
41
- if (m) {
42
- // Octet range is NOT validated (999.10.0.1 parses but matches no private
43
- // prefix → treated as public). Intentional: the guard checks known-private
44
- // prefixes; invalid IPs are harmless false-negatives for SSRF purposes.
45
- const [a, b] = [Number(m[1]), Number(m[2])]
46
- if (a === 10 || (a === 172 && b >= 16 && b <= 31) || a === 192 && b === 168 || a === 169 && b === 254 || a === 0) return true
47
- }
48
- return false
49
- }
50
-
51
- /** Normalize Windows line endings to Unix: \r\n → \n.
52
- * Applied on every text-file read so that edit/hash matching
53
- * and hash computation are platform-consistent. */
54
- export function normalizeEOL(text) {
55
- return text.replace(/\r\n/g, "\n")
56
- }
57
-
58
- /** Detect a file's EOL style by the type of its FIRST newline: "\r\n" first →
59
- * the whole file is written back as CRLF; a bare "\n" or no newline → LF.
60
- * Never counts occurrences (mixed files follow the first line's style). */
61
- export function detectFileEol(text) {
62
- const i = text.indexOf("\n")
63
- return i > 0 && text[i - 1] === "\r" ? "\r\n" : "\n"
64
- }
65
-
66
- /** Join lines with the EOL style detected from the original text (write-back restore). */
67
- export function joinWithEol(lines, originalText) {
68
- return lines.join(detectFileEol(originalText))
69
- }
70
-
71
- const MAJORITY_EOL_MAX_FILES = 20
72
- const EOL_SNIFF_BYTES = 4096
73
-
74
- /** Majority EOL style of a directory's existing files (≤20 files, first 4KB each).
75
- * New files follow the directory's majority style; empty dir / tie / LF majority → "\n". */
76
- export function majorityEol(dirPath) {
77
- let names
78
- try { names = readdirSync(dirPath) } catch { return "\n" }
79
- let crlf = 0, lf = 0
80
- for (const name of names) {
81
- if (crlf + lf >= MAJORITY_EOL_MAX_FILES) break
82
- try {
83
- const p = join(dirPath, name)
84
- if (!statSync(p).isFile()) continue
85
- const fd = openSync(p, "r")
86
- let head = ""
87
- try {
88
- const buf = Buffer.alloc(EOL_SNIFF_BYTES)
89
- const n = readSync(fd, buf, 0, EOL_SNIFF_BYTES, 0)
90
- head = buf.subarray(0, n).toString("utf8")
91
- } finally {
92
- closeSync(fd)
93
- }
94
- if (detectFileEol(head) === "\r\n") crlf++
95
- else lf++
96
- } catch { /* unreadable entry — skip */ }
97
- }
98
- return crlf > lf ? "\r\n" : "\n"
99
- }
100
-
101
- const CANDIDATE_MAX_LEN = 500
102
- const CANDIDATE_PREVIEW_LEN = 80
103
-
104
- /** Longest-common-substring length (rolling-row DP). Inputs are pre-truncated by the caller. */
105
- function lcsLength(a, b) {
106
- // Reused DP buffers (review R9#6): per-line allocation caused GC pressure on
107
- // large files — hoist two module-level rows, grow to fit, swap by index.
108
- const need = b.length + 1
109
- if (_lcsBuf0.length < need) {
110
- const size = Math.max(need, _lcsBuf0.length * 2)
111
- _lcsBuf0 = new Uint16Array(size)
112
- _lcsBuf1 = new Uint16Array(size)
113
- }
114
- let prev = _lcsBuf0, cur = _lcsBuf1
115
- prev.fill(0, 0, need)
116
- let best = 0
117
- for (let i = 1; i <= a.length; i++) {
118
- cur[0] = 0
119
- const ca = a.charCodeAt(i - 1)
120
- for (let j = 1; j < need; j++) {
121
- if (ca === b.charCodeAt(j - 1)) {
122
- const v = prev[j - 1] + 1
123
- cur[j] = v
124
- if (v > best) best = v
125
- } else cur[j] = 0 // must reset — buffer is reused
126
- }
127
- const t = prev; prev = cur; cur = t
128
- }
129
- return best
130
- }
131
- let _lcsBuf0 = new Uint16Array(0), _lcsBuf1 = new Uint16Array(0)
132
- /** Line-level similarity candidates for a failed edit: score = LCS(oldString, line) / max(len).
133
- * Multi-line old_string matches on its FIRST line only (failures usually diverge there).
134
- * Both sides are truncated to 500 chars before scoring so minified files can't blow the budget.
135
- * Returns up to topN [{ line (1-based), preview, score }] with score >= threshold, best first. */
136
- export function findCandidates(lines, oldString, topN = 3, threshold = 0.5) {
137
- const needle = oldString.split("\n")[0].slice(0, CANDIDATE_MAX_LEN)
138
- if (!needle) return []
139
- const scored = []
140
- for (let i = 0; i < lines.length; i++) {
141
- const raw = lines[i]
142
- if (!raw) continue
143
- const line = raw.length > CANDIDATE_MAX_LEN ? raw.slice(0, CANDIDATE_MAX_LEN) : raw
144
- const longer = Math.max(needle.length, line.length)
145
- const shorter = Math.min(needle.length, line.length)
146
- // LCS ≤ shorter side — a length ratio below the threshold can never reach it; skip the DP.
147
- if (shorter / longer < threshold) continue
148
- const score = lcsLength(needle, line) / longer
149
- if (score >= threshold) scored.push({ line: i + 1, preview: raw.slice(0, CANDIDATE_PREVIEW_LEN), score })
150
- }
151
- scored.sort((a, b) => b.score - a.score || a.line - b.line)
152
- return scored.slice(0, topN)
153
- }
154
-
155
- /** Appended to hashline_edit results when the file contains U+FFFD (encoding-corruption probe). */
156
- export const FFFD_WARNING = "⚠ file contains U+FFFD (replacement char) — encoding may be corrupted; hash-based addressing may be unreliable. Consider fixing the file encoding first."
157
-
158
- /** Convert to OpenAI tools parameter format */
159
- export function toOpenAISchema(tool) {
160
- return {
161
- type: "function",
162
- function: {
163
- name: tool.name,
164
- description: tool.description,
165
- parameters: tool.parameters,
166
- },
167
- }
168
- }
169
-
170
- /** Strip ANSI escape sequences */
171
- export function sanitizeOutput(s) {
172
- return s
173
- // 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
174
- .replace(/\x1b\[[0-9;?]*[\x40-\x7E]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[=>#][0-9]?/g, "")
175
- .replace(/\r\n/g, "\n")
176
- .replace(/\r/g, "\n")
177
- }
178
-
179
- /** Truncate text to max chars, appending a truncation notice */
180
- export function truncate(text, max = MAX_OUTPUT_CHARS) {
181
- if (text.length <= max) return text
182
- return text.slice(0, max) + `\n[... truncated: ${text.length - max} chars omitted — redirect to a file if you need the full output]`
183
- }
184
-
185
- /** Streaming decoder: encoding sniffing ASCII→UTF-8→GBK.
186
- * KNOWN LIMITATION (accepted): the fallback is hardcoded to GBK (Chinese) —
187
- * Shift-JIS/EUC-KR pages decode as mojibake. Real-world usage is dominated
188
- * by UTF-8; a charset-aware variant would need the Content-Type header.
189
- * Each call creates an independent decoder instance — must not be shared across parallel streams (internal decoder state accumulates). */
190
- export function makeDecoder() {
191
- let decoder = null
192
- let pending = Buffer.alloc(0)
193
- return (d, flush = false) => {
194
- pending = Buffer.concat([pending, d])
195
- if (!decoder) {
196
- const hasHighByte = pending.some((b) => b >= 0x80)
197
- if (!hasHighByte) { const s = pending.toString("ascii"); pending = Buffer.alloc(0); return s }
198
- for (let trim = 0; trim <= ENCODING_DETECT_MAX_TRIM && !decoder; trim++) {
199
- try { new TextDecoder("utf-8", { fatal: true }).decode(pending.subarray(0, pending.length - trim)); decoder = new TextDecoder("utf-8") }
200
- catch { /* continue */ }
201
- }
202
- if (!decoder) decoder = new TextDecoder("gbk")
203
- }
204
- const s = decoder.decode(pending, { stream: !flush })
205
- pending = Buffer.alloc(0)
206
- return s
207
- }
208
- }
209
-
210
- /** Single-file git diff. Silently returns empty on failure. Large diffs that exceed maxBuffer are truncated rather than swallowed. */
211
- export function gitDiffOne(cwd, abs) {
212
- try {
213
- const diff = execFileSync("git", ["--no-pager", "diff", "--no-color", "--", abs], {
214
- cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 10 * 1024 * 1024,
215
- }).trim()
216
- if (!diff) return ""
217
- const lines = diff.split("\n")
218
- if (lines.length <= 200) return diff
219
- return lines.slice(0, 200).join("\n") + `\n... (${lines.length - 200} more diff lines)`
220
- } catch (e) {
221
- // maxBuffer overflow: e.stdout contains partial collected output; other errors (non-git repo etc.) return empty
222
- if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" && e.stdout) {
223
- const lines = e.stdout.toString().split("\n")
224
- return lines.slice(0, 200).join("\n") + `\n... (diff too large, showing first 200 of more lines)`
225
- }
226
- return ""
227
- }
228
- }
229
-
230
- /** Auto syntax check after file modification */
231
- export async function autoSyntaxCheck(abs) {
232
- if (!/\.(m?js)$/i.test(abs)) return ""
233
- try {
234
- await new Promise((resolve, reject) => {
235
- const child = execFile("node", ["--check", abs], { timeout: SYNTAX_CHECK_TIMEOUT, stdio: ["ignore", "pipe", "pipe"] })
236
- let stderr = ""
237
- child.stderr.on("data", (d) => { stderr += d.toString() })
238
- child.on("error", reject)
239
- child.on("close", (code) => {
240
- if (code === 0) resolve()
241
- else {
242
- const err = new Error(stderr.trim() || `node --check exited with code ${code}`)
243
- err.stderr = stderr
244
- reject(err)
245
- }
246
- })
247
- })
248
- return "\nSyntax: OK"
249
- } catch (e) {
250
- const err = (e.stderr || e.stdout || e.message || "").toString().split("\n").slice(0, 3).join("\n")
251
- return `\nSyntax: FAILED — ${err}\n(If this file was corrupted by a bad edit, recover it from a checkpoint: checkpoint action=list then action=rewind with the latest id.)`
252
- }
253
- }
254
-
255
- /** Resolve realpath by walking up the directory tree */
256
- function realpathNearest(abs) {
257
- let cur = abs
258
- const tail = []
259
- while (!existsSync(cur)) {
260
- const parent = dirname(cur)
261
- if (parent === cur) return abs
262
- tail.unshift(cur.slice(parent.length + 1))
263
- cur = parent
264
- }
265
- try { const real = realpathSync(cur); return tail.length ? join(real, ...tail) : real }
266
- catch { return abs }
267
- }
268
-
269
- // cwd is effectively constant per CLI session — the cache never grows in
270
- // practice. A long-running server with rotating cwds would leak; revisit with
271
- // an LRU if that usage ever appears.
272
- const realCwdCache = new Map()
273
- /** Resolve cwd to realpath, cached */
274
- function realCwd(cwd) {
275
- if (!realCwdCache.has(cwd)) realCwdCache.set(cwd, realpathNearest(resolve(cwd)))
276
- return realCwdCache.get(cwd)
277
- }
278
-
279
- /** Resolve a user-supplied path relative to cwd — no directory boundary assertion
280
- * (§10.1 2026-09-02: workspace confinement removed; resolveInCwd ≡ resolveExternal,
281
- * trust model + approval gate is the only guard — same boundary as bash). */
282
- export function resolveInCwd(ctx, p) {
283
- return resolveExternal(ctx, p)
284
- }
285
-
286
- /** Resolve a path relative to cwd without boundary check — kept for compatibility (≡ resolveInCwd) */
287
- export function resolveExternal(ctx, p) {
288
- const cwd = realCwd(ctx.cwd)
289
- return resolve(cwd, p)
290
- }
291
-
292
- /** Coarse segmentation for destructive pre-check (also splits on > >> < so destructive detection still works through redirection) */
293
- export function shellSegments(command) {
294
- return command.split(/&&|\|\||>>|\$\(|[;|\n<>]|`|[(]/)
295
- }
296
-
297
- /**
298
- * Blank out quoted regions (single/double/backtick) with spaces, preserving length.
299
- * Lets detectDanger ignore shell metacharacters inside quoted script bodies —
300
- * e.g. `node -e "if (a > b) …"` comparisons are not danger signals.
301
- */
302
- function blankQuoted(command) {
303
- let out = ""
304
- let quote = null
305
- for (let i = 0; i < command.length; i++) {
306
- const ch = command[i]
307
- if (quote) {
308
- if (ch === "\\" && quote !== "'") { out += " "; i++; out += " "; continue }
309
- if (ch === quote) { quote = null; out += " "; continue }
310
- // Backticks are COMMAND SUBSTITUTION — the content executes, so it must
311
- // stay visible to detectDanger (echo `cat > /tmp/x` still runs). Only '
312
- // and " are literal regions.
313
- out += quote === "`" ? ch : " "
314
- } else if (ch === "'" || ch === '"' || ch === "`") {
315
- quote = ch
316
- out += " "
317
- } else {
318
- out += ch
319
- }
320
- }
321
- return out
322
- }
323
-
324
- /**
325
- * Whether a single command segment is destructive — ALWAYS FALSE (deliberate).
326
- *
327
- * 决策(2026-08):文本拦截对恶意模型是安全剧场——空白变体/heredoc/node -e/写脚本执行
328
- * 都能绕过,拦住的只有正常操作(如清理临时目录、rm node_modules 重装)。
329
- * 真实防线在工具审批层(autoApprove)与快照兜底(gitGuardSnapshot / checkpoint auto-snapshot),
330
- * 与 env 过滤、git 破坏操作"快照后放行、永不拦截"同一哲学。
331
- * 项目工具自带确认门(如 thin5 scripts/db.mjs --write/--danger)不应被双重拦截。
332
- */
333
- export function isDestructiveCommand() {
334
- return false
335
- }
336
-
337
- /**
338
- * 危险命令识别(只标注、不拦截)——参考 kimi-code apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts
339
- * DANGER_PATTERNS。定位:给审批中的人打红色警告标签,提升决策信息,不是机器防线。
340
- * 拦截无用(可绕过),标注有用(人看到了才知道该多看一眼)。
341
- */
342
- const DANGER_PATTERNS = [
343
- { pattern: /\brm\s+(-[a-zA-Z]*[rRfF][a-zA-Z]*|--recursive|--force)/i, label: "recursive delete" },
344
- { pattern: /\bsudo\b/i, label: "sudo" },
345
- { pattern: /\b(curl|wget)\b[^|]*\|\s*(sh|bash|zsh)\b/i, label: "pipe to shell" },
346
- { pattern: /\bdd\b[^|]*\bof=/i, label: "dd write" },
347
- { pattern: /\bmkfs\b/i, label: "mkfs" },
348
- { pattern: />\s*\/dev\/(sd|nvme|disk|hd)/i, label: "write to raw device" },
349
- { pattern: /\bchmod\s+(?:-[rR]\s+)?777\b/i, label: "chmod 777" },
350
- { pattern: /:\(\)\s*\{\s*:\|:&\s*\}/i, label: "fork bomb" },
351
- ]
352
-
353
- /** 返回危险标注 label(如 "recursive delete");无危险返回 undefined。
354
- * 引号感知:引号(单/双)内的内容清空后再检测——commit message、echo 文本等
355
- * 纯文本不误标;危险命令(rm -rf "$dir")的参数在引号外,仍命中。
356
- * 反引号内容保留(命令替换会执行)。 */
357
- export function detectDanger(command) {
358
- const s = blankQuoted(String(command ?? ""))
359
- for (const { pattern, label } of DANGER_PATTERNS) {
360
- if (pattern.test(s)) return label
361
- }
362
- return undefined
363
- }
364
-
365
-
366
- /** Glob dialect (TOOLS.md §17) lives in glob-dialect.mjs — single authority for
367
- * brace expansion / exclusion / explicit syntax errors (extracted 2026-09-06 —
368
- * advisor #5: shared.mjs exceeded 500 lines). Re-exported so existing importers
369
- * (system.mjs tools, ls filter, tests) keep their import paths unchanged. */
370
- export {
371
- globToRegex,
372
- splitGlobPatterns,
373
- compileGlobMatchers,
374
- GLOB_EXTGLOB_ERROR,
375
- GLOB_EMPTY_BRACE_ERROR,
376
- GLOB_UNCLOSED_BRACE_ERROR,
377
- GLOB_NESTED_BRACE_ERROR,
378
- } from "./glob-dialect.mjs"
379
-
380
-
381
- /** Decode a numeric HTML entity to its code point — invalid/out-of-range
382
- * values (e.g. &#999999999999;) must not throw RangeError; keep the source
383
- * text as-is (display-only residue is acceptable). */
384
- function decodeNumericEntity(_, digits) {
385
- const n = Number(digits)
386
- return Number.isSafeInteger(n) && n <= 0x10ffff ? String.fromCodePoint(n) : _
387
- }
388
-
389
- /** Strip HTML tags. KNOWN LIMITATION (accepted): the `/<[^>]+>/g` regex treats
390
- * the first `>` (or a `<` inside an attribute value) as the tag boundary —
391
- * `<img alt="a > b">` truncates the match and leaves text residue. A full
392
- * HTML parser is out of scope; real-world HTML with angle brackets in
393
- * attributes is rare and the residue is display-only (never parsed). */
394
- export function stripTags(html) {
395
- return html
396
- .replace(/<[^>]+>/g, "")
397
- .replace(/&#0*(\d+);/g, (m, n) => decodeNumericEntity(m, n))
398
- .replace(/&#x([0-9a-fA-F]+);/g, (m, h) => decodeNumericEntity(m, parseInt(h, 16)))
399
- .replace(/&nbsp;|&ensp;/g, " ")
400
- .replace(/&lt;/g, "<")
401
- .replace(/&gt;/g, ">")
402
- .replace(/&quot;/g, '"')
403
- .replace(/&amp;/g, "&")
404
- .replace(/\s+/g, " ")
405
- .trim()
406
- }
407
-
408
- /** HTML → plain text: strip scripts/styles, newline block tags, strip tags, decode entities, compress blank lines */
409
- export function htmlToText(html) {
410
- return html
411
- .replace(/<script[\s\S]*?<\/script>/gi, "")
412
- .replace(/<style[\s\S]*?<\/style>/gi, "")
413
- .replace(/<noscript[\s\S]*?<\/noscript>/gi, "")
414
- .replace(/<\/(p|div|li|ul|ol|h[1-6]|tr|table|section|article|header|footer|blockquote|pre)>/gi, "\n")
415
- .replace(/<br\s*\/?>/gi, "\n")
416
- .replace(/<li[^>]*>/gi, "- ")
417
- .replace(/<[^>]+>/g, "")
418
- .replace(/&#0*(\d+);/g, (m, n) => decodeNumericEntity(m, n))
419
- .replace(/&#x([0-9a-fA-F]+);/g, (m, h) => decodeNumericEntity(m, parseInt(h, 16)))
420
- .replace(/&nbsp;|&ensp;/g, " ")
421
- .replace(/&lt;/g, "<")
422
- .replace(/&gt;/g, ">")
423
- .replace(/&quot;/g, '"')
424
- .replace(/&amp;/g, "&") // &amp; must be decoded last, otherwise &amp;lt; gets double-decoded to <
425
- .replace(/[ \t]+/g, " ")
426
- .replace(/\n\s*\n\s*\n+/g, "\n\n")
427
- .trim()
428
- }
429
-
430
- /** Execute a git command. maxBuffer 10MB prevents large diff/log overflow; on overflow, returns truncated partial output rather than empty.
431
- * config: optional array of `-c key=value` overrides (e.g. ["http.proxy=http://10.2.2.112:3128"]) —
432
- * inserted verbatim after `git`, so network actions (push/fetch/pull/ls-remote) can route through a proxy. */
433
- export function runGit(cwd, cmdArgs, config = []) {
434
- try {
435
- return execFileSync("git", [...config, ...cmdArgs], { cwd, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }).trim().replace(/\r/g, "")
436
- } catch (e) {
437
- // maxBuffer overflow: e.stdout contains partial collected output — return it
438
- // (callers show "(truncated)"-style tails). ALL OTHER errors (non-git repo,
439
- // permission, bad command) return "" — matching gitDiffOne's pattern: a
440
- // failed git call must not masquerade as partial success.
441
- if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" && e.stdout) {
442
- return String(e.stdout).trim().replace(/\r/g, "").split("\n").slice(0, 200).join("\n")
443
- }
444
- return ""
445
- }
446
- }
package/src/tools/tree.md DELETED
@@ -1,14 +0,0 @@
1
- Generate a directory tree of the codebase (default depth 3). Skips dotfiles, .git/node_modules/dist/build/bin/obj and other build/vendor dirs, and binary files. Use to quickly see which modules exist and where files live.
2
-
3
- **Route to tree instead of bash:**
4
- - `tree` / `find .` / `dir /s` → tree
5
-
6
- Parameters:
7
- - path: Root directory (default cwd)
8
- - depth: Tree depth (default 3, max 6). Directories are listed before files, both sorted.
9
-
10
- Notes:
11
- - Capped at 200 entries.
12
- - Directories end with `/`; tree-drawing uses `├──`/`└──`/`│`.
13
- - Use depth for a shallow overview; use `ls` for one directory, `glob` for a specific file pattern.
14
- - Returns the directory text tree — directories first (`dir/`), files after, both sorted, capped at 200 entries.
@@ -1,66 +0,0 @@
1
- /**
2
- * tree.mjs — directory tree tool (parity with thinworker `repomap`).
3
- * Renders a repo's directory tree (default depth 3), skipping dotfiles,
4
- * build/vendor dirs and binary files, so the model can see which modules
5
- * exist without shelling out to `tree`/`find`.
6
- */
7
- import { DESC, truncate, resolveInCwd } from "./shared.mjs"
8
- import { readdir, stat } from "node:fs/promises"
9
- import { join, basename, extname } from "node:path"
10
-
11
- const MAX_ENTRIES = 200
12
- const DEFAULT_DEPTH = 3
13
- const SKIP_DIRS = new Set(["node_modules", "bin", "obj", "dist", "build", "coverage", "turbo", ".git", ".thincoder", ".vs", ".venv", "__pycache__", ".idea"])
14
- const BINARY_EXTS = new Set([".exe", ".dll", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".docx", ".xlsx", ".pptx", ".zip", ".7z", ".mp3", ".mp4", ".woff", ".woff2", ".ico"])
15
-
16
- export const treeTool = {
17
- name: "tree",
18
- description: DESC("tree"),
19
- parameters: {
20
- type: "object",
21
- properties: {
22
- path: { type: "string", description: "Root directory (default cwd)" },
23
- depth: { type: "integer", description: `Tree depth (default ${DEFAULT_DEPTH}, max 6)` },
24
- },
25
- required: [],
26
- },
27
- readonly: true,
28
- async execute(args, ctx) {
29
- const root = resolveInCwd(ctx, args.path ?? ".")
30
- let st
31
- try { st = await stat(root) } catch { return `Error: directory not found: ${args.path ?? "."}` }
32
- if (!st.isDirectory()) return `Error: not a directory: ${args.path ?? "."}`
33
- const d = Number(args.depth)
34
- const maxDepth = Number.isInteger(d) && d > 0 ? Math.min(d, 6) : DEFAULT_DEPTH
35
- const lines = [basename(root) + "/"]
36
- const state = { count: 1, capped: false }
37
- await walk(root, 0, maxDepth, "", lines, state)
38
- return truncate(lines.join("\n"))
39
- },
40
- }
41
-
42
- async function walk(dir, depth, maxDepth, prefix, lines, state) {
43
- if (state.capped) return
44
- let entries
45
- try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }
46
- const items = []
47
- for (const e of entries) {
48
- if (e.name.startsWith(".")) continue // dotfiles + dotdirs
49
- const isDir = e.isDirectory()
50
- if (isDir) { if (SKIP_DIRS.has(e.name)) continue; items.push({ name: e.name, isDir: true }) }
51
- else if (!BINARY_EXTS.has(extname(e.name).toLowerCase())) items.push({ name: e.name, isDir: false })
52
- }
53
- items.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))
54
-
55
- for (let i = 0; i < items.length; i++) {
56
- if (state.capped) return
57
- if (state.count >= MAX_ENTRIES) { state.capped = true; lines.push(prefix + "…(更多项已省略)"); return }
58
- const { name, isDir } = items[i]
59
- const isLast = i === items.length - 1
60
- lines.push(prefix + (isLast ? "└── " : "├── ") + (isDir ? name + "/" : name))
61
- state.count++
62
- if (isDir && depth + 1 < maxDepth) {
63
- await walk(join(dir, name), depth + 1, maxDepth, prefix + (isLast ? " " : "│ "), lines, state)
64
- }
65
- }
66
- }
@@ -1,22 +0,0 @@
1
- Wait until a condition becomes true — polls the condition every interval_ms and returns as soon as it holds or the timeout_ms ceiling passes. Use it ONLY for genuinely asynchronous waits: an async subagent/consult still settling, a file appearing, a port opening. Synchronous tools (advisor, blocking subagent spawn) return when done — waiting after them is NOT needed and wastes time.
2
-
3
- Parameters:
4
- - condition (required): a semantic condition expression:
5
- - `advisor settled` — an in-flight advisor review session has finished (async advisor-role children settled)
6
- - `subagent id:N done` — async subagent N (the id your async spawn ack returned) has settled
7
- - `consult done` — every consult_start session has drained
8
- - `file exists:path` — the file at path exists (path relative to cwd)
9
- - `port open:N` — something is listening on 127.0.0.1 port N
10
- - The agent-internal conditions (advisor / subagent / consult) apply to ASYNC sessions only — a synchronous call already completed before it returned and has nothing to wait for. An unknown condition is an explicit error (`wait_for: unsupported condition "..."` — the supported forms are listed above), never a silent wait.
11
- - interval_ms: poll interval (default 1000, floor 100 — polling never busy-spins)
12
- - timeout_ms: overall ceiling (default 30000; config.json `agent.waitForTimeoutMs` overrides the default; hard cap 600000 like the execute tool)
13
-
14
- Returns:
15
- - `wait_for: condition satisfied after Nms (N checks): "<condition>"` — condition held
16
- - `wait_for: timed out after Nms waiting for "<condition>" ...` — ceiling passed with the condition still false (never burns beyond the ceiling)
17
- - interrupts (Ctrl+C / cancel) exit immediately; malformed conditions and unsupported syntax error out explicitly
18
-
19
- Notes:
20
- - Read-only and non-destructive — it only observes (agent pools, the filesystem, a local port probe).
21
- - Blocking by design — call it ALONE in a turn, not batched with calls that depend on its result.
22
- - Do not use it to wait after synchronous tools (advisor / blocking subagent spawn / verify return only when done).