thincoder 0.7.8 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +32 -13
  2. package/bin/thincoder.js +4 -0
  3. package/bin/thincoder.mjs +27 -346
  4. package/package.json +2 -2
  5. package/src/agent/dispatch.mjs +98 -0
  6. package/src/agent/helpers.mjs +185 -0
  7. package/src/agent/setup.mjs +117 -0
  8. package/src/agent-tools/goal.mjs +71 -0
  9. package/src/agent-tools/plan.mjs +31 -0
  10. package/src/agent-tools/recent-changes.mjs +23 -0
  11. package/src/agent-tools/skill.mjs +46 -0
  12. package/src/agent-tools/subagent.mjs +113 -0
  13. package/src/agent-tools/task.mjs +67 -0
  14. package/src/agent-tools/verify.mjs +198 -0
  15. package/src/agent-tools.mjs +12 -0
  16. package/src/agent.mjs +90 -1040
  17. package/src/cli/distill-command.mjs +85 -0
  18. package/src/cli/make-agent.mjs +85 -0
  19. package/src/cli/memory-command.mjs +63 -0
  20. package/src/cli/permission.mjs +41 -0
  21. package/src/cli/setup-wizard.mjs +70 -0
  22. package/src/config.mjs +5 -8
  23. package/src/context.mjs +10 -13
  24. package/src/distill.mjs +4 -3
  25. package/src/embedding.mjs +4 -2
  26. package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
  27. package/src/mcp/helpers.mjs +37 -0
  28. package/src/mcp/transport-http.mjs +176 -0
  29. package/src/mcp/transport-stdio.mjs +84 -0
  30. package/src/mcp/transport-ws.mjs +87 -0
  31. package/src/mcp.mjs +4 -428
  32. package/src/memory/code-index.mjs +211 -0
  33. package/src/memory/code-sync.mjs +306 -0
  34. package/src/memory/core.mjs +277 -0
  35. package/src/memory/docs.mjs +262 -0
  36. package/src/memory/schema.mjs +426 -0
  37. package/src/memory.mjs +12 -1403
  38. package/src/provider/core.mjs +239 -0
  39. package/src/provider/index.mjs +6 -0
  40. package/src/provider/rate.mjs +104 -0
  41. package/src/session.mjs +18 -5
  42. package/src/tools/bash.mjs +144 -0
  43. package/src/tools/file.mjs +205 -0
  44. package/src/tools/git.mjs +166 -0
  45. package/src/tools/glob.mjs +51 -0
  46. package/src/tools/grep.mjs +100 -0
  47. package/src/tools/index.mjs +22 -0
  48. package/src/tools/ls.mjs +36 -0
  49. package/src/tools/patch.mjs +226 -0
  50. package/src/tools/repomap-parse.mjs +168 -0
  51. package/src/tools/shared.mjs +257 -0
  52. package/src/tools/system.mjs +336 -0
  53. package/src/tools/web.mjs +121 -0
  54. package/src/tools.mjs +2 -1194
  55. package/src/tui/agent-turn.mjs +254 -0
  56. package/src/tui/ansi.mjs +32 -0
  57. package/src/tui/clipboard.mjs +48 -0
  58. package/src/tui/cmd-auto.mjs +21 -0
  59. package/src/tui/cmd-clear.mjs +26 -0
  60. package/src/tui/cmd-config.mjs +72 -0
  61. package/src/tui/cmd-exit.mjs +5 -0
  62. package/src/tui/cmd-extract.mjs +5 -0
  63. package/src/tui/cmd-goal.mjs +47 -0
  64. package/src/tui/cmd-help.mjs +25 -0
  65. package/src/tui/cmd-init.mjs +91 -0
  66. package/src/tui/cmd-mcp.mjs +146 -0
  67. package/src/tui/cmd-model.mjs +7 -0
  68. package/src/tui/cmd-new.mjs +18 -0
  69. package/src/tui/cmd-plan.mjs +21 -0
  70. package/src/tui/cmd-reindex.mjs +44 -0
  71. package/src/tui/cmd-restore.mjs +39 -0
  72. package/src/tui/cmd-session.mjs +42 -0
  73. package/src/tui/cmd-skills.mjs +17 -0
  74. package/src/tui/cmd-think.mjs +56 -0
  75. package/src/tui/config-helpers.mjs +34 -0
  76. package/src/tui/distill-cmd.mjs +45 -0
  77. package/src/tui/index.mjs +330 -0
  78. package/src/tui/interaction.mjs +79 -0
  79. package/src/tui/key-handler.mjs +267 -0
  80. package/src/tui/layout.mjs +115 -0
  81. package/src/tui/pickers.mjs +279 -0
  82. package/src/tui/render-frame.mjs +304 -0
  83. package/src/tui/render.mjs +205 -0
  84. package/src/tui/slash-commands.mjs +138 -0
  85. package/src/tui/startup.mjs +113 -0
  86. package/src/tui/wizard.mjs +168 -0
  87. package/src/tui-render.mjs +4 -0
  88. package/src/tui.mjs +3 -2566
  89. package/src/provider.mjs +0 -383
  90. /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
  91. /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
  92. /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
  93. /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
  94. /package/src/{main-overlay.md → prompts/main.md} +0 -0
  95. /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
  96. /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
  97. /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
@@ -0,0 +1,85 @@
1
+ import { join } from "node:path"
2
+ import { loadConfig, configPath } from "../config.mjs"
3
+ import { createMemory } from "../memory.mjs"
4
+ import { teamConfig, gitAuthor } from "./make-agent.mjs"
5
+ import { setupWizard } from "./setup-wizard.mjs"
6
+ import { askPermission } from "./permission.mjs"
7
+
8
+ /** 缺 key 时的统一提示 */
9
+ function noKeyMessage() {
10
+ return `还没有配置 API key。运行 thincoder 进入 TUI,用 /provider add 和 /provider key 配置;或直接编辑 ${configPath}`
11
+ }
12
+
13
+ /** thincoder distill <transcript-file> [--yes] [--scope=...]
14
+ * 返回退出码:0=成功,1=错误 */
15
+ export async function distillCommand(args, exitSoon) {
16
+ const flags = {}
17
+ const positional = []
18
+ for (const a of args) {
19
+ const m = a.match(/^--([\w-]+)(?:=(.*))?$/)
20
+ if (m) flags[m[1]] = m[2] ?? true
21
+ else positional.push(a)
22
+ }
23
+ const file = positional[0]
24
+ if (!file) {
25
+ console.error("Usage: thincoder distill <transcript-file> [--yes] [--scope=personal|project|team]")
26
+ return 1
27
+ }
28
+ const { readFile } = await import("node:fs/promises")
29
+ const transcript = await readFile(file, "utf8")
30
+
31
+ const config = loadConfig()
32
+ let provider = config.provider
33
+ if (!provider.apiKey) {
34
+ if (!process.stdin.isTTY) {
35
+ console.error(noKeyMessage())
36
+ return 1
37
+ }
38
+ provider = await setupWizard()
39
+ if (!provider) {
40
+ return 1
41
+ }
42
+ }
43
+ const memory = createMemory({ dbPath: config.memory.dbPath })
44
+ const team = teamConfig(config)
45
+ const { extractCandidates, saveCandidate } = await import("../distill.mjs")
46
+
47
+ console.error("[distill] extracting candidates...")
48
+ let candidates
49
+ try {
50
+ candidates = await extractCandidates(provider, transcript)
51
+ } catch (error) {
52
+ console.error(`[distill] ${error.message}`)
53
+ return 1
54
+ }
55
+ if (candidates.length === 0) {
56
+ console.log("No distillable knowledge found in this session.")
57
+ return 0
58
+ }
59
+
60
+ const opts = {
61
+ projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
62
+ team,
63
+ author: gitAuthor(),
64
+ }
65
+ let saved = 0
66
+ for (const c of candidates) {
67
+ if (flags.scope) c.scope = flags.scope
68
+ console.log(`\n--- candidate ---`)
69
+ console.log(`[${c.type}] ${c.title} (scope: ${c.scope})`)
70
+ console.log(c.content)
71
+ if (c.type === "rule") {
72
+ console.log("(rule 类知识通常建议手动撰写;确认提取吗?)")
73
+ }
74
+ const accept = flags.yes ? true : await askPermission("distill-save", { title: c.title })
75
+ if (!accept) {
76
+ console.log("skipped")
77
+ continue
78
+ }
79
+ const where = await saveCandidate(memory, c, opts)
80
+ console.log(`saved -> ${where}`)
81
+ saved++
82
+ }
83
+ console.log(`\nDistilled ${saved}/${candidates.length} entries.`)
84
+ return 0
85
+ }
@@ -0,0 +1,85 @@
1
+ import { execSync } from "node:child_process"
2
+ import { join } from "node:path"
3
+ import { createAgent } from "../agent.mjs"
4
+ import { loadConfig, configDir } from "../config.mjs"
5
+ import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } from "../memory.mjs"
6
+ import { repoOutlineTool } from "../tools/repomap.mjs"
7
+ import { builtinTools } from "../tools/index.mjs"
8
+
9
+ /** 组装一个带记忆的 agent(同步各层索引后返回) */
10
+ export async function makeAgent() {
11
+ const config = loadConfig()
12
+ const provider = config.provider
13
+ const providers = config.providersList
14
+ const memory = createMemory({ dbPath: config.memory.dbPath })
15
+ // 向量检索:配了 embedding 就启用(惰性生成向量,首次搜索时补算)
16
+ if (config.embedding?.apiKey) {
17
+ const { createEmbedder } = await import("../embedding.mjs")
18
+ memory.embedder = createEmbedder(config.embedding)
19
+ }
20
+ const cwd = process.cwd()
21
+ // code/doc 索引按 origin(项目根目录)隔离:检索只查本项目
22
+ memory.codeOrigin = cwd
23
+ // Project 层:启动时同步 .thincoder/memory/ 目录到索引(有就同步,没有就跳过)
24
+ if (config.memory.projectDir) {
25
+ memory.projectOrigin = join(cwd, config.memory.projectDir)
26
+ await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
27
+ }
28
+ // Team 层(可选):首次自动 clone;启动只索引本地目录,拉取远端走显式 thincoder sync
29
+ const team = teamConfig(config)
30
+ if (team) {
31
+ const { ensureClone } = await import("../git/gitmem.mjs")
32
+ await ensureClone(team)
33
+ await syncDir(memory, { layer: "team", dir: team.dir })
34
+ }
35
+ const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd)]
36
+
37
+ // MCP servers:并行连接(一个死 server 不会拖住启动),失败的收集警告(TUI 下 stderr 不可见,通过 agent 对象传递)
38
+ const mcpServers = config.mcp?.servers ?? []
39
+ let mcpTools = []
40
+ const mcpWarnings = []
41
+ if (mcpServers.length) {
42
+ const { connectMcpServer } = await import("../mcp.mjs")
43
+ const results = await Promise.allSettled(mcpServers.map((srv) => connectMcpServer(srv)))
44
+ for (let i = 0; i < results.length; i++) {
45
+ const r = results[i]
46
+ if (r.status === "fulfilled") {
47
+ mcpTools = mcpTools.concat(r.value)
48
+ } else {
49
+ const srv = mcpServers[i]
50
+ const msg = `MCP server "${srv.name ?? srv.command}" failed to connect: ${r.reason?.message ?? r.reason}`
51
+ console.error(`[mcp] ${msg}`)
52
+ mcpWarnings.push(msg)
53
+ }
54
+ }
55
+ }
56
+
57
+ const agent = createAgent({
58
+ provider,
59
+ tools: [...baseTools, ...mcpTools],
60
+ config,
61
+ cwd,
62
+ memory,
63
+ })
64
+ agent.providers = providers
65
+ agent.activeProvider = config.activeProvider
66
+ agent._mcpWarnings = mcpWarnings
67
+ return agent
68
+ }
69
+
70
+ /** 读取 team 配置并补全默认目录;未配置返回 null */
71
+ export function teamConfig(config) {
72
+ const team = config.memory?.team
73
+ if (!team?.repo) return null
74
+ const name = team.name ?? "default"
75
+ return { name, repo: team.repo, dir: team.dir ?? join(configDir, "teams", name) }
76
+ }
77
+
78
+ /** 条目作者:git config user.name 兜底 unknown */
79
+ export function gitAuthor() {
80
+ try {
81
+ return execSync("git config user.name", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "unknown"
82
+ } catch {
83
+ return "unknown"
84
+ }
85
+ }
@@ -0,0 +1,63 @@
1
+ import { put, remove, search, list } from "../memory.mjs"
2
+
3
+ /** thincoder memory <list|search|put|remove> 子命令 */
4
+ export async function memoryCommand(memory, args) {
5
+ const [sub, ...rest] = args
6
+
7
+ const flags = {}
8
+ const positional = []
9
+ for (const a of rest) {
10
+ const m = a.match(/^--([\w-]+)=(.*)$/)
11
+ if (m) flags[m[1]] = m[2]
12
+ else positional.push(a)
13
+ }
14
+
15
+ switch (sub) {
16
+ case "list": {
17
+ const entries = await list(memory, { type: flags.type })
18
+ printEntries(entries)
19
+ break
20
+ }
21
+ case "search": {
22
+ const query = positional.join(" ")
23
+ if (!query) {
24
+ console.error("Usage: thincoder memory search <query>")
25
+ return 1
26
+ }
27
+ printEntries(await search(memory, query, { limit: 10 }))
28
+ break
29
+ }
30
+ case "put": {
31
+ if (!flags.type || !flags.title || !flags.content) {
32
+ console.error("Usage: thincoder memory put --type=<rule|knowledge|decision|pattern> --title=<t> --content=<c> [--tags=<t>]")
33
+ return 1
34
+ }
35
+ const id = await put(memory, { type: flags.type, title: flags.title, content: flags.content, tags: flags.tags ?? "" })
36
+ console.log(`Saved (id=${id})`)
37
+ break
38
+ }
39
+ case "remove": {
40
+ const id = Number(positional[0])
41
+ if (!id) {
42
+ console.error("Usage: thincoder memory remove <id>")
43
+ return 1
44
+ }
45
+ console.log((await remove(memory, id)) ? `Removed #${id}` : `No entry #${id}`)
46
+ break
47
+ }
48
+ default:
49
+ console.error("Usage: thincoder memory <list|search|put|remove>")
50
+ return 1
51
+ }
52
+ }
53
+
54
+ function printEntries(entries) {
55
+ if (entries.length === 0) {
56
+ console.log("(no entries)")
57
+ return
58
+ }
59
+ for (const e of entries) {
60
+ console.log(`#${e.id} [${e.type}] ${e.title}${e.tags ? ` (${e.tags})` : ""}`)
61
+ console.log(` ${e.content.split("\n")[0].slice(0, 100)}`)
62
+ }
63
+ }
@@ -0,0 +1,41 @@
1
+ import { createInterface } from "node:readline"
2
+
3
+ /** CLI 版工具参数摘要(截断长 JSON) */
4
+ export function summarize(toolArgs) {
5
+ const s = JSON.stringify(toolArgs)
6
+ return s.length > 120 ? s.slice(0, 120) + "..." : s
7
+ }
8
+
9
+ /** 权限请求的关键信息(按工具定制),与 TUI 的 formatPermission 对齐。name 可能带子 agent 前缀("coder/bash"),取基名匹配 */
10
+ export function formatPermission(name, args) {
11
+ const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
12
+ const base = name.includes("/") ? name.split("/").pop() : name
13
+ if (base === "bash") return cap(args.command ?? "")
14
+ if (base === "write") return `${args.path}(写入 ${(args.content ?? "").length} 字符)\n${cap(args.content ?? "", 1000)}`
15
+ if (base === "edit") {
16
+ const oldLines = cap(args.old_string ?? "", 500).split("\n").map((l) => `- ${l}`).join("\n")
17
+ const newLines = cap(args.new_string ?? "", 500).split("\n").map((l) => `+ ${l}`).join("\n")
18
+ return `${args.path}\n${oldLines}\n ↓\n${newLines}`
19
+ }
20
+ if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
21
+ if (base === "subagent") return cap(args.task ?? "", 500)
22
+ if (base === "memory_put") return `[${args.type ?? ""}] ${args.title ?? ""}\n${cap(args.content ?? "", 500)}`
23
+ return cap(summarize(args), 300)
24
+ }
25
+
26
+ /** 权限确认:TTY 下交互询问 y/n;非交互环境默认拒绝(安全优先) */
27
+ export async function askPermission(name, toolArgs) {
28
+ if (!process.stdin.isTTY) {
29
+ console.error(`\n[deny] ${name} (non-interactive, side-effect tools require a TTY)`)
30
+ return false
31
+ }
32
+ const rl = createInterface({ input: process.stdin, output: process.stderr })
33
+ try {
34
+ const answer = await new Promise((resolve) => {
35
+ rl.question(`\n[allow?] ${name}\n${formatPermission(name, toolArgs)}\n(y/N) `, resolve)
36
+ })
37
+ return answer.trim().toLowerCase() === "y"
38
+ } finally {
39
+ rl.close()
40
+ }
41
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { createInterface } from "node:readline"
3
+ import { configPath, saveConfig, PROVIDER_PRESETS } from "../config.mjs"
4
+
5
+ /** 首次使用(TTY 下的 chat/distill):问答式配置一个 provider 并落盘,返回运行时 provider;取消返回 null */
6
+ export async function setupWizard() {
7
+ // 自带缓冲的提问器:rl.question 在输入被管道/快速粘贴时会丢行(问题注册前 line 已到达)
8
+ const rl = createInterface({ input: process.stdin, terminal: false })
9
+ const buffered = []
10
+ let waiter = null
11
+ rl.on("line", (line) => {
12
+ if (waiter) {
13
+ const w = waiter
14
+ waiter = null
15
+ w(line)
16
+ } else {
17
+ buffered.push(line)
18
+ }
19
+ })
20
+ const ask = (q) =>
21
+ new Promise((resolve) => {
22
+ process.stderr.write(q)
23
+ if (buffered.length) resolve(buffered.shift())
24
+ else waiter = resolve
25
+ })
26
+ try {
27
+ const presets = Object.entries(PROVIDER_PRESETS)
28
+ console.error("首次使用,先配置一个模型提供商:")
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())
32
+ let name, baseURL, model
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()
37
+ if (!name || !/^https?:\/\//.test(baseURL) || !model) {
38
+ console.error("输入不完整或 baseURL 不合法,已取消")
39
+ return null
40
+ }
41
+ } else if (choice >= 1 && choice <= presets.length) {
42
+ name = presets[choice - 1][0]
43
+ baseURL = presets[choice - 1][1].baseURL
44
+ model = presets[choice - 1][1].model
45
+ } else {
46
+ console.error("无效选择,已取消")
47
+ return null
48
+ }
49
+ const apiKey = (await ask(`${name} 的 API key: `)).trim()
50
+ if (!apiKey) {
51
+ console.error("key 不能为空,已取消")
52
+ return null
53
+ }
54
+ const embedKey = (await ask("可选:embedding API key(SiliconFlow,向量检索用;回车跳过): ")).trim()
55
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
56
+ const providers = raw.providers?.length ? raw.providers : []
57
+ const existing = providers.find((p) => p.name === name)
58
+ if (existing) Object.assign(existing, { baseURL, model, apiKey })
59
+ else providers.push({ name, baseURL, model, apiKey })
60
+ raw.providers = providers
61
+ raw.activeProvider = name
62
+ if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
63
+ saveConfig(raw)
64
+ console.error(`配置完成:${name} / ${model}(已写入 ${configPath})`)
65
+ console.error(embedKey ? "向量检索已启用\n" : "(未配 embedding key:记忆为纯文本检索,之后在 config.json 的 embedding.apiKey 补上即可开启向量检索)\n")
66
+ return { name, baseURL, model, apiKey }
67
+ } finally {
68
+ rl.close()
69
+ }
70
+ }
package/src/config.mjs CHANGED
@@ -16,8 +16,8 @@ export const configPath = join(configDir, "config.json")
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" },
19
- glm: { baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 131072, desc: "智谱 GLM" },
20
- qwen: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", maxTokens: 131072, desc: "通义千问" },
19
+ glm: { baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 131072, desc: "Zhipu GLM" },
20
+ qwen: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", maxTokens: 131072, desc: "Qwen / Alibaba" },
21
21
  minimax: { baseURL: "https://api.minimax.chat/v1", chatPath: "/text/chatcompletion_v2", model: "MiniMax-M3", maxTokens: 131072, desc: "MiniMax" },
22
22
  }
23
23
 
@@ -131,8 +131,8 @@ export function findProvider(providers, name) {
131
131
  if (name) {
132
132
  const found = providers.find((p) => p.name === name)
133
133
  if (found) return found
134
- const available = providers.map((p) => p.name).join(", ") || "()"
135
- throw new Error(`activeProvider "${name}" 不在 providers 列表中(可用: ${available}),请检查配置是否打错字: ${configPath}`)
134
+ const available = providers.map((p) => p.name).join(", ") || "(empty)"
135
+ throw new Error(`activeProvider "${name}" not in providers list (available: ${available}); check for a typo in: ${configPath}`)
136
136
  }
137
137
  return providers[0] ?? { name: "default", baseURL: "", model: "" }
138
138
  }
@@ -148,7 +148,7 @@ export function loadConfig() {
148
148
  try {
149
149
  config = JSON.parse(readFileSync(configPath, "utf8"))
150
150
  } catch (error) {
151
- throw new Error(`配置文件不是合法 JSON,请检查或删除: ${configPath}\n ${error.message}`)
151
+ throw new Error(`Config file is not valid JSON, check or delete it: ${configPath}\n ${error.message}`)
152
152
  }
153
153
  }
154
154
 
@@ -190,9 +190,6 @@ export function loadConfig() {
190
190
  const keyVar = envMap[merged.activeProvider]
191
191
  if (keyVar && process.env[keyVar]) runtimeProvider.apiKey = process.env[keyVar]
192
192
  }
193
- if (!runtimeProvider.apiKey) {
194
- runtimeProvider.apiKey = process.env.THINCODER_API_KEY
195
- }
196
193
 
197
194
  // embedding apiKey
198
195
  if (!merged.embedding.apiKey) {
package/src/context.mjs CHANGED
@@ -5,14 +5,8 @@
5
5
  * 压缩策略:保留最早 2 条 + 最近 N 条,中间由 LLM 摘要成一条(学 kimi-code,简化版)。
6
6
  */
7
7
 
8
- import { chat } from "./provider.mjs"
9
-
10
- /** 粗估一段文本的 token 数:ASCII 约 4 字符 1 token,CJK 等非 ASCII 约 1 字符 1 token */
11
- function estimateText(s) {
12
- let nonAscii = 0
13
- for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) nonAscii++
14
- return Math.ceil((s.length - nonAscii) / 4) + nonAscii
15
- }
8
+ import { chat } from "./provider/index.mjs"
9
+ import { estimateText } from "./provider/rate.mjs"
16
10
 
17
11
  /** 粗估一组消息的 token 数(正文 + 思考链 + tool_calls 参数) */
18
12
  export function estimateTokens(messages) {
@@ -208,15 +202,18 @@ const OVERSIZE_CONTENT_LIMIT = 8_000
208
202
  * 不动 reasoning_content(DeepSeek/Kimi 回传协议)与 tool_calls 配对结构,无协议 400 风险。
209
203
  * 只在 compressIfNeeded 判定超阈值后调用。返回是否有消息被截断。
210
204
  */
211
- export function shrinkOversized(agent) {
205
+ export function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
212
206
  let shrunk = false
213
207
  for (const m of agent.history) {
214
208
  if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") continue
215
- if (m.content.length <= OVERSIZE_CONTENT_LIMIT) continue
209
+ if (m.content.length <= limit) continue
210
+ // 截断保留首尾,中间换桩说明;keepHead/keepTail 按比例但不超过 limit 的 50%/25%
211
+ const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
212
+ const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
216
213
  m.content =
217
- m.content.slice(0, 4_000) +
218
- `\n[... ${m.content.length - 6_000} chars truncated — single message too large for context window ...]\n` +
219
- m.content.slice(-2_000)
214
+ m.content.slice(0, keepHead) +
215
+ `\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
216
+ m.content.slice(-keepTail)
220
217
  shrunk = true
221
218
  }
222
219
  if (shrunk) {
package/src/distill.mjs CHANGED
@@ -4,9 +4,9 @@
4
4
  * 绝不做会话结束后的全自动沉淀。
5
5
  */
6
6
 
7
- import { chat } from "./provider.mjs"
7
+ import { chat } from "./provider/index.mjs"
8
8
  import { put, putMarkdown } from "./memory.mjs"
9
- import { commitAndPush } from "./gitmem.mjs"
9
+ import { commitAndPush } from "./git/gitmem.mjs"
10
10
 
11
11
  const DISTILL_PROMPT = `你是知识提取器。阅读下面的 agent 工作会话记录,提取值得跨会话长期记住的知识。
12
12
 
@@ -47,7 +47,8 @@ export async function extractCandidates(provider, transcript) {
47
47
  const res = await chat(provider, {
48
48
  messages: [{ role: "user", content: DISTILL_PROMPT + transcript }],
49
49
  })
50
- const match = res.content.match(/\[[\s\S]*\]/)
50
+ // 非贪婪匹配第一个 JSON 数组(贪婪 [\s\S]* 会跨多个数组把中间文本也吃进去)
51
+ const match = res.content.match(/\[[\s\S]*?\]/)
51
52
  if (!match) return []
52
53
  try {
53
54
  const parsed = JSON.parse(match[0])
package/src/embedding.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  * 向量在入库前归一化,之后点积即余弦相似度。
6
6
  */
7
7
 
8
- import { RETRYABLE_STATUS } from "./provider.mjs"
8
+ import { RETRYABLE_STATUS } from "./provider/index.mjs"
9
9
  const MAX_RETRIES = 3
10
10
  const BATCH_SIZE = 32 // 单次请求的文本数上限(SiliconFlow 限制内)
11
11
 
@@ -48,8 +48,9 @@ export async function embed(embedder, texts, { signal } = {}) {
48
48
 
49
49
  /** 余弦相似度(输入均已归一化,点积即余弦) */
50
50
  export function cosine(a, b) {
51
+ if (a.length !== b.length) return 0
51
52
  let sum = 0
52
- const n = Math.min(a.length, b.length)
53
+ const n = a.length
53
54
  for (let i = 0; i < n; i++) sum += a[i] * b[i]
54
55
  return sum
55
56
  }
@@ -63,6 +64,7 @@ export function toBlob(vec) {
63
64
  export function fromBlob(buf) {
64
65
  // BLOB 可能来自 Buffer 池,byteOffset 不保证 4 对齐,直接建视图会 RangeError——先复制对齐
65
66
  if (buf.byteOffset % 4 !== 0) buf = new Uint8Array(buf)
67
+ if (buf.byteLength % 4 !== 0) return new Float32Array(0)
66
68
  return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4)
67
69
  }
68
70
 
@@ -9,7 +9,7 @@ import { createHash } from "node:crypto"
9
9
  import { existsSync } from "node:fs"
10
10
  import { cp, mkdir, readFile, readdir, rm, writeFile, copyFile } from "node:fs/promises"
11
11
  import { dirname, join, relative } from "node:path"
12
- import { configDir } from "./config.mjs"
12
+ import { configDir } from "../config.mjs"
13
13
 
14
14
  const MAX_CHECKPOINTS = 20
15
15
 
@@ -0,0 +1,37 @@
1
+ /**
2
+ * mcp/helpers.mjs — MCP 共享工具函数与常量
3
+ */
4
+
5
+ export const INIT_TIMEOUT_MS = 30_000
6
+ export const CALL_TIMEOUT_MS = 120_000
7
+ export const ENDPOINT_WAIT_MS = 5_000
8
+
9
+ let nextRpcId = 0
10
+ export function rpcId() {
11
+ return String(++nextRpcId)
12
+ }
13
+
14
+ export function withTimeout(promise, ms) {
15
+ let timer
16
+ const timeout = new Promise((_, reject) => {
17
+ timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
18
+ timer.unref?.()
19
+ })
20
+ return Promise.race([promise.finally(() => clearTimeout(timer)), timeout])
21
+ }
22
+
23
+ export function quoteArg(s) {
24
+ return /[\s"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
25
+ }
26
+
27
+ export function withAuthToken(wsUrl, authorization) {
28
+ if (!authorization) return wsUrl
29
+ const token = authorization.replace(/^Bearer\s+/i, "")
30
+ const u = new URL(wsUrl)
31
+ u.searchParams.set("token", token)
32
+ return u.href
33
+ }
34
+
35
+ export function sanitizeToolName(name) {
36
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
37
+ }