thincoder 0.12.66 → 0.12.68

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 (69) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +23 -5
  3. package/bin/thincoder.mjs +43 -331
  4. package/package.json +2 -2
  5. package/src/acp/bridge.mjs +13 -12
  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/cli/permission.mjs +1 -1
  10. package/src/command-interactive.mjs +186 -0
  11. package/src/command-table.mjs +185 -0
  12. package/src/completions.mjs +36 -12
  13. package/src/crash-reports.mjs +42 -20
  14. package/src/distill.mjs +3 -3
  15. package/src/heap-watch.mjs +5 -14
  16. package/src/prompt-injections.mjs +1 -1
  17. package/src/tui/agent-turn.mjs +114 -69
  18. package/src/tui/clipboard.mjs +74 -30
  19. package/src/tui/cmd-advisor.mjs +54 -13
  20. package/src/tui/cmd-clear.mjs +2 -2
  21. package/src/tui/cmd-config.mjs +26 -2
  22. package/src/tui/cmd-eng.mjs +1 -1
  23. package/src/tui/cmd-mcp.mjs +1 -1
  24. package/src/tui/cmd-new.mjs +2 -2
  25. package/src/tui/cmd-reindex.mjs +1 -1
  26. package/src/tui/cmd-session.mjs +13 -5
  27. package/src/tui/cmd-think.mjs +35 -17
  28. package/src/tui/cmd-timers.mjs +33 -0
  29. package/src/tui/conversation-writer.mjs +2 -2
  30. package/src/tui/display-budget.mjs +9 -9
  31. package/src/tui/fold-block.mjs +1 -1
  32. package/src/tui/index.mjs +23 -5
  33. package/src/tui/interaction.mjs +5 -3
  34. package/src/tui/key-handler-busy.mjs +17 -12
  35. package/src/tui/key-handler-ctrlc.mjs +5 -5
  36. package/src/tui/key-handler-edit.mjs +12 -8
  37. package/src/tui/key-handler-search.mjs +1 -1
  38. package/src/tui/key-handler.mjs +2 -2
  39. package/src/tui/key-modes.mjs +1 -1
  40. package/src/tui/layout.mjs +5 -5
  41. package/src/tui/ledger-surface.mjs +26 -5
  42. package/src/tui/lifecycle-records.mjs +199 -0
  43. package/src/tui/model-picker.mjs +16 -199
  44. package/src/tui/mouse.mjs +5 -5
  45. package/src/tui/pickers.mjs +6 -2
  46. package/src/tui/provider-admin.mjs +213 -0
  47. package/src/tui/queued-merge.mjs +12 -0
  48. package/src/tui/queued-pickup.mjs +34 -0
  49. package/src/tui/render-conversation.mjs +49 -4
  50. package/src/tui/render-frame.mjs +54 -11
  51. package/src/tui/render-loop.mjs +2 -3
  52. package/src/tui/render-segments.mjs +4 -4
  53. package/src/tui/render.mjs +1 -1
  54. package/src/tui/slash-commands.mjs +3 -0
  55. package/src/tui/startup.mjs +28 -3
  56. package/src/tui/subagent-blocks.mjs +37 -29
  57. package/src/tui/subagent-children.mjs +5 -5
  58. package/src/tui/subagent-freeze.mjs +43 -41
  59. package/src/tui/subagent-panel.mjs +11 -11
  60. package/src/tui/suspension-drive.mjs +173 -253
  61. package/src/tui/timer-watch.mjs +64 -0
  62. package/src/tui/tool-args.mjs +10 -71
  63. package/src/tui/tool-display.mjs +7 -7
  64. package/src/tui/tool-events.mjs +40 -35
  65. package/src/tui/tui-lifecycle.mjs +13 -4
  66. package/src/tui/tui-state.mjs +3 -2
  67. package/src/tui/turn-face.mjs +6 -3
  68. package/src/tui/wizard.mjs +7 -2
  69. package/src/tui/wrapped-spawn.mjs +3 -3
@@ -20,7 +20,7 @@
20
20
  * `{ stopReason: "end_turn" }` (kimi session.ts parity).
21
21
  */
22
22
  import { detectDanger, normalizeEOL, joinWithEol } from "@thincoder/core/tools/shared.mjs"
23
- import { computeEditEntry, validateEditEntry, assertEditArgsExclusive, hasLineParams } from "@thincoder/core/tools/edit-diff.mjs"
23
+ import { computeEditEntry, validateEditEntry, assertEditArgsExclusive, assertEditsContainer, assertEditEntries, hasLineParams, EDIT_ENTRY_NO_PATH, EDIT_ABORT_PREFIX, editEntryLabel } from "@thincoder/core/tools/edit-diff.mjs"
24
24
  // 第 27 批 §12.3①/③:relay 前缀文法单一权威(模块直连——不自持正则副本)。
25
25
  import { parseRelayPath } from "@thincoder/core/agent/relay-prefix.mjs"
26
26
  // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(本面仅日志——PROVIDER.md §6.20)
@@ -106,7 +106,6 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
106
106
  // (edit-diff.mjs——校验→判定序→应用:行级 LCS、零重叠→替换即删、replace_all 字面替换全部)。
107
107
  // 桥只留「读 IDE 缓冲 → computeEditEntry → 写回 IDE 缓冲」——错误文本经抛错原样透传
108
108
  // ——与本地通道逐字一致(NF15.6b / AC15.10:not found / occurrences / 空 old / 空 new)。
109
- const EDIT_ABORT_PREFIX = "edit aborted (atomic — no files written): "
110
109
 
111
110
  // 不变量(§11.7-2):**任何** fs/* 反向 RPC 发出前必过客户端能力位——守卫住在本函数内
112
111
  // (防御面:路由分支先判,守卫兜底,漏判不发静默超时)。
@@ -137,24 +136,23 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
137
136
  await writeBuffer(p, joinWithEol(normalizeEOL(out.updated).split("\n"), raw))
138
137
  return `OK: edited ${p} via IDE (${out.occurrences} occurrence(s))${out.note ? ` — ${out.note}` : ""}`
139
138
  }
140
- /** 数组形态(D15.7):条目校验(path——顶层默认自 args.path ?? args.filePath(pathOf)——
141
- * 2026-09-05 用户裁定 CLI parity——/validateEditEntry/互斥(只对顶层 old/new)——同本地 edit-batch 措辞)→
142
- * 读全部涉及文件缓冲(同文件去重——一次读)→ 逐条 computeEditEntry(abortPrefix——批量
139
+ /** 数组形态(D15.7):守卫与文案单源 = 本地 edit-diff(assertEditsContainer / assertEditEntries /
140
+ * validateEditEntry——EDIT.md §8 D-7:核 edit-batch 与桥同调用、桥零字面副本)→
141
+ * 读全部涉及文件缓冲(同文件去重——一次读)→ 逐条 computeEditEntry(EDIT_ABORT_PREFIX——批量
143
142
  * 原子前缀;同文件条目按数组序串行累积——第二条基于第一条结果)→ 全部通过 → 逐文件写回
144
143
  * 一次(判失败 → 零写;写失败 → 同本地 edit-batch 既有原子语义)。 */
145
144
  const editBatch = async (args) => {
146
145
  const edits = args.edits
147
- if (!Array.isArray(edits) || edits.length === 0) {
148
- throw new Error("edits must be a non-empty array of {path, old_string | line/startLine+endLine, new_string}")
149
- }
146
+ assertEditsContainer(edits)
150
147
  assertEditArgsExclusive(args)
148
+ assertEditEntries(edits)
151
149
  const groups = new Map() // path → { path, raw, content, edits }
152
150
  for (const e of edits) {
153
151
  // 2026-09-05 用户裁定(CLI parity——本地 edit-batch 同句):条目自带 path 优先;
154
152
  // 缺省回退顶层 path(pathOf——path/filePath 别名同单形态)
155
153
  const p = e.path ?? pathOf(args)
156
- if (!p) throw new Error("each edit must have a path — give each entry its own path or pass a top-level path")
157
- validateEditEntry(e, { label: `edit for ${p}: `, rich: false })
154
+ if (!p) throw new Error(EDIT_ENTRY_NO_PATH)
155
+ validateEditEntry(e, { label: editEntryLabel(p), rich: false })
158
156
  let g = groups.get(p)
159
157
  if (!g) {
160
158
  g = { path: p, raw: "", content: "", edits: [] }
@@ -310,9 +308,12 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
310
308
  // 2026-09-08 D1:单形态按行号改(path + line/startLine/endLine + new_string——无
311
309
  // old_string)同样走 IDE 缓冲通道(hasLineParams 判定)——否则回落本地写盘会与
312
310
  // IDE 缓冲脱敏;editSingle → computeEditEntry 行号语义自动继承。
313
- if (base === "edit" && canReadFile && canWriteFile && (Array.isArray(args?.edits) || (path && typeof args?.new_string === "string" && (typeof args?.old_string === "string" || hasLineParams(args))))) {
311
+ // #327(TOOLS.md §6.17 裁定 3):路由判据**归一**——`edits` 真值判(与核 execute 同判据):
312
+ // 真值 ⇒ 批量分支(共享容器守卫、同一错误面);假值 ⇒ 单形态面——「桥径单形态应用」分支消除。
313
+ const hasEdits = Boolean(args?.edits)
314
+ if (base === "edit" && canReadFile && canWriteFile && (hasEdits || (path && typeof args?.new_string === "string" && (typeof args?.old_string === "string" || hasLineParams(args))))) {
314
315
  try {
315
- const text = Array.isArray(args?.edits) ? await editBatch(args) : await editSingle(path, args)
316
+ const text = hasEdits ? await editBatch(args) : await editSingle(path, args)
316
317
  return { handled: true, result: text }
317
318
  } catch (e) {
318
319
  return { handled: true, result: `Error: ${e.message}` }
@@ -18,6 +18,9 @@ import { loadManifest, normalizeCwd, bindRecordStore, newSession, saveManifest,
18
18
  // core `session.mjs` 未 re-export 该名——直引 manifest 档,先例 = `thincoder-vscode/src/extension/session-io.mjs`)。
19
19
  import { staleClaims } from "@thincoder/core/session-slots-manifest.mjs"
20
20
  import { PLAN_ENGINEERING_REFUSED } from "@thincoder/core/agent-tools/plan.mjs"
21
+ // #363①(off 形族单源——MODEL-SPECS.md §16.2):thinking 开关的取形引核(本档零副本)。
22
+ import { thinkOffShape } from "@thincoder/core/think-off.mjs"
23
+ import { providerSpec } from "@thincoder/core/config.mjs"
21
24
  import { ACP_ERRORS } from "./transport.mjs"
22
25
 
23
26
  /** ENG-PLAN-EXCLUSION(FR31 ② · E7「命令面逐面钉定」):工程真值 = 核单源同键
@@ -85,7 +88,15 @@ export function applyConfigOption(agent, configId, value) {
85
88
  }
86
89
  case "thinking": {
87
90
  if (typeof value !== "boolean" || !agent.provider) return false
88
- agent.provider.thinking = value ? { type: "enabled" } : { type: "disabled" }
91
+ // off 取形 = 核单源(MODEL-SPECS.md §16.2——#363①):effort 族 ⇒ `null`(载荷门据其补发
92
+ // `reasoning_effort:"none"` = 唯一有效 off 路径);其余族 ⇒ `{ type:"disabled" }`。旧形
93
+ // 全族一形 `{type:"disabled"}` 在 effort 族被载荷层 falsy 跳过 ⇒ 关思考静默失效。
94
+ // on 侧同引核(thinkEnabledValue——自定义开值族 MiniMax = "adaptive")。
95
+ const spec = providerSpec(agent.provider)
96
+ agent.provider.thinking = value ? { type: spec.thinkEnabledValue ?? "enabled" } : thinkOffShape(spec)
97
+ // effort 族:`null` 标记在载荷层由「无显式档」门控(provider/core.mjs:193 显式档优先会
98
+ // 跳过标记)⇒ off 清档位(与 CLI `/think off` 同法——cmd-think.mjs `applyThink` off 支)。
99
+ if (!value) delete agent.provider.reasoningEffort
89
100
  return true
90
101
  }
91
102
  case "mode": {
@@ -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()
@@ -1,4 +1,4 @@
1
- /** permission.mjs — CLI 转口档(P2 机制层端差批 §2.20):闸语义 + 每工具请示文案单源 = 核
1
+ /** permission.mjs — CLI 转口档(P2 机制层端差批 docs/batches/2026-09-20-mechanism-parity-batch.md §2.20):闸语义 + 每工具请示文案单源 = 核
2
2
  * `permission.mjs`(#165 落点——本端零第二实现);展示面由核的 `io.ask` 缝承载。
3
3
  * 导出面与核逐字对齐,保既有 import 面(`bin/thincoder.mjs` · `distill-command.mjs`)。 */
4
4
  export { summarize, formatPermission, askPermission } from "@thincoder/core/permission.mjs"
@@ -0,0 +1,186 @@
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(F4)+ MODEL-MERGE-SESSION:headless 无 TUI —— 可读错误 + 退出码 1,
34
+ // 不弹 UI、不崩溃;文案改引 defaultModel(F-6 引导 A headless 面——/config → 默认模型)
35
+ if (agent._providerInvalid) {
36
+ const dm = agent.config?.defaultModel
37
+ console.error(`[error] 未配置有效模型(defaultModel "${dm ?? "(not set)"}":${agent._providerInvalidReason})。请运行 thincoder 进入 TUI 设置(/config → 默认模型;或 /model 选择后以会话槽生效),或编辑 ${configPath}`)
38
+ exitSoon(1)
39
+ return
40
+ }
41
+ if (!agent.provider.apiKey) {
42
+ if (!process.stdin.isTTY) {
43
+ console.error(noKeyMessage())
44
+ exitSoon(1)
45
+ return
46
+ }
47
+ const p = await setupWizard()
48
+ if (!p) {
49
+ exitSoon(1)
50
+ return
51
+ }
52
+ agent.provider = p
53
+ agent.activeProvider = p.name
54
+ // Wizard may have configured an embedding key: attach vector search
55
+ const fresh = loadConfig()
56
+ if (fresh.embedding?.apiKey && agent.memory && !agent.memory.embedder) {
57
+ const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
58
+ agent.memory.embedder = createEmbedder(fresh.embedding)
59
+ }
60
+ }
61
+ if (auto) agent.autoApprove = true
62
+ // Accumulate token usage, output to stderr at the end (don't pollute stdout pipe)
63
+ const usageTotal = { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }
64
+ try {
65
+ await runAgent(agent, prompt, {
66
+ onToken: (text) => process.stdout.write(text),
67
+ onWait: (ev) => {
68
+ const s = waitStatusText(ev)
69
+ if (s) console.error(`[rate-limit] ${s}`)
70
+ },
71
+ onToolCall: (name, toolArgs) => {
72
+ console.error(`\n[tool] ${name} ${summarize(toolArgs)}`)
73
+ },
74
+ onToolResult: (name, result) => {
75
+ const preview = result.length > 200 ? result.slice(0, 200) + "..." : result
76
+ console.error(`[done] ${name} -> ${preview.split("\n")[0]}`)
77
+ },
78
+ onToolOutput: (name, chunk) => process.stderr.write(chunk),
79
+ onCompress: () => console.error(`\n[context] Context too long, auto-compacted (early conversation summarized by LLM)`),
80
+ onTaskUpdate: (items) => {
81
+ const done = items.filter((i) => i.status === "done").length
82
+ const current = items.find((i) => i.status === "in_progress")
83
+ console.error(`[task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`)
84
+ },
85
+ onUsage: (usage) => {
86
+ usageTotal.prompt += usage.prompt_tokens ?? 0
87
+ usageTotal.completion += usage.completion_tokens ?? 0
88
+ usageTotal.cacheHit += usage.prompt_cache_hit_tokens ?? 0
89
+ usageTotal.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
90
+ },
91
+ onPermissionRequest: (name, toolArgs) => (agent.autoApprove ? true : askPermission(name, toolArgs)),
92
+ })
93
+ process.stdout.write("\n")
94
+ if (usageTotal.prompt > 0) {
95
+ const cacheTotal = usageTotal.cacheHit + usageTotal.cacheMiss
96
+ const hitPart = cacheTotal > 0 ? ` cache-hit ${Math.round((usageTotal.cacheHit / cacheTotal) * 100)}%` : ""
97
+ console.error(`[usage] prompt ${usageTotal.prompt} + completion ${usageTotal.completion}${hitPart}`)
98
+ }
99
+ } catch (error) {
100
+ // 用 name 判断而非 instanceof:不依赖"与 runAgent 同一个模块实例"这一隐式约定
101
+ if (error.name === "ContinueError") {
102
+ console.error(`\n[paused] Agent stopped after ${error.turn} turns. Run in TUI to continue.`)
103
+ exitSoon(0)
104
+ } else {
105
+ console.error(`\n[error] ${error.message}`)
106
+ exitSoon(1)
107
+ }
108
+ }
109
+ }
110
+
111
+ /** tui ∕ 无参默认 —— 交互 TUI 启动(原 `case "tui": case undefined:` 块体逐字搬移)。 */
112
+ export async function tuiCommand(ctx) {
113
+ const { exitSoon } = ctx
114
+ // 恢复上次的会话(同一项目目录);provider 按保存的名字切回(用户上次可能换过模型)
115
+ // 2026-09-05 §6.10(R4):恢复决策按本端记录 resumeSlot(D-2 ①②③)——manifest active
116
+ // 只作"无记录端"的一次性继承源,不再作本端恢复第一依据(D-6)。
117
+ // 2026-09-18(装配门禁批 #30 · KD-M1-13 / KD-M1-15):resumeSlot **前移至装配之前**——
118
+ // 装配期模式门判据 = 会话权威值(槽优先 + config 回退);恢复记录经 slotData 形参进
119
+ // 装配钩子(同进程同一调用前移 ⇒ 零新增写动作;钩子内不调 resumeSlot——非纯读)。
120
+ const { resumeSlot, applySession, sessionDescriptor } = await import("@thincoder/core/session.mjs")
121
+ // F-MI7:resumeSlot = async(探测束不阻塞事件循环)——此处必须 await
122
+ const { slot, data } = await resumeSlot(process.cwd())
123
+ const agent = await assembleAgent({ slotData: data })
124
+ // SESSION.md §6.8 D-S1:TUI 路径在 startTUI 前清空无效 provider——空 provider 不流入 runAgent
125
+ // (崩溃源:chat() 缺 model → 网关 400 或 fetch("undefined/...") TypeError)
126
+ if (agent._providerInvalid) agent.provider = null
127
+ const config = loadConfig()
128
+ if (data) {
129
+ // applySession 内部已按槽复合重算 compactThreshold(auto 时)——不再需要 switched 分支
130
+ // TUI-OOM-ROOTCAUSE(SESSION.md §6.14):传 slot → 绑定记录存储(身份核验 + 对账)
131
+ // ——人读线 = 尾窗(磁盘为准);未传 = 模式 F(全量数组)
132
+ applySession(agent, data, { slot })
133
+ // SESSION.md §6.11(2026-09-08——N6 评审 🔴 修复):process restarted 句 = 进程级
134
+ // 信号——仅本启动 resume 路径设一次(真进程重启恢复盘上会话);/session 切换与 ACP
135
+ // 加载走同一 applySession 收敛但不设(不误报进程重启)。prepareRun 发句即清——进程
136
+ // 内一次。resumed:yes 独立由 applySession 的恢复事件武装(data.history 非空)。
137
+ agent._processRestartPending = true
138
+ }
139
+ // D-3 钉槽:applySession 清 _slot 后立即钉回恢复槽——首保存必落恢复槽(消除
140
+ // "load → 首回合保存"窗口内并发方翻 active 导致的静默迁移);/new 经 resetSessionState
141
+ // 清 _slot(newSession 已认领 + 写记录,语义保留)
142
+ agent._slot = slot
143
+ // M1 重估点(会话起点②——applySession 之后 · KD-M1-13 取值点②):CLI 的工程模式权威值 =
144
+ // 会话槽(/eng 只写槽不写 config.json);此时 agent.config.agent.engineering 已由
145
+ // applySession 按槽订正 ⇒ 与装配期判据同值 ⇒ 幂等重估(值同 ⇒ 无副作用)。
146
+ attachManifest(agent)
147
+ // D-S3 优先级补全:applySession 可能已用会话中的有效 provider 修复(config 无效 + 会话有效)——
148
+ // 修复后复验清除标记,仅当两者都无效才弹重选(validateProvider 幂等)
149
+ if (agent._providerInvalid) validateProvider(agent)
150
+ // MCP 连接失败在 TUI alt-buffer 下 stderr 不可见,注入为下一条 user 消息后的提醒
151
+ if (agent._mcpWarnings?.length) {
152
+ agent._pendingReminders = agent._pendingReminders ?? []
153
+ agent._pendingReminders.push(
154
+ `[System reminder: ${agent._mcpWarnings.length} MCP server(s) failed to connect at startup:\n` +
155
+ agent._mcpWarnings.map((w) => ` - ${w}`).join("\n") +
156
+ `\nYou can try reconnecting with /mcp connect <name>.]`
157
+ )
158
+ }
159
+ const { startTUI } = await import("./tui.mjs")
160
+ try {
161
+ await startTUI(agent, {
162
+ projectDir: config.memory.projectDir ? (isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)) : null,
163
+ team: teamConfig(config),
164
+ author: gitAuthor(),
165
+ restored: data ? sessionDescriptor(agent, data) : data,
166
+ // R25(F-R25c):TUI 启动显示一行"上次运行异常终止"提示(showStartup 渲染——无匹配不传)
167
+ crashNotice: recentCrashHint() ?? undefined,
168
+ })
169
+ } catch (error) {
170
+ console.error(`[error] ${error.message}`)
171
+ exitSoon(1)
172
+ }
173
+ }
174
+
175
+ /** acp —— Agent Client Protocol 服务(原 `case "acp"` 块体逐字搬移)。 */
176
+ export async function acpCommand(args, ctx) {
177
+ const { exitSoon } = ctx
178
+ const { runAcpServer, runAcpLogin } = await import("./acp.mjs")
179
+ // §11.2 改法 4:`args` 语义 = 追加到已配置的 agent 调用——`--login` = 终端认证流程,
180
+ // 不进 stdio 服务模式(不带该旗标时行为零变);退出码语义 = 0 成功 / 非 0 失败。
181
+ if (args.includes("--login")) {
182
+ exitSoon(await runAcpLogin())
183
+ return
184
+ }
185
+ await runAcpServer()
186
+ }