thincoder 0.8.2 → 0.8.4

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 (98) 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 +12 -11
  8. package/src/agent-tools/plan.mjs +5 -6
  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 +7 -26
  13. package/src/agent-tools/verify.mjs +21 -19
  14. package/src/agent-tools.mjs +3 -3
  15. package/src/agent.mjs +82 -92
  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 +56 -53
  22. package/src/context.mjs +59 -61
  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 +0 -6
  40. package/src/prompts/discipline.md +9 -0
  41. package/src/prompts/main.md +24 -24
  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 -42
  47. package/src/skills.mjs +17 -17
  48. package/src/tools/checkpoint.md +6 -2
  49. package/src/tools/file.mjs +20 -14
  50. package/src/tools/git.mjs +83 -12
  51. package/src/tools/index.mjs +1 -1
  52. package/src/tools/ls.md +1 -1
  53. package/src/tools/patch.mjs +20 -18
  54. package/src/tools/repomap-parse.mjs +17 -17
  55. package/src/tools/repomap.mjs +29 -29
  56. package/src/tools/shared.mjs +55 -28
  57. package/src/tools/system.mjs +163 -118
  58. package/src/tools/web.mjs +6 -6
  59. package/src/tui/agent-turn.mjs +82 -26
  60. package/src/tui/ansi.mjs +5 -3
  61. package/src/tui/clipboard.mjs +2 -2
  62. package/src/tui/cmd-auto.mjs +3 -12
  63. package/src/tui/cmd-clear.mjs +1 -1
  64. package/src/tui/cmd-config.mjs +108 -23
  65. package/src/tui/cmd-exit.mjs +2 -2
  66. package/src/tui/cmd-extract.mjs +11 -3
  67. package/src/tui/cmd-goal.mjs +3 -12
  68. package/src/tui/cmd-help.mjs +2 -2
  69. package/src/tui/cmd-init.mjs +3 -3
  70. package/src/tui/cmd-mcp.mjs +38 -25
  71. package/src/tui/cmd-model.mjs +1 -1
  72. package/src/tui/cmd-new.mjs +33 -13
  73. package/src/tui/cmd-plan.mjs +3 -12
  74. package/src/tui/cmd-reindex.mjs +2 -2
  75. package/src/tui/cmd-restore.mjs +1 -1
  76. package/src/tui/cmd-session.mjs +1 -1
  77. package/src/tui/cmd-skills.mjs +1 -1
  78. package/src/tui/cmd-think.mjs +4 -11
  79. package/src/tui/config-helpers.mjs +6 -6
  80. package/src/tui/distill-cmd.mjs +4 -3
  81. package/src/tui/index.mjs +131 -72
  82. package/src/tui/interaction.mjs +13 -12
  83. package/src/tui/key-handler.mjs +34 -26
  84. package/src/tui/layout.mjs +22 -17
  85. package/src/tui/pickers.mjs +19 -19
  86. package/src/tui/render-frame.mjs +37 -11
  87. package/src/tui/render.mjs +25 -24
  88. package/src/tui/slash-commands.mjs +22 -22
  89. package/src/tui/startup.mjs +15 -14
  90. package/src/tui/wizard.mjs +11 -11
  91. package/src/tui.mjs +2 -2
  92. package/bin/thincoder.js +0 -4
  93. package/src/tools/bash.mjs +0 -144
  94. package/src/tools/glob.mjs +0 -51
  95. package/src/tools/grep.mjs +0 -100
  96. package/src/tools/ls.mjs +0 -36
  97. package/src/tools.mjs +0 -2
  98. package/src/tui-render.mjs +0 -4
@@ -6,26 +6,26 @@ import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } fro
6
6
  import { repoOutlineTool } from "../tools/repomap.mjs"
7
7
  import { builtinTools } from "../tools/index.mjs"
8
8
 
9
- /** 组装一个带记忆的 agent(同步各层索引后返回) */
10
- export async function makeAgent() {
9
+ /** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
10
+ export async function assembleAgent() {
11
11
  const config = loadConfig()
12
12
  const provider = config.provider
13
13
  const providers = config.providersList
14
14
  const memory = createMemory({ dbPath: config.memory.dbPath })
15
- // 向量检索:配了 embedding 就启用(惰性生成向量,首次搜索时补算)
15
+ // Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
16
16
  if (config.embedding?.apiKey) {
17
17
  const { createEmbedder } = await import("../embedding.mjs")
18
18
  memory.embedder = createEmbedder(config.embedding)
19
19
  }
20
20
  const cwd = process.cwd()
21
- // code/doc 索引按 origin(项目根目录)隔离:检索只查本项目
21
+ // code/doc indices isolated by origin (project root dir): search only scoped to this project
22
22
  memory.codeOrigin = cwd
23
- // Project 层:启动时同步 .thincoder/memory/ 目录到索引(有就同步,没有就跳过)
23
+ // Project layer: sync .thincoder/memory/ dir to index on startup (sync if present, skip otherwise)
24
24
  if (config.memory.projectDir) {
25
25
  memory.projectOrigin = join(cwd, config.memory.projectDir)
26
26
  await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
27
27
  }
28
- // Team 层(可选):首次自动 clone;启动只索引本地目录,拉取远端走显式 thincoder sync
28
+ // Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
29
29
  const team = teamConfig(config)
30
30
  if (team) {
31
31
  const { ensureClone } = await import("../git/gitmem.mjs")
@@ -34,7 +34,7 @@ export async function makeAgent() {
34
34
  }
35
35
  const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd)]
36
36
 
37
- // MCP servers:并行连接(一个死 server 不会拖住启动),失败的收集警告(TUI stderr 不可见,通过 agent 对象传递)
37
+ // MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
38
38
  const mcpServers = config.mcp?.servers ?? []
39
39
  let mcpTools = []
40
40
  const mcpWarnings = []
@@ -67,7 +67,7 @@ export async function makeAgent() {
67
67
  return agent
68
68
  }
69
69
 
70
- /** 读取 team 配置并补全默认目录;未配置返回 null */
70
+ /** Read team config and fill in default dir; return null if not configured */
71
71
  export function teamConfig(config) {
72
72
  const team = config.memory?.team
73
73
  if (!team?.repo) return null
@@ -75,7 +75,7 @@ export function teamConfig(config) {
75
75
  return { name, repo: team.repo, dir: team.dir ?? join(configDir, "teams", name) }
76
76
  }
77
77
 
78
- /** 条目作者:git config user.name 兜底 unknown */
78
+ /** Entry author: git config user.name, fallback "unknown" */
79
79
  export function gitAuthor() {
80
80
  try {
81
81
  return execSync("git config user.name", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "unknown"
@@ -1,6 +1,6 @@
1
1
  import { put, remove, search, list } from "../memory.mjs"
2
2
 
3
- /** thincoder memory <list|search|put|remove> 子命令 */
3
+ /** thincoder memory <list|search|put|remove> subcommands */
4
4
  export async function memoryCommand(memory, args) {
5
5
  const [sub, ...rest] = args
6
6
 
@@ -1,12 +1,12 @@
1
1
  import { createInterface } from "node:readline"
2
2
 
3
- /** CLI 版工具参数摘要(截断长 JSON */
3
+ /** CLI tool arg summary (truncated long JSON) */
4
4
  export function summarize(toolArgs) {
5
5
  const s = JSON.stringify(toolArgs)
6
6
  return s.length > 120 ? s.slice(0, 120) + "..." : s
7
7
  }
8
8
 
9
- /** 权限请求的关键信息(按工具定制),与 TUI formatPermission 对齐。name 可能带子 agent 前缀("coder/bash"),取基名匹配 */
9
+ /** Permission request key info (per-tool customized), aligned with TUI formatPermission. name may include sub-agent prefix ("coder/bash") — extract basename for matching */
10
10
  export function formatPermission(name, args) {
11
11
  const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
12
12
  const base = name.includes("/") ? name.split("/").pop() : name
@@ -23,7 +23,7 @@ export function formatPermission(name, args) {
23
23
  return cap(summarize(args), 300)
24
24
  }
25
25
 
26
- /** 权限确认:TTY 下交互询问 y/n;非交互环境默认拒绝(安全优先) */
26
+ /** Permission confirmation: interactive y/n on TTY; non-interactive defaults to deny (safety-first) */
27
27
  export async function askPermission(name, toolArgs) {
28
28
  if (!process.stdin.isTTY) {
29
29
  console.error(`\n[deny] ${name} (non-interactive, side-effect tools require a TTY)`)
@@ -2,9 +2,9 @@ import { existsSync, readFileSync } from "node:fs"
2
2
  import { createInterface } from "node:readline"
3
3
  import { configPath, saveConfig, PROVIDER_PRESETS } from "../config.mjs"
4
4
 
5
- /** 首次使用(TTY 下的 chat/distill):问答式配置一个 provider 并落盘,返回运行时 provider;取消返回 null */
5
+ /** First-time setup (TTY chat / distill): ask a few questions to configure a provider, save to disk, return runtime provider. Cancel returns null. */
6
6
  export async function setupWizard() {
7
- // 自带缓冲的提问器:rl.question 在输入被管道/快速粘贴时会丢行(问题注册前 line 已到达)
7
+ // Buffered asker: rl.question loses lines when input is piped/fast-pasted (line arrives before question is registered)
8
8
  const rl = createInterface({ input: process.stdin, terminal: false })
9
9
  const buffered = []
10
10
  let waiter = null
@@ -25,17 +25,17 @@ export async function setupWizard() {
25
25
  })
26
26
  try {
27
27
  const presets = Object.entries(PROVIDER_PRESETS)
28
- console.error("首次使用,先配置一个模型提供商:")
28
+ console.error("First time using ThinCoder — let's configure a model provider:")
29
29
  presets.forEach(([n, p], i) => console.error(` ${i + 1}. ${n.padEnd(10)} ${p.desc}`))
30
- console.error(` ${presets.length + 1}. 自定义端点`)
31
- const choice = Number((await ask(`选择 [1-${presets.length + 1}]: `)).trim())
30
+ console.error(` ${presets.length + 1}. Custom endpoint`)
31
+ const choice = Number((await ask(`Pick [1-${presets.length + 1}]: `)).trim())
32
32
  let name, baseURL, model
33
33
  if (choice === presets.length + 1) {
34
- name = (await ask("名称(如 my-openai): ")).trim()
35
- baseURL = (await ask("baseURL(如 https://api.openai.com/v1): ")).trim().replace(/\/+$/, "")
36
- model = (await ask("模型(如 gpt-4o): ")).trim()
34
+ name = (await ask("Name (e.g. my-openai): ")).trim()
35
+ baseURL = (await ask("baseURL (e.g. https://api.openai.com/v1): ")).trim().replace(/\/+$/, "")
36
+ model = (await ask("Model (e.g. gpt-4o): ")).trim()
37
37
  if (!name || !/^https?:\/\//.test(baseURL) || !model) {
38
- console.error("输入不完整或 baseURL 不合法,已取消")
38
+ console.error("Incomplete input or invalid baseURL — cancelled")
39
39
  return null
40
40
  }
41
41
  } else if (choice >= 1 && choice <= presets.length) {
@@ -43,15 +43,15 @@ export async function setupWizard() {
43
43
  baseURL = presets[choice - 1][1].baseURL
44
44
  model = presets[choice - 1][1].model
45
45
  } else {
46
- console.error("无效选择,已取消")
46
+ console.error("Invalid choice — cancelled")
47
47
  return null
48
48
  }
49
- const apiKey = (await ask(`${name} 的 API key: `)).trim()
49
+ const apiKey = (await ask(`API key for ${name}: `)).trim()
50
50
  if (!apiKey) {
51
- console.error("key 不能为空,已取消")
51
+ console.error("API key cannot be empty — cancelled")
52
52
  return null
53
53
  }
54
- const embedKey = (await ask("可选:embedding API keySiliconFlow,向量检索用;回车跳过): ")).trim()
54
+ const embedKey = (await ask("Optional: embedding API key (SiliconFlow, for vector search; press Enter to skip): ")).trim()
55
55
  const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
56
56
  const providers = raw.providers?.length ? raw.providers : []
57
57
  const existing = providers.find((p) => p.name === name)
@@ -61,8 +61,8 @@ export async function setupWizard() {
61
61
  raw.activeProvider = name
62
62
  if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
63
63
  saveConfig(raw)
64
- console.error(`配置完成:${name} / ${model}(已写入 ${configPath})`)
65
- console.error(embedKey ? "向量检索已启用\n" : "(未配 embedding key:记忆为纯文本检索,之后在 config.json embedding.apiKey 补上即可开启向量检索)\n")
64
+ console.error(`Configured: ${name} / ${model} (saved to ${configPath})`)
65
+ console.error(embedKey ? "Vector search enabled\n" : "(No embedding key configured: memory search will use text-only FTS. Add embedding.apiKey to config.json to enable vector search later.)\n")
66
66
  return { name, baseURL, model, apiKey }
67
67
  } finally {
68
68
  rl.close()
package/src/config.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * config.mjs — 配置加载与保存
3
- * provider 结构:providers[] + activeProvider
4
- * 配置文件:~/.thincoder/config.json
5
- * API key 可用环境变量兜底(未在 providers 中配置时)。
2
+ * config.mjs — configuration loading and saving
3
+ * Multi-provider structure: providers[] + activeProvider
4
+ * Config file: ~/.thincoder/config.json
5
+ * API key can fall back to environment variables (when not configured in providers).
6
6
  */
7
7
 
8
8
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
@@ -12,7 +12,7 @@ import { join } from "node:path"
12
12
  export const configDir = join(homedir(), ".thincoder")
13
13
  export const configPath = join(configDir, "config.json")
14
14
 
15
- /** 内置提供商预设:/provider add <预设名>、首次启动向导共用 */
15
+ /** Built-in provider presets: shared by /provider add <preset> and first-run wizard */
16
16
  export const PROVIDER_PRESETS = {
17
17
  deepseek: { baseURL: "https://api.deepseek.com/v1", model: "deepseek-v4-pro", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 393216, desc: "DeepSeek" },
18
18
  kimi: { baseURL: "https://api.moonshot.cn/v1", model: "kimi-k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
@@ -21,15 +21,17 @@ export const PROVIDER_PRESETS = {
21
21
  minimax: { baseURL: "https://api.minimax.chat/v1", chatPath: "/text/chatcompletion_v2", model: "MiniMax-M3", maxTokens: 131072, desc: "MiniMax" },
22
22
  }
23
23
 
24
- // 默认 provider deepseek 预设保持一致(去掉 desc 展示字段)
25
- const { desc: _presetDesc, ...deepseekPreset } = PROVIDER_PRESETS.deepseek
24
+ // Default provider matches deepseek preset (strip the desc display field)
25
+ const { desc: _, ...deepseekPreset } = PROVIDER_PRESETS.deepseek
26
26
 
27
27
  const DEFAULTS = {
28
28
  providers: [{ name: "deepseek", ...deepseekPreset }],
29
29
  activeProvider: "deepseek",
30
30
  agent: {
31
31
  maxTurns: 100,
32
+ subagentTurns: 100,
32
33
  compactThreshold: 100000,
34
+ verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
33
35
  },
34
36
  memory: {
35
37
  dbPath: join(configDir, "memory.db"),
@@ -46,60 +48,60 @@ const DEFAULTS = {
46
48
  }
47
49
 
48
50
  /**
49
- * 已知模型的能力规格表(前缀匹配,长的在前)。
50
- * 用于压缩阈值推导、截断续写协议选择、能力感知优化。
51
+ * Known model capability spec table (prefix match, longer first).
52
+ * Used for compaction threshold derivation, continuation protocol selection, and capability-aware optimization.
51
53
  *
52
- * context: 上下文窗口(tokens
53
- * maxOutput: 最大输出 tokens(默认 context
54
- * thinking: 是否支持思考/推理模式
55
- * partialMode: Kimi/Qwen Partial Mode 截断续写(assistant 消息带 partial:true
56
- * prefixMode: DeepSeek Prefix Completion 截断续写(走 /beta 端点,带 prefix:true
57
- * multimodal: 是否多模态(支持图片/视觉输入)
58
- * cacheMode: 上下文缓存方式:"auto"=自动/"prompt"=需显式/"none"=不支持
59
- * thinkApi: 思考模式 API 类型:"type"=thinking.type 字段 / "effort"=reasoning_effort 字段
60
- * reasoningEcho: reasoning_content 跨轮回传策略:"required"=必须回传(缺失报错)/"optional"=回传可选(默认不回传)
61
- * reasoningEffortEnum: reasoning_effort 合法枚举值(未声明则不校验,原样透传)
62
- * tempRange: temperature 合法范围 [min, max](未声明则不裁剪)
54
+ * context: context window (tokens)
55
+ * maxOutput: max output tokens (defaults to context)
56
+ * thinking: whether thinking/reasoning mode is supported
57
+ * partialMode: Kimi/Qwen Partial Mode truncation continuation (assistant message with partial:true)
58
+ * prefixMode: DeepSeek Prefix Completion truncation continuation (uses /beta endpoint, with prefix:true)
59
+ * multimodal: whether multimodal (image/vision input supported)
60
+ * cacheMode: context caching mode: "auto"=automatic / "prompt"=needs explicit / "none"=unsupported
61
+ * thinkApi: thinking API type: "type"=thinking.type field / "effort"=reasoning_effort field
62
+ * reasoningEcho: reasoning_content cross-turn echo strategy: "required"=must echo (error if missing) / "optional"=echo optional (default: don't echo)
63
+ * reasoningEffortEnum: valid reasoning_effort enum values (if undeclared, no validation — passed through as-is)
64
+ * tempRange: valid temperature range [min, max] (if undeclared, no clamping)
63
65
  */
64
66
  const MODEL_SPECS = [
65
- // DeepSeek V4 系列
67
+ // DeepSeek V4 series
66
68
  ["deepseek-v4-pro", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
67
69
  ["deepseek-v4-flash", { context: 256_000, maxOutput: 384_000, thinking: false, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
68
70
  ["deepseek-reasoner", { context: 256_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
69
71
  ["deepseek-chat", { context: 256_000, maxOutput: 384_000, thinking: false, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
70
- // Kimi 系列
72
+ // Kimi series
71
73
  ["kimi-k3", { context: 1_000_000, maxOutput: 128_000, thinking: true, partialMode: true, multimodal: true, cacheMode: "prompt", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
72
74
  ["kimi-k2", { context: 256_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none" }],
73
75
  ["moonshot", { context: 128_000, maxOutput: 32_000, thinking: false, cacheMode: "none" }],
74
- // GLM 系列
76
+ // GLM series
75
77
  ["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1] }],
76
78
  ["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1] }],
77
79
  ["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1] }],
78
- // GPT 系列
80
+ // GPT series
79
81
  ["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false, cacheMode: "prompt" }],
80
82
  ["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
81
- // Qwen 系列
83
+ // Qwen series
82
84
  ["qwen3.8-max-preview", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
83
85
  ["qwen3.7-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
84
86
  ["qwen3.8-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
85
87
  ["qwen-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
86
88
  ["qwen-plus", { context: 1_000_000, maxOutput: 32_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
87
89
  ["qwen", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
88
- // MiniMax 系列
90
+ // MiniMax series
89
91
  ["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
90
92
  ["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
91
93
  ["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, cacheMode: "auto" }],
92
94
  ]
93
95
  const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000, cacheMode: "none" }
94
- // 窗口利用率上限:0.8DeepSeek 内部即全窗口;压缩本身要花一次 LLM 调用,过早压缩是纯浪费。
95
- // 20% 余量给压缩后的尾部增长与输出 token)
96
- // 1M 窗口模型按 0.8 = 80 token,历史涨到那么大才压缩会打爆 TPM 预算、
97
- // 压缩请求本身也可能 429。加 cap:不超过 maxOutput 8 倍(128K×8≈100万→实际仍偏大但合理),
98
- // 不超过 30 万(大窗口模型的合理工作上限,再大缓存命中率下降)
96
+ // Window utilization cap: 0.8 (DeepSeek internally uses full window; compaction itself costs an LLM call, premature compaction is pure waste.
97
+ // Reserve 20% headroom for post-compaction tail growth and output tokens)
98
+ // But for 1M-window models, 0.8 = 800K tokens waiting until history grows that large would blow the TPM budget,
99
+ // and the compaction request itself might 429. Add caps: no more than 8× maxOutput (128K×8≈1M → still large but reasonable),
100
+ // no more than 300K (reasonable working ceiling for large-window models; beyond that cache hit rates drop)
99
101
  const COMPACT_RATIO = 0.8
100
102
  const COMPACT_CAP_TOKENS = 300_000
101
103
 
102
- /** 按模型名前缀查规格(大小写不敏感),未知模型给保守默认 */
104
+ /** Look up spec by model name prefix (case-insensitive), conservative default for unknown models */
103
105
  export function specForModel(model) {
104
106
  const m = (model ?? "").toLowerCase()
105
107
  for (const [prefix, spec] of [...MODEL_SPECS].sort((a,b) => b[0].length - a[0].length)) {
@@ -108,24 +110,25 @@ export function specForModel(model) {
108
110
  return DEFAULT_SPEC
109
111
  }
110
112
 
113
+ /** Return the context window size for a given model name */
111
114
  export function contextWindowForModel(model) {
112
115
  return specForModel(model).context
113
116
  }
114
117
 
115
- /** 推导压缩阈值;explicit 为配置文件中显式设置的值(优先),否则按模型自动算 */
118
+ /** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority), otherwise auto-computed from model */
116
119
  export function resolveCompactThreshold(explicit, model) {
117
120
  if (explicit != null) return { value: explicit, auto: false }
118
121
  const spec = specForModel(model)
119
122
  const ratioBased = Math.floor(spec.context * COMPACT_RATIO)
120
- // 大窗口模型(1M)按比例算出来太大,用 cap 限制——宁可早压缩也别让历史涨到打爆 TPM
123
+ // Large-window models (1M) produce too-large ratio-based values; cap them — better to compact early than let history grow until it blows the TPM budget
121
124
  const value = Math.min(ratioBased, COMPACT_CAP_TOKENS)
122
125
  return { value, auto: true }
123
126
  }
124
127
 
125
128
  /**
126
- * providers[] 中按 name 查找。
127
- * name 非空但找不到时抛错——activeProvider 打错字静默落到第一个 provider,会拿错 key 打错端点。
128
- * name 为空时返回第一个。
129
+ * Find provider by name in providers[].
130
+ * Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
131
+ * Returns the first provider when name is empty.
129
132
  */
130
133
  export function findProvider(providers, name) {
131
134
  if (name) {
@@ -138,9 +141,9 @@ export function findProvider(providers, name) {
138
141
  }
139
142
 
140
143
  /**
141
- * 加载配置。
142
- * 环境变量优先级:THINCODER_ACTIVE_PROVIDER > 配置文件 activeProvider
143
- * THINCODER_API_KEY / THINCODER_BASE_URL / THINCODER_MODEL 覆盖当前激活 provider 的对应字段
144
+ * Load configuration.
145
+ * Env var priority: THINCODER_ACTIVE_PROVIDER > config file activeProvider
146
+ * THINCODER_API_KEY / THINCODER_BASE_URL / THINCODER_MODEL override the current active provider's corresponding fields
144
147
  */
145
148
  export function loadConfig() {
146
149
  let config = {}
@@ -162,29 +165,29 @@ export function loadConfig() {
162
165
  embedding: { ...DEFAULTS.embedding, ...config.embedding },
163
166
  }
164
167
 
165
- // baseURL 尾斜杠归一化(防拼出 //chat/completions
168
+ // Normalize baseURL trailing slash (prevents //chat/completions)
166
169
  for (const p of merged.providers) {
167
170
  if (p.baseURL) p.baseURL = p.baseURL.replace(/\/+$/, "")
168
171
  }
169
172
 
170
- // 环境变量覆盖 activeProvider
173
+ // Env var overrides activeProvider
171
174
  if (process.env.THINCODER_ACTIVE_PROVIDER) {
172
175
  merged.activeProvider = process.env.THINCODER_ACTIVE_PROVIDER
173
176
  }
174
177
 
175
- // 获取当前激活的 provider
178
+ // Get the currently active provider
176
179
  const active = findProvider(merged.providers, merged.activeProvider)
177
180
 
178
- // 构建运行时 provider 对象(供 agent.provider 使用)
181
+ // Build runtime provider object (for agent.provider usage)
179
182
  const runtimeProvider = { ...active }
180
183
 
181
- // 环境变量覆盖当前激活 provider 的字段
184
+ // Env vars override current active provider's fields
182
185
  if (process.env.THINCODER_API_KEY) runtimeProvider.apiKey = process.env.THINCODER_API_KEY
183
186
  if (process.env.THINCODER_BASE_URL) runtimeProvider.baseURL = process.env.THINCODER_BASE_URL
184
187
  if (process.env.THINCODER_MODEL) runtimeProvider.model = process.env.THINCODER_MODEL
185
188
 
186
- // apiKey 还可用环境变量兜底(当 providers 里没配 key 时)
187
- // 提供商专用的环境变量只对同名 provider 生效,避免 key 串到错误的端点
189
+ // apiKey also falls back to env vars (when providers doesn't include a key)
190
+ // Provider-specific env vars only apply to the matching provider name, preventing keys from leaking to wrong endpoints
188
191
  if (!runtimeProvider.apiKey?.trim()) {
189
192
  const envMap = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }
190
193
  const keyVar = envMap[merged.activeProvider]
@@ -196,13 +199,13 @@ export function loadConfig() {
196
199
  merged.embedding.apiKey = process.env.SILICONFLOW_API_KEY || process.env.THINCODER_EMBEDDING_API_KEY
197
200
  }
198
201
 
199
- // 压缩阈值跟模型走
202
+ // Compaction threshold follows the model
200
203
  const explicitThreshold = config.agent?.compactThreshold
201
204
  const { value, auto } = resolveCompactThreshold(explicitThreshold, runtimeProvider.model)
202
205
  merged.agent.compactThreshold = value
203
206
  merged.agent.compactThresholdAuto = auto
204
207
 
205
- // 回写到 merged 方便上层使用
208
+ // Write back to merged for convenient access by upper layers
206
209
  merged.provider = runtimeProvider
207
210
  merged.providersList = merged.providers
208
211
 
@@ -210,12 +213,12 @@ export function loadConfig() {
210
213
  }
211
214
 
212
215
  /**
213
- * 保存配置。保留 providers 列表结构和 activeProvider 指针。
214
- * providers[i].apiKey 仅在显式传入时才写入(不覆盖环境变量兜底的 key)
216
+ * Save configuration. Preserves providers list structure and activeProvider pointer.
217
+ * providers[i].apiKey is only written when explicitly passed in (does not overwrite env-var-fallback keys).
215
218
  */
216
219
  export function saveConfig(config) {
217
220
  mkdirSync(configDir, { recursive: true })
218
- // 0600config.json API key,不能世界可读(POSIX;Windows chmod 尽力而为)
221
+ // 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
219
222
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
220
- try { chmodSync(configPath, 0o600) } catch { /* Windows 上可能失败,忽略 */ }
223
+ try { chmodSync(configPath, 0o600) } catch { /* may fail on Windows, ignore */ }
221
224
  }