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/bin/kc-beta.js +14 -2
- package/package.json +1 -1
- package/src/agent/context-window.js +151 -0
- package/src/agent/engine.js +202 -5
- package/src/agent/event-log.js +111 -0
- package/src/agent/llm-client.js +352 -59
- package/src/agent/pipelines/base.js +6 -0
- package/src/agent/pipelines/distillation.js +18 -0
- package/src/agent/pipelines/extraction.js +21 -0
- package/src/agent/pipelines/initializer.js +22 -6
- package/src/agent/pipelines/production-qc.js +19 -0
- package/src/agent/pipelines/skill-authoring.js +14 -0
- package/src/agent/pipelines/skill-testing.js +20 -0
- package/src/agent/retry.js +83 -0
- package/src/agent/session-state.js +78 -0
- package/src/agent/token-counter.js +62 -0
- package/src/agent/tools/document-parse.js +3 -3
- package/src/agent/tools/web-search.js +107 -0
- package/src/agent/tools/worker-llm-call.js +14 -5
- package/src/cli/components.js +16 -4
- package/src/cli/config.js +246 -0
- package/src/cli/index.js +99 -10
- package/src/cli/onboard.js +151 -57
- package/src/config.js +20 -7
- package/src/providers.js +370 -0
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
|
-
|
|
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
|
-
//
|
|
295
|
-
streaming
|
|
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
|
}
|
package/src/cli/onboard.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 <
|
|
125
|
-
const marker =
|
|
126
|
-
console.log(` ${i + 1}. ${
|
|
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 =
|
|
134
|
+
const provider = providers[Math.max(0, Math.min(providerIdx, providers.length - 1))];
|
|
130
135
|
console.log();
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
if (provider.
|
|
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}
|
|
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
|
-
|
|
148
|
-
|
|
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.
|
|
155
|
-
tiers[tier] = await ask(
|
|
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.
|
|
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
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
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
|
};
|