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.
- package/CHANGELOG.md +52 -0
- package/README.md +23 -5
- package/bin/thincoder.mjs +43 -331
- package/package.json +2 -2
- package/src/acp/bridge.mjs +13 -12
- package/src/acp/handlers-session.mjs +12 -1
- package/src/cli/make-agent.mjs +44 -119
- package/src/cli/memory-command.mjs +45 -3
- package/src/cli/permission.mjs +1 -1
- package/src/command-interactive.mjs +186 -0
- package/src/command-table.mjs +185 -0
- package/src/completions.mjs +36 -12
- package/src/crash-reports.mjs +42 -20
- package/src/distill.mjs +3 -3
- package/src/heap-watch.mjs +5 -14
- package/src/prompt-injections.mjs +1 -1
- package/src/tui/agent-turn.mjs +114 -69
- package/src/tui/clipboard.mjs +74 -30
- package/src/tui/cmd-advisor.mjs +54 -13
- package/src/tui/cmd-clear.mjs +2 -2
- package/src/tui/cmd-config.mjs +26 -2
- package/src/tui/cmd-eng.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +1 -1
- package/src/tui/cmd-new.mjs +2 -2
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-session.mjs +13 -5
- package/src/tui/cmd-think.mjs +35 -17
- package/src/tui/cmd-timers.mjs +33 -0
- package/src/tui/conversation-writer.mjs +2 -2
- package/src/tui/display-budget.mjs +9 -9
- package/src/tui/fold-block.mjs +1 -1
- package/src/tui/index.mjs +23 -5
- package/src/tui/interaction.mjs +5 -3
- package/src/tui/key-handler-busy.mjs +17 -12
- package/src/tui/key-handler-ctrlc.mjs +5 -5
- package/src/tui/key-handler-edit.mjs +12 -8
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +2 -2
- package/src/tui/key-modes.mjs +1 -1
- package/src/tui/layout.mjs +5 -5
- package/src/tui/ledger-surface.mjs +26 -5
- package/src/tui/lifecycle-records.mjs +199 -0
- package/src/tui/model-picker.mjs +16 -199
- package/src/tui/mouse.mjs +5 -5
- package/src/tui/pickers.mjs +6 -2
- package/src/tui/provider-admin.mjs +213 -0
- package/src/tui/queued-merge.mjs +12 -0
- package/src/tui/queued-pickup.mjs +34 -0
- package/src/tui/render-conversation.mjs +49 -4
- package/src/tui/render-frame.mjs +54 -11
- package/src/tui/render-loop.mjs +2 -3
- package/src/tui/render-segments.mjs +4 -4
- package/src/tui/render.mjs +1 -1
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tui/startup.mjs +28 -3
- package/src/tui/subagent-blocks.mjs +37 -29
- package/src/tui/subagent-children.mjs +5 -5
- package/src/tui/subagent-freeze.mjs +43 -41
- package/src/tui/subagent-panel.mjs +11 -11
- package/src/tui/suspension-drive.mjs +173 -253
- package/src/tui/timer-watch.mjs +64 -0
- package/src/tui/tool-args.mjs +10 -71
- package/src/tui/tool-display.mjs +7 -7
- package/src/tui/tool-events.mjs +40 -35
- package/src/tui/tui-lifecycle.mjs +13 -4
- package/src/tui/tui-state.mjs +3 -2
- package/src/tui/turn-face.mjs +6 -3
- package/src/tui/wizard.mjs +7 -2
- package/src/tui/wrapped-spawn.mjs +3 -3
package/src/acp/bridge.mjs
CHANGED
|
@@ -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
|
|
141
|
-
*
|
|
142
|
-
* 读全部涉及文件缓冲(同文件去重——一次读)→ 逐条 computeEditEntry(
|
|
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
|
-
|
|
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(
|
|
157
|
-
validateEditEntry(e, { label:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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": {
|
package/src/cli/make-agent.mjs
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
config
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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()
|
package/src/cli/permission.mjs
CHANGED
|
@@ -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
|
+
}
|