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
|
@@ -21,8 +21,10 @@ export class DocumentSearchTool extends BaseTool {
|
|
|
21
21
|
|
|
22
22
|
get description() {
|
|
23
23
|
return (
|
|
24
|
-
"Search for text across documents
|
|
25
|
-
"
|
|
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
|
|
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 {
|
|
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(
|
|
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
|
|
27
|
-
"
|
|
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
|
|
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
|
|
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
|
|
28
|
-
"
|
|
29
|
-
"
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
});
|
package/src/agent/workspace.js
CHANGED
|
@@ -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
|
package/src/cli/components.js
CHANGED
|
@@ -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 },
|
|
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, {
|
|
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
|
}
|