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/markdown.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * markdown.mjs — 记忆条目的 markdown + frontmatter 格式
3
- * 零依赖解析/序列化。条目格式见 ARCHITECTURE-v2.md
2
+ * markdown.mjs — markdown + frontmatter format for memory entries
3
+ * Zero-dependency parsing/serialization. Entry format see ARCHITECTURE-v2.md.
4
4
  */
5
5
 
6
6
  const VALID_TYPES = new Set(["rule", "knowledge", "decision", "pattern"])
7
7
 
8
8
  /**
9
- * 解析 markdown 条目。
9
+ * Parse a markdown entry.
10
10
  * → { meta: { type, title, tags, author, created, embedding? }, content }
11
- * frontmatter 或缺必要字段时抛错。
11
+ * Throws if frontmatter is missing or required fields absent.
12
12
  */
13
13
  export function parseEntry(text) {
14
14
  const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
@@ -36,13 +36,13 @@ export function parseEntry(text) {
36
36
  }
37
37
 
38
38
  /**
39
- * 序列化为 markdown 条目文本。
39
+ * Serialize to markdown entry text.
40
40
  */
41
41
  export function serializeEntry(meta, content) {
42
42
  if (!VALID_TYPES.has(meta.type)) throw new Error(`invalid type "${meta.type}"`)
43
43
  if (!meta.title) throw new Error("meta.title is required")
44
- // frontmatter 标量必须单行:title/author 含换行会注入伪 frontmatter
45
- // (如 title "x\ntype: rule" 解析时覆盖真实 type),tags 含换行/逗号同理
44
+ // frontmatter scalars must be single-line: newlines in title/author would inject fake frontmatter rows
45
+ // (e.g. title "x\ntype: rule" would override real type when parsed), same for tags with newlines/commas
46
46
  const tags = (meta.tags ?? []).map((t) => oneLine(t).replaceAll(",", " ")).join(", ")
47
47
  const lines = [
48
48
  "---",
@@ -57,7 +57,7 @@ export function serializeEntry(meta, content) {
57
57
  return lines.join("\n")
58
58
  }
59
59
 
60
- /** 标题转文件名 slug:保留中英文数字,其余转连字符 */
60
+ /** Convert title to filename slug: keep alphanumeric + CJK, convert rest to hyphens */
61
61
  export function slugify(title) {
62
62
  return title
63
63
  .trim()
@@ -67,23 +67,23 @@ export function slugify(title) {
67
67
  .slice(0, 50) || "untitled"
68
68
  }
69
69
 
70
- /** 生成条目文件名:YYYYMMDD-<slug>-<rand4>.md */
70
+ /** Generate entry filename: YYYYMMDD-<slug>-<rand4>.md */
71
71
  export function entryFilename(title, date = new Date()) {
72
72
  const ymd = date.toISOString().slice(0, 10).replaceAll("-", "")
73
73
  const rand = Math.random().toString(36).slice(2, 6)
74
74
  return `${ymd}-${slugify(title)}-${rand}.md`
75
75
  }
76
76
 
77
- // ---------------------------------------------------------------- 内部
77
+ // ---------------------------------------------------------------- internal
78
78
 
79
- /** 压成单行(frontmatter 标量用):换行折叠为空格,防注入伪字段行 */
79
+ /** Collapse to single line (for frontmatter scalars): fold newlines into spaces, prevent injecting fake field lines */
80
80
  function oneLine(v) {
81
81
  return String(v).replace(/\s*\r?\n\s*/g, " ").trim()
82
82
  }
83
83
 
84
84
  /**
85
- * 极简 YAML 子集解析:只支持 `key: value` `key: [a, b, c]`。
86
- * 我们的 frontmatter 是自己生成的,不需要完整 YAML
85
+ * Minimal YAML subset parser: only supports `key: value` and `key: [a, b, c]`.
86
+ * Our frontmatter is self-generated, no need for full YAML.
87
87
  */
88
88
  function parseFrontmatter(text) {
89
89
  const meta = {}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * mcp/helpers.mjs — MCP 共享工具函数与常量
2
+ * mcp/helpers.mjs — MCP shared utility functions and constants
3
3
  */
4
4
 
5
5
  export const INIT_TIMEOUT_MS = 30_000
@@ -7,10 +7,12 @@ export const CALL_TIMEOUT_MS = 120_000
7
7
  export const ENDPOINT_WAIT_MS = 5_000
8
8
 
9
9
  let nextRpcId = 0
10
+ /** Generate a unique incrementing RPC ID string */
10
11
  export function rpcId() {
11
12
  return String(++nextRpcId)
12
13
  }
13
14
 
15
+ /** Race a promise against a timeout, rejecting after ms milliseconds */
14
16
  export function withTimeout(promise, ms) {
15
17
  let timer
16
18
  const timeout = new Promise((_, reject) => {
@@ -20,10 +22,12 @@ export function withTimeout(promise, ms) {
20
22
  return Promise.race([promise.finally(() => clearTimeout(timer)), timeout])
21
23
  }
22
24
 
25
+ /** Quote a shell argument: wrap in double-quotes if it contains whitespace or quotes */
23
26
  export function quoteArg(s) {
24
27
  return /[\s"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
25
28
  }
26
29
 
30
+ /** Append a Bearer token as a query parameter to a WebSocket URL */
27
31
  export function withAuthToken(wsUrl, authorization) {
28
32
  if (!authorization) return wsUrl
29
33
  const token = authorization.replace(/^Bearer\s+/i, "")
@@ -32,6 +36,7 @@ export function withAuthToken(wsUrl, authorization) {
32
36
  return u.href
33
37
  }
34
38
 
39
+ /** Sanitize a tool name: replace non-alphanumeric chars with underscores, cap at 64 chars */
35
40
  export function sanitizeToolName(name) {
36
41
  return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
37
42
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { rpcId, CALL_TIMEOUT_MS, ENDPOINT_WAIT_MS, withTimeout } from "./helpers.mjs"
5
5
 
6
+ /** Create an MCP HTTP+SSE transport for Streamable HTTP servers */
6
7
  export function httpTransport(baseURL, extraHeaders = {}) {
7
8
  const url = baseURL.replace(/\/+$/, "")
8
9
  let sessionId = null
@@ -82,7 +83,7 @@ export function httpTransport(baseURL, extraHeaders = {}) {
82
83
  pending.delete(msg.id)
83
84
  resolver(msg)
84
85
  }
85
- } catch { /* JSON,忽略 */ }
86
+ } catch { /* not JSON, ignore */ }
86
87
  }
87
88
  } catch (error) {
88
89
  if (!closed) {
@@ -4,6 +4,7 @@
4
4
  import { spawn } from "node:child_process"
5
5
  import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
6
6
 
7
+ /** Create an MCP stdio transport over a spawned child process */
7
8
  export function stdioTransport(command, args) {
8
9
  const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env } }
9
10
  const child =
@@ -39,7 +40,7 @@ export function stdioTransport(command, args) {
39
40
  pending.delete(msg.id)
40
41
  resolver(msg)
41
42
  }
42
- } catch { /* JSON 行忽略 */ }
43
+ } catch { /* non-JSON line, ignore */ }
43
44
  }
44
45
  })
45
46
 
@@ -77,7 +78,7 @@ export function stdioTransport(command, args) {
77
78
  if (closed) return
78
79
  try {
79
80
  child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n")
80
- } catch { /* 忽略 */ }
81
+ } catch { /* ignore */ }
81
82
  }
82
83
 
83
84
  return { send, notify, close: () => { if (!closed) child.kill() } }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { rpcId, INIT_TIMEOUT_MS, CALL_TIMEOUT_MS, withTimeout, withAuthToken } from "./helpers.mjs"
5
5
 
6
+ /** Create an MCP WebSocket transport */
6
7
  export function wsTransport(wsUrl, extraHeaders = {}) {
7
8
  const pending = new Map()
8
9
  let closed = false
@@ -36,7 +37,7 @@ export function wsTransport(wsUrl, extraHeaders = {}) {
36
37
  pending.delete(msg.id)
37
38
  resolver(msg)
38
39
  }
39
- } catch { /* JSON,忽略 */ }
40
+ } catch { /* not JSON, ignore */ }
40
41
  })
41
42
 
42
43
  ws.addEventListener("error", (event) => {
@@ -80,7 +81,7 @@ export function wsTransport(wsUrl, extraHeaders = {}) {
80
81
  const close = () => {
81
82
  closed = true
82
83
  failAll("Connection closed")
83
- try { ws?.close() } catch { /* 忽略 */ }
84
+ try { ws?.close() } catch { /* ignore */ }
84
85
  }
85
86
 
86
87
  return { send, notify, close, connect }
package/src/mcp.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * mcp.mjs — MCP (Model Context Protocol) client
3
- * config: { command, args?, name } { url, name, headers? } { wsUrl, name, headers? }
3
+ * config: { command, args?, name } or { url, name, headers? } or { wsUrl, name, headers? }
4
4
  */
5
5
  import { INIT_TIMEOUT_MS, withTimeout, sanitizeToolName } from "./mcp/helpers.mjs"
6
6
  import { stdioTransport } from "./mcp/transport-stdio.mjs"
@@ -46,6 +46,7 @@ async function doInitialize(transport, name) {
46
46
  return toolsResp.result?.tools ?? []
47
47
  }
48
48
 
49
+ /** Connect to an MCP server (stdio/http/ws), initialize, and return built tool wrappers */
49
50
  export async function connectMcpServer(config) {
50
51
  if (config.wsUrl) {
51
52
  const transport = wsTransport(config.wsUrl, config.headers ?? {})
@@ -64,7 +65,7 @@ export async function connectMcpServer(config) {
64
65
  try {
65
66
  await transport.openSSE()
66
67
  } catch {
67
- // 不支持 GET server(纯 Streamable HTTP POST):降级为无 SSE 模式
68
+ // Server doesn't support GET (pure Streamable HTTP POST): degrade to no-SSE mode
68
69
  }
69
70
  try {
70
71
  const mcpTools = await doInitialize(transport, config.name ?? config.url)
@@ -89,12 +90,14 @@ export async function connectMcpServer(config) {
89
90
  throw new Error(`MCP server "${config.name}": needs either 'wsUrl' (websocket), 'command' (stdio), or 'url' (http)`)
90
91
  }
91
92
 
93
+ /** Close all MCP transport connections on an agent's tools */
92
94
  export function closeAllMcp(agent) {
93
95
  for (const t of agent.tools) {
94
96
  if (t._mcpTransport) t._mcpTransport.close()
95
97
  }
96
98
  }
97
99
 
100
+ /** Remove MCP tools belonging to a specific server from the agent's tool list */
98
101
  export function removeMcpTools(agent, serverName) {
99
102
  const keep = []
100
103
  for (const t of agent.tools) {
@@ -1,10 +1,10 @@
1
1
  /**
2
- * memory/code-index.mjs — 代码和文档的分块、语言检测、符号提取
2
+ * memory/code-index.mjs — code and document chunking, language detection, symbol extraction
3
3
  */
4
4
 
5
5
  import { segmentCJK, CODE_EXTS, DOC_EXTS, SKIP_DIRS, BIG_FILE_LINES } from "./schema.mjs"
6
6
 
7
- /** 推断文件语言(按扩展名) */
7
+ /** Infer file language by extension */
8
8
  export function detectLanguage(filename) {
9
9
  const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase()
10
10
  const map = {
@@ -21,8 +21,8 @@ export function detectLanguage(filename) {
21
21
  }
22
22
 
23
23
  /**
24
- * 用正则提取 JS/TS 文件的顶层符号声明(函数、类、const 导出等)。
25
- * 返回 [{ name, line, kind }]
24
+ * Extract top-level symbol declarations (functions, classes, const exports, etc.)
25
+ * from JS/TS files using regex. Returns [{ name, line, kind }].
26
26
  */
27
27
  export function extractSymbols(lines, ext) {
28
28
  const jsish = new Set([".mjs", ".js", ".ts", ".jsx", ".tsx"])
@@ -41,7 +41,7 @@ export function extractSymbols(lines, ext) {
41
41
  return symbols
42
42
  }
43
43
 
44
- /** 提取 Python 文件的顶层 def/class */
44
+ /** Extract top-level def/class from Python files. */
45
45
  export function extractPySymbols(lines) {
46
46
  const symbols = []
47
47
  const re = /^(?:async\s+)?(?:def|class)\s+(\w+)/gm
@@ -54,8 +54,9 @@ export function extractPySymbols(lines) {
54
54
  }
55
55
 
56
56
  /**
57
- * 将一个文件拆成代码块。小文件整文件一块;大文件按符号切分,符号间的内容并入前一个符号块。
58
- * 每个块会额外带上符号前的 JSDoc / docstring 注释,提升搜索质量。
57
+ * Split a file into code chunks. Small files become a single chunk;
58
+ * large files are split by symbol, with inter-symbol content merged into the preceding symbol chunk.
59
+ * Each chunk includes the JSDoc/docstring before its symbol to improve search quality.
59
60
  */
60
61
  export function chunkCode(lines, filepath) {
61
62
  const ext = filepath.slice(filepath.lastIndexOf(".")).toLowerCase()
@@ -91,9 +92,9 @@ export function chunkCode(lines, filepath) {
91
92
  }
92
93
 
93
94
  /**
94
- * 提取指定行之前的 JSDoc / docstring 注释。
95
- * JS/TS: 向前扫描 /** ... *​/ // 连续注释行
96
- * Python: 符号定义行的下一行开始找 """...""" docstring
95
+ * Extract the JSDoc/docstring comment preceding a given line.
96
+ * JS/TS: scan backwards for JSDoc block comments or consecutive // comment lines
97
+ * Python: look for a """...""" docstring on the line after the symbol definition
97
98
  */
98
99
  export function extractLeadingDoc(lines, lineNum, ext) {
99
100
  if (ext === ".py") {
@@ -139,12 +140,12 @@ export function extractLeadingDoc(lines, lineNum, ext) {
139
140
  return text.length > 0 && text.length < 300 ? text : ""
140
141
  }
141
142
 
142
- /** 将控制权交还给事件循环一个 tick(让键盘输入有机会被处理) */
143
+ /** Yield control to the event loop for one tick (allows keyboard input to be processed) */
143
144
  export function yieldTick() {
144
145
  return new Promise((r) => setTimeout(r, 0))
145
146
  }
146
147
 
147
- /** 单文件入索引:删除旧块分块插入新块 */
148
+ /** Index a single file: delete old chunks chunkinsert new chunks */
148
149
  export function _upsertCodeFile(memory, origin, rel, lines, lang, mtimeMs) {
149
150
  const chunks = chunkCode(lines, rel)
150
151
  memory.db.exec("BEGIN")
@@ -166,8 +167,8 @@ export function _upsertCodeFile(memory, origin, rel, lines, lang, mtimeMs) {
166
167
  }
167
168
 
168
169
  /**
169
- * ## 标题切分 markdown 文件。每个 ## section 独立入索引,
170
- * 标题路径做 heading(如 "README.md > 部署 > Docker"),方便检索定位。
170
+ * Split a markdown file by ## headings. Each ## section is indexed independently,
171
+ * with the heading path as the heading label (e.g. "README.md > Deployment > Docker") for easy retrieval.
171
172
  */
172
173
  export function chunkMarkdown(lines, filepath) {
173
174
  const chunks = []
@@ -190,6 +191,7 @@ export function chunkMarkdown(lines, filepath) {
190
191
  return chunks.filter((c) => c.content)
191
192
  }
192
193
 
194
+ /** Upsert a documentation file's chunks into the doc_chunks table within a transaction */
193
195
  export function _upsertDocFile(memory, origin, rel, lines, mtimeMs) {
194
196
  const chunks = chunkMarkdown(lines, rel)
195
197
  const lang = rel.endsWith(".rst") ? "rst" : rel.endsWith(".adoc") ? "asciidoc" : rel.endsWith(".txt") ? "text" : "markdown"
@@ -1,18 +1,22 @@
1
1
  /**
2
- * memory/code-sync.mjs — 代码索引同步、检索、增量更新
2
+ * memory/code-sync.mjs — code index sync, retrieval, incremental update
3
3
  */
4
4
 
5
5
  import { readFile, stat } from "node:fs/promises"
6
6
  import { join, relative } from "node:path"
7
7
  import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
8
8
  import { CODE_EXTS, DOC_EXTS, SKIP_DIRS } from "./schema.mjs"
9
- import { buildFtsQuery, ensureEmbeddings } from "./core.mjs"
9
+ import { buildFtsQuery, ensureEmbeddings, EMBED_TEXT_MAX_LEN } from "./core.mjs"
10
10
  import { detectLanguage, _upsertCodeFile, _upsertDocFile, yieldTick } from "./code-index.mjs"
11
11
 
12
+ const DIFF_FULL_SYNC_THRESHOLD = 200
13
+ const CODE_EMBED_BATCH = 64
14
+
12
15
  /**
13
- * git 驱动增量索引:用 git diff 找出上次索引以来的变更文件,
14
- * 只重建这些文件的 FTS5 块(不碰向量)。比全量 mtime 扫描快一个数量级。
15
- * 返回 { updated, removed, skipped } null(git 不可用)。
16
+ * git-driven incremental indexing: use git diff to find files changed since
17
+ * the last index, and only rebuild FTS5 chunks for those files (vectors are untouched).
18
+ * An order of magnitude faster than full mtime scanning.
19
+ * Returns { updated, removed, skipped } or null (git unavailable).
16
20
  */
17
21
  export async function gitSync(memory, dir, { onProgress } = {}) {
18
22
  const { execSync, execFileSync } = await import("node:child_process")
@@ -34,8 +38,8 @@ export async function gitSync(memory, dir, { onProgress } = {}) {
34
38
  return null
35
39
  }
36
40
 
37
- if (diffOut.length > 200) {
38
- // diff 太大,增量没意义——回退全量同步并更新锚点
41
+ if (diffOut.length > DIFF_FULL_SYNC_THRESHOLD) {
42
+ // diff too large, incremental is useless — fall back to full sync and update anchor
39
43
  await codeSync(memory, dir, { onProgress })
40
44
  const { docSync } = await import("./docs.mjs")
41
45
  await docSync(memory, dir, { onProgress })
@@ -60,11 +64,11 @@ export async function gitSync(memory, dir, { onProgress } = {}) {
60
64
  if (CODE_EXTS.has(ext)) {
61
65
  const lang = detectLanguage(abs)
62
66
  let mtimeMs = 0
63
- try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* 新文件 */ }
67
+ try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* new file */ }
64
68
  _upsertCodeFile(memory, dir, rel, lines, lang, mtimeMs)
65
69
  } else {
66
70
  let mtimeMs = 0
67
- try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* 新文件 */ }
71
+ try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* new file */ }
68
72
  _upsertDocFile(memory, dir, rel, lines, mtimeMs)
69
73
  }
70
74
  updated++
@@ -94,8 +98,8 @@ export async function gitSync(memory, dir, { onProgress } = {}) {
94
98
  }
95
99
 
96
100
  /**
97
- * 同步代码索引:扫描 dir 下所有源文件 分块 → upsert code_chunks
98
- * mtime 增量——只重建变更过的文件块。
101
+ * Sync code index: scan all source files under dir → chunk → upsert into code_chunks.
102
+ * Incremental by mtime — only rebuilds chunks for files that have changed.
99
103
  */
100
104
  export async function codeSync(memory, dir, { onProgress } = {}) {
101
105
  const files = []
@@ -165,19 +169,19 @@ export async function codeSync(memory, dir, { onProgress } = {}) {
165
169
  return { updated, removed, skipped, failed, errors, total: files.length }
166
170
  }
167
171
 
168
- /** 记录当前 HEAD 作为索引锚点(gitSync 增量 diff 基准);非 git 仓库静默跳过 */
172
+ /** Record current HEAD as the index anchor (gitSync incremental diff baseline); silently skip non-git repos */
169
173
  export async function markIndexedCommit(memory, dir) {
170
174
  try {
171
175
  const { execSync } = await import("node:child_process")
172
176
  const head = execSync("git rev-parse HEAD", { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim()
173
177
  memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
174
178
  ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head)
175
- } catch { /* git 仓库或 git 不可用,跳过 */ }
179
+ } catch { /* not a git repo or git unavailable, skip */ }
176
180
  }
177
181
 
178
182
  /**
179
- * 代码检索:FTS5(BM25) + 可选向量余弦,RRF 合并。
180
- * embedder 时退化为纯 FTS;ftsQuery 为空且有 embedder 时退化为纯向量。
183
+ * Code search: FTS5(BM25) + optional vector cosine, RRF merged.
184
+ * Falls back to pure FTS when no embedder; falls back to pure vector when ftsQuery is empty and embedder is present.
181
185
  */
182
186
  export async function codeSearch(memory, query, { limit = 5 } = {}) {
183
187
  const ftsQuery = buildFtsQuery(query)
@@ -219,14 +223,20 @@ export async function codeSearch(memory, query, { limit = 5 } = {}) {
219
223
  const fetchChunk = memory.db.prepare(`
220
224
  SELECT path, language, symbol_name, content, line_start, line_end FROM code_chunks WHERE rowid = ?
221
225
  `)
222
- return [...scores.entries()]
226
+ const sorted = [...scores.entries()]
223
227
  .sort((a, b) => b[1] - a[1])
224
228
  .slice(0, limit)
225
- .map(([rowid]) => fetchChunk.get(rowid))
229
+ return sorted
230
+ .map(([rowid, score]) => {
231
+ const chunk = fetchChunk.get(rowid)
232
+ if (!chunk) return null
233
+ chunk._score = Math.round(score * 100) / 100
234
+ return chunk
235
+ })
226
236
  .filter(Boolean)
227
237
  }
228
238
 
229
- /** 惰性补算 code_chunks 缺失的向量 */
239
+ /** Lazily backfill missing vectors for code_chunks */
230
240
  export async function ensureCodeEmbeddings(memory) {
231
241
  if (!memory.embedder) return
232
242
  const modelKey = memory.embedder.model
@@ -237,17 +247,17 @@ export async function ensureCodeEmbeddings(memory) {
237
247
  ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(modelKey)
238
248
  }
239
249
 
240
- const pending = memory.db.prepare(`SELECT rowid, path, symbol_name, content FROM code_chunks WHERE embedding IS NULL LIMIT 64`).all()
250
+ const pending = memory.db.prepare(`SELECT rowid, path, symbol_name, content FROM code_chunks WHERE embedding IS NULL LIMIT ${CODE_EMBED_BATCH}`).all()
241
251
  if (pending.length === 0) return
242
252
 
243
- const texts = pending.map((r) => `${r.path}${r.symbol_name ? " :: " + r.symbol_name : ""}\n${r.content.slice(0, 2000)}`)
253
+ const texts = pending.map((r) => `${r.path}${r.symbol_name ? " :: " + r.symbol_name : ""}\n${r.content.slice(0, EMBED_TEXT_MAX_LEN)}`)
244
254
  const vecs = await embed(memory.embedder, texts)
245
255
 
246
256
  const update = memory.db.prepare(`UPDATE code_chunks SET embedding = ? WHERE rowid = ?`)
247
257
  pending.forEach((r, i) => update.run(toBlob(vecs[i]), r.rowid))
248
258
  }
249
259
 
250
- /** 生成 code_search 工具(只读)。 */
260
+ /** Generate the code_search tool (read-only). */
251
261
  export function codeSearchTool(memory) {
252
262
  return {
253
263
  name: "code_search",
@@ -266,14 +276,14 @@ export function codeSearchTool(memory) {
266
276
  const results = await codeSearch(memory, args.query, { limit: args.limit ?? 5 })
267
277
  if (results.length === 0) return "(no matching code)"
268
278
  return results.map((r) =>
269
- `${r.path}${r.symbol_name ? ` :: ${r.symbol_name}` : ""} (L${r.line_start}-L${r.line_end}):\n${r.content.slice(0, 2000)}`
279
+ `${r.path}${r.symbol_name ? ` :: ${r.symbol_name}` : ""} (L${r.line_start}-L${r.line_end}, relevance ${r._score?.toFixed(2) ?? "?"}):\n${r.content.slice(0, 2000)}`
270
280
  ).join("\n\n---\n\n")
271
281
  },
272
282
  }
273
283
  }
274
284
 
275
285
  /**
276
- * 单文件增量重索引:write/edit/delete 后调用,只重建这一条路径。
286
+ * Single-file incremental reindex: called after write/edit/delete, only rebuilds this one path.
277
287
  */
278
288
  export async function reindexFile(memory, cwd, absPath) {
279
289
  const ext = absPath.slice(absPath.lastIndexOf(".")).toLowerCase()
@@ -293,14 +303,14 @@ export async function reindexFile(memory, cwd, absPath) {
293
303
  if (CODE_EXTS.has(ext)) {
294
304
  const lang = detectLanguage(absPath)
295
305
  let mtimeMs = 0
296
- try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* 新文件 */ }
306
+ try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* new file */ }
297
307
  _upsertCodeFile(memory, cwd, rel, lines, lang, mtimeMs)
298
308
  } else if (DOC_EXTS.has(ext)) {
299
309
  let mtimeMs = 0
300
- try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* 新文件 */ }
310
+ try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* new file */ }
301
311
  _upsertDocFile(memory, cwd, rel, lines, mtimeMs)
302
312
  }
303
313
  if (memory.embedder) {
304
- try { await ensureEmbeddings(memory) } catch { /* embedding 失败不阻塞 */ }
314
+ try { await ensureEmbeddings(memory) } catch { /* embedding failure is non-blocking */ }
305
315
  }
306
316
  }