kc-beta 0.1.2 → 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.
package/src/cli/index.js CHANGED
@@ -29,11 +29,23 @@ function App({ engine, config }) {
29
29
  const [sessionId, setSessionId] = useState(engine.workspace.sessionId);
30
30
  const [phase, setPhase] = useState(engine.currentPhase);
31
31
  const [showWelcome, setShowWelcome] = useState(true);
32
+ const [spinnerStatus, setSpinnerStatus] = useState(null);
33
+ const [contextTokens, setContextTokens] = useState(0);
34
+ const [contextLimit, setContextLimit] = useState(config.kcContextLimit || 200000);
32
35
 
33
36
  const engineRef = useRef(engine);
34
37
  const streamingRef = useRef(false);
35
38
  const queueRef = useRef([]);
36
39
 
40
+ // Update context stats
41
+ const updateContextStats = useCallback(() => {
42
+ try {
43
+ const stats = engineRef.current.getContextStats();
44
+ setContextTokens(stats.totalTokens);
45
+ setContextLimit(stats.limit);
46
+ } catch { /* ignore */ }
47
+ }, []);
48
+
37
49
  const addMessage = useCallback((msg) => {
38
50
  setMessages((prev) => [...prev, msg]);
39
51
  }, []);
@@ -43,6 +55,7 @@ function App({ engine, config }) {
43
55
  setStreaming(true);
44
56
  setStreamingText("");
45
57
  setCurrentTool(null);
58
+ setSpinnerStatus("Thinking...");
46
59
 
47
60
  let accumulated = "";
48
61
 
@@ -52,6 +65,7 @@ function App({ engine, config }) {
52
65
  case "text_delta":
53
66
  accumulated += event.text ?? "";
54
67
  setStreamingText(accumulated);
68
+ setSpinnerStatus("Thinking...");
55
69
  break;
56
70
 
57
71
  case "turn_complete":
@@ -61,6 +75,8 @@ function App({ engine, config }) {
61
75
  accumulated = "";
62
76
  setStreamingText("");
63
77
  setCurrentTool(null);
78
+ setSpinnerStatus(null);
79
+ updateContextStats();
64
80
  break;
65
81
 
66
82
  case "tool_start":
@@ -71,6 +87,7 @@ function App({ engine, config }) {
71
87
  setStreamingText("");
72
88
  }
73
89
  setCurrentTool({ name: event.name, input: event.input, output: null, isError: false, isRunning: true });
90
+ setSpinnerStatus(`Running ${event.name}...`);
74
91
  break;
75
92
 
76
93
  case "tool_result":
@@ -83,6 +100,7 @@ function App({ engine, config }) {
83
100
  toolIsError: event.isError,
84
101
  });
85
102
  setCurrentTool(null);
103
+ setSpinnerStatus("Analyzing results...");
86
104
  break;
87
105
 
88
106
  case "pipeline_event": {
@@ -103,13 +121,15 @@ function App({ engine, config }) {
103
121
 
104
122
  streamingRef.current = false;
105
123
  setStreaming(false);
124
+ setSpinnerStatus(null);
125
+ updateContextStats();
106
126
 
107
127
  // Process queue
108
128
  if (queueRef.current.length > 0) {
109
129
  const next = queueRef.current.shift();
110
130
  runTurn(next);
111
131
  }
112
- }, [addMessage]);
132
+ }, [addMessage, updateContextStats]);
113
133
 
114
134
  const handleSlashCommand = useCallback((text) => {
115
135
  const parts = text.split(/\s+/);
@@ -125,6 +145,7 @@ function App({ engine, config }) {
125
145
  " /help Show this help\n" +
126
146
  " /status Show session info, model, phase, workspace\n" +
127
147
  " /clear Clear conversation history (keep workspace)\n" +
148
+ " /compact Summarize older messages to reduce context\n" +
128
149
  " /sessions List all sessions\n" +
129
150
  " /resume <name> Resume a previous session\n" +
130
151
  " /rename <name> Rename current session\n" +
@@ -132,26 +153,53 @@ function App({ engine, config }) {
132
153
  });
133
154
  return true;
134
155
 
135
- case "/status":
156
+ case "/status": {
157
+ const stats = engineRef.current.getContextStats();
136
158
  addMessage({
137
159
  role: "system",
138
160
  content:
139
161
  `Session: ${engineRef.current.workspace.sessionId}\n` +
140
162
  `Phase: ${engineRef.current.currentPhase.toUpperCase()}\n` +
141
163
  `Model: ${config.kcModel}\n` +
164
+ `Provider: ${config.provider || "unknown"}\n` +
142
165
  `LLM URL: ${config.llmBaseUrl}\n` +
143
166
  `Workspace: ${engineRef.current.workspace.cwd}\n` +
144
167
  `Tools: ${engineRef.current.toolRegistry.size} registered\n` +
145
- `History: ${engineRef.current.history.messages.length} messages`,
168
+ `History: ${engineRef.current.history.messages.length} messages\n` +
169
+ `Context: ~${stats.totalTokens} tokens (${stats.percentage}% of ${stats.limit})`,
146
170
  });
147
171
  return true;
172
+ }
148
173
 
149
174
  case "/clear":
150
175
  engineRef.current.history = new ConversationHistory(engineRef.current.workspace.cwd);
151
176
  setMessages([]);
152
177
  addMessage({ role: "system", content: "Conversation cleared. Workspace and pipeline state preserved." });
178
+ updateContextStats();
153
179
  return true;
154
180
 
181
+ case "/compact": {
182
+ addMessage({ role: "system", content: "Compacting conversation history..." });
183
+ // Run compact asynchronously
184
+ (async () => {
185
+ try {
186
+ const result = await engineRef.current.compact();
187
+ if (result) {
188
+ addMessage({
189
+ role: "system",
190
+ content: `Compacted: removed ${result.removedCount} messages, kept ${result.retainedCount}. Summary: ~${result.summaryTokens} tokens.`,
191
+ });
192
+ } else {
193
+ addMessage({ role: "system", content: "Nothing to compact (conversation is short enough)." });
194
+ }
195
+ updateContextStats();
196
+ } catch (err) {
197
+ addMessage({ role: "system", content: `Compact failed: ${err.message}` });
198
+ }
199
+ })();
200
+ return true;
201
+ }
202
+
155
203
  case "/rename":
156
204
  if (!arg) {
157
205
  addMessage({ role: "system", content: "Usage: /rename <new_name>" });
@@ -193,19 +241,46 @@ function App({ engine, config }) {
193
241
  addMessage({ role: "system", content: "Sessions:\n" + lines.join("\n") + "\n\nUsage: /resume <name>" });
194
242
  }
195
243
  } else {
196
- addMessage({ role: "system", content: `Resuming session: ${arg} (not yet implemented)` });
244
+ // Resume a previous session
245
+ (async () => {
246
+ try {
247
+ const client = new LLMClient({
248
+ apiKey: config.llmApiKey,
249
+ baseUrl: config.llmBaseUrl,
250
+ authType: config.authType,
251
+ apiFormat: config.apiFormat,
252
+ });
253
+ const resumed = await AgentEngine.resume({ client, config, sessionId: arg });
254
+ engineRef.current = resumed;
255
+ setSessionId(resumed.workspace.sessionId);
256
+ setPhase(resumed.currentPhase);
257
+ setMessages([]);
258
+ addMessage({
259
+ role: "system",
260
+ content:
261
+ `Resumed session: ${arg}\n` +
262
+ `Phase: ${resumed.currentPhase.toUpperCase()}\n` +
263
+ `History: ${resumed.history.messages.length} messages restored`,
264
+ });
265
+ updateContextStats();
266
+ } catch (err) {
267
+ addMessage({ role: "system", content: `Resume failed: ${err.message}` });
268
+ }
269
+ })();
197
270
  }
198
271
  return true;
199
272
 
200
273
  case "/exit":
201
274
  case "/quit":
275
+ // Save state before exit
276
+ try { engineRef.current.saveState(); } catch { /* ignore */ }
202
277
  exit();
203
278
  return true;
204
279
 
205
280
  default:
206
281
  return false;
207
282
  }
208
- }, [addMessage, config, exit]);
283
+ }, [addMessage, config, exit, updateContextStats]);
209
284
 
210
285
  const handleSubmit = useCallback((text) => {
211
286
  const trimmed = text.trim();
@@ -233,10 +308,12 @@ function App({ engine, config }) {
233
308
  queueRef.current.length = 0;
234
309
  addMessage({ role: "system", content: "[Queue cleared]" });
235
310
  } else {
311
+ try { engineRef.current.saveState(); } catch { /* ignore */ }
236
312
  exit();
237
313
  }
238
314
  }
239
315
  if (key.ctrl && input === "d") {
316
+ try { engineRef.current.saveState(); } catch { /* ignore */ }
240
317
  exit();
241
318
  }
242
319
  });
@@ -291,9 +368,9 @@ function App({ engine, config }) {
291
368
  isRunning: true,
292
369
  }) : null,
293
370
 
294
- // Spinner while waiting
295
- streaming && !streamingText && !currentTool
296
- ? h(CookingSpinner)
371
+ // Activity indicator while KC is working
372
+ streaming
373
+ ? h(CookingSpinner, { status: spinnerStatus })
297
374
  : null,
298
375
 
299
376
  // Separator + Input
@@ -305,13 +382,18 @@ function App({ engine, config }) {
305
382
  isActive: !streaming,
306
383
  }),
307
384
  h(HRule),
308
- h(StatusBar, { sessionId, phase }),
385
+ h(StatusBar, { sessionId, phase, contextTokens, contextLimit }),
309
386
  );
310
387
  }
311
388
 
312
- export async function main() {
389
+ export async function main({ languageOverride } = {}) {
313
390
  const config = loadSettings();
314
391
 
392
+ // Session-only language override (does NOT persist to config)
393
+ if (languageOverride) {
394
+ config.language = languageOverride;
395
+ }
396
+
315
397
  if (!config.llmApiKey) {
316
398
  console.error("Error: No API key configured. Run 'kc-beta onboard' first.");
317
399
  process.exit(1);
@@ -320,10 +402,17 @@ export async function main() {
320
402
  const client = new LLMClient({
321
403
  apiKey: config.llmApiKey,
322
404
  baseUrl: config.llmBaseUrl,
405
+ authType: config.authType,
406
+ apiFormat: config.apiFormat,
323
407
  });
324
408
 
325
409
  const engine = new AgentEngine({ client, config });
326
410
 
411
+ // Save state on process exit
412
+ const saveOnExit = () => { try { engine.saveState(); } catch { /* ignore */ } };
413
+ process.on("SIGINT", saveOnExit);
414
+ process.on("SIGTERM", saveOnExit);
415
+
327
416
  const instance = render(h(App, { engine, config }));
328
417
  await instance.waitUntilExit();
329
418
  }
@@ -2,6 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import readline from "node:readline";
4
4
  import os from "node:os";
5
+ import { getProviders, getProviderById, getProviderLabels, classifyModels, getCuratedModels } from "../providers.js";
6
+ import { LLMClient } from "../agent/llm-client.js";
5
7
 
6
8
  const CONFIG_DIR = path.join(os.homedir(), ".kc_agent");
7
9
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
@@ -23,27 +25,29 @@ const L = {
23
25
  langPrompt: "Language",
24
26
  langOptions: ["English", "中文"],
25
27
  providerPrompt: "LLM Provider",
26
- providerLabels: [
27
- "SiliconFlow (recommended for China)",
28
- "Aliyun Bailian",
29
- "Anthropic",
30
- "OpenAI",
31
- "Custom (enter base URL)",
32
- ],
33
28
  current: "current",
34
29
  choose: "Choose",
35
30
  baseUrl: "Base URL",
36
31
  baseUrlRequired: "Base URL is required for custom provider.",
37
32
  apiKey: "API Key",
38
33
  apiKeyRequired: "required",
39
- apiKeyKeep: "Enter to keep",
34
+ apiKeyKeep: "Press Enter to keep",
40
35
  apiKeyMissing: "API key is required. Run 'kc-beta onboard' again.",
36
+ keyType: "Key Type",
37
+ keyTypeOptions: ["API Key (pay-per-use)", "Coding Plan Key (subscription)"],
41
38
  conductorModel: "Conductor Model",
42
39
  workerTiers: "Worker LLM Tiers",
43
- tierHint: "Enter to accept defaults",
40
+ tierHint: "Press Enter to accept defaults",
44
41
  accuracy: "Accuracy Threshold",
45
42
  saved: "Saved to",
46
43
  runHint: "Run {cmd} to start the agent.",
44
+ discovering: "Discovering available models...",
45
+ discoveryFailed: "Could not auto-discover models. Using provider defaults.",
46
+ discoveryFound: "Found {n} models. Suggested tier assignments:",
47
+ discoveryAccept: "Press Enter to accept, or type model name to override",
48
+ enterSkip: "Press Enter to skip",
49
+ enterDefault: "Press Enter to use default",
50
+ bedrockWarn: "AWS Bedrock is not yet fully supported. Authentication will fail at runtime.",
47
51
  },
48
52
  zh: {
49
53
  title: "KC Agent 配置向导",
@@ -51,13 +55,6 @@ const L = {
51
55
  langPrompt: "语言",
52
56
  langOptions: ["English", "中文"],
53
57
  providerPrompt: "大模型服务商",
54
- providerLabels: [
55
- "SiliconFlow(国内推荐)",
56
- "阿里云百炼",
57
- "Anthropic",
58
- "OpenAI",
59
- "自定义(输入接口地址)",
60
- ],
61
58
  current: "当前",
62
59
  choose: "选择",
63
60
  baseUrl: "接口地址",
@@ -66,31 +63,33 @@ const L = {
66
63
  apiKeyRequired: "必填",
67
64
  apiKeyKeep: "回车保留当前密钥",
68
65
  apiKeyMissing: "API 密钥为必填项。请重新运行 'kc-beta onboard'。",
66
+ keyType: "密钥类型",
67
+ keyTypeOptions: ["API Key(按量付费)", "Coding Plan Key(包年包月)"],
69
68
  conductorModel: "主模型",
70
69
  workerTiers: "Worker 模型分层",
71
70
  tierHint: "回车接受默认值",
72
71
  accuracy: "准确率阈值",
73
72
  saved: "已保存至",
74
73
  runHint: "运行 {cmd} 启动 Agent。",
74
+ discovering: "正在发现可用模型...",
75
+ discoveryFailed: "无法自动发现模型,使用默认配置。",
76
+ discoveryFound: "发现 {n} 个模型。建议分层:",
77
+ discoveryAccept: "回车接受,或输入模型名称覆盖",
78
+ enterSkip: "回车跳过",
79
+ enterDefault: "回车使用默认值",
80
+ bedrockWarn: "AWS Bedrock 尚未完全支持。运行时认证将失败。",
75
81
  },
76
82
  };
77
83
 
78
- const PROVIDERS = [
79
- { name: "SiliconFlow", base_url: "https://api.siliconflow.cn/v1", model: "Pro/zai-org/GLM-5",
80
- tiers: { tier1: "Pro/zai-org/GLM-5, Pro/moonshotai/Kimi-K2.5", tier2: "Pro/deepseek-ai/DeepSeek-V3.2, Pro/MiniMaxAI/MiniMax-M2.5", tier3: "Qwen/Qwen3.5-122B-A10B", tier4: "Qwen/Qwen3.5-35B-A3B" } },
81
- { name: "Aliyun", base_url: "https://coding.dashscope.aliyuncs.com/v1", model: "glm-5",
82
- tiers: { tier1: "glm-5", tier2: "deepseek-v3", tier3: "qwen-plus", tier4: "qwen-turbo" } },
83
- { name: "Anthropic", base_url: "https://api.anthropic.com/v1", model: "claude-sonnet-4-20250514",
84
- tiers: { tier1: "claude-sonnet-4-20250514", tier2: "claude-sonnet-4-20250514", tier3: "claude-haiku-4-5-20251001", tier4: "claude-haiku-4-5-20251001" } },
85
- { name: "OpenAI", base_url: "https://api.openai.com/v1", model: "gpt-4o",
86
- tiers: { tier1: "gpt-4o", tier2: "gpt-4o-mini", tier3: "gpt-4o-mini", tier4: "gpt-4o-mini" } },
87
- { name: "Custom", base_url: "", model: "", tiers: { tier1: "", tier2: "", tier3: "", tier4: "" } },
88
- ];
89
-
90
- function ask(rl, question, defaultValue = "") {
84
+ function ask(rl, question, defaultValue = "", hint = "") {
91
85
  const suffix = defaultValue ? ` ${DIM}[${defaultValue}]${RESET}` : "";
86
+ const hintText = hint
87
+ ? ` ${GRAY}(${hint})${RESET}`
88
+ : defaultValue
89
+ ? ` ${GRAY}(Press Enter to keep)${RESET}`
90
+ : "";
92
91
  return new Promise((resolve) => {
93
- rl.question(`${question}${suffix}: `, (answer) => resolve(answer.trim() || defaultValue));
92
+ rl.question(`${question}${suffix}${hintText}: `, (answer) => resolve(answer.trim() || defaultValue));
94
93
  });
95
94
  }
96
95
 
@@ -101,11 +100,14 @@ export async function onboard() {
101
100
  if (fs.existsSync(CONFIG_PATH)) {
102
101
  try { existing = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")); } catch { /* ignore */ }
103
102
  }
103
+ const isUpdate = Object.keys(existing).length > 0;
104
104
 
105
105
  console.log();
106
106
  console.log(` ${BOLD}KC Agent Setup / KC Agent 配置向导${RESET}`);
107
107
  console.log(` ${GRAY}${"─".repeat(40)}${RESET}`);
108
108
  console.log();
109
+
110
+ // --- Language ---
109
111
  console.log(` ${CYAN}Language / 语言:${RESET}`);
110
112
  console.log(` 1. English`);
111
113
  console.log(` 2. 中文`);
@@ -115,67 +117,155 @@ export async function onboard() {
115
117
  const t = L[lang];
116
118
  console.log();
117
119
 
118
- if (Object.keys(existing).length > 0) {
120
+ if (isUpdate) {
119
121
  console.log(` ${DIM}${t.existingConfig}${RESET}`);
120
122
  console.log();
121
123
  }
122
124
 
125
+ // --- Provider ---
126
+ const providers = getProviders();
127
+ const labels = getProviderLabels(lang);
123
128
  console.log(` ${CYAN}${t.providerPrompt}:${RESET}`);
124
- for (let i = 0; i < PROVIDERS.length; i++) {
125
- const marker = PROVIDERS[i].name.toLowerCase() === existing.provider ? ` ${GREEN}(${t.current})${RESET}` : "";
126
- console.log(` ${i + 1}. ${t.providerLabels[i]}${marker}`);
129
+ for (let i = 0; i < labels.length; i++) {
130
+ const marker = providers[i].id === existing.provider ? ` ${GREEN}(${t.current})${RESET}` : "";
131
+ console.log(` ${i + 1}. ${labels[i].label}${marker}`);
127
132
  }
128
133
  const providerIdx = parseInt(await ask(rl, ` ${GRAY}>${RESET} ${t.choose}`, "1"), 10) - 1;
129
- const provider = PROVIDERS[Math.max(0, Math.min(providerIdx, PROVIDERS.length - 1))];
134
+ const provider = providers[Math.max(0, Math.min(providerIdx, providers.length - 1))];
130
135
  console.log();
131
136
 
132
- let baseUrl = provider.base_url;
133
- if (provider.name === "Custom") {
137
+ // Bedrock warning
138
+ if (provider.id === "bedrock") {
139
+ console.log(` ${YELLOW}⚠ ${t.bedrockWarn}${RESET}`);
140
+ console.log();
141
+ }
142
+
143
+ // --- Base URL ---
144
+ let baseUrl = provider.baseUrl;
145
+ if (provider.id === "custom") {
134
146
  baseUrl = await ask(rl, ` ${t.baseUrl}`, existing.base_url || "");
135
147
  if (!baseUrl) { console.log(` ${RED}${t.baseUrlRequired}${RESET}`); rl.close(); process.exit(1); }
148
+ console.log();
136
149
  }
137
150
 
151
+ // --- Aliyun coding plan key sub-option ---
152
+ let useCodingPlan = false;
153
+ if (provider.supportsCodingPlanKey) {
154
+ console.log(` ${CYAN}${t.keyType}:${RESET}`);
155
+ console.log(` 1. ${t.keyTypeOptions[0]}`);
156
+ console.log(` 2. ${t.keyTypeOptions[1]}`);
157
+ const keyTypeChoice = await ask(rl, ` ${GRAY}>${RESET} ${t.choose}`, "1");
158
+ useCodingPlan = keyTypeChoice === "2";
159
+ if (useCodingPlan && provider.codingPlanUrl) {
160
+ baseUrl = provider.codingPlanUrl;
161
+ }
162
+ console.log();
163
+ }
164
+
165
+ // --- API Key ---
138
166
  const maskedExisting = existing.api_key ? existing.api_key.slice(0, 6) + "..." + existing.api_key.slice(-4) : "";
167
+ const keyHint = maskedExisting ? t.apiKeyKeep : t.apiKeyRequired;
139
168
  const keyPrompt = maskedExisting
140
- ? ` ${CYAN}${t.apiKey}${RESET} ${DIM}(${maskedExisting}, ${t.apiKeyKeep})${RESET}`
169
+ ? ` ${CYAN}${t.apiKey}${RESET} ${DIM}(${maskedExisting})${RESET}`
141
170
  : ` ${CYAN}${t.apiKey}${RESET} ${YELLOW}(${t.apiKeyRequired})${RESET}`;
142
- const apiKey = await ask(rl, keyPrompt, "");
171
+ const apiKey = await ask(rl, keyPrompt, "", keyHint);
143
172
  const finalKey = apiKey || existing.api_key || "";
144
173
  if (!finalKey) { console.log(` ${RED}${t.apiKeyMissing}${RESET}`); rl.close(); process.exit(1); }
145
174
  console.log();
146
175
 
147
- const defaultModel = provider.model || existing.conductor_model || "";
148
- const model = await ask(rl, ` ${CYAN}${t.conductorModel}${RESET}`, defaultModel);
176
+ // --- Auto-discovery ---
177
+ let discoveredModels = null;
178
+ let suggestedTiers = null;
179
+ let suggestedConductor = null;
180
+
181
+ // Try curated models first (for providers without /models endpoint)
182
+ const curated = getCuratedModels(provider.id);
183
+
184
+ if (curated) {
185
+ // Use curated model list
186
+ discoveredModels = curated;
187
+ const classified = classifyModels(curated);
188
+ suggestedTiers = classified.tiers;
189
+ suggestedConductor = classified.conductor;
190
+ console.log(` ${GREEN}✓${RESET} ${t.discoveryFound.replace("{n}", curated.length)}`);
191
+ if (suggestedConductor) {
192
+ console.log(` ${DIM}Conductor: ${suggestedConductor}${RESET}`);
193
+ }
194
+ for (const [tier, models] of Object.entries(suggestedTiers)) {
195
+ if (models) console.log(` ${DIM}${tier.toUpperCase()}: ${models}${RESET}`);
196
+ }
197
+ console.log();
198
+ } else if (provider.modelsEndpoint) {
199
+ // Query /models endpoint
200
+ console.log(` ${DIM}${t.discovering}${RESET}`);
201
+ try {
202
+ const tempClient = new LLMClient({
203
+ apiKey: finalKey,
204
+ baseUrl: baseUrl,
205
+ authType: provider.authType,
206
+ apiFormat: provider.apiFormat,
207
+ });
208
+ discoveredModels = await tempClient.listModels();
209
+
210
+ if (discoveredModels && discoveredModels.length > 0) {
211
+ const classified = classifyModels(discoveredModels);
212
+ suggestedTiers = classified.tiers;
213
+ suggestedConductor = classified.conductor;
214
+ console.log(` ${GREEN}✓${RESET} ${t.discoveryFound.replace("{n}", discoveredModels.length)}`);
215
+ if (suggestedConductor) {
216
+ console.log(` ${DIM}Conductor: ${suggestedConductor}${RESET}`);
217
+ }
218
+ for (const [tier, models] of Object.entries(suggestedTiers)) {
219
+ if (models) console.log(` ${DIM}${tier.toUpperCase()}: ${models}${RESET}`);
220
+ }
221
+ } else {
222
+ console.log(` ${DIM}${t.discoveryFailed}${RESET}`);
223
+ }
224
+ } catch {
225
+ console.log(` ${DIM}${t.discoveryFailed}${RESET}`);
226
+ }
227
+ console.log();
228
+ }
229
+
230
+ // --- Conductor model ---
231
+ const defaultModel = suggestedConductor || provider.defaultModel || existing.conductor_model || "";
232
+ const model = await ask(
233
+ rl,
234
+ ` ${CYAN}${t.conductorModel}${RESET}`,
235
+ defaultModel,
236
+ isUpdate ? t.enterDefault : "",
237
+ );
149
238
  console.log();
150
239
 
240
+ // --- Worker tiers ---
151
241
  console.log(` ${CYAN}${t.workerTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
152
242
  const tiers = {};
153
243
  for (const tier of ["tier1", "tier2", "tier3", "tier4"]) {
154
- const def = provider.tiers[tier] || existing?.tiers?.[tier] || "";
155
- tiers[tier] = await ask(rl, ` ${tier.toUpperCase()}`, def);
244
+ const def = suggestedTiers?.[tier] || provider.defaultTiers[tier] || existing?.tiers?.[tier] || "";
245
+ tiers[tier] = await ask(
246
+ rl,
247
+ ` ${tier.toUpperCase()}`,
248
+ def,
249
+ t.discoveryAccept ? "" : "",
250
+ );
156
251
  }
157
252
  console.log();
158
253
 
159
- const defaultAcc = existing.accuracy_threshold?.toString() || "0.9";
160
- const accuracy = parseFloat(await ask(rl, ` ${CYAN}${t.accuracy}${RESET}`, defaultAcc));
161
- console.log();
162
-
163
- // Advanced thresholds (Enter to keep defaults)
164
- const advLabel = lang === "zh" ? "高级阈值" : "Advanced Thresholds";
165
- const skipHint = lang === "zh" ? "回车使用默认值" : "Enter to keep defaults";
166
- console.log(` ${CYAN}${advLabel}${RESET} ${DIM}(${skipHint})${RESET}`);
167
- const systemicThreshold = parseFloat(await ask(rl, ` ${lang === "zh" ? "系统性问题阈值" : "Systemic threshold"}`, existing.systemic_threshold?.toString() || "0.10"));
168
- const spotCheckRate = parseFloat(await ask(rl, ` ${lang === "zh" ? "抽查比率" : "Spot-check rate"}`, existing.spot_check_rate?.toString() || "0.10"));
169
- const tierTolerance = parseFloat(await ask(rl, ` ${lang === "zh" ? "降级容差" : "Tier downgrade tolerance"}`, existing.tier_tolerance?.toString() || "0.05"));
170
- console.log();
171
-
172
254
  rl.close();
173
255
 
256
+ // Preserve existing thresholds or set defaults (editable via 'kc-beta config')
257
+ const accuracy = existing.accuracy_threshold ?? 0.9;
258
+ const systemicThreshold = existing.systemic_threshold ?? 0.10;
259
+ const spotCheckRate = existing.spot_check_rate ?? 0.10;
260
+ const tierTolerance = existing.tier_tolerance ?? 0.05;
261
+
174
262
  const config = {
175
263
  language: lang,
176
- provider: provider.name.toLowerCase(),
264
+ provider: provider.id,
177
265
  api_key: finalKey,
178
266
  base_url: baseUrl,
267
+ auth_type: provider.authType,
268
+ api_format: provider.apiFormat,
179
269
  conductor_model: model,
180
270
  tiers,
181
271
  accuracy_threshold: accuracy,
@@ -190,5 +280,9 @@ export async function onboard() {
190
280
  console.log(` ${GREEN}✓${RESET} ${t.saved} ${GRAY}${CONFIG_PATH}${RESET}`);
191
281
  console.log();
192
282
  console.log(` ${t.runHint.replace("{cmd}", `${BOLD}kc-beta${RESET}`)}`);
283
+ const configHint = lang === "zh"
284
+ ? ` ${DIM}运行 ${BOLD}kc-beta config${RESET}${DIM} 调整阈值和高级设置。${RESET}`
285
+ : ` ${DIM}Run ${BOLD}kc-beta config${RESET}${DIM} to adjust thresholds and advanced settings.${RESET}`;
286
+ console.log(configHint);
193
287
  console.log();
194
288
  }
package/src/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import { getProviderById } from "./providers.js";
4
5
 
5
6
  const GLOBAL_CONFIG_DIR = path.join(os.homedir(), ".kc_agent");
6
7
  const GLOBAL_CONFIG_PATH = path.join(GLOBAL_CONFIG_DIR, "config.json");
@@ -43,23 +44,29 @@ function loadEnvFile(envPath) {
43
44
 
44
45
  /**
45
46
  * Load settings by merging: global config (lowest) -> workspace .env (highest).
47
+ * Supports both new generic keys (LLM_API_KEY) and legacy keys (SILICONFLOW_API_KEY).
46
48
  * @param {string} [workspacePath] - Optional workspace directory for .env override
47
49
  */
48
50
  export function loadSettings(workspacePath) {
49
51
  const gc = loadGlobalConfig();
50
52
  const env = workspacePath ? loadEnvFile(path.join(workspacePath, ".env")) : {};
51
53
 
54
+ // Resolve provider metadata for authType/apiFormat defaults
55
+ const provider = gc.provider || "siliconflow";
56
+ const providerDef = getProviderById(provider);
57
+
52
58
  return {
53
- // Conductor LLM
54
- llmApiKey: env.SILICONFLOW_API_KEY || gc.api_key || "",
55
- llmBaseUrl: env.SILICONFLOW_BASE_URL || gc.base_url || "https://api.siliconflow.cn/v1",
59
+ // Provider identity
60
+ provider,
61
+ authType: gc.auth_type || providerDef?.authType || "bearer",
62
+ apiFormat: gc.api_format || providerDef?.apiFormat || "openai",
63
+
64
+ // Conductor LLM (generic keys with legacy fallback)
65
+ llmApiKey: env.LLM_API_KEY || env.SILICONFLOW_API_KEY || gc.api_key || "",
66
+ llmBaseUrl: env.LLM_BASE_URL || env.SILICONFLOW_BASE_URL || gc.base_url || "https://api.siliconflow.cn/v1",
56
67
  kcModel: gc.conductor_model || "glm-5",
57
68
  kcMaxTokens: 65536,
58
69
 
59
- // Worker LLMs (SiliconFlow)
60
- siliconflowApiKey: env.SILICONFLOW_API_KEY || gc.api_key || "",
61
- siliconflowBaseUrl: env.SILICONFLOW_BASE_URL || gc.base_url || "https://api.siliconflow.cn/v1",
62
-
63
70
  // Tier models (from .env or global config tiers)
64
71
  tier1: env.TIER1 || gc.tiers?.tier1 || "",
65
72
  tier2: env.TIER2 || gc.tiers?.tier2 || "",
@@ -90,6 +97,12 @@ export function loadSettings(workspacePath) {
90
97
  maxIterations: parseInt(env.MAX_ITERATIONS || "20", 10),
91
98
  monitorFrequency: env.MONITOR_FREQUENCY || "mid",
92
99
 
100
+ // Web search
101
+ tavilyApiKey: env.TAVILY_API_KEY || gc.tavily_api_key || "",
102
+
103
+ // Context management
104
+ kcContextLimit: parseInt(env.KC_CONTEXT_LIMIT || "200000", 10),
105
+
93
106
  // Language
94
107
  language: env.LANGUAGE || gc.language || "en",
95
108
  };