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.
Files changed (55) hide show
  1. package/bin/kc-beta.js +14 -2
  2. package/package.json +1 -1
  3. package/src/agent/context-window.js +151 -0
  4. package/src/agent/context.js +8 -4
  5. package/src/agent/engine.js +261 -8
  6. package/src/agent/event-log.js +111 -0
  7. package/src/agent/llm-client.js +352 -59
  8. package/src/agent/pipelines/base.js +6 -0
  9. package/src/agent/pipelines/distillation.js +18 -0
  10. package/src/agent/pipelines/extraction.js +21 -0
  11. package/src/agent/pipelines/initializer.js +75 -14
  12. package/src/agent/pipelines/production-qc.js +19 -0
  13. package/src/agent/pipelines/skill-authoring.js +14 -0
  14. package/src/agent/pipelines/skill-testing.js +20 -0
  15. package/src/agent/retry.js +83 -0
  16. package/src/agent/session-state.js +79 -0
  17. package/src/agent/skill-loader.js +13 -1
  18. package/src/agent/token-counter.js +62 -0
  19. package/src/agent/tools/document-parse.js +104 -21
  20. package/src/agent/tools/document-search.js +24 -8
  21. package/src/agent/tools/sandbox-exec.js +16 -5
  22. package/src/agent/tools/web-search.js +107 -0
  23. package/src/agent/tools/worker-llm-call.js +14 -5
  24. package/src/agent/tools/workspace-file.js +47 -20
  25. package/src/agent/workspace.js +24 -1
  26. package/src/cli/components.js +24 -5
  27. package/src/cli/config.js +340 -0
  28. package/src/cli/index.js +113 -11
  29. package/src/cli/onboard.js +216 -53
  30. package/src/config.js +63 -10
  31. package/src/model-tiers.json +153 -0
  32. package/src/providers.js +367 -0
  33. package/template/AGENT.md +20 -0
  34. package/template/skills/en/meta/compliance-judgment/SKILL.md +10 -42
  35. package/template/skills/en/meta/document-chunking/SKILL.md +32 -0
  36. package/template/skills/en/meta/document-parsing/SKILL.md +11 -18
  37. package/template/skills/en/meta/entity-extraction/SKILL.md +13 -28
  38. package/template/skills/en/meta/tree-processing/SKILL.md +19 -1
  39. package/template/skills/en/meta-meta/auto-model-selection/SKILL.md +53 -0
  40. package/template/skills/en/meta-meta/pdf-review-dashboard/SKILL.md +57 -0
  41. package/template/skills/en/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  42. package/template/skills/en/meta-meta/rule-extraction/SKILL.md +24 -1
  43. package/template/skills/en/meta-meta/skill-authoring/SKILL.md +6 -0
  44. package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +4 -0
  45. package/template/skills/zh/meta/compliance-judgment/SKILL.md +41 -262
  46. package/template/skills/zh/meta/document-chunking/SKILL.md +32 -0
  47. package/template/skills/zh/meta/document-parsing/SKILL.md +65 -132
  48. package/template/skills/zh/meta/entity-extraction/SKILL.md +68 -230
  49. package/template/skills/zh/meta/tree-processing/SKILL.md +82 -194
  50. package/template/skills/zh/meta-meta/auto-model-selection/SKILL.md +51 -0
  51. package/template/skills/zh/meta-meta/pdf-review-dashboard/SKILL.md +55 -0
  52. package/template/skills/zh/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  53. package/template/skills/zh/meta-meta/rule-extraction/SKILL.md +79 -164
  54. package/template/skills/zh/meta-meta/skill-authoring/SKILL.md +64 -185
  55. 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
- addMessage({ role: "system", content: `Resuming session: ${arg} (not yet implemented)` });
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
- // Spinner while waiting
295
- streaming && !streamingText && !currentTool
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
  }