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,314 +0,0 @@
1
- /**
2
- * repomap.mjs — repo dependency outline
3
- * Real-time import/export parsing, generates compact text for LLMs to understand code structure.
4
- * No index stored — reads and parses files on each call, ~50ms.
5
- */
6
- import { existsSync } from "node:fs"
7
- import { readFile, stat } from "node:fs/promises"
8
- import { join } from "node:path"
9
- import { normalizeEOL } from "./shared.mjs"
10
-
11
- /** Extract JS/TS file import paths (normalize by stripping .ts/.js/.mjs suffixes) */
12
- function parseImports(lines, ext) {
13
- const imports = []
14
- const text = lines.join("\n")
15
- // standard import
16
- const re = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+\s*,?\s*(?:{[^}]*})?)\s*from\s*['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g
17
- let m
18
- while ((m = re.exec(text))) {
19
- const raw = m[1] || m[2]
20
- if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
21
- imports.push(normalizeExt(raw))
22
- }
23
- // re-export: export { x } from './module'
24
- const reExportRe = /export\s*\{[^}]*\}\s*from\s*['"]([^'"]+)['"]/g
25
- while ((m = reExportRe.exec(text))) {
26
- const raw = m[1]
27
- if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
28
- imports.push(normalizeExt(raw))
29
- }
30
- return [...new Set(imports)]
31
- }
32
-
33
- /** Extract JS/TS file export symbols */
34
- function parseExports(lines, ext) {
35
- const exports = []
36
- const text = lines.join("\n")
37
- // export function/class/const/let/var name
38
- const namedRe = /export\s+(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+)|(?:const|let|var)\s+(\w+))/g
39
- let m
40
- while ((m = namedRe.exec(text))) {
41
- exports.push(m[1] || m[2] || m[3])
42
- }
43
- // export default function/class name / export default expression
44
- const defaultRe = /export\s+default\s+(?:(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+))|(\w+))/g
45
- while ((m = defaultRe.exec(text))) {
46
- const name = m[1] || m[2] || m[3]
47
- if (name) exports.push(name)
48
- else if (!exports.some((e) => e === "default")) exports.push("default")
49
- }
50
- // export { a, b as c } — prefer the "as" alias as the exported name
51
- const braceRe = /export\s*\{([^}]+)\}/g
52
- while ((m = braceRe.exec(text))) {
53
- for (const name of m[1].split(",")) {
54
- const parts = name.trim().split(/\s+/)
55
- // "a as b" → b (exported name), "a" → a
56
- const exported = parts.length >= 3 ? parts[2] : parts[0]
57
- if (exported) exports.push(exported)
58
- }
59
- }
60
- // export const { a, b } = ... (destructured export)
61
- const destructRe = /export\s+(?:const|let|var)\s*\{([^}]+)\}\s*=/g
62
- while ((m = destructRe.exec(text))) {
63
- for (const name of m[1].split(",")) {
64
- const parts = name.trim().split(/\s*:\s*/)
65
- const n = parts[0].trim()
66
- if (n) exports.push(n)
67
- }
68
- }
69
- return [...new Set(exports)]
70
- }
71
-
72
- /** Extract Python imports and top-level def/class */
73
- function parsePyOutline(lines) {
74
- const imports = []
75
- const symbols = []
76
- for (const line of lines) {
77
- const fromRe = line.match(/^from\s+(\S+)\s+import\s+(.+)/)
78
- if (fromRe) {
79
- const rel = pyRelPath(fromRe[1])
80
- if (rel) imports.push(rel)
81
- continue
82
- }
83
- const impRe = line.match(/^import\s+(.+)/)
84
- if (impRe) {
85
- for (const mod of impRe[1].split(",")) {
86
- const rel = pyRelPath(mod.trim().split(/\s+/)[0])
87
- if (rel) imports.push(rel)
88
- }
89
- continue
90
- }
91
- const defRe = line.match(/^(?:async\s+)?(?:def|class)\s+(\w+)/)
92
- if (defRe) symbols.push(defRe[1])
93
- }
94
- return { imports: [...new Set(imports)], symbols: [...new Set(symbols)] }
95
- }
96
-
97
- /**
98
- * Python relative import → relative file path:
99
- * Leading n dots mean go up n-1 levels ("." = current package), module dots become path separators.
100
- * Non-relative imports (not starting with .) or bare package imports ("from . import x") return null.
101
- */
102
- function pyRelPath(mod) {
103
- if (!mod?.startsWith(".")) return null
104
- const dots = mod.match(/^\.+/)[0].length
105
- const rest = mod.slice(dots).replaceAll(".", "/")
106
- if (!rest) return null
107
- return normalizeExt("../".repeat(dots - 1) + rest)
108
- }
109
-
110
- function normalizeExt(p) {
111
- return p.replace(/\.(m?js|jsx|tsx?)$/i, "")
112
- }
113
-
114
- /**
115
- * Internal: scan all files, build forward dependency graph + reverse reference graph.
116
- * Returns { deps, importers, fileCount } shared by buildOutline / buildSummary.
117
- */
118
- async function _buildDepGraph(db, cwd) {
119
- const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ORDER BY path`).all().map((r) => r.path)
120
- if (allFiles.length === 0) return null
121
-
122
- const deps = new Map() // path → { imports: Set, exports: Set, size: number, dir: string }
123
- const importers = new Map() // importee → Set<importer>
124
-
125
- for (let i = 0; i < allFiles.length; i++) {
126
- const rel = allFiles[i]
127
- const abs = join(cwd, ...rel.split("/"))
128
- if (!existsSync(abs)) continue
129
- // Large file guard: skip files over 10MB to prevent OOM
130
- const fst = await stat(abs).catch(() => null)
131
- if (fst && fst.size > 10_000_000) continue
132
- let text
133
- try { text = normalizeEOL(await readFile(abs, "utf8")) } catch { continue }
134
- const lines = text.split("\n")
135
- const ext = rel.slice(rel.lastIndexOf(".")).toLowerCase()
136
-
137
- let imports, exports
138
- if (ext === ".py") {
139
- const py = parsePyOutline(lines)
140
- imports = py.imports
141
- exports = py.symbols
142
- } else {
143
- imports = parseImports(lines, ext)
144
- exports = parseExports(lines, ext)
145
- }
146
-
147
- // Resolve import paths to relative paths (handle ./ ../)
148
- const resolved = []
149
- for (let imp of imports) {
150
- if (imp.startsWith("./")) imp = imp.slice(2)
151
- const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ""
152
- const parts = imp.split("/")
153
- if (parts[0] === "..") {
154
- const up = dir.split("/").filter(Boolean)
155
- let i = 0
156
- while (parts[i] === ".." && up.length > 0) { up.pop(); i++ }
157
- resolved.push([...up, ...parts.slice(i)].join("/"))
158
- } else {
159
- resolved.push(dir ? `${dir}/${imp}` : imp)
160
- }
161
- }
162
-
163
- const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "."
164
- deps.set(rel, { imports: new Set(resolved), exports: new Set(exports), size: Math.floor(text.length / 1024), dir })
165
-
166
- for (const r of resolved) {
167
- if (!importers.has(r)) importers.set(r, new Set())
168
- importers.get(r).add(rel)
169
- }
170
- // Yield the event loop every 20 files to prevent TUI freeze
171
- if (i % 20 === 19) await new Promise(r => setImmediate(r))
172
- }
173
-
174
- return { deps, importers, fileCount: allFiles.length }
175
- }
176
-
177
- /**
178
- * Generate a compact architecture summary (replaces the old full-dump injection).
179
- * Three layers, each with decreasing information density:
180
- * 1. Directory-level dependencies (meaningful only for multi-directory projects, skipped for single-directory)
181
- * 2. Hub files Top-12 (most-imported files — architecture skeleton)
182
- * 3. Entry points (files with no importers — startup/top-level entry points)
183
- * Output is naturally bounded (~1000-2000 chars), no more OUTLINE_INJECT_MAX hard truncation.
184
- */
185
- export async function buildSummary(db, cwd) {
186
- const graph = await _buildDepGraph(db, cwd)
187
- if (!graph) return "(no indexed source files; run codeSync or /reindex first)"
188
- const { deps, importers, fileCount } = graph
189
-
190
- const out = []
191
- out.push(`${fileCount} source files indexed.`)
192
-
193
- // 1) Directory-level dependencies
194
- const dirDeps = new Map() // dir → Set<imported-dir>
195
- const dirSet = new Set()
196
- for (const [rel, d] of deps) {
197
- dirSet.add(d.dir)
198
- if (!dirDeps.has(d.dir)) dirDeps.set(d.dir, new Set())
199
- for (const imp of d.imports) {
200
- const targetDir = imp.includes("/") ? imp.slice(0, imp.lastIndexOf("/")) : "."
201
- if (targetDir !== d.dir) dirDeps.get(d.dir).add(targetDir)
202
- }
203
- }
204
- if (dirSet.size > 1) {
205
- out.push("Directory dependencies:")
206
- for (const dir of [...dirSet].sort()) {
207
- const targets = dirDeps.get(dir)
208
- if (targets?.size) {
209
- out.push(` ${dir}/ → ${[...targets].sort().join(", ")}/`)
210
- } else {
211
- out.push(` ${dir}/ (leaf)`)
212
- }
213
- }
214
- }
215
-
216
- // 2) Hub files Top-12: sorted by import count descending
217
- const HUB_LIMIT = 12
218
- const hubScores = []
219
- for (const [rel] of deps) {
220
- const key = rel.replace(/\.(m?js|jsx|tsx?)$/i, "")
221
- const rev = importers.get(key)
222
- if (rev?.size) hubScores.push({ path: rel, count: rev.size })
223
- }
224
- hubScores.sort((a, b) => b.count - a.count)
225
- if (hubScores.length > 0) {
226
- out.push(`Hub files (by inbound dependencies, top ${Math.min(hubScores.length, HUB_LIMIT)}):`)
227
- for (const h of hubScores.slice(0, HUB_LIMIT)) {
228
- const d = deps.get(h.path)
229
- const kb = d?.size ? ` (${d.size} KB)` : ""
230
- const key = h.path.replace(/\.(m?js|jsx|tsx?)$/i, "")
231
- const rev = importers.get(key)
232
- const shortRefs = rev.size <= 5
233
- ? [...rev].join(", ")
234
- : [...rev].slice(0, 4).join(", ") + ` +${rev.size - 4} more`
235
- out.push(` ${h.path}${kb} — imported by: ${shortRefs}`)
236
- }
237
- }
238
-
239
- // 3) Entry points: files not imported by others (leaf/entry)
240
- const entries = []
241
- for (const [rel] of deps) {
242
- const key = rel.replace(/\.(m?js|jsx|tsx?)$/i, "")
243
- if (!importers.has(key) || importers.get(key).size === 0) {
244
- entries.push(rel)
245
- }
246
- }
247
- if (entries.length > 0 && entries.length < fileCount) {
248
- const limit = 8
249
- const shown = entries.slice(0, limit)
250
- out.push(`Entry points (not imported by others):`)
251
- for (const e of shown) out.push(` ${e}`)
252
- if (entries.length > limit) out.push(` ... +${entries.length - limit} more`)
253
- }
254
-
255
- out.push("For detailed per-file relationships, call repo_outline with a file path.")
256
- return out.join("\n")
257
- }
258
-
259
- /** Get known file list from code_chunks (reuse index), parse by path to generate outline text */
260
- export async function buildOutline(db, cwd, focusPath) {
261
- const graph = await _buildDepGraph(db, cwd)
262
- if (!graph) return "(no indexed source files; run codeSync or /reindex first)"
263
- const { deps, importers } = graph
264
-
265
- const files = focusPath ? [focusPath] : [...deps.keys()]
266
- const out = []
267
- const sorted = files.sort()
268
- for (const rel of sorted) {
269
- const d = deps.get(rel)
270
- if (!d) continue
271
- const parts = []
272
- // imported by (strip extension when matching, since import paths usually don't include .mjs/.js suffix)
273
- const key = rel.replace(/\.(m?js|jsx|tsx?)$/i, "")
274
- const rev = importers.get(key)
275
- if (rev?.size) parts.push(`← imported by: ${[...rev].join(", ")}`)
276
- // imports
277
- if (d.imports.size) parts.push(`→ imports: ${[...d.imports].join(", ")}`)
278
- // exports
279
- if (d.exports.size) parts.push(`→ exports: ${[...d.exports].join(", ")}`)
280
-
281
- const kb = d.size > 0 ? ` (${d.size} KB)` : ""
282
- if (parts.length) {
283
- out.push(`${rel}${kb}\n ${parts.join("\n ")}`)
284
- } else {
285
- out.push(`${rel}${kb}`)
286
- }
287
- }
288
-
289
- return out.join("\n")
290
- }
291
-
292
- /**
293
- * Build the repo_outline tool (read-only).
294
- * Requires memory.db (reuses code_chunks file list) and cwd.
295
- */
296
- export function repoOutlineTool(db, cwd) {
297
- return {
298
- name: "repo_outline",
299
- description:
300
- "Show the project's file dependency outline: which files import/export from which, and what symbols they export. Use when you need to understand the project structure, find where a function is defined, or see what files depend on a module. Pass a path to focus on a single file's relationships. Find code by keyword or snippet with code_search.",
301
- parameters: {
302
- type: "object",
303
- properties: {
304
- path: { type: "string", description: "Optional: focus on a specific file path (relative to project root)" },
305
- },
306
- required: [],
307
- },
308
- readonly: true,
309
- async execute(args) {
310
- const outline = await buildOutline(db, cwd, args.path ?? null)
311
- return outline
312
- },
313
- }
314
- }
@@ -1,236 +0,0 @@
1
- import {
2
- DESC,
3
- truncate,
4
- resolveInCwd,
5
- globToRegex,
6
- splitGlobPatterns,
7
- compileGlobMatchers,
8
- normalizeEOL,
9
- IGNORED_DIRS,
10
- } from "./shared.mjs";
11
- import { readFile, readdir, stat, lstat } from "node:fs/promises";
12
- import { join } from "node:path";
13
-
14
- /** Escape a string for literal regex matching (grep literal=true). */
15
- function escapeRegExp(s) {
16
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
17
- }
18
-
19
- // ---------------------------------------------------------------- glob
20
-
21
- export const globTool = {
22
- name: "glob",
23
- description: DESC("glob"),
24
- parameters: {
25
- type: "object",
26
- properties: {
27
- pattern: { type: "string", description: "Glob pattern — supports **, *, ?, [..], {a,b} braces, and space-separated exclusion (\"**/*.js !test/**\")" },
28
- path: { type: "string", description: "Directory to search in (default cwd)" },
29
- },
30
- required: ["pattern"],
31
- },
32
- readonly: true,
33
- async execute(args, ctx) {
34
- const base = resolveInCwd(ctx, args.path ?? ".")
35
- // §17 调用侧拆分(评审 #5 职责分层):空格分隔 include !exclude 多模式在这里拆;
36
- // globToRegex 只收单个模式(数组由 compileGlobMatchers 收)。
37
- let match
38
- try {
39
- const parts = splitGlobPatterns(args.pattern)
40
- if (parts.length === 0) return "Error: pattern is required"
41
- match = compileGlobMatchers(parts).test
42
- } catch (e) {
43
- return `glob error: invalid pattern "${args.pattern}": ${e.message}`
44
- }
45
- const results = []
46
- for await (const relPath of walkFiles(base)) {
47
- if (match(relPath)) {
48
- results.push(relPath)
49
- if (results.length >= 1000) break
50
- }
51
- }
52
- if (results.length === 0) return "(no matches)"
53
- return truncate(results.sort().join("\n"))
54
- },
55
- }
56
-
57
- /** Recursively traverse files, yield relative paths (skip IGNORED_DIRS) */
58
- async function* walkFiles(dir, rel = "") {
59
- let entries
60
- try {
61
- entries = await readdir(dir, { withFileTypes: true })
62
- } catch {
63
- return
64
- }
65
- for (const e of entries) {
66
- // Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
67
- if (e.isSymbolicLink()) continue
68
- if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
69
- const relPath = rel ? `${rel}/${e.name}` : e.name
70
- if (e.isDirectory()) {
71
- yield* walkFiles(join(dir, e.name), relPath)
72
- } else {
73
- yield relPath
74
- }
75
- }
76
- }
77
-
78
- // ---------------------------------------------------------------- grep
79
-
80
- export const grepTool = {
81
- name: "grep",
82
- description: DESC("grep"),
83
- parameters: {
84
- type: "object",
85
- properties: {
86
- pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
87
- path: { type: "string", description: "Directory or file to search (default cwd)" },
88
- glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
89
- ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
90
- literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
91
- before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
92
- after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
93
- },
94
- required: ["pattern"],
95
- },
96
- readonly: true,
97
- async execute(args, ctx) {
98
- const base = resolveInCwd(ctx, args.path ?? ".")
99
- let regex
100
- try {
101
- const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
102
- regex = new RegExp(pat, args.ignoreCase ? "i" : "")
103
- } catch (e) {
104
- throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
105
- }
106
- // §17 调用侧拆分:glob 参数支持空格分隔 include !exclude 多模式(include/exclude 求交)。
107
- let fileTest = null
108
- if (args.glob) {
109
- try {
110
- const parts = splitGlobPatterns(args.glob)
111
- if (parts.length > 0) fileTest = compileGlobMatchers(parts).test
112
- } catch (e) {
113
- throw new Error(`grep glob /${args.glob}/ is invalid: ${e.message}`, { cause: e })
114
- }
115
- }
116
- const before = Math.max(0, Math.floor(args.before ?? 0))
117
- const after = Math.max(0, Math.floor(args.after ?? 0))
118
- const wantCtx = before > 0 || after > 0
119
- const hits = [] // { file, line(1-based), text }
120
- const fileLines = new Map() // file -> string[] (cached only when context lines requested)
121
-
122
- async function search(file) {
123
- let content
124
- try {
125
- // Large file guard: skip files over 10MB to prevent OOM
126
- const fst = await stat(file)
127
- if (fst.size > 10_000_000) return
128
- content = normalizeEOL(await readFile(file, "utf8"))
129
- } catch {
130
- return // Skip unreadable files; binary files will be read as UTF-8 and searched (may produce garbled matches)
131
- }
132
- const lines = content.split("\n")
133
- if (wantCtx) fileLines.set(file, lines)
134
- for (let i = 0; i < lines.length; i++) {
135
- if (regex.test(lines[i])) {
136
- hits.push({ file, line: i + 1, text: lines[i] })
137
- if (hits.length >= 200) return
138
- }
139
- }
140
- }
141
-
142
- async function walk(target, rel) {
143
- if (hits.length >= 200) return
144
- // Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
145
- let s
146
- try { s = await lstat(target) } catch { return }
147
- if (!s.isDirectory()) {
148
- // rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
149
- // 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
150
- if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
151
- return
152
- }
153
- let entries
154
- try {
155
- entries = await readdir(target, { withFileTypes: true })
156
- } catch {
157
- return
158
- }
159
- for (const e of entries) {
160
- if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
161
- await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
162
- }
163
- }
164
-
165
- await walk(base, "")
166
- if (hits.length === 0) return "(no matches)"
167
-
168
- // No context: keep original path:line: content format
169
- if (!wantCtx) {
170
- return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
171
- }
172
-
173
- // With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
174
- const fileMatched = new Map() // file -> Set<line>
175
- for (const h of hits) {
176
- if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
177
- fileMatched.get(h.file).add(h.line)
178
- }
179
- const out = []
180
- for (const [file, matchedLines] of fileMatched) {
181
- const lines = fileLines.get(file) ?? []
182
- const lineSet = new Set()
183
- for (const ml of matchedLines) {
184
- for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
185
- }
186
- for (const l of [...lineSet].sort((a, b) => a - b)) {
187
- const sep = matchedLines.has(l) ? ":" : "-"
188
- out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
189
- }
190
- }
191
- return truncate(out.join("\n"))
192
- },
193
- }
194
-
195
- export const lsTool = {
196
- name: "ls",
197
- description: DESC("ls"),
198
- parameters: {
199
- type: "object",
200
- properties: {
201
- path: { type: "string", description: "Directory path (default cwd)" },
202
- filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
203
- },
204
- },
205
- readonly: true,
206
- async execute(args, ctx) {
207
- const abs = resolveInCwd(ctx, args.path ?? ".")
208
- const filterRe = args.filter ? globToRegex(args.filter) : null
209
- let entries
210
- try {
211
- entries = await readdir(abs, { withFileTypes: true })
212
- } catch (e) {
213
- if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
214
- throw e
215
- }
216
- const rows = await Promise.all(
217
- entries
218
- .filter((e) => !filterRe || filterRe.test(e.name))
219
- .slice(0, 500)
220
- .map(async (e) => {
221
- const s = await stat(join(abs, e.name)).catch(() => null)
222
- const isDir = e.isDirectory()
223
- return {
224
- dir: isDir,
225
- name: e.name + (isDir ? "/" : ""),
226
- size: s?.size ?? 0,
227
- mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
228
- }
229
- }),
230
- )
231
- rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
232
- if (rows.length === 0) return "(no entries)"
233
- const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
234
- return truncate(out.join("\n"))
235
- },
236
- }