kc-beta 0.1.2 → 0.3.0
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/context.js +8 -4
- package/src/agent/engine.js +261 -8
- 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 +75 -14
- 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 +79 -0
- package/src/agent/skill-loader.js +13 -1
- package/src/agent/token-counter.js +62 -0
- package/src/agent/tools/document-parse.js +104 -21
- package/src/agent/tools/document-search.js +24 -8
- package/src/agent/tools/sandbox-exec.js +16 -5
- package/src/agent/tools/web-search.js +107 -0
- package/src/agent/tools/worker-llm-call.js +14 -5
- package/src/agent/tools/workspace-file.js +47 -20
- package/src/agent/workspace.js +24 -1
- package/src/cli/components.js +24 -5
- package/src/cli/config.js +340 -0
- package/src/cli/index.js +113 -11
- package/src/cli/onboard.js +216 -53
- package/src/config.js +63 -10
- package/src/model-tiers.json +153 -0
- package/src/providers.js +367 -0
- package/template/AGENT.md +20 -0
- package/template/skills/en/meta/compliance-judgment/SKILL.md +10 -42
- package/template/skills/en/meta/document-chunking/SKILL.md +32 -0
- package/template/skills/en/meta/document-parsing/SKILL.md +11 -18
- package/template/skills/en/meta/entity-extraction/SKILL.md +13 -28
- package/template/skills/en/meta/tree-processing/SKILL.md +19 -1
- package/template/skills/en/meta-meta/auto-model-selection/SKILL.md +53 -0
- package/template/skills/en/meta-meta/pdf-review-dashboard/SKILL.md +57 -0
- package/template/skills/en/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
- package/template/skills/en/meta-meta/rule-extraction/SKILL.md +24 -1
- package/template/skills/en/meta-meta/skill-authoring/SKILL.md +6 -0
- package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +4 -0
- package/template/skills/zh/meta/compliance-judgment/SKILL.md +41 -262
- package/template/skills/zh/meta/document-chunking/SKILL.md +32 -0
- package/template/skills/zh/meta/document-parsing/SKILL.md +65 -132
- package/template/skills/zh/meta/entity-extraction/SKILL.md +68 -230
- package/template/skills/zh/meta/tree-processing/SKILL.md +82 -194
- package/template/skills/zh/meta-meta/auto-model-selection/SKILL.md +51 -0
- package/template/skills/zh/meta-meta/pdf-review-dashboard/SKILL.md +55 -0
- package/template/skills/zh/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
- package/template/skills/zh/meta-meta/rule-extraction/SKILL.md +79 -164
- package/template/skills/zh/meta-meta/skill-authoring/SKILL.md +64 -185
- package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +95 -216
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { getProviders, getProviderById, getProviderLabels } from "../providers.js";
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), ".kc_agent");
|
|
8
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
9
|
+
|
|
10
|
+
const ESC = "\x1b[";
|
|
11
|
+
const RESET = `${ESC}0m`;
|
|
12
|
+
const BOLD = `${ESC}1m`;
|
|
13
|
+
const DIM = `${ESC}2m`;
|
|
14
|
+
const GREEN = `${ESC}32m`;
|
|
15
|
+
const CYAN = `${ESC}36m`;
|
|
16
|
+
const GRAY = `${ESC}90m`;
|
|
17
|
+
const YELLOW = `${ESC}33m`;
|
|
18
|
+
const RED = `${ESC}31m`;
|
|
19
|
+
|
|
20
|
+
const L = {
|
|
21
|
+
en: {
|
|
22
|
+
title: "KC Agent Configuration",
|
|
23
|
+
noConfig: "No config found. Run 'kc-beta onboard' first.",
|
|
24
|
+
menu: "Configuration Categories",
|
|
25
|
+
choose: "Choose category (q to quit)",
|
|
26
|
+
categories: ["LLM Provider & API Key", "Model Tiers", "VLM Tiers (Vision/OCR)", "Worker LLM Provider", "Quality Thresholds", "Language"],
|
|
27
|
+
saved: "Saved.",
|
|
28
|
+
back: "← Back to menu",
|
|
29
|
+
enterKeep: "Press Enter to keep",
|
|
30
|
+
enterDefault: "Press Enter to use default",
|
|
31
|
+
currentValue: "current",
|
|
32
|
+
provider: "Provider",
|
|
33
|
+
baseUrl: "Base URL",
|
|
34
|
+
apiKey: "API Key",
|
|
35
|
+
conductor: "Conductor Model",
|
|
36
|
+
language: "Language",
|
|
37
|
+
langOptions: ["English", "中文"],
|
|
38
|
+
},
|
|
39
|
+
zh: {
|
|
40
|
+
title: "KC Agent 配置",
|
|
41
|
+
noConfig: "未找到配置。请先运行 'kc-beta onboard'。",
|
|
42
|
+
menu: "配置类别",
|
|
43
|
+
choose: "选择类别(q 退出)",
|
|
44
|
+
categories: ["大模型服务商 & API 密钥", "模型分层", "VLM 视觉模型分层", "Worker LLM 服务商", "质量阈值", "语言"],
|
|
45
|
+
saved: "已保存。",
|
|
46
|
+
back: "← 返回菜单",
|
|
47
|
+
enterKeep: "回车保留当前值",
|
|
48
|
+
enterDefault: "回车使用默认值",
|
|
49
|
+
currentValue: "当前",
|
|
50
|
+
provider: "服务商",
|
|
51
|
+
baseUrl: "接口地址",
|
|
52
|
+
apiKey: "API 密钥",
|
|
53
|
+
conductor: "主模型",
|
|
54
|
+
language: "语言",
|
|
55
|
+
langOptions: ["English", "中文"],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function loadConfig() {
|
|
60
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
61
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")); } catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function saveConfig(config) {
|
|
67
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
68
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ask(rl, question, defaultValue = "", hint = "") {
|
|
72
|
+
const suffix = defaultValue ? ` ${DIM}[${defaultValue}]${RESET}` : "";
|
|
73
|
+
const hintText = hint
|
|
74
|
+
? ` ${GRAY}(${hint})${RESET}`
|
|
75
|
+
: defaultValue
|
|
76
|
+
? ` ${GRAY}(Press Enter to keep)${RESET}`
|
|
77
|
+
: "";
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
rl.question(`${question}${suffix}${hintText}: `, (answer) => resolve(answer.trim() || defaultValue));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function maskKey(key) {
|
|
84
|
+
if (!key || key.length < 10) return key || "";
|
|
85
|
+
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Category 1: LLM Provider & API Key
|
|
90
|
+
*/
|
|
91
|
+
async function editProvider(rl, config, t) {
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(` ${BOLD}${t.categories[0]}${RESET}`);
|
|
94
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
95
|
+
|
|
96
|
+
// Show current
|
|
97
|
+
const currentProvider = getProviderById(config.provider);
|
|
98
|
+
console.log(` ${DIM}${t.currentValue}: ${config.provider} (${currentProvider?.name || "unknown"})${RESET}`);
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
// Provider selection
|
|
102
|
+
const providers = getProviders();
|
|
103
|
+
const labels = getProviderLabels(config.language || "en");
|
|
104
|
+
console.log(` ${CYAN}${t.provider}:${RESET}`);
|
|
105
|
+
for (let i = 0; i < labels.length; i++) {
|
|
106
|
+
const marker = providers[i].id === config.provider ? ` ${GREEN}(${t.currentValue})${RESET}` : "";
|
|
107
|
+
console.log(` ${i + 1}. ${labels[i].label}${marker}`);
|
|
108
|
+
}
|
|
109
|
+
const currentIdx = providers.findIndex((p) => p.id === config.provider);
|
|
110
|
+
const providerChoice = await ask(rl, ` ${GRAY}>${RESET} ${t.choose.split(" (")[0]}`, String(currentIdx + 1 || 1));
|
|
111
|
+
const provIdx = parseInt(providerChoice, 10) - 1;
|
|
112
|
+
const provider = providers[Math.max(0, Math.min(provIdx, providers.length - 1))];
|
|
113
|
+
config.provider = provider.id;
|
|
114
|
+
config.auth_type = provider.authType;
|
|
115
|
+
config.api_format = provider.apiFormat;
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
// Base URL
|
|
119
|
+
if (provider.id === "custom") {
|
|
120
|
+
config.base_url = await ask(rl, ` ${CYAN}${t.baseUrl}${RESET}`, config.base_url || "");
|
|
121
|
+
} else if (provider.supportsCodingPlanKey) {
|
|
122
|
+
// Providers with coding plan support — ask which key type
|
|
123
|
+
const keyTypeLabel = config.language === "zh" ? "密钥类型" : "Key Type";
|
|
124
|
+
const opt1 = config.language === "zh" ? "API Key(按量付费)" : "API Key (pay-per-use)";
|
|
125
|
+
const opt2 = config.language === "zh" ? "Coding Plan Key(包年包月)" : "Coding Plan Key (subscription)";
|
|
126
|
+
console.log(` ${CYAN}${keyTypeLabel}:${RESET}`);
|
|
127
|
+
const isCodingPlan = config.base_url === provider.codingPlanUrl;
|
|
128
|
+
console.log(` 1. ${opt1}${!isCodingPlan ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
|
|
129
|
+
console.log(` 2. ${opt2}${isCodingPlan ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
|
|
130
|
+
const keyTypeDefault = isCodingPlan ? "2" : "1";
|
|
131
|
+
const keyTypeChoice = await ask(rl, ` ${GRAY}>${RESET}`, keyTypeDefault);
|
|
132
|
+
config.base_url = keyTypeChoice === "2" ? provider.codingPlanUrl : provider.baseUrl;
|
|
133
|
+
console.log(` ${CYAN}${t.baseUrl}${RESET}: ${DIM}${config.base_url}${RESET}`);
|
|
134
|
+
} else {
|
|
135
|
+
// Keep existing base_url if it matches this provider, otherwise use default
|
|
136
|
+
const defaultUrl = provider.baseUrl;
|
|
137
|
+
console.log(` ${CYAN}${t.baseUrl}${RESET}: ${DIM}${defaultUrl}${RESET}`);
|
|
138
|
+
config.base_url = defaultUrl;
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
|
|
142
|
+
// API Key
|
|
143
|
+
const masked = maskKey(config.api_key);
|
|
144
|
+
const keyPrompt = masked
|
|
145
|
+
? ` ${CYAN}${t.apiKey}${RESET} ${DIM}(${masked})${RESET}`
|
|
146
|
+
: ` ${CYAN}${t.apiKey}${RESET}`;
|
|
147
|
+
const newKey = await ask(rl, keyPrompt, "", masked ? t.enterKeep : "");
|
|
148
|
+
if (newKey) config.api_key = newKey;
|
|
149
|
+
|
|
150
|
+
// Conductor model
|
|
151
|
+
console.log();
|
|
152
|
+
const defaultModel = provider.defaultModel || config.conductor_model || "";
|
|
153
|
+
config.conductor_model = await ask(rl, ` ${CYAN}${t.conductor}${RESET}`, config.conductor_model || defaultModel, t.enterKeep);
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Category 2: Model Tiers
|
|
159
|
+
*/
|
|
160
|
+
async function editTiers(rl, config, t) {
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(` ${BOLD}${t.categories[1]}${RESET}`);
|
|
163
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
164
|
+
console.log();
|
|
165
|
+
|
|
166
|
+
const tiers = config.tiers || {};
|
|
167
|
+
const provider = getProviderById(config.provider);
|
|
168
|
+
const defaults = provider?.defaultTiers || {};
|
|
169
|
+
|
|
170
|
+
for (const tier of ["tier1", "tier2", "tier3", "tier4"]) {
|
|
171
|
+
const current = tiers[tier] || defaults[tier] || "";
|
|
172
|
+
tiers[tier] = await ask(rl, ` ${CYAN}${tier.toUpperCase()}${RESET}`, current, t.enterKeep);
|
|
173
|
+
}
|
|
174
|
+
config.tiers = tiers;
|
|
175
|
+
console.log();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Category 3: VLM Tiers (Vision/OCR)
|
|
180
|
+
*/
|
|
181
|
+
async function editVlmTiers(rl, config, t) {
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(` ${BOLD}${t.categories[2]}${RESET}`);
|
|
184
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
185
|
+
console.log();
|
|
186
|
+
|
|
187
|
+
const vlmTiers = config.vlm_tiers || {};
|
|
188
|
+
const provider = getProviderById(config.provider);
|
|
189
|
+
const defaults = provider?.defaultVlm || {};
|
|
190
|
+
|
|
191
|
+
for (const tier of ["tier1", "tier2", "tier3"]) {
|
|
192
|
+
const current = vlmTiers[tier] || defaults[tier] || "";
|
|
193
|
+
vlmTiers[tier] = await ask(rl, ` ${CYAN}${tier.toUpperCase()}${RESET}`, current, t.enterKeep);
|
|
194
|
+
}
|
|
195
|
+
config.vlm_tiers = vlmTiers;
|
|
196
|
+
console.log();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Category 4: Worker LLM Provider
|
|
201
|
+
*/
|
|
202
|
+
async function editWorkerProvider(rl, config, t) {
|
|
203
|
+
console.log();
|
|
204
|
+
console.log(` ${BOLD}${t.categories[3]}${RESET}`);
|
|
205
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
206
|
+
console.log();
|
|
207
|
+
|
|
208
|
+
const currentWorker = config.worker_provider || "";
|
|
209
|
+
const statusLabel = currentWorker
|
|
210
|
+
? `${currentWorker} (${getProviderById(currentWorker)?.name || "unknown"})`
|
|
211
|
+
: config.language === "zh" ? "(与主服务商相同)" : "(same as conductor)";
|
|
212
|
+
console.log(` ${DIM}${t.currentValue}: ${statusLabel}${RESET}`);
|
|
213
|
+
console.log();
|
|
214
|
+
|
|
215
|
+
const sameLabel = config.language === "zh" ? "使用与主服务商相同配置?" : "Use same provider as conductor?";
|
|
216
|
+
const sameChoice = await ask(rl, ` ${sameLabel}`, "Y", "Y/n");
|
|
217
|
+
|
|
218
|
+
if (sameChoice.toLowerCase() === "n" || sameChoice.toLowerCase() === "no") {
|
|
219
|
+
const providers = getProviders();
|
|
220
|
+
const labels = getProviderLabels(config.language || "en");
|
|
221
|
+
console.log();
|
|
222
|
+
console.log(` ${CYAN}${t.categories[3]}:${RESET}`);
|
|
223
|
+
for (let i = 0; i < labels.length; i++) {
|
|
224
|
+
const marker = providers[i].id === config.worker_provider ? ` ${GREEN}(${t.currentValue})${RESET}` : "";
|
|
225
|
+
console.log(` ${i + 1}. ${labels[i].label}${marker}`);
|
|
226
|
+
}
|
|
227
|
+
const wIdx = parseInt(await ask(rl, ` ${GRAY}>${RESET}`, "1"), 10) - 1;
|
|
228
|
+
const wp = providers[Math.max(0, Math.min(wIdx, providers.length - 1))];
|
|
229
|
+
config.worker_provider = wp.id;
|
|
230
|
+
config.worker_auth_type = wp.authType;
|
|
231
|
+
config.worker_api_format = wp.apiFormat;
|
|
232
|
+
|
|
233
|
+
if (wp.id === "custom") {
|
|
234
|
+
config.worker_base_url = await ask(rl, ` ${CYAN}Base URL${RESET}`, config.worker_base_url || "");
|
|
235
|
+
} else {
|
|
236
|
+
config.worker_base_url = wp.baseUrl;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Worker API Key
|
|
240
|
+
const masked = maskKey(config.worker_api_key);
|
|
241
|
+
const keyPrompt = masked
|
|
242
|
+
? ` ${CYAN}API Key (Worker)${RESET} ${DIM}(${masked})${RESET}`
|
|
243
|
+
: ` ${CYAN}API Key (Worker)${RESET}`;
|
|
244
|
+
const newKey = await ask(rl, keyPrompt, "", masked ? t.enterKeep : "");
|
|
245
|
+
if (newKey) config.worker_api_key = newKey;
|
|
246
|
+
} else {
|
|
247
|
+
// Clear worker-specific config (use conductor)
|
|
248
|
+
config.worker_provider = "";
|
|
249
|
+
config.worker_api_key = "";
|
|
250
|
+
config.worker_base_url = "";
|
|
251
|
+
config.worker_auth_type = "";
|
|
252
|
+
config.worker_api_format = "";
|
|
253
|
+
}
|
|
254
|
+
console.log();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Category 5: Quality Thresholds
|
|
259
|
+
*/
|
|
260
|
+
async function editThresholds(rl, config, t) {
|
|
261
|
+
console.log();
|
|
262
|
+
console.log(` ${BOLD}${t.categories[4]}${RESET}`);
|
|
263
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
264
|
+
console.log();
|
|
265
|
+
|
|
266
|
+
config.accuracy_threshold = parseFloat(
|
|
267
|
+
await ask(rl, ` ${CYAN}Accuracy threshold${RESET}`, String(config.accuracy_threshold ?? 0.9), t.enterKeep)
|
|
268
|
+
);
|
|
269
|
+
config.systemic_threshold = parseFloat(
|
|
270
|
+
await ask(rl, ` ${CYAN}Systemic threshold${RESET}`, String(config.systemic_threshold ?? 0.10), t.enterKeep)
|
|
271
|
+
);
|
|
272
|
+
config.spot_check_rate = parseFloat(
|
|
273
|
+
await ask(rl, ` ${CYAN}Spot-check rate${RESET}`, String(config.spot_check_rate ?? 0.10), t.enterKeep)
|
|
274
|
+
);
|
|
275
|
+
config.tier_tolerance = parseFloat(
|
|
276
|
+
await ask(rl, ` ${CYAN}Tier downgrade tolerance${RESET}`, String(config.tier_tolerance ?? 0.05), t.enterKeep)
|
|
277
|
+
);
|
|
278
|
+
console.log();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Category 4: Language
|
|
283
|
+
*/
|
|
284
|
+
async function editLanguage(rl, config, t) {
|
|
285
|
+
console.log();
|
|
286
|
+
console.log(` ${BOLD}${t.categories[5]}${RESET}`);
|
|
287
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
288
|
+
console.log();
|
|
289
|
+
|
|
290
|
+
console.log(` ${CYAN}${t.language}:${RESET}`);
|
|
291
|
+
console.log(` 1. ${t.langOptions[0]}${config.language === "en" ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
|
|
292
|
+
console.log(` 2. ${t.langOptions[1]}${config.language === "zh" ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
|
|
293
|
+
const langDefault = config.language === "zh" ? "2" : "1";
|
|
294
|
+
const langChoice = await ask(rl, ` ${GRAY}>${RESET}`, langDefault);
|
|
295
|
+
config.language = langChoice === "2" ? "zh" : "en";
|
|
296
|
+
console.log();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const CATEGORY_HANDLERS = [editProvider, editTiers, editVlmTiers, editWorkerProvider, editThresholds, editLanguage];
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Main config editor loop.
|
|
303
|
+
*/
|
|
304
|
+
export async function configEditor() {
|
|
305
|
+
const config = loadConfig();
|
|
306
|
+
if (!config) {
|
|
307
|
+
console.log(`\n ${RED}${L.en.noConfig}${RESET}\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const lang = config.language || "en";
|
|
312
|
+
const t = L[lang];
|
|
313
|
+
|
|
314
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
315
|
+
|
|
316
|
+
while (true) {
|
|
317
|
+
console.log();
|
|
318
|
+
console.log(` ${BOLD}${t.title}${RESET}`);
|
|
319
|
+
console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
|
|
320
|
+
console.log();
|
|
321
|
+
for (let i = 0; i < t.categories.length; i++) {
|
|
322
|
+
console.log(` ${i + 1}. ${t.categories[i]}`);
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
|
|
326
|
+
const choice = await ask(rl, ` ${GRAY}>${RESET} ${t.choose}`, "");
|
|
327
|
+
|
|
328
|
+
if (choice === "q" || choice === "Q" || choice === "") break;
|
|
329
|
+
|
|
330
|
+
const idx = parseInt(choice, 10) - 1;
|
|
331
|
+
if (idx >= 0 && idx < CATEGORY_HANDLERS.length) {
|
|
332
|
+
await CATEGORY_HANDLERS[idx](rl, config, t);
|
|
333
|
+
saveConfig(config);
|
|
334
|
+
console.log(` ${GREEN}✓${RESET} ${t.saved}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
rl.close();
|
|
339
|
+
console.log();
|
|
340
|
+
}
|
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,25 +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` +
|
|
166
|
+
`Project: ${engineRef.current.workspace.projectDir || "(none)"}\n` +
|
|
143
167
|
`Workspace: ${engineRef.current.workspace.cwd}\n` +
|
|
144
168
|
`Tools: ${engineRef.current.toolRegistry.size} registered\n` +
|
|
145
|
-
`History: ${engineRef.current.history.messages.length} messages
|
|
169
|
+
`History: ${engineRef.current.history.messages.length} messages\n` +
|
|
170
|
+
`Context: ~${stats.totalTokens} tokens (${stats.percentage}% of ${stats.limit})`,
|
|
146
171
|
});
|
|
147
172
|
return true;
|
|
173
|
+
}
|
|
148
174
|
|
|
149
175
|
case "/clear":
|
|
150
176
|
engineRef.current.history = new ConversationHistory(engineRef.current.workspace.cwd);
|
|
151
177
|
setMessages([]);
|
|
152
178
|
addMessage({ role: "system", content: "Conversation cleared. Workspace and pipeline state preserved." });
|
|
179
|
+
updateContextStats();
|
|
180
|
+
return true;
|
|
181
|
+
|
|
182
|
+
case "/compact": {
|
|
183
|
+
addMessage({ role: "system", content: "Compacting conversation history..." });
|
|
184
|
+
// Run compact asynchronously
|
|
185
|
+
(async () => {
|
|
186
|
+
try {
|
|
187
|
+
const result = await engineRef.current.compact();
|
|
188
|
+
if (result) {
|
|
189
|
+
addMessage({
|
|
190
|
+
role: "system",
|
|
191
|
+
content: `Compacted: removed ${result.removedCount} messages, kept ${result.retainedCount}. Summary: ~${result.summaryTokens} tokens.`,
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
addMessage({ role: "system", content: "Nothing to compact (conversation is short enough)." });
|
|
195
|
+
}
|
|
196
|
+
updateContextStats();
|
|
197
|
+
} catch (err) {
|
|
198
|
+
addMessage({ role: "system", content: `Compact failed: ${err.message}` });
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
153
201
|
return true;
|
|
202
|
+
}
|
|
154
203
|
|
|
155
204
|
case "/rename":
|
|
156
205
|
if (!arg) {
|
|
@@ -193,19 +242,46 @@ function App({ engine, config }) {
|
|
|
193
242
|
addMessage({ role: "system", content: "Sessions:\n" + lines.join("\n") + "\n\nUsage: /resume <name>" });
|
|
194
243
|
}
|
|
195
244
|
} else {
|
|
196
|
-
|
|
245
|
+
// Resume a previous session
|
|
246
|
+
(async () => {
|
|
247
|
+
try {
|
|
248
|
+
const client = new LLMClient({
|
|
249
|
+
apiKey: config.llmApiKey,
|
|
250
|
+
baseUrl: config.llmBaseUrl,
|
|
251
|
+
authType: config.authType,
|
|
252
|
+
apiFormat: config.apiFormat,
|
|
253
|
+
});
|
|
254
|
+
const resumed = await AgentEngine.resume({ client, config, sessionId: arg });
|
|
255
|
+
engineRef.current = resumed;
|
|
256
|
+
setSessionId(resumed.workspace.sessionId);
|
|
257
|
+
setPhase(resumed.currentPhase);
|
|
258
|
+
setMessages([]);
|
|
259
|
+
addMessage({
|
|
260
|
+
role: "system",
|
|
261
|
+
content:
|
|
262
|
+
`Resumed session: ${arg}\n` +
|
|
263
|
+
`Phase: ${resumed.currentPhase.toUpperCase()}\n` +
|
|
264
|
+
`History: ${resumed.history.messages.length} messages restored`,
|
|
265
|
+
});
|
|
266
|
+
updateContextStats();
|
|
267
|
+
} catch (err) {
|
|
268
|
+
addMessage({ role: "system", content: `Resume failed: ${err.message}` });
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
197
271
|
}
|
|
198
272
|
return true;
|
|
199
273
|
|
|
200
274
|
case "/exit":
|
|
201
275
|
case "/quit":
|
|
276
|
+
// Save state before exit
|
|
277
|
+
try { engineRef.current.saveState(); } catch { /* ignore */ }
|
|
202
278
|
exit();
|
|
203
279
|
return true;
|
|
204
280
|
|
|
205
281
|
default:
|
|
206
282
|
return false;
|
|
207
283
|
}
|
|
208
|
-
}, [addMessage, config, exit]);
|
|
284
|
+
}, [addMessage, config, exit, updateContextStats]);
|
|
209
285
|
|
|
210
286
|
const handleSubmit = useCallback((text) => {
|
|
211
287
|
const trimmed = text.trim();
|
|
@@ -233,17 +309,19 @@ function App({ engine, config }) {
|
|
|
233
309
|
queueRef.current.length = 0;
|
|
234
310
|
addMessage({ role: "system", content: "[Queue cleared]" });
|
|
235
311
|
} else {
|
|
312
|
+
try { engineRef.current.saveState(); } catch { /* ignore */ }
|
|
236
313
|
exit();
|
|
237
314
|
}
|
|
238
315
|
}
|
|
239
316
|
if (key.ctrl && input === "d") {
|
|
317
|
+
try { engineRef.current.saveState(); } catch { /* ignore */ }
|
|
240
318
|
exit();
|
|
241
319
|
}
|
|
242
320
|
});
|
|
243
321
|
|
|
244
322
|
return h(Box, { flexDirection: "column" },
|
|
245
323
|
// Welcome banner
|
|
246
|
-
showWelcome ? h(WelcomeBanner) : null,
|
|
324
|
+
showWelcome ? h(WelcomeBanner, { projectDir: config.projectDir }) : null,
|
|
247
325
|
|
|
248
326
|
// Message history
|
|
249
327
|
...messages.map((msg, i) => {
|
|
@@ -291,9 +369,9 @@ function App({ engine, config }) {
|
|
|
291
369
|
isRunning: true,
|
|
292
370
|
}) : null,
|
|
293
371
|
|
|
294
|
-
//
|
|
295
|
-
streaming
|
|
296
|
-
? h(CookingSpinner)
|
|
372
|
+
// Activity indicator while KC is working
|
|
373
|
+
streaming
|
|
374
|
+
? h(CookingSpinner, { status: spinnerStatus })
|
|
297
375
|
: null,
|
|
298
376
|
|
|
299
377
|
// Separator + Input
|
|
@@ -305,25 +383,49 @@ function App({ engine, config }) {
|
|
|
305
383
|
isActive: !streaming,
|
|
306
384
|
}),
|
|
307
385
|
h(HRule),
|
|
308
|
-
h(StatusBar, { sessionId, phase }),
|
|
386
|
+
h(StatusBar, { sessionId, phase, contextTokens, contextLimit }),
|
|
309
387
|
);
|
|
310
388
|
}
|
|
311
389
|
|
|
312
|
-
export async function main() {
|
|
390
|
+
export async function main({ languageOverride } = {}) {
|
|
313
391
|
const config = loadSettings();
|
|
314
392
|
|
|
393
|
+
// Capture user's project directory (CWD at launch)
|
|
394
|
+
config.projectDir = process.cwd();
|
|
395
|
+
|
|
396
|
+
// Session-only language override (does NOT persist to config)
|
|
397
|
+
if (languageOverride) {
|
|
398
|
+
config.language = languageOverride;
|
|
399
|
+
}
|
|
400
|
+
|
|
315
401
|
if (!config.llmApiKey) {
|
|
316
402
|
console.error("Error: No API key configured. Run 'kc-beta onboard' first.");
|
|
317
403
|
process.exit(1);
|
|
318
404
|
}
|
|
319
405
|
|
|
406
|
+
// Warn if all worker LLM tiers are blank
|
|
407
|
+
const allTiersBlank = !config.tier1 && !config.tier2 && !config.tier3 && !config.tier4;
|
|
408
|
+
if (allTiersBlank) {
|
|
409
|
+
const msg = config.language === "zh"
|
|
410
|
+
? " ⚠ 所有 Worker LLM 分层为空。DISTILL 模式将不可用。运行 'kc-beta config' 或 'kc-beta onboard' 配置模型分层。"
|
|
411
|
+
: " ⚠ All worker LLM tiers are blank. DISTILL mode will not work. Run 'kc-beta config' or 'kc-beta onboard' to configure model tiers.";
|
|
412
|
+
console.log(`\x1b[33m${msg}\x1b[0m\n`);
|
|
413
|
+
}
|
|
414
|
+
|
|
320
415
|
const client = new LLMClient({
|
|
321
416
|
apiKey: config.llmApiKey,
|
|
322
417
|
baseUrl: config.llmBaseUrl,
|
|
418
|
+
authType: config.authType,
|
|
419
|
+
apiFormat: config.apiFormat,
|
|
323
420
|
});
|
|
324
421
|
|
|
325
422
|
const engine = new AgentEngine({ client, config });
|
|
326
423
|
|
|
424
|
+
// Save state on process exit
|
|
425
|
+
const saveOnExit = () => { try { engine.saveState(); } catch { /* ignore */ } };
|
|
426
|
+
process.on("SIGINT", saveOnExit);
|
|
427
|
+
process.on("SIGTERM", saveOnExit);
|
|
428
|
+
|
|
327
429
|
const instance = render(h(App, { engine, config }));
|
|
328
430
|
await instance.waitUntilExit();
|
|
329
431
|
}
|