kc-beta 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/QUICKSTART.md +149 -0
  2. package/README.md +207 -0
  3. package/package.json +12 -2
  4. package/src/agent/context.js +8 -4
  5. package/src/agent/engine.js +154 -9
  6. package/src/agent/pipelines/initializer.js +53 -8
  7. package/src/agent/session-state.js +1 -0
  8. package/src/agent/skill-loader.js +13 -1
  9. package/src/agent/task-manager.js +186 -0
  10. package/src/agent/tools/document-parse.js +99 -21
  11. package/src/agent/tools/document-search.js +24 -8
  12. package/src/agent/tools/sandbox-exec.js +16 -5
  13. package/src/agent/tools/workspace-file.js +47 -20
  14. package/src/agent/workspace.js +24 -1
  15. package/src/cli/components.js +42 -1
  16. package/src/cli/config.js +100 -6
  17. package/src/cli/index.js +39 -2
  18. package/src/cli/onboard.js +70 -1
  19. package/src/config.js +43 -3
  20. package/src/model-tiers.json +153 -0
  21. package/src/providers.js +63 -66
  22. package/template/AGENT.md +20 -0
  23. package/template/skills/en/meta/compliance-judgment/SKILL.md +10 -42
  24. package/template/skills/en/meta/document-chunking/SKILL.md +32 -0
  25. package/template/skills/en/meta/document-parsing/SKILL.md +11 -18
  26. package/template/skills/en/meta/entity-extraction/SKILL.md +13 -28
  27. package/template/skills/en/meta/tree-processing/SKILL.md +19 -1
  28. package/template/skills/en/meta-meta/auto-model-selection/SKILL.md +53 -0
  29. package/template/skills/en/meta-meta/pdf-review-dashboard/SKILL.md +57 -0
  30. package/template/skills/en/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  31. package/template/skills/en/meta-meta/rule-extraction/SKILL.md +24 -1
  32. package/template/skills/en/meta-meta/skill-authoring/SKILL.md +6 -0
  33. package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +4 -0
  34. package/template/skills/zh/meta/compliance-judgment/SKILL.md +41 -262
  35. package/template/skills/zh/meta/document-chunking/SKILL.md +32 -0
  36. package/template/skills/zh/meta/document-parsing/SKILL.md +65 -132
  37. package/template/skills/zh/meta/entity-extraction/SKILL.md +68 -230
  38. package/template/skills/zh/meta/tree-processing/SKILL.md +82 -194
  39. package/template/skills/zh/meta-meta/auto-model-selection/SKILL.md +51 -0
  40. package/template/skills/zh/meta-meta/pdf-review-dashboard/SKILL.md +55 -0
  41. package/template/skills/zh/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  42. package/template/skills/zh/meta-meta/rule-extraction/SKILL.md +79 -164
  43. package/template/skills/zh/meta-meta/skill-authoring/SKILL.md +64 -185
  44. package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +95 -216
@@ -5,9 +5,9 @@ import { BaseTool, ToolResult } from "./base.js";
5
5
  const MAX_READ = 50_000;
6
6
 
7
7
  /**
8
- * Read, write, or list files in the workspace directory.
9
- * All paths are resolved relative to the workspace root with
10
- * traversal protection. VersionManager hooks into writes for automatic versioning.
8
+ * Read, write, or list files in the workspace or project directory.
9
+ * All paths are resolved relative to the chosen scope with
10
+ * traversal protection. VersionManager hooks into workspace writes for automatic versioning.
11
11
  */
12
12
  export class WorkspaceFileTool extends BaseTool {
13
13
  /**
@@ -24,9 +24,10 @@ export class WorkspaceFileTool extends BaseTool {
24
24
 
25
25
  get description() {
26
26
  return (
27
- "Read, write, or list files in the workspace directory. " +
28
- "Operations: read (returns file content), write (creates/overwrites a file), " +
29
- "list (shows directory contents)."
27
+ "Read, write, or list files. " +
28
+ "scope='workspace' (default): KC's working directory for rules, skills, workflows, results. " +
29
+ "scope='project': the user's project folder where KC was launched — source regulations and samples live here. " +
30
+ "Operations: read (returns file content), write (creates/overwrites a file), list (shows directory contents)."
30
31
  );
31
32
  }
32
33
 
@@ -41,34 +42,58 @@ export class WorkspaceFileTool extends BaseTool {
41
42
  },
42
43
  path: {
43
44
  type: "string",
44
- description: "Relative path within the workspace. Defaults to '.' for list.",
45
+ description: "Relative path within the chosen scope. Defaults to '.' for list.",
45
46
  },
46
47
  content: {
47
48
  type: "string",
48
49
  description: "File content to write (required for write operation)",
49
50
  },
51
+ scope: {
52
+ type: "string",
53
+ enum: ["workspace", "project"],
54
+ description: "Which directory to operate in. 'workspace' (default) = KC's workspace. 'project' = user's project directory.",
55
+ },
50
56
  },
51
57
  required: ["operation"],
52
58
  };
53
59
  }
54
60
 
61
+ _resolveForScope(filePath, scope) {
62
+ if (scope === "project") {
63
+ return this._workspace.resolveProjectPath(filePath);
64
+ }
65
+ return this._workspace.resolvePath(filePath);
66
+ }
67
+
68
+ _baseForScope(scope) {
69
+ if (scope === "project") {
70
+ return this._workspace.projectDir;
71
+ }
72
+ return this._workspace.cwd;
73
+ }
74
+
55
75
  async execute(input) {
56
76
  const op = input.operation || "";
57
77
  const filePath = input.path || ".";
58
78
  const content = input.content || "";
79
+ const scope = input.scope || "workspace";
80
+
81
+ if (scope === "project" && !this._workspace.projectDir) {
82
+ return new ToolResult("No project directory available. KC was launched without a project context.", true);
83
+ }
59
84
 
60
85
  try {
61
- if (op === "read") return this._read(filePath);
62
- if (op === "write") return this._write(filePath, content);
63
- if (op === "list") return this._list(filePath);
86
+ if (op === "read") return this._read(filePath, scope);
87
+ if (op === "write") return this._write(filePath, content, scope);
88
+ if (op === "list") return this._list(filePath, scope);
64
89
  return new ToolResult(`Unknown operation: ${op}`, true);
65
90
  } catch (err) {
66
91
  return new ToolResult(`File error: ${err.message}`, true);
67
92
  }
68
93
  }
69
94
 
70
- _read(filePath) {
71
- const resolved = this._workspace.resolvePath(filePath);
95
+ _read(filePath, scope) {
96
+ const resolved = this._resolveForScope(filePath, scope);
72
97
  if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
73
98
  return new ToolResult(`File not found: ${filePath}`, true);
74
99
  }
@@ -79,27 +104,28 @@ export class WorkspaceFileTool extends BaseTool {
79
104
  return new ToolResult(text);
80
105
  }
81
106
 
82
- _write(filePath, content) {
107
+ _write(filePath, content, scope) {
83
108
  if (!filePath || filePath === ".") {
84
109
  return new ToolResult("Path required for write operation", true);
85
110
  }
86
- const resolved = this._workspace.resolvePath(filePath);
111
+ const resolved = this._resolveForScope(filePath, scope);
87
112
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
88
113
  fs.writeFileSync(resolved, content, "utf-8");
89
114
 
90
- // Version tracking (structural cannot be bypassed)
115
+ // Version tracking only for workspace writes
91
116
  let traceId = null;
92
- if (this._versionManager) {
117
+ if (scope === "workspace" && this._versionManager) {
93
118
  traceId = this._versionManager.onWrite(filePath, content);
94
119
  }
95
120
 
96
- let msg = `Wrote ${content.length} chars to ${filePath}`;
121
+ const label = scope === "project" ? `[project] ${filePath}` : filePath;
122
+ let msg = `Wrote ${content.length} chars to ${label}`;
97
123
  if (traceId) msg += ` [trace: ${traceId}]`;
98
124
  return new ToolResult(msg);
99
125
  }
100
126
 
101
- _list(filePath) {
102
- const resolved = this._workspace.resolvePath(filePath);
127
+ _list(filePath, scope) {
128
+ const resolved = this._resolveForScope(filePath, scope);
103
129
  if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
104
130
  return new ToolResult(`Not a directory: ${filePath}`, true);
105
131
  }
@@ -112,8 +138,9 @@ export class WorkspaceFileTool extends BaseTool {
112
138
  if (entries.length === 0) {
113
139
  return new ToolResult("(empty directory)");
114
140
  }
141
+ const base = this._baseForScope(scope);
115
142
  const lines = entries.map((e) => {
116
- const rel = path.relative(this._workspace.cwd, path.join(resolved, e.name));
143
+ const rel = path.relative(base, path.join(resolved, e.name));
117
144
  const marker = e.isDirectory() ? "[dir] " : " ";
118
145
  return `${marker}${rel}`;
119
146
  });
@@ -11,11 +11,13 @@ export class Workspace {
11
11
  /**
12
12
  * @param {string} root - Workspace root directory
13
13
  * @param {string} [sessionId] - Session identifier (auto-generated if omitted)
14
+ * @param {string} [projectDir] - User's project directory (CWD at launch)
14
15
  */
15
- constructor(root, sessionId) {
16
+ constructor(root, sessionId, projectDir) {
16
17
  this.root = path.resolve(root);
17
18
  this.sessionId = sessionId || crypto.randomUUID().replace(/-/g, "").slice(0, 12);
18
19
  this.path = path.resolve(this.root, this.sessionId);
20
+ this.projectDir = projectDir ? path.resolve(projectDir) : null;
19
21
  fs.mkdirSync(this.path, { recursive: true });
20
22
  }
21
23
 
@@ -42,6 +44,27 @@ export class Workspace {
42
44
  return resolved;
43
45
  }
44
46
 
47
+ /**
48
+ * Resolve a user-supplied relative path against the project directory.
49
+ * Same traversal protection as resolvePath() but for the project folder.
50
+ * @param {string} userPath
51
+ * @returns {string}
52
+ */
53
+ resolveProjectPath(userPath) {
54
+ if (!this.projectDir) {
55
+ throw new Error("No project directory available");
56
+ }
57
+ if (path.isAbsolute(userPath)) {
58
+ throw new Error(`Absolute paths not allowed: ${userPath}`);
59
+ }
60
+ const resolved = path.resolve(this.projectDir, userPath);
61
+ const base = path.resolve(this.projectDir);
62
+ if (resolved !== base && !resolved.startsWith(base + path.sep)) {
63
+ throw new Error(`Path escapes project directory: ${userPath}`);
64
+ }
65
+ return resolved;
66
+ }
67
+
45
68
  /**
46
69
  * Rename the workspace folder. Returns the new sessionId.
47
70
  * @param {string} newName
@@ -52,9 +52,43 @@ export function StatusBar({ sessionId, phase, contextTokens, contextLimit }) {
52
52
  );
53
53
  }
54
54
 
55
+ // --- Task dashboard (ralph-loop) ---
56
+
57
+ export function TaskDashboard({ tasks, progress }) {
58
+ if (!tasks || tasks.length === 0) return null;
59
+
60
+ const { total, completed } = progress || { total: 0, completed: 0 };
61
+ const barWidth = 20;
62
+ const filled = total > 0 ? Math.round((completed / total) * barWidth) : 0;
63
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
64
+
65
+ // Show at most 8 tasks — current + a few before/after
66
+ const currentIdx = tasks.findIndex((t) => t.status === "in_progress");
67
+ const startIdx = Math.max(0, Math.min(currentIdx - 2, tasks.length - 8));
68
+ const visible = tasks.slice(startIdx, startIdx + 8);
69
+ const hasMore = tasks.length > 8;
70
+
71
+ return h(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, borderStyle: "single", borderColor: "gray", paddingLeft: 1, paddingRight: 1 },
72
+ h(Text, { dimColor: true }, `Tasks [${bar}] ${completed}/${total}`),
73
+ ...visible.map((t) => {
74
+ const icon = t.status === "completed" ? "\u2713"
75
+ : t.status === "in_progress" ? "\u25b8"
76
+ : t.status === "failed" ? "\u2717"
77
+ : "\u00b7";
78
+ const color = t.status === "completed" ? "green"
79
+ : t.status === "in_progress" ? "cyan"
80
+ : t.status === "failed" ? "red"
81
+ : "gray";
82
+ const label = `${t.ruleId || t.id} ${t.title}`;
83
+ return h(Text, { key: t.id, color }, ` ${icon} ${label.slice(0, 50)}`);
84
+ }),
85
+ hasMore ? h(Text, { dimColor: true }, ` ... ${tasks.length - 8} more`) : null,
86
+ );
87
+ }
88
+
55
89
  // --- Welcome banner ---
56
90
 
57
- export function WelcomeBanner() {
91
+ export function WelcomeBanner({ projectDir } = {}) {
58
92
  return h(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1 },
59
93
  h(Box, null,
60
94
  h(Text, { bold: true }, "KC AGENT CLI"),
@@ -62,6 +96,13 @@ export function WelcomeBanner() {
62
96
  ),
63
97
  h(Text, { dimColor: true }, "Hope you never know what KC was."),
64
98
  h(Text, null, ""),
99
+ projectDir
100
+ ? h(Box, { flexDirection: "column" },
101
+ h(Text, { dimColor: true }, `Project: ${projectDir}`),
102
+ h(Text, { color: "yellow", dimColor: true }, "KC has full read/write access to this directory. We recommend backing up important files."),
103
+ )
104
+ : null,
105
+ h(Text, null, ""),
65
106
  h(Text, { dimColor: true }, "Product of Memium / kitchen-engineer42"),
66
107
  );
67
108
  }
package/src/cli/config.js CHANGED
@@ -23,7 +23,7 @@ const L = {
23
23
  noConfig: "No config found. Run 'kc-beta onboard' first.",
24
24
  menu: "Configuration Categories",
25
25
  choose: "Choose category (q to quit)",
26
- categories: ["LLM Provider & API Key", "Model Tiers", "Quality Thresholds", "Language"],
26
+ categories: ["LLM Provider & API Key", "Model Tiers", "VLM Tiers (Vision/OCR)", "Worker LLM Provider", "Quality Thresholds", "Language"],
27
27
  saved: "Saved.",
28
28
  back: "← Back to menu",
29
29
  enterKeep: "Press Enter to keep",
@@ -41,7 +41,7 @@ const L = {
41
41
  noConfig: "未找到配置。请先运行 'kc-beta onboard'。",
42
42
  menu: "配置类别",
43
43
  choose: "选择类别(q 退出)",
44
- categories: ["大模型服务商 & API 密钥", "模型分层", "质量阈值", "语言"],
44
+ categories: ["大模型服务商 & API 密钥", "模型分层", "VLM 视觉模型分层", "Worker LLM 服务商", "质量阈值", "语言"],
45
45
  saved: "已保存。",
46
46
  back: "← 返回菜单",
47
47
  enterKeep: "回车保留当前值",
@@ -118,11 +118,26 @@ async function editProvider(rl, config, t) {
118
118
  // Base URL
119
119
  if (provider.id === "custom") {
120
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}`);
121
134
  } else {
135
+ // Keep existing base_url if it matches this provider, otherwise use default
122
136
  const defaultUrl = provider.baseUrl;
123
137
  console.log(` ${CYAN}${t.baseUrl}${RESET}: ${DIM}${defaultUrl}${RESET}`);
124
138
  config.base_url = defaultUrl;
125
139
  }
140
+ console.log();
126
141
 
127
142
  // API Key
128
143
  const masked = maskKey(config.api_key);
@@ -161,14 +176,93 @@ async function editTiers(rl, config, t) {
161
176
  }
162
177
 
163
178
  /**
164
- * Category 3: Quality Thresholds
179
+ * Category 3: VLM Tiers (Vision/OCR)
165
180
  */
166
- async function editThresholds(rl, config, t) {
181
+ async function editVlmTiers(rl, config, t) {
167
182
  console.log();
168
183
  console.log(` ${BOLD}${t.categories[2]}${RESET}`);
169
184
  console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
170
185
  console.log();
171
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
+
172
266
  config.accuracy_threshold = parseFloat(
173
267
  await ask(rl, ` ${CYAN}Accuracy threshold${RESET}`, String(config.accuracy_threshold ?? 0.9), t.enterKeep)
174
268
  );
@@ -189,7 +283,7 @@ async function editThresholds(rl, config, t) {
189
283
  */
190
284
  async function editLanguage(rl, config, t) {
191
285
  console.log();
192
- console.log(` ${BOLD}${t.categories[3]}${RESET}`);
286
+ console.log(` ${BOLD}${t.categories[5]}${RESET}`);
193
287
  console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
194
288
  console.log();
195
289
 
@@ -202,7 +296,7 @@ async function editLanguage(rl, config, t) {
202
296
  console.log();
203
297
  }
204
298
 
205
- const CATEGORY_HANDLERS = [editProvider, editTiers, editThresholds, editLanguage];
299
+ const CATEGORY_HANDLERS = [editProvider, editTiers, editVlmTiers, editWorkerProvider, editThresholds, editLanguage];
206
300
 
207
301
  /**
208
302
  * Main config editor loop.
package/src/cli/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  StatusBar,
11
11
  CookingSpinner,
12
12
  ToolBlock,
13
+ TaskDashboard,
13
14
  HRule,
14
15
  InputPrompt,
15
16
  } from "./components.js";
@@ -32,6 +33,8 @@ function App({ engine, config }) {
32
33
  const [spinnerStatus, setSpinnerStatus] = useState(null);
33
34
  const [contextTokens, setContextTokens] = useState(0);
34
35
  const [contextLimit, setContextLimit] = useState(config.kcContextLimit || 200000);
36
+ const [taskList, setTaskList] = useState([]);
37
+ const [taskProgress, setTaskProgress] = useState(null);
35
38
 
36
39
  const engineRef = useRef(engine);
37
40
  const streamingRef = useRef(false);
@@ -60,7 +63,7 @@ function App({ engine, config }) {
60
63
  let accumulated = "";
61
64
 
62
65
  try {
63
- for await (const event of engineRef.current.runTurn(text)) {
66
+ for await (const event of engineRef.current.runTaskLoop(text)) {
64
67
  switch (event.type) {
65
68
  case "text_delta":
66
69
  accumulated += event.text ?? "";
@@ -110,6 +113,16 @@ function App({ engine, config }) {
110
113
  break;
111
114
  }
112
115
 
116
+ case "task_progress": {
117
+ const tp = event.data;
118
+ setTaskList(engineRef.current.taskManager.getAllTasks());
119
+ setTaskProgress(tp.progress);
120
+ if (tp.status === "in_progress") {
121
+ setSpinnerStatus(`Task: ${tp.title}`);
122
+ }
123
+ break;
124
+ }
125
+
113
126
  case "error":
114
127
  addMessage({ role: "system", content: `Error: ${event.message ?? "Unknown error"}` });
115
128
  break;
@@ -144,6 +157,7 @@ function App({ engine, config }) {
144
157
  "Commands:\n" +
145
158
  " /help Show this help\n" +
146
159
  " /status Show session info, model, phase, workspace\n" +
160
+ " /tasks Show task progress\n" +
147
161
  " /clear Clear conversation history (keep workspace)\n" +
148
162
  " /compact Summarize older messages to reduce context\n" +
149
163
  " /sessions List all sessions\n" +
@@ -163,6 +177,7 @@ function App({ engine, config }) {
163
177
  `Model: ${config.kcModel}\n` +
164
178
  `Provider: ${config.provider || "unknown"}\n` +
165
179
  `LLM URL: ${config.llmBaseUrl}\n` +
180
+ `Project: ${engineRef.current.workspace.projectDir || "(none)"}\n` +
166
181
  `Workspace: ${engineRef.current.workspace.cwd}\n` +
167
182
  `Tools: ${engineRef.current.toolRegistry.size} registered\n` +
168
183
  `History: ${engineRef.current.history.messages.length} messages\n` +
@@ -171,6 +186,13 @@ function App({ engine, config }) {
171
186
  return true;
172
187
  }
173
188
 
189
+ case "/tasks":
190
+ addMessage({
191
+ role: "system",
192
+ content: engineRef.current.taskManager.formatForDisplay(),
193
+ });
194
+ return true;
195
+
174
196
  case "/clear":
175
197
  engineRef.current.history = new ConversationHistory(engineRef.current.workspace.cwd);
176
198
  setMessages([]);
@@ -320,7 +342,10 @@ function App({ engine, config }) {
320
342
 
321
343
  return h(Box, { flexDirection: "column" },
322
344
  // Welcome banner
323
- showWelcome ? h(WelcomeBanner) : null,
345
+ showWelcome ? h(WelcomeBanner, { projectDir: config.projectDir }) : null,
346
+
347
+ // Task dashboard (ralph-loop)
348
+ taskList.length > 0 ? h(TaskDashboard, { tasks: taskList, progress: taskProgress }) : null,
324
349
 
325
350
  // Message history
326
351
  ...messages.map((msg, i) => {
@@ -389,6 +414,9 @@ function App({ engine, config }) {
389
414
  export async function main({ languageOverride } = {}) {
390
415
  const config = loadSettings();
391
416
 
417
+ // Capture user's project directory (CWD at launch)
418
+ config.projectDir = process.cwd();
419
+
392
420
  // Session-only language override (does NOT persist to config)
393
421
  if (languageOverride) {
394
422
  config.language = languageOverride;
@@ -399,6 +427,15 @@ export async function main({ languageOverride } = {}) {
399
427
  process.exit(1);
400
428
  }
401
429
 
430
+ // Warn if all worker LLM tiers are blank
431
+ const allTiersBlank = !config.tier1 && !config.tier2 && !config.tier3 && !config.tier4;
432
+ if (allTiersBlank) {
433
+ const msg = config.language === "zh"
434
+ ? " ⚠ 所有 Worker LLM 分层为空。DISTILL 模式将不可用。运行 'kc-beta config' 或 'kc-beta onboard' 配置模型分层。"
435
+ : " ⚠ All worker LLM tiers are blank. DISTILL mode will not work. Run 'kc-beta config' or 'kc-beta onboard' to configure model tiers.";
436
+ console.log(`\x1b[33m${msg}\x1b[0m\n`);
437
+ }
438
+
402
439
  const client = new LLMClient({
403
440
  apiKey: config.llmApiKey,
404
441
  baseUrl: config.llmBaseUrl,
@@ -37,7 +37,11 @@ const L = {
37
37
  keyTypeOptions: ["API Key (pay-per-use)", "Coding Plan Key (subscription)"],
38
38
  conductorModel: "Conductor Model",
39
39
  workerTiers: "Worker LLM Tiers",
40
+ vlmTiers: "VLM Tiers (Vision/OCR)",
40
41
  tierHint: "Press Enter to accept defaults",
42
+ workerConfig: "Worker LLM Provider",
43
+ workerSameProvider: "Use same provider for worker LLMs?",
44
+ yesNo: "Y/n",
41
45
  accuracy: "Accuracy Threshold",
42
46
  saved: "Saved to",
43
47
  runHint: "Run {cmd} to start the agent.",
@@ -67,7 +71,11 @@ const L = {
67
71
  keyTypeOptions: ["API Key(按量付费)", "Coding Plan Key(包年包月)"],
68
72
  conductorModel: "主模型",
69
73
  workerTiers: "Worker 模型分层",
74
+ vlmTiers: "VLM 视觉模型分层(OCR)",
70
75
  tierHint: "回车接受默认值",
76
+ workerConfig: "Worker LLM 服务商",
77
+ workerSameProvider: "Worker LLM 使用同一服务商?",
78
+ yesNo: "Y/n",
71
79
  accuracy: "准确率阈值",
72
80
  saved: "已保存至",
73
81
  runHint: "运行 {cmd} 启动 Agent。",
@@ -237,7 +245,7 @@ export async function onboard() {
237
245
  );
238
246
  console.log();
239
247
 
240
- // --- Worker tiers ---
248
+ // --- Worker LLM tiers ---
241
249
  console.log(` ${CYAN}${t.workerTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
242
250
  const tiers = {};
243
251
  for (const tier of ["tier1", "tier2", "tier3", "tier4"]) {
@@ -251,6 +259,59 @@ export async function onboard() {
251
259
  }
252
260
  console.log();
253
261
 
262
+ // --- VLM tiers (vision/OCR) ---
263
+ console.log(` ${CYAN}${t.vlmTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
264
+ const vlmTiers = {};
265
+ for (const tier of ["tier1", "tier2", "tier3"]) {
266
+ const def = provider.defaultVlm?.[tier] || existing?.vlm_tiers?.[tier] || "";
267
+ vlmTiers[tier] = await ask(
268
+ rl,
269
+ ` ${tier.toUpperCase()}`,
270
+ def,
271
+ );
272
+ }
273
+ console.log();
274
+
275
+ // --- Worker LLM provider (optional) ---
276
+ console.log(` ${CYAN}${t.workerConfig}${RESET}`);
277
+ const sameProvider = await ask(rl, ` ${t.workerSameProvider}`, "Y", t.yesNo);
278
+ let workerProvider = "";
279
+ let workerApiKey = "";
280
+ let workerBaseUrl = "";
281
+ let workerAuthType = "";
282
+ let workerApiFormat = "";
283
+
284
+ if (sameProvider.toLowerCase() === "n" || sameProvider.toLowerCase() === "no") {
285
+ // Pick a different provider for workers
286
+ console.log();
287
+ console.log(` ${CYAN}${t.providerPrompt} (Worker):${RESET}`);
288
+ for (let i = 0; i < labels.length; i++) {
289
+ console.log(` ${i + 1}. ${labels[i].label}`);
290
+ }
291
+ const wIdx = parseInt(await ask(rl, ` ${GRAY}>${RESET} ${t.choose}`, "1"), 10) - 1;
292
+ const wp = providers[Math.max(0, Math.min(wIdx, providers.length - 1))];
293
+ workerProvider = wp.id;
294
+ workerAuthType = wp.authType;
295
+ workerApiFormat = wp.apiFormat;
296
+ workerBaseUrl = wp.baseUrl;
297
+
298
+ if (wp.id === "custom") {
299
+ workerBaseUrl = await ask(rl, ` ${t.baseUrl}`, existing.worker_base_url || "");
300
+ }
301
+
302
+ // Worker API key
303
+ const wMasked = existing.worker_api_key ? existing.worker_api_key.slice(0, 6) + "..." + existing.worker_api_key.slice(-4) : "";
304
+ const wKeyHint = wMasked ? t.apiKeyKeep : t.apiKeyRequired;
305
+ workerApiKey = await ask(
306
+ rl,
307
+ ` ${CYAN}${t.apiKey} (Worker)${RESET}`,
308
+ "",
309
+ wKeyHint,
310
+ );
311
+ workerApiKey = workerApiKey || existing.worker_api_key || "";
312
+ }
313
+ console.log();
314
+
254
315
  rl.close();
255
316
 
256
317
  // Preserve existing thresholds or set defaults (editable via 'kc-beta config')
@@ -268,6 +329,14 @@ export async function onboard() {
268
329
  api_format: provider.apiFormat,
269
330
  conductor_model: model,
270
331
  tiers,
332
+ vlm_tiers: vlmTiers,
333
+ // Worker LLM (optional — empty means use conductor config)
334
+ worker_provider: workerProvider,
335
+ worker_api_key: workerApiKey,
336
+ worker_base_url: workerBaseUrl,
337
+ worker_auth_type: workerAuthType,
338
+ worker_api_format: workerApiFormat,
339
+ // Thresholds
271
340
  accuracy_threshold: accuracy,
272
341
  systemic_threshold: systemicThreshold,
273
342
  spot_check_rate: spotCheckRate,
package/src/config.js CHANGED
@@ -55,7 +55,7 @@ export function loadSettings(workspacePath) {
55
55
  const provider = gc.provider || "siliconflow";
56
56
  const providerDef = getProviderById(provider);
57
57
 
58
- return {
58
+ const settings = {
59
59
  // Provider identity
60
60
  provider,
61
61
  authType: gc.auth_type || providerDef?.authType || "bearer",
@@ -73,8 +73,17 @@ export function loadSettings(workspacePath) {
73
73
  tier3: env.TIER3 || gc.tiers?.tier3 || "",
74
74
  tier4: env.TIER4 || gc.tiers?.tier4 || "",
75
75
 
76
- // OCR models
77
- ocrModelTier1: env.OCR_MODEL_TIER1 || "zai-org/GLM-4.6V",
76
+ // VLM tiers (vision/OCR models)
77
+ vlmTier1: env.VLM_TIER1 || gc.vlm_tiers?.tier1 || "",
78
+ vlmTier2: env.VLM_TIER2 || gc.vlm_tiers?.tier2 || "",
79
+ vlmTier3: env.VLM_TIER3 || gc.vlm_tiers?.tier3 || "",
80
+
81
+ // Worker LLM — optional, defaults to conductor config
82
+ workerProvider: gc.worker_provider || "",
83
+ workerApiKey: env.WORKER_API_KEY || gc.worker_api_key || "",
84
+ workerBaseUrl: env.WORKER_BASE_URL || gc.worker_base_url || "",
85
+ workerAuthType: gc.worker_auth_type || "",
86
+ workerApiFormat: gc.worker_api_format || "",
78
87
 
79
88
  // Document parsing
80
89
  mineruApiUrl: env.MINERU_API_URL || "",
@@ -106,6 +115,37 @@ export function loadSettings(workspacePath) {
106
115
  // Language
107
116
  language: env.LANGUAGE || gc.language || "en",
108
117
  };
118
+
119
+ // Effective worker config (falls back to conductor config)
120
+ settings.effectiveWorkerProvider = () => settings.workerProvider || settings.provider;
121
+ settings.effectiveWorkerApiKey = () => settings.workerApiKey || settings.llmApiKey;
122
+ settings.effectiveWorkerBaseUrl = () => {
123
+ if (settings.workerBaseUrl) return settings.workerBaseUrl;
124
+ // If worker uses a different provider, use that provider's default base URL
125
+ if (settings.workerProvider && settings.workerProvider !== settings.provider) {
126
+ const wp = getProviderById(settings.workerProvider);
127
+ return wp?.baseUrl || settings.llmBaseUrl;
128
+ }
129
+ return settings.llmBaseUrl;
130
+ };
131
+ settings.effectiveWorkerAuthType = () => {
132
+ if (settings.workerAuthType) return settings.workerAuthType;
133
+ if (settings.workerProvider && settings.workerProvider !== settings.provider) {
134
+ const wp = getProviderById(settings.workerProvider);
135
+ return wp?.authType || settings.authType;
136
+ }
137
+ return settings.authType;
138
+ };
139
+ settings.effectiveWorkerApiFormat = () => {
140
+ if (settings.workerApiFormat) return settings.workerApiFormat;
141
+ if (settings.workerProvider && settings.workerProvider !== settings.provider) {
142
+ const wp = getProviderById(settings.workerProvider);
143
+ return wp?.apiFormat || settings.apiFormat;
144
+ }
145
+ return settings.apiFormat;
146
+ };
147
+
148
+ return settings;
109
149
  }
110
150
 
111
151
  export { GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH };