@thincoder/core 0.9.1

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 (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,314 @@
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
+ }
@@ -0,0 +1,248 @@
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
+ // §2.5 #65 并入(VSC「路径不存在明确报错」):目标路径缺失不再静默空清单。
47
+ try {
48
+ await stat(base)
49
+ } catch {
50
+ return `glob error: path not found: ${base}`
51
+ }
52
+ for await (const relPath of walkFiles(base)) {
53
+ if (match(relPath)) {
54
+ results.push(relPath)
55
+ if (results.length >= 1000) break
56
+ }
57
+ }
58
+ if (results.length === 0) return "(no matches)"
59
+ return truncate(results.sort().join("\n"))
60
+ },
61
+ }
62
+
63
+ /** Recursively traverse files, yield relative paths (skip IGNORED_DIRS) */
64
+ async function* walkFiles(dir, rel = "") {
65
+ let entries
66
+ try {
67
+ entries = await readdir(dir, { withFileTypes: true })
68
+ } catch {
69
+ return
70
+ }
71
+ for (const e of entries) {
72
+ // Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
73
+ if (e.isSymbolicLink()) continue
74
+ if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
75
+ const relPath = rel ? `${rel}/${e.name}` : e.name
76
+ if (e.isDirectory()) {
77
+ yield* walkFiles(join(dir, e.name), relPath)
78
+ } else {
79
+ yield relPath
80
+ }
81
+ }
82
+ }
83
+
84
+ // ---------------------------------------------------------------- grep
85
+
86
+ export const grepTool = {
87
+ name: "grep",
88
+ description: DESC("grep"),
89
+ parameters: {
90
+ type: "object",
91
+ properties: {
92
+ pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
93
+ path: { type: "string", description: "Directory or file to search (default cwd)" },
94
+ glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
95
+ ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
96
+ literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
97
+ before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
98
+ after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
99
+ },
100
+ required: ["pattern"],
101
+ },
102
+ readonly: true,
103
+ async execute(args, ctx) {
104
+ const base = resolveInCwd(ctx, args.path ?? ".")
105
+ let regex
106
+ try {
107
+ const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
108
+ regex = new RegExp(pat, args.ignoreCase ? "i" : "")
109
+ } catch (e) {
110
+ throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
111
+ }
112
+ // §2.5 #65 并入(VSC「路径不存在明确报错」):目标路径缺失不再静默 "(no matches)"。
113
+ try {
114
+ await lstat(base)
115
+ } catch {
116
+ return `grep error: path not found: ${base}`
117
+ }
118
+ // §17 调用侧拆分:glob 参数支持空格分隔 include !exclude 多模式(include/exclude 求交)。
119
+ let fileTest = null
120
+ if (args.glob) {
121
+ try {
122
+ const parts = splitGlobPatterns(args.glob)
123
+ if (parts.length > 0) fileTest = compileGlobMatchers(parts).test
124
+ } catch (e) {
125
+ throw new Error(`grep glob /${args.glob}/ is invalid: ${e.message}`, { cause: e })
126
+ }
127
+ }
128
+ const before = Math.max(0, Math.floor(args.before ?? 0))
129
+ const after = Math.max(0, Math.floor(args.after ?? 0))
130
+ const wantCtx = before > 0 || after > 0
131
+ const hits = [] // { file, line(1-based), text }
132
+ const fileLines = new Map() // file -> string[] (cached only when context lines requested)
133
+
134
+ async function search(file) {
135
+ let content
136
+ try {
137
+ // Large file guard: skip files over 10MB to prevent OOM
138
+ const fst = await stat(file)
139
+ if (fst.size > 10_000_000) return
140
+ content = normalizeEOL(await readFile(file, "utf8"))
141
+ } catch {
142
+ return // Skip unreadable files; binary files will be read as UTF-8 and searched (may produce garbled matches)
143
+ }
144
+ const lines = content.split("\n")
145
+ if (wantCtx) fileLines.set(file, lines)
146
+ for (let i = 0; i < lines.length; i++) {
147
+ if (regex.test(lines[i])) {
148
+ hits.push({ file, line: i + 1, text: lines[i] })
149
+ if (hits.length >= 200) return
150
+ }
151
+ }
152
+ }
153
+
154
+ async function walk(target, rel) {
155
+ if (hits.length >= 200) return
156
+ // Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
157
+ let s
158
+ try { s = await lstat(target) } catch { return }
159
+ if (!s.isDirectory()) {
160
+ // rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
161
+ // 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
162
+ if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
163
+ return
164
+ }
165
+ let entries
166
+ try {
167
+ entries = await readdir(target, { withFileTypes: true })
168
+ } catch {
169
+ return
170
+ }
171
+ for (const e of entries) {
172
+ if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
173
+ await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
174
+ }
175
+ }
176
+
177
+ await walk(base, "")
178
+ if (hits.length === 0) return "(no matches)"
179
+
180
+ // No context: keep original path:line: content format
181
+ if (!wantCtx) {
182
+ return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
183
+ }
184
+
185
+ // With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
186
+ const fileMatched = new Map() // file -> Set<line>
187
+ for (const h of hits) {
188
+ if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
189
+ fileMatched.get(h.file).add(h.line)
190
+ }
191
+ const out = []
192
+ for (const [file, matchedLines] of fileMatched) {
193
+ const lines = fileLines.get(file) ?? []
194
+ const lineSet = new Set()
195
+ for (const ml of matchedLines) {
196
+ for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
197
+ }
198
+ for (const l of [...lineSet].sort((a, b) => a - b)) {
199
+ const sep = matchedLines.has(l) ? ":" : "-"
200
+ out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
201
+ }
202
+ }
203
+ return truncate(out.join("\n"))
204
+ },
205
+ }
206
+
207
+ export const lsTool = {
208
+ name: "ls",
209
+ description: DESC("ls"),
210
+ parameters: {
211
+ type: "object",
212
+ properties: {
213
+ path: { type: "string", description: "Directory path (default cwd)" },
214
+ filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
215
+ },
216
+ },
217
+ readonly: true,
218
+ async execute(args, ctx) {
219
+ const abs = resolveInCwd(ctx, args.path ?? ".")
220
+ const filterRe = args.filter ? globToRegex(args.filter) : null
221
+ let entries
222
+ try {
223
+ entries = await readdir(abs, { withFileTypes: true })
224
+ } catch (e) {
225
+ if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
226
+ throw e
227
+ }
228
+ const rows = await Promise.all(
229
+ entries
230
+ .filter((e) => !filterRe || filterRe.test(e.name))
231
+ .slice(0, 500)
232
+ .map(async (e) => {
233
+ const s = await stat(join(abs, e.name)).catch(() => null)
234
+ const isDir = e.isDirectory()
235
+ return {
236
+ dir: isDir,
237
+ name: e.name + (isDir ? "/" : ""),
238
+ size: s?.size ?? 0,
239
+ mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
240
+ }
241
+ }),
242
+ )
243
+ rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
244
+ if (rows.length === 0) return "(no entries)"
245
+ const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
246
+ return truncate(out.join("\n"))
247
+ },
248
+ }