@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.
- package/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- 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
|
+
}
|
package/tools/search.mjs
ADDED
|
@@ -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
|
+
}
|