@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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 (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -0,0 +1,556 @@
1
+ import path from "node:path"
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises"
3
+ import YAML from "yaml"
4
+ import { userRootDir } from "../storage/paths.mjs"
5
+
6
+ // --- 标准厂商预设 ---
7
+ export const VENDOR_PRESETS = {
8
+ anthropic: {
9
+ label: "Anthropic (Claude)",
10
+ type: "anthropic",
11
+ base_url: "https://api.anthropic.com/v1",
12
+ default_model: "claude-opus-4-6",
13
+ models: ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"],
14
+ supports_thinking: true,
15
+ supports_vision: true,
16
+ key_env: "ANTHROPIC_API_KEY"
17
+ },
18
+ openai: {
19
+ label: "OpenAI (GPT)",
20
+ type: "openai",
21
+ base_url: "https://api.openai.com/v1",
22
+ default_model: "gpt-4o",
23
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
24
+ supports_thinking: false,
25
+ supports_vision: true,
26
+ key_env: "OPENAI_API_KEY"
27
+ },
28
+ qwen: {
29
+ label: "通义千问 Qwen (DashScope)",
30
+ type: "openai-compatible",
31
+ base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
32
+ default_model: "qwen-max",
33
+ models: ["qwen-max", "qwen-plus", "qwen-turbo", "qwen3-coder-plus"],
34
+ supports_thinking: false,
35
+ supports_vision: true,
36
+ key_env: "DASHSCOPE_API_KEY"
37
+ },
38
+ "coding-plan": {
39
+ label: "Coding Plan (阿里云百炼)",
40
+ protocols: {
41
+ openai: {
42
+ type: "openai-compatible",
43
+ base_url: "https://coding.dashscope.aliyuncs.com/v1"
44
+ },
45
+ anthropic: {
46
+ type: "anthropic",
47
+ base_url: "https://coding.dashscope.aliyuncs.com/apps/anthropic"
48
+ }
49
+ },
50
+ default_model: "qwen3.5-plus",
51
+ models: ["qwen3.5-plus", "kimi-k2.5", "glm-5", "MiniMax-M2.5", "qwen3-coder-next", "qwen3-coder-plus", "glm-4.7"],
52
+ context_limit: 983616,
53
+ supports_thinking: false,
54
+ supports_vision: true,
55
+ key_env: "CODING_PLAN_API_KEY"
56
+ },
57
+ glm: {
58
+ label: "智谱 GLM",
59
+ type: "openai-compatible",
60
+ base_url: "https://open.bigmodel.cn/api/paas/v4",
61
+ default_model: "glm-4-plus",
62
+ models: ["glm-4-plus", "glm-4-air", "glm-4-flash"],
63
+ supports_thinking: false,
64
+ supports_vision: true,
65
+ key_env: "ZHIPU_API_KEY"
66
+ },
67
+ deepseek: {
68
+ label: "DeepSeek",
69
+ type: "openai-compatible",
70
+ base_url: "https://api.deepseek.com",
71
+ default_model: "deepseek-chat",
72
+ models: ["deepseek-chat", "deepseek-reasoner"],
73
+ supports_thinking: true,
74
+ supports_vision: false,
75
+ key_env: "DEEPSEEK_API_KEY"
76
+ },
77
+ ollama: {
78
+ label: "Ollama (本地,无需 API Key)",
79
+ type: "ollama",
80
+ base_url: "http://localhost:11434",
81
+ default_model: "qwen3",
82
+ models: ["qwen3", "deepseek-coder", "llama3.1"],
83
+ supports_thinking: false,
84
+ supports_vision: false,
85
+ key_env: ""
86
+ }
87
+ }
88
+
89
+ const VENDOR_KEYS = Object.keys(VENDOR_PRESETS)
90
+
91
+ function userConfigPathLabel() {
92
+ const userRoot = userRootDir()
93
+ const home = process.env.HOME || process.env.USERPROFILE
94
+ if (!home) return `${path.resolve(userRoot).replace(/\\/g, "/")}/config.yaml`
95
+ const normalizedHome = path.resolve(home).replace(/\\/g, "/")
96
+ const normalizedRoot = path.resolve(userRoot).replace(/\\/g, "/")
97
+ if (normalizedRoot === normalizedHome) {
98
+ return "~/config.yaml"
99
+ }
100
+ if (normalizedRoot.startsWith(`${normalizedHome}/`)) {
101
+ return `~${normalizedRoot.slice(normalizedHome.length)}/config.yaml`
102
+ }
103
+ return `${normalizedRoot}/config.yaml`
104
+ }
105
+
106
+ // --- 编辑模式的可配置字段 ---
107
+ const EDIT_FIELDS = [
108
+ { key: "type", label: "Provider 类型", type: "provider_type" },
109
+ { key: "base_url", label: "Base URL", type: "string" },
110
+ { key: "api_key", label: "API Key", type: "secret" },
111
+ { key: "default_model", label: "默认模型", type: "string" },
112
+ { key: "max_tokens", label: "最大输出长度 (tokens)", type: "int", min: 1 },
113
+ { key: "context_limit", label: "上下文长度 (tokens)", type: "int", min: 1024 },
114
+ { key: "thinking", label: "Thinking 模式", type: "thinking" },
115
+ ]
116
+
117
+ // --- 向导状态 ---
118
+ export function createWizardState() {
119
+ return {
120
+ active: false,
121
+ step: "vendor",
122
+ vendorKey: null,
123
+ preset: null,
124
+ isCustom: false,
125
+ customName: null,
126
+ customBaseUrl: null,
127
+ customType: null,
128
+ apiKey: null,
129
+ defaultModel: null,
130
+ contextLimit: null,
131
+ thinking: false,
132
+ // edit 模式
133
+ editMode: false,
134
+ editName: null,
135
+ editConfig: null,
136
+ editFieldIdx: 0,
137
+ editChanges: {}
138
+ }
139
+ }
140
+
141
+ // --- 启动向导 ---
142
+ export function startWizard(wiz, print) {
143
+ Object.assign(wiz, createWizardState())
144
+ wiz.active = true
145
+ wiz.step = "vendor"
146
+ print(buildVendorMenu())
147
+ }
148
+
149
+ // --- 启动编辑向导 ---
150
+ export function startEditWizard(wiz, name, existingCfg, print) {
151
+ Object.assign(wiz, createWizardState())
152
+ wiz.active = true
153
+ wiz.editMode = true
154
+ wiz.editName = name
155
+ wiz.editConfig = { ...existingCfg }
156
+ wiz.editFieldIdx = 0
157
+ wiz.editChanges = {}
158
+ wiz.step = "edit_field"
159
+ _printEditHeader(wiz, print)
160
+ _promptEditField(wiz, print)
161
+ }
162
+
163
+ function _printEditHeader(wiz, print) {
164
+ const cfg = wiz.editConfig
165
+ const keyDisplay = cfg.api_key ? cfg.api_key.slice(0, 8) + "..." : cfg.api_key_env ? `(env: ${cfg.api_key_env})` : "(未设置)"
166
+ const thinkingDisplay = cfg.thinking?.type === "enabled" ? `启用 (budget: ${cfg.thinking.budget_tokens || "默认"})` : "关闭"
167
+ const lines = [
168
+ "",
169
+ ` ── 编辑 Provider: ${wiz.editName} ──`,
170
+ "",
171
+ ` type: ${cfg.type || wiz.editName}`,
172
+ ` base_url: ${cfg.base_url || "(未设置)"}`,
173
+ ` api_key: ${keyDisplay}`,
174
+ ` default_model: ${cfg.default_model || "(未设置)"}`,
175
+ ` max_tokens: ${cfg.max_tokens || "(默认 16384)"}`,
176
+ ` context_limit: ${cfg.context_limit || "(未设置)"}`,
177
+ ` thinking: ${thinkingDisplay}`,
178
+ "",
179
+ " 逐项修改(输入 0 保留原值,输入新值覆盖,q 取消):",
180
+ ""
181
+ ]
182
+ print(lines.join("\n"))
183
+ }
184
+
185
+ function _promptEditField(wiz, print) {
186
+ const field = EDIT_FIELDS[wiz.editFieldIdx]
187
+ if (!field) return
188
+ const cfg = wiz.editConfig
189
+ let current
190
+ if (field.type === "secret") {
191
+ current = cfg[field.key] ? cfg[field.key].slice(0, 8) + "..." : "(未设置)"
192
+ } else if (field.type === "thinking") {
193
+ current = cfg.thinking?.type === "enabled"
194
+ ? `启用 (budget: ${cfg.thinking.budget_tokens || "默认"})`
195
+ : "关闭"
196
+ } else if (field.type === "provider_type") {
197
+ current = cfg[field.key] || wiz.editName
198
+ print(` ${field.label} [${current}](可选: openai-compatible, anthropic, openai, ollama;0=保留):`)
199
+ return
200
+ } else {
201
+ current = cfg[field.key] ?? "(未设置)"
202
+ }
203
+ print(` ${field.label} [${current}](0=保留):`)
204
+ }
205
+
206
+ function buildVendorMenu() {
207
+ const lines = ["", " ── 配置 Provider ──", ""]
208
+ VENDOR_KEYS.forEach((k, i) => {
209
+ lines.push(` ${i + 1}. ${VENDOR_PRESETS[k].label}`)
210
+ })
211
+ const base = VENDOR_KEYS.length
212
+ lines.push(` ${base + 1}. 自定义 OpenAI 兼容 API`)
213
+ lines.push(` ${base + 2}. 自定义 Anthropic 兼容 API`)
214
+ lines.push("")
215
+ lines.push(" 输入编号(q 取消):")
216
+ return lines.join("\n")
217
+ }
218
+
219
+ // --- 处理向导输入,返回 { done, cancelled, providerName } ---
220
+ export async function handleWizardInput(wiz, input, print) {
221
+ const val = String(input || "").trim()
222
+
223
+ if (val.toLowerCase() === "q" || val.toLowerCase() === "quit") {
224
+ wiz.active = false
225
+ print("已取消 provider 配置。")
226
+ return { done: true, cancelled: true }
227
+ }
228
+
229
+ switch (wiz.step) {
230
+ case "vendor": return _stepVendor(wiz, val, print)
231
+ case "protocol": return _stepProtocol(wiz, val, print)
232
+ case "custom_name": return _stepCustomName(wiz, val, print)
233
+ case "custom_url": return _stepCustomUrl(wiz, val, print)
234
+ case "apikey": return _stepApiKey(wiz, val, print)
235
+ case "model": return _stepModel(wiz, val, print)
236
+ case "context": return _stepContext(wiz, val, print)
237
+ case "thinking": return _stepThinking(wiz, val, print)
238
+ case "confirm": return _stepConfirm(wiz, val, print)
239
+ case "edit_field": return _stepEditField(wiz, val, print)
240
+ case "edit_thinking_budget": return _stepEditThinkingBudget(wiz, val, print)
241
+ case "edit_confirm": return _stepEditConfirm(wiz, val, print)
242
+ default:
243
+ wiz.active = false
244
+ return { done: true, cancelled: true }
245
+ }
246
+ }
247
+
248
+ function _stepProtocol(wiz, val, print) {
249
+ const protoKeys = Object.keys(wiz.preset.protocols)
250
+ const idx = parseInt(val, 10) - 1
251
+ if (isNaN(idx) || idx < 0 || idx >= protoKeys.length) {
252
+ print(` 请输入 1-${protoKeys.length} 之间的编号:`)
253
+ return { done: false }
254
+ }
255
+ const chosen = wiz.preset.protocols[protoKeys[idx]]
256
+ // 将选中的协议信息写入 preset(覆盖 type / base_url)
257
+ wiz.preset = { ...wiz.preset, type: chosen.type, base_url: chosen.base_url }
258
+ wiz.step = "apikey"
259
+ print(`\n 输入 Coding Plan 专属 API Key(sk-sp-xxx,0=跳过使用环境变量 ${wiz.preset.key_env}):`)
260
+ return { done: false }
261
+ }
262
+
263
+ function _stepVendor(wiz, val, print) {
264
+
265
+ const total = VENDOR_KEYS.length
266
+ const idx = parseInt(val, 10) - 1
267
+ if (isNaN(idx) || idx < 0 || idx > total + 1) {
268
+ print(` 请输入 1-${total + 2} 之间的编号:`)
269
+ return { done: false }
270
+ }
271
+ if (idx < total) {
272
+ wiz.vendorKey = VENDOR_KEYS[idx]
273
+ wiz.preset = VENDOR_PRESETS[wiz.vendorKey]
274
+ wiz.defaultModel = wiz.preset.default_model
275
+ if (wiz.preset.protocols) {
276
+ wiz.step = "protocol"
277
+ const protoKeys = Object.keys(wiz.preset.protocols)
278
+ const lines = ["\n 选择 API 协议:"]
279
+ protoKeys.forEach((k, i) => {
280
+ const p = wiz.preset.protocols[k]
281
+ lines.push(` ${i + 1}. ${k} 兼容 (${p.base_url})`)
282
+ })
283
+ lines.push("", " 输入编号:")
284
+ print(lines.join("\n"))
285
+ } else if (wiz.preset.type === "ollama") {
286
+ wiz.step = "model"
287
+ print(`\n 默认模型 [${wiz.defaultModel}](0=使用默认,或输入模型名):`)
288
+ } else {
289
+ wiz.step = "apikey"
290
+ print(`\n 输入 API Key(0=跳过,使用环境变量 ${wiz.preset.key_env}):`)
291
+ }
292
+ } else if (idx === total) {
293
+ wiz.isCustom = true
294
+ wiz.customType = "openai-compatible"
295
+ wiz.step = "custom_name"
296
+ print("\n 自定义 Provider 名称(如 moonshot、kimi):")
297
+ } else {
298
+ wiz.isCustom = true
299
+ wiz.customType = "anthropic"
300
+ wiz.step = "custom_name"
301
+ print("\n 自定义 Provider 名称(如 my-claude):")
302
+ }
303
+ return { done: false }
304
+ }
305
+
306
+ function _stepCustomName(wiz, val, print) {
307
+ if (!val) { print(" 名称不能为空:"); return { done: false } }
308
+ wiz.customName = val.replace(/[^a-z0-9_-]/gi, "_").toLowerCase()
309
+ wiz.step = "custom_url"
310
+ print("\n Base URL(如 https://api.example.com/v1):")
311
+ return { done: false }
312
+ }
313
+
314
+ function _stepCustomUrl(wiz, val, print) {
315
+ if (!val || !val.startsWith("http")) {
316
+ print(" 请输入有效 URL(以 http 开头):")
317
+ return { done: false }
318
+ }
319
+ wiz.customBaseUrl = val
320
+ wiz.step = "apikey"
321
+ print("\n 输入 API Key(0=跳过):")
322
+ return { done: false }
323
+ }
324
+
325
+ function _stepApiKey(wiz, val, print) {
326
+ wiz.apiKey = (val && val !== "0") ? val : null
327
+ wiz.step = "model"
328
+ const defModel = wiz.preset?.default_model || ""
329
+ print(`\n 默认模型${defModel ? ` [${defModel}]` : ""}(0=使用默认):`)
330
+ return { done: false }
331
+ }
332
+
333
+ function _stepModel(wiz, val, print) {
334
+ wiz.defaultModel = (val && val !== "0") ? val : (wiz.preset?.default_model || "")
335
+ wiz.step = "context"
336
+ const ctxHint = wiz.preset?.context_limit ? ` [${wiz.preset.context_limit}]` : ""
337
+ print(`\n 上下文长度(tokens,如 32768,0=使用默认${ctxHint}):`)
338
+ return { done: false }
339
+ }
340
+
341
+ function _stepContext(wiz, val, print) {
342
+ if (val && val !== "0") {
343
+ const n = parseInt(val, 10)
344
+ if (isNaN(n) || n < 1024) {
345
+ print(" 请输入有效整数(>= 1024),或输入 0 跳过:")
346
+ return { done: false }
347
+ }
348
+ wiz.contextLimit = n
349
+ } else if (wiz.preset?.context_limit) {
350
+ wiz.contextLimit = wiz.preset.context_limit
351
+ }
352
+ const supportsThinking = wiz.preset?.supports_thinking || false
353
+ if (supportsThinking) {
354
+ wiz.step = "thinking"
355
+ print("\n 启用 thinking 模式?(y/N):")
356
+ } else {
357
+ wiz.step = "confirm"
358
+ print(_buildConfirmPrompt(wiz))
359
+ }
360
+ return { done: false }
361
+ }
362
+
363
+ function _stepThinking(wiz, val, print) {
364
+ wiz.thinking = val.toLowerCase() === "y" || val.toLowerCase() === "yes"
365
+ wiz.step = "confirm"
366
+ print(_buildConfirmPrompt(wiz))
367
+ return { done: false }
368
+ }
369
+
370
+ async function _stepConfirm(wiz, val, print) {
371
+ if (val.toLowerCase() === "n" || val.toLowerCase() === "no") {
372
+ wiz.active = false
373
+ print("已取消。")
374
+ return { done: true, cancelled: true }
375
+ }
376
+ const cfg = _buildProviderConfig(wiz)
377
+ const name = wiz.customName || wiz.vendorKey
378
+ await _saveProviderConfig(cfg)
379
+ wiz.active = false
380
+ print(`\n 已保存 provider "${name}" 到 ${userConfigPathLabel()},已生效。`)
381
+ return { done: true, cancelled: false, providerName: name, configPatch: cfg }
382
+ }
383
+
384
+ function _buildConfirmPrompt(wiz) {
385
+ const name = wiz.customName || wiz.vendorKey
386
+ const keyDisplay = wiz.apiKey ? wiz.apiKey.slice(0, 8) + "..." : "(使用环境变量)"
387
+ const lines = [
388
+ "",
389
+ " 配置摘要:",
390
+ ` 名称: ${name}`,
391
+ ` 类型: ${wiz.preset?.type || wiz.customType}`,
392
+ ` Base URL: ${wiz.preset?.base_url || wiz.customBaseUrl}`,
393
+ ` API Key: ${keyDisplay}`,
394
+ ` 模型: ${wiz.defaultModel || "(未设置)"}`,
395
+ ` 上下文: ${wiz.contextLimit ? wiz.contextLimit + " tokens" : "(默认)"}`,
396
+ wiz.thinking ? " Thinking: 启用" : null,
397
+ "",
398
+ ` 保存到 ${userConfigPathLabel()}?(Y/n):`
399
+ ].filter(Boolean)
400
+ return lines.join("\n")
401
+ }
402
+
403
+ function _buildProviderConfig(wiz) {
404
+ const name = wiz.customName || wiz.vendorKey
405
+ const preset = wiz.preset
406
+ const entry = {}
407
+
408
+ if (wiz.isCustom) {
409
+ entry.type = wiz.customType
410
+ entry.base_url = wiz.customBaseUrl
411
+ } else {
412
+ // 内置 openai/anthropic/ollama 不需要 type 字段(直接用 key 名匹配)
413
+ // 但 Coding Plan 等带 protocols 的 preset 必须显式指定 type
414
+ const needExplicitType = preset.type !== "openai" && preset.type !== "anthropic" && preset.type !== "ollama"
415
+ const hasProtocols = VENDOR_PRESETS[wiz.vendorKey]?.protocols
416
+ if (needExplicitType || hasProtocols) {
417
+ entry.type = preset.type
418
+ }
419
+ entry.base_url = preset.base_url
420
+ }
421
+
422
+ if (wiz.apiKey) entry.api_key = wiz.apiKey
423
+ if (wiz.defaultModel) entry.default_model = wiz.defaultModel
424
+ if (wiz.contextLimit) entry.context_limit = wiz.contextLimit
425
+ if (wiz.thinking) entry.thinking = { type: "enabled", budget_tokens: 8000 }
426
+
427
+ return { provider: { default: name, [name]: entry } }
428
+ }
429
+
430
+ function _stepEditField(wiz, val, print) {
431
+ const field = EDIT_FIELDS[wiz.editFieldIdx]
432
+ if (val && val !== "0") {
433
+ if (field.type === "provider_type") {
434
+ const valid = ["openai", "openai-compatible", "anthropic", "ollama"]
435
+ if (!valid.includes(val)) {
436
+ print(` 无效类型,可选: ${valid.join(", ")};输入 0 保留:`)
437
+ return { done: false }
438
+ }
439
+ wiz.editChanges[field.key] = val
440
+ } else if (field.type === "int") {
441
+ const n = parseInt(val, 10)
442
+ if (isNaN(n) || (field.min && n < field.min)) {
443
+ print(` 请输入有效整数${field.min ? ` (>= ${field.min})` : ""},或输入 0 跳过:`)
444
+ return { done: false }
445
+ }
446
+ wiz.editChanges[field.key] = n
447
+ } else if (field.type === "thinking") {
448
+ const yes = val.toLowerCase() === "y" || val.toLowerCase() === "yes"
449
+ if (yes) {
450
+ // 继续问 budget
451
+ wiz.step = "edit_thinking_budget"
452
+ const cur = wiz.editConfig.thinking?.budget_tokens
453
+ print(` thinking budget_tokens [${cur || 10000}](0=保留):`)
454
+ return { done: false }
455
+ } else {
456
+ wiz.editChanges.thinking = null
457
+ }
458
+ } else {
459
+ wiz.editChanges[field.key] = val
460
+ }
461
+ }
462
+ // 下一个字段
463
+ wiz.editFieldIdx++
464
+ if (wiz.editFieldIdx >= EDIT_FIELDS.length) {
465
+ wiz.step = "edit_confirm"
466
+ _printEditSummary(wiz, print)
467
+ return { done: false }
468
+ }
469
+ _promptEditField(wiz, print)
470
+ return { done: false }
471
+ }
472
+
473
+ function _stepEditThinkingBudget(wiz, val, print) {
474
+ if (val === "0") val = ""
475
+ const budget = val ? parseInt(val, 10) : (wiz.editConfig.thinking?.budget_tokens || 10000)
476
+ if (val && (isNaN(budget) || budget < 0)) {
477
+ print(" 请输入有效整数 (>= 0),或输入 0 保留:")
478
+ return { done: false }
479
+ }
480
+ wiz.editChanges.thinking = { type: "enabled", budget_tokens: budget }
481
+ wiz.editFieldIdx++
482
+ if (wiz.editFieldIdx >= EDIT_FIELDS.length) {
483
+ wiz.step = "edit_confirm"
484
+ _printEditSummary(wiz, print)
485
+ return { done: false }
486
+ }
487
+ wiz.step = "edit_field"
488
+ _promptEditField(wiz, print)
489
+ return { done: false }
490
+ }
491
+
492
+ function _printEditSummary(wiz, print) {
493
+ const keys = Object.keys(wiz.editChanges)
494
+ if (!keys.length) {
495
+ print("\n 未做任何修改。")
496
+ wiz.active = false
497
+ return
498
+ }
499
+ const lines = ["", " 修改摘要:"]
500
+ for (const k of keys) {
501
+ const v = wiz.editChanges[k]
502
+ if (k === "api_key" && v) {
503
+ lines.push(` ${k}: ${v.slice(0, 8)}...`)
504
+ } else if (k === "thinking") {
505
+ lines.push(` ${k}: ${v ? `启用 (budget: ${v.budget_tokens})` : "关闭"}`)
506
+ } else {
507
+ lines.push(` ${k}: ${v}`)
508
+ }
509
+ }
510
+ lines.push("", " 保存修改?(Y/n):")
511
+ print(lines.join("\n"))
512
+ }
513
+
514
+ async function _stepEditConfirm(wiz, val, print) {
515
+ if (val.toLowerCase() === "n" || val.toLowerCase() === "no") {
516
+ wiz.active = false
517
+ print("已取消。")
518
+ return { done: true, cancelled: true }
519
+ }
520
+ // 合并修改到已有配置并保存
521
+ const merged = { ...wiz.editConfig }
522
+ for (const [k, v] of Object.entries(wiz.editChanges)) {
523
+ if (v === null) {
524
+ delete merged[k]
525
+ } else {
526
+ merged[k] = v
527
+ }
528
+ }
529
+ const saveCfg = { provider: { [wiz.editName]: merged } }
530
+ await _saveProviderConfig(saveCfg, false)
531
+ wiz.active = false
532
+ print(`\n 已更新 provider "${wiz.editName}" 到 ${userConfigPathLabel()},已生效。`)
533
+ return { done: true, cancelled: false, providerName: wiz.editName, configPatch: saveCfg }
534
+ }
535
+
536
+ async function _saveProviderConfig(newCfg, setDefault = true) {
537
+ const configPath = path.join(userRootDir(), "config.yaml")
538
+ await mkdir(path.dirname(configPath), { recursive: true })
539
+
540
+ let existing = {}
541
+ try {
542
+ const raw = await readFile(configPath, "utf8")
543
+ existing = YAML.parse(raw) || {}
544
+ } catch {
545
+ // 文件不存在,从空对象开始
546
+ }
547
+
548
+ if (!existing.provider) existing.provider = {}
549
+ // 合并 provider 配置,保留已有的其他 provider
550
+ Object.assign(existing.provider, newCfg.provider)
551
+ if (setDefault && newCfg.provider.default) {
552
+ existing.provider.default = newCfg.provider.default
553
+ }
554
+
555
+ await writeFile(configPath, YAML.stringify(existing), "utf8")
556
+ }
@@ -0,0 +1,30 @@
1
+ export async function buildCapabilitySnapshot({
2
+ mode,
3
+ cwd = process.cwd(),
4
+ configState,
5
+ customCommands = [],
6
+ skillRegistry,
7
+ toolRegistry,
8
+ mcpRegistry,
9
+ listAgents = () => []
10
+ }) {
11
+ const skills = skillRegistry.isReady() ? skillRegistry.list() : []
12
+ const tools = await toolRegistry.list({
13
+ mode,
14
+ cwd,
15
+ config: configState?.config
16
+ }).catch(() => [])
17
+ const mcpEntries = mcpRegistry.healthSnapshot()
18
+ const healthyMcp = mcpEntries.filter((entry) => entry.ok).length
19
+ const agents = listAgents() || []
20
+
21
+ return {
22
+ mode,
23
+ customCommands: customCommands.length,
24
+ skills: skills.length,
25
+ tools: tools.length,
26
+ mcpServers: mcpEntries.length,
27
+ healthyMcp,
28
+ agents: agents.length
29
+ }
30
+ }
@@ -0,0 +1,23 @@
1
+ export function renderInstalledCommandSurface({ customCommands = [], skills = [] } = {}) {
2
+ const lines = []
3
+ if (!customCommands.length && !skills.length) return ["no custom commands or skills found"]
4
+
5
+ if (customCommands.length) {
6
+ lines.push("custom commands:")
7
+ customCommands.forEach((cmd) => lines.push(` /${cmd.name} (${cmd.scope}) -> ${cmd.source}`))
8
+ }
9
+
10
+ const nonTemplateSkills = skills.filter((skill) => skill.type !== "template")
11
+ if (nonTemplateSkills.length) {
12
+ lines.push("skills:")
13
+ nonTemplateSkills.forEach((skill) =>
14
+ lines.push(` /${skill.name} (${skill.type}${skill.scope ? ", " + skill.scope : ""})`)
15
+ )
16
+ }
17
+
18
+ return lines
19
+ }
20
+
21
+ export function describeReloadSummary({ commandCount, skillCount, agentCount }) {
22
+ return `reloaded commands: ${commandCount}, skills: ${skillCount}, agents: ${agentCount}`
23
+ }
@@ -0,0 +1,40 @@
1
+ export async function runReplController({
2
+ ctx,
3
+ state,
4
+ providersConfigured,
5
+ customCommands,
6
+ recentSessions,
7
+ historyLines,
8
+ mcpStatusLines = [],
9
+ startTuiRepl,
10
+ startLineRepl,
11
+ clearScreenFn,
12
+ stdout = process.stdout,
13
+ stdin = process.stdin,
14
+ log = console.log
15
+ }) {
16
+ if (stdout.isTTY && stdin.isTTY) {
17
+ await startTuiRepl({
18
+ ctx,
19
+ state,
20
+ providersConfigured,
21
+ customCommands,
22
+ recentSessions,
23
+ historyLines,
24
+ mcpStatusLines
25
+ })
26
+ return "tui"
27
+ }
28
+
29
+ clearScreenFn(stdout)
30
+ for (const line of mcpStatusLines) log(line)
31
+ await startLineRepl({
32
+ ctx,
33
+ state,
34
+ providersConfigured,
35
+ customCommands,
36
+ recentSessions,
37
+ historyLines
38
+ })
39
+ return "line"
40
+ }