thincoder 0.8.1 → 0.8.3
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/README.md +2 -1
- package/bin/thincoder.cjs +4 -0
- package/bin/thincoder.mjs +25 -25
- package/package.json +2 -2
- package/src/agent/dispatch.mjs +17 -12
- package/src/agent/helpers.mjs +22 -11
- package/src/agent/setup.mjs +32 -10
- package/src/agent-tools/goal.mjs +7 -6
- package/src/agent-tools/plan.mjs +3 -3
- package/src/agent-tools/recent-changes.mjs +3 -3
- package/src/agent-tools/skill.mjs +6 -6
- package/src/agent-tools/subagent.mjs +19 -18
- package/src/agent-tools/task.mjs +4 -4
- package/src/agent-tools/verify.mjs +21 -19
- package/src/agent-tools.mjs +3 -3
- package/src/agent.mjs +58 -37
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +9 -9
- package/src/cli/memory-command.mjs +1 -1
- package/src/cli/permission.mjs +3 -3
- package/src/cli/setup-wizard.mjs +15 -15
- package/src/config.mjs +55 -53
- package/src/context.mjs +59 -56
- package/src/distill.mjs +35 -35
- package/src/embedding.mjs +17 -17
- package/src/git/checkpoint.mjs +211 -38
- package/src/git/gitmem.mjs +21 -20
- package/src/markdown.mjs +13 -13
- package/src/mcp/helpers.mjs +6 -1
- package/src/mcp/transport-http.mjs +2 -1
- package/src/mcp/transport-stdio.mjs +3 -2
- package/src/mcp/transport-ws.mjs +3 -2
- package/src/mcp.mjs +5 -2
- package/src/memory/code-index.mjs +16 -14
- package/src/memory/code-sync.mjs +36 -26
- package/src/memory/core.mjs +42 -35
- package/src/memory/docs.mjs +24 -15
- package/src/memory/schema.mjs +28 -27
- package/src/memory.mjs +2 -2
- package/src/prompts/coder.md +1 -1
- package/src/prompts/discipline.md +3 -0
- package/src/prompts/main.md +1 -2
- package/src/prompts/system.md +21 -16
- package/src/provider/core.mjs +11 -6
- package/src/provider/index.mjs +2 -2
- package/src/provider/rate.mjs +11 -11
- package/src/session.mjs +73 -40
- package/src/skills.mjs +17 -17
- package/src/tools/checkpoint.md +6 -2
- package/src/tools/file.mjs +16 -12
- package/src/tools/git.mjs +83 -12
- package/src/tools/index.mjs +1 -1
- package/src/tools/patch.mjs +20 -18
- package/src/tools/repomap-parse.mjs +17 -17
- package/src/tools/repomap.mjs +29 -29
- package/src/tools/shared.mjs +49 -28
- package/src/tools/system.mjs +163 -118
- package/src/tools/web.mjs +6 -6
- package/src/tui/agent-turn.mjs +82 -26
- package/src/tui/ansi.mjs +4 -2
- package/src/tui/clipboard.mjs +2 -2
- package/src/tui/cmd-auto.mjs +1 -1
- package/src/tui/cmd-clear.mjs +1 -1
- package/src/tui/cmd-config.mjs +2 -2
- package/src/tui/cmd-exit.mjs +2 -2
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-goal.mjs +4 -4
- package/src/tui/cmd-help.mjs +1 -1
- package/src/tui/cmd-init.mjs +3 -3
- package/src/tui/cmd-mcp.mjs +5 -5
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +1 -1
- package/src/tui/cmd-plan.mjs +1 -1
- package/src/tui/cmd-reindex.mjs +2 -2
- package/src/tui/cmd-restore.mjs +1 -1
- package/src/tui/cmd-session.mjs +1 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +2 -2
- package/src/tui/config-helpers.mjs +6 -6
- package/src/tui/distill-cmd.mjs +3 -3
- package/src/tui/index.mjs +131 -72
- package/src/tui/interaction.mjs +13 -12
- package/src/tui/key-handler.mjs +31 -23
- package/src/tui/layout.mjs +22 -17
- package/src/tui/pickers.mjs +19 -19
- package/src/tui/render-frame.mjs +37 -11
- package/src/tui/render.mjs +25 -24
- package/src/tui/slash-commands.mjs +14 -14
- package/src/tui/startup.mjs +15 -14
- package/src/tui/wizard.mjs +11 -11
- package/src/tui.mjs +2 -2
- package/bin/thincoder.js +0 -4
- package/src/tools/bash.mjs +0 -144
- package/src/tools/glob.mjs +0 -51
- package/src/tools/grep.mjs +0 -100
- package/src/tools/ls.mjs +0 -36
- package/src/tools.mjs +0 -2
- package/src/tui-render.mjs +0 -4
package/src/tools/git.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export const gitDiffTool = {
|
|
|
20
20
|
readonly: true,
|
|
21
21
|
execute(args, ctx) {
|
|
22
22
|
const ref = args.ref ?? "HEAD"
|
|
23
|
-
// ref
|
|
23
|
+
// ref supplied by model and placed before "--": validate charset, prevent "--output=..." etc. from being treated as git options
|
|
24
24
|
if (!/^[A-Za-z0-9._\/~^][A-Za-z0-9._\/~^-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
|
|
25
25
|
const flags = args.staged ? ["--staged"] : []
|
|
26
26
|
const paths = args.path ? [args.path] : []
|
|
@@ -49,14 +49,14 @@ export const gitStatusTool = {
|
|
|
49
49
|
const conflicts = []
|
|
50
50
|
for (const line of porcelain.split("\n")) {
|
|
51
51
|
if (!line) continue
|
|
52
|
-
// porcelain: XY path — 2
|
|
53
|
-
//
|
|
52
|
+
// porcelain: XY path — 2 status chars + space + file path (some environments have only 1 space)
|
|
53
|
+
// Strip possible CR (execFileSync on some Windows git leaves \r at end of line but not in newline)
|
|
54
54
|
const clean = line.replace(/\r/g, "")
|
|
55
|
-
//
|
|
55
|
+
// Try matching "XY path" or "XY path" (variable spacing)
|
|
56
56
|
const m = clean.match(/^(..?)\s+(.+)$/)
|
|
57
57
|
if (!m) continue
|
|
58
58
|
const [, status, rawFile] = m
|
|
59
|
-
//
|
|
59
|
+
// Rename entries in porcelain output are "R old -> new", split for clarity instead of treating as a literal filename
|
|
60
60
|
const file = status.includes("R") && rawFile.includes(" -> ") ? rawFile.replace(" -> ", " → ") : rawFile
|
|
61
61
|
const idx = status[0] ?? " "
|
|
62
62
|
const wt = status[1] ?? " "
|
|
@@ -128,7 +128,7 @@ export const questionTool = {
|
|
|
128
128
|
},
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
131
|
+
/** Execute a git command; returns empty string when not a git repo / git unavailable */
|
|
132
132
|
|
|
133
133
|
// ---------------------------------------------------------------- checkpoint
|
|
134
134
|
|
|
@@ -138,8 +138,9 @@ export const checkpointTool = {
|
|
|
138
138
|
parameters: {
|
|
139
139
|
type: "object",
|
|
140
140
|
properties: {
|
|
141
|
-
action: { type: "string", enum: ["list", "create", "rewind"], description: "list snapshots / create one now / restore a snapshot by id" },
|
|
142
|
-
id: { type: "string", description: "Snapshot id (required for rewind)" },
|
|
141
|
+
action: { type: "string", enum: ["list", "create", "rewind", "cat"], description: "list snapshots / create one now / restore a snapshot by id / read a file's content from a snapshot" },
|
|
142
|
+
id: { type: "string", description: "Snapshot id (required for rewind and cat; optional for list — shows file tree of that snapshot)" },
|
|
143
|
+
path: { type: "string", description: "Restore only this single file from the checkpoint (tracked or untracked). Other files are left untouched." },
|
|
143
144
|
},
|
|
144
145
|
required: ["action"],
|
|
145
146
|
},
|
|
@@ -149,18 +150,88 @@ export const checkpointTool = {
|
|
|
149
150
|
if (!isGitRepo(ctx.cwd)) throw new Error("Not a git repository — checkpoints unavailable")
|
|
150
151
|
if (args.action === "create") {
|
|
151
152
|
const cp = await createCheckpoint(ctx.cwd)
|
|
152
|
-
return `Checkpoint ${cp.id} created (${cp.files} file(s)
|
|
153
|
+
return `Checkpoint ${cp.id} created (${cp.files} file(s): ${cp.tracked.length} tracked, ${cp.untracked.length} untracked)`
|
|
153
154
|
}
|
|
154
155
|
if (args.action === "rewind") {
|
|
155
156
|
if (!args.id) throw new Error("id is required for rewind — use action=list to see snapshot ids")
|
|
156
|
-
const s = await rewind(ctx.cwd, args.id)
|
|
157
|
-
|
|
157
|
+
const s = await rewind(ctx.cwd, args.id, { path: args.path })
|
|
158
|
+
if (args.path) {
|
|
159
|
+
return `Restored "${args.path}" (${s.type}) from checkpoint ${args.id}.\n(The pre-rewind state was snapshotted first — you can rewind again to go back.)`
|
|
160
|
+
}
|
|
161
|
+
return `Rewound to checkpoint ${args.id}: patch ${s.patchApplied ? "applied" : "(empty)"}, ${s.restored ?? 0} untracked file(s) restored, ${s.deleted ?? 0} file(s) deleted.\n(The pre-rewind state was snapshotted first — you can rewind again to go back.)`
|
|
162
|
+
}
|
|
163
|
+
if (args.action === "cat") {
|
|
164
|
+
if (!args.id) throw new Error("id is required for cat — use action=list to see snapshot ids")
|
|
165
|
+
if (!args.path) throw new Error("path is required for cat — specify which file to read")
|
|
166
|
+
const { catFile } = await import("../git/checkpoint.mjs")
|
|
167
|
+
return await catFile(ctx.cwd, args.id, args.path)
|
|
158
168
|
}
|
|
159
169
|
if (args.action === "list") {
|
|
160
170
|
const cps = await listCheckpoints(ctx.cwd)
|
|
161
171
|
if (cps.length === 0) return "(no checkpoints yet — one is auto-created before each user task)"
|
|
162
|
-
|
|
172
|
+
|
|
173
|
+
// Specific id: show the file tree within that snapshot
|
|
174
|
+
if (args.id) {
|
|
175
|
+
const cp = cps.find((c) => c.id === args.id)
|
|
176
|
+
if (!cp) throw new Error(`checkpoint ${args.id} not found`)
|
|
177
|
+
return formatFileTree(cp)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Overview: list of all snapshots
|
|
181
|
+
return cps.map((c) => {
|
|
182
|
+
const parts = [`${c.id} ${new Date(c.time).toISOString()}`]
|
|
183
|
+
if (c.tracked.length) parts.push(`${c.tracked.length} tracked: ${c.tracked.join(", ")}`)
|
|
184
|
+
if (c.untracked.length) parts.push(`${c.untracked.length} untracked: ${c.untracked.join(", ")}`)
|
|
185
|
+
return parts.join(" ")
|
|
186
|
+
}).join("\n")
|
|
163
187
|
}
|
|
164
188
|
throw new Error(`Unknown action: ${args.action}`)
|
|
165
189
|
},
|
|
166
190
|
}
|
|
191
|
+
|
|
192
|
+
/** Format a checkpoint's file list as a directory tree (directories first, indented display) */
|
|
193
|
+
function formatFileTree(cp) {
|
|
194
|
+
const all = [
|
|
195
|
+
...(cp.tracked ?? []).map((f) => ({ path: f, type: "" })),
|
|
196
|
+
...(cp.untracked ?? []).map((f) => ({ path: f, type: " (untracked)" })),
|
|
197
|
+
]
|
|
198
|
+
if (all.length === 0) return "(empty checkpoint)"
|
|
199
|
+
|
|
200
|
+
// Sort by path (directories group naturally)
|
|
201
|
+
all.sort((a, b) => a.path.localeCompare(b.path))
|
|
202
|
+
|
|
203
|
+
// Build directory → file list map
|
|
204
|
+
const tree = new Map()
|
|
205
|
+
for (const { path, type } of all) {
|
|
206
|
+
const dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "."
|
|
207
|
+
if (!tree.has(dir)) tree.set(dir, [])
|
|
208
|
+
tree.get(dir).push({ name: path.slice(dir === "." ? 0 : dir.length + 1), type })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Output sorted by directory
|
|
212
|
+
const lines = []
|
|
213
|
+
const dirs = [...tree.keys()].sort()
|
|
214
|
+
for (const dir of dirs) {
|
|
215
|
+
if (dir !== "." && !lines.includes(dir + "/")) {
|
|
216
|
+
// Parent directory before child directory
|
|
217
|
+
const parts = dir.split("/")
|
|
218
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
219
|
+
const prefix = parts.slice(0, i).join("/") + "/"
|
|
220
|
+
if (!lines.includes(prefix)) lines.push(prefix)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Ensure directories precede files
|
|
225
|
+
for (const dir of dirs) {
|
|
226
|
+
if (dir !== ".") {
|
|
227
|
+
for (const { name, type } of tree.get(dir)) {
|
|
228
|
+
lines.push(` ${dir}/${name}${type}`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const { name, type } of tree.get(".") ?? []) {
|
|
233
|
+
lines.push(name + type)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return lines.join("\n")
|
|
237
|
+
}
|
package/src/tools/index.mjs
CHANGED
package/src/tools/patch.mjs
CHANGED
|
@@ -13,11 +13,12 @@ import { existsSync } from "node:fs";
|
|
|
13
13
|
import { join, relative, dirname } from "node:path";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Parse a unified diff: returns [{ path, isNew, hunks: [{ ops: [{type:" "|"-"|"+", text}] }] }]
|
|
17
|
+
* Consume hunk lines by the line counts in the @@ header — LLMs often strip context blank lines to pure empty lines,
|
|
18
|
+
* so we use counts rather than first characters to determine hunk boundaries
|
|
18
19
|
*/
|
|
19
20
|
function parsePatch(patch) {
|
|
20
|
-
//
|
|
21
|
+
// Patch text often comes from CRLF terminals/model output; trailing \r mixed into hunk content breaks context matching, strip uniformly
|
|
21
22
|
const lines = patch.replace(/\r(?=\n|$)/g, "").split("\n")
|
|
22
23
|
const files = []
|
|
23
24
|
let cur = null
|
|
@@ -48,7 +49,7 @@ function parsePatch(patch) {
|
|
|
48
49
|
if (i >= lines.length) throw new Error("Malformed patch: hunk truncated (line counts in @@ header not satisfied)")
|
|
49
50
|
const hl = lines[i]
|
|
50
51
|
if (hl.startsWith("\\")) { i++; continue } // ""
|
|
51
|
-
const tag = hl === "" ? " " : hl[0] //
|
|
52
|
+
const tag = hl === "" ? " " : hl[0] // pure blank line: treat leniently as context line
|
|
52
53
|
const text = hl === "" ? "" : hl.slice(1)
|
|
53
54
|
if (tag === " ") { hunk.ops.push({ type: " ", text }); oldNeed--; newNeed-- }
|
|
54
55
|
else if (tag === "-") { hunk.ops.push({ type: "-", text }); oldNeed-- }
|
|
@@ -59,13 +60,13 @@ function parsePatch(patch) {
|
|
|
59
60
|
cur.hunks.push(hunk)
|
|
60
61
|
continue
|
|
61
62
|
}
|
|
62
|
-
i++ // diff --git / index /
|
|
63
|
+
i++ // skip diff --git / index / blank lines and other metadata
|
|
63
64
|
}
|
|
64
65
|
if (files.length === 0) throw new Error("No file changes found in patch (need --- / +++ headers)")
|
|
65
66
|
return files
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/**
|
|
69
|
+
/** Apply hunks sequentially onto an in-memory line array; any failure throws (caller guarantees nothing is written to disk). Ignores trailing \r when comparing; context lines retain original bytes */
|
|
69
70
|
function applyHunks(fileLines, hunks, eol, path) {
|
|
70
71
|
const cr = eol === "\r\n" ? "\r" : ""
|
|
71
72
|
for (let h = 0; h < hunks.length; h++) {
|
|
@@ -88,7 +89,7 @@ function applyHunks(fileLines, hunks, eol, path) {
|
|
|
88
89
|
const out = []
|
|
89
90
|
let src = pos
|
|
90
91
|
for (const op of hunks[h].ops) {
|
|
91
|
-
if (op.type === " ") out.push(fileLines[src++]) //
|
|
92
|
+
if (op.type === " ") out.push(fileLines[src++]) // preserve original context line (trailing whitespace as-is)
|
|
92
93
|
else if (op.type === "-") src++
|
|
93
94
|
else out.push(op.text + cr)
|
|
94
95
|
}
|
|
@@ -107,13 +108,13 @@ export const applyPatchTool = {
|
|
|
107
108
|
required: ["patch"],
|
|
108
109
|
},
|
|
109
110
|
readonly: false,
|
|
110
|
-
/**
|
|
111
|
+
/** For the agent layer to track touched files (multi-path, replaces single path parameter) */
|
|
111
112
|
touchedPaths(args) {
|
|
112
113
|
try { return parsePatch(args.patch ?? "").map((f) => f.path) } catch { return [] }
|
|
113
114
|
},
|
|
114
115
|
async execute(args, ctx) {
|
|
115
116
|
const files = parsePatch(args.patch ?? "")
|
|
116
|
-
//
|
|
117
|
+
// Read all into memory first for trial: if any hunk fails, abort entirely — never write a partial patch (atomicity)
|
|
117
118
|
const planned = []
|
|
118
119
|
for (const f of files) {
|
|
119
120
|
const abs = resolveInCwd(ctx, f.path)
|
|
@@ -129,7 +130,7 @@ export const applyPatchTool = {
|
|
|
129
130
|
planned.push({ abs, path: f.path, content: lines.join("\n"), isNew: false })
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
|
-
//
|
|
133
|
+
// Multi-file write: write all to .tmp first, rename only after all succeed — failure cleans up written .tmp without affecting committed files
|
|
133
134
|
const { rename, unlink } = await import("node:fs/promises")
|
|
134
135
|
const written = []
|
|
135
136
|
try {
|
|
@@ -142,17 +143,18 @@ export const applyPatchTool = {
|
|
|
142
143
|
await rename(p.abs + ".thincoder-tmp", p.abs)
|
|
143
144
|
}
|
|
144
145
|
} catch (renameError) {
|
|
145
|
-
// rename
|
|
146
|
+
// rename phase failed: clean up leftover .tmp files
|
|
146
147
|
for (const abs of written) {
|
|
147
148
|
try { await unlink(abs + ".thincoder-tmp") } catch {}
|
|
148
149
|
}
|
|
149
150
|
throw renameError
|
|
150
151
|
}
|
|
151
152
|
const summary = planned.map((p) => ` ${p.isNew ? "created " : "modified"} ${p.path}`).join("\n")
|
|
152
|
-
const
|
|
153
|
-
const r = autoSyntaxCheck(p.abs)
|
|
153
|
+
const syntaxChecks = await Promise.all(planned.map(async (p) => {
|
|
154
|
+
const r = await autoSyntaxCheck(p.abs)
|
|
154
155
|
return r ? `${p.path}:${r.replace("Syntax: ", "")}` : ""
|
|
155
|
-
})
|
|
156
|
+
}))
|
|
157
|
+
const syntaxResults = syntaxChecks.filter(Boolean).join("\n")
|
|
156
158
|
return `Applied patch to ${planned.length} file(s):\n${summary}${syntaxResults ? "\n\nSyntax checks:\n" + syntaxResults : ""}`
|
|
157
159
|
},
|
|
158
160
|
}
|
|
@@ -181,7 +183,7 @@ export const syntaxCheckTool = {
|
|
|
181
183
|
})
|
|
182
184
|
return `Syntax OK: ${args.path}`
|
|
183
185
|
} catch (e) {
|
|
184
|
-
// node --check
|
|
186
|
+
// node --check writes errors to stderr
|
|
185
187
|
const msg = (e.stderr || e.stdout || e.message || "").trim()
|
|
186
188
|
return `Syntax error in ${args.path}:\n${msg || "(unknown)"}`
|
|
187
189
|
}
|
|
@@ -207,15 +209,15 @@ export const deleteTool = {
|
|
|
207
209
|
if (!existsSync(abs)) throw new Error(`File not found: ${args.path}`)
|
|
208
210
|
const s = await stat(abs)
|
|
209
211
|
if (s.isDirectory()) throw new Error(`"${args.path}" is a directory — use bash to remove directories`)
|
|
210
|
-
// git
|
|
211
|
-
//
|
|
212
|
+
// git-tracked files: refuse direct deletion (safety net); untracked: allow
|
|
213
|
+
// Use resolved relative path (normalized forward slashes) to prevent backslash/unusual paths from bypassing ls-files matching
|
|
212
214
|
const rel = relative(ctx.cwd, abs).replace(/\\/g, "/")
|
|
213
215
|
let tracked = false
|
|
214
216
|
try {
|
|
215
217
|
execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: ctx.cwd, stdio: "ignore" })
|
|
216
218
|
tracked = true
|
|
217
219
|
} catch {
|
|
218
|
-
//
|
|
220
|
+
// untracked / non-git repo
|
|
219
221
|
}
|
|
220
222
|
if (tracked && !args.force) throw new Error(`"${args.path}" is git-tracked. Set force=true to delete anyway.`)
|
|
221
223
|
await unlink(abs)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* repomap-parse.mjs —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* repomap-parse.mjs — repo dependency graph parser (zero dependencies, pure regex)
|
|
3
|
+
* Gets known file list from code_chunks, parses each file's import/export relationships in real time,
|
|
4
|
+
* builds forward dependency graph + reverse reference graph. Shared by repomap.mjs's buildSummary / buildOutline.
|
|
5
5
|
*/
|
|
6
6
|
import { readFileSync, existsSync } from "node:fs"
|
|
7
7
|
import { join } from "node:path"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Scan all files, build forward dependency graph + reverse reference graph.
|
|
11
|
+
* Returns { deps, importers, fileCount } shared by buildOutline / buildSummary.
|
|
12
12
|
*/
|
|
13
13
|
export function buildDepGraph(db, cwd) {
|
|
14
14
|
const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ORDER BY path`).all().map((r) => r.path)
|
|
@@ -34,7 +34,7 @@ export function buildDepGraph(db, cwd) {
|
|
|
34
34
|
exports = parseExports(lines, ext)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// Resolve import paths to relative paths (handle ./ ../)
|
|
38
38
|
const resolved = []
|
|
39
39
|
for (let imp of imports) {
|
|
40
40
|
if (imp.startsWith("./")) imp = imp.slice(2)
|
|
@@ -62,17 +62,17 @@ export function buildDepGraph(db, cwd) {
|
|
|
62
62
|
return { deps, importers, fileCount: allFiles.length }
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// ----------------------------------------------------------
|
|
65
|
+
// ---------------------------------------------------------- internal implementation
|
|
66
66
|
|
|
67
67
|
function normalizeExt(p) {
|
|
68
68
|
return p.replace(/\.(m?js|jsx|tsx?)$/i, "")
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
71
|
+
/** Extract JS/TS file import paths (normalize by stripping .ts/.js/.mjs suffixes) */
|
|
72
72
|
function parseImports(lines, ext) {
|
|
73
73
|
const imports = []
|
|
74
74
|
const text = lines.join("\n")
|
|
75
|
-
//
|
|
75
|
+
// standard import
|
|
76
76
|
const re = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+\s*,?\s*(?:{[^}]*})?)\s*from\s*['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g
|
|
77
77
|
let m
|
|
78
78
|
while ((m = re.exec(text))) {
|
|
@@ -90,7 +90,7 @@ function parseImports(lines, ext) {
|
|
|
90
90
|
return [...new Set(imports)]
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
93
|
+
/** Extract JS/TS file export symbols */
|
|
94
94
|
function parseExports(lines, ext) {
|
|
95
95
|
const exports = []
|
|
96
96
|
const text = lines.join("\n")
|
|
@@ -107,17 +107,17 @@ function parseExports(lines, ext) {
|
|
|
107
107
|
if (name) exports.push(name)
|
|
108
108
|
else if (!exports.some((e) => e === "default")) exports.push("default")
|
|
109
109
|
}
|
|
110
|
-
// export { a, b as c }
|
|
110
|
+
// export { a, b as c } — prefer the "as" alias as the exported name
|
|
111
111
|
const braceRe = /export\s*\{([^}]+)\}/g
|
|
112
112
|
while ((m = braceRe.exec(text))) {
|
|
113
113
|
for (const name of m[1].split(",")) {
|
|
114
114
|
const parts = name.trim().split(/\s+/)
|
|
115
|
-
// "a as b" → b
|
|
115
|
+
// "a as b" → b (exported name), "a" → a
|
|
116
116
|
const exported = parts.length >= 3 ? parts[2] : parts[0]
|
|
117
117
|
if (exported) exports.push(exported)
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
// export const { a, b } =
|
|
120
|
+
// export const { a, b } = ... (destructured export)
|
|
121
121
|
const destructRe = /export\s+(?:const|let|var)\s*\{([^}]+)\}\s*=/g
|
|
122
122
|
while ((m = destructRe.exec(text))) {
|
|
123
123
|
for (const name of m[1].split(",")) {
|
|
@@ -129,7 +129,7 @@ function parseExports(lines, ext) {
|
|
|
129
129
|
return [...new Set(exports)]
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/**
|
|
132
|
+
/** Extract Python imports and top-level def/class */
|
|
133
133
|
function parsePyOutline(lines) {
|
|
134
134
|
const imports = []
|
|
135
135
|
const symbols = []
|
|
@@ -155,9 +155,9 @@ function parsePyOutline(lines) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
* Python
|
|
159
|
-
*
|
|
160
|
-
*
|
|
158
|
+
* Python relative import → relative file path:
|
|
159
|
+
* Leading n dots mean go up n-1 levels ("." = current package), module dots become path separators.
|
|
160
|
+
* Non-relative imports (not starting with .) or bare package imports ("from . import x") return null.
|
|
161
161
|
*/
|
|
162
162
|
function pyRelPath(mod) {
|
|
163
163
|
if (!mod?.startsWith(".")) return null
|
package/src/tools/repomap.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* repomap.mjs —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* repomap.mjs — repo dependency outline (zero dependencies, pure regex)
|
|
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
5
|
*/
|
|
6
6
|
import { readFileSync, existsSync } from "node:fs"
|
|
7
7
|
import { join } from "node:path"
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** Extract JS/TS file import paths (normalize by stripping .ts/.js/.mjs suffixes) */
|
|
10
10
|
function parseImports(lines, ext) {
|
|
11
11
|
const imports = []
|
|
12
12
|
const text = lines.join("\n")
|
|
13
|
-
//
|
|
13
|
+
// standard import
|
|
14
14
|
const re = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+\s*,?\s*(?:{[^}]*})?)\s*from\s*['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g
|
|
15
15
|
let m
|
|
16
16
|
while ((m = re.exec(text))) {
|
|
@@ -28,7 +28,7 @@ function parseImports(lines, ext) {
|
|
|
28
28
|
return [...new Set(imports)]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/** Extract JS/TS file export symbols */
|
|
32
32
|
function parseExports(lines, ext) {
|
|
33
33
|
const exports = []
|
|
34
34
|
const text = lines.join("\n")
|
|
@@ -45,17 +45,17 @@ function parseExports(lines, ext) {
|
|
|
45
45
|
if (name) exports.push(name)
|
|
46
46
|
else if (!exports.some((e) => e === "default")) exports.push("default")
|
|
47
47
|
}
|
|
48
|
-
// export { a, b as c }
|
|
48
|
+
// export { a, b as c } — prefer the "as" alias as the exported name
|
|
49
49
|
const braceRe = /export\s*\{([^}]+)\}/g
|
|
50
50
|
while ((m = braceRe.exec(text))) {
|
|
51
51
|
for (const name of m[1].split(",")) {
|
|
52
52
|
const parts = name.trim().split(/\s+/)
|
|
53
|
-
// "a as b" → b
|
|
53
|
+
// "a as b" → b (exported name), "a" → a
|
|
54
54
|
const exported = parts.length >= 3 ? parts[2] : parts[0]
|
|
55
55
|
if (exported) exports.push(exported)
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
// export const { a, b } =
|
|
58
|
+
// export const { a, b } = ... (destructured export)
|
|
59
59
|
const destructRe = /export\s+(?:const|let|var)\s*\{([^}]+)\}\s*=/g
|
|
60
60
|
while ((m = destructRe.exec(text))) {
|
|
61
61
|
for (const name of m[1].split(",")) {
|
|
@@ -67,7 +67,7 @@ function parseExports(lines, ext) {
|
|
|
67
67
|
return [...new Set(exports)]
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
70
|
+
/** Extract Python imports and top-level def/class */
|
|
71
71
|
function parsePyOutline(lines) {
|
|
72
72
|
const imports = []
|
|
73
73
|
const symbols = []
|
|
@@ -93,9 +93,9 @@ function parsePyOutline(lines) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Python
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* Python relative import → relative file path:
|
|
97
|
+
* Leading n dots mean go up n-1 levels ("." = current package), module dots become path separators.
|
|
98
|
+
* Non-relative imports (not starting with .) or bare package imports ("from . import x") return null.
|
|
99
99
|
*/
|
|
100
100
|
function pyRelPath(mod) {
|
|
101
101
|
if (!mod?.startsWith(".")) return null
|
|
@@ -110,8 +110,8 @@ function normalizeExt(p) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
113
|
+
* Internal: scan all files, build forward dependency graph + reverse reference graph.
|
|
114
|
+
* Returns { deps, importers, fileCount } shared by buildOutline / buildSummary.
|
|
115
115
|
*/
|
|
116
116
|
function _buildDepGraph(db, cwd) {
|
|
117
117
|
const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ORDER BY path`).all().map((r) => r.path)
|
|
@@ -137,7 +137,7 @@ function _buildDepGraph(db, cwd) {
|
|
|
137
137
|
exports = parseExports(lines, ext)
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
//
|
|
140
|
+
// Resolve import paths to relative paths (handle ./ ../)
|
|
141
141
|
const resolved = []
|
|
142
142
|
for (let imp of imports) {
|
|
143
143
|
if (imp.startsWith("./")) imp = imp.slice(2)
|
|
@@ -166,12 +166,12 @@ function _buildDepGraph(db, cwd) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* 1.
|
|
172
|
-
* 2.
|
|
173
|
-
* 3.
|
|
174
|
-
*
|
|
169
|
+
* Generate a compact architecture summary (replaces the old full-dump injection).
|
|
170
|
+
* Three layers, each with decreasing information density:
|
|
171
|
+
* 1. Directory-level dependencies (meaningful only for multi-directory projects, skipped for single-directory)
|
|
172
|
+
* 2. Hub files Top-12 (most-imported files — architecture skeleton)
|
|
173
|
+
* 3. Entry points (files with no importers — startup/top-level entry points)
|
|
174
|
+
* Output is naturally bounded (~1000-2000 chars), no more OUTLINE_INJECT_MAX hard truncation.
|
|
175
175
|
*/
|
|
176
176
|
export function buildSummary(db, cwd) {
|
|
177
177
|
const graph = _buildDepGraph(db, cwd)
|
|
@@ -181,7 +181,7 @@ export function buildSummary(db, cwd) {
|
|
|
181
181
|
const out = []
|
|
182
182
|
out.push(`${fileCount} source files indexed.`)
|
|
183
183
|
|
|
184
|
-
// 1)
|
|
184
|
+
// 1) Directory-level dependencies
|
|
185
185
|
const dirDeps = new Map() // dir → Set<imported-dir>
|
|
186
186
|
const dirSet = new Set()
|
|
187
187
|
for (const [rel, d] of deps) {
|
|
@@ -204,7 +204,7 @@ export function buildSummary(db, cwd) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
// 2)
|
|
207
|
+
// 2) Hub files Top-12: sorted by import count descending
|
|
208
208
|
const HUB_LIMIT = 12
|
|
209
209
|
const hubScores = []
|
|
210
210
|
for (const [rel] of deps) {
|
|
@@ -227,7 +227,7 @@ export function buildSummary(db, cwd) {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
// 3)
|
|
230
|
+
// 3) Entry points: files not imported by others (leaf/entry)
|
|
231
231
|
const entries = []
|
|
232
232
|
for (const [rel] of deps) {
|
|
233
233
|
const key = rel.replace(/\.(m?js|jsx|tsx?)$/i, "")
|
|
@@ -247,7 +247,7 @@ export function buildSummary(db, cwd) {
|
|
|
247
247
|
return out.join("\n")
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
/**
|
|
250
|
+
/** Get known file list from code_chunks (reuse index), parse by path to generate outline text */
|
|
251
251
|
export function buildOutline(db, cwd, focusPath) {
|
|
252
252
|
const graph = _buildDepGraph(db, cwd)
|
|
253
253
|
if (!graph) return "(no indexed source files; run codeSync or /reindex first)"
|
|
@@ -260,7 +260,7 @@ export function buildOutline(db, cwd, focusPath) {
|
|
|
260
260
|
const d = deps.get(rel)
|
|
261
261
|
if (!d) continue
|
|
262
262
|
const parts = []
|
|
263
|
-
// imported by
|
|
263
|
+
// imported by (strip extension when matching, since import paths usually don't include .mjs/.js suffix)
|
|
264
264
|
const key = rel.replace(/\.(m?js|jsx|tsx?)$/i, "")
|
|
265
265
|
const rev = importers.get(key)
|
|
266
266
|
if (rev?.size) parts.push(`← imported by: ${[...rev].join(", ")}`)
|
|
@@ -281,8 +281,8 @@ export function buildOutline(db, cwd, focusPath) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
284
|
+
* Build the repo_outline tool (read-only).
|
|
285
|
+
* Requires memory.db (reuses code_chunks file list) and cwd.
|
|
286
286
|
*/
|
|
287
287
|
export function repoOutlineTool(db, cwd) {
|
|
288
288
|
return {
|