@thincoder/core 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
package/config.mjs
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.mjs — configuration loading and saving
|
|
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).
|
|
10
|
+
* Config file: ~/.thincoder/config.json
|
|
11
|
+
* API key comes from providers[].apiKey only — environment variables are not a key source.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
15
|
+
import { dirname, join } from "node:path"
|
|
16
|
+
import { parseModelRef, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
|
|
17
|
+
// 老形态迁移核(M7 v2——纯函数零依赖——本文件超 500 行硬限拆分,VSC 同构文件)
|
|
18
|
+
import { migrateLegacyModelFields } from "./config-migrate.mjs"
|
|
19
|
+
import { expandHome } from "./expand-home.mjs"
|
|
20
|
+
// 核内单一读写面 + 预设表(§2.5 #129 / #131——拆档以容纳 500 行硬限)
|
|
21
|
+
import {
|
|
22
|
+
PROVIDER_PRESETS, presetToEntry, configDir, configPath, _setConfigPathForTest, _resetConfigPathForTest,
|
|
23
|
+
_configPath, writeConfigAtomic,
|
|
24
|
+
} from "./config-io.mjs"
|
|
25
|
+
|
|
26
|
+
export { parseModelRef, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
|
|
27
|
+
export { PROVIDER_PRESETS, presetToEntry, configDir, configPath, _setConfigPathForTest, _resetConfigPathForTest }
|
|
28
|
+
// 核内单一写盘执行体(已迁 `config-io.mjs`)——本档 re-export 保持既有导入面(§2.5 #131)。
|
|
29
|
+
export { writeConfigAtomic }
|
|
30
|
+
|
|
31
|
+
function cfgPath() { return _configPath() }
|
|
32
|
+
|
|
33
|
+
export const DEFAULTS = {
|
|
34
|
+
defaultModel: null, // top-level "provider:model" composite — new-session starting point (F-1)
|
|
35
|
+
agent: {
|
|
36
|
+
maxTurns: 200,
|
|
37
|
+
subagentTurns: 100,
|
|
38
|
+
subagentModel: null, // default subagent model: "provider:model" | provider name | model name (parent provider); null = inherit parent provider
|
|
39
|
+
subagentModels: {}, // per-type override: { explore, plan, coder, "eng-coder" } — priority: subagent tool model arg > this[role] > subagentModel > parent provider
|
|
40
|
+
goalTurns: 200,
|
|
41
|
+
compactThreshold: 100000,
|
|
42
|
+
verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
|
|
43
|
+
// Multi-model consultation ("会诊") + escalate ("飞刀") — CLI parity with the VS Code plugin.
|
|
44
|
+
// consultModels: candidate pool for BOTH consult and escalate ({ provider, model, effort? }, up to 5).
|
|
45
|
+
consultModels: [],
|
|
46
|
+
consultTurns: 40, // per-consultant tool-turn budget (diagnosis tasks)
|
|
47
|
+
consultTimeoutMs: 600000, // wall-clock ceiling per consultant (10min)
|
|
48
|
+
streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
|
|
49
|
+
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)
|
|
50
|
+
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
51
|
+
engineering: false, // strict methodology enforcement — design-before-code (design review + user approval before code)
|
|
52
|
+
// Async pool limits (AGENT-LOOP-SUBAGENT.md §6.10 D-24a/R14 + R13 — POOL-CONFIG-
|
|
53
|
+
// UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
|
|
54
|
+
// pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
|
|
55
|
+
// 4 路" + advisor 评审池并入同一可配体系——三池统一默认 4)。engCoder/other
|
|
56
|
+
// runtime-validated at every subagent pool admission (subagent-async
|
|
57
|
+
// resolvePoolLimits); advisor runtime-validated at every advisor launch
|
|
58
|
+
// (advisor-async advisorPoolLimitFor——F-2——两域读取器独立不共享)——非法/缺省
|
|
59
|
+
// 回退 4——settings tool 与 /config 并发池菜单写此键——变更下个 spawn/launch 生效。
|
|
60
|
+
// ⚠ 与 subagent-async.mjs ASYNC_POOL_LIMITS(两键)/advisor-async.mjs
|
|
61
|
+
// ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
|
|
62
|
+
// 勿单侧改默认。
|
|
63
|
+
poolLimits: { engCoder: 4, other: 4, advisor: 4 },
|
|
64
|
+
},
|
|
65
|
+
memory: {
|
|
66
|
+
dbPath: join(configDir, "memory.db"),
|
|
67
|
+
projectDir: ".thincoder/memory",
|
|
68
|
+
team: null,
|
|
69
|
+
},
|
|
70
|
+
shell: null, // bash tool shell executable (e.g. "C:\\Program Files\\Git\\bin\\bash.exe" or "pwsh"); null = system default (cmd on Windows, /bin/sh elsewhere)
|
|
71
|
+
embedding: {
|
|
72
|
+
baseURL: "https://api.siliconflow.cn/v1",
|
|
73
|
+
model: "BAAI/bge-m3",
|
|
74
|
+
},
|
|
75
|
+
mcp: {
|
|
76
|
+
servers: [],
|
|
77
|
+
},
|
|
78
|
+
websearch: {
|
|
79
|
+
// Structured search via Tavily when a key is set — empty apiKey → Bing RSS/HTML fallback (zero-config).
|
|
80
|
+
apiKey: "", // Tavily key (tvly-...) — optional
|
|
81
|
+
},
|
|
82
|
+
traces: {
|
|
83
|
+
enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
|
|
84
|
+
retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
89
|
+
// extract — config.mjs had grown past the 300-line advisory). Re-exported here
|
|
90
|
+
// so the 23 existing importers keep their import paths.
|
|
91
|
+
import { specForModel, providerSpec, specMatch, assistantToolCallMessage } from "./model-specs.mjs"
|
|
92
|
+
export { specForModel, providerSpec, specMatch, assistantToolCallMessage }
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
// Window utilization threshold: compacts at 60% context, reserving 40% headroom
|
|
96
|
+
// for injected context (directory tree, git context, outline, project instructions,
|
|
97
|
+
// memory/doc search results) which can consume 30-50K tokens each turn.
|
|
98
|
+
const COMPACT_RATIO = 0.6
|
|
99
|
+
|
|
100
|
+
/** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority), otherwise auto-computed from model.
|
|
101
|
+
* Second param accepts EITHER a model name string (pure spec lookup — legacy caller:
|
|
102
|
+
* first-run wizard) OR a provider object (providerSpec — the providers[].context
|
|
103
|
+
* override in K units is honored, PROVIDER.md §15 T-C2). */
|
|
104
|
+
export function resolveCompactThreshold(explicit, modelOrProvider) {
|
|
105
|
+
if (explicit != null) return { value: explicit, auto: false }
|
|
106
|
+
const provider = typeof modelOrProvider === "string" ? { model: modelOrProvider } : (modelOrProvider ?? {})
|
|
107
|
+
const spec = providerSpec(provider)
|
|
108
|
+
const value = Math.floor(spec.context * COMPACT_RATIO)
|
|
109
|
+
return { value, auto: true }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Bailian (阿里云百炼) host check — enable_thinking is a Bailian-only extension parameter;
|
|
114
|
+
* sending it to other endpoints (kimi/glm/custom proxies) would pollute the request.
|
|
115
|
+
*/
|
|
116
|
+
export function isBailianHost(baseURL) {
|
|
117
|
+
return typeof baseURL === "string"
|
|
118
|
+
&& (baseURL.includes("dashscope.aliyuncs.com") || baseURL.includes(".maas.aliyuncs.com"))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the Bailian `enable_thinking` switch for qwen hybrid-thinking models (PROVIDER.md §12).
|
|
123
|
+
* qwen3.x on Bailian defaults to thinking ON, so an explicit off must send enable_thinking:false
|
|
124
|
+
* or the server silently keeps thinking. Whitelist: model name starts with "qwen" (excluding the
|
|
125
|
+
* non-thinking qwen3-coder line) AND the provider points at a Bailian host.
|
|
126
|
+
* provider.thinking === null → false (explicit off: /think off, panel off — NF1 convention)
|
|
127
|
+
* provider.reasoningEffort → true (effort tier implies thinking on; rides with reasoning_effort)
|
|
128
|
+
* otherwise → undefined (field omitted — server default stays, no behavior change)
|
|
129
|
+
* NOTE: spec carries no model field today — the name comes from provider.model (spec?.model is
|
|
130
|
+
* a forward-compatible fallback). Keep the body byte-aligned with thincoder-vscode config.mjs
|
|
131
|
+
* (cross-repo parity test compares them).
|
|
132
|
+
*/
|
|
133
|
+
export function resolveEnableThinking(provider, spec) {
|
|
134
|
+
const model = (provider?.model ?? spec?.model ?? "").toLowerCase()
|
|
135
|
+
if (!model.startsWith("qwen") || model.startsWith("qwen3-coder")) return undefined
|
|
136
|
+
if (!isBailianHost(provider?.baseURL)) return undefined
|
|
137
|
+
if (provider.thinking === null) return false
|
|
138
|
+
if (provider.reasoningEffort) return true
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Module-level one-time warn dedupe for invalid providers[].context (PROVIDER.md §15 D-C1). */
|
|
143
|
+
const warnedContextProviders = new Set()
|
|
144
|
+
|
|
145
|
+
/** F-4 (IKCDMR) 软失败清洗(D-S1 范式——loadConfig 是 CLI 启动砖点):consultModels 非法
|
|
146
|
+
* 条目过滤不 throw——非数组 → []、形状非法/未知渠道丢弃、超 5 截断(keep 前 5)。
|
|
147
|
+
* 返回 { keep, dropped }——dropped 供一次性警告;merged.agent.consultModelsFiltered 挂载
|
|
148
|
+
* 供后续首帧引导消费(数据层——本批文件面无消费方——同 providerInvalidReason 载体先例)。 */
|
|
149
|
+
function sanitizeConsultModels(cm, providerNames) {
|
|
150
|
+
if (cm === undefined || cm === null) return { keep: [], dropped: [] }
|
|
151
|
+
if (!Array.isArray(cm)) {
|
|
152
|
+
return { keep: [], dropped: [`agent.consultModels must be an array of { provider, model } entries (got ${typeof cm})`] }
|
|
153
|
+
}
|
|
154
|
+
const names = new Set(providerNames)
|
|
155
|
+
const keep = []
|
|
156
|
+
const dropped = []
|
|
157
|
+
for (const entry of cm) {
|
|
158
|
+
if (keep.length >= 5) { dropped.push(`over the 5-entry cap — dropped ${JSON.stringify(entry)}`); continue }
|
|
159
|
+
if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || !entry.provider.trim()
|
|
160
|
+
|| typeof entry.model !== "string" || !entry.model.trim()) {
|
|
161
|
+
dropped.push(`invalid entry (expected { provider: string, model: string }) — got ${JSON.stringify(entry)}`)
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
if (!names.has(entry.provider)) {
|
|
165
|
+
dropped.push(`entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${[...names].join(", ") || "none"})`)
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
keep.push(entry)
|
|
169
|
+
}
|
|
170
|
+
return { keep, dropped }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** F-4:过滤警告——进程级一次性(loadConfig 同进程可多次调用:TUI 启动 + reloadConfig)。 */
|
|
174
|
+
let warnedConsultModels = false
|
|
175
|
+
function warnConsultModelsFiltered(dropped, path) {
|
|
176
|
+
if (warnedConsultModels || dropped.length === 0) return
|
|
177
|
+
warnedConsultModels = true
|
|
178
|
+
console.warn(`[config] agent.consultModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
|
|
179
|
+
dropped.map((d) => ` - ${d}`).join("\n") +
|
|
180
|
+
`\n Fix: clean agent.consultModels in ${path} or use /config → consult/escalate pool menu.`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Find provider by name in providers[].
|
|
185
|
+
* 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.
|
|
186
|
+
* Returns the first provider when name is empty.
|
|
187
|
+
*/
|
|
188
|
+
export function findProvider(providers, name) {
|
|
189
|
+
if (name) {
|
|
190
|
+
const found = providers.find((p) => p.name === name)
|
|
191
|
+
if (found) return found
|
|
192
|
+
const available = providers.map((p) => p.name).join(", ") || "(empty)"
|
|
193
|
+
throw new Error(`activeProvider "${name}" not in providers list (available: ${available}); check for a typo in: ${configPath}`)
|
|
194
|
+
}
|
|
195
|
+
return providers[0] ?? { name: "default", baseURL: "", model: "" }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Normalize proxy config to { uri, web, model } or undefined (uri/url both accepted; invalid types dropped) */
|
|
199
|
+
export function normalizeProxy(proxy) {
|
|
200
|
+
if (typeof proxy === "string") return proxy ? { uri: proxy, web: true, model: false } : undefined
|
|
201
|
+
if (!proxy || typeof proxy !== "object" || Array.isArray(proxy)) return undefined
|
|
202
|
+
const uri = proxy.uri || proxy.url || ""
|
|
203
|
+
if (typeof uri !== "string" || !uri) return undefined
|
|
204
|
+
return { uri, web: proxy.web !== false, model: proxy.model === true }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Load configuration.
|
|
209
|
+
* No env-var overrides — config.json is the single source of truth
|
|
210
|
+
* (API keys, baseURL, model, activeProvider all come from the file).
|
|
211
|
+
*/
|
|
212
|
+
/** Keep only { header: "string value" } pairs from a provider's headers field — anything
|
|
213
|
+
* else (null, arrays, nested objects) is dropped so it can never reach a fetch call.
|
|
214
|
+
* Authorization is built-in and cannot be overridden from headers (core.mjs spreads first). */
|
|
215
|
+
function sanitizeProviderHeaders(p) {
|
|
216
|
+
if (p.headers == null || typeof p.headers !== "object" || Array.isArray(p.headers)) { delete p.headers; return p }
|
|
217
|
+
const clean = {}
|
|
218
|
+
for (const [k, v] of Object.entries(p.headers)) {
|
|
219
|
+
if (typeof v === "string" && k.toLowerCase() !== "authorization") clean[k] = v
|
|
220
|
+
}
|
|
221
|
+
if (Object.keys(clean).length > 0) p.headers = clean
|
|
222
|
+
else delete p.headers
|
|
223
|
+
return p
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** loadConfig 内联迁移核已迁 config-migrate.mjs(500 行硬限拆分——VSC 同构)——
|
|
227
|
+
* migrateLegacyModelFields 纯函数 + 本文件 import/写回编排(折中 C 见其头注释)。 */
|
|
228
|
+
export function loadConfig() {
|
|
229
|
+
let config = {}
|
|
230
|
+
const path = cfgPath()
|
|
231
|
+
if (existsSync(path)) {
|
|
232
|
+
try {
|
|
233
|
+
config = JSON.parse(readFileSync(path, "utf8"))
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(`Config file is not valid JSON, check or delete it: ${path}\n ${error.message}`, { cause: error })
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── 迁移(M7 v2):检测老字段(models[] / active* / 垃圾值)→ 内存迁移态先行;写回失败绝不阻断(下次 load 重试——幂等)──
|
|
240
|
+
if (migrateLegacyModelFields(config)) {
|
|
241
|
+
try {
|
|
242
|
+
const r = writeConfigAtomic(path, migrateLegacyModelFields) // 磁盘 fresh raw 同变换
|
|
243
|
+
if (r.ok === false) console.warn(`[config] migration write-back skipped (${r.reason}) — memory state continues, retried on next load`)
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.warn(`[config] migration write-back failed — memory state continues, retried on next load: ${e.message}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const merged = {
|
|
250
|
+
...DEFAULTS,
|
|
251
|
+
...config,
|
|
252
|
+
defaultModel: typeof config.defaultModel === "string" && config.defaultModel.trim() ? config.defaultModel : null,
|
|
253
|
+
providers: Array.isArray(config.providers)
|
|
254
|
+
? config.providers.map((p) => sanitizeProviderHeaders({ ...p }))
|
|
255
|
+
: [],
|
|
256
|
+
agent: { ...DEFAULTS.agent, ...config.agent },
|
|
257
|
+
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
258
|
+
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
259
|
+
traces: { ...DEFAULTS.traces, ...config.traces },
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 家目录展开(第 29 批):config 路径字段单一规范化点——只读归一(磁盘原文保留)
|
|
263
|
+
merged.memory.dbPath = expandHome(merged.memory.dbPath)
|
|
264
|
+
merged.memory.projectDir = expandHome(merged.memory.projectDir)
|
|
265
|
+
const team = merged.memory.team
|
|
266
|
+
if (team && typeof team === "object" && !Array.isArray(team) && team.dir !== undefined) {
|
|
267
|
+
merged.memory.team = { ...team, dir: expandHome(team.dir) } // 无 dir 键不注入(零键面变化)
|
|
268
|
+
}
|
|
269
|
+
merged.shell = expandHome(merged.shell)
|
|
270
|
+
|
|
271
|
+
// providers[].model 内存归一(v2 M3:非空字符串保留;非字符串/空串归一删除)——渠道默认模型
|
|
272
|
+
// 单值;无默认模型合法(模型选择经 /models 拉取候选——准入判据见 M8/M9)。
|
|
273
|
+
for (const p of merged.providers) {
|
|
274
|
+
if (typeof p.model === "string" && p.model.trim()) continue
|
|
275
|
+
if (p.model !== undefined) delete p.model
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// providers[].context (K units, PROVIDER.md §15 D-C1): positive integer only — invalid
|
|
279
|
+
// values (0/negative/non-numeric) are IGNORED (spec value applies) with a ONE-TIME warn
|
|
280
|
+
// per provider name (module-level dedupe, same precedent as warnedModels in model-specs.mjs).
|
|
281
|
+
for (const p of merged.providers) {
|
|
282
|
+
if (p.context === undefined) continue
|
|
283
|
+
if (Number.isInteger(Number(p.context)) && Number(p.context) > 0) { p.context = Number(p.context); continue } // 数字字符串("128")归一为数字——两端语义统一(code review #1)
|
|
284
|
+
if (!warnedContextProviders.has(p.name ?? "(unnamed)")) {
|
|
285
|
+
warnedContextProviders.add(p.name ?? "(unnamed)")
|
|
286
|
+
console.warn(`[config] provider "${p.name}" context must be a positive integer in K units (e.g. 128 = 128K) — got ${JSON.stringify(p.context)} — ignored, using the model spec value`)
|
|
287
|
+
}
|
|
288
|
+
delete p.context
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// F-4 (IKCDMR) consultModels 软失败化——D-S1 范式(defaultModel 无效同族):非法条目
|
|
292
|
+
// 过滤不 throw(去 startup brick——无修复入口的硬崩消);一次性启动警告内嵌修复指引
|
|
293
|
+
// (引导清条目载体——保留 discoverability);过滤记录挂 merged.agent.consultModelsFiltered
|
|
294
|
+
// (数据层——consumer-ready——首帧 UI 消费点同 D-S1 promptProviderIfInvalid 属后续批)。
|
|
295
|
+
// VSC loadAgentSettings 同规则(共享 config——双端锁步)。
|
|
296
|
+
const cmClean = sanitizeConsultModels(merged.agent.consultModels, merged.providers.map((p) => p.name))
|
|
297
|
+
warnConsultModelsFiltered(cmClean.dropped, path)
|
|
298
|
+
merged.agent.consultModels = cmClean.keep
|
|
299
|
+
if (cmClean.dropped.length) merged.agent.consultModelsFiltered = cmClean.dropped
|
|
300
|
+
|
|
301
|
+
// Backward compatibility: promote root-level config fields to agent sub-object
|
|
302
|
+
if (config.verifyGuard !== undefined) {
|
|
303
|
+
merged.agent.verifyGuard = config.verifyGuard
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Normalize baseURL trailing slash (prevents //chat/completions)
|
|
307
|
+
for (const p of merged.providers) {
|
|
308
|
+
if (p.baseURL) p.baseURL = p.baseURL.replace(/\/+$/, "")
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Normalize proxy: string → { uri, web:true, model:false }; object 补默认值;非法类型丢弃。
|
|
312
|
+
// 保证 agent.config.proxy 永远是规范形态或 undefined
|
|
313
|
+
merged.proxy = normalizeProxy(merged.proxy)
|
|
314
|
+
|
|
315
|
+
// Runtime provider = config.defaultModel 复合解析(F-2——resolveRuntimeProvider)。
|
|
316
|
+
// 无效/未设 → {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打 _providerInvalid
|
|
317
|
+
// 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用 findProvider
|
|
318
|
+
// throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
319
|
+
merged.provider = resolveRuntimeProvider(merged.providers, merged.defaultModel)
|
|
320
|
+
merged.providerInvalidReason = merged.provider.name
|
|
321
|
+
? null
|
|
322
|
+
: defaultModelReason(merged.providers, merged.defaultModel)
|
|
323
|
+
|
|
324
|
+
// Compaction threshold follows the model (provider-level context override honored — providerSpec)
|
|
325
|
+
const explicitThreshold = config.agent?.compactThreshold
|
|
326
|
+
const { value, auto } = resolveCompactThreshold(explicitThreshold, merged.provider)
|
|
327
|
+
merged.agent.compactThreshold = value
|
|
328
|
+
merged.agent.compactThresholdAuto = auto
|
|
329
|
+
|
|
330
|
+
// Write back to merged for convenient access by upper layers
|
|
331
|
+
// fetch 超时可配置(2026-09-01:agent.fetchTimeoutMs——provider/core.mjs effectiveFetchTimeoutMs 消费)
|
|
332
|
+
merged.provider.fetchTimeoutMs = Number.isFinite(merged.agent?.fetchTimeoutMs) && merged.agent.fetchTimeoutMs > 0
|
|
333
|
+
? merged.agent.fetchTimeoutMs : undefined
|
|
334
|
+
merged.providersList = merged.providers
|
|
335
|
+
merged.advisor = { ...merged.agent.advisor } // promote for consistent access (decoupled copy)
|
|
336
|
+
|
|
337
|
+
return merged
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* MCP.md §5 D-3 (2026-09-01): re-read config.json and replace ONLY the agent's mcp section
|
|
342
|
+
* — the agent 代配 closed loop (agent edits config.json with its edit tool, /mcp picks it
|
|
343
|
+
* up). Never touches other config sections (providers/activeProvider stay as loaded).
|
|
344
|
+
*
|
|
345
|
+
* Malformed disk config → memory state kept, { ok:false, error } returned (the /mcp menu
|
|
346
|
+
* shows "⚠ disk config unreadable"). Never throws.
|
|
347
|
+
*
|
|
348
|
+
* 对账 (reconciliation, MCP.md §5 D-3 / T23): returns which disk servers CHANGED
|
|
349
|
+
* (fingerprint differs) or are DELETED from disk while still connected — fingerprint =
|
|
350
|
+
* endpoint + token + headers/env key order. Existing connections are NOT torn down (an
|
|
351
|
+
* in-use server must not be dropped): a deleted-but-connected server KEEPS its memory
|
|
352
|
+
* entry (appended after the disk list) so the /mcp list can still show the row with the
|
|
353
|
+
* "⚠ disk changed" mark. A server that is merely NEW on disk is not drift. persistRaw
|
|
354
|
+
* write + reload is idempotent (fingerprints equal → no drift mark).
|
|
355
|
+
*
|
|
356
|
+
* @param path optional config path override (tests inject a tmp file; default configPath)
|
|
357
|
+
*/
|
|
358
|
+
export function reloadMcpFromDisk(agent, path) {
|
|
359
|
+
const memoryServers = Array.isArray(agent.config?.mcp?.servers) ? agent.config.mcp.servers : []
|
|
360
|
+
const fileExists = existsSync(path ?? configPath)
|
|
361
|
+
const diskMcp = readMcpSection(path)
|
|
362
|
+
if (!diskMcp.ok) return { ok: false, error: diskMcp.error, changedNames: [] }
|
|
363
|
+
// Missing/deleted config file → keep whichever mcp servers the session had (never
|
|
364
|
+
// silently drop user servers because the file vanished — same memory-keeps policy
|
|
365
|
+
// as the malformed-disk fallback).
|
|
366
|
+
let diskServers = diskMcp.servers
|
|
367
|
+
if (diskServers.length === 0 && !fileExists) diskServers = memoryServers
|
|
368
|
+
// Drift vs the RAW disk list: fingerprint-changed or deleted-from-disk (T23 ⚠ 标记依据)
|
|
369
|
+
const diskNames = new Set(diskServers.filter((s) => s?.name).map((s) => s.name))
|
|
370
|
+
const changedNames = diffMcpServers(memoryServers, diskServers)
|
|
371
|
+
// Connected servers deleted from disk stay in the list (memory copy) — T23: the row
|
|
372
|
+
// must remain visible (marked ⚠) and its live connection untouched. They are already
|
|
373
|
+
// in changedNames (absent from disk), and stay flagged on every reload until the user
|
|
374
|
+
// reconnects (re-persists them) or removes them — real drift, honestly reported.
|
|
375
|
+
const connectedNames = new Set((agent.tools ?? []).filter((t) => t?._mcpName).map((t) => t._mcpName))
|
|
376
|
+
const keptConnected = memoryServers.filter((s) => s?.name && connectedNames.has(s.name) && !diskNames.has(s.name))
|
|
377
|
+
const finalServers = [...diskServers, ...keptConnected]
|
|
378
|
+
agent.config ??= {}
|
|
379
|
+
agent.config.mcp = { ...agent.config.mcp, servers: finalServers }
|
|
380
|
+
return { ok: true, servers: finalServers, changedNames }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Disk read behind reloadMcpFromDisk — bounded, never throws. */
|
|
384
|
+
function readMcpSection(path = configPath) {
|
|
385
|
+
try {
|
|
386
|
+
if (!existsSync(path)) return { ok: true, servers: [] }
|
|
387
|
+
const raw = JSON.parse(readFileSync(path, "utf8"))
|
|
388
|
+
const servers = raw?.mcp?.servers
|
|
389
|
+
// 非数组 = 畸形磁盘配置(2.3 代码正确性批)——ok:false 走调用方既有畸形回退(reloadMcpFromDisk 早退)
|
|
390
|
+
if (servers !== undefined && !Array.isArray(servers)) return { ok: false, error: "mcp.servers must be an array" }
|
|
391
|
+
return { ok: true, servers: Array.isArray(servers) ? servers : [] }
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return { ok: false, error: error?.message ?? String(error) }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Fingerprint = endpoint + token + headers/env entries in key order (JSON.stringify
|
|
398
|
+
* of a normalized subset — key order included, matching connectMcpServer's
|
|
399
|
+
* configFingerprint semantics: any change the connect layer would see counts).
|
|
400
|
+
* Drift = CHANGED (fingerprint differs) or DELETED (missing from disk) — a server
|
|
401
|
+
* that is new on disk is not drift (no live connection to protect). */
|
|
402
|
+
function diffMcpServers(memoryServers, diskServers) {
|
|
403
|
+
const memFp = new Map(memoryServers.filter((s) => s?.name).map((s) => [s.name, mcpFingerprint(s)]))
|
|
404
|
+
const diskFp = new Map(diskServers.filter((s) => s?.name).map((s) => [s.name, mcpFingerprint(s)]))
|
|
405
|
+
const changed = []
|
|
406
|
+
for (const [name, fp] of diskFp) if (memFp.has(name) && memFp.get(name) !== fp) changed.push(name)
|
|
407
|
+
for (const name of memFp.keys()) if (!diskFp.has(name)) changed.push(name)
|
|
408
|
+
return changed
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function mcpFingerprint(s) {
|
|
412
|
+
return JSON.stringify([
|
|
413
|
+
s.wsUrl ?? s.url ?? s.command ?? null,
|
|
414
|
+
s.args ?? null,
|
|
415
|
+
s.token ?? null,
|
|
416
|
+
s.headers ?? null,
|
|
417
|
+
s.env ?? null,
|
|
418
|
+
])
|
|
419
|
+
}
|