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
@@ -21,8 +21,10 @@ export class DocumentSearchTool extends BaseTool {
21
21
 
22
22
  get description() {
23
23
  return (
24
- "Search for text across documents in the workspace. Returns matching " +
25
- "passages with file path and context. Supports plain text and regex queries."
24
+ "Search for text across documents. " +
25
+ "scope='workspace' (default) searches KC's workspace. " +
26
+ "scope='project' searches the user's project directory. " +
27
+ "Returns matching passages with file path and context. Supports plain text and regex queries."
26
28
  );
27
29
  }
28
30
 
@@ -31,9 +33,14 @@ export class DocumentSearchTool extends BaseTool {
31
33
  type: "object",
32
34
  properties: {
33
35
  query: { type: "string", description: "Search query (plain text or regex pattern)" },
34
- path: { type: "string", description: "Subdirectory to search in (default: entire workspace)" },
36
+ path: { type: "string", description: "Subdirectory to search in (default: entire scope root)" },
35
37
  max_results: { type: "integer", description: `Maximum results to return (default: ${MAX_RESULTS})` },
36
38
  regex: { type: "boolean", description: "Treat query as regex pattern (default: false)" },
39
+ scope: {
40
+ type: "string",
41
+ enum: ["workspace", "project"],
42
+ description: "Which directory to search. 'workspace' (default) or 'project'.",
43
+ },
37
44
  },
38
45
  required: ["query"],
39
46
  };
@@ -44,11 +51,19 @@ export class DocumentSearchTool extends BaseTool {
44
51
  const searchPath = input.path || ".";
45
52
  const maxResults = input.max_results || MAX_RESULTS;
46
53
  const useRegex = input.regex || false;
54
+ const scope = input.scope || "workspace";
47
55
 
48
56
  if (!query) return new ToolResult("No query provided", true);
57
+ if (scope === "project" && !this._workspace.projectDir) {
58
+ return new ToolResult("No project directory available", true);
59
+ }
49
60
 
50
61
  let searchDir;
51
- try { searchDir = this._workspace.resolvePath(searchPath); }
62
+ try {
63
+ searchDir = scope === "project"
64
+ ? this._workspace.resolveProjectPath(searchPath)
65
+ : this._workspace.resolvePath(searchPath);
66
+ }
52
67
  catch (e) { return new ToolResult(e.message, true); }
53
68
 
54
69
  if (!fs.existsSync(searchDir) || !fs.statSync(searchDir).isDirectory()) {
@@ -62,8 +77,9 @@ export class DocumentSearchTool extends BaseTool {
62
77
  return new ToolResult(`Invalid regex: ${e.message}`, true);
63
78
  }
64
79
 
80
+ const baseDir = scope === "project" ? this._workspace.projectDir : this._workspace.cwd;
65
81
  const results = [];
66
- this._searchDir(searchDir, pattern, results, maxResults);
82
+ this._searchDir(searchDir, pattern, results, maxResults, baseDir);
67
83
 
68
84
  if (results.length === 0) return new ToolResult(`No matches found for: ${query}`);
69
85
 
@@ -76,7 +92,7 @@ export class DocumentSearchTool extends BaseTool {
76
92
  return new ToolResult(`Found ${results.length} match(es):\n\n${lines.join("\n")}`);
77
93
  }
78
94
 
79
- _searchDir(dir, pattern, results, maxResults) {
95
+ _searchDir(dir, pattern, results, maxResults, baseDir) {
80
96
  let entries;
81
97
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
82
98
  catch { return; }
@@ -87,7 +103,7 @@ export class DocumentSearchTool extends BaseTool {
87
103
 
88
104
  if (entry.isDirectory()) {
89
105
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "__pycache__") continue;
90
- this._searchDir(fullPath, pattern, results, maxResults);
106
+ this._searchDir(fullPath, pattern, results, maxResults, baseDir);
91
107
  } else if (entry.isFile() && TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
92
108
  let content;
93
109
  try { content = fs.readFileSync(fullPath, "utf-8"); }
@@ -100,7 +116,7 @@ export class DocumentSearchTool extends BaseTool {
100
116
  const end = Math.min(content.length, match.index + match[0].length + CONTEXT_CHARS);
101
117
  const context = content.slice(start, end).trim();
102
118
  const lineNum = content.slice(0, match.index).split("\n").length;
103
- const relPath = path.relative(this._workspace.cwd, fullPath);
119
+ const relPath = path.relative(baseDir, fullPath);
104
120
 
105
121
  results.push({ file: relPath, line: lineNum, match: match[0], context });
106
122
  if (results.length >= maxResults) break;
@@ -23,8 +23,9 @@ export class SandboxExecTool extends BaseTool {
23
23
 
24
24
  get description() {
25
25
  return (
26
- "Execute a shell command in the workspace directory. " +
27
- "Use for running scripts, installing packages, listing files, etc. " +
26
+ "Execute a shell command. " +
27
+ "cwd='workspace' (default) runs in KC's workspace. " +
28
+ "cwd='project' runs in the user's project directory. " +
28
29
  "Pipes, redirects, and chained commands (&&) are supported."
29
30
  );
30
31
  }
@@ -37,6 +38,11 @@ export class SandboxExecTool extends BaseTool {
37
38
  type: "string",
38
39
  description: "The shell command to execute (e.g. 'python script.py', 'ls -la')",
39
40
  },
41
+ cwd: {
42
+ type: "string",
43
+ enum: ["workspace", "project"],
44
+ description: "Working directory. 'workspace' (default) = KC's workspace. 'project' = user's project directory.",
45
+ },
40
46
  },
41
47
  required: ["command"],
42
48
  };
@@ -44,12 +50,17 @@ export class SandboxExecTool extends BaseTool {
44
50
 
45
51
  async execute(input) {
46
52
  const command = input.command || "";
53
+ const cwdScope = input.cwd || "workspace";
47
54
  if (!command.trim()) {
48
55
  return new ToolResult("No command provided", true);
49
56
  }
50
57
 
58
+ const effectiveCwd = (cwdScope === "project" && this._workspace.projectDir)
59
+ ? this._workspace.projectDir
60
+ : this._workspace.cwd;
61
+
51
62
  try {
52
- const { output, code } = await this._run(command);
63
+ const { output, code } = await this._run(command, effectiveCwd);
53
64
  let result = output;
54
65
  if (result.length > MAX_OUTPUT) {
55
66
  result = result.slice(0, MAX_OUTPUT) + "\n[truncated]";
@@ -70,11 +81,11 @@ export class SandboxExecTool extends BaseTool {
70
81
  * @param {string} command
71
82
  * @returns {Promise<{output: string, code: number}>}
72
83
  */
73
- _run(command) {
84
+ _run(command, cwd) {
74
85
  return new Promise((resolve, reject) => {
75
86
  const controller = new AbortController();
76
87
  const proc = spawn("sh", ["-c", command], {
77
- cwd: this._workspace.cwd,
88
+ cwd,
78
89
  stdio: ["ignore", "pipe", "pipe"],
79
90
  signal: controller.signal,
80
91
  });
@@ -0,0 +1,107 @@
1
+ import { BaseTool, ToolResult } from "./base.js";
2
+
3
+ /**
4
+ * Web search via Tavily API.
5
+ * Returns extracted text content from search results.
6
+ */
7
+ export class WebSearchTool extends BaseTool {
8
+ /**
9
+ * @param {string} apiKey - Tavily API key
10
+ */
11
+ constructor(apiKey) {
12
+ super();
13
+ this._apiKey = apiKey;
14
+ }
15
+
16
+ get name() { return "web_search"; }
17
+
18
+ get description() {
19
+ return (
20
+ "Search the web for information using Tavily. Returns extracted text from top results. " +
21
+ "IMPORTANT: Always prioritize information from user-provided domain documents " +
22
+ "(uploaded regulations, sample files, workspace documents) over web search results. " +
23
+ "Use web search only when: (1) the needed information is not in provided documents, " +
24
+ "(2) you need to verify or supplement document content with external sources, or " +
25
+ "(3) the user explicitly asks for web information (e.g., latest LLM model info, API docs)."
26
+ );
27
+ }
28
+
29
+ get inputSchema() {
30
+ return {
31
+ type: "object",
32
+ properties: {
33
+ query: {
34
+ type: "string",
35
+ description: "The search query",
36
+ },
37
+ search_depth: {
38
+ type: "string",
39
+ enum: ["basic", "advanced"],
40
+ description: "Search depth: 'basic' for fast results, 'advanced' for more thorough search (default: basic)",
41
+ },
42
+ max_results: {
43
+ type: "integer",
44
+ description: "Maximum number of results to return (default: 5, max: 10)",
45
+ },
46
+ },
47
+ required: ["query"],
48
+ };
49
+ }
50
+
51
+ async execute(input) {
52
+ const query = input.query || "";
53
+ if (!query.trim()) {
54
+ return new ToolResult("No query provided", true);
55
+ }
56
+
57
+ if (!this._apiKey) {
58
+ return new ToolResult(
59
+ "Web search is not configured. Set TAVILY_API_KEY in your .env file or global config.",
60
+ true,
61
+ );
62
+ }
63
+
64
+ const searchDepth = input.search_depth || "basic";
65
+ const maxResults = Math.min(input.max_results || 5, 10);
66
+
67
+ try {
68
+ const resp = await fetch("https://api.tavily.com/search", {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({
72
+ api_key: this._apiKey,
73
+ query,
74
+ search_depth: searchDepth,
75
+ max_results: maxResults,
76
+ }),
77
+ signal: AbortSignal.timeout(15000),
78
+ });
79
+
80
+ if (!resp.ok) {
81
+ const text = await resp.text();
82
+ return new ToolResult(`Tavily API error ${resp.status}: ${text}`, true);
83
+ }
84
+
85
+ const data = await resp.json();
86
+ const results = data.results || [];
87
+
88
+ if (results.length === 0) {
89
+ return new ToolResult(`No results found for: ${query}`);
90
+ }
91
+
92
+ const lines = [];
93
+ for (const r of results) {
94
+ lines.push(`--- ${r.title || "Untitled"} ---`);
95
+ lines.push(`URL: ${r.url || ""}`);
96
+ lines.push(r.content || "(no content)");
97
+ lines.push("");
98
+ }
99
+
100
+ return new ToolResult(
101
+ `Found ${results.length} result(s) for "${query}":\n\n${lines.join("\n")}`,
102
+ );
103
+ } catch (err) {
104
+ return new ToolResult(`Web search failed: ${err.message}`, true);
105
+ }
106
+ }
107
+ }
@@ -8,15 +8,27 @@ import { BaseTool, ToolResult } from "./base.js";
8
8
  * the configured API provider.
9
9
  */
10
10
  export class WorkerLLMCallTool extends BaseTool {
11
- constructor(workspace, { apiKey, baseUrl } = {}) {
11
+ constructor(workspace, { apiKey, baseUrl, authType = "bearer" } = {}) {
12
12
  super();
13
13
  this._workspace = workspace;
14
14
  this._apiKey = apiKey || "";
15
15
  this._baseUrl = (baseUrl || "https://api.siliconflow.cn/v1").replace(/\/+$/, "");
16
+ this._authType = authType;
16
17
  this._tierModels = {};
17
18
  this._loadTiers();
18
19
  }
19
20
 
21
+ _buildHeaders() {
22
+ const headers = { "Content-Type": "application/json" };
23
+ if (this._authType === "x-api-key") {
24
+ headers["x-api-key"] = this._apiKey;
25
+ headers["anthropic-version"] = "2023-06-01";
26
+ } else {
27
+ headers["Authorization"] = `Bearer ${this._apiKey}`;
28
+ }
29
+ return headers;
30
+ }
31
+
20
32
  _loadTiers() {
21
33
  const envPath = path.join(this._workspace.cwd, ".env");
22
34
  if (!fs.existsSync(envPath)) return;
@@ -78,10 +90,7 @@ export class WorkerLLMCallTool extends BaseTool {
78
90
  try {
79
91
  const resp = await fetch(`${this._baseUrl}/chat/completions`, {
80
92
  method: "POST",
81
- headers: {
82
- "Authorization": `Bearer ${this._apiKey}`,
83
- "Content-Type": "application/json",
84
- },
93
+ headers: this._buildHeaders(),
85
94
  body: JSON.stringify({ model, messages, max_tokens: maxTokens }),
86
95
  signal: AbortSignal.timeout(120000),
87
96
  });
@@ -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
@@ -12,7 +12,7 @@ const COOKING_WORDS = [
12
12
  "Stewing", "Tempering", "Whisking", "Zesting", "Garnishing", "Drizzling",
13
13
  ];
14
14
 
15
- export function CookingSpinner() {
15
+ export function CookingSpinner({ status }) {
16
16
  const [idx, setIdx] = useState(Math.floor(Math.random() * COOKING_WORDS.length));
17
17
 
18
18
  useEffect(() => {
@@ -20,9 +20,11 @@ export function CookingSpinner() {
20
20
  return () => clearInterval(timer);
21
21
  }, []);
22
22
 
23
+ const displayText = status || `${COOKING_WORDS[idx]}...`;
24
+
23
25
  return h(Box, null,
24
26
  h(Text, { color: "yellow" }, " * "),
25
- h(Text, { dimColor: true }, `${COOKING_WORDS[idx]}...`),
27
+ h(Text, { dimColor: true }, displayText),
26
28
  );
27
29
  }
28
30
 
@@ -30,19 +32,29 @@ export function CookingSpinner() {
30
32
 
31
33
  const LENAT_QUOTE = "Intelligence is ten million rules.";
32
34
 
33
- export function StatusBar({ sessionId, phase }) {
35
+ export function StatusBar({ sessionId, phase, contextTokens, contextLimit }) {
36
+ const pct = contextLimit ? Math.round((contextTokens / contextLimit) * 100) : 0;
37
+ const ctxColor = pct > 80 ? "red" : pct > 60 ? "yellow" : "green";
38
+ const ctxLabel = contextTokens >= 1000
39
+ ? `${(contextTokens / 1000).toFixed(1)}k`
40
+ : `${contextTokens || 0}`;
41
+ const limitLabel = contextLimit >= 1000
42
+ ? `${(contextLimit / 1000).toFixed(0)}k`
43
+ : `${contextLimit || 0}`;
44
+
34
45
  return h(Box, { marginTop: 0 },
35
46
  h(Text, { dimColor: true }, " ⏵⏵ KC Agent CLI "),
36
47
  h(Text, { dimColor: true }, sessionId ? `[${sessionId}]` : ""),
37
48
  phase ? h(Text, { color: "cyan" }, ` ${phase.toUpperCase()}`) : null,
38
49
  h(Text, { color: "green" }, " ● "),
39
- h(Text, { dimColor: true }, ${LENAT_QUOTE}`),
50
+ h(Text, { color: ctxColor }, `CTX: ${ctxLabel}/${limitLabel} (${pct}%)`),
51
+ h(Text, { dimColor: true }, ` · ${LENAT_QUOTE}`),
40
52
  );
41
53
  }
42
54
 
43
55
  // --- Welcome banner ---
44
56
 
45
- export function WelcomeBanner() {
57
+ export function WelcomeBanner({ projectDir } = {}) {
46
58
  return h(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1 },
47
59
  h(Box, null,
48
60
  h(Text, { bold: true }, "KC AGENT CLI"),
@@ -50,6 +62,13 @@ export function WelcomeBanner() {
50
62
  ),
51
63
  h(Text, { dimColor: true }, "Hope you never know what KC was."),
52
64
  h(Text, null, ""),
65
+ projectDir
66
+ ? h(Box, { flexDirection: "column" },
67
+ h(Text, { dimColor: true }, `Project: ${projectDir}`),
68
+ h(Text, { color: "yellow", dimColor: true }, "KC has full read/write access to this directory. We recommend backing up important files."),
69
+ )
70
+ : null,
71
+ h(Text, null, ""),
53
72
  h(Text, { dimColor: true }, "Product of Memium / kitchen-engineer42"),
54
73
  );
55
74
  }