@yeaft/webchat-agent 0.1.442 → 0.1.444
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/connection/message-router.js +5 -1
- package/package.json +1 -1
- package/unify/config.js +18 -0
- 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/unify/web-bridge.js +27 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grep.js — Search file contents for patterns.
|
|
3
|
+
*
|
|
4
|
+
* Searches for regex patterns in files. Tries to use ripgrep (rg) if
|
|
5
|
+
* available for performance, falls back to a Node.js implementation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import { readdir, readFile, stat } from 'fs/promises';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { resolve, join, relative, extname } from 'path';
|
|
13
|
+
|
|
14
|
+
/** Max output lines. */
|
|
15
|
+
const MAX_LINES = 250;
|
|
16
|
+
|
|
17
|
+
/** Binary extensions to skip. */
|
|
18
|
+
const BINARY_EXTS = new Set([
|
|
19
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp',
|
|
20
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wav',
|
|
21
|
+
'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
|
|
22
|
+
'.exe', '.dll', '.so', '.dylib', '.o',
|
|
23
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
24
|
+
'.woff', '.woff2', '.ttf', '.otf',
|
|
25
|
+
'.sqlite', '.db',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if ripgrep is available.
|
|
30
|
+
*/
|
|
31
|
+
function hasRipgrep() {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const proc = spawn('rg', ['--version'], { stdio: 'pipe' });
|
|
34
|
+
proc.on('close', (code) => resolve(code === 0));
|
|
35
|
+
proc.on('error', () => resolve(false));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run ripgrep and return results.
|
|
41
|
+
*/
|
|
42
|
+
function runRipgrep(pattern, searchPath, options) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const args = [
|
|
45
|
+
pattern,
|
|
46
|
+
searchPath,
|
|
47
|
+
'--no-heading',
|
|
48
|
+
'--line-number',
|
|
49
|
+
'--color', 'never',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
if (options.caseInsensitive) args.push('-i');
|
|
53
|
+
if (options.glob) args.push('--glob', options.glob);
|
|
54
|
+
if (options.type) args.push('--type', options.type);
|
|
55
|
+
if (options.filesOnly) args.push('-l');
|
|
56
|
+
if (options.count) args.push('-c');
|
|
57
|
+
if (options.context) args.push('-C', String(options.context));
|
|
58
|
+
if (options.before) args.push('-B', String(options.before));
|
|
59
|
+
if (options.after) args.push('-A', String(options.after));
|
|
60
|
+
if (options.multiline) args.push('-U', '--multiline-dotall');
|
|
61
|
+
args.push('--max-count', String(options.maxResults || 500));
|
|
62
|
+
|
|
63
|
+
const proc = spawn('rg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
64
|
+
let stdout = '';
|
|
65
|
+
let stderr = '';
|
|
66
|
+
|
|
67
|
+
proc.stdout.on('data', (chunk) => {
|
|
68
|
+
stdout += chunk.toString();
|
|
69
|
+
// Truncate early if way too large
|
|
70
|
+
if (stdout.length > 512 * 1024) {
|
|
71
|
+
try { proc.kill(); } catch {}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
75
|
+
proc.on('close', (code) => {
|
|
76
|
+
if (code === 0 || code === 1) {
|
|
77
|
+
resolve(stdout);
|
|
78
|
+
} else {
|
|
79
|
+
reject(new Error(stderr || `rg exited with code ${code}`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
proc.on('error', reject);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fallback: Node.js grep implementation.
|
|
88
|
+
*/
|
|
89
|
+
async function nodeGrep(pattern, searchPath, options) {
|
|
90
|
+
const regex = new RegExp(pattern, options.caseInsensitive ? 'gi' : 'g');
|
|
91
|
+
const results = [];
|
|
92
|
+
const SKIP = new Set(['node_modules', '.git', '__pycache__', '.next', 'dist', 'build', '.cache']);
|
|
93
|
+
|
|
94
|
+
async function searchDir(dir) {
|
|
95
|
+
if (results.length >= (options.maxResults || 500)) return;
|
|
96
|
+
let entries;
|
|
97
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (results.length >= (options.maxResults || 500)) return;
|
|
101
|
+
const fullPath = join(dir, entry.name);
|
|
102
|
+
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
if (SKIP.has(entry.name)) continue;
|
|
105
|
+
await searchDir(fullPath);
|
|
106
|
+
} else {
|
|
107
|
+
const ext = extname(entry.name).toLowerCase();
|
|
108
|
+
if (BINARY_EXTS.has(ext)) continue;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const fileStat = await stat(fullPath);
|
|
112
|
+
if (fileStat.size > 1024 * 1024) continue; // skip files > 1MB
|
|
113
|
+
|
|
114
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
115
|
+
const relPath = relative(searchPath, fullPath);
|
|
116
|
+
|
|
117
|
+
if (options.filesOnly) {
|
|
118
|
+
if (regex.test(content)) results.push(relPath);
|
|
119
|
+
regex.lastIndex = 0;
|
|
120
|
+
} else if (options.count) {
|
|
121
|
+
const matches = content.match(regex);
|
|
122
|
+
if (matches) results.push(`${relPath}:${matches.length}`);
|
|
123
|
+
} else {
|
|
124
|
+
const lines = content.split('\n');
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
if (regex.test(lines[i])) {
|
|
127
|
+
results.push(`${relPath}:${i + 1}:${lines[i]}`);
|
|
128
|
+
}
|
|
129
|
+
regex.lastIndex = 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Skip unreadable files
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await searchDir(searchPath);
|
|
140
|
+
return results.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default defineTool({
|
|
144
|
+
name: 'Grep',
|
|
145
|
+
description: `Search file contents for a regex pattern.
|
|
146
|
+
|
|
147
|
+
Uses ripgrep (rg) when available for fast searching, with a Node.js fallback.
|
|
148
|
+
|
|
149
|
+
Output modes:
|
|
150
|
+
- "content" — show matching lines with file path and line numbers
|
|
151
|
+
- "files_with_matches" — show only file paths that match (default)
|
|
152
|
+
- "count" — show match count per file
|
|
153
|
+
|
|
154
|
+
Guidelines:
|
|
155
|
+
- Uses regex syntax (escape special chars: \\., \\{, etc.)
|
|
156
|
+
- Use glob or type filters to narrow the search
|
|
157
|
+
- Skips binary files and common large directories (node_modules, .git)
|
|
158
|
+
- Results are limited to 500 matches by default`,
|
|
159
|
+
parameters: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
pattern: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'Regex pattern to search for',
|
|
165
|
+
},
|
|
166
|
+
path: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'File or directory to search (default: cwd)',
|
|
169
|
+
},
|
|
170
|
+
output_mode: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
enum: ['content', 'files_with_matches', 'count'],
|
|
173
|
+
description: 'Output format (default: "files_with_matches")',
|
|
174
|
+
},
|
|
175
|
+
glob: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
description: 'Glob filter for file names (e.g. "*.js", "*.{ts,tsx}")',
|
|
178
|
+
},
|
|
179
|
+
type: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: 'File type filter (e.g. "js", "py", "rust")',
|
|
182
|
+
},
|
|
183
|
+
case_insensitive: {
|
|
184
|
+
type: 'boolean',
|
|
185
|
+
description: 'Case-insensitive search (default: false)',
|
|
186
|
+
},
|
|
187
|
+
context: {
|
|
188
|
+
type: 'number',
|
|
189
|
+
description: 'Lines of context around matches (for "content" mode)',
|
|
190
|
+
},
|
|
191
|
+
before: {
|
|
192
|
+
type: 'number',
|
|
193
|
+
description: 'Lines before each match',
|
|
194
|
+
},
|
|
195
|
+
after: {
|
|
196
|
+
type: 'number',
|
|
197
|
+
description: 'Lines after each match',
|
|
198
|
+
},
|
|
199
|
+
multiline: {
|
|
200
|
+
type: 'boolean',
|
|
201
|
+
description: 'Enable multiline matching',
|
|
202
|
+
},
|
|
203
|
+
head_limit: {
|
|
204
|
+
type: 'number',
|
|
205
|
+
description: 'Limit output to first N results (default: 250)',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ['pattern'],
|
|
209
|
+
},
|
|
210
|
+
modes: ['work'],
|
|
211
|
+
isConcurrencySafe: () => true,
|
|
212
|
+
isReadOnly: () => true,
|
|
213
|
+
async execute(input, ctx) {
|
|
214
|
+
const {
|
|
215
|
+
pattern, path: searchPath, output_mode = 'files_with_matches',
|
|
216
|
+
glob: globFilter, type, case_insensitive = false,
|
|
217
|
+
context, before, after, multiline = false,
|
|
218
|
+
head_limit = MAX_LINES,
|
|
219
|
+
} = input;
|
|
220
|
+
|
|
221
|
+
if (!pattern) return JSON.stringify({ error: 'pattern is required' });
|
|
222
|
+
|
|
223
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
224
|
+
const absPath = searchPath ? resolve(cwd, searchPath) : cwd;
|
|
225
|
+
|
|
226
|
+
if (!existsSync(absPath)) {
|
|
227
|
+
return JSON.stringify({ error: `Path not found: ${absPath}` });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const options = {
|
|
231
|
+
caseInsensitive: case_insensitive,
|
|
232
|
+
glob: globFilter,
|
|
233
|
+
type,
|
|
234
|
+
filesOnly: output_mode === 'files_with_matches',
|
|
235
|
+
count: output_mode === 'count',
|
|
236
|
+
context,
|
|
237
|
+
before,
|
|
238
|
+
after,
|
|
239
|
+
multiline,
|
|
240
|
+
maxResults: head_limit * 2,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
let result;
|
|
245
|
+
const rgAvailable = await hasRipgrep();
|
|
246
|
+
|
|
247
|
+
if (rgAvailable) {
|
|
248
|
+
result = await runRipgrep(pattern, absPath, options);
|
|
249
|
+
} else {
|
|
250
|
+
result = await nodeGrep(pattern, absPath, options);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!result || !result.trim()) {
|
|
254
|
+
return '(no matches)';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Limit output lines
|
|
258
|
+
const lines = result.trim().split('\n');
|
|
259
|
+
if (lines.length > head_limit) {
|
|
260
|
+
return lines.slice(0, head_limit).join('\n') + `\n\n... (${lines.length - head_limit} more results)`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result.trim();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
return JSON.stringify({ error: `Grep failed: ${err.message}` });
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* history-search.js — Search conversation history.
|
|
3
|
+
*
|
|
4
|
+
* Searches through persisted conversation messages for keywords.
|
|
5
|
+
* Uses the ConversationStore's search functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { searchMessages } from '../conversation/search.js';
|
|
10
|
+
|
|
11
|
+
export default defineTool({
|
|
12
|
+
name: 'HistorySearch',
|
|
13
|
+
description: `Search through past conversation history.
|
|
14
|
+
|
|
15
|
+
Searches for keywords in previously persisted conversation messages.
|
|
16
|
+
Useful for finding previous discussions, decisions, or code snippets.
|
|
17
|
+
|
|
18
|
+
Results are returned newest-first with message role and content.`,
|
|
19
|
+
parameters: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
keyword: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Search keyword (case-insensitive)',
|
|
25
|
+
},
|
|
26
|
+
limit: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
description: 'Maximum number of results (default: 20)',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['keyword'],
|
|
32
|
+
},
|
|
33
|
+
modes: ['chat', 'work'],
|
|
34
|
+
isConcurrencySafe: () => true,
|
|
35
|
+
isReadOnly: () => true,
|
|
36
|
+
async execute(input, ctx) {
|
|
37
|
+
const { keyword, limit = 20 } = input;
|
|
38
|
+
if (!keyword) return JSON.stringify({ error: 'keyword is required' });
|
|
39
|
+
|
|
40
|
+
const yeaftDir = ctx?.yeaftDir;
|
|
41
|
+
if (!yeaftDir) {
|
|
42
|
+
return JSON.stringify({ error: 'Yeaft directory not configured — no conversation history available' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const results = searchMessages(yeaftDir, keyword, limit);
|
|
47
|
+
|
|
48
|
+
if (results.length === 0) {
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
results: [],
|
|
51
|
+
message: `No matches found for "${keyword}"`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
results: results.map(msg => ({
|
|
57
|
+
role: msg.role,
|
|
58
|
+
content: msg.content?.slice(0, 2000) + (msg.content?.length > 2000 ? '...' : ''),
|
|
59
|
+
mode: msg.mode,
|
|
60
|
+
timestamp: msg.timestamp,
|
|
61
|
+
})),
|
|
62
|
+
totalResults: results.length,
|
|
63
|
+
keyword,
|
|
64
|
+
}, null, 2);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return JSON.stringify({ error: `History search failed: ${err.message}` });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image-generation.js — Generate images via external API.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to a configured image generation service (DALL-E, etc.).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineTool } from './types.js';
|
|
8
|
+
|
|
9
|
+
export default defineTool({
|
|
10
|
+
name: 'ImageGeneration',
|
|
11
|
+
description: `Generate an image from a text description.
|
|
12
|
+
|
|
13
|
+
Uses a configured image generation API to create images.
|
|
14
|
+
Requires an image generation API endpoint in config.
|
|
15
|
+
|
|
16
|
+
Guidelines:
|
|
17
|
+
- Provide detailed, specific descriptions for best results
|
|
18
|
+
- Specify style, composition, and mood
|
|
19
|
+
- Images are saved to the working directory`,
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
prompt: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Text description of the image to generate',
|
|
26
|
+
},
|
|
27
|
+
output_path: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'File path to save the generated image',
|
|
30
|
+
},
|
|
31
|
+
size: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
enum: ['256x256', '512x512', '1024x1024'],
|
|
34
|
+
description: 'Image size (default: "1024x1024")',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['prompt'],
|
|
38
|
+
},
|
|
39
|
+
modes: ['chat', 'work'],
|
|
40
|
+
isConcurrencySafe: () => true,
|
|
41
|
+
isReadOnly: () => false,
|
|
42
|
+
async execute(input, ctx) {
|
|
43
|
+
const { prompt, output_path, size = '1024x1024' } = input;
|
|
44
|
+
if (!prompt) return JSON.stringify({ error: 'prompt is required' });
|
|
45
|
+
|
|
46
|
+
const imageApiUrl = ctx?.config?.imageApiUrl;
|
|
47
|
+
if (!imageApiUrl) {
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
error: 'No image generation API configured.',
|
|
50
|
+
hint: 'Configure imageApiUrl in ~/.yeaft/config.json',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(imageApiUrl, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ prompt, size }),
|
|
59
|
+
signal: ctx?.signal,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
return JSON.stringify({ error: `Image API returned ${response.status}: ${response.statusText}` });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
// If output_path specified, save the image
|
|
69
|
+
if (output_path && data.url) {
|
|
70
|
+
const { resolve: resolvePath } = await import('path');
|
|
71
|
+
const { writeFile } = await import('fs/promises');
|
|
72
|
+
|
|
73
|
+
const imgResponse = await fetch(data.url);
|
|
74
|
+
const buffer = Buffer.from(await imgResponse.arrayBuffer());
|
|
75
|
+
const absPath = resolvePath(ctx?.cwd || process.cwd(), output_path);
|
|
76
|
+
await writeFile(absPath, buffer);
|
|
77
|
+
|
|
78
|
+
return JSON.stringify({
|
|
79
|
+
success: true,
|
|
80
|
+
path: absPath,
|
|
81
|
+
size,
|
|
82
|
+
prompt: prompt.slice(0, 100),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return JSON.stringify({
|
|
87
|
+
success: true,
|
|
88
|
+
url: data.url,
|
|
89
|
+
size,
|
|
90
|
+
prompt: prompt.slice(0, 100),
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err.name === 'AbortError') return JSON.stringify({ error: 'Generation cancelled' });
|
|
94
|
+
return JSON.stringify({ error: `Image generation failed: ${err.message}` });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
package/unify/tools/index.js
CHANGED
|
@@ -9,21 +9,113 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { ToolRegistry } from './registry.js';
|
|
12
|
+
|
|
13
|
+
// --- Existing tools ---
|
|
12
14
|
import mcpTools from './mcp-tools.js';
|
|
13
15
|
import skillTool from './skill.js';
|
|
14
16
|
import enterWorktree from './enter-worktree.js';
|
|
15
17
|
import exitWorktree from './exit-worktree.js';
|
|
16
18
|
|
|
19
|
+
// --- P0 Core tools ---
|
|
20
|
+
import askUser from './ask-user.js';
|
|
21
|
+
import memoryRead from './memory-read.js';
|
|
22
|
+
import memoryWrite from './memory-write.js';
|
|
23
|
+
import memorySearch from './memory-search.js';
|
|
24
|
+
import webSearch from './web-search.js';
|
|
25
|
+
import webFetch from './web-fetch.js';
|
|
26
|
+
import historySearch from './history-search.js';
|
|
27
|
+
|
|
28
|
+
// --- P0 File tools ---
|
|
29
|
+
import bash from './bash.js';
|
|
30
|
+
import fileRead from './file-read.js';
|
|
31
|
+
import fileWrite from './file-write.js';
|
|
32
|
+
import fileEdit from './file-edit.js';
|
|
33
|
+
import globTool from './glob.js';
|
|
34
|
+
import grepTool from './grep.js';
|
|
35
|
+
import listDir from './list-dir.js';
|
|
36
|
+
import applyPatch from './apply-patch.js';
|
|
37
|
+
|
|
38
|
+
// --- P1 Agent tools ---
|
|
39
|
+
import agentTool from './agent.js';
|
|
40
|
+
import sendMessage from './send-message.js';
|
|
41
|
+
import waitAgent from './wait-agent.js';
|
|
42
|
+
import closeAgent from './close-agent.js';
|
|
43
|
+
import listAgents from './list-agents.js';
|
|
44
|
+
|
|
45
|
+
// --- P1 Task tools ---
|
|
46
|
+
import {
|
|
47
|
+
taskCreate,
|
|
48
|
+
taskUpdate,
|
|
49
|
+
taskList,
|
|
50
|
+
taskGet,
|
|
51
|
+
followupTask,
|
|
52
|
+
updatePlan,
|
|
53
|
+
} from './task-tools.js';
|
|
54
|
+
|
|
55
|
+
// --- P2 Auxiliary tools ---
|
|
56
|
+
import { jsRepl, jsReplReset } from './js-repl.js';
|
|
57
|
+
import notebookEdit from './notebook-edit.js';
|
|
58
|
+
import imageGeneration from './image-generation.js';
|
|
59
|
+
import viewImage from './view-image.js';
|
|
60
|
+
import toolSearch from './tool-search.js';
|
|
61
|
+
import requestPermissions from './request-permissions.js';
|
|
62
|
+
import writeStdin from './write-stdin.js';
|
|
63
|
+
|
|
17
64
|
/**
|
|
18
65
|
* All built-in tools, flattened into a single array.
|
|
19
66
|
* mcpTools is already an array; the rest are single ToolDef objects.
|
|
20
67
|
* @type {import('./types.js').ToolDef[]}
|
|
21
68
|
*/
|
|
22
69
|
export const allTools = [
|
|
70
|
+
// Existing tools
|
|
23
71
|
...mcpTools,
|
|
24
72
|
skillTool,
|
|
25
73
|
enterWorktree,
|
|
26
74
|
exitWorktree,
|
|
75
|
+
|
|
76
|
+
// P0 Core
|
|
77
|
+
askUser,
|
|
78
|
+
memoryRead,
|
|
79
|
+
memoryWrite,
|
|
80
|
+
memorySearch,
|
|
81
|
+
webSearch,
|
|
82
|
+
webFetch,
|
|
83
|
+
historySearch,
|
|
84
|
+
|
|
85
|
+
// P0 File
|
|
86
|
+
bash,
|
|
87
|
+
fileRead,
|
|
88
|
+
fileWrite,
|
|
89
|
+
fileEdit,
|
|
90
|
+
globTool,
|
|
91
|
+
grepTool,
|
|
92
|
+
listDir,
|
|
93
|
+
applyPatch,
|
|
94
|
+
|
|
95
|
+
// P1 Agent
|
|
96
|
+
agentTool,
|
|
97
|
+
sendMessage,
|
|
98
|
+
waitAgent,
|
|
99
|
+
closeAgent,
|
|
100
|
+
listAgents,
|
|
101
|
+
|
|
102
|
+
// P1 Task
|
|
103
|
+
taskCreate,
|
|
104
|
+
taskUpdate,
|
|
105
|
+
taskList,
|
|
106
|
+
taskGet,
|
|
107
|
+
followupTask,
|
|
108
|
+
updatePlan,
|
|
109
|
+
|
|
110
|
+
// P2 Auxiliary
|
|
111
|
+
jsRepl,
|
|
112
|
+
jsReplReset,
|
|
113
|
+
notebookEdit,
|
|
114
|
+
imageGeneration,
|
|
115
|
+
viewImage,
|
|
116
|
+
toolSearch,
|
|
117
|
+
requestPermissions,
|
|
118
|
+
writeStdin,
|
|
27
119
|
];
|
|
28
120
|
|
|
29
121
|
/**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* js-repl.js — JavaScript REPL for evaluating expressions.
|
|
3
|
+
*
|
|
4
|
+
* Runs JavaScript code in a persistent VM context, allowing
|
|
5
|
+
* state to be maintained across calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { createContext, runInContext } from 'vm';
|
|
10
|
+
|
|
11
|
+
/** Persistent VM context per session. */
|
|
12
|
+
let vmContext = null;
|
|
13
|
+
|
|
14
|
+
function getContext() {
|
|
15
|
+
if (!vmContext) {
|
|
16
|
+
vmContext = createContext({
|
|
17
|
+
console: {
|
|
18
|
+
log: (...args) => { vmContext.__output.push(args.map(String).join(' ')); },
|
|
19
|
+
error: (...args) => { vmContext.__output.push('[error] ' + args.map(String).join(' ')); },
|
|
20
|
+
warn: (...args) => { vmContext.__output.push('[warn] ' + args.map(String).join(' ')); },
|
|
21
|
+
},
|
|
22
|
+
setTimeout,
|
|
23
|
+
setInterval,
|
|
24
|
+
clearTimeout,
|
|
25
|
+
clearInterval,
|
|
26
|
+
JSON,
|
|
27
|
+
Math,
|
|
28
|
+
Date,
|
|
29
|
+
RegExp,
|
|
30
|
+
Array,
|
|
31
|
+
Object,
|
|
32
|
+
String,
|
|
33
|
+
Number,
|
|
34
|
+
Boolean,
|
|
35
|
+
Map,
|
|
36
|
+
Set,
|
|
37
|
+
WeakMap,
|
|
38
|
+
WeakSet,
|
|
39
|
+
Promise,
|
|
40
|
+
Error,
|
|
41
|
+
Buffer,
|
|
42
|
+
__output: [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
vmContext.__output = [];
|
|
46
|
+
return vmContext;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const jsRepl = defineTool({
|
|
50
|
+
name: 'JsRepl',
|
|
51
|
+
description: `Evaluate JavaScript code in a persistent REPL environment.
|
|
52
|
+
|
|
53
|
+
The REPL context persists across calls — variables and functions
|
|
54
|
+
defined in one call are available in subsequent calls.
|
|
55
|
+
|
|
56
|
+
Guidelines:
|
|
57
|
+
- Use for calculations, data transformations, and quick experiments
|
|
58
|
+
- State is preserved between calls (use JsReplReset to clear)
|
|
59
|
+
- console.log output is captured and returned
|
|
60
|
+
- Returns the last expression's value plus any console output
|
|
61
|
+
- No filesystem or network access from within the REPL`,
|
|
62
|
+
parameters: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
code: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'JavaScript code to evaluate',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ['code'],
|
|
71
|
+
},
|
|
72
|
+
modes: ['chat', 'work'],
|
|
73
|
+
isConcurrencySafe: () => false,
|
|
74
|
+
isReadOnly: () => true,
|
|
75
|
+
async execute(input, ctx) {
|
|
76
|
+
const { code } = input;
|
|
77
|
+
if (!code) return JSON.stringify({ error: 'code is required' });
|
|
78
|
+
|
|
79
|
+
const vmCtx = getContext();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = runInContext(code, vmCtx, {
|
|
83
|
+
timeout: 10000, // 10 second timeout
|
|
84
|
+
displayErrors: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const output = vmCtx.__output.slice();
|
|
88
|
+
const resultStr = result === undefined ? '' : String(result);
|
|
89
|
+
|
|
90
|
+
const parts = [];
|
|
91
|
+
if (output.length > 0) parts.push(output.join('\n'));
|
|
92
|
+
if (resultStr) parts.push(`→ ${resultStr}`);
|
|
93
|
+
|
|
94
|
+
return parts.join('\n') || '(no output)';
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const output = vmCtx.__output.slice();
|
|
97
|
+
const parts = [];
|
|
98
|
+
if (output.length > 0) parts.push(output.join('\n'));
|
|
99
|
+
parts.push(`Error: ${err.message}`);
|
|
100
|
+
return parts.join('\n');
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const jsReplReset = defineTool({
|
|
106
|
+
name: 'JsReplReset',
|
|
107
|
+
description: `Reset the JavaScript REPL environment.
|
|
108
|
+
|
|
109
|
+
Clears all variables and state from previous evaluations.
|
|
110
|
+
Use when you want a clean slate.`,
|
|
111
|
+
parameters: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {},
|
|
114
|
+
},
|
|
115
|
+
modes: ['chat', 'work'],
|
|
116
|
+
isConcurrencySafe: () => false,
|
|
117
|
+
isReadOnly: () => false,
|
|
118
|
+
async execute(input, ctx) {
|
|
119
|
+
vmContext = null;
|
|
120
|
+
return JSON.stringify({ success: true, message: 'REPL context reset' });
|
|
121
|
+
},
|
|
122
|
+
});
|