minimal-agent 0.11.0 → 0.12.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/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
|
@@ -22,7 +22,7 @@ function within(root, candidate) {
|
|
|
22
22
|
return rel === '' ? '' : null;
|
|
23
23
|
return rel.split(sep).join('/');
|
|
24
24
|
}
|
|
25
|
-
export async function fileActivityMetadata(filePath, activity, verification = 'not_run', purpose = activity === 'read' ? 'source' : 'support', cwd = getWorkingDir()) {
|
|
25
|
+
export async function fileActivityMetadata(filePath, activity, verification = 'not_run', purpose = activity === 'read' ? 'source' : 'support', cwd = getWorkingDir(), patch) {
|
|
26
26
|
const lexicalRoot = resolve(cwd);
|
|
27
27
|
const root = await realpath(lexicalRoot).catch(() => lexicalRoot);
|
|
28
28
|
const candidate = await realpath(filePath).catch(() => resolve(filePath));
|
|
@@ -45,6 +45,7 @@ export async function fileActivityMetadata(filePath, activity, verification = 'n
|
|
|
45
45
|
...(MIME_BY_EXT[extname(relativePath).toLowerCase()] ? { mime: MIME_BY_EXT[extname(relativePath).toLowerCase()] } : {}),
|
|
46
46
|
verification,
|
|
47
47
|
purpose,
|
|
48
|
+
...(patch ? { patch } : {}),
|
|
48
49
|
}],
|
|
49
50
|
};
|
|
50
51
|
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
FileActivityKind,
|
|
9
9
|
FileActivityMetadata,
|
|
10
10
|
FileVerification,
|
|
11
|
+
FilePatchSummary,
|
|
11
12
|
} from '../../types.js';
|
|
12
13
|
|
|
13
14
|
const MIME_BY_EXT: Record<string, string> = {
|
|
@@ -37,6 +38,7 @@ export async function fileActivityMetadata(
|
|
|
37
38
|
verification: FileVerification = 'not_run',
|
|
38
39
|
purpose: ConversationFilePurpose = activity === 'read' ? 'source' : 'support',
|
|
39
40
|
cwd = getWorkingDir(),
|
|
41
|
+
patch?: FilePatchSummary,
|
|
40
42
|
): Promise<FileActivityMetadata | undefined> {
|
|
41
43
|
const lexicalRoot = resolve(cwd);
|
|
42
44
|
const root = await realpath(lexicalRoot).catch(() => lexicalRoot);
|
|
@@ -58,6 +60,7 @@ export async function fileActivityMetadata(
|
|
|
58
60
|
...(MIME_BY_EXT[extname(relativePath).toLowerCase()] ? { mime: MIME_BY_EXT[extname(relativePath).toLowerCase()] } : {}),
|
|
59
61
|
verification,
|
|
60
62
|
purpose,
|
|
63
|
+
...(patch ? { patch } : {}),
|
|
61
64
|
}],
|
|
62
65
|
};
|
|
63
66
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Minimal recoverability boundary for model-issued shell commands. */
|
|
2
|
+
const BASH_DELETE = [
|
|
3
|
+
/(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i,
|
|
4
|
+
/(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i,
|
|
5
|
+
];
|
|
6
|
+
const GIT_CLEAN = /(?:^|[;&|\n])\s*git\b[^\n;&|]*\bclean\b([^\n;&|]*)/i;
|
|
7
|
+
const POWERSHELL_DELETE = [
|
|
8
|
+
/(?:^|[;|\n])\s*(?:Remove-Item|Clear-RecycleBin)\b/i,
|
|
9
|
+
/(?:^|[;|\n])\s*(?:del|erase|rd|rmdir|rm|ri)\b/i,
|
|
10
|
+
/\.Delete\s*\(/i,
|
|
11
|
+
];
|
|
12
|
+
export function permanentDeleteReason(command, shell) {
|
|
13
|
+
let hit = (shell === 'bash' ? BASH_DELETE : POWERSHELL_DELETE).some((pattern) => pattern.test(command));
|
|
14
|
+
if (!hit && shell === 'bash') {
|
|
15
|
+
const gitClean = GIT_CLEAN.exec(command);
|
|
16
|
+
const options = gitClean?.[1] ?? '';
|
|
17
|
+
const dryRun = /--dry-run\b/.test(options) ||
|
|
18
|
+
/(?:^|\s)-[a-z]*n[a-z]*(?:\s|$)/i.test(options);
|
|
19
|
+
hit = Boolean(gitClean &&
|
|
20
|
+
!dryRun &&
|
|
21
|
+
/(?:^|\s)-[a-z]*f[a-z]*(?:\s|$)|--force\b/i.test(options));
|
|
22
|
+
}
|
|
23
|
+
return hit
|
|
24
|
+
? '该命令会绕过系统回收站永久删除文件。请改用 Trash;清理 Git 未跟踪文件请用 Trash(action="git-clean")。'
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Minimal recoverability boundary for model-issued shell commands. */
|
|
2
|
+
const BASH_DELETE = [
|
|
3
|
+
/(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i,
|
|
4
|
+
/(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i,
|
|
5
|
+
];
|
|
6
|
+
const GIT_CLEAN = /(?:^|[;&|\n])\s*git\b[^\n;&|]*\bclean\b([^\n;&|]*)/i;
|
|
7
|
+
const POWERSHELL_DELETE = [
|
|
8
|
+
/(?:^|[;|\n])\s*(?:Remove-Item|Clear-RecycleBin)\b/i,
|
|
9
|
+
/(?:^|[;|\n])\s*(?:del|erase|rd|rmdir|rm|ri)\b/i,
|
|
10
|
+
/\.Delete\s*\(/i,
|
|
11
|
+
];
|
|
12
|
+
export function permanentDeleteReason(
|
|
13
|
+
command: string,
|
|
14
|
+
shell: 'bash' | 'powershell',
|
|
15
|
+
): string | undefined {
|
|
16
|
+
let hit = (shell === 'bash' ? BASH_DELETE : POWERSHELL_DELETE).some(
|
|
17
|
+
(pattern) => pattern.test(command),
|
|
18
|
+
);
|
|
19
|
+
if (!hit && shell === 'bash') {
|
|
20
|
+
const gitClean = GIT_CLEAN.exec(command);
|
|
21
|
+
const options = gitClean?.[1] ?? '';
|
|
22
|
+
const dryRun =
|
|
23
|
+
/--dry-run\b/.test(options) ||
|
|
24
|
+
/(?:^|\s)-[a-z]*n[a-z]*(?:\s|$)/i.test(options);
|
|
25
|
+
hit = Boolean(
|
|
26
|
+
gitClean &&
|
|
27
|
+
!dryRun &&
|
|
28
|
+
/(?:^|\s)-[a-z]*f[a-z]*(?:\s|$)|--force\b/i.test(options),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return hit
|
|
32
|
+
? '该命令会绕过系统回收站永久删除文件。请改用 Trash;清理 Git 未跟踪文件请用 Trash(action="git-clean")。'
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createWriteStream, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { sessionRuntimeDir, pruneRuntimeOutputs, registerRuntimeOutput, unregisterRuntimeOutput, } from '../../session/runtimeStore.js';
|
|
5
|
+
const SPILL_AT = 64 * 1024, MAX_BYTES = 512 * 1024 * 1024, PREVIEW = 32 * 1024;
|
|
6
|
+
export class ToolOutputStore {
|
|
7
|
+
stdout = '';
|
|
8
|
+
stderr = '';
|
|
9
|
+
stdoutTail = '';
|
|
10
|
+
stderrTail = '';
|
|
11
|
+
stream;
|
|
12
|
+
pending = [];
|
|
13
|
+
hash = createHash('sha256');
|
|
14
|
+
diskHash;
|
|
15
|
+
diskBytes = 0;
|
|
16
|
+
bytes = 0;
|
|
17
|
+
complete = true;
|
|
18
|
+
path;
|
|
19
|
+
constructor(identity) {
|
|
20
|
+
const dir = join(sessionRuntimeDir(identity), 'outputs');
|
|
21
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
22
|
+
this.path = join(dir, `procout_${randomUUID()}.log`);
|
|
23
|
+
}
|
|
24
|
+
append(source, value) {
|
|
25
|
+
const raw = Buffer.from(value);
|
|
26
|
+
if (this.bytes >= MAX_BYTES) {
|
|
27
|
+
this.complete = false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
const selected = raw.subarray(0, MAX_BYTES - this.bytes);
|
|
31
|
+
if (selected.length < raw.length)
|
|
32
|
+
this.complete = false;
|
|
33
|
+
const shouldSpill = !this.stream && this.bytes + selected.length > SPILL_AT;
|
|
34
|
+
if (shouldSpill) {
|
|
35
|
+
this.stream = createWriteStream(this.path, { flags: 'wx', mode: 0o600 });
|
|
36
|
+
this.diskHash = createHash('sha256');
|
|
37
|
+
registerRuntimeOutput(this.path);
|
|
38
|
+
for (const item of this.pending)
|
|
39
|
+
this.writeFrame(item.source, item.data);
|
|
40
|
+
this.pending = [];
|
|
41
|
+
}
|
|
42
|
+
this.bytes += selected.length;
|
|
43
|
+
this.hash.update(selected);
|
|
44
|
+
const text = selected.toString('utf8');
|
|
45
|
+
if (source === 'stdout') {
|
|
46
|
+
if (this.stdout.length < PREVIEW)
|
|
47
|
+
this.stdout += text.slice(0, PREVIEW - this.stdout.length);
|
|
48
|
+
this.stdoutTail = (this.stdoutTail + text).slice(-PREVIEW);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (this.stderr.length < PREVIEW)
|
|
52
|
+
this.stderr += text.slice(0, PREVIEW - this.stderr.length);
|
|
53
|
+
this.stderrTail = (this.stderrTail + text).slice(-PREVIEW);
|
|
54
|
+
}
|
|
55
|
+
if (this.stream)
|
|
56
|
+
return this.writeFrame(source, selected);
|
|
57
|
+
this.pending.push({ source, data: Buffer.from(selected) });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
frame(source, data) {
|
|
61
|
+
return Buffer.concat([Buffer.from(`[${source}] `), data]);
|
|
62
|
+
}
|
|
63
|
+
writeFrame(source, data) {
|
|
64
|
+
const framed = this.frame(source, data);
|
|
65
|
+
this.diskBytes += framed.length;
|
|
66
|
+
this.diskHash.update(framed);
|
|
67
|
+
return this.stream.write(framed);
|
|
68
|
+
}
|
|
69
|
+
onDrain(callback) {
|
|
70
|
+
this.stream?.once('drain', callback);
|
|
71
|
+
}
|
|
72
|
+
async finish() {
|
|
73
|
+
if (this.stream) {
|
|
74
|
+
try {
|
|
75
|
+
await new Promise((resolve, reject) => {
|
|
76
|
+
this.stream.once('error', reject);
|
|
77
|
+
this.stream.end(() => resolve());
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
unregisterRuntimeOutput(this.path);
|
|
82
|
+
void pruneRuntimeOutputs();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const preview = (head, tail) => this.stream && tail && !head.endsWith(tail)
|
|
86
|
+
? `${head}\n... [middle stored on disk] ...\n${tail}`
|
|
87
|
+
: head;
|
|
88
|
+
return {
|
|
89
|
+
stdout: preview(this.stdout, this.stdoutTail),
|
|
90
|
+
stderr: preview(this.stderr, this.stderrTail),
|
|
91
|
+
...(this.stream ? { path: this.path } : {}),
|
|
92
|
+
bytes: this.stream ? this.diskBytes : this.bytes,
|
|
93
|
+
sha256: this.stream ? this.diskHash.digest('hex') : this.hash.digest('hex'),
|
|
94
|
+
complete: this.complete,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createWriteStream, mkdirSync, type WriteStream } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
sessionRuntimeDir,
|
|
6
|
+
pruneRuntimeOutputs,
|
|
7
|
+
registerRuntimeOutput,
|
|
8
|
+
type RuntimeIdentity,
|
|
9
|
+
unregisterRuntimeOutput,
|
|
10
|
+
} from '../../session/runtimeStore.js';
|
|
11
|
+
|
|
12
|
+
const SPILL_AT = 64 * 1024,
|
|
13
|
+
MAX_BYTES = 512 * 1024 * 1024,
|
|
14
|
+
PREVIEW = 32 * 1024;
|
|
15
|
+
export interface StoredOutput {
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
sha256: string;
|
|
21
|
+
complete: boolean;
|
|
22
|
+
}
|
|
23
|
+
export class ToolOutputStore {
|
|
24
|
+
private stdout = '';
|
|
25
|
+
private stderr = '';
|
|
26
|
+
private stdoutTail = '';
|
|
27
|
+
private stderrTail = '';
|
|
28
|
+
private stream?: WriteStream;
|
|
29
|
+
private pending: Array<{ source: 'stdout' | 'stderr'; data: Buffer }> = [];
|
|
30
|
+
private hash = createHash('sha256');
|
|
31
|
+
private diskHash?: ReturnType<typeof createHash>;
|
|
32
|
+
private diskBytes = 0;
|
|
33
|
+
private bytes = 0;
|
|
34
|
+
private complete = true;
|
|
35
|
+
readonly path: string;
|
|
36
|
+
constructor(identity: RuntimeIdentity) {
|
|
37
|
+
const dir = join(sessionRuntimeDir(identity), 'outputs');
|
|
38
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
39
|
+
this.path = join(dir, `procout_${randomUUID()}.log`);
|
|
40
|
+
}
|
|
41
|
+
append(source: 'stdout' | 'stderr', value: Buffer | string): boolean {
|
|
42
|
+
const raw = Buffer.from(value);
|
|
43
|
+
if (this.bytes >= MAX_BYTES) {
|
|
44
|
+
this.complete = false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const selected = raw.subarray(0, MAX_BYTES - this.bytes);
|
|
48
|
+
if (selected.length < raw.length) this.complete = false;
|
|
49
|
+
const shouldSpill = !this.stream && this.bytes + selected.length > SPILL_AT;
|
|
50
|
+
if (shouldSpill) {
|
|
51
|
+
this.stream = createWriteStream(this.path, { flags: 'wx', mode: 0o600 });
|
|
52
|
+
this.diskHash = createHash('sha256');
|
|
53
|
+
registerRuntimeOutput(this.path);
|
|
54
|
+
for (const item of this.pending) this.writeFrame(item.source, item.data);
|
|
55
|
+
this.pending = [];
|
|
56
|
+
}
|
|
57
|
+
this.bytes += selected.length;
|
|
58
|
+
this.hash.update(selected);
|
|
59
|
+
const text = selected.toString('utf8');
|
|
60
|
+
if (source === 'stdout') {
|
|
61
|
+
if (this.stdout.length < PREVIEW)
|
|
62
|
+
this.stdout += text.slice(0, PREVIEW - this.stdout.length);
|
|
63
|
+
this.stdoutTail = (this.stdoutTail + text).slice(-PREVIEW);
|
|
64
|
+
} else {
|
|
65
|
+
if (this.stderr.length < PREVIEW)
|
|
66
|
+
this.stderr += text.slice(0, PREVIEW - this.stderr.length);
|
|
67
|
+
this.stderrTail = (this.stderrTail + text).slice(-PREVIEW);
|
|
68
|
+
}
|
|
69
|
+
if (this.stream) return this.writeFrame(source, selected);
|
|
70
|
+
this.pending.push({ source, data: Buffer.from(selected) });
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
private frame(source: 'stdout' | 'stderr', data: Buffer): Buffer {
|
|
74
|
+
return Buffer.concat([Buffer.from(`[${source}] `), data]);
|
|
75
|
+
}
|
|
76
|
+
private writeFrame(source: 'stdout' | 'stderr', data: Buffer): boolean {
|
|
77
|
+
const framed = this.frame(source, data);
|
|
78
|
+
this.diskBytes += framed.length;
|
|
79
|
+
this.diskHash!.update(framed);
|
|
80
|
+
return this.stream!.write(framed);
|
|
81
|
+
}
|
|
82
|
+
onDrain(callback: () => void): void {
|
|
83
|
+
this.stream?.once('drain', callback);
|
|
84
|
+
}
|
|
85
|
+
async finish(): Promise<StoredOutput> {
|
|
86
|
+
if (this.stream) {
|
|
87
|
+
try {
|
|
88
|
+
await new Promise<void>((resolve, reject) => {
|
|
89
|
+
this.stream!.once('error', reject);
|
|
90
|
+
this.stream!.end(() => resolve());
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
unregisterRuntimeOutput(this.path);
|
|
94
|
+
void pruneRuntimeOutputs();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const preview = (head: string, tail: string) =>
|
|
98
|
+
this.stream && tail && !head.endsWith(tail)
|
|
99
|
+
? `${head}\n... [middle stored on disk] ...\n${tail}`
|
|
100
|
+
: head;
|
|
101
|
+
return {
|
|
102
|
+
stdout: preview(this.stdout, this.stdoutTail),
|
|
103
|
+
stderr: preview(this.stderr, this.stderrTail),
|
|
104
|
+
...(this.stream ? { path: this.path } : {}),
|
|
105
|
+
bytes: this.stream ? this.diskBytes : this.bytes,
|
|
106
|
+
sha256: this.stream ? this.diskHash!.digest('hex') : this.hash.digest('hex'),
|
|
107
|
+
complete: this.complete,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
import { getResourceSearchPaths } from '../../utils/resourcePaths.js';
|
|
7
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
8
|
+
const MAX_SKILL_BYTES = 256 * 1024;
|
|
9
|
+
const schema = z.object({
|
|
10
|
+
name: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(200)
|
|
14
|
+
.regex(/^[^/\\]+$/),
|
|
15
|
+
arguments: z.string().max(20_000).optional(),
|
|
16
|
+
});
|
|
17
|
+
const activated = new Map();
|
|
18
|
+
async function candidates(name, cwd) {
|
|
19
|
+
const exact = [];
|
|
20
|
+
for (const root of getResourceSearchPaths('skills', import.meta.url, cwd)) {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.isDirectory())
|
|
30
|
+
continue;
|
|
31
|
+
const path = join(root, entry.name, 'SKILL.md');
|
|
32
|
+
try {
|
|
33
|
+
const text = await readFile(path, 'utf8');
|
|
34
|
+
const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
35
|
+
const metadata = frontmatter
|
|
36
|
+
? parseYaml(frontmatter[1])
|
|
37
|
+
: undefined;
|
|
38
|
+
const declared = typeof metadata?.name === 'string' ? metadata.name.trim() : undefined;
|
|
39
|
+
if (entry.name === name || declared === name)
|
|
40
|
+
exact.push(path);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...new Set(exact)];
|
|
46
|
+
}
|
|
47
|
+
async function call(input, context) {
|
|
48
|
+
const found = await candidates(input.name, context.cwd);
|
|
49
|
+
if (!found.length)
|
|
50
|
+
return { ok: false, error: `没有找到名称精确匹配的 Skill:${input.name}` };
|
|
51
|
+
// Search roots are ordered: project skill intentionally overrides the bundled skill.
|
|
52
|
+
const path = found[0];
|
|
53
|
+
const content = await readFile(path);
|
|
54
|
+
if (content.length > MAX_SKILL_BYTES)
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: `Skill 指令过大(${content.length} > ${MAX_SKILL_BYTES} bytes),拒绝加载半份指令。`,
|
|
58
|
+
};
|
|
59
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
60
|
+
const key = `${context.workspaceId ?? context.cwd ?? 'local'}:${input.name}`;
|
|
61
|
+
const activationSignature = createHash('sha256')
|
|
62
|
+
.update(hash)
|
|
63
|
+
.update('\0')
|
|
64
|
+
.update(input.arguments ?? '')
|
|
65
|
+
.digest('hex');
|
|
66
|
+
if (activated.get(key) === activationSignature)
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
content: `Skill ${input.name} 已按相同内容激活(sha256=${hash},目录=${join(path, '..')})。无需重复注入。`,
|
|
70
|
+
};
|
|
71
|
+
activated.set(key, activationSignature);
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
content: `Skill: ${input.name}\nRoot: ${join(path, '..')}\nSHA-256: ${hash}${input.arguments ? `\nUser arguments: ${input.arguments}` : ''}\n\n${content.toString('utf8')}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export const skillTool = {
|
|
78
|
+
name: 'Skill',
|
|
79
|
+
description: 'Loads one installed Skill by exact name and returns its complete SKILL.md instructions, root directory and content hash. Use this instead of manually locating SKILL.md.',
|
|
80
|
+
inputSchema: schema,
|
|
81
|
+
parameters: toToolParameters(schema),
|
|
82
|
+
isReadOnly: true,
|
|
83
|
+
isConcurrencySafe: true,
|
|
84
|
+
call: async () => ({ ok: false, error: 'Skill requires runtime context' }),
|
|
85
|
+
callWithContext: (input, _signal, context) => call(input, context),
|
|
86
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
|
|
7
|
+
import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
|
|
8
|
+
import { getResourceSearchPaths } from '../../utils/resourcePaths.js';
|
|
9
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
10
|
+
|
|
11
|
+
const MAX_SKILL_BYTES = 256 * 1024;
|
|
12
|
+
const schema = z.object({
|
|
13
|
+
name: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(200)
|
|
17
|
+
.regex(/^[^/\\]+$/),
|
|
18
|
+
arguments: z.string().max(20_000).optional(),
|
|
19
|
+
});
|
|
20
|
+
type Input = z.infer<typeof schema>;
|
|
21
|
+
const activated = new Map<string, string>();
|
|
22
|
+
|
|
23
|
+
async function candidates(name: string, cwd?: string): Promise<string[]> {
|
|
24
|
+
const exact: string[] = [];
|
|
25
|
+
for (const root of getResourceSearchPaths('skills', import.meta.url, cwd)) {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (!entry.isDirectory()) continue;
|
|
34
|
+
const path = join(root, entry.name, 'SKILL.md');
|
|
35
|
+
try {
|
|
36
|
+
const text = await readFile(path, 'utf8');
|
|
37
|
+
const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
38
|
+
const metadata = frontmatter
|
|
39
|
+
? (parseYaml(frontmatter[1]!) as { name?: unknown })
|
|
40
|
+
: undefined;
|
|
41
|
+
const declared =
|
|
42
|
+
typeof metadata?.name === 'string' ? metadata.name.trim() : undefined;
|
|
43
|
+
if (entry.name === name || declared === name) exact.push(path);
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...new Set(exact)];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function call(
|
|
51
|
+
input: Input,
|
|
52
|
+
context: ToolCallContext,
|
|
53
|
+
): Promise<ToolResult> {
|
|
54
|
+
const found = await candidates(input.name, context.cwd);
|
|
55
|
+
if (!found.length)
|
|
56
|
+
return { ok: false, error: `没有找到名称精确匹配的 Skill:${input.name}` };
|
|
57
|
+
// Search roots are ordered: project skill intentionally overrides the bundled skill.
|
|
58
|
+
const path = found[0]!;
|
|
59
|
+
const content = await readFile(path);
|
|
60
|
+
if (content.length > MAX_SKILL_BYTES)
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: `Skill 指令过大(${content.length} > ${MAX_SKILL_BYTES} bytes),拒绝加载半份指令。`,
|
|
64
|
+
};
|
|
65
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
66
|
+
const key = `${context.workspaceId ?? context.cwd ?? 'local'}:${input.name}`;
|
|
67
|
+
const activationSignature = createHash('sha256')
|
|
68
|
+
.update(hash)
|
|
69
|
+
.update('\0')
|
|
70
|
+
.update(input.arguments ?? '')
|
|
71
|
+
.digest('hex');
|
|
72
|
+
if (activated.get(key) === activationSignature)
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
content: `Skill ${input.name} 已按相同内容激活(sha256=${hash},目录=${join(path, '..')})。无需重复注入。`,
|
|
76
|
+
};
|
|
77
|
+
activated.set(key, activationSignature);
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
content: `Skill: ${input.name}\nRoot: ${join(path, '..')}\nSHA-256: ${hash}${input.arguments ? `\nUser arguments: ${input.arguments}` : ''}\n\n${content.toString('utf8')}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const skillTool: Tool<Input> = {
|
|
85
|
+
name: 'Skill',
|
|
86
|
+
description:
|
|
87
|
+
'Loads one installed Skill by exact name and returns its complete SKILL.md instructions, root directory and content hash. Use this instead of manually locating SKILL.md.',
|
|
88
|
+
inputSchema: schema,
|
|
89
|
+
parameters: toToolParameters(schema),
|
|
90
|
+
isReadOnly: true,
|
|
91
|
+
isConcurrencySafe: true,
|
|
92
|
+
call: async () => ({ ok: false, error: 'Skill requires runtime context' }),
|
|
93
|
+
callWithContext: (input, _signal, context) => call(input, context),
|
|
94
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
5
|
+
import { resolveToolPath } from '../shared/fileUtils.js';
|
|
6
|
+
import { recordRead } from '../shared/fileState.js';
|
|
7
|
+
const schema = z
|
|
8
|
+
.object({
|
|
9
|
+
file_path: z.string().min(1),
|
|
10
|
+
start_line: z.number().int().min(1),
|
|
11
|
+
end_line: z.number().int().min(1),
|
|
12
|
+
context: z.number().int().min(0).max(500).default(50).optional(),
|
|
13
|
+
})
|
|
14
|
+
.refine((v) => v.end_line >= v.start_line, {
|
|
15
|
+
message: 'end_line 必须大于等于 start_line',
|
|
16
|
+
});
|
|
17
|
+
async function call(input, cwd, state) {
|
|
18
|
+
const resolved = resolveToolPath(input.file_path, cwd);
|
|
19
|
+
if (!resolved.ok)
|
|
20
|
+
return resolved;
|
|
21
|
+
const before = input.context ?? 50, from = Math.max(1, input.start_line - before), to = input.end_line + before;
|
|
22
|
+
const lines = [];
|
|
23
|
+
let total = 0;
|
|
24
|
+
let hasMore = false;
|
|
25
|
+
try {
|
|
26
|
+
const source = createReadStream(resolved.resolvedPath, {
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
});
|
|
29
|
+
const rl = createInterface({
|
|
30
|
+
input: source,
|
|
31
|
+
crlfDelay: Infinity,
|
|
32
|
+
});
|
|
33
|
+
for await (const line of rl) {
|
|
34
|
+
total++;
|
|
35
|
+
if (total >= from && total <= to)
|
|
36
|
+
lines.push(`${String(total).padStart(6)}\t${line}`);
|
|
37
|
+
if (total > to) {
|
|
38
|
+
hasMore = true;
|
|
39
|
+
rl.close();
|
|
40
|
+
source.destroy();
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return { ok: false, error: `Snip 读取失败:${e.message}` };
|
|
47
|
+
}
|
|
48
|
+
recordRead(resolved.resolvedPath, {
|
|
49
|
+
partial: true,
|
|
50
|
+
offset: from - 1,
|
|
51
|
+
limit: Math.max(0, Math.min(to, total) - from + 1),
|
|
52
|
+
}, state);
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
content: `${lines.join('\n')}\n\n[requested ${input.start_line}-${input.end_line}; shown ${from}-${Math.min(to, total)}; ${hasMore ? `more than ${to} total lines; continue at ${to + 1}` : `total ${total} lines`}]`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export const snipTool = {
|
|
59
|
+
name: 'Snip',
|
|
60
|
+
description: 'Reads a precise line range from a potentially huge text file with bounded surrounding context, without loading the whole file into memory.',
|
|
61
|
+
inputSchema: schema,
|
|
62
|
+
parameters: toToolParameters(schema),
|
|
63
|
+
isReadOnly: true,
|
|
64
|
+
isConcurrencySafe: true,
|
|
65
|
+
call: (input) => call(input),
|
|
66
|
+
callWithContext: (input, _s, ctx) => call(input, ctx.cwd, ctx.sessionState?.fileReads),
|
|
67
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
|
|
5
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
6
|
+
import { resolveToolPath } from '../shared/fileUtils.js';
|
|
7
|
+
import { recordRead, type FileReadState } from '../shared/fileState.js';
|
|
8
|
+
|
|
9
|
+
const schema = z
|
|
10
|
+
.object({
|
|
11
|
+
file_path: z.string().min(1),
|
|
12
|
+
start_line: z.number().int().min(1),
|
|
13
|
+
end_line: z.number().int().min(1),
|
|
14
|
+
context: z.number().int().min(0).max(500).default(50).optional(),
|
|
15
|
+
})
|
|
16
|
+
.refine((v) => v.end_line >= v.start_line, {
|
|
17
|
+
message: 'end_line 必须大于等于 start_line',
|
|
18
|
+
});
|
|
19
|
+
type Input = z.infer<typeof schema>;
|
|
20
|
+
async function call(
|
|
21
|
+
input: Input,
|
|
22
|
+
cwd?: string,
|
|
23
|
+
state?: FileReadState,
|
|
24
|
+
): Promise<ToolResult> {
|
|
25
|
+
const resolved = resolveToolPath(input.file_path, cwd);
|
|
26
|
+
if (!resolved.ok) return resolved;
|
|
27
|
+
const before = input.context ?? 50,
|
|
28
|
+
from = Math.max(1, input.start_line - before),
|
|
29
|
+
to = input.end_line + before;
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
let total = 0;
|
|
32
|
+
let hasMore = false;
|
|
33
|
+
try {
|
|
34
|
+
const source = createReadStream(resolved.resolvedPath, {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
});
|
|
37
|
+
const rl = createInterface({
|
|
38
|
+
input: source,
|
|
39
|
+
crlfDelay: Infinity,
|
|
40
|
+
});
|
|
41
|
+
for await (const line of rl) {
|
|
42
|
+
total++;
|
|
43
|
+
if (total >= from && total <= to)
|
|
44
|
+
lines.push(`${String(total).padStart(6)}\t${line}`);
|
|
45
|
+
if (total > to) {
|
|
46
|
+
hasMore = true;
|
|
47
|
+
rl.close();
|
|
48
|
+
source.destroy();
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { ok: false, error: `Snip 读取失败:${(e as Error).message}` };
|
|
54
|
+
}
|
|
55
|
+
recordRead(
|
|
56
|
+
resolved.resolvedPath,
|
|
57
|
+
{
|
|
58
|
+
partial: true,
|
|
59
|
+
offset: from - 1,
|
|
60
|
+
limit: Math.max(0, Math.min(to, total) - from + 1),
|
|
61
|
+
},
|
|
62
|
+
state,
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
content: `${lines.join('\n')}\n\n[requested ${input.start_line}-${input.end_line}; shown ${from}-${Math.min(to, total)}; ${hasMore ? `more than ${to} total lines; continue at ${to + 1}` : `total ${total} lines`}]`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export const snipTool: Tool<Input> = {
|
|
70
|
+
name: 'Snip',
|
|
71
|
+
description:
|
|
72
|
+
'Reads a precise line range from a potentially huge text file with bounded surrounding context, without loading the whole file into memory.',
|
|
73
|
+
inputSchema: schema,
|
|
74
|
+
parameters: toToolParameters(schema),
|
|
75
|
+
isReadOnly: true,
|
|
76
|
+
isConcurrencySafe: true,
|
|
77
|
+
call: (input) => call(input),
|
|
78
|
+
callWithContext: (input, _s, ctx) =>
|
|
79
|
+
call(input, ctx.cwd, ctx.sessionState?.fileReads),
|
|
80
|
+
};
|