thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/cli/setup-wizard.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
1
|
import { createInterface } from "node:readline"
|
|
3
|
-
import { configPath,
|
|
2
|
+
import { configPath, writeConfigAtomic, PROVIDER_PRESETS } from "../config.mjs"
|
|
3
|
+
import { probeChannelModels } from "../tui/model-catalog.mjs"
|
|
4
4
|
|
|
5
5
|
/** First-time setup (TTY chat / distill): ask a few questions to configure a provider, save to disk, return runtime provider. Cancel returns null. */
|
|
6
6
|
export async function setupWizard() {
|
|
@@ -29,7 +29,7 @@ export async function setupWizard() {
|
|
|
29
29
|
presets.forEach(([n, p], i) => console.error(` ${i + 1}. ${n.padEnd(10)} ${p.desc}`))
|
|
30
30
|
console.error(` ${presets.length + 1}. Custom endpoint`)
|
|
31
31
|
const choice = Number((await ask(`Pick [1-${presets.length + 1}]: `)).trim())
|
|
32
|
-
let name, baseURL, model
|
|
32
|
+
let name, baseURL, model, presetFields = null
|
|
33
33
|
if (choice === presets.length + 1) {
|
|
34
34
|
name = (await ask("Name (e.g. my-openai): ")).trim()
|
|
35
35
|
baseURL = (await ask("baseURL (e.g. https://api.openai.com/v1): ")).trim().replace(/\/+$/, "")
|
|
@@ -39,9 +39,11 @@ export async function setupWizard() {
|
|
|
39
39
|
return null
|
|
40
40
|
}
|
|
41
41
|
} else if (choice >= 1 && choice <= presets.length) {
|
|
42
|
+
// 预设全量字段随行(format/thinking/maxTokens…)——探针分派与落盘同源(M9 需要 format)
|
|
43
|
+
presetFields = presets[choice - 1][1]
|
|
42
44
|
name = presets[choice - 1][0]
|
|
43
|
-
baseURL =
|
|
44
|
-
model =
|
|
45
|
+
baseURL = presetFields.baseURL
|
|
46
|
+
model = presetFields.model
|
|
45
47
|
} else {
|
|
46
48
|
console.error("Invalid choice — cancelled")
|
|
47
49
|
return null
|
|
@@ -52,19 +54,42 @@ export async function setupWizard() {
|
|
|
52
54
|
return null
|
|
53
55
|
}
|
|
54
56
|
const embedKey = (await ask("Optional: embedding API key (SiliconFlow, for vector search; press Enter to skip): ")).trim()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
// M9 配置阶段准入(加渠道 = 配置写入面):探一次 `/models`——探通/探不通都不阻断保存,
|
|
58
|
+
// 仅明示结果(失败文案 = M8 消息本体;无绕过指引)。
|
|
59
|
+
// 探针输入带预设扩展字段(format 决定 M1 分派——claude/gemini 预设必须走自己的拉取分支)。
|
|
60
|
+
const probe = await probeChannelModels(presetFields ? { ...presetFields, name, apiKey } : { name, baseURL, apiKey })
|
|
61
|
+
console.error(probe.ok
|
|
62
|
+
? `✓ ${name}: /models 可用(${probe.list.length} 个模型可候选)`
|
|
63
|
+
: `⚠ ${probe.message}`)
|
|
64
|
+
// D-F5b:磁盘新鲜读 → mutate → mtime 门控写(writeConfigAtomic 收口);冲突 = 放弃
|
|
65
|
+
// + 提示重试(首配场景另有实例同时写盘——极低概率;不自动合并——决策点① A)
|
|
66
|
+
// MODEL-SELECTION v2:渠道默认模型 = 单值 `model` + defaultModel 顶层复合;
|
|
67
|
+
// 预设扩展字段(format/thinking/maxTokens…)随行落盘——与 TUI wizard 近似同构(探针/聊天字段同源),
|
|
68
|
+
// 但 `thinking` 复制语义不同:本文件用 `!== undefined` → kimi/kimi-code 预设的 `thinking: null`
|
|
69
|
+
// **被落盘**;TUI 路径用真值过滤(model-picker.mjs / wizard.mjs)不落该键。
|
|
70
|
+
// `thinking: null`(NF1 = 显式 off)落盘差异已登记待口径统一(父侧)。
|
|
71
|
+
const r = writeConfigAtomic(configPath, (raw) => {
|
|
72
|
+
const providers = raw.providers?.length ? raw.providers : []
|
|
73
|
+
const existing = providers.find((p) => p.name === name)
|
|
74
|
+
const rec = { name, baseURL, model, apiKey }
|
|
75
|
+
for (const k of ["format", "thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
|
|
76
|
+
if (presetFields?.[k] !== undefined) rec[k] = presetFields[k]
|
|
77
|
+
}
|
|
78
|
+
if (existing) Object.assign(existing, rec) // 渠道老字段(models 候选清单)由 config-migrate 在下次 load 统一清理
|
|
79
|
+
else providers.push(rec)
|
|
80
|
+
raw.providers = providers
|
|
81
|
+
raw.defaultModel = `${name}:${model}`
|
|
82
|
+
delete raw.activeProvider
|
|
83
|
+
delete raw.activeModel
|
|
84
|
+
if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
|
|
85
|
+
})
|
|
86
|
+
if (!r.ok) {
|
|
87
|
+
console.error("config changed on disk concurrently — retry")
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
console.error(`Configured: ${name} / ${model} (defaultModel = ${name}:${model} — saved to ${configPath})`)
|
|
66
91
|
console.error(embedKey ? "Vector search enabled\n" : "(No embedding key configured: memory search will use text-only FTS. Add embedding.apiKey to config.json to enable vector search later.)\n")
|
|
67
|
-
return { name, baseURL, model, apiKey }
|
|
92
|
+
return { name, baseURL, model, apiKey, defaultModel: `${name}:${model}` }
|
|
68
93
|
} finally {
|
|
69
94
|
rl.close()
|
|
70
95
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* completions.mjs — `thincoder completion <shell>` shell 补全脚本发射(bash/zsh/fish)。
|
|
3
|
+
* bin/thincoder.mjs case "completion" 直写段拆分(2026-09-08——500 行硬限触碰执行:
|
|
4
|
+
* 段逐字迁出,脚本字节与既有输出一致)。
|
|
5
|
+
*/
|
|
6
|
+
export function printCompletion(shell) {
|
|
7
|
+
if (shell === "bash") {
|
|
8
|
+
process.stdout.write(`_thincoder() {
|
|
9
|
+
local cur prev words cword
|
|
10
|
+
_init_completion 2>/dev/null || { COMPREPLY=(); return; }
|
|
11
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
12
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
13
|
+
case "\${COMP_WORDS[1]}" in
|
|
14
|
+
chat) COMPREPLY=( \\$(compgen -W "--auto" -- "\\$cur") ) ;;
|
|
15
|
+
memory)
|
|
16
|
+
case "\\$prev" in
|
|
17
|
+
memory) COMPREPLY=( \\$(compgen -W "list search put remove" -- "\\$cur") ) ;;
|
|
18
|
+
list) COMPREPLY=( \\$(compgen -W "--type=rule --type=knowledge --type=decision --type=pattern" -- "\\$cur") ) ;;
|
|
19
|
+
put) COMPREPLY=( \\$(compgen -W "--type= --title= --content= --tags=" -- "\\$cur") ) ;;
|
|
20
|
+
esac ;;
|
|
21
|
+
distill) COMPREPLY=( \\$(compgen -W "--yes --layer=" -- "\\$cur") ) ;;
|
|
22
|
+
completion) COMPREPLY=( \\$(compgen -W "bash zsh fish" -- "\\$cur") ) ;;
|
|
23
|
+
*)
|
|
24
|
+
COMPREPLY=( \\$(compgen -W "chat acp memory sync reindex distill upgrade completion session -v --version -h --help" -- "\\$cur") ) ;;
|
|
25
|
+
esac
|
|
26
|
+
}
|
|
27
|
+
complete -F _thincoder thincoder
|
|
28
|
+
`)
|
|
29
|
+
} else if (shell === "zsh") {
|
|
30
|
+
process.stdout.write(`#compdef thincoder
|
|
31
|
+
|
|
32
|
+
_thincoder() {
|
|
33
|
+
local context state state_descr line
|
|
34
|
+
typeset -A opt_args
|
|
35
|
+
_arguments -C \\
|
|
36
|
+
'1: :->cmd' \\
|
|
37
|
+
'*:: :->args'
|
|
38
|
+
|
|
39
|
+
case "\\$state" in
|
|
40
|
+
cmd)
|
|
41
|
+
_values 'command' \\
|
|
42
|
+
'chat[One-shot agent run with tools]' \\
|
|
43
|
+
'acp[Agent Client Protocol server for IDEs]' \\
|
|
44
|
+
'memory[Manage long-term memory]' \\
|
|
45
|
+
'sync[Sync team memory repo]' \\
|
|
46
|
+
'reindex[Rebuild local index from markdown]' \\
|
|
47
|
+
'distill[Extract knowledge from session transcript]' \\
|
|
48
|
+
'upgrade[Update to latest version from npm]' \\
|
|
49
|
+
'completion[Generate shell completion script]' \\
|
|
50
|
+
'session[Session dir GC: session gc --dry-run|--confirm]'
|
|
51
|
+
;;
|
|
52
|
+
args)
|
|
53
|
+
case "\\$words[1]" in
|
|
54
|
+
chat) _arguments '--auto[Auto-approve all tool calls]' ;;
|
|
55
|
+
memory)
|
|
56
|
+
case "\\$words[2]" in
|
|
57
|
+
list) _arguments '--type=[Filter by type]' ;;
|
|
58
|
+
put) _arguments '--type=[Entry type]' '--title=[Title]' '--content=[Content]' '--tags=[Space-separated tags]' ;;
|
|
59
|
+
esac ;;
|
|
60
|
+
distill) _arguments '--yes[Skip confirmation]' '--layer=[Layer filter]' ;;
|
|
61
|
+
completion) _values 'shell' 'bash' 'zsh' 'fish' ;;
|
|
62
|
+
esac ;;
|
|
63
|
+
esac
|
|
64
|
+
}
|
|
65
|
+
_thincoder
|
|
66
|
+
`)
|
|
67
|
+
} else if (shell === "fish") {
|
|
68
|
+
process.stdout.write(`# thincoder completions for fish shell
|
|
69
|
+
complete -c thincoder -f
|
|
70
|
+
|
|
71
|
+
# Subcommands
|
|
72
|
+
complete -c thincoder -a chat -d 'One-shot agent run with tools'
|
|
73
|
+
complete -c thincoder -a memory -d 'Manage long-term memory'
|
|
74
|
+
complete -c thincoder -a sync -d 'Sync team memory repo'
|
|
75
|
+
complete -c thincoder -a reindex -d 'Rebuild local index from markdown'
|
|
76
|
+
complete -c thincoder -a distill -d 'Extract knowledge from session'
|
|
77
|
+
complete -c thincoder -a upgrade -d 'Update to latest version'
|
|
78
|
+
complete -c thincoder -a completion -d 'Shell completion'
|
|
79
|
+
complete -c thincoder -a session -d 'Session dir GC (session gc --dry-run|--confirm)'
|
|
80
|
+
complete -c thincoder -a acp -d 'Agent Client Protocol server for IDEs'
|
|
81
|
+
|
|
82
|
+
# Flags
|
|
83
|
+
complete -c thincoder -s v -l version -d 'Print version'
|
|
84
|
+
complete -c thincoder -s h -l help -d 'Print help'
|
|
85
|
+
|
|
86
|
+
# chat flags
|
|
87
|
+
complete -c thincoder -n '__fish_seen_subcommand_from chat' -l auto -d 'Auto-approve tool calls'
|
|
88
|
+
|
|
89
|
+
# memory subcommands
|
|
90
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a list -d 'List entries'
|
|
91
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a search -d 'Search memory'
|
|
92
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a put -d 'Add entry'
|
|
93
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a remove -d 'Remove entry'
|
|
94
|
+
|
|
95
|
+
# memory list flags
|
|
96
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from list' -l type -d 'Filter by type' -xa 'rule knowledge decision pattern'
|
|
97
|
+
|
|
98
|
+
# memory put flags
|
|
99
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l type -d 'Entry type'
|
|
100
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l title -d 'Title'
|
|
101
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l content -d 'Content'
|
|
102
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l tags -d 'Space-separated tags'
|
|
103
|
+
|
|
104
|
+
# distill flags
|
|
105
|
+
complete -c thincoder -n '__fish_seen_subcommand_from distill' -l yes -d 'Skip confirmation'
|
|
106
|
+
complete -c thincoder -n '__fish_seen_subcommand_from distill' -l layer -d 'Layer filter'
|
|
107
|
+
|
|
108
|
+
# completion shells
|
|
109
|
+
complete -c thincoder -n '__fish_seen_subcommand_from completion' -a bash -d 'Bash completions'
|
|
110
|
+
complete -c thincoder -n '__fish_seen_subcommand_from completion' -a zsh -d 'Zsh completions'
|
|
111
|
+
complete -c thincoder -n '__fish_seen_subcommand_from completion' -a fish -d 'Fish completions'
|
|
112
|
+
`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-migrate.mjs — model-schema legacy-shape migration core (MODEL-SELECTION v2, §16.2 M7).
|
|
3
|
+
* Split out of config.mjs (500-line hard limit). Pure function, zero imports — the
|
|
4
|
+
* write-back orchestration (writeConfigAtomic / persistRaw, failure tolerance) stays in
|
|
5
|
+
* the caller (CLI: config.mjs loadConfig; VSC: config-io.mjs loadRaw — same rule, 双端各自
|
|
6
|
+
* 独立实现——不做同步依赖).
|
|
7
|
+
*
|
|
8
|
+
* 目标形态 C:每渠道恰好一个默认模型(`providers[].model` 单值)+ 顶层 `defaultModel` 复合。
|
|
9
|
+
* 老形态迁移(v2):
|
|
10
|
+
* - A:`providers[].model` + `activeProvider/activeModel` → `p.model` **保留**(不再搬入 models);
|
|
11
|
+
* `activeProvider/activeModel` → `defaultModel` 复合(`activeModel` 优先;AP 不存在/AM 空 → 首渠道回退)
|
|
12
|
+
* - B:`providers[].models[]` → `p.model = defaultModel 属本渠道的模型段 ?? 现有 p.model(非空字符串)
|
|
13
|
+
* ?? models[] 首个非空字符串`,然后 `delete p.models`
|
|
14
|
+
* - 混合/垃圾:非数组 `p.models` / 非字符串(空串)`p.model` → 删除(清理)
|
|
15
|
+
* - 顺序:先构造/读取有效 `defaultModel` 值(含老 active* 转换),再按它给各渠道播种 `p.model`
|
|
16
|
+
* - 幂等:无老字段(无 `models` / 无 active* / 无垃圾值)→ 返回 false 不动;slot 旧字段
|
|
17
|
+
* (会话文件内)不迁移——读侧容忍
|
|
18
|
+
* - 空结果合法:渠道无模型来源 → `p.model` 不设(模型选择经 `/models` 拉取候选——M8/M9)
|
|
19
|
+
*/
|
|
20
|
+
export function migrateLegacyModelFields(raw) {
|
|
21
|
+
if (!raw || typeof raw !== "object") return false
|
|
22
|
+
const providers = Array.isArray(raw.providers) ? raw.providers : []
|
|
23
|
+
let changed = false
|
|
24
|
+
|
|
25
|
+
// ── ① 有效 defaultModel 值(含老 active* 转换)──
|
|
26
|
+
if (raw.activeProvider !== undefined || raw.activeModel !== undefined) {
|
|
27
|
+
const rawAP = typeof raw.activeProvider === "string" && raw.activeProvider.trim() ? raw.activeProvider : ""
|
|
28
|
+
const rawAM = typeof raw.activeModel === "string" && raw.activeModel.trim() ? raw.activeModel : ""
|
|
29
|
+
const ap = rawAP ? providers.find((p) => p?.name === rawAP) : null
|
|
30
|
+
if (ap && rawAM) {
|
|
31
|
+
if (raw.defaultModel == null) { raw.defaultModel = `${rawAP}:${rawAM}`; changed = true }
|
|
32
|
+
} else {
|
|
33
|
+
// AP 已不存在 / AM 无法归属 → 首渠道回退(该渠道有模型来源才可用——迁移产物必可用)
|
|
34
|
+
const first = providers.find((p) => seedModel(p) != null)
|
|
35
|
+
if (first && raw.defaultModel == null) { raw.defaultModel = `${first.name}:${seedModel(first)}`; changed = true }
|
|
36
|
+
}
|
|
37
|
+
delete raw.activeProvider
|
|
38
|
+
delete raw.activeModel
|
|
39
|
+
changed = true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── ② 渠道播种单值 `p.model` + 清 models ──
|
|
43
|
+
const dm = typeof raw.defaultModel === "string" && raw.defaultModel.trim() ? raw.defaultModel : null
|
|
44
|
+
const dmSep = dm ? dm.indexOf(":") : -1
|
|
45
|
+
const dmProvider = dmSep > 0 ? dm.slice(0, dmSep) : null
|
|
46
|
+
const dmModel = dmSep > 0 && dmSep < dm.length - 1 ? dm.slice(dmSep + 1) : null
|
|
47
|
+
for (const p of providers) {
|
|
48
|
+
if (!p || typeof p !== "object") continue
|
|
49
|
+
if (Array.isArray(p.models)) {
|
|
50
|
+
const next = (p.name === dmProvider && dmModel)
|
|
51
|
+
? dmModel
|
|
52
|
+
: (typeof p.model === "string" && p.model.trim() ? p.model : seedModel(p))
|
|
53
|
+
if (next == null) delete p.model
|
|
54
|
+
else p.model = next
|
|
55
|
+
delete p.models
|
|
56
|
+
changed = true
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (p.models !== undefined) { delete p.models; changed = true } // 非数组 models —— 垃圾清理
|
|
60
|
+
if (p.model !== undefined && !(typeof p.model === "string" && p.model.trim())) { delete p.model; changed = true }
|
|
61
|
+
}
|
|
62
|
+
return changed
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 渠道级模型来源读数(迁移用):现有 `p.model`(非空字符串优先)或 `models[]` 首个非空字符串。 */
|
|
66
|
+
function seedModel(p) {
|
|
67
|
+
if (typeof p?.model === "string" && p.model.trim()) return p.model
|
|
68
|
+
const models = Array.isArray(p?.models) ? p.models : []
|
|
69
|
+
return models.find((m) => typeof m === "string" && m.trim()) ?? null
|
|
70
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config.mjs — configuration loading and saving
|
|
3
|
-
*
|
|
3
|
+
* Model-merge schema v2 (2026-09-10 MODEL-SELECTION): providers[] carry ONE default model
|
|
4
|
+
* per channel (`providers[].model` — the new-install seed / empty-slot fallback); the
|
|
5
|
+
* candidates list field models[] is gone — the available-model list is fetched from the
|
|
6
|
+
* provider at runtime (`GET /models`, PROVIDER.md §16). config.defaultModel (top level,
|
|
7
|
+
* "provider:model" composite) is the new-session starting point; activeProvider/activeModel
|
|
8
|
+
* are gone from config (session slots keep their own double fields). Legacy fields migrate
|
|
9
|
+
* on load (write-back failure never blocks startup).
|
|
4
10
|
* Config file: ~/.thincoder/config.json
|
|
5
11
|
* API key can fall back to environment variables (when not configured in providers).
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
14
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
9
15
|
import { homedir } from "node:os"
|
|
10
|
-
import { join } from "node:path"
|
|
16
|
+
import { dirname, join } from "node:path"
|
|
17
|
+
import { parseModelRef, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
|
|
18
|
+
// 老形态迁移核(M7 v2——纯函数零依赖——本文件超 500 行硬限拆分,VSC 同构文件)
|
|
19
|
+
import { migrateLegacyModelFields } from "./config-migrate.mjs"
|
|
20
|
+
import { expandHome } from "./expand-home.mjs"
|
|
21
|
+
|
|
22
|
+
export { parseModelRef, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
|
|
11
23
|
|
|
12
24
|
export const configDir = join(homedir(), ".thincoder")
|
|
13
25
|
export const configPath = join(configDir, "config.json")
|
|
14
26
|
|
|
15
|
-
/**
|
|
27
|
+
/** Test seam: override the config file location (mirrors thincoder-vscode config-io.mjs). */
|
|
28
|
+
let _pathOverride = null
|
|
29
|
+
export function _setConfigPathForTest(p) { _pathOverride = p }
|
|
30
|
+
export function _resetConfigPathForTest() { _pathOverride = null }
|
|
31
|
+
function cfgPath() { return _pathOverride ?? configPath }
|
|
32
|
+
|
|
33
|
+
/** Built-in provider presets: shared by /provider add <preset> and first-run wizard.
|
|
34
|
+
* MODEL-SELECTION v2 (2026-09-10): each preset carries exactly ONE default model
|
|
35
|
+
* (`model` — 新装启动种子/槽位空兜底; 候选清单不再预置——运行期从 provider 拉取). */
|
|
16
36
|
export const PROVIDER_PRESETS = {
|
|
17
|
-
deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-
|
|
37
|
+
deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-flash", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 393216, desc: "DeepSeek" },
|
|
18
38
|
kimi: { baseURL: "https://api.moonshot.cn/v1", model: "kimi-k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
|
|
19
39
|
"kimi-code": { baseURL: "https://api.kimi.com/coding/v1", model: "k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi For Coding (platform.kimi.com — sk-kimi- keys; NOT interchangeable with Moonshot)" },
|
|
20
40
|
glm: { baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM" },
|
|
@@ -37,7 +57,7 @@ export const PROVIDER_PRESETS = {
|
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
export const DEFAULTS = {
|
|
40
|
-
|
|
60
|
+
defaultModel: null, // top-level "provider:model" composite — new-session starting point (F-1)
|
|
41
61
|
agent: {
|
|
42
62
|
maxTurns: 200,
|
|
43
63
|
subagentTurns: 100,
|
|
@@ -54,7 +74,19 @@ export const DEFAULTS = {
|
|
|
54
74
|
streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
|
|
55
75
|
advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
|
|
56
76
|
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
57
|
-
engineering: false, // strict methodology enforcement —
|
|
77
|
+
engineering: false, // strict methodology enforcement — design-before-code (design review + user approval before code)
|
|
78
|
+
// Async pool limits (AGENT-LOOP.md §11.1 D-24a/R14 + §11.2 R13 — POOL-CONFIG-
|
|
79
|
+
// UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
|
|
80
|
+
// pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
|
|
81
|
+
// 4 路" + advisor 评审池并入同一可配体系——三池统一默认 4)。engCoder/other
|
|
82
|
+
// runtime-validated at every subagent pool admission (subagent-async
|
|
83
|
+
// resolvePoolLimits); advisor runtime-validated at every advisor launch
|
|
84
|
+
// (advisor-async advisorPoolLimitFor——F-2——两域读取器独立不共享)——非法/缺省
|
|
85
|
+
// 回退 4——settings tool 与 /config 并发池菜单写此键——变更下个 spawn/launch 生效。
|
|
86
|
+
// ⚠ 与 subagent-async.mjs ASYNC_POOL_LIMITS(两键)/advisor-async.mjs
|
|
87
|
+
// ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
|
|
88
|
+
// 勿单侧改默认。
|
|
89
|
+
poolLimits: { engCoder: 4, other: 4, advisor: 4 },
|
|
58
90
|
},
|
|
59
91
|
memory: {
|
|
60
92
|
dbPath: join(configDir, "memory.db"),
|
|
@@ -70,7 +102,7 @@ export const DEFAULTS = {
|
|
|
70
102
|
servers: [],
|
|
71
103
|
},
|
|
72
104
|
websearch: {
|
|
73
|
-
|
|
105
|
+
// Structured search via Tavily when a key is set — empty apiKey → Bing RSS/HTML fallback (zero-config).
|
|
74
106
|
apiKey: "", // Tavily key (tvly-...) — optional
|
|
75
107
|
},
|
|
76
108
|
traces: {
|
|
@@ -82,8 +114,8 @@ export const DEFAULTS = {
|
|
|
82
114
|
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
83
115
|
// extract — config.mjs had grown past the 300-line advisory). Re-exported here
|
|
84
116
|
// so the 23 existing importers keep their import paths.
|
|
85
|
-
import { specForModel, providerSpec } from "./model-specs.mjs"
|
|
86
|
-
export { specForModel, providerSpec }
|
|
117
|
+
import { specForModel, providerSpec, specMatch } from "./model-specs.mjs"
|
|
118
|
+
export { specForModel, providerSpec, specMatch }
|
|
87
119
|
|
|
88
120
|
|
|
89
121
|
// Window utilization threshold: compacts at 60% context, reserving 40% headroom
|
|
@@ -136,6 +168,44 @@ export function resolveEnableThinking(provider, spec) {
|
|
|
136
168
|
/** Module-level one-time warn dedupe for invalid providers[].context (PROVIDER.md §15 D-C1). */
|
|
137
169
|
const warnedContextProviders = new Set()
|
|
138
170
|
|
|
171
|
+
/** F-4 (IKCDMR) 软失败清洗(D-S1 范式——loadConfig 是 CLI 启动砖点):consultModels 非法
|
|
172
|
+
* 条目过滤不 throw——非数组 → []、形状非法/未知渠道丢弃、超 5 截断(keep 前 5)。
|
|
173
|
+
* 返回 { keep, dropped }——dropped 供一次性警告;merged.agent.consultModelsFiltered 挂载
|
|
174
|
+
* 供后续首帧引导消费(数据层——本批文件面无消费方——同 providerInvalidReason 载体先例)。 */
|
|
175
|
+
function sanitizeConsultModels(cm, providerNames) {
|
|
176
|
+
if (cm === undefined || cm === null) return { keep: [], dropped: [] }
|
|
177
|
+
if (!Array.isArray(cm)) {
|
|
178
|
+
return { keep: [], dropped: [`agent.consultModels must be an array of { provider, model } entries (got ${typeof cm})`] }
|
|
179
|
+
}
|
|
180
|
+
const names = new Set(providerNames)
|
|
181
|
+
const keep = []
|
|
182
|
+
const dropped = []
|
|
183
|
+
for (const entry of cm) {
|
|
184
|
+
if (keep.length >= 5) { dropped.push(`over the 5-entry cap — dropped ${JSON.stringify(entry)}`); continue }
|
|
185
|
+
if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || !entry.provider.trim()
|
|
186
|
+
|| typeof entry.model !== "string" || !entry.model.trim()) {
|
|
187
|
+
dropped.push(`invalid entry (expected { provider: string, model: string }) — got ${JSON.stringify(entry)}`)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (!names.has(entry.provider)) {
|
|
191
|
+
dropped.push(`entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${[...names].join(", ") || "none"})`)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
keep.push(entry)
|
|
195
|
+
}
|
|
196
|
+
return { keep, dropped }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** F-4:过滤警告——进程级一次性(loadConfig 同进程可多次调用:TUI 启动 + reloadConfig)。 */
|
|
200
|
+
let warnedConsultModels = false
|
|
201
|
+
function warnConsultModelsFiltered(dropped, path) {
|
|
202
|
+
if (warnedConsultModels || dropped.length === 0) return
|
|
203
|
+
warnedConsultModels = true
|
|
204
|
+
console.warn(`[config] agent.consultModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
|
|
205
|
+
dropped.map((d) => ` - ${d}`).join("\n") +
|
|
206
|
+
`\n Fix: clean agent.consultModels in ${path} or use /config → consult/escalate pool menu.`)
|
|
207
|
+
}
|
|
208
|
+
|
|
139
209
|
/**
|
|
140
210
|
* Find provider by name in providers[].
|
|
141
211
|
* Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
|
|
@@ -179,29 +249,58 @@ function sanitizeProviderHeaders(p) {
|
|
|
179
249
|
return p
|
|
180
250
|
}
|
|
181
251
|
|
|
252
|
+
/** loadConfig 内联迁移核已迁 config-migrate.mjs(500 行硬限拆分——VSC 同构)——
|
|
253
|
+
* migrateLegacyModelFields 纯函数 + 本文件 import/写回编排(折中 C 见其头注释)。 */
|
|
182
254
|
export function loadConfig() {
|
|
183
255
|
let config = {}
|
|
184
|
-
|
|
256
|
+
const path = cfgPath()
|
|
257
|
+
if (existsSync(path)) {
|
|
185
258
|
try {
|
|
186
|
-
config = JSON.parse(readFileSync(
|
|
259
|
+
config = JSON.parse(readFileSync(path, "utf8"))
|
|
187
260
|
} catch (error) {
|
|
188
|
-
throw new Error(`Config file is not valid JSON, check or delete it: ${
|
|
261
|
+
throw new Error(`Config file is not valid JSON, check or delete it: ${path}\n ${error.message}`, { cause: error })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── 迁移(M7 v2):检测老字段(models[] / active* / 垃圾值)→ 内存迁移态先行;写回失败绝不阻断(下次 load 重试——幂等)──
|
|
266
|
+
if (migrateLegacyModelFields(config)) {
|
|
267
|
+
try {
|
|
268
|
+
const r = writeConfigAtomic(path, migrateLegacyModelFields) // 磁盘 fresh raw 同变换
|
|
269
|
+
if (r.ok === false) console.warn(`[config] migration write-back skipped (${r.reason}) — memory state continues, retried on next load`)
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.warn(`[config] migration write-back failed — memory state continues, retried on next load: ${e.message}`)
|
|
189
272
|
}
|
|
190
273
|
}
|
|
191
274
|
|
|
192
275
|
const merged = {
|
|
193
276
|
...DEFAULTS,
|
|
194
277
|
...config,
|
|
278
|
+
defaultModel: typeof config.defaultModel === "string" && config.defaultModel.trim() ? config.defaultModel : null,
|
|
195
279
|
providers: Array.isArray(config.providers)
|
|
196
280
|
? config.providers.map((p) => sanitizeProviderHeaders({ ...p }))
|
|
197
281
|
: [],
|
|
198
|
-
activeProvider: config.activeProvider ?? "",
|
|
199
282
|
agent: { ...DEFAULTS.agent, ...config.agent },
|
|
200
283
|
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
201
284
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
202
285
|
traces: { ...DEFAULTS.traces, ...config.traces },
|
|
203
286
|
}
|
|
204
287
|
|
|
288
|
+
// 家目录展开(第 29 批):config 路径字段单一规范化点——只读归一(磁盘原文保留)
|
|
289
|
+
merged.memory.dbPath = expandHome(merged.memory.dbPath)
|
|
290
|
+
merged.memory.projectDir = expandHome(merged.memory.projectDir)
|
|
291
|
+
const team = merged.memory.team
|
|
292
|
+
if (team && typeof team === "object" && !Array.isArray(team) && team.dir !== undefined) {
|
|
293
|
+
merged.memory.team = { ...team, dir: expandHome(team.dir) } // 无 dir 键不注入(零键面变化)
|
|
294
|
+
}
|
|
295
|
+
merged.shell = expandHome(merged.shell)
|
|
296
|
+
|
|
297
|
+
// providers[].model 内存归一(v2 M3:非空字符串保留;非字符串/空串归一删除)——渠道默认模型
|
|
298
|
+
// 单值;无默认模型合法(模型选择经 /models 拉取候选——准入判据见 M8/M9)。
|
|
299
|
+
for (const p of merged.providers) {
|
|
300
|
+
if (typeof p.model === "string" && p.model.trim()) continue
|
|
301
|
+
if (p.model !== undefined) delete p.model
|
|
302
|
+
}
|
|
303
|
+
|
|
205
304
|
// providers[].context (K units, PROVIDER.md §15 D-C1): positive integer only — invalid
|
|
206
305
|
// values (0/negative/non-numeric) are IGNORED (spec value applies) with a ONE-TIME warn
|
|
207
306
|
// per provider name (module-level dedupe, same precedent as warnedModels in model-specs.mjs).
|
|
@@ -215,27 +314,15 @@ export function loadConfig() {
|
|
|
215
314
|
delete p.context
|
|
216
315
|
}
|
|
217
316
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
// Fail fast at load: a pool entry whose provider doesn't exist in providers[] fails
|
|
228
|
-
// every consult/escalate call at runtime with a quiet error string (eats a turn).
|
|
229
|
-
const providerNames = merged.providers.map((p) => p.name)
|
|
230
|
-
for (const entry of cm) {
|
|
231
|
-
if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || typeof entry.model !== "string") {
|
|
232
|
-
throw new Error(`agent.consultModels entries must be { provider: string, model: string } objects (got ${JSON.stringify(entry)})`)
|
|
233
|
-
}
|
|
234
|
-
if (!providerNames.includes(entry.provider)) {
|
|
235
|
-
throw new Error(`agent.consultModels entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${providerNames.join(", ") || "none"})`)
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
317
|
+
// F-4 (IKCDMR) consultModels 软失败化——D-S1 范式(defaultModel 无效同族):非法条目
|
|
318
|
+
// 过滤不 throw(去 startup brick——无修复入口的硬崩消);一次性启动警告内嵌修复指引
|
|
319
|
+
// (引导清条目载体——保留 discoverability);过滤记录挂 merged.agent.consultModelsFiltered
|
|
320
|
+
// (数据层——consumer-ready——首帧 UI 消费点同 D-S1 promptProviderIfInvalid 属后续批)。
|
|
321
|
+
// VSC loadAgentSettings 同规则(共享 config——双端锁步)。
|
|
322
|
+
const cmClean = sanitizeConsultModels(merged.agent.consultModels, merged.providers.map((p) => p.name))
|
|
323
|
+
warnConsultModelsFiltered(cmClean.dropped, path)
|
|
324
|
+
merged.agent.consultModels = cmClean.keep
|
|
325
|
+
if (cmClean.dropped.length) merged.agent.consultModelsFiltered = cmClean.dropped
|
|
239
326
|
|
|
240
327
|
// Backward compatibility: promote root-level config fields to agent sub-object
|
|
241
328
|
if (config.verifyGuard !== undefined) {
|
|
@@ -251,35 +338,24 @@ export function loadConfig() {
|
|
|
251
338
|
// 保证 agent.config.proxy 永远是规范形态或 undefined
|
|
252
339
|
merged.proxy = normalizeProxy(merged.proxy)
|
|
253
340
|
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
} catch {
|
|
263
|
-
active = {}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Build runtime provider object (for agent.provider usage)
|
|
267
|
-
const runtimeProvider = { ...active }
|
|
268
|
-
|
|
269
|
-
// activeModel overrides provider's default model (config only)
|
|
270
|
-
if (merged.activeModel) runtimeProvider.model = merged.activeModel
|
|
271
|
-
merged.activeModel = merged.activeModel || null // normalize for agent.activeModel
|
|
341
|
+
// Runtime provider = config.defaultModel 复合解析(F-2——resolveRuntimeProvider)。
|
|
342
|
+
// 无效/未设 → {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打 _providerInvalid
|
|
343
|
+
// 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用 findProvider
|
|
344
|
+
// throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
345
|
+
merged.provider = resolveRuntimeProvider(merged.providers, merged.defaultModel)
|
|
346
|
+
merged.providerInvalidReason = merged.provider.name
|
|
347
|
+
? null
|
|
348
|
+
: defaultModelReason(merged.providers, merged.defaultModel)
|
|
272
349
|
|
|
273
350
|
// Compaction threshold follows the model (provider-level context override honored — providerSpec)
|
|
274
351
|
const explicitThreshold = config.agent?.compactThreshold
|
|
275
|
-
const { value, auto } = resolveCompactThreshold(explicitThreshold,
|
|
352
|
+
const { value, auto } = resolveCompactThreshold(explicitThreshold, merged.provider)
|
|
276
353
|
merged.agent.compactThreshold = value
|
|
277
354
|
merged.agent.compactThresholdAuto = auto
|
|
278
355
|
|
|
279
356
|
// Write back to merged for convenient access by upper layers
|
|
280
|
-
merged.provider = runtimeProvider
|
|
281
357
|
// fetch 超时可配置(2026-09-01:agent.fetchTimeoutMs——provider/core.mjs effectiveFetchTimeoutMs 消费)
|
|
282
|
-
|
|
358
|
+
merged.provider.fetchTimeoutMs = Number.isFinite(merged.agent?.fetchTimeoutMs) && merged.agent.fetchTimeoutMs > 0
|
|
283
359
|
? merged.agent.fetchTimeoutMs : undefined
|
|
284
360
|
merged.providersList = merged.providers
|
|
285
361
|
merged.advisor = { ...merged.agent.advisor } // promote for consistent access (decoupled copy)
|
|
@@ -336,7 +412,8 @@ function readMcpSection(path = configPath) {
|
|
|
336
412
|
if (!existsSync(path)) return { ok: true, servers: [] }
|
|
337
413
|
const raw = JSON.parse(readFileSync(path, "utf8"))
|
|
338
414
|
const servers = raw?.mcp?.servers
|
|
339
|
-
|
|
415
|
+
// 非数组 = 畸形磁盘配置(2.3 代码正确性批)——ok:false 走调用方既有畸形回退(reloadMcpFromDisk 早退)
|
|
416
|
+
if (servers !== undefined && !Array.isArray(servers)) return { ok: false, error: "mcp.servers must be an array" }
|
|
340
417
|
return { ok: true, servers: Array.isArray(servers) ? servers : [] }
|
|
341
418
|
} catch (error) {
|
|
342
419
|
return { ok: false, error: error?.message ?? String(error) }
|
|
@@ -368,12 +445,52 @@ function mcpFingerprint(s) {
|
|
|
368
445
|
}
|
|
369
446
|
|
|
370
447
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
448
|
+
* R10 F5(D-F5b,2026-09-06)——config.json 写前 mtime 门控收口函数(session-rename
|
|
449
|
+
* mtime-conflict 先例同型——MULTI-INSTANCE-COLLAB.md §2a.2)。所有 config.json 写点
|
|
450
|
+
* (config-helpers persistRaw / cmd-config saveProxy / cli setup-wizard / settings
|
|
451
|
+
* writeDisk)都经它落盘。
|
|
452
|
+
*
|
|
453
|
+
* 本函数持有整条「新鲜读 → mutate 单操作 → 写前重 stat → 写」链:
|
|
454
|
+
* - t0 = 写前重 stat 的比对基线,取在**新鲜读之前**(stat→read 序):若对端在本端
|
|
455
|
+
* stat 与 read 之间的微窗口写入,只会造成假冲突(放弃重试),绝不会带着旧内容覆盖
|
|
456
|
+
* 对端新值——read→stat 序存在漏检窗口(stat 已反映对端新 mtime → 门控放行旧内容)。
|
|
457
|
+
* - 写前重 stat ≠ t0 → **放弃**本次写(D-F5a 后各流已是 fresh 单操作语义,磁盘上对端
|
|
458
|
+
* 的新值保持在线不抹);先 copy `.bak-{ts}` 留现场(仅冲突时——config 低频写不膨胀;
|
|
459
|
+
* copy 而非 rename:冲突即放弃、本体不动,"保现场"是额外副本,非轮转腾位)。
|
|
460
|
+
* - 返回 { ok:false, reason:"mtime-conflict" },调用方提示 "config changed on disk
|
|
461
|
+
* concurrently — retry"——不自动合并(config 是用户显式操作——重试比猜测合并安全,
|
|
462
|
+
* 决策点① A)。
|
|
463
|
+
* - 文件缺失(首写)→ t0 = null;对端在本端读后创建 → null ≠ 新 mtime → 冲突放弃。
|
|
464
|
+
* - 畸形文件拒写(throw,绝不静默覆盖);写后 chmod 0600 尽力而为(saveConfig 旧语义)。
|
|
465
|
+
*
|
|
466
|
+
* @param path config.json 路径(生产默认 configPath;测试注入 tmp 路径)
|
|
467
|
+
* @param mutate 在磁盘新鲜 raw 上执行单操作的同步回调(如 push/splice/单字段补丁)
|
|
468
|
+
* @returns { ok: true } | { ok: false, reason: "mtime-conflict" }
|
|
373
469
|
*/
|
|
374
|
-
export function
|
|
375
|
-
|
|
470
|
+
export function writeConfigAtomic(path, mutate) {
|
|
471
|
+
const mtimeOf = (p) => {
|
|
472
|
+
try { return statSync(p).mtimeMs } catch { return null } // 缺失 → null(t0 比对基线)
|
|
473
|
+
}
|
|
474
|
+
const t0 = mtimeOf(path) // stat 先于 read(安全方向——见头注释)
|
|
475
|
+
const text = existsSync(path) ? readFileSync(path, "utf8") : null
|
|
476
|
+
let raw = {}
|
|
477
|
+
if (text !== null) {
|
|
478
|
+
try {
|
|
479
|
+
raw = JSON.parse(text)
|
|
480
|
+
} catch (error) {
|
|
481
|
+
throw new Error(`config file not parseable — refusing to overwrite: ${path} — ${error.message}`, { cause: error })
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
mutate(raw)
|
|
485
|
+
const t1 = mtimeOf(path)
|
|
486
|
+
if (t0 !== t1) {
|
|
487
|
+
// 对端在我们新鲜读后改过磁盘 → 放弃本次写(对端内容保持在线);.bak 副本留现场
|
|
488
|
+
try { if (existsSync(path)) copyFileSync(path, `${path}.bak-${Date.now()}`) } catch { /* 现场保留失败不阻断冲突报告 */ }
|
|
489
|
+
return { ok: false, reason: "mtime-conflict" }
|
|
490
|
+
}
|
|
491
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
376
492
|
// 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
|
|
377
|
-
writeFileSync(
|
|
378
|
-
try { chmodSync(
|
|
493
|
+
writeFileSync(path, JSON.stringify(raw, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
494
|
+
try { chmodSync(path, 0o600) } catch { /* may fail on Windows, ignore */ }
|
|
495
|
+
return { ok: true }
|
|
379
496
|
}
|