@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.
Files changed (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
package/config-io.mjs ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * config-io.mjs — 核内 config.json 的**单一读写面** + provider 持久化面 + 监视钩子
3
+ * (CORE-UNIFICATION §2.5 CONFIG 组:#131「融合:核内单一读写 + 面板写面按端注入」·
4
+ * #132 监视面按端注入 · #177「纯持久化函数取一侧 + UI 壳按端注入」)。
5
+ *
6
+ * 组成与来源(逐条可追):
7
+ * - `configDir` / `configPath` / `_setConfigPathForTest` / `_resetConfigPathForTest` /
8
+ * `_configPath` —— CLI `config.mjs` 的路径与测试缝(逐字随迁;拆档以容纳 500 行硬限)。
9
+ * - `writeConfigAtomic` —— CLI `config.mjs`(R10 F5b mtime 门控:stat→read 序 + 冲突放弃
10
+ * + `.bak` 留现场 + 0600)。**核内唯一写盘执行体**。
11
+ * - `$schema` 写盘 —— VSC `config-io.mjs` 的 `saveRaw` 会写 `$schema` 指针、CLI 不写
12
+ * (§2.5 #80 端差)⇒ 以 `opts.schema` **按端注入**,默认不写(= CLI 语义,零行为变)。
13
+ * - `loadRaw` / `resolveProviders` —— VSC `config-io.mjs`(老形态迁移先行;providers 归一 +
14
+ * 默认渠道派生)。迁移写回经核内唯一写盘执行体(与 CLI `loadConfig` 同一条链路)。
15
+ * - `persistRaw` / `conflictError` / `CONFIG_CONFLICT_HINT` —— VSC `config-io.mjs`(读-改-写
16
+ * 链 + 冲突提示串;执行体 = `writeConfigAtomic`)。
17
+ * - `onConfigSelfWrite` —— VSC `config-io.mjs` 的自写通知(监视面属端侧,订阅挂在核内写面)。
18
+ * - `cascadeRemoveProvider` —— VSC `config-consult.mjs`(删渠道的悬挂引用级联清理,逐字随迁)。
19
+ * - `setProviderKey` / `removeProviderKeyFromConfig` / `addProviderEntry` / `removeProviderEntry`
20
+ * —— VSC `config-io.mjs` + `extension/provider-flows.mjs` 的**纯持久化函数**(无 UI;UI 壳
21
+ * QuickPick / TUI picker 留端侧)。
22
+ */
23
+
24
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
25
+ import { homedir } from "node:os"
26
+ import { dirname, join } from "node:path"
27
+ import { migrateLegacyModelFields } from "./config-migrate.mjs"
28
+ import { PROVIDER_PRESETS, presetToEntry } from "./config-presets.mjs"
29
+
30
+ export { PROVIDER_PRESETS, presetToEntry }
31
+
32
+ export const configDir = join(homedir(), ".thincoder")
33
+ export const configPath = join(configDir, "config.json")
34
+
35
+ /** Test seam: override the config file location (mirrors the CLI / VSC seams). */
36
+ let _pathOverride = null
37
+ export function _setConfigPathForTest(p) { _pathOverride = p }
38
+ export function _resetConfigPathForTest() { _pathOverride = null }
39
+ export function _configPath() { return _pathOverride ?? configPath }
40
+
41
+ /** F5b 冲突提示文案(D-F5b 同型——调用方展示 / 抛出)。 */
42
+ export const CONFIG_CONFLICT_HINT = "config changed on disk concurrently — retry"
43
+
44
+ /**
45
+ * Read → mutate → write(CLI `writeConfigAtomic`)——**核内唯一写盘执行体**。
46
+ *
47
+ * - t0 = 写前重 stat 的比对基线,取在**新鲜读之前**(stat→read 序):若对端在本端 stat 与
48
+ * read 之间的微窗口写入,只会造成假冲突(放弃重试),绝不会带着旧内容覆盖对端新值——
49
+ * read→stat 序存在漏检窗口(stat 已反映对端新 mtime → 门控放行旧内容)。
50
+ * - 写前重 stat ≠ t0 → **放弃**本次写;先 copy `.bak-{ts}` 留现场(仅冲突时)。
51
+ * - 返回 { ok:false, reason:"mtime-conflict" },调用方提示 CONFIG_CONFLICT_HINT——不自动合并。
52
+ * - 文件缺失(首写)→ t0 = null;对端在本端读后创建 → null ≠ 新 mtime → 冲突放弃。
53
+ * - 畸形文件拒写(throw,绝不静默覆盖);写后 chmod 0600 尽力而为。
54
+ *
55
+ * @param path config.json 路径(生产默认 configPath;测试注入 tmp 路径)
56
+ * @param mutate 在磁盘新鲜 raw 上执行单操作的同步回调
57
+ * @param {{schema?: string}} [opts] schema = 端注入的 `$schema` 指针(未给不写——§2.5 #80)
58
+ */
59
+ export function writeConfigAtomic(path, mutate, opts = {}) {
60
+ const mtimeOf = (p) => {
61
+ try { return statSync(p).mtimeMs } catch { return null } // 缺失 → null(t0 比对基线)
62
+ }
63
+ const t0 = mtimeOf(path) // stat 先于 read(安全方向——见上)
64
+ const text = existsSync(path) ? readFileSync(path, "utf8") : null
65
+ let raw = {}
66
+ if (text !== null) {
67
+ try {
68
+ raw = JSON.parse(text)
69
+ } catch (error) {
70
+ throw new Error(`config file not parseable — refusing to overwrite: ${path} — ${error.message}`, { cause: error })
71
+ }
72
+ }
73
+ mutate(raw)
74
+ // §2.5 #80:`$schema` 写盘按端差注入(VSC 写 schema 指针、CLI 不写)——默认关闭(CLI 语义)。
75
+ if (opts.schema) raw.$schema = opts.schema
76
+ const t1 = mtimeOf(path)
77
+ if (t0 !== t1) {
78
+ try { if (existsSync(path)) copyFileSync(path, `${path}.bak-${Date.now()}`) } catch { /* 现场保留失败不阻断冲突报告 */ }
79
+ return { ok: false, reason: "mtime-conflict" }
80
+ }
81
+ mkdirSync(dirname(path), { recursive: true })
82
+ // 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
83
+ writeFileSync(path, JSON.stringify(raw, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
84
+ try { chmodSync(path, 0o600) } catch { /* may fail on Windows, ignore */ }
85
+ for (const fn of [...selfWriteFns]) {
86
+ try { fn() } catch (e) { console.warn(`[config] self-write subscriber failed: ${e.message}`) }
87
+ }
88
+ return { ok: true }
89
+ }
90
+
91
+ /** 读 → 改写 → 写(VSC `persistRaw` / CLI `tui persistRaw` / `cli setup-wizard` 同一链)。 */
92
+ export function persistRaw(mutate, opts = {}) {
93
+ return writeConfigAtomic(opts.path ?? _configPath(), mutate, opts)
94
+ }
95
+
96
+ /** 冲突判定收口:写结果 → CONFIG_CONFLICT_HINT 或 null。 */
97
+ export function conflictError(result) {
98
+ return result?.reason === "mtime-conflict" ? CONFIG_CONFLICT_HINT : null
99
+ }
100
+
101
+ // ─── Self-write notifications(§2.5 #131 / #132:监视面按端注入,订阅挂在核内写面)───
102
+ // 写成功后的同步回调让外部写感知面(配置监视)把基线刷到当前元组——自写 ⇒ 事件到达时
103
+ // 元组已等于基线 ⇒ 零推送(不抖动消费者);冲突放弃路径(无写)不回调。
104
+ const selfWriteFns = new Set()
105
+
106
+ /** 订阅写盘成功后的同步回调;返回退订函数(监视面的 dispose 随退)。 */
107
+ export function onConfigSelfWrite(fn) {
108
+ if (typeof fn !== "function") return () => {}
109
+ selfWriteFns.add(fn)
110
+ return () => { selfWriteFns.delete(fn) }
111
+ }
112
+
113
+ /**
114
+ * Read the raw config object ({} when missing). Throws on invalid JSON — same as CLI loadConfig.
115
+ * 老形态检测 → 内存迁移态先行;写回失败绝不阻断(冲突 → 下次读重试——幂等)。
116
+ */
117
+ export function loadRaw() {
118
+ const path = _configPath()
119
+ if (!existsSync(path)) return {}
120
+ let raw
121
+ try {
122
+ raw = JSON.parse(readFileSync(path, "utf8"))
123
+ } catch (error) {
124
+ throw new Error(`Config file is not valid JSON, check or delete it: ${path}\n ${error.message}`, { cause: error })
125
+ }
126
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}
127
+ if (migrateLegacyModelFields(raw)) {
128
+ try {
129
+ const r = writeConfigAtomic(path, migrateLegacyModelFields)
130
+ if (r && r.ok === false) console.warn(`[config] model-merge migration write-back skipped (${r.reason}) — memory state continues, retried on next load`)
131
+ } catch (e) {
132
+ console.warn(`[config] model-merge migration write-back failed — memory state continues, retried on next load: ${e.message}`)
133
+ }
134
+ }
135
+ return raw
136
+ }
137
+
138
+ /** Module-level one-time warn dedupe for invalid providers[].context (PROVIDER.md §15 D-C1). */
139
+ const warnedContext = new Set()
140
+
141
+ /**
142
+ * Resolve providers list + active name from disk(VSC `config-io.mjs` 逐字随迁)。BaseURL
143
+ * trailing slashes normalized. `activeProvider` 语义 = config.defaultModel 的渠道;缺失/失效
144
+ * → 回退首 provider。渠道单值 `model` 归一:非字符串/空串删除。
145
+ */
146
+ export function resolveProviders() {
147
+ const raw = loadRaw()
148
+ const providers = Array.isArray(raw.providers)
149
+ ? raw.providers.filter((p) => p && typeof p === "object" && p.name)
150
+ : []
151
+ for (const p of providers) {
152
+ if (typeof p.baseURL === "string") p.baseURL = p.baseURL.replace(/\/+$/, "")
153
+ if (typeof p.model === "string" && p.model.trim()) p.model = p.model.trim()
154
+ else delete p.model
155
+ // PROVIDER.md §15 D-C1: providers[].context must be a positive integer (K units).
156
+ if (p.context != null) {
157
+ const n = Number(p.context)
158
+ if (!Number.isInteger(n) || n <= 0) {
159
+ if (!warnedContext.has(p.name)) {
160
+ warnedContext.add(p.name)
161
+ console.warn(`[config] provider "${p.name}" has invalid context ${JSON.stringify(p.context)} — ignored (expected a positive integer in K units, e.g. 128 = 128K); using the model spec value.`)
162
+ }
163
+ delete p.context
164
+ }
165
+ }
166
+ }
167
+ let activeProvider = ""
168
+ if (typeof raw.defaultModel === "string" && raw.defaultModel.includes(":")) {
169
+ const dmProvider = raw.defaultModel.slice(0, raw.defaultModel.indexOf(":"))
170
+ if (providers.some((p) => p.name === dmProvider)) activeProvider = dmProvider
171
+ }
172
+ if (!activeProvider) activeProvider = providers[0]?.name ?? ""
173
+ return { providers, activeProvider }
174
+ }
175
+
176
+ /** 级联清理(删渠道的共享写点):raw 移除 name 渠道后清悬挂——
177
+ * consultModels 条目 / subagentModels 角色值(=== name 或 "name:…")/ advisor.provider。纯 mutate。 */
178
+ export function cascadeRemoveProvider(raw, name) {
179
+ const a = raw?.agent
180
+ if (!a || typeof a !== "object" || Array.isArray(a)) return
181
+ if (Array.isArray(a.consultModels)) {
182
+ const keep = a.consultModels.filter((m) => m?.provider !== name)
183
+ if (keep.length) a.consultModels = keep
184
+ else delete a.consultModels
185
+ }
186
+ if (a.subagentModels && typeof a.subagentModels === "object" && !Array.isArray(a.subagentModels)) {
187
+ for (const role of Object.keys(a.subagentModels)) {
188
+ const v = a.subagentModels[role]
189
+ if (typeof v === "string" && (v === name || v.startsWith(`${name}:`))) delete a.subagentModels[role]
190
+ }
191
+ if (!Object.keys(a.subagentModels).length) delete a.subagentModels
192
+ }
193
+ if (a.advisor && typeof a.advisor === "object" && !Array.isArray(a.advisor) && a.advisor.provider === name) {
194
+ delete a.advisor.provider
195
+ }
196
+ }
197
+
198
+ // ─── Provider 持久化面(§2.5 #177「纯持久化函数取一侧 + UI 壳按端注入」)───
199
+
200
+ /** 写一个渠道的 key(整 providers[] 重写)。冲突 → CONFIG_CONFLICT_HINT;成功 null。 */
201
+ export function setProviderKey(name, key) {
202
+ const r = persistRaw((raw) => {
203
+ raw.providers = Array.isArray(raw.providers) ? raw.providers : []
204
+ const entry = raw.providers.find((p) => p?.name === name)
205
+ if (entry) entry.apiKey = key
206
+ })
207
+ return conflictError(r)
208
+ }
209
+
210
+ /** 移除一个渠道的 key(保留渠道条目)。冲突提示同 setProviderKey。 */
211
+ export function removeProviderKeyFromConfig(name) {
212
+ const r = persistRaw((raw) => {
213
+ const entry = Array.isArray(raw.providers) ? raw.providers.find((p) => p?.name === name) : null
214
+ if (entry) delete entry.apiKey
215
+ })
216
+ return conflictError(r)
217
+ }
218
+
219
+ const FORMATS = ["openai", "anthropic", "google"]
220
+
221
+ /** 新增渠道。payload: { preset?: name, custom?: { name, baseURL, model, format }, key? }
222
+ * 返回错误串或 null。 */
223
+ export function addProviderEntry({ preset, custom, key } = {}) {
224
+ let providers
225
+ try {
226
+ ({ providers } = resolveProviders())
227
+ } catch (e) {
228
+ return e.message
229
+ }
230
+ const existing = new Set(providers.map((p) => p.name))
231
+
232
+ let entry
233
+ if (preset) {
234
+ if (!PROVIDER_PRESETS[preset]) return `Unknown preset: ${preset}`
235
+ if (existing.has(preset)) return `Provider "${preset}" already exists`
236
+ entry = presetToEntry(preset)
237
+ } else if (custom) {
238
+ const name = (custom.name || "").trim()
239
+ if (!name) return "Provider name is required"
240
+ if (existing.has(name) || PROVIDER_PRESETS[name]) return `Name "${name}" is already in use`
241
+ const baseURL = (custom.baseURL || "").trim().replace(/\/+$/, "")
242
+ if (!baseURL) return "Base URL is required"
243
+ const model = (custom.model || "").trim()
244
+ if (!model) return "Model is required"
245
+ const format = (custom.format || "openai").trim()
246
+ if (!FORMATS.includes(format)) return `Unknown API format: ${format} (expected ${FORMATS.join("/")})`
247
+ entry = { name, baseURL, model } // MODEL-SELECTION:渠道单值默认模型(候选清单字段已退场)
248
+ if (format !== "openai") entry.format = format
249
+ } else {
250
+ return "Add provider needs a preset or a custom config"
251
+ }
252
+
253
+ const r = persistRaw((raw) => { (raw.providers ??= []).push(entry) })
254
+ const err = conflictError(r)
255
+ if (err) return err // F5b:config 被并发方改过——放弃 + 提示重试(决策① A)
256
+ const k = (key || "").trim()
257
+ if (k) setProviderKey(entry.name, k)
258
+ return null
259
+ }
260
+
261
+ /** 移除渠道。激活渠道受保护(CLI parity)。返回错误串或 null。 */
262
+ export function removeProviderEntry(name) {
263
+ let providers, activeProvider
264
+ try {
265
+ ({ providers, activeProvider } = resolveProviders())
266
+ } catch (e) {
267
+ return e.message
268
+ }
269
+ if (!providers.some((p) => p.name === name)) return `No provider named "${name}"`
270
+ if (name === activeProvider) return "The active provider cannot be removed — switch active first"
271
+ const r = persistRaw((raw) => {
272
+ raw.providers = (raw.providers ?? []).filter((p) => p?.name !== name)
273
+ // F-4 (IKCDMR——AC-4 级联):删渠道同步清 consultModels/subagentModels/advisor.provider 悬挂引用
274
+ cascadeRemoveProvider(raw, name)
275
+ })
276
+ return conflictError(r) // F5b:冲突 → 错误串提示(调用方 providerError 通道展示)
277
+ }
@@ -0,0 +1,178 @@
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
+ * ── 核内合并(CORE-UNIFICATION §2.5 #79 · CONFIG 组「取并集」)──
21
+ * ② `migrateCore`(VS Code 旧设置 / 密钥库迁移遍——VSC 侧逐字随迁 + 依赖面收核为注入参数):
22
+ * CLI 侧无此通道,VSC 侧「不迁即丢密钥」⇒ 进核。读-改-写编排(loadRaw / saveRaw /
23
+ * conflictError——写回失败绝不阻断)由调用方注入(核内 = `config.mjs`);
24
+ * 本档被 `config.mjs` import ⇒ 不得反向 import(循环)——故 ② 保持纯函数形态。
25
+ */
26
+
27
+ export function migrateLegacyModelFields(raw) {
28
+ if (!raw || typeof raw !== "object") return false
29
+ const providers = Array.isArray(raw.providers) ? raw.providers : []
30
+ let changed = false
31
+
32
+ // ── ① 有效 defaultModel 值(含老 active* 转换)──
33
+ if (raw.activeProvider !== undefined || raw.activeModel !== undefined) {
34
+ const rawAP = typeof raw.activeProvider === "string" && raw.activeProvider.trim() ? raw.activeProvider : ""
35
+ const rawAM = typeof raw.activeModel === "string" && raw.activeModel.trim() ? raw.activeModel : ""
36
+ const ap = rawAP ? providers.find((p) => p?.name === rawAP) : null
37
+ if (ap && rawAM) {
38
+ if (raw.defaultModel == null) { raw.defaultModel = `${rawAP}:${rawAM}`; changed = true }
39
+ } else {
40
+ // AP 已不存在 / AM 无法归属 → 首渠道回退(该渠道有模型来源才可用——迁移产物必可用)
41
+ const first = providers.find((p) => seedModel(p) != null)
42
+ if (first && raw.defaultModel == null) { raw.defaultModel = `${first.name}:${seedModel(first)}`; changed = true }
43
+ }
44
+ delete raw.activeProvider
45
+ delete raw.activeModel
46
+ changed = true
47
+ }
48
+
49
+ // ── ② 渠道播种单值 `p.model` + 清 models ──
50
+ const dm = typeof raw.defaultModel === "string" && raw.defaultModel.trim() ? raw.defaultModel : null
51
+ const dmSep = dm ? dm.indexOf(":") : -1
52
+ const dmProvider = dmSep > 0 ? dm.slice(0, dmSep) : null
53
+ const dmModel = dmSep > 0 && dmSep < dm.length - 1 ? dm.slice(dmSep + 1) : null
54
+ for (const p of providers) {
55
+ if (!p || typeof p !== "object") continue
56
+ if (Array.isArray(p.models)) {
57
+ const next = (p.name === dmProvider && dmModel)
58
+ ? dmModel
59
+ : (typeof p.model === "string" && p.model.trim() ? p.model : seedModel(p))
60
+ if (next == null) delete p.model
61
+ else p.model = next
62
+ delete p.models
63
+ changed = true
64
+ continue
65
+ }
66
+ if (p.models !== undefined) { delete p.models; changed = true } // 非数组 models —— 垃圾清理
67
+ if (p.model !== undefined && !(typeof p.model === "string" && p.model.trim())) { delete p.model; changed = true }
68
+ }
69
+ return changed
70
+ }
71
+
72
+ /** 渠道级模型来源读数(迁移用):现有 `p.model`(非空字符串优先)或 `models[]` 首个非空字符串。 */
73
+ function seedModel(p) {
74
+ if (typeof p?.model === "string" && p.model.trim()) return p.model
75
+ const models = Array.isArray(p?.models) ? p.models : []
76
+ return models.find((m) => typeof m === "string" && m.trim()) ?? null
77
+ }
78
+
79
+ /**
80
+ * ② VS Code 旧设置 / 密钥库迁移遍(CORE-UNIFICATION §2.5 #79——**取并集**:CLI 侧无此通道,
81
+ * VSC 侧「不迁即丢密钥」)。来源 = `thincoder-vscode/src/config-migrate.mjs` 的 `migrateCore`
82
+ * ——逐字随迁,**依赖面收核**(原 import 的 `PROVIDER_PRESETS` / `presetToEntry` 与
83
+ * `loadRaw` / `saveRaw` / `conflictError` 改为**注入参数**——本档由 `config.mjs` import,
84
+ * 不得反向 import `config.mjs`(循环)⇒ 纯函数形态保留,调用方供应依赖)。
85
+ *
86
+ * Pure function:
87
+ * deps.secrets — { get(key), delete(key) } (async, may throw)
88
+ * deps.flags — { get(): Promise<boolean>, set(): Promise<void> } (one-shot guard)
89
+ * deps.legacySettings — the old `thincoder.providers` settings value (object or undefined)
90
+ * deps.clearLegacySettings— async fn that deletes the legacy settings key
91
+ * deps.loadRaw / deps.saveRaw / deps.conflictError — 核内单一读写面(config.mjs)
92
+ * deps.presets / deps.presetToEntry — 核内预设表(config.mjs)
93
+ *
94
+ * Never overwrites an apiKey already present in config.json (the CLI may have written it first).
95
+ * Builtin preset names missing from providers[] are created from PROVIDER_PRESETS; unknown
96
+ * names are only created when baseURL/model metadata is recoverable.
97
+ */
98
+ export async function migrateCore(deps) {
99
+ const { secrets, flags, legacySettings, clearLegacySettings } = deps
100
+ const loadRaw = deps.loadRaw
101
+ const saveRaw = deps.saveRaw
102
+ const conflictError = deps.conflictError
103
+ const PROVIDER_PRESETS = deps.presets
104
+ const presetToEntry = deps.presetToEntry
105
+ if (await flags.get()) return
106
+
107
+ const raw = loadRaw()
108
+ const providers = Array.isArray(raw.providers) ? raw.providers : []
109
+ raw.providers = providers
110
+ const byName = new Map(providers.filter((p) => p && typeof p === "object" && p.name).map((p) => [p.name, p]))
111
+
112
+ const legacyMeta = {}
113
+ for (const [name, entry] of Object.entries(legacySettings || {})) {
114
+ if (entry && typeof entry === "object") legacyMeta[name] = entry
115
+ }
116
+
117
+ function applyKey(name, key) {
118
+ if (!key) return
119
+ let entry = byName.get(name)
120
+ if (!entry) {
121
+ if (PROVIDER_PRESETS[name]) {
122
+ entry = presetToEntry(name)
123
+ } else {
124
+ const meta = legacyMeta[name]
125
+ if (!meta?.baseURL || !meta?.model) return // orphan key, no way to reconstruct
126
+ entry = { name, baseURL: meta.baseURL, model: meta.model }
127
+ // PROVIDER.md §15 D-C4: migrate-settings is the same source — a legacy
128
+ // settings entry carrying context (K units) rides along into config.json.
129
+ if (meta.context != null) entry.context = meta.context
130
+ }
131
+ entry.apiKey = key
132
+ providers.push(entry)
133
+ byName.set(name, entry)
134
+ return
135
+ }
136
+ if (!entry.apiKey) entry.apiKey = key // never clobber an existing key
137
+ }
138
+
139
+ // SecretStorage keys first, then settings.json keys (both are legacy; config.json wins)
140
+ const legacyNames = [...Object.keys(PROVIDER_PRESETS), "custom"]
141
+ for (const name of legacyNames) {
142
+ try { applyKey(name, await secrets.get(`thincoder.provider.${name}`)) } catch { /* ignore */ }
143
+ }
144
+ for (const [name, entry] of Object.entries(legacySettings || {})) {
145
+ applyKey(name, typeof entry === "string" ? entry : entry?.key)
146
+ }
147
+
148
+ // Embedding key (legacy SecretStorage → config.embedding)
149
+ try {
150
+ const embKey = await secrets.get("thincoder.embedding.apiKey")
151
+ if (embKey) {
152
+ const emb = raw.embedding && typeof raw.embedding === "object" ? raw.embedding : {}
153
+ if (!emb.apiKey) emb.apiKey = embKey
154
+ if (!emb.baseURL) emb.baseURL = EMBEDDING_DEFAULTS.baseURL
155
+ if (!emb.model) emb.model = EMBEDDING_DEFAULTS.model
156
+ raw.embedding = emb
157
+ }
158
+ } catch { /* ignore */ }
159
+
160
+ // Legacy MCP servers were removed together with the `thincoder.mcpServers`
161
+ // setting itself (pre-release, no migration — see package.json history).
162
+
163
+ // F5b(R10——MULTI-INSTANCE-COLLAB.md D-F5b):迁移是读-改-写链(读后多 await 异步
164
+ // 窗口)——saveRaw 写前 mtime 门控冲突({ok:false, reason:"mtime-conflict"})→ 放弃
165
+ // 本次迁移:legacy 存储不清理、flags 不置位——下次启动自动重试(绝不能清了 legacy
166
+ // 又丢了 config 写——密钥才会真正丢失)。
167
+ if (conflictError(saveRaw(raw))) return
168
+
169
+ // Clean up legacy stores so nothing reads them again
170
+ for (const name of legacyNames) {
171
+ try { await secrets.delete(`thincoder.provider.${name}`) } catch { /* ignore */ }
172
+ }
173
+ try { await secrets.delete("thincoder.embedding.apiKey") } catch { /* ignore */ }
174
+ try { await clearLegacySettings() } catch { /* ignore */ }
175
+ await flags.set()
176
+ }
177
+
178
+ const EMBEDDING_DEFAULTS = { baseURL: "https://api.siliconflow.cn/v1", model: "BAAI/bge-m3" }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * config-presets.mjs — provider preset table + entry builder(核内单一预设面)。
3
+ *
4
+ * 来源(#129 融合:自 CLI / VSC 两张逐条同值的预设表取一侧):
5
+ * - `PROVIDER_PRESETS` = 取 CLI 侧为单一权威(VSC 侧现经由
6
+ * `@thincoder/core/config.mjs` 取用,无独立镜像档;本档 = 两表
7
+ * 融合后的唯一定义处)。
8
+ * - `presetToEntry` = 取 VSC 侧独有导出(CLI 侧对应实现在
9
+ * `cli/setup-wizard.mjs`,同字节语义)。
10
+ *
11
+ * 预先声明(MODEL-SELECTION v2,2026-09-10):每个预设恰好携带一个默认模型(`model` ——
12
+ * 新装启动种子 / 槽位空兜底;候选清单字段不再预置——运行期从 provider 拉取)。
13
+ */
14
+
15
+ /** Built-in provider presets: shared by /provider add <preset> and first-run wizard. */
16
+ export const PROVIDER_PRESETS = {
17
+ deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-flash", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 393216, desc: "DeepSeek" },
18
+ kimi: { baseURL: "https://api.moonshot.cn/v1", model: "kimi-k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
19
+ "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
+ glm: { baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM" },
21
+ "glm-code": { baseURL: "https://open.bigmodel.cn/api/coding/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM Coding Plan (coding endpoint — same key as GLM; server-forced thinking)" },
22
+ qwen: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", reasoningEffort: "high", maxTokens: 131072, desc: "Qwen / Alibaba" },
23
+ qwenplan: { baseURL: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", reasoningEffort: "high", maxTokens: 131072, desc: "Qwen Token Plan (百炼套餐)" },
24
+ mimo: { baseURL: "https://api.xiaomimimo.com/v1", model: "mimo-v2.5-pro", thinking: { type: "enabled" }, maxTokens: 131072, desc: "MiMo (Xiaomi)" },
25
+ mimoplan: { baseURL: "https://token-plan-cn.xiaomimimo.com/v1", model: "mimo-v2.5-pro", thinking: { type: "enabled" }, maxTokens: 131072, desc: "MiMo Token Plan (小米套餐 — tp- keys; 与按量付费 sk- 密钥不通用)" },
26
+ minimax: { baseURL: "https://api.minimaxi.com/v1", model: "MiniMax-M3", thinking: { type: "adaptive" }, maxTokens: 128000, chatPath: "/text/chatcompletion_v2", desc: "MiniMax" },
27
+ openai: { baseURL: "https://api.openai.com/v1", model: "gpt-4o", desc: "OpenAI" },
28
+ claude: { baseURL: "https://api.anthropic.com/v1", model: "claude-sonnet-4", format: "anthropic", maxTokens: 8192, desc: "Claude (Anthropic)" },
29
+ gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta", model: "gemini-2.5-flash", format: "google", maxTokens: 8192, desc: "Gemini (Google)" },
30
+ grok: { baseURL: "https://api.x.ai/v1", model: "grok-4.5", maxTokens: 65536, desc: "Grok (xAI)" },
31
+ mistral: { baseURL: "https://api.mistral.ai/v1", model: "mistral-large", maxTokens: 32768, desc: "Mistral" },
32
+ volcengine: { baseURL: "https://ark.cn-beijing.volces.com/api/v3", model: "doubao-seed-2-0-code-preview-260215", maxTokens: 131072, desc: "Volcengine Ark (豆包)" },
33
+ hunyuan: { baseURL: "https://api.hunyuan.cloud.tencent.com/v1", model: "hunyuan-pro", maxTokens: 32768, desc: "Hunyuan (腾讯混元)" },
34
+ // TokenHub = Tencent MaaS 聚合网关(另一主机;`hy3` 在本规格表有行)。`thinking` / `reasoningEffort` /
35
+ // `maxTokens` 一律**不设 = 不发**(两新渠道的 thinking 载荷与 max_tokens 行为未测 —— MODEL-SPECS §9.6 D-13)。
36
+ tokenhub: { baseURL: "https://tokenhub.tencentmaas.com/v1", model: "hy3", desc: "Tencent TokenHub (腾讯混元网关)" },
37
+ siliconflow: { baseURL: "https://api.siliconflow.cn/v1", model: "deepseek-ai/DeepSeek-V3", maxTokens: 32768, desc: "SiliconFlow (硅基流动)" },
38
+ openrouter: { baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4", maxTokens: 32768, desc: "OpenRouter" },
39
+ groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32768, desc: "Groq" },
40
+ }
41
+
42
+ /** Build the stored provider entry from a preset — strip the display field, keep the rest
43
+ * (含单值 `model`——候选清单不再由预设播种)。 */
44
+ export function presetToEntry(name) {
45
+ const preset = PROVIDER_PRESETS[name]
46
+ if (!preset) return null
47
+ const { desc: _, ...entry } = preset
48
+ return { name, ...entry }
49
+ }