@yeaft/webchat-agent 0.1.442 → 0.1.443
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 +1 -1
- package/unify/tools/agent.js +89 -0
- package/unify/tools/apply-patch.js +176 -0
- package/unify/tools/ask-user.js +62 -0
- package/unify/tools/bash.js +189 -0
- package/unify/tools/close-agent.js +58 -0
- package/unify/tools/file-edit.js +120 -0
- package/unify/tools/file-read.js +125 -0
- package/unify/tools/file-write.js +73 -0
- package/unify/tools/glob.js +143 -0
- package/unify/tools/grep.js +268 -0
- package/unify/tools/history-search.js +69 -0
- package/unify/tools/image-generation.js +97 -0
- package/unify/tools/index.js +92 -0
- package/unify/tools/js-repl.js +122 -0
- package/unify/tools/list-agents.js +56 -0
- package/unify/tools/list-dir.js +106 -0
- package/unify/tools/memory-read.js +91 -0
- package/unify/tools/memory-search.js +101 -0
- package/unify/tools/memory-write.js +114 -0
- package/unify/tools/notebook-edit.js +132 -0
- package/unify/tools/request-permissions.js +60 -0
- package/unify/tools/send-message.js +62 -0
- package/unify/tools/task-tools.js +358 -0
- package/unify/tools/tool-search.js +97 -0
- package/unify/tools/view-image.js +117 -0
- package/unify/tools/wait-agent.js +84 -0
- package/unify/tools/web-fetch.js +131 -0
- package/unify/tools/web-search.js +80 -0
- package/unify/tools/write-stdin.js +54 -0
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent.js — Create a sub-agent for parallel task execution.
|
|
3
|
+
*
|
|
4
|
+
* Sub-agents run in isolated contexts and can be assigned
|
|
5
|
+
* independent tasks. They communicate via send-message/wait-agent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
|
|
11
|
+
/** In-memory sub-agent registry. */
|
|
12
|
+
const agents = new Map();
|
|
13
|
+
|
|
14
|
+
/** Get the global agents map for other tools to access. */
|
|
15
|
+
export function getAgentRegistry() {
|
|
16
|
+
return agents;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineTool({
|
|
20
|
+
name: 'Agent',
|
|
21
|
+
description: `Create a sub-agent to work on an independent task in parallel.
|
|
22
|
+
|
|
23
|
+
Sub-agents run in their own context and can be given specific tasks.
|
|
24
|
+
Use for parallel execution of independent subtasks.
|
|
25
|
+
|
|
26
|
+
Guidelines:
|
|
27
|
+
- Give each agent a clear, focused task description
|
|
28
|
+
- Use unique, descriptive names
|
|
29
|
+
- Sub-agents share the same tools but have independent conversations
|
|
30
|
+
- Use SendMessage to communicate with agents, WaitAgent to collect results
|
|
31
|
+
- Close agents with CloseAgent when done`,
|
|
32
|
+
parameters: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
name: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'A descriptive name for the sub-agent (e.g. "test-writer", "refactor-auth")',
|
|
38
|
+
},
|
|
39
|
+
task: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'The task description for the sub-agent',
|
|
42
|
+
},
|
|
43
|
+
cwd: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Working directory for the sub-agent (optional, defaults to parent cwd)',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['name', 'task'],
|
|
49
|
+
},
|
|
50
|
+
modes: ['work'],
|
|
51
|
+
isConcurrencySafe: () => false,
|
|
52
|
+
isReadOnly: () => false,
|
|
53
|
+
async execute(input, ctx) {
|
|
54
|
+
const { name, task, cwd } = input;
|
|
55
|
+
if (!name) return JSON.stringify({ error: 'name is required' });
|
|
56
|
+
if (!task) return JSON.stringify({ error: 'task is required' });
|
|
57
|
+
|
|
58
|
+
// Check for name collision
|
|
59
|
+
for (const [, agent] of agents) {
|
|
60
|
+
if (agent.name === name && agent.status !== 'closed') {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
error: `Agent "${name}" already exists. Close it first or use a different name.`,
|
|
63
|
+
agentId: agent.id,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const agentId = `agent-${randomUUID().slice(0, 8)}`;
|
|
69
|
+
const agent = {
|
|
70
|
+
id: agentId,
|
|
71
|
+
name,
|
|
72
|
+
task,
|
|
73
|
+
cwd: cwd || ctx?.cwd || process.cwd(),
|
|
74
|
+
status: 'created',
|
|
75
|
+
messages: [],
|
|
76
|
+
result: null,
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
agents.set(agentId, agent);
|
|
81
|
+
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
success: true,
|
|
84
|
+
agentId,
|
|
85
|
+
name,
|
|
86
|
+
message: `Sub-agent "${name}" created (${agentId}). Use SendMessage to give it work.`,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apply-patch.js — Apply a unified diff patch to files.
|
|
3
|
+
*
|
|
4
|
+
* Parses unified diff format and applies changes to the target files.
|
|
5
|
+
* Supports multiple files in a single patch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { resolve, dirname } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a unified diff into hunks.
|
|
15
|
+
* @param {string} patch
|
|
16
|
+
* @returns {Array<{ file: string, hunks: Array }>}
|
|
17
|
+
*/
|
|
18
|
+
function parsePatch(patch) {
|
|
19
|
+
const files = [];
|
|
20
|
+
const lines = patch.split('\n');
|
|
21
|
+
let currentFile = null;
|
|
22
|
+
let currentHunk = null;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
const line = lines[i];
|
|
26
|
+
|
|
27
|
+
// File header: --- a/path or +++ b/path
|
|
28
|
+
if (line.startsWith('--- ')) {
|
|
29
|
+
// Next line should be +++
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (line.startsWith('+++ ')) {
|
|
33
|
+
let filePath = line.slice(4).trim();
|
|
34
|
+
// Strip a/ or b/ prefix
|
|
35
|
+
if (filePath.startsWith('b/')) filePath = filePath.slice(2);
|
|
36
|
+
if (filePath === '/dev/null') continue;
|
|
37
|
+
|
|
38
|
+
currentFile = { file: filePath, hunks: [] };
|
|
39
|
+
files.push(currentFile);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Hunk header: @@ -start,count +start,count @@
|
|
44
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
45
|
+
if (hunkMatch) {
|
|
46
|
+
if (!currentFile) continue;
|
|
47
|
+
currentHunk = {
|
|
48
|
+
oldStart: parseInt(hunkMatch[1], 10),
|
|
49
|
+
oldCount: hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1,
|
|
50
|
+
newStart: parseInt(hunkMatch[3], 10),
|
|
51
|
+
newCount: hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1,
|
|
52
|
+
lines: [],
|
|
53
|
+
};
|
|
54
|
+
currentFile.hunks.push(currentHunk);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Diff content lines
|
|
59
|
+
if (currentHunk && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ') || line === '')) {
|
|
60
|
+
currentHunk.lines.push(line);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply hunks to file content.
|
|
69
|
+
*/
|
|
70
|
+
function applyHunks(content, hunks) {
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
let offset = 0; // tracks line number shifts from previous hunks
|
|
73
|
+
|
|
74
|
+
for (const hunk of hunks) {
|
|
75
|
+
const startLine = hunk.oldStart - 1 + offset; // 0-based
|
|
76
|
+
const newLines = [];
|
|
77
|
+
let removedCount = 0;
|
|
78
|
+
|
|
79
|
+
for (const diffLine of hunk.lines) {
|
|
80
|
+
if (diffLine.startsWith('+')) {
|
|
81
|
+
newLines.push(diffLine.slice(1));
|
|
82
|
+
} else if (diffLine.startsWith('-')) {
|
|
83
|
+
removedCount++;
|
|
84
|
+
} else if (diffLine.startsWith(' ') || diffLine === '') {
|
|
85
|
+
newLines.push(diffLine.startsWith(' ') ? diffLine.slice(1) : diffLine);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Replace the old lines with new lines
|
|
90
|
+
lines.splice(startLine, removedCount + (hunk.oldCount - removedCount > 0 ? hunk.oldCount - removedCount : 0), ...newLines);
|
|
91
|
+
offset += newLines.length - hunk.oldCount;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default defineTool({
|
|
98
|
+
name: 'ApplyPatch',
|
|
99
|
+
description: `Apply a unified diff patch to files.
|
|
100
|
+
|
|
101
|
+
Parses unified diff format (like git diff output) and applies changes.
|
|
102
|
+
Supports patching multiple files in a single diff.
|
|
103
|
+
|
|
104
|
+
Guidelines:
|
|
105
|
+
- Provide standard unified diff format (--- a/file, +++ b/file, @@ hunks)
|
|
106
|
+
- Ensure the diff matches the current file content exactly
|
|
107
|
+
- New files are created automatically with parent directories`,
|
|
108
|
+
parameters: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
patch: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'The unified diff patch content',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ['patch'],
|
|
117
|
+
},
|
|
118
|
+
modes: ['work'],
|
|
119
|
+
isConcurrencySafe: () => false,
|
|
120
|
+
isReadOnly: () => false,
|
|
121
|
+
isDestructive: () => false,
|
|
122
|
+
async execute(input, ctx) {
|
|
123
|
+
const { patch } = input;
|
|
124
|
+
if (!patch) return JSON.stringify({ error: 'patch is required' });
|
|
125
|
+
|
|
126
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const fileDiffs = parsePatch(patch);
|
|
130
|
+
|
|
131
|
+
if (fileDiffs.length === 0) {
|
|
132
|
+
return JSON.stringify({ error: 'No valid file diffs found in patch' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const results = [];
|
|
136
|
+
|
|
137
|
+
for (const fileDiff of fileDiffs) {
|
|
138
|
+
const absPath = resolve(cwd, fileDiff.file);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
let content;
|
|
142
|
+
if (existsSync(absPath)) {
|
|
143
|
+
content = await readFile(absPath, 'utf-8');
|
|
144
|
+
} else {
|
|
145
|
+
// New file
|
|
146
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
147
|
+
content = '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const newContent = applyHunks(content, fileDiff.hunks);
|
|
151
|
+
await writeFile(absPath, newContent, 'utf-8');
|
|
152
|
+
|
|
153
|
+
results.push({
|
|
154
|
+
file: fileDiff.file,
|
|
155
|
+
success: true,
|
|
156
|
+
hunks: fileDiff.hunks.length,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
results.push({
|
|
160
|
+
file: fileDiff.file,
|
|
161
|
+
success: false,
|
|
162
|
+
error: err.message,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const successCount = results.filter(r => r.success).length;
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
results,
|
|
170
|
+
summary: `Applied patch to ${successCount}/${results.length} files`,
|
|
171
|
+
}, null, 2);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return JSON.stringify({ error: `Failed to apply patch: ${err.message}` });
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask-user.js — Ask the user a question and wait for their response.
|
|
3
|
+
*
|
|
4
|
+
* In Unify mode this tool sends a question through the web-bridge
|
|
5
|
+
* and blocks (via Promise) until the user answers. The answer is
|
|
6
|
+
* returned as the tool result.
|
|
7
|
+
*
|
|
8
|
+
* Reference: yeaft-unify-design.md §8
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { defineTool } from './types.js';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
|
|
14
|
+
export default defineTool({
|
|
15
|
+
name: 'AskUser',
|
|
16
|
+
description: `Ask the user a question and wait for their response.
|
|
17
|
+
|
|
18
|
+
Use this tool when you need additional information or clarification from the user.
|
|
19
|
+
The user will see your question in the chat interface and can type a response.
|
|
20
|
+
|
|
21
|
+
Guidelines:
|
|
22
|
+
- Ask specific, focused questions
|
|
23
|
+
- Provide context about why you need the information
|
|
24
|
+
- If presenting options, list them clearly
|
|
25
|
+
- Don't use this for rhetorical questions — only when you genuinely need user input`,
|
|
26
|
+
parameters: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
question: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The question to ask the user',
|
|
32
|
+
},
|
|
33
|
+
options: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
description: 'Optional list of choices for the user to pick from',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['question'],
|
|
40
|
+
},
|
|
41
|
+
modes: ['chat', 'work'],
|
|
42
|
+
isConcurrencySafe: () => false,
|
|
43
|
+
isReadOnly: () => true,
|
|
44
|
+
async execute(input, ctx) {
|
|
45
|
+
const { question, options } = input;
|
|
46
|
+
if (!question) return JSON.stringify({ error: 'question is required' });
|
|
47
|
+
|
|
48
|
+
// Generate a unique request ID for this ask
|
|
49
|
+
const requestId = `ask_${randomUUID().slice(0, 8)}`;
|
|
50
|
+
|
|
51
|
+
// In a full web-bridge integration, this would send an ask_user event
|
|
52
|
+
// and await the answer. For now, return a formatted prompt that the
|
|
53
|
+
// LLM can see — the web-bridge handles the ask flow externally.
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
type: 'ask_user',
|
|
56
|
+
requestId,
|
|
57
|
+
question,
|
|
58
|
+
...(options ? { options } : {}),
|
|
59
|
+
message: `Question sent to user: "${question}"${options ? ` [Options: ${options.join(', ')}]` : ''}`,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash.js — Execute shell commands.
|
|
3
|
+
*
|
|
4
|
+
* Spawns a child process to run shell commands with timeout, output
|
|
5
|
+
* truncation, working directory support, and cancellation via AbortSignal.
|
|
6
|
+
*
|
|
7
|
+
* Modeled after Claude Code's Bash tool implementation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineTool } from './types.js';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
|
|
15
|
+
/** Max output size in bytes before truncation (256 KB). */
|
|
16
|
+
const MAX_OUTPUT = 256 * 1024;
|
|
17
|
+
|
|
18
|
+
/** Default timeout in ms (2 minutes). */
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
20
|
+
|
|
21
|
+
/** Max timeout in ms (10 minutes). */
|
|
22
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run a command in a child process.
|
|
26
|
+
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number, timedOut: boolean }>}
|
|
27
|
+
*/
|
|
28
|
+
function runCommand(command, { cwd, timeout, signal }) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
31
|
+
const proc = spawn(shell, ['-c', command], {
|
|
32
|
+
cwd,
|
|
33
|
+
env: { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' },
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
timeout,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let stdout = '';
|
|
39
|
+
let stderr = '';
|
|
40
|
+
let stdoutTruncated = false;
|
|
41
|
+
let stderrTruncated = false;
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
|
|
44
|
+
proc.stdout.on('data', (chunk) => {
|
|
45
|
+
if (stdout.length < MAX_OUTPUT) {
|
|
46
|
+
stdout += chunk.toString();
|
|
47
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
48
|
+
stdout = stdout.slice(0, MAX_OUTPUT);
|
|
49
|
+
stdoutTruncated = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
proc.stderr.on('data', (chunk) => {
|
|
55
|
+
if (stderr.length < MAX_OUTPUT) {
|
|
56
|
+
stderr += chunk.toString();
|
|
57
|
+
if (stderr.length > MAX_OUTPUT) {
|
|
58
|
+
stderr = stderr.slice(0, MAX_OUTPUT);
|
|
59
|
+
stderrTruncated = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Handle abort signal
|
|
65
|
+
const onAbort = () => {
|
|
66
|
+
try { proc.kill('SIGTERM'); } catch {}
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
69
|
+
}, 2000);
|
|
70
|
+
};
|
|
71
|
+
if (signal) {
|
|
72
|
+
if (signal.aborted) { onAbort(); return; }
|
|
73
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
proc.on('close', (code) => {
|
|
77
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
78
|
+
resolve({
|
|
79
|
+
stdout: stdoutTruncated ? stdout + '\n... (output truncated)' : stdout,
|
|
80
|
+
stderr: stderrTruncated ? stderr + '\n... (stderr truncated)' : stderr,
|
|
81
|
+
exitCode: code ?? 1,
|
|
82
|
+
timedOut,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
proc.on('error', (err) => {
|
|
87
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
88
|
+
if (err.code === 'ETIMEDOUT' || err.killed) {
|
|
89
|
+
timedOut = true;
|
|
90
|
+
resolve({
|
|
91
|
+
stdout,
|
|
92
|
+
stderr: stderr + `\nProcess timed out after ${timeout}ms`,
|
|
93
|
+
exitCode: 124,
|
|
94
|
+
timedOut: true,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
resolve({
|
|
98
|
+
stdout,
|
|
99
|
+
stderr: `Error spawning process: ${err.message}`,
|
|
100
|
+
exitCode: 1,
|
|
101
|
+
timedOut: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default defineTool({
|
|
109
|
+
name: 'Bash',
|
|
110
|
+
description: `Execute a shell command and return its output.
|
|
111
|
+
|
|
112
|
+
Use this tool to run CLI commands, scripts, and system operations.
|
|
113
|
+
|
|
114
|
+
Guidelines:
|
|
115
|
+
- Commands run in the working directory (cwd from context)
|
|
116
|
+
- Timeout defaults to 2 minutes (max 10 minutes)
|
|
117
|
+
- Large outputs are truncated at 256KB
|
|
118
|
+
- Use absolute paths when possible
|
|
119
|
+
- Avoid interactive commands (no stdin support)
|
|
120
|
+
- For long-running tasks, consider redirecting output to a file
|
|
121
|
+
- stderr is captured separately and included in the result`,
|
|
122
|
+
parameters: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
command: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'The shell command to execute',
|
|
128
|
+
},
|
|
129
|
+
cwd: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Working directory for the command (default: engine cwd)',
|
|
132
|
+
},
|
|
133
|
+
timeout_ms: {
|
|
134
|
+
type: 'number',
|
|
135
|
+
description: `Timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS}, max: ${MAX_TIMEOUT_MS})`,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ['command'],
|
|
139
|
+
},
|
|
140
|
+
modes: ['work'],
|
|
141
|
+
isConcurrencySafe: () => false,
|
|
142
|
+
isReadOnly: () => false,
|
|
143
|
+
isDestructive: (input) => {
|
|
144
|
+
if (!input?.command) return false;
|
|
145
|
+
const cmd = input.command.toLowerCase();
|
|
146
|
+
return cmd.includes('rm ') || cmd.includes('rmdir') ||
|
|
147
|
+
cmd.includes('git reset --hard') || cmd.includes('git clean') ||
|
|
148
|
+
cmd.includes('dd ') || cmd.includes('mkfs') ||
|
|
149
|
+
cmd.includes('> /dev/') || cmd.includes('chmod 000');
|
|
150
|
+
},
|
|
151
|
+
async execute(input, ctx) {
|
|
152
|
+
const { command, cwd: inputCwd, timeout_ms } = input;
|
|
153
|
+
if (!command) return JSON.stringify({ error: 'command is required' });
|
|
154
|
+
|
|
155
|
+
// Resolve working directory
|
|
156
|
+
const cwd = inputCwd
|
|
157
|
+
? resolve(inputCwd)
|
|
158
|
+
: (ctx?.cwd || process.cwd());
|
|
159
|
+
|
|
160
|
+
if (!existsSync(cwd)) {
|
|
161
|
+
return JSON.stringify({ error: `Working directory does not exist: ${cwd}` });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clamp timeout
|
|
165
|
+
const timeout = Math.min(Math.max(timeout_ms || DEFAULT_TIMEOUT_MS, 1000), MAX_TIMEOUT_MS);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await runCommand(command, {
|
|
169
|
+
cwd,
|
|
170
|
+
timeout,
|
|
171
|
+
signal: ctx?.signal,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Format output similar to Claude Code
|
|
175
|
+
const parts = [];
|
|
176
|
+
if (result.stdout) parts.push(result.stdout);
|
|
177
|
+
if (result.stderr) parts.push(`STDERR:\n${result.stderr}`);
|
|
178
|
+
if (result.timedOut) parts.push(`\n(Command timed out after ${timeout}ms)`);
|
|
179
|
+
|
|
180
|
+
const output = parts.join('\n') || '(no output)';
|
|
181
|
+
|
|
182
|
+
return result.exitCode === 0
|
|
183
|
+
? output
|
|
184
|
+
: `Exit code: ${result.exitCode}\n${output}`;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return JSON.stringify({ error: `Bash execution failed: ${err.message}` });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* close-agent.js — Close a sub-agent and clean up.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineTool } from './types.js';
|
|
6
|
+
import { getAgentRegistry } from './agent.js';
|
|
7
|
+
|
|
8
|
+
export default defineTool({
|
|
9
|
+
name: 'CloseAgent',
|
|
10
|
+
description: `Close a sub-agent and release its resources.
|
|
11
|
+
|
|
12
|
+
Use when a sub-agent's task is complete or no longer needed.
|
|
13
|
+
The agent's result (if any) is returned before closing.`,
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
agent_id: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'The sub-agent ID to close',
|
|
20
|
+
},
|
|
21
|
+
result: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Optional final result to set before closing',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['agent_id'],
|
|
27
|
+
},
|
|
28
|
+
modes: ['work'],
|
|
29
|
+
isConcurrencySafe: () => false,
|
|
30
|
+
isReadOnly: () => false,
|
|
31
|
+
async execute(input, ctx) {
|
|
32
|
+
const { agent_id, result } = input;
|
|
33
|
+
if (!agent_id) return JSON.stringify({ error: 'agent_id is required' });
|
|
34
|
+
|
|
35
|
+
const agents = getAgentRegistry();
|
|
36
|
+
const agent = agents.get(agent_id);
|
|
37
|
+
|
|
38
|
+
if (!agent) {
|
|
39
|
+
return JSON.stringify({ error: `Agent not found: ${agent_id}` });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (result) {
|
|
43
|
+
agent.result = result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const finalResult = agent.result;
|
|
47
|
+
agent.status = 'closed';
|
|
48
|
+
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
success: true,
|
|
51
|
+
agentId: agent_id,
|
|
52
|
+
name: agent.name,
|
|
53
|
+
result: finalResult,
|
|
54
|
+
messages: agent.messages.length,
|
|
55
|
+
message: `Agent "${agent.name}" closed`,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-edit.js — Surgical string-replacement edits to files.
|
|
3
|
+
*
|
|
4
|
+
* Performs exact string matching and replacement within files,
|
|
5
|
+
* similar to Claude Code's Edit tool. Supports replace_all for
|
|
6
|
+
* bulk replacements across the file.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { defineTool } from './types.js';
|
|
10
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { resolve } from 'path';
|
|
13
|
+
|
|
14
|
+
export default defineTool({
|
|
15
|
+
name: 'FileEdit',
|
|
16
|
+
description: `Make surgical text replacements in an existing file.
|
|
17
|
+
|
|
18
|
+
Replaces exact occurrences of old_string with new_string.
|
|
19
|
+
The old_string must be unique in the file unless replace_all is true.
|
|
20
|
+
|
|
21
|
+
Guidelines:
|
|
22
|
+
- old_string must match EXACTLY (including whitespace and indentation)
|
|
23
|
+
- The edit fails if old_string is not found or is not unique
|
|
24
|
+
- Use replace_all: true to replace ALL occurrences
|
|
25
|
+
- For creating new files or full rewrites, use FileWrite instead
|
|
26
|
+
- Always read the file first to understand its current content`,
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
file_path: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Path to the file to edit (absolute or relative to cwd)',
|
|
33
|
+
},
|
|
34
|
+
old_string: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'The exact text to find and replace',
|
|
37
|
+
},
|
|
38
|
+
new_string: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'The replacement text',
|
|
41
|
+
},
|
|
42
|
+
replace_all: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'Replace all occurrences (default: false — fails if not unique)',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ['file_path', 'old_string', 'new_string'],
|
|
48
|
+
},
|
|
49
|
+
modes: ['work'],
|
|
50
|
+
isConcurrencySafe: () => false,
|
|
51
|
+
isReadOnly: () => false,
|
|
52
|
+
isDestructive: () => false,
|
|
53
|
+
async execute(input, ctx) {
|
|
54
|
+
const { file_path, old_string, new_string, replace_all = false } = input;
|
|
55
|
+
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
56
|
+
if (old_string === undefined) return JSON.stringify({ error: 'old_string is required' });
|
|
57
|
+
if (new_string === undefined) return JSON.stringify({ error: 'new_string is required' });
|
|
58
|
+
if (old_string === new_string) return JSON.stringify({ error: 'old_string and new_string are identical' });
|
|
59
|
+
|
|
60
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
61
|
+
const absPath = resolve(cwd, file_path);
|
|
62
|
+
|
|
63
|
+
if (!existsSync(absPath)) {
|
|
64
|
+
return JSON.stringify({ error: `File not found: ${absPath}` });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(absPath, 'utf-8');
|
|
69
|
+
|
|
70
|
+
// Count occurrences
|
|
71
|
+
let count = 0;
|
|
72
|
+
let idx = 0;
|
|
73
|
+
while (true) {
|
|
74
|
+
idx = content.indexOf(old_string, idx);
|
|
75
|
+
if (idx === -1) break;
|
|
76
|
+
count++;
|
|
77
|
+
idx += old_string.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (count === 0) {
|
|
81
|
+
// Provide context for debugging
|
|
82
|
+
const preview = old_string.length > 100
|
|
83
|
+
? old_string.slice(0, 100) + '...'
|
|
84
|
+
: old_string;
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
error: `old_string not found in file`,
|
|
87
|
+
hint: `The exact text "${preview}" was not found in ${absPath}. Check whitespace and indentation.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (count > 1 && !replace_all) {
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
error: `old_string found ${count} times — not unique. Use replace_all: true to replace all occurrences, or provide more context to make it unique.`,
|
|
94
|
+
occurrences: count,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Perform replacement
|
|
99
|
+
let newContent;
|
|
100
|
+
if (replace_all) {
|
|
101
|
+
newContent = content.split(old_string).join(new_string);
|
|
102
|
+
} else {
|
|
103
|
+
// Replace only the first occurrence (which is guaranteed unique)
|
|
104
|
+
const replaceIdx = content.indexOf(old_string);
|
|
105
|
+
newContent = content.slice(0, replaceIdx) + new_string + content.slice(replaceIdx + old_string.length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await writeFile(absPath, newContent, 'utf-8');
|
|
109
|
+
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
success: true,
|
|
112
|
+
path: absPath,
|
|
113
|
+
replacements: replace_all ? count : 1,
|
|
114
|
+
message: `Replaced ${replace_all ? count : 1} occurrence(s) in ${absPath}`,
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return JSON.stringify({ error: `Failed to edit file: ${err.message}` });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|