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,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).