@kkelly-offical/kkcode 0.1.7 → 0.2.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 (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -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/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2981
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +298 -298
  96. package/src/session/engine.mjs +417 -232
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1097
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -900
  105. package/src/session/loop.mjs +1005 -930
  106. package/src/session/prompt/agent.txt +25 -25
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +31 -31
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +196 -195
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -519
  116. package/src/session/system-prompt.mjs +308 -273
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +99 -93
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -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
+ }