thincoder 0.12.67 → 0.12.69

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 (64) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +21 -5
  3. package/bin/thincoder.mjs +41 -344
  4. package/package.json +2 -2
  5. package/src/acp/bridge.mjs +5 -2
  6. package/src/acp/handlers-session.mjs +12 -1
  7. package/src/cli/make-agent.mjs +44 -119
  8. package/src/cli/memory-command.mjs +45 -3
  9. package/src/command-interactive.mjs +196 -0
  10. package/src/command-table.mjs +185 -0
  11. package/src/completions.mjs +36 -12
  12. package/src/crash-reports.mjs +42 -20
  13. package/src/distill.mjs +3 -3
  14. package/src/heap-watch.mjs +5 -14
  15. package/src/tui/agent-turn.mjs +88 -50
  16. package/src/tui/clipboard.mjs +74 -30
  17. package/src/tui/cmd-advisor.mjs +39 -13
  18. package/src/tui/cmd-clear.mjs +2 -2
  19. package/src/tui/cmd-config.mjs +17 -1
  20. package/src/tui/cmd-eng.mjs +1 -1
  21. package/src/tui/cmd-mcp.mjs +1 -1
  22. package/src/tui/cmd-new.mjs +2 -2
  23. package/src/tui/cmd-reindex.mjs +1 -1
  24. package/src/tui/cmd-session.mjs +13 -5
  25. package/src/tui/cmd-think.mjs +35 -17
  26. package/src/tui/cmd-timers.mjs +33 -0
  27. package/src/tui/conversation-writer.mjs +2 -2
  28. package/src/tui/display-budget.mjs +9 -9
  29. package/src/tui/fold-block.mjs +1 -1
  30. package/src/tui/index.mjs +54 -18
  31. package/src/tui/interaction.mjs +4 -2
  32. package/src/tui/key-handler-ctrlc.mjs +1 -1
  33. package/src/tui/key-handler-search.mjs +1 -1
  34. package/src/tui/key-handler.mjs +1 -1
  35. package/src/tui/key-modes.mjs +1 -1
  36. package/src/tui/layout.mjs +5 -5
  37. package/src/tui/lifecycle-records.mjs +199 -0
  38. package/src/tui/model-picker.mjs +16 -199
  39. package/src/tui/mouse.mjs +3 -3
  40. package/src/tui/pickers.mjs +6 -2
  41. package/src/tui/provider-admin.mjs +213 -0
  42. package/src/tui/queued-merge.mjs +4 -60
  43. package/src/tui/queued-pickup.mjs +3 -3
  44. package/src/tui/render-conversation.mjs +13 -4
  45. package/src/tui/render-frame.mjs +43 -6
  46. package/src/tui/render-loop.mjs +2 -3
  47. package/src/tui/render-segments.mjs +2 -2
  48. package/src/tui/render.mjs +1 -1
  49. package/src/tui/slash-commands.mjs +3 -0
  50. package/src/tui/startup.mjs +28 -3
  51. package/src/tui/subagent-blocks.mjs +27 -19
  52. package/src/tui/subagent-children.mjs +5 -5
  53. package/src/tui/subagent-freeze.mjs +39 -37
  54. package/src/tui/subagent-panel.mjs +7 -7
  55. package/src/tui/suspension-drive.mjs +172 -259
  56. package/src/tui/timer-watch.mjs +64 -0
  57. package/src/tui/tool-args.mjs +10 -71
  58. package/src/tui/tool-display.mjs +6 -6
  59. package/src/tui/tool-events.mjs +33 -28
  60. package/src/tui/tui-lifecycle.mjs +13 -4
  61. package/src/tui/tui-state.mjs +3 -2
  62. package/src/tui/turn-face.mjs +4 -1
  63. package/src/tui/wizard.mjs +7 -2
  64. package/src/tui/wrapped-spawn.mjs +3 -3
@@ -1,15 +1,20 @@
1
- import { execSync } from "node:child_process"
2
- import { isAbsolute, join } from "node:path"
3
- import { createAgent } from "@thincoder/core/agent.mjs"
4
- import { loadConfig, configDir } from "@thincoder/core/config.mjs"
5
- import { createMemory, syncDir } from "@thincoder/core/memory.mjs"
6
- import { discoverRules } from "@thincoder/core/rules.mjs"
7
- // #70(CORE-UNIFICATION TOOLS):注册表与消费侧拼装面归位核内——单源 `assembleBuiltinTools`
8
- // (静态表 ∪ memory / code_search / doc_search / repo_outline / settings / peer_instances 六面 ∪ 门控 read_image)。
9
- // 传 `model` 必需:漏传 ⇒ `read_image` 对所有模型静默消失(门判据 = specForModel(model)?.multimodal)。
10
- import { assembleBuiltinTools } from "@thincoder/core/tools/index.mjs"
1
+ /**
2
+ * make-agent.mjs — CLI 装配面(`assembleAgent` 出档;消费面 = `command-interactive.mjs` ∕ `acp.mjs` ∕
3
+ * `distill-command.mjs` ∕ `memory-command.mjs` ∕ `command-table.mjs`)。
4
+ * **装配序 = 核单源**(`thincoder-core/agent/assemble.mjs` —— B5 装配序上提批):本档 = 端壳 adapter,
5
+ * 留面 = cwd(`process.cwd()`)· MCP 合入 + `applyToolExclusions`(入核 `toolsFinalize` 缝 —— 位次保持:
6
+ * 工具装配后、`createAgent` 前)· `_mcpWarnings` · M1 装配钩子 `attachManifest` · 校验调用;装配序本体
7
+ * 与团队层取值随上提归核,本档四名**同名转口**(`teamConfig` ∕ `gitAuthor` ∕ `validateProvider` ∕
8
+ * `DEFAULT_DEPS`)——调用面 import 路径与名面零改。
9
+ */
10
+ import { join } from "node:path"
11
+ // 装配序核单源(B5 装配序上提批):本档 = 核调用 + MCP 缝 + 后处理。
12
+ import { assembleAgent as coreAssemble, DEFAULT_DEPS, gitAuthor, teamConfig, validateProvider } from "@thincoder/core/agent/assemble.mjs"
11
13
  import { resolveEngineeringManifest, projectView } from "@thincoder/core/manifest.mjs"
12
14
 
15
+ // 四名转口(核单源**恒等绑定**;调用面零改 —— 枚举见档头)。
16
+ export { DEFAULT_DEPS, gitAuthor, teamConfig, validateProvider }
17
+
13
18
  /** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
14
19
  * 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
15
20
  * ACP 通道经 `assembleAgent({ excludeTools })` 传入(acp.mjs `ACP_EXCLUDED_TOOLS`)。 */
@@ -20,49 +25,35 @@ export function applyToolExclusions(tools, excludeTools = []) {
20
25
  return kept.length === tools.length ? tools : kept
21
26
  }
22
27
 
23
- /** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
24
- export async function assembleAgent({ excludeTools = [], slotData } = {}) {
25
- const config = loadConfig()
26
- const provider = config.provider
27
- const providers = config.providersList
28
-
29
- // Inject proxy URI into providers (double opt-in: provider.proxy + config.proxy.model)
30
- const { injectProxy } = await import("@thincoder/core/proxy.mjs")
31
- injectProxy(providers, config)
32
- // config.provider 是 loadConfig 里的独立拷贝,同步注入结果
33
- if (provider?.name) provider.proxyUri = providers.find((p) => p.name === provider.name)?.proxyUri
34
-
35
- const memory = createMemory({ dbPath: config.memory.dbPath })
36
- // Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
37
- if (config.embedding?.apiKey) {
38
- const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
39
- memory.embedder = createEmbedder(config.embedding)
40
- }
28
+ /** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return)
29
+ * = 核装配序(`thincoder-core/agent/assemble.mjs`)+ 本端留面(MCP 合入 ∕ 剔除经 `toolsFinalize` 缝 ·
30
+ * `_mcpWarnings` · M1 装配钩子 · 校验调用)。`deps` 逐项可注入(缺省 = 核缺省)。 */
31
+ export async function assembleAgent({ excludeTools = [], slotData, deps = {} } = {}) {
41
32
  const cwd = process.cwd()
42
- // Merge project-level rules (.thincoder/rules/*.md) into config; file rules take priority (first),
43
- // config.json rules append (deduped by pattern). Users can override with explicit config rules.
44
- const fileRules = discoverRules(cwd)
45
- if (fileRules.length) {
46
- const filePatterns = new Set(fileRules.map(r => r.pattern))
47
- const configRules = (config.agent?.streamRules || []).filter(r => !filePatterns.has(r.pattern))
48
- config.agent.streamRules = [...fileRules, ...configRules]
49
- }
50
- // code/doc indices isolated by origin (project root dir): search only scoped to this project
51
- memory.codeOrigin = cwd
52
- // Project layer: sync .thincoder/memory/ dir to index on startup (sync if present, skip otherwise)
53
- if (config.memory.projectDir) {
54
- memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir)
55
- await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
56
- }
57
- // Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
58
- const team = teamConfig(config)
59
- if (team) {
60
- const { ensureClone } = await import("@thincoder/core/git/gitmem.mjs")
61
- await ensureClone(team)
62
- await syncDir(memory, { layer: "team", dir: team.dir })
63
- }
64
- const baseTools = await assembleBuiltinTools({ memory, cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team, model: provider?.model ?? null })
33
+ /** MCP 连接警告(缝体出参——装配后落 `agent._mcpWarnings`)。 */
34
+ const warnings = []
35
+ const agent = await coreAssemble({
36
+ cwd,
37
+ deps,
38
+ // 位次保持:工具装配后、createAgent 前(旧内联序 :64 → :66-110 → :112 逐位同)。
39
+ toolsFinalize: (baseTools, { config }) => finalizeTools(baseTools, { cwd, config, excludeTools, warnings }),
40
+ })
41
+ agent._mcpWarnings = warnings
42
+ // M1 装配钩子(会话起点①——装配期):判据 = 会话权威值(`slotData` 槽值优先 + config 回退);
43
+ // 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null);非 ok 态不抛(启动零拒绝)。
44
+ // 钩子后移的可观察后果(报明 / 建档晚于 memory sync / MCP 连接 / 工具装配——功能等价,仍在
45
+ // 进入正常循环之前)见 docs/core/design/MANIFEST.md §2.2。
46
+ attachManifest(agent, { cwd, slotData })
47
+ // SESSION.md §6.8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
48
+ // 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
49
+ validateProvider(agent, agent.config)
50
+ return agent
51
+ }
65
52
 
53
+ /** MCP 合入 + 工具剔除(核 `toolsFinalize` 缝体——CLI 专用;位次 = 工具装配后、`createAgent` 前)。
54
+ * 连接并发(死服务器不阻塞启动),失败收集为警告(TUI alt-buffer 下 stderr 不可见——经 `agent`
55
+ * 对象传递);`warnings` = 出参数组(调用侧装配 `agent._mcpWarnings`)。 */
56
+ async function finalizeTools(baseTools, { cwd, config, excludeTools, warnings }) {
66
57
  // MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
67
58
  const mcpServers = config.mcp?.servers ?? []
68
59
  // Read project-level .mcp.json (standard MCP client convention) — merge into mcpServers
@@ -92,7 +83,6 @@ export async function assembleAgent({ excludeTools = [], slotData } = {}) {
92
83
  console.error(`[mcp] Failed to read .mcp.json: ${e.message}`)
93
84
  }
94
85
  let mcpTools = []
95
- const mcpWarnings = []
96
86
  if (mcpServers.length) {
97
87
  const { connectMcpServer } = await import("@thincoder/core/mcp.mjs")
98
88
  const results = await Promise.allSettled(mcpServers.map((srv) => connectMcpServer(srv)))
@@ -104,37 +94,11 @@ export async function assembleAgent({ excludeTools = [], slotData } = {}) {
104
94
  const srv = mcpServers[i]
105
95
  const msg = `MCP server "${srv.name ?? srv.command}" failed to connect: ${r.reason?.message ?? r.reason}`
106
96
  console.error(`[mcp] ${msg}`)
107
- mcpWarnings.push(msg)
97
+ warnings.push(msg)
108
98
  }
109
99
  }
110
100
  }
111
-
112
- const agent = createAgent({
113
- provider,
114
- tools: applyToolExclusions([...baseTools, ...mcpTools], excludeTools),
115
- config,
116
- cwd,
117
- memory,
118
- })
119
- agent.providers = providers
120
- // MODEL-MERGE-SESSION:config 无 active*——会话运行时起点 = defaultModel 复合解析
121
- // (provider 对象带解析后 model——API/spec 消费点零改);无效 → {} + 原因走 D-S1 标记
122
- agent.activeProvider = provider.name ?? ""
123
- agent.activeModel = provider.model ?? null
124
- agent._mcpWarnings = mcpWarnings
125
- // M1 装配钩子(会话起点①——装配期):判据 = 会话权威值(`slotData` 槽值优先 + config 回退);
126
- // 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null);非 ok 态不抛(启动零拒绝)。
127
- // 钩子后移的可观察后果(报明 / 建档晚于 memory sync / MCP 连接 / 工具装配——功能等价,仍在
128
- // 进入正常循环之前)见 docs/core/design/MANIFEST.md §2.2。
129
- attachManifest(agent, { cwd, slotData })
130
- // SESSION.md §6.8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
131
- // 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
132
- validateProvider(agent)
133
- // defaultModel 无效/未设的具体原因覆盖通用判据文案(providers 存在时更有指导性)
134
- if (agent._providerInvalid && config.providerInvalidReason) {
135
- agent._providerInvalidReason = config.providerInvalidReason
136
- }
137
- return agent
101
+ return applyToolExclusions([...baseTools, ...mcpTools], excludeTools)
138
102
  }
139
103
 
140
104
  /**
@@ -174,42 +138,3 @@ export function attachManifest(agent, { cwd = process.cwd(), slotData } = {}) {
174
138
  agent.manifest = r.ok ? r.manifest : null
175
139
  return agent.manifest
176
140
  }
177
-
178
- /**
179
- * SESSION.md §6.8 D-S1 — provider 有效性校验(assembleAgent 后唯一校验点,TUI/chat 两路径同源)。
180
- * 判据(评审 #1/#2):仅 model/baseURL 缺失判 invalid——**不得用 MODEL_SPECS 成员资格判无效**
181
- * (未知模型 = 受支持场景:自定义端点模型不在 spec 表是常态,误判会让自定义模型用户每次恢复都弹重选)。
182
- * apiKey 缺失不判(既有 wizard /model 流程处理)。幂等:有效时清标记,无效时置标记 + 原因。
183
- * 不抛错、不退出。返回 agent 便于链式调用。
184
- */
185
- export function validateProvider(agent) {
186
- const ok = Boolean(agent.provider?.name && agent.provider.model && agent.provider.baseURL)
187
- if (ok) {
188
- delete agent._providerInvalid
189
- delete agent._providerInvalidReason
190
- } else {
191
- agent._providerInvalid = true
192
- agent._providerInvalidReason = !agent.provider?.name
193
- ? "provider 不存在"
194
- : !agent.provider.model ? "model 缺失"
195
- : "缺少 baseURL"
196
- }
197
- return agent
198
- }
199
-
200
- /** Read team config and fill in default dir; return null if not configured */
201
- export function teamConfig(config) {
202
- const team = config.memory?.team
203
- if (!team?.repo) return null
204
- const name = team.name ?? "default"
205
- return { name, repo: team.repo, dir: team.dir ?? join(configDir, "teams", name) }
206
- }
207
-
208
- /** Entry author: git config user.name, fallback "unknown" */
209
- export function gitAuthor() {
210
- try {
211
- return execSync("git config user.name", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "unknown"
212
- } catch {
213
- return "unknown"
214
- }
215
- }
@@ -3,10 +3,12 @@ import { loadConfig } from "@thincoder/core/config.mjs"
3
3
  import { teamConfig } from "./make-agent.mjs"
4
4
  import { put, search, list } from "@thincoder/core/memory/core.mjs"
5
5
  import { deleteByUid } from "@thincoder/core/memory/delete.mjs"
6
+ import { sweepMemory, formatSweepReport } from "@thincoder/core/memory/sweep.mjs"
6
7
 
7
- /** thincoder memory <list|search|put|remove> subcommands.
8
+ /** thincoder memory <list|search|put|remove|sweep> subcommands.
8
9
  * opts.dirs: { project, team } layer directories for project/team file deletion (tests inject their own);
9
- * falls back to the same config-derived dirs the agent uses. */
10
+ * falls back to the same config-derived dirs the agent uses.
11
+ * opts.dbPath: 库路径覆盖(sweep 的备份命名依据——缺省 = `config.memory.dbPath` 展开值)。 */
10
12
  export async function memoryCommand(memory, args, opts = {}) {
11
13
  const [sub, ...rest] = args
12
14
 
@@ -57,12 +59,52 @@ export async function memoryCommand(memory, args, opts = {}) {
57
59
  }
58
60
  break
59
61
  }
62
+ case "sweep": {
63
+ const parsed = parseSweepArgs(rest)
64
+ if (parsed.error) {
65
+ console.error(parsed.error)
66
+ console.error(SWEEP_USAGE)
67
+ return 1
68
+ }
69
+ try {
70
+ const result = sweepMemory(memory, { origin: parsed.origin, path: parsed.path, confirm: parsed.confirm, dbPath: opts.dbPath ?? loadConfig().memory.dbPath })
71
+ for (const line of formatSweepReport(result)) console.log(line)
72
+ if (result.dryRun) console.log("(dry-run——零写;要落写加 --confirm)")
73
+ } catch (e) {
74
+ console.error(e.message)
75
+ return 1
76
+ }
77
+ break
78
+ }
60
79
  default:
61
- console.error("Usage: thincoder memory <list|search|put|remove>")
80
+ console.error("Usage: thincoder memory <list|search|put|remove|sweep>")
62
81
  return 1
63
82
  }
64
83
  }
65
84
 
85
+ const SWEEP_USAGE = "Usage: thincoder memory sweep [--origin <o>] [--path <sub>] [--dry-run|--confirm]"
86
+
87
+ /** sweep 参数面:`--origin <o>` ∕ `--path <sub>`(各两形:空格 ∕ `=`)+ 裸 `--dry-run` ∕ `--confirm`
88
+ * (缺省 = 干跑;两者互斥);`--path` 须与 `--origin` 同用(§6.14 面①-3 存量剪枝窄化档)。 */
89
+ function parseSweepArgs(rest) {
90
+ let origin = null, path = null, sawDry = false, sawConfirm = false
91
+ for (let i = 0; i < rest.length; i++) {
92
+ const a = rest[i]
93
+ const eq = a.match(/^--(origin|path)=(.*)$/)
94
+ if (eq) { if (eq[1] === "origin") origin = eq[2]; else path = eq[2]; continue }
95
+ if (a === "--origin") { origin = rest[++i] ?? ""; continue }
96
+ if (a === "--path") { path = rest[++i] ?? ""; continue }
97
+ if (a === "--dry-run") { sawDry = true; continue }
98
+ if (a === "--confirm") { sawConfirm = true; continue }
99
+ return { error: `Unknown argument: ${a}` }
100
+ }
101
+ if (origin === "") return { error: "--origin 需要一个非空路径" }
102
+ if (path === "") return { error: "--path 需要一个非空前缀" }
103
+ if (path !== null && origin === null) return { error: "--path 须与 --origin 同用" }
104
+ if (sawDry && sawConfirm) return { error: "--dry-run 与 --confirm 互斥" }
105
+ return { origin, path, confirm: sawConfirm }
106
+ }
107
+
66
108
  /** Layer directories for project/team file deletion — derived from the same config the agent uses. */
67
109
  function cliDirs() {
68
110
  const config = loadConfig()
@@ -0,0 +1,196 @@
1
+ /**
2
+ * command-interactive.mjs — 交互长驻三命令(chat ∕ tui ∕ acp)——`bin/thincoder.mjs` 命令分发表
3
+ * 外提(2026-09-28 拆档批 R4 · 纯移动——命令行为零改):case 块体逐字搬移为函数(唯二机械改 =
4
+ * `break` → `return` · 相对 import 路径随住档改指);依赖经 ctx 注入(`exitSoon` ∕ `noKeyMessage`
5
+ * ——bin 装配面注入;先例 = `src/tui/turn-face.mjs` 出档);分发落点 = `src/command-table.mjs`。
6
+ */
7
+ import { isAbsolute, join } from "node:path"
8
+ import { runAgent } from "@thincoder/core/agent.mjs"
9
+ import { loadConfig, configPath } from "@thincoder/core/config.mjs"
10
+ // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
11
+ import { waitStatusText } from "@thincoder/core/provider/wait-status.mjs"
12
+ import { assembleAgent, attachManifest, teamConfig, gitAuthor, validateProvider } from "./cli/make-agent.mjs"
13
+ import { setupWizard } from "./cli/setup-wizard.mjs"
14
+ import { summarize, askPermission } from "./cli/permission.mjs"
15
+ import { recentCrashHint } from "./crash-reports.mjs"
16
+
17
+ /** chat —— 一次性 agent 运行(原 `bin/thincoder.mjs` `case "chat"` 块体逐字搬移)。 */
18
+ export async function chatCommand(args, ctx) {
19
+ const { exitSoon, noKeyMessage } = ctx
20
+ const auto = args.includes("--auto")
21
+ const prompt = args.filter((a) => a !== "--auto").join(" ").trim()
22
+ if (!prompt) {
23
+ console.error('Usage: thincoder chat [--auto] "<prompt>"')
24
+ exitSoon(1)
25
+ return
26
+ }
27
+
28
+ // R25(F-R25c):非交互/chat 模式——上次异常终止提示写 stderr 一行(无匹配不提示)
29
+ const crashNotice = recentCrashHint()
30
+ if (crashNotice) console.error(crashNotice)
31
+
32
+ const agent = await assembleAgent()
33
+ // SESSION.md §6.8 D-S4(#841 收正):headless 无 TUI——按**核统一态分流**(判据单源 = PROVIDER.md §6.22,
34
+ // 与 TUI 面同式):**invalid 类**(`state === "invalid"` ∨ `providerInvalidReason` 非空——无渠/key
35
+ // ⇒ 不可运行)⇒ 可读错误 + 退出码 1(不弹 UI、不崩溃);**fallback**(无有效 defaultModel 但可运行)
36
+ // ⇒ stderr 一行明示 + 继续运行(不退出——D-S2 同态同解)。条目结构不全不在本门(负向锁 =
37
+ // PROVIDER.md:437)——归运行期失败径(stderr + 非 0 退出)。
38
+ const headlessCfg = agent.config ?? {}
39
+ const invalidClass = headlessCfg.providerState === "invalid"
40
+ || (typeof headlessCfg.providerInvalidReason === "string" && headlessCfg.providerInvalidReason !== "")
41
+ if (invalidClass) {
42
+ console.error(`[error] 未配置有效渠道(${agent._providerInvalidReason ?? headlessCfg.providerStateReason ?? "provider unavailable"})。请运行 thincoder 进入 TUI 设置(/config 设置渠道与密钥;或 /model 选择后以会话槽生效),或编辑 ${configPath}`)
43
+ exitSoon(1)
44
+ return
45
+ }
46
+ if (headlessCfg.providerState === "fallback" && agent.provider?.name) {
47
+ const m = typeof agent.provider.model === "string" && agent.provider.model ? `:${agent.provider.model}` : ""
48
+ console.error(`尚未设置默认模型:本次使用 \`${agent.provider.name}${m}\`——/config → 默认模型 设置一次;/model 仅改本会话`)
49
+ }
50
+ if (!agent.provider.apiKey) {
51
+ if (!process.stdin.isTTY) {
52
+ console.error(noKeyMessage())
53
+ exitSoon(1)
54
+ return
55
+ }
56
+ const p = await setupWizard()
57
+ if (!p) {
58
+ exitSoon(1)
59
+ return
60
+ }
61
+ agent.provider = p
62
+ agent.activeProvider = p.name
63
+ // Wizard may have configured an embedding key: attach vector search
64
+ const fresh = loadConfig()
65
+ if (fresh.embedding?.apiKey && agent.memory && !agent.memory.embedder) {
66
+ const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
67
+ agent.memory.embedder = createEmbedder(fresh.embedding)
68
+ }
69
+ }
70
+ if (auto) agent.autoApprove = true
71
+ // Accumulate token usage, output to stderr at the end (don't pollute stdout pipe)
72
+ const usageTotal = { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }
73
+ try {
74
+ await runAgent(agent, prompt, {
75
+ onToken: (text) => process.stdout.write(text),
76
+ onWait: (ev) => {
77
+ const s = waitStatusText(ev)
78
+ if (s) console.error(`[rate-limit] ${s}`)
79
+ },
80
+ onToolCall: (name, toolArgs) => {
81
+ console.error(`\n[tool] ${name} ${summarize(toolArgs)}`)
82
+ },
83
+ onToolResult: (name, result) => {
84
+ const preview = result.length > 200 ? result.slice(0, 200) + "..." : result
85
+ console.error(`[done] ${name} -> ${preview.split("\n")[0]}`)
86
+ },
87
+ onToolOutput: (name, chunk) => process.stderr.write(chunk),
88
+ onCompress: () => console.error(`\n[context] Context too long, auto-compacted (early conversation summarized by LLM)`),
89
+ onTaskUpdate: (items) => {
90
+ const done = items.filter((i) => i.status === "done").length
91
+ const current = items.find((i) => i.status === "in_progress")
92
+ console.error(`[task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`)
93
+ },
94
+ onUsage: (usage) => {
95
+ usageTotal.prompt += usage.prompt_tokens ?? 0
96
+ usageTotal.completion += usage.completion_tokens ?? 0
97
+ usageTotal.cacheHit += usage.prompt_cache_hit_tokens ?? 0
98
+ usageTotal.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
99
+ },
100
+ onPermissionRequest: (name, toolArgs) => (agent.autoApprove ? true : askPermission(name, toolArgs)),
101
+ })
102
+ process.stdout.write("\n")
103
+ if (usageTotal.prompt > 0) {
104
+ const cacheTotal = usageTotal.cacheHit + usageTotal.cacheMiss
105
+ const hitPart = cacheTotal > 0 ? ` cache-hit ${Math.round((usageTotal.cacheHit / cacheTotal) * 100)}%` : ""
106
+ console.error(`[usage] prompt ${usageTotal.prompt} + completion ${usageTotal.completion}${hitPart}`)
107
+ }
108
+ } catch (error) {
109
+ // 用 name 判断而非 instanceof:不依赖"与 runAgent 同一个模块实例"这一隐式约定
110
+ if (error.name === "ContinueError") {
111
+ console.error(`\n[paused] Agent stopped after ${error.turn} turns. Run in TUI to continue.`)
112
+ exitSoon(0)
113
+ } else {
114
+ console.error(`\n[error] ${error.message}`)
115
+ exitSoon(1)
116
+ }
117
+ }
118
+ }
119
+
120
+ /** tui ∕ 无参默认 —— 交互 TUI 启动(原 `case "tui": case undefined:` 块体逐字搬移)。 */
121
+ export async function tuiCommand(ctx) {
122
+ const { exitSoon } = ctx
123
+ // 恢复上次的会话(同一项目目录);provider 按保存的名字切回(用户上次可能换过模型)
124
+ // 2026-09-05 §6.10(R4):恢复决策按本端记录 resumeSlot(D-2 ①②③)——manifest active
125
+ // 只作"无记录端"的一次性继承源,不再作本端恢复第一依据(D-6)。
126
+ // 2026-09-18(装配门禁批 #30 · KD-M1-13 / KD-M1-15):resumeSlot **前移至装配之前**——
127
+ // 装配期模式门判据 = 会话权威值(槽优先 + config 回退);恢复记录经 slotData 形参进
128
+ // 装配钩子(同进程同一调用前移 ⇒ 零新增写动作;钩子内不调 resumeSlot——非纯读)。
129
+ const { resumeSlot, applySession, sessionDescriptor } = await import("@thincoder/core/session.mjs")
130
+ // F-MI7:resumeSlot = async(探测束不阻塞事件循环)——此处必须 await
131
+ const { slot, data } = await resumeSlot(process.cwd())
132
+ const agent = await assembleAgent({ slotData: data })
133
+ // SESSION.md §6.8 D-S1(#841 收正):TUI 路径**仅在 `state === "invalid"`**(无渠/key)于 startTUI
134
+ // 前置 `agent.provider = null`——空 provider 不流入 runAgent(崩溃源:chat() 缺 model → 网关 400
135
+ // 或 fetch("undefined/...") TypeError);`fallback` 态 provider 有效——不清(可运行 + 明示)。
136
+ if (agent.config?.providerState === "invalid") agent.provider = null
137
+ const config = loadConfig()
138
+ if (data) {
139
+ // applySession 内部已按槽复合重算 compactThreshold(auto 时)——不再需要 switched 分支
140
+ // TUI-OOM-ROOTCAUSE(SESSION.md §6.14):传 slot → 绑定记录存储(身份核验 + 对账)
141
+ // ——人读线 = 尾窗(磁盘为准);未传 = 模式 F(全量数组)
142
+ applySession(agent, data, { slot })
143
+ // SESSION.md §6.11(2026-09-08——N6 评审 🔴 修复):process restarted 句 = 进程级
144
+ // 信号——仅本启动 resume 路径设一次(真进程重启恢复盘上会话);/session 切换与 ACP
145
+ // 加载走同一 applySession 收敛但不设(不误报进程重启)。prepareRun 发句即清——进程
146
+ // 内一次。resumed:yes 独立由 applySession 的恢复事件武装(data.history 非空)。
147
+ agent._processRestartPending = true
148
+ }
149
+ // D-3 钉槽:applySession 清 _slot 后立即钉回恢复槽——首保存必落恢复槽(消除
150
+ // "load → 首回合保存"窗口内并发方翻 active 导致的静默迁移);/new 经 resetSessionState
151
+ // 清 _slot(newSession 已认领 + 写记录,语义保留)
152
+ agent._slot = slot
153
+ // M1 重估点(会话起点②——applySession 之后 · KD-M1-13 取值点②):CLI 的工程模式权威值 =
154
+ // 会话槽(/eng 只写槽不写 config.json);此时 agent.config.agent.engineering 已由
155
+ // applySession 按槽订正 ⇒ 与装配期判据同值 ⇒ 幂等重估(值同 ⇒ 无副作用)。
156
+ attachManifest(agent)
157
+ // D-S3 优先级补全:applySession 可能已用会话中的有效 provider 修复(config 无效 + 会话有效)——
158
+ // 修复后复验清除标记,仅当两者都无效才弹重选(validateProvider 幂等)
159
+ if (agent._providerInvalid) validateProvider(agent)
160
+ // MCP 连接失败在 TUI alt-buffer 下 stderr 不可见,注入为下一条 user 消息后的提醒
161
+ if (agent._mcpWarnings?.length) {
162
+ agent._pendingReminders = agent._pendingReminders ?? []
163
+ agent._pendingReminders.push(
164
+ `[System reminder: ${agent._mcpWarnings.length} MCP server(s) failed to connect at startup:\n` +
165
+ agent._mcpWarnings.map((w) => ` - ${w}`).join("\n") +
166
+ `\nYou can try reconnecting with /mcp connect <name>.]`
167
+ )
168
+ }
169
+ const { startTUI } = await import("./tui.mjs")
170
+ try {
171
+ await startTUI(agent, {
172
+ projectDir: config.memory.projectDir ? (isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)) : null,
173
+ team: teamConfig(config),
174
+ author: gitAuthor(),
175
+ restored: data ? sessionDescriptor(agent, data) : data,
176
+ // R25(F-R25c):TUI 启动显示一行"上次运行异常终止"提示(showStartup 渲染——无匹配不传)
177
+ crashNotice: recentCrashHint() ?? undefined,
178
+ })
179
+ } catch (error) {
180
+ console.error(`[error] ${error.message}`)
181
+ exitSoon(1)
182
+ }
183
+ }
184
+
185
+ /** acp —— Agent Client Protocol 服务(原 `case "acp"` 块体逐字搬移)。 */
186
+ export async function acpCommand(args, ctx) {
187
+ const { exitSoon } = ctx
188
+ const { runAcpServer, runAcpLogin } = await import("./acp.mjs")
189
+ // §11.2 改法 4:`args` 语义 = 追加到已配置的 agent 调用——`--login` = 终端认证流程,
190
+ // 不进 stdio 服务模式(不带该旗标时行为零变);退出码语义 = 0 成功 / 非 0 失败。
191
+ if (args.includes("--login")) {
192
+ exitSoon(await runAcpLogin())
193
+ return
194
+ }
195
+ await runAcpServer()
196
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * command-table.mjs — CLI 命令分发表(`bin/thincoder.mjs` 外提——2026-09-28 拆档批 R4 · 纯移动 ·
3
+ * 命令行为 ∕ USAGE 文本 ∕ 旗标语义零改):分发骨架 + 八薄命令族(memory ∕ sync ∕ distill ∕
4
+ * reindex ∕ completion ∕ upgrade ∕ session ∕ ledger)+ help ∕ version;交互长驻三命令
5
+ * (chat ∕ tui ∕ acp)经 `command-interactive.mjs` 承接。依赖经 ctx 注入(`usage` ∕ `version` ∕
6
+ * `exitSoon`——bin 装配面注入;先例 = `src/tui/turn-face.mjs` 出档)。
7
+ */
8
+ import { readFileSync } from "node:fs"
9
+ import { isAbsolute, join } from "node:path"
10
+ import { loadConfig } from "@thincoder/core/config.mjs"
11
+ import { createMemory, syncDir } from "@thincoder/core/memory.mjs"
12
+ import { teamConfig } from "./cli/make-agent.mjs"
13
+ import { memoryCommand } from "./cli/memory-command.mjs"
14
+ import { distillCommand } from "./cli/distill-command.mjs"
15
+ import { chatCommand, tuiCommand, acpCommand } from "./command-interactive.mjs"
16
+
17
+ /** 命令分发(原 `bin/thincoder.mjs` 顶层 switch 骨架逐字搬移——命令行为零改)。 */
18
+ export async function runCommandTable(command, args, ctx) {
19
+ const { usage, version, exitSoon } = ctx
20
+ switch (command) {
21
+ case "chat": {
22
+ await chatCommand(args, ctx)
23
+ break
24
+ }
25
+
26
+ case "memory": {
27
+ const config = loadConfig()
28
+ const memory = createMemory({ dbPath: config.memory.dbPath })
29
+ if (config.embedding?.apiKey) {
30
+ const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
31
+ memory.embedder = createEmbedder(config.embedding)
32
+ }
33
+ if (config.memory.projectDir) {
34
+ memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)
35
+ }
36
+ const code = await memoryCommand(memory, args)
37
+ if (code) exitSoon(code)
38
+ break
39
+ }
40
+
41
+ case "sync": {
42
+ const config = loadConfig()
43
+ const team = teamConfig(config)
44
+ if (!team) {
45
+ console.error("Team memory not configured. Set memory.team in ~/.thincoder/config.json:")
46
+ console.error(' "team": { "name": "myteam", "repo": "git@github.com:org/team-memory.git" }')
47
+ exitSoon(1)
48
+ break
49
+ }
50
+ const memory = createMemory({ dbPath: config.memory.dbPath })
51
+ const { ensureClone, pullTeam } = await import("@thincoder/core/git/gitmem.mjs")
52
+ try {
53
+ const cloned = await ensureClone(team)
54
+ if (cloned) console.log(`Cloned team repo to ${team.dir}`)
55
+ await pullTeam(team.dir)
56
+ const stats = await syncDir(memory, { layer: "team", dir: team.dir })
57
+ console.log(`Synced. Index: +${stats.added} ~${stats.updated} -${stats.removed}`)
58
+ } catch (error) {
59
+ console.error(`[error] ${error.message}`)
60
+ exitSoon(1)
61
+ }
62
+ break
63
+ }
64
+
65
+ case "distill": {
66
+ const code = await distillCommand(args, exitSoon)
67
+ if (code) exitSoon(code)
68
+ break
69
+ }
70
+
71
+ case "reindex": {
72
+ const config = loadConfig()
73
+ const memory = createMemory({ dbPath: config.memory.dbPath })
74
+ // files 层(project/team 的 markdown 索引)全量重建;索引是易失品,真相在 markdown
75
+ memory.db.prepare(`DELETE FROM files`).run()
76
+ const cwd = process.cwd()
77
+ let total = { added: 0, removed: 0 }
78
+ if (config.memory.projectDir) {
79
+ const s = await syncDir(memory, { layer: "project", dir: isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir) })
80
+ total.added += s.added
81
+ }
82
+ const team = teamConfig(config)
83
+ if (team) {
84
+ const { ensureClone } = await import("@thincoder/core/git/gitmem.mjs")
85
+ await ensureClone(team)
86
+ const s = await syncDir(memory, { layer: "team", dir: team.dir })
87
+ total.added += s.added
88
+ }
89
+ console.log(`Reindexed ${total.added} markdown entries (project${team ? " + team" : ""}). Vectors will be lazily regenerated on next search.`)
90
+ break
91
+ }
92
+
93
+ case "tui":
94
+ case undefined: {
95
+ await tuiCommand(ctx)
96
+ break
97
+ }
98
+
99
+ case "completion": {
100
+ const shell = args[0]
101
+ if (!["bash", "zsh", "fish"].includes(shell)) {
102
+ console.error(`Usage: thincoder completion <bash|zsh|fish>`)
103
+ exitSoon(1)
104
+ break
105
+ }
106
+ // §11.2 交付行数债(2026-09-08——500 行硬限触碰执行):直写段逐字迁出
107
+ // src/completions.mjs(脚本字节不变——printCompletion 发射)
108
+ const { printCompletion } = await import("./completions.mjs")
109
+ printCompletion(shell)
110
+ break
111
+ }
112
+
113
+ case "upgrade": {
114
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
115
+ const local = pkg.version
116
+ const { checkForUpdate } = await import("./upgrade.mjs")
117
+ const result = await checkForUpdate(local)
118
+ if (!result) {
119
+ console.error("[upgrade] Unable to query npm registry — check your network connection and that npm is installed")
120
+ exitSoon(1)
121
+ break
122
+ }
123
+ if (!result.newer) {
124
+ console.log(`ThinCoder ${local} is already the latest.`)
125
+ } else {
126
+ console.log(`Upgrading: ${local} → ${result.latest}`)
127
+ const { execSync } = await import("node:child_process")
128
+ execSync("npm install -g thincoder@latest", { stdio: "inherit" })
129
+ console.log(`Upgraded to ${result.latest}`)
130
+ }
131
+ break
132
+ }
133
+
134
+ case "acp": {
135
+ await acpCommand(args, ctx)
136
+ break
137
+ }
138
+
139
+ case "session": {
140
+ // SESSION.md §6.12 / §6.17:会话目录 GC 显式面(冷 cwd 90 天面 + 三合取存量组全量面)——
141
+ // 双端同面(2026-09-21 端差注销:VS Code 端命令 `thincoder.sessionGc` 走同一核数据面)。
142
+ // SESSION.md §6.19 D-SE46(2026-09-22 会话索引批):`session index` = 派生索引库显式面
143
+ // (--status 读数 / --rebuild 全量重扫)——同一 `session` 分发族(壳侧分发,实现在核)。
144
+ if (args[0] === "index") {
145
+ const { runSessionIndex } = await import("@thincoder/core/session-index-cmd.mjs")
146
+ process.exitCode = await runSessionIndex(args)
147
+ break
148
+ }
149
+ const { runSessionGc } = await import("@thincoder/core/session-gc.mjs")
150
+ process.exitCode = await runSessionGc(args)
151
+ break
152
+ }
153
+
154
+ case "ledger": {
155
+ // LEDGER.md §2.2(2026-09-25 批):台账存量收正面——变体键合并迁移(migrate)/ 残档审计(audit);
156
+ // 核内实现(ledger-migrate.mjs),壳侧只分发(先例 = `session gc` 双档:--dry-run 只读 / --confirm 执行)。
157
+ if (args[0] === "migrate" || args[0] === "audit") {
158
+ const m = await import("@thincoder/core/ledger-migrate.mjs")
159
+ process.exitCode = await (args[0] === "migrate" ? m.runLedgerMigrate(args) : m.runLedgerAudit(args))
160
+ break
161
+ }
162
+ console.error("Usage: thincoder ledger migrate --dry-run | --confirm [--from <key>] | thincoder ledger audit [--root <dir>]")
163
+ exitSoon(1)
164
+ break
165
+ }
166
+
167
+ case "--help":
168
+ case "-h": {
169
+ process.stdout.write(usage)
170
+ break
171
+ }
172
+
173
+ case "--version":
174
+ case "-v": {
175
+ console.log(version)
176
+ break
177
+ }
178
+
179
+ default: {
180
+ console.error(`Unknown command: ${command}\n`)
181
+ process.stdout.write(usage)
182
+ exitSoon(1)
183
+ }
184
+ }
185
+ }