thincoder 0.12.60 → 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.
Files changed (152) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +27 -122
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +21 -6
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +26 -9
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/batch-segment.mjs +195 -0
  30. package/src/agent-tools/consult.mjs +23 -10
  31. package/src/agent-tools/design-token.mjs +14 -1
  32. package/src/agent-tools/digest-budget.mjs +76 -0
  33. package/src/agent-tools/eng.mjs +3 -3
  34. package/src/agent-tools/escalate-async.mjs +16 -13
  35. package/src/agent-tools/read-history.mjs +13 -3
  36. package/src/agent-tools/review-streak.mjs +93 -0
  37. package/src/agent-tools/settings.mjs +130 -17
  38. package/src/agent-tools/subagent-actions.mjs +15 -6
  39. package/src/agent-tools/subagent-async.mjs +66 -14
  40. package/src/agent-tools/subagent-panel.mjs +22 -15
  41. package/src/agent-tools/subagent-run.mjs +9 -6
  42. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  43. package/src/agent-tools/subagent-spawn.mjs +63 -16
  44. package/src/agent-tools/subagent.mjs +175 -49
  45. package/src/agent-tools/verify.mjs +13 -34
  46. package/src/agent-tools.mjs +1 -0
  47. package/src/agent.mjs +38 -21
  48. package/src/cli/distill-command.mjs +2 -2
  49. package/src/cli/make-agent.mjs +23 -7
  50. package/src/cli/memory-command.mjs +2 -2
  51. package/src/cli/setup-wizard.mjs +29 -9
  52. package/src/completions.mjs +114 -0
  53. package/src/config-migrate.mjs +70 -0
  54. package/src/config.mjs +132 -63
  55. package/src/conventions.mjs +223 -0
  56. package/src/crash-reports.mjs +7 -2
  57. package/src/expand-home.mjs +16 -0
  58. package/src/generate-title.mjs +1 -1
  59. package/src/hooks.mjs +7 -3
  60. package/src/memory/code-index.mjs +9 -3
  61. package/src/memory/code-sync.mjs +70 -31
  62. package/src/memory/delete.mjs +2 -0
  63. package/src/memory/docs.mjs +10 -6
  64. package/src/memory/file-walk.mjs +109 -0
  65. package/src/memory/schema.mjs +15 -3
  66. package/src/model-ref.mjs +66 -0
  67. package/src/model-specs.mjs +42 -8
  68. package/src/prompt-overlays.mjs +73 -16
  69. package/src/prompts/advisor-design.md +18 -8
  70. package/src/prompts/advisor-round1.md +7 -1
  71. package/src/prompts/advisor-round2.md +13 -2
  72. package/src/prompts/advisor-round3.md +13 -2
  73. package/src/prompts/common.md +115 -0
  74. package/src/prompts/consult-base.md +2 -0
  75. package/src/prompts/discipline-engineering.md +217 -0
  76. package/src/prompts/discipline-normal.md +179 -0
  77. package/src/prompts/persona-coder.md +21 -0
  78. package/src/prompts/persona-eng-coder.md +37 -0
  79. package/src/prompts/persona-eng-designer.md +55 -0
  80. package/src/prompts/persona-engineering.md +54 -0
  81. package/src/prompts/persona-explore.md +15 -0
  82. package/src/prompts/persona-normal.md +27 -0
  83. package/src/prompts/persona-plan.md +26 -0
  84. package/src/provider/anthropic.mjs +4 -4
  85. package/src/provider/core.mjs +13 -32
  86. package/src/provider/errors.mjs +26 -1
  87. package/src/provider/google.mjs +5 -6
  88. package/src/provider/index.mjs +2 -1
  89. package/src/provider/list-models.mjs +93 -0
  90. package/src/provider/rate.mjs +2 -1
  91. package/src/provider/responses.mjs +5 -3
  92. package/src/provider/sse.mjs +3 -4
  93. package/src/proxy.mjs +9 -14
  94. package/src/session-slots.mjs +7 -2
  95. package/src/session.mjs +59 -38
  96. package/src/token-ttl.mjs +2 -1
  97. package/src/tools/{system.mjs → bash.mjs} +6 -243
  98. package/src/tools/file.mjs +30 -10
  99. package/src/tools/git.md +1 -1
  100. package/src/tools/git.mjs +15 -34
  101. package/src/tools/index.mjs +4 -2
  102. package/src/tools/ops.mjs +20 -7
  103. package/src/tools/question.md +1 -0
  104. package/src/tools/question.mjs +26 -0
  105. package/src/tools/read.md +1 -1
  106. package/src/tools/read_image.md +1 -1
  107. package/src/tools/search.mjs +236 -0
  108. package/src/tui/agent-turn.mjs +32 -13
  109. package/src/tui/ansi.mjs +2 -0
  110. package/src/tui/clipboard.mjs +7 -1
  111. package/src/tui/cmd-advisor.mjs +3 -2
  112. package/src/tui/cmd-config.mjs +108 -37
  113. package/src/tui/cmd-eng.mjs +11 -27
  114. package/src/tui/cmd-exit.mjs +6 -8
  115. package/src/tui/cmd-model.mjs +14 -12
  116. package/src/tui/cmd-reindex.mjs +7 -0
  117. package/src/tui/cmd-submodel.mjs +8 -5
  118. package/src/tui/cmd-undo.mjs +4 -3
  119. package/src/tui/index.mjs +38 -37
  120. package/src/tui/key-handler.mjs +61 -17
  121. package/src/tui/key-modes.mjs +86 -8
  122. package/src/tui/layout.mjs +18 -10
  123. package/src/tui/model-catalog.mjs +89 -0
  124. package/src/tui/model-picker.mjs +498 -0
  125. package/src/tui/mouse.mjs +47 -10
  126. package/src/tui/pickers.mjs +28 -410
  127. package/src/tui/render-frame.mjs +32 -16
  128. package/src/tui/render-loop.mjs +2 -0
  129. package/src/tui/render-segments.mjs +5 -19
  130. package/src/tui/render.mjs +37 -5
  131. package/src/tui/slash-commands.mjs +2 -2
  132. package/src/tui/subagent-blocks.mjs +69 -90
  133. package/src/tui/subagent-children.mjs +50 -64
  134. package/src/tui/subagent-freeze.mjs +40 -43
  135. package/src/tui/subagent-panel.mjs +21 -66
  136. package/src/tui/suspension-drive.mjs +28 -82
  137. package/src/tui/tool-args.mjs +4 -2
  138. package/src/tui/tool-display.mjs +4 -3
  139. package/src/tui/tool-events.mjs +23 -16
  140. package/src/tui/tui-lifecycle.mjs +18 -5
  141. package/src/tui/wizard.mjs +52 -18
  142. package/src/tui/wrapped-spawn.mjs +38 -0
  143. package/src/prompts/coder.md +0 -13
  144. package/src/prompts/discipline.md +0 -84
  145. package/src/prompts/eng-coder.md +0 -19
  146. package/src/prompts/engineering-sub.md +0 -14
  147. package/src/prompts/engineering.md +0 -87
  148. package/src/prompts/explore.md +0 -12
  149. package/src/prompts/main.md +0 -34
  150. package/src/prompts/methodology-template.md +0 -38
  151. package/src/prompts/plan.md +0 -9
  152. package/src/prompts/system.md +0 -44
@@ -1,4 +1,4 @@
1
- import { join } from "node:path"
1
+ import { isAbsolute, join } from "node:path"
2
2
  import { loadConfig, configPath } from "../config.mjs"
3
3
  import { createMemory } from "../memory.mjs"
4
4
  import { teamConfig, gitAuthor } from "./make-agent.mjs"
@@ -64,7 +64,7 @@ export async function distillCommand(args, exitSoon) {
64
64
  }
65
65
 
66
66
  const opts = {
67
- projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
67
+ projectDir: config.memory.projectDir ? (isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)) : null,
68
68
  team,
69
69
  author: gitAuthor(),
70
70
  }
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process"
2
- import { join } from "node:path"
2
+ import { isAbsolute, join } from "node:path"
3
3
  import { createAgent } from "../agent.mjs"
4
4
  import { loadConfig, configDir } from "../config.mjs"
5
5
  import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } from "../memory.mjs"
@@ -10,8 +10,18 @@ import { discoverRules } from "../rules.mjs"
10
10
  // R10 L2(MULTI-INSTANCE-COLLAB §2a.4 D-L2b):peer_instances 只读工具——挂感知模块导出
11
11
  import { peerInstancesTool } from "../peer-instances.mjs"
12
12
 
13
+ /** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
14
+ * 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
15
+ * ACP 通道经 `assembleAgent({ excludeTools })` 传入(acp.mjs `ACP_EXCLUDED_TOOLS`)。 */
16
+ export function applyToolExclusions(tools, excludeTools = []) {
17
+ const excluded = new Set(excludeTools ?? [])
18
+ if (excluded.size === 0) return tools
19
+ const kept = tools.filter((t) => !excluded.has(t?.name))
20
+ return kept.length === tools.length ? tools : kept
21
+ }
22
+
13
23
  /** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
14
- export async function assembleAgent() {
24
+ export async function assembleAgent({ excludeTools = [] } = {}) {
15
25
  const config = loadConfig()
16
26
  const provider = config.provider
17
27
  const providers = config.providersList
@@ -20,7 +30,7 @@ export async function assembleAgent() {
20
30
  const { injectProxy } = await import("../proxy.mjs")
21
31
  injectProxy(providers, config)
22
32
  // config.provider 是 loadConfig 里的独立拷贝,同步注入结果
23
- provider.proxyUri = providers.find((p) => p.name === config.activeProvider)?.proxyUri
33
+ if (provider?.name) provider.proxyUri = providers.find((p) => p.name === provider.name)?.proxyUri
24
34
 
25
35
  const memory = createMemory({ dbPath: config.memory.dbPath })
26
36
  // Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
@@ -41,7 +51,7 @@ export async function assembleAgent() {
41
51
  memory.codeOrigin = cwd
42
52
  // Project layer: sync .thincoder/memory/ dir to index on startup (sync if present, skip otherwise)
43
53
  if (config.memory.projectDir) {
44
- memory.projectOrigin = join(cwd, config.memory.projectDir)
54
+ memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir)
45
55
  await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
46
56
  }
47
57
  // Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
@@ -101,18 +111,24 @@ export async function assembleAgent() {
101
111
 
102
112
  const agent = createAgent({
103
113
  provider,
104
- tools: [...baseTools, ...mcpTools],
114
+ tools: applyToolExclusions([...baseTools, ...mcpTools], excludeTools),
105
115
  config,
106
116
  cwd,
107
117
  memory,
108
118
  })
109
119
  agent.providers = providers
110
- agent.activeProvider = config.activeProvider
111
- agent.activeModel = config.activeModel ?? null
120
+ // MODEL-MERGE-SESSION:config 无 active*——会话运行时起点 = defaultModel 复合解析
121
+ // (provider 对象带解析后 model——API/spec 消费点零改);无效 → {} + 原因走 D-S1 标记
122
+ agent.activeProvider = provider.name ?? ""
123
+ agent.activeModel = provider.model ?? null
112
124
  agent._mcpWarnings = mcpWarnings
113
125
  // SESSION.md §8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
114
126
  // 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
115
127
  validateProvider(agent)
128
+ // defaultModel 无效/未设的具体原因覆盖通用判据文案(providers 存在时更有指导性)
129
+ if (agent._providerInvalid && config.providerInvalidReason) {
130
+ agent._providerInvalidReason = config.providerInvalidReason
131
+ }
116
132
  return agent
117
133
  }
118
134
 
@@ -1,4 +1,4 @@
1
- import { join } from "node:path"
1
+ import { isAbsolute, join } from "node:path"
2
2
  import { loadConfig } from "../config.mjs"
3
3
  import { teamConfig } from "./make-agent.mjs"
4
4
  import { put, search, list } from "../memory/core.mjs"
@@ -67,7 +67,7 @@ export async function memoryCommand(memory, args, opts = {}) {
67
67
  function cliDirs() {
68
68
  const config = loadConfig()
69
69
  const dirs = { project: null, team: null }
70
- if (config.memory?.projectDir) dirs.project = join(process.cwd(), config.memory.projectDir)
70
+ if (config.memory?.projectDir) dirs.project = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)
71
71
  const team = teamConfig(config)
72
72
  if (team) dirs.team = team.dir
73
73
  return dirs
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from "node:readline"
2
2
  import { configPath, writeConfigAtomic, PROVIDER_PRESETS } from "../config.mjs"
3
+ import { probeChannelModels } from "../tui/model-catalog.mjs"
3
4
 
4
5
  /** First-time setup (TTY chat / distill): ask a few questions to configure a provider, save to disk, return runtime provider. Cancel returns null. */
5
6
  export async function setupWizard() {
@@ -28,7 +29,7 @@ export async function setupWizard() {
28
29
  presets.forEach(([n, p], i) => console.error(` ${i + 1}. ${n.padEnd(10)} ${p.desc}`))
29
30
  console.error(` ${presets.length + 1}. Custom endpoint`)
30
31
  const choice = Number((await ask(`Pick [1-${presets.length + 1}]: `)).trim())
31
- let name, baseURL, model
32
+ let name, baseURL, model, presetFields = null
32
33
  if (choice === presets.length + 1) {
33
34
  name = (await ask("Name (e.g. my-openai): ")).trim()
34
35
  baseURL = (await ask("baseURL (e.g. https://api.openai.com/v1): ")).trim().replace(/\/+$/, "")
@@ -38,9 +39,11 @@ export async function setupWizard() {
38
39
  return null
39
40
  }
40
41
  } else if (choice >= 1 && choice <= presets.length) {
42
+ // 预设全量字段随行(format/thinking/maxTokens…)——探针分派与落盘同源(M9 需要 format)
43
+ presetFields = presets[choice - 1][1]
41
44
  name = presets[choice - 1][0]
42
- baseURL = presets[choice - 1][1].baseURL
43
- model = presets[choice - 1][1].model
45
+ baseURL = presetFields.baseURL
46
+ model = presetFields.model
44
47
  } else {
45
48
  console.error("Invalid choice — cancelled")
46
49
  return null
@@ -51,25 +54,42 @@ export async function setupWizard() {
51
54
  return null
52
55
  }
53
56
  const embedKey = (await ask("Optional: embedding API key (SiliconFlow, for vector search; press Enter to skip): ")).trim()
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}`)
54
64
  // D-F5b:磁盘新鲜读 → mutate → mtime 门控写(writeConfigAtomic 收口);冲突 = 放弃
55
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)落盘差异已登记待口径统一(父侧)。
56
71
  const r = writeConfigAtomic(configPath, (raw) => {
57
72
  const providers = raw.providers?.length ? raw.providers : []
58
73
  const existing = providers.find((p) => p.name === name)
59
- if (existing) Object.assign(existing, { baseURL, model, apiKey })
60
- else providers.push({ name, baseURL, model, apiKey })
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)
61
80
  raw.providers = providers
62
- raw.activeProvider = name
63
- delete raw.activeModel // reset to default model
81
+ raw.defaultModel = `${name}:${model}`
82
+ delete raw.activeProvider
83
+ delete raw.activeModel
64
84
  if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
65
85
  })
66
86
  if (!r.ok) {
67
87
  console.error("config changed on disk concurrently — retry")
68
88
  return null
69
89
  }
70
- console.error(`Configured: ${name} / ${model} (saved to ${configPath})`)
90
+ console.error(`Configured: ${name} / ${model} (defaultModel = ${name}:${model} — saved to ${configPath})`)
71
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")
72
- return { name, baseURL, model, apiKey }
92
+ return { name, baseURL, model, apiKey, defaultModel: `${name}:${model}` }
73
93
  } finally {
74
94
  rl.close()
75
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
+ }