thincoder 0.12.62 → 0.12.63

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