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.
- package/README.md +32 -13
- package/bin/thincoder.js +4 -0
- package/bin/thincoder.mjs +27 -346
- package/package.json +2 -2
- package/src/agent/dispatch.mjs +98 -0
- package/src/agent/helpers.mjs +185 -0
- package/src/agent/setup.mjs +117 -0
- package/src/agent-tools/goal.mjs +71 -0
- package/src/agent-tools/plan.mjs +31 -0
- package/src/agent-tools/recent-changes.mjs +23 -0
- package/src/agent-tools/skill.mjs +46 -0
- package/src/agent-tools/subagent.mjs +113 -0
- package/src/agent-tools/task.mjs +67 -0
- package/src/agent-tools/verify.mjs +198 -0
- package/src/agent-tools.mjs +12 -0
- package/src/agent.mjs +90 -1040
- package/src/cli/distill-command.mjs +85 -0
- package/src/cli/make-agent.mjs +85 -0
- package/src/cli/memory-command.mjs +63 -0
- package/src/cli/permission.mjs +41 -0
- package/src/cli/setup-wizard.mjs +70 -0
- package/src/config.mjs +5 -8
- package/src/context.mjs +10 -13
- package/src/distill.mjs +4 -3
- package/src/embedding.mjs +4 -2
- package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
- package/src/mcp/helpers.mjs +37 -0
- package/src/mcp/transport-http.mjs +176 -0
- package/src/mcp/transport-stdio.mjs +84 -0
- package/src/mcp/transport-ws.mjs +87 -0
- package/src/mcp.mjs +4 -428
- package/src/memory/code-index.mjs +211 -0
- package/src/memory/code-sync.mjs +306 -0
- package/src/memory/core.mjs +277 -0
- package/src/memory/docs.mjs +262 -0
- package/src/memory/schema.mjs +426 -0
- package/src/memory.mjs +12 -1403
- package/src/provider/core.mjs +239 -0
- package/src/provider/index.mjs +6 -0
- package/src/provider/rate.mjs +104 -0
- package/src/session.mjs +18 -5
- package/src/tools/bash.mjs +144 -0
- package/src/tools/file.mjs +205 -0
- package/src/tools/git.mjs +166 -0
- package/src/tools/glob.mjs +51 -0
- package/src/tools/grep.mjs +100 -0
- package/src/tools/index.mjs +22 -0
- package/src/tools/ls.mjs +36 -0
- package/src/tools/patch.mjs +226 -0
- package/src/tools/repomap-parse.mjs +168 -0
- package/src/tools/shared.mjs +257 -0
- package/src/tools/system.mjs +336 -0
- package/src/tools/web.mjs +121 -0
- package/src/tools.mjs +2 -1194
- package/src/tui/agent-turn.mjs +254 -0
- package/src/tui/ansi.mjs +32 -0
- package/src/tui/clipboard.mjs +48 -0
- package/src/tui/cmd-auto.mjs +21 -0
- package/src/tui/cmd-clear.mjs +26 -0
- package/src/tui/cmd-config.mjs +72 -0
- package/src/tui/cmd-exit.mjs +5 -0
- package/src/tui/cmd-extract.mjs +5 -0
- package/src/tui/cmd-goal.mjs +47 -0
- package/src/tui/cmd-help.mjs +25 -0
- package/src/tui/cmd-init.mjs +91 -0
- package/src/tui/cmd-mcp.mjs +146 -0
- package/src/tui/cmd-model.mjs +7 -0
- package/src/tui/cmd-new.mjs +18 -0
- package/src/tui/cmd-plan.mjs +21 -0
- package/src/tui/cmd-reindex.mjs +44 -0
- package/src/tui/cmd-restore.mjs +39 -0
- package/src/tui/cmd-session.mjs +42 -0
- package/src/tui/cmd-skills.mjs +17 -0
- package/src/tui/cmd-think.mjs +56 -0
- package/src/tui/config-helpers.mjs +34 -0
- package/src/tui/distill-cmd.mjs +45 -0
- package/src/tui/index.mjs +330 -0
- package/src/tui/interaction.mjs +79 -0
- package/src/tui/key-handler.mjs +267 -0
- package/src/tui/layout.mjs +115 -0
- package/src/tui/pickers.mjs +279 -0
- package/src/tui/render-frame.mjs +304 -0
- package/src/tui/render.mjs +205 -0
- package/src/tui/slash-commands.mjs +138 -0
- package/src/tui/startup.mjs +113 -0
- package/src/tui/wizard.mjs +168 -0
- package/src/tui-render.mjs +4 -0
- package/src/tui.mjs +3 -2566
- package/src/provider.mjs +0 -383
- /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
- /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
- /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
- /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
- /package/src/{main-overlay.md → prompts/main.md} +0 -0
- /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
- /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
- /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: "
|
|
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}"
|
|
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(
|
|
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 <=
|
|
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,
|
|
218
|
-
`\n[... ${m.content.length -
|
|
219
|
-
m.content.slice(-
|
|
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
|
-
|
|
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 =
|
|
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 "
|
|
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
|
+
}
|