thincoder 0.8.2 → 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.
Files changed (97) hide show
  1. package/README.md +2 -1
  2. package/bin/thincoder.mjs +25 -25
  3. package/package.json +1 -1
  4. package/src/agent/dispatch.mjs +17 -12
  5. package/src/agent/helpers.mjs +22 -11
  6. package/src/agent/setup.mjs +32 -10
  7. package/src/agent-tools/goal.mjs +7 -6
  8. package/src/agent-tools/plan.mjs +3 -3
  9. package/src/agent-tools/recent-changes.mjs +3 -3
  10. package/src/agent-tools/skill.mjs +6 -6
  11. package/src/agent-tools/subagent.mjs +19 -18
  12. package/src/agent-tools/task.mjs +4 -4
  13. package/src/agent-tools/verify.mjs +21 -19
  14. package/src/agent-tools.mjs +3 -3
  15. package/src/agent.mjs +58 -37
  16. package/src/cli/distill-command.mjs +2 -2
  17. package/src/cli/make-agent.mjs +9 -9
  18. package/src/cli/memory-command.mjs +1 -1
  19. package/src/cli/permission.mjs +3 -3
  20. package/src/cli/setup-wizard.mjs +15 -15
  21. package/src/config.mjs +55 -53
  22. package/src/context.mjs +59 -56
  23. package/src/distill.mjs +35 -35
  24. package/src/embedding.mjs +17 -17
  25. package/src/git/checkpoint.mjs +211 -38
  26. package/src/git/gitmem.mjs +21 -20
  27. package/src/markdown.mjs +13 -13
  28. package/src/mcp/helpers.mjs +6 -1
  29. package/src/mcp/transport-http.mjs +2 -1
  30. package/src/mcp/transport-stdio.mjs +3 -2
  31. package/src/mcp/transport-ws.mjs +3 -2
  32. package/src/mcp.mjs +5 -2
  33. package/src/memory/code-index.mjs +16 -14
  34. package/src/memory/code-sync.mjs +36 -26
  35. package/src/memory/core.mjs +42 -35
  36. package/src/memory/docs.mjs +24 -15
  37. package/src/memory/schema.mjs +28 -27
  38. package/src/memory.mjs +2 -2
  39. package/src/prompts/coder.md +1 -1
  40. package/src/prompts/discipline.md +3 -0
  41. package/src/prompts/main.md +1 -2
  42. package/src/prompts/system.md +21 -16
  43. package/src/provider/core.mjs +11 -6
  44. package/src/provider/index.mjs +2 -2
  45. package/src/provider/rate.mjs +11 -11
  46. package/src/session.mjs +73 -40
  47. package/src/skills.mjs +17 -17
  48. package/src/tools/checkpoint.md +6 -2
  49. package/src/tools/file.mjs +16 -12
  50. package/src/tools/git.mjs +83 -12
  51. package/src/tools/index.mjs +1 -1
  52. package/src/tools/patch.mjs +20 -18
  53. package/src/tools/repomap-parse.mjs +17 -17
  54. package/src/tools/repomap.mjs +29 -29
  55. package/src/tools/shared.mjs +49 -28
  56. package/src/tools/system.mjs +163 -118
  57. package/src/tools/web.mjs +6 -6
  58. package/src/tui/agent-turn.mjs +82 -26
  59. package/src/tui/ansi.mjs +4 -2
  60. package/src/tui/clipboard.mjs +2 -2
  61. package/src/tui/cmd-auto.mjs +1 -1
  62. package/src/tui/cmd-clear.mjs +1 -1
  63. package/src/tui/cmd-config.mjs +2 -2
  64. package/src/tui/cmd-exit.mjs +2 -2
  65. package/src/tui/cmd-extract.mjs +1 -1
  66. package/src/tui/cmd-goal.mjs +4 -4
  67. package/src/tui/cmd-help.mjs +1 -1
  68. package/src/tui/cmd-init.mjs +3 -3
  69. package/src/tui/cmd-mcp.mjs +5 -5
  70. package/src/tui/cmd-model.mjs +1 -1
  71. package/src/tui/cmd-new.mjs +1 -1
  72. package/src/tui/cmd-plan.mjs +1 -1
  73. package/src/tui/cmd-reindex.mjs +2 -2
  74. package/src/tui/cmd-restore.mjs +1 -1
  75. package/src/tui/cmd-session.mjs +1 -1
  76. package/src/tui/cmd-skills.mjs +1 -1
  77. package/src/tui/cmd-think.mjs +2 -2
  78. package/src/tui/config-helpers.mjs +6 -6
  79. package/src/tui/distill-cmd.mjs +3 -3
  80. package/src/tui/index.mjs +131 -72
  81. package/src/tui/interaction.mjs +13 -12
  82. package/src/tui/key-handler.mjs +31 -23
  83. package/src/tui/layout.mjs +22 -17
  84. package/src/tui/pickers.mjs +19 -19
  85. package/src/tui/render-frame.mjs +37 -11
  86. package/src/tui/render.mjs +25 -24
  87. package/src/tui/slash-commands.mjs +14 -14
  88. package/src/tui/startup.mjs +15 -14
  89. package/src/tui/wizard.mjs +11 -11
  90. package/src/tui.mjs +2 -2
  91. package/bin/thincoder.js +0 -4
  92. package/src/tools/bash.mjs +0 -144
  93. package/src/tools/glob.mjs +0 -51
  94. package/src/tools/grep.mjs +0 -100
  95. package/src/tools/ls.mjs +0 -36
  96. package/src/tools.mjs +0 -2
  97. 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 由模型提供且位于 "--" 之前:校验字符集,防 "--output=..." 之类被 git 当成选项
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 状态字符 + 空格 + 文件路径(部分环境只 1 空格)
53
- // 去掉可能的 CRexecFileSync 在某些 Windows git 下会残留 \r 在行末但不在换行符中)
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
- // 尝试匹配 "XY path" "XY path"(可变间距)
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
- // 重命名条目 porcelain 输出为 "R old -> new",拆开明确展示而非当成一个字面文件名
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
- /** 执行 git 命令;非 git 仓库 / git 不可用时返回空字符串 */
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) captured)`
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
- return `Rewound to checkpoint ${args.id}: patch ${s.patchApplied ? "applied" : "(empty)"}, ${s.restored} untracked file(s) restored, ${s.deleted} file(s) deleted.\n(The pre-rewind state was snapshotted first — you can rewind again to go back.)`
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
- return cps.map((c) => `${c.id} ${new Date(c.time).toISOString()} ${c.untracked} untracked file(s)`).join("\n")
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
+ }
@@ -1,4 +1,4 @@
1
- // tools/index.mjs — 后端兼容重导出
1
+ // tools/index.mjs — backend-compatible re-export
2
2
  export { toOpenAISchema } from "./shared.mjs";
3
3
 
4
4
  import { readTool, writeTool, editTool, insertAfterTool, readImageTool } from "./file.mjs";
@@ -13,11 +13,12 @@ import { existsSync } from "node:fs";
13
13
  import { join, relative, dirname } from "node:path";
14
14
 
15
15
  /**
16
- * 解析统一 diff(unified diff):返回 [{ path, isNew, hunks: [{ ops: [{type:" "|"-"|"+", text}] }] }]
17
- * @@ 头的行数计数消费 hunk 行——LLM 常把上下文空行剥成纯空行,靠计数而不是行首字符判断 hunk 边界
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
- // 补丁文本常来自 CRLF 终端/模型输出,行尾 \r 会混进 hunk 内容导致上下文对不上,统一剥掉
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
- /** 在内存行数组上按序应用 hunks;任何一步失败抛错(调用方保证不落盘)。比较时忽略行尾 \r,上下文行保留原始字节 */
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
- /** agent 层追踪触碰文件(多路径,替代单 path 参数) */
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
- // 先全部读入内存试算:任何一个 hunk 不上就整体抛错,不写半个补丁(原子性)
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
- // 多文件写:先全部写 .tmp,全部成功后再 rename——任一写失败清理已写的 .tmp 不影响已落盘的文件
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 阶段失败:清理残留 .tmp 文件
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 syntaxResults = planned.map((p) => {
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
- }).filter(Boolean).join("\n")
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 把错误写到 stderr
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
- // 用解析后的相对路径(统一正斜杠),防反斜杠/非常规路径绕过 ls-files 匹配
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
- // 未跟踪 / git 仓库
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 — 仓库依赖图解析(零依赖,纯 regex
3
- * code_chunks 取已知文件列表,实时解析每个文件的 import/export 关系,
4
- * 构建正向依赖图 + 反向引用图。被 repomap.mjs buildSummary / buildOutline 共用。
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
- * 返回 { deps, importers, fileCount } buildOutline / buildSummary 共用。
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
- // import 路径解析成相对路径(处理 ./ ../)
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
- /** 提取 JS/TS 文件的 import 路径(去掉 .ts/.js/.mjs 后缀统一) */
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
- // 普通 import
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
- /** 提取 JS/TS 文件的 export 符号 */
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 } —— 优先取 as 后的导出名
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(导出名),"a" → a
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
- /** 提取 Python import 和顶层 def/class */
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
- * 前导 n 个点表示上溯 n-1 层("."=当前包),模块点号转路径分隔符。
160
- * 非相对导入(不以 . 开头)或纯包导入("from . import x")返回 null
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
@@ -1,16 +1,16 @@
1
1
  /**
2
- * repomap.mjs — 仓库依赖大纲(零依赖,纯 regex
3
- * 实时解析 import/export 关系,生成紧凑文本给 LLM 理解代码结构。
4
- * 不存索引——每次调用读文件解析,~50ms 完成。
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
- /** 提取 JS/TS 文件的 import 路径(去掉 .ts/.js/.mjs 后缀统一) */
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
- // 普通 import
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
- /** 提取 JS/TS 文件的 export 符号 */
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 } —— 优先取 as 后的导出名
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(导出名),"a" → a
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
- /** 提取 Python import 和顶层 def/class */
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
- * 前导 n 个点表示上溯 n-1 层("."=当前包),模块点号转路径分隔符。
98
- * 非相对导入(不以 . 开头)或纯包导入("from . import x")返回 null
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
- * 返回 { deps, importers, fileCount } buildOutline / buildSummary 共用。
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
- // import 路径解析成相对路径(处理 ./ ../)
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. 枢纽文件 Top-12(被 import 最多的文件——架构骨架)
173
- * 3. 入口文件(无人 import 的文件——启动/顶层入口)
174
- * 输出天然有界(~1000-2000 字符),不再需要 OUTLINE_INJECT_MAX 硬截断。
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) 枢纽文件 Top-12:按被 import 次数降序
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) 入口文件:无人 import 的(叶子/入口)
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
- /** code_chunks 取已知文件列表(复用索引),按路径解析生成大纲文本 */
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(匹配时去掉扩展名,因为 import 路径通常不含 .mjs/.js 后缀)
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
- * 生成 repo_outline 工具(只读)。
285
- * 需要 memory.db(复用 code_chunks 文件列表)和 cwd
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 {