@semalt-ai/code 1.6.0 → 1.8.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/lib/tools.js CHANGED
@@ -1,81 +1,837 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const os = require('os');
4
7
  const path = require('path');
5
8
  const { spawnSync } = require('child_process');
6
9
 
7
- function createToolExecutor(permissionManager, ui) {
8
- const { FG_DARK, FG_GRAY, FG_GREEN, FG_RED, RST } = ui;
10
+ const { logToolCall } = require('./audit');
11
+
12
+ const MEMORY_PATH = path.join(os.homedir(), '.semalt-ai', 'memory.json');
13
+
14
+ const _dryRun = process.argv.includes('--dry-run');
15
+ const _skippedOps = [];
16
+ function getSkippedOps() { return _skippedOps.slice(); }
17
+
18
+ // When the full TUI is active, suppress direct console.log writes — the UI
19
+ // handles all tool-status display via onToolEnd callbacks instead.
20
+ let _uiActive = false;
21
+ function setUIActive(v) { _uiActive = v; }
22
+ function _log(...args) { if (!_uiActive) console.log(...args); }
23
+
24
+ // Chunk store for large http_get responses.
25
+ // Maps URL → { chunks: string[], total: number, delivered: number }
26
+ const _httpChunkStore = new Map();
27
+ const HTTP_CHUNK_CHARS = 10000;
28
+
29
+ function createToolExecutor(permissionManager, ui, getConfig) {
30
+ const { BOLD, DIM, FG_DARK, FG_GRAY, FG_GREEN, FG_RED, FG_YELLOW, RST, renderDiff } = ui;
31
+
32
+ function _parseNumberedOptions(text) {
33
+ const options = [];
34
+ for (const line of text.split('\n')) {
35
+ const m = line.match(/^\s*\d+[.)]\s+(.+)$/);
36
+ if (m) options.push(m[1].trim());
37
+ }
38
+ return options.length >= 2 ? options : [];
39
+ }
9
40
 
10
41
  async function agentExecShell(command) {
11
- const approved = await permissionManager.askPermission('shell', command);
42
+ if (_dryRun) {
43
+ _log(` ${FG_DARK}[dry-run] $ ${command}${RST}`);
44
+ _skippedOps.push({ category: 'cmd', symbol: '▶', desc: command });
45
+ logToolCall('exec', { command }, false, 'dry-run');
46
+ return { exit_code: 0, stdout: '', stderr: 'dry-run: command skipped' };
47
+ }
48
+
49
+ const approved = await permissionManager.askPermission('shell', command, 'exec');
12
50
  if (!approved) {
51
+ logToolCall('exec', { command }, false, 'denied');
13
52
  return { exit_code: -1, stdout: '', stderr: 'Permission denied by user' };
14
53
  }
15
54
 
16
- console.log(` ${FG_DARK}$ ${command}${RST}`);
17
55
  try {
18
- const result = spawnSync(command, { shell: true, encoding: 'utf8', timeout: 60000 });
56
+ const cfg = getConfig ? getConfig() : {};
57
+ const timeout = cfg.command_timeout_ms || 30000;
58
+ const result = spawnSync(command, { shell: true, encoding: 'utf8', timeout });
19
59
  const stdout = result.stdout || '';
20
60
  const stderr = result.stderr || '';
21
- const combined = stdout + (stderr ? `\n${stderr}` : '');
22
- const lines = combined.trim().split('\n').filter((line) => line !== '');
23
-
24
- if (lines.length > 20) {
25
- lines.slice(0, 15).forEach((line) => console.log(` ${FG_GRAY}${line}${RST}`));
26
- console.log(` ${FG_DARK}... (${lines.length - 15} more lines)${RST}`);
27
- } else {
28
- lines.forEach((line) => console.log(` ${FG_GRAY}${line}${RST}`));
29
- }
30
- console.log();
31
-
32
- return { exit_code: result.status ?? 0, stdout, stderr };
61
+ const exitCode = result.status ?? 0;
62
+ logToolCall('exec', { command }, true, exitCode === 0 ? 'ok' : 'error');
63
+ return { exit_code: exitCode, stdout, stderr };
33
64
  } catch (error) {
34
- console.log(` ${FG_RED}✗ ${error.message}${RST}`);
65
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
66
+ logToolCall('exec', { command }, true, 'error');
35
67
  return { exit_code: -1, stdout: '', stderr: error.message };
36
68
  }
37
69
  }
38
70
 
39
- async function agentExecFile(action, filePath, content = null) {
40
- if (action === 'read') {
41
- const approved = await permissionManager.askPermission('file', `Read ${filePath}`);
42
- if (!approved) return { error: 'Permission denied' };
71
+ async function agentExecFile(action, ...args) {
72
+ const [arg0 = null, arg1 = null, arg2 = null, arg3 = null] = args;
43
73
 
74
+ if (action === 'read') {
75
+ const filePath = arg0;
76
+ try {
77
+ const stat = fs.statSync(filePath);
78
+ const cfg = getConfig ? getConfig() : {};
79
+ const maxBytes = (cfg.max_file_size_kb || 512) * 1024;
80
+ if (stat.size > maxBytes) {
81
+ const kb = (stat.size / 1024).toFixed(0);
82
+ logToolCall('read_file', { path: filePath }, false, 'error');
83
+ return { error: `File too large: ${kb} KB exceeds max_file_size_kb=${cfg.max_file_size_kb || 512}` };
84
+ }
85
+ } catch {
86
+ // file doesn't exist yet — readFileSync will report it
87
+ }
88
+ const approved = await permissionManager.askPermission('file', `Read ${filePath}`, 'read_file');
89
+ if (!approved) {
90
+ logToolCall('read_file', { path: filePath }, false, 'denied');
91
+ return { error: 'Permission denied' };
92
+ }
44
93
  try {
45
94
  const data = fs.readFileSync(filePath, 'utf8');
46
95
  const lines = data.split('\n').length;
47
96
  if (lines > 10) {
48
- console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath} (${lines} lines, ${data.length} chars)${RST}`);
97
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath} (${lines} lines, ${data.length} chars)${RST}`);
49
98
  } else {
50
- console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath}${RST}`);
99
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath}${RST}`);
51
100
  }
101
+ logToolCall('read_file', { path: filePath }, true, 'ok');
52
102
  return { content: data, path: filePath };
53
103
  } catch (error) {
54
- console.log(` ${FG_RED}✗ ${error.message}${RST}`);
104
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
105
+ logToolCall('read_file', { path: filePath }, true, 'error');
55
106
  return { error: error.message };
56
107
  }
57
108
  }
58
109
 
59
110
  if (action === 'write' || action === 'append') {
111
+ const filePath = arg0;
112
+ const content = arg1;
113
+ const tag = action === 'write' ? 'write_file' : 'append_file';
114
+
115
+ const blocked = permissionManager.readonlyBlock(tag);
116
+ if (blocked) {
117
+ logToolCall(tag, { path: filePath, content }, false, 'denied');
118
+ return blocked;
119
+ }
120
+
121
+ // Read existing content for diff display
122
+ let existing = '';
123
+ try { existing = fs.readFileSync(filePath, 'utf8'); } catch {}
124
+
125
+ // For append the final state is existing + new content
126
+ const finalContent = action === 'write' ? (content || '') : (existing + (content || ''));
127
+
128
+ const diffOutput = renderDiff(existing, finalContent, filePath);
129
+ process.stdout.write(diffOutput + '\n');
130
+
131
+ // Dry-run: record the skipped op and return without writing
132
+ if (_dryRun) {
133
+ const verb = action === 'write' ? 'write' : 'append';
134
+ _skippedOps.push({ category: 'file', symbol: '✎', desc: `${verb} ${filePath}` });
135
+ logToolCall(tag, { path: filePath }, false, 'dry-run');
136
+ return { status: 'dry-run', message: 'dry-run: write skipped', path: filePath };
137
+ }
138
+
139
+ // Permission check — routes through TUI dialog in chat mode, interactiveSelect in legacy CLI mode
60
140
  let desc = `${action === 'write' ? 'Write' : 'Append to'} ${filePath}`;
61
141
  if (content) desc += ` (${content.length} chars)`;
62
- const approved = await permissionManager.askPermission('file', desc);
63
- if (!approved) return { error: 'Permission denied' };
64
-
142
+ const approved = await permissionManager.askPermission('file', desc, tag);
143
+ if (!approved) {
144
+ logToolCall(tag, { path: filePath, content }, false, 'denied');
145
+ return { error: 'Permission denied' };
146
+ }
65
147
  try {
66
148
  const dir = path.dirname(filePath);
67
149
  if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });
68
150
  if (action === 'write') fs.writeFileSync(filePath, content || '');
69
151
  else fs.appendFileSync(filePath, content || '');
70
152
  const verb = action === 'write' ? 'Wrote' : 'Appended to';
71
- console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}${verb} ${filePath}${RST}`);
153
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}${verb} ${filePath}${RST}`);
154
+ logToolCall(tag, { path: filePath, content }, true, 'ok');
72
155
  return { status: 'ok', path: filePath, bytes: (content || '').length };
73
156
  } catch (error) {
74
- console.log(` ${FG_RED}✗ ${error.message}${RST}`);
157
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
158
+ logToolCall(tag, { path: filePath, content }, true, 'error');
159
+ return { error: error.message };
160
+ }
161
+ }
162
+
163
+ if (action === 'list_dir') {
164
+ const dirPath = arg0;
165
+ const approved = await permissionManager.askPermission('file', `List ${dirPath}`, 'list_dir');
166
+ if (!approved) {
167
+ logToolCall('list_dir', { path: dirPath }, false, 'denied');
168
+ return { error: 'Permission denied' };
169
+ }
170
+ try {
171
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
172
+ const items = entries.map((e) => {
173
+ if (e.isSymbolicLink()) return `[L] ${e.name}`;
174
+ if (e.isDirectory()) return `[D] ${e.name}`;
175
+ return `[F] ${e.name}`;
176
+ });
177
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Listed ${dirPath} (${items.length} items)${RST}`);
178
+ logToolCall('list_dir', { path: dirPath }, true, 'ok');
179
+ return { items, path: dirPath };
180
+ } catch (error) {
181
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
182
+ logToolCall('list_dir', { path: dirPath }, true, 'error');
183
+ return { error: error.message };
184
+ }
185
+ }
186
+
187
+ if (action === 'delete_file') {
188
+ const filePath = arg0;
189
+
190
+ const blocked = permissionManager.readonlyBlock('delete_file');
191
+ if (blocked) {
192
+ logToolCall('delete_file', { path: filePath }, false, 'denied');
193
+ return blocked;
194
+ }
195
+
196
+ _log(` ${FG_YELLOW}${BOLD}⚠ Deleting: ${filePath}${RST}`);
197
+
198
+ const approved = await permissionManager.askPermission('file', `Delete ${filePath}`, 'delete_file');
199
+ if (!approved) {
200
+ logToolCall('delete_file', { path: filePath }, false, 'denied');
201
+ return { error: 'Permission denied' };
202
+ }
203
+ try {
204
+ fs.unlinkSync(filePath);
205
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Deleted ${filePath}${RST}`);
206
+ logToolCall('delete_file', { path: filePath }, true, 'ok');
207
+ return { status: 'ok', path: filePath };
208
+ } catch (error) {
209
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
210
+ logToolCall('delete_file', { path: filePath }, true, 'error');
211
+ return { error: error.message };
212
+ }
213
+ }
214
+
215
+ if (action === 'make_dir') {
216
+ const dirPath = arg0;
217
+ const approved = await permissionManager.askPermission('file', `Create directory ${dirPath}`, 'make_dir');
218
+ if (!approved) {
219
+ logToolCall('make_dir', { path: dirPath }, false, 'denied');
220
+ return { error: 'Permission denied' };
221
+ }
222
+ try {
223
+ fs.mkdirSync(dirPath, { recursive: true });
224
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Created directory ${dirPath}${RST}`);
225
+ logToolCall('make_dir', { path: dirPath }, true, 'ok');
226
+ return { status: 'ok', path: dirPath };
227
+ } catch (error) {
228
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
229
+ logToolCall('make_dir', { path: dirPath }, true, 'error');
230
+ return { error: error.message };
231
+ }
232
+ }
233
+
234
+ if (action === 'remove_dir') {
235
+ const dirPath = arg0;
236
+ const approved = await permissionManager.askPermission('file', `Remove directory ${dirPath}`, 'remove_dir');
237
+ if (!approved) {
238
+ logToolCall('remove_dir', { path: dirPath }, false, 'denied');
239
+ return { error: 'Permission denied' };
240
+ }
241
+ try {
242
+ fs.rmSync(dirPath, { recursive: true, force: true });
243
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Removed directory ${dirPath}${RST}`);
244
+ logToolCall('remove_dir', { path: dirPath }, true, 'ok');
245
+ return { status: 'ok', path: dirPath };
246
+ } catch (error) {
247
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
248
+ logToolCall('remove_dir', { path: dirPath }, true, 'error');
249
+ return { error: error.message };
250
+ }
251
+ }
252
+
253
+ if (action === 'move_file') {
254
+ const src = arg0;
255
+ const dst = arg1;
256
+
257
+ const blocked = permissionManager.readonlyBlock('move_file');
258
+ if (blocked) {
259
+ logToolCall('move_file', { src, dst }, false, 'denied');
260
+ return blocked;
261
+ }
262
+
263
+ _log(` ${FG_YELLOW}${BOLD}⚠ Moving: ${src} → ${dst}${RST}`);
264
+
265
+ const approved = await permissionManager.askPermission('file', `Move ${src} to ${dst}`, 'move_file');
266
+ if (!approved) {
267
+ logToolCall('move_file', { src, dst }, false, 'denied');
268
+ return { error: 'Permission denied' };
269
+ }
270
+ try {
271
+ const dstDir = path.dirname(dst);
272
+ if (dstDir && dstDir !== '.') fs.mkdirSync(dstDir, { recursive: true });
273
+ const cfg = getConfig ? getConfig() : {};
274
+ const timeout = cfg.command_timeout_ms || 30000;
275
+ const mvResult = spawnSync('mv', [src, dst], { encoding: 'utf8', timeout });
276
+ if (mvResult.error && mvResult.error.code === 'ENOENT') throw new Error('mv not available');
277
+ if (mvResult.error) throw mvResult.error;
278
+ if (mvResult.status !== 0) throw new Error((mvResult.stderr || 'mv failed').trim());
279
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Moved ${src} → ${dst}${RST}`);
280
+ logToolCall('move_file', { src, dst }, true, 'ok');
281
+ return { status: 'ok', src, dst };
282
+ } catch (mvErr) {
283
+ // Fallback: JS rename (works only within same filesystem)
284
+ try {
285
+ fs.renameSync(src, dst);
286
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Moved ${src} → ${dst}${RST}`);
287
+ logToolCall('move_file', { src, dst }, true, 'ok');
288
+ return { status: 'ok', src, dst };
289
+ } catch (error) {
290
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
291
+ logToolCall('move_file', { src, dst }, true, 'error');
292
+ return { error: error.message };
293
+ }
294
+ }
295
+ }
296
+
297
+ if (action === 'copy_file') {
298
+ const src = arg0;
299
+ const dst = arg1;
300
+
301
+ const blocked = permissionManager.readonlyBlock('copy_file');
302
+ if (blocked) {
303
+ logToolCall('copy_file', { src, dst }, false, 'denied');
304
+ return blocked;
305
+ }
306
+
307
+ const approved = await permissionManager.askPermission('file', `Copy ${src} to ${dst}`, 'copy_file');
308
+ if (!approved) {
309
+ logToolCall('copy_file', { src, dst }, false, 'denied');
310
+ return { error: 'Permission denied' };
311
+ }
312
+ try {
313
+ const dstDir = path.dirname(dst);
314
+ if (dstDir && dstDir !== '.') fs.mkdirSync(dstDir, { recursive: true });
315
+ fs.cpSync(src, dst, { recursive: true });
316
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Copied ${src} → ${dst}${RST}`);
317
+ logToolCall('copy_file', { src, dst }, true, 'ok');
318
+ return { status: 'ok', src, dst };
319
+ } catch (error) {
320
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
321
+ logToolCall('copy_file', { src, dst }, true, 'error');
322
+ return { error: error.message };
323
+ }
324
+ }
325
+
326
+ if (action === 'edit_file') {
327
+ const filePath = arg0;
328
+ const lineNum = arg1;
329
+ const newContent = arg2;
330
+ const approved = await permissionManager.askPermission('file', `Edit line ${lineNum} in ${filePath}`, 'edit_file');
331
+ if (!approved) {
332
+ logToolCall('edit_file', { path: filePath, line: lineNum }, false, 'denied');
333
+ return { error: 'Permission denied' };
334
+ }
335
+ try {
336
+ const data = fs.readFileSync(filePath, 'utf8');
337
+ const lines = data.split('\n');
338
+ if (lineNum < 1 || lineNum > lines.length) {
339
+ logToolCall('edit_file', { path: filePath, line: lineNum }, true, 'error');
340
+ return { error: `Line ${lineNum} out of range (file has ${lines.length} lines)` };
341
+ }
342
+ lines[lineNum - 1] = newContent;
343
+ fs.writeFileSync(filePath, lines.join('\n'));
344
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Edited line ${lineNum} in ${filePath}${RST}`);
345
+ logToolCall('edit_file', { path: filePath, line: lineNum }, true, 'ok');
346
+ return { status: 'ok', path: filePath, line: lineNum };
347
+ } catch (error) {
348
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
349
+ logToolCall('edit_file', { path: filePath, line: lineNum }, true, 'error');
350
+ return { error: error.message };
351
+ }
352
+ }
353
+
354
+ if (action === 'search_in_file') {
355
+ const filePath = arg0;
356
+ const pattern = arg1;
357
+ const approved = await permissionManager.askPermission('file', `Search in ${filePath}`, 'search_in_file');
358
+ if (!approved) {
359
+ logToolCall('search_in_file', { path: filePath, pattern }, false, 'denied');
360
+ return { error: 'Permission denied' };
361
+ }
362
+ try {
363
+ const cfg = getConfig ? getConfig() : {};
364
+ const timeout = cfg.command_timeout_ms || 30000;
365
+ const gr = spawnSync('grep', ['-n', '-E', '--', pattern, filePath], { encoding: 'utf8', timeout });
366
+ if (gr.error && gr.error.code === 'ENOENT') throw new Error('grep not available');
367
+ if (gr.error) throw gr.error;
368
+ // grep exit 1 = no matches (not an error), 2 = real error
369
+ if (gr.status === 2) throw new Error((gr.stderr || '').trim() || 'grep error');
370
+ const matches = (gr.stdout || '').split('\n').filter(Boolean).map(line => {
371
+ const colon = line.indexOf(':');
372
+ return colon === -1 ? null : { line: parseInt(line.slice(0, colon), 10), content: line.slice(colon + 1) };
373
+ }).filter(Boolean);
374
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Found ${matches.length} match(es) in ${filePath}${RST}`);
375
+ logToolCall('search_in_file', { path: filePath, pattern }, true, 'ok');
376
+ return { matches, path: filePath };
377
+ } catch (grepErr) {
378
+ // Fallback: JS regex search
379
+ try {
380
+ const data = fs.readFileSync(filePath, 'utf8');
381
+ const regex = new RegExp(pattern);
382
+ const matches = data.split('\n')
383
+ .map((content, idx) => regex.test(content) ? { line: idx + 1, content } : null)
384
+ .filter(Boolean);
385
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Found ${matches.length} match(es) in ${filePath}${RST}`);
386
+ logToolCall('search_in_file', { path: filePath, pattern }, true, 'ok');
387
+ return { matches, path: filePath };
388
+ } catch (error) {
389
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
390
+ logToolCall('search_in_file', { path: filePath, pattern }, true, 'error');
391
+ return { error: error.message };
392
+ }
393
+ }
394
+ }
395
+
396
+ if (action === 'replace_in_file') {
397
+ const filePath = arg0;
398
+ const searchStr = arg1;
399
+ const replaceStr = arg2;
400
+ const flags = arg3 || '';
401
+ const approved = await permissionManager.askPermission('file', `Replace in ${filePath}`, 'replace_in_file');
402
+ if (!approved) {
403
+ logToolCall('replace_in_file', { path: filePath, search: searchStr }, false, 'denied');
404
+ return { error: 'Permission denied' };
405
+ }
406
+ try {
407
+ const data = fs.readFileSync(filePath, 'utf8');
408
+ const safeFlags = flags.replace(/[^gimsuy]/g, '');
409
+ const countFlags = safeFlags.includes('g') ? safeFlags : safeFlags + 'g';
410
+ const count = (data.match(new RegExp(searchStr, countFlags)) || []).length;
411
+ const regex = new RegExp(searchStr, safeFlags || undefined);
412
+ const newData = data.replace(regex, replaceStr);
413
+ fs.writeFileSync(filePath, newData);
414
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Replaced ${count} occurrence(s) in ${filePath}${RST}`);
415
+ logToolCall('replace_in_file', { path: filePath, search: searchStr }, true, 'ok');
416
+ return { status: 'ok', path: filePath, count };
417
+ } catch (error) {
418
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
419
+ logToolCall('replace_in_file', { path: filePath, search: searchStr }, true, 'error');
420
+ return { error: error.message };
421
+ }
422
+ }
423
+
424
+ if (action === 'search_files') {
425
+ const pattern = arg0;
426
+ const searchDir = arg1 || '.';
427
+ const approved = await permissionManager.askPermission('file', `Search files: ${pattern} in ${searchDir}`, 'search_files');
428
+ if (!approved) {
429
+ logToolCall('search_files', { pattern, dir: searchDir }, false, 'denied');
430
+ return { error: 'Permission denied' };
431
+ }
432
+ try {
433
+ const cfg = getConfig ? getConfig() : {};
434
+ const timeout = cfg.command_timeout_ms || 30000;
435
+ // Split glob: "src/**/*.js" → root=searchDir/src, nameGlob="*.js"
436
+ const parts = pattern.split('/');
437
+ const nameGlob = parts[parts.length - 1];
438
+ const subDirs = parts.slice(0, -1).filter(p => p !== '**' && p !== '');
439
+ const searchRoot = subDirs.length > 0 ? path.join(searchDir, ...subDirs) : searchDir;
440
+ const fr = spawnSync('find', [searchRoot, '-type', 'f', '-name', nameGlob], { encoding: 'utf8', timeout });
441
+ if (fr.error && fr.error.code === 'ENOENT') throw new Error('find not available');
442
+ if (fr.error) throw fr.error;
443
+ const files = (fr.stdout || '').split('\n').filter(Boolean)
444
+ .map(f => path.relative(searchDir, f))
445
+ .filter(Boolean)
446
+ .sort();
447
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Found ${files.length} file(s) matching "${pattern}"${RST}`);
448
+ logToolCall('search_files', { pattern, dir: searchDir }, true, 'ok');
449
+ return { files, pattern, dir: searchDir };
450
+ } catch (_findErr) {
451
+ // Fallback: JS glob walker
452
+ try {
453
+ let regStr = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
454
+ regStr = regStr.replace(/\*\*/g, '\x00');
455
+ regStr = regStr.replace(/\*/g, '[^/]*');
456
+ regStr = regStr.replace(/\x00\//g, '(?:.*/)?');
457
+ regStr = regStr.replace(/\x00/g, '.*');
458
+ const regex = new RegExp(`^${regStr}$`);
459
+ const matchName = !pattern.includes('/');
460
+ const files = [];
461
+ function walk(dir, rel) {
462
+ let entries;
463
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
464
+ for (const entry of entries) {
465
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
466
+ if (regex.test(matchName ? entry.name : relPath)) files.push(relPath);
467
+ if (entry.isDirectory()) walk(path.join(dir, entry.name), relPath);
468
+ }
469
+ }
470
+ walk(searchDir, '');
471
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Found ${files.length} file(s) matching "${pattern}"${RST}`);
472
+ logToolCall('search_files', { pattern, dir: searchDir }, true, 'ok');
473
+ return { files, pattern, dir: searchDir };
474
+ } catch (error) {
475
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
476
+ logToolCall('search_files', { pattern, dir: searchDir }, true, 'error');
477
+ return { error: error.message };
478
+ }
479
+ }
480
+ }
481
+
482
+ if (action === 'file_stat') {
483
+ const filePath = arg0;
484
+ const approved = await permissionManager.askPermission('file', `Stat ${filePath}`, 'file_stat');
485
+ if (!approved) {
486
+ logToolCall('file_stat', { path: filePath }, false, 'denied');
487
+ return { error: 'Permission denied' };
488
+ }
489
+ try {
490
+ const stat = fs.statSync(filePath);
491
+ const type = stat.isDirectory() ? 'directory' : stat.isSymbolicLink() ? 'symlink' : 'file';
492
+ const size_kb = (stat.size / 1024).toFixed(2);
493
+ const mode = '0o' + stat.mode.toString(8);
494
+ const mtime = stat.mtime.toISOString();
495
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Stat ${filePath}${RST}`);
496
+ logToolCall('file_stat', { path: filePath }, true, 'ok');
497
+ return { path: filePath, size_kb, mtime, type, mode };
498
+ } catch (error) {
499
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
500
+ logToolCall('file_stat', { path: filePath }, true, 'error');
501
+ return { error: error.message };
502
+ }
503
+ }
504
+
505
+ if (action === 'get_env') {
506
+ const varName = arg0;
507
+ const value = process.env[varName];
508
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Got env ${varName}${RST}`);
509
+ logToolCall('get_env', { name: varName }, true, 'ok');
510
+ return { name: varName, value: value !== undefined ? value : null };
511
+ }
512
+
513
+ if (action === 'set_env') {
514
+ const varName = arg0;
515
+ const value = arg1 || '';
516
+ const approved = await permissionManager.askPermission('shell', `Set env ${varName}=${value}`, 'set_env');
517
+ if (!approved) {
518
+ logToolCall('set_env', { name: varName }, false, 'denied');
519
+ return { error: 'Permission denied' };
520
+ }
521
+ process.env[varName] = value;
522
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Set env ${varName}${RST}`);
523
+ logToolCall('set_env', { name: varName }, true, 'ok');
524
+ return { status: 'ok', name: varName };
525
+ }
526
+
527
+ if (action === 'download') {
528
+ const url = arg0;
529
+ if (_dryRun) {
530
+ _skippedOps.push({ category: 'net', symbol: '↓', desc: `download ${url}` });
531
+ logToolCall('download', { url }, false, 'dry-run');
532
+ return { status: 'dry-run', message: 'dry-run: network call skipped' };
533
+ }
534
+ let fileName;
535
+ try {
536
+ fileName = path.basename(new URL(url).pathname) || 'download';
537
+ } catch {
538
+ fileName = 'download';
539
+ }
540
+ const outPath = path.join(process.cwd(), fileName);
541
+ const approved = await permissionManager.askPermission('shell', `Download ${url}`, 'download');
542
+ if (!approved) {
543
+ logToolCall('download', { url }, false, 'denied');
544
+ return { error: 'Permission denied' };
545
+ }
546
+ const dlResult = spawnSync('curl', ['-sLo', outPath, url], { encoding: 'utf8', timeout: 120000 });
547
+ if (!dlResult.error || dlResult.error.code !== 'ENOENT') {
548
+ if (dlResult.error || dlResult.status !== 0) {
549
+ try { fs.unlinkSync(outPath); } catch {}
550
+ const msg = dlResult.error ? dlResult.error.message : (dlResult.stderr || 'curl failed').trim();
551
+ _log(` ${FG_RED}✗ ${msg}${RST}`);
552
+ logToolCall('download', { url }, true, 'error');
553
+ return { error: msg };
554
+ }
555
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Downloaded to ${outPath}${RST}`);
556
+ logToolCall('download', { url }, true, 'ok');
557
+ return { status: 'ok', path: outPath };
558
+ }
559
+ // Fallback: Node.js http/https
560
+ return new Promise((resolve) => {
561
+ const proto = url.startsWith('https') ? https : http;
562
+ const file = fs.createWriteStream(outPath);
563
+ proto.get(url, (res) => {
564
+ res.pipe(file);
565
+ file.on('finish', () => {
566
+ file.close();
567
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Downloaded to ${outPath}${RST}`);
568
+ logToolCall('download', { url }, true, 'ok');
569
+ resolve({ status: 'ok', path: outPath });
570
+ });
571
+ }).on('error', (err) => {
572
+ fs.unlink(outPath, () => {});
573
+ _log(` ${FG_RED}✗ ${err.message}${RST}`);
574
+ logToolCall('download', { url }, true, 'error');
575
+ resolve({ error: err.message });
576
+ });
577
+ });
578
+ }
579
+
580
+ if (action === 'upload') {
581
+ const filePath = arg0;
582
+ const encodedContent = arg1 || '';
583
+ const approved = await permissionManager.askPermission('file', `Upload to ${filePath}`, 'upload');
584
+ if (!approved) {
585
+ logToolCall('upload', { path: filePath }, false, 'denied');
586
+ return { error: 'Permission denied' };
587
+ }
588
+ try {
589
+ const dir = path.dirname(filePath);
590
+ if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });
591
+ const buffer = Buffer.from(encodedContent.trim(), 'base64');
592
+ fs.writeFileSync(filePath, buffer);
593
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Uploaded ${buffer.length} bytes to ${filePath}${RST}`);
594
+ logToolCall('upload', { path: filePath }, true, 'ok');
595
+ return { status: 'ok', path: filePath, bytes: buffer.length };
596
+ } catch (error) {
597
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
598
+ logToolCall('upload', { path: filePath }, true, 'error');
599
+ return { error: error.message };
600
+ }
601
+ }
602
+
603
+ function buildHttpResult(url, statusCode, body, raw) {
604
+ // Strip HTML markup so the LLM receives readable text instead of raw HTML.
605
+ // Reduces a typical 150k-char page to ~25-40k chars, cutting chunk count
606
+ // from ~16 to ~3 and preventing context-overflow re-fetch loops.
607
+ // Pass raw=true to skip stripping (e.g. when the agent needs to parse markup).
608
+ const looksLikeHtml = !raw && (/^\s*<!doctype\s+html/i.test(body) || /^\s*<html[\s>]/i.test(body));
609
+ const processedBody = looksLikeHtml
610
+ ? body
611
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
612
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
613
+ .replace(/<[^>]+>/g, ' ')
614
+ .replace(/&nbsp;/gi, ' ')
615
+ .replace(/&amp;/gi, '&')
616
+ .replace(/&lt;/gi, '<')
617
+ .replace(/&gt;/gi, '>')
618
+ .replace(/&quot;/gi, '"')
619
+ .replace(/&#x?[\da-f]+;/gi, ' ')
620
+ .replace(/\s{2,}/g, ' ')
621
+ .trim()
622
+ : body;
623
+
624
+ if (processedBody.length > HTTP_CHUNK_CHARS) {
625
+ const chunks = [];
626
+ for (let i = 0; i < processedBody.length; i += HTTP_CHUNK_CHARS) {
627
+ chunks.push(processedBody.slice(i, i + HTTP_CHUNK_CHARS));
628
+ }
629
+ _httpChunkStore.set(url, { chunks: chunks.slice(1), total: chunks.length, delivered: 1 });
630
+ return { status_code: statusCode, body: chunks[0], chunked: true, part: 1, total_parts: chunks.length, key: url };
631
+ }
632
+ _httpChunkStore.delete(url);
633
+ return { status_code: statusCode, body: processedBody };
634
+ }
635
+
636
+ if (action === 'http_get_next') {
637
+ const key = arg0;
638
+ const store = _httpChunkStore.get(key);
639
+ if (!store || store.chunks.length === 0) {
640
+ _httpChunkStore.delete(key);
641
+ return { key, body: '', part: null, total_parts: null, done: true };
642
+ }
643
+ const nextChunk = store.chunks[0];
644
+ store.chunks = store.chunks.slice(1);
645
+ store.delivered += 1;
646
+ const done = store.chunks.length === 0;
647
+ if (done) _httpChunkStore.delete(key);
648
+ return { key, body: nextChunk, part: store.delivered, total_parts: store.total, done };
649
+ }
650
+
651
+ if (action === 'http_get') {
652
+ const url = arg0;
653
+ const rawHtml = arg1 === 'true';
654
+ if (_dryRun) {
655
+ _skippedOps.push({ category: 'net', symbol: '↓', desc: `GET ${url}` });
656
+ logToolCall('http_get', { url }, false, 'dry-run');
657
+ return { status: 'dry-run', message: 'dry-run: network call skipped' };
658
+ }
659
+ const approved = await permissionManager.askPermission('shell', `HTTP GET ${url}`, 'http_get');
660
+ if (!approved) {
661
+ logToolCall('http_get', { url }, false, 'denied');
662
+ return { error: 'Permission denied' };
663
+ }
664
+ const httpCfg = getConfig ? getConfig() : {};
665
+ const curlTimeout = Math.max(15, Math.floor((httpCfg.request_timeout_ms || 15000) / 1000));
666
+ // Try curl first: -sL follows redirects; -w appends status code on its own line
667
+ const curlResult = spawnSync(
668
+ 'curl', ['-sL', '--max-time', String(curlTimeout), '-w', '\n%{http_code}', url],
669
+ { encoding: 'utf8', timeout: (curlTimeout + 5) * 1000 }
670
+ );
671
+ if (!curlResult.error || curlResult.error.code !== 'ENOENT') {
672
+ if (curlResult.error) {
673
+ _log(` ${FG_RED}✗ ${curlResult.error.message}${RST}`);
674
+ logToolCall('http_get', { url }, true, 'error');
675
+ return { error: curlResult.error.message };
676
+ }
677
+ const stdout = curlResult.stdout || '';
678
+ const lastNl = stdout.lastIndexOf('\n');
679
+ const body = lastNl >= 0 ? stdout.slice(0, lastNl) : stdout;
680
+ const statusCode = parseInt((lastNl >= 0 ? stdout.slice(lastNl + 1) : '').trim(), 10) || 0;
681
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}HTTP GET ${url} (${statusCode}, ${body.length} chars)${RST}`);
682
+ logToolCall('http_get', { url }, true, statusCode < 400 ? 'ok' : 'error');
683
+ return buildHttpResult(url, statusCode, body, rawHtml);
684
+ }
685
+ // Fallback: Node.js http/https
686
+ return new Promise((resolve) => {
687
+ function doGet(target, redirectsLeft) {
688
+ const proto = target.startsWith('https') ? https : http;
689
+ const req = proto.get(target, (res) => {
690
+ if ([301, 302, 303, 307, 308].includes(res.statusCode) && redirectsLeft > 0 && res.headers.location) {
691
+ return doGet(res.headers.location, redirectsLeft - 1);
692
+ }
693
+ let data = '';
694
+ res.setEncoding('utf8');
695
+ res.on('data', (chunk) => { data += chunk; });
696
+ res.on('end', () => {
697
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}HTTP GET ${target} (${res.statusCode}, ${data.length} chars)${RST}`);
698
+ logToolCall('http_get', { url: target }, true, res.statusCode < 400 ? 'ok' : 'error');
699
+ resolve(buildHttpResult(url, res.statusCode, data, rawHtml));
700
+ });
701
+ });
702
+ req.on('error', (err) => {
703
+ _log(` ${FG_RED}✗ ${err.message}${RST}`);
704
+ logToolCall('http_get', { url: target }, true, 'error');
705
+ resolve({ error: err.message });
706
+ });
707
+ req.setTimeout(15000, () => { req.destroy(); logToolCall('http_get', { url: target }, true, 'error'); resolve({ error: 'Request timeout' }); });
708
+ }
709
+ doGet(url, 3);
710
+ });
711
+ }
712
+
713
+ if (action === 'ask_user') {
714
+ const question = arg0;
715
+ const approved = await permissionManager.askPermission('shell', `Ask user: ${question}`, 'ask_user');
716
+ if (!approved) {
717
+ logToolCall('ask_user', { question }, false, 'denied');
718
+ return { error: 'Permission denied' };
719
+ }
720
+ const options = _parseNumberedOptions(question);
721
+ if (options.length >= 2) {
722
+ const selected = await permissionManager.captureSelect({ options });
723
+ logToolCall('ask_user', { question }, true, 'ok');
724
+ return { question, answer: selected || options[0] };
725
+ }
726
+ if (!process.stdout.isTTY || process.stdin.isRaw) {
727
+ process.stdout.write(`\n ${FG_YELLOW}?${RST} ${question}\n ${DIM}[auto-answering 'y']${RST}\n`);
728
+ logToolCall('ask_user', { question }, true, 'ok');
729
+ return { question, answer: 'y' };
730
+ }
731
+ process.stdout.write(`\n ${FG_YELLOW}?${RST} ${question}\n ${FG_GRAY}>${RST} `);
732
+ const buf = Buffer.alloc(4096);
733
+ let input = '';
734
+ while (true) {
735
+ const n = fs.readSync(0, buf, 0, 1);
736
+ if (n === 0) break;
737
+ const ch = buf[0];
738
+ if (ch === 0x0a) break;
739
+ if (ch === 0x0d) continue;
740
+ input += String.fromCharCode(ch);
741
+ }
742
+ _log();
743
+ logToolCall('ask_user', { question }, true, 'ok');
744
+ return { question, answer: input };
745
+ }
746
+
747
+ if (action === 'store_memory') {
748
+ const key = arg0;
749
+ const value = arg1 || '';
750
+ const approved = await permissionManager.askPermission('file', `Store memory: ${key}`, 'store_memory');
751
+ if (!approved) {
752
+ logToolCall('store_memory', { key }, false, 'denied');
753
+ return { error: 'Permission denied' };
754
+ }
755
+ try {
756
+ let mem = {};
757
+ try { mem = JSON.parse(fs.readFileSync(MEMORY_PATH, 'utf8')); } catch {}
758
+ mem[key] = value;
759
+ fs.mkdirSync(path.dirname(MEMORY_PATH), { recursive: true });
760
+ fs.writeFileSync(MEMORY_PATH, JSON.stringify(mem, null, 2));
761
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Stored memory: ${key}${RST}`);
762
+ logToolCall('store_memory', { key }, true, 'ok');
763
+ return { status: 'ok', key };
764
+ } catch (error) {
765
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
766
+ logToolCall('store_memory', { key }, true, 'error');
767
+ return { error: error.message };
768
+ }
769
+ }
770
+
771
+ if (action === 'recall_memory') {
772
+ const key = arg0;
773
+ const approved = await permissionManager.askPermission('file', `Recall memory: ${key}`, 'recall_memory');
774
+ if (!approved) {
775
+ logToolCall('recall_memory', { key }, false, 'denied');
776
+ return { error: 'Permission denied' };
777
+ }
778
+ try {
779
+ let mem = {};
780
+ try { mem = JSON.parse(fs.readFileSync(MEMORY_PATH, 'utf8')); } catch {}
781
+ const found = key in mem;
782
+ const value = found ? mem[key] : null;
783
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Recalled memory: ${key}${RST}`);
784
+ logToolCall('recall_memory', { key }, true, 'ok');
785
+ return { key, value, found };
786
+ } catch (error) {
787
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
788
+ logToolCall('recall_memory', { key }, true, 'error');
789
+ return { error: error.message };
790
+ }
791
+ }
792
+
793
+ if (action === 'list_memories') {
794
+ const approved = await permissionManager.askPermission('file', 'List memories', 'list_memories');
795
+ if (!approved) {
796
+ logToolCall('list_memories', {}, false, 'denied');
797
+ return { error: 'Permission denied' };
798
+ }
799
+ try {
800
+ let mem = {};
801
+ try { mem = JSON.parse(fs.readFileSync(MEMORY_PATH, 'utf8')); } catch {}
802
+ const keys = Object.keys(mem);
803
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Listed ${keys.length} memory key(s)${RST}`);
804
+ logToolCall('list_memories', {}, true, 'ok');
805
+ return { keys };
806
+ } catch (error) {
807
+ _log(` ${FG_RED}✗ ${error.message}${RST}`);
808
+ logToolCall('list_memories', {}, true, 'error');
75
809
  return { error: error.message };
76
810
  }
77
811
  }
78
812
 
813
+ if (action === 'system_info') {
814
+ const approved = await permissionManager.askPermission('shell', 'System info', 'system_info');
815
+ if (!approved) {
816
+ logToolCall('system_info', {}, false, 'denied');
817
+ return { error: 'Permission denied' };
818
+ }
819
+ const info = {
820
+ platform: os.platform(),
821
+ arch: os.arch(),
822
+ hostname: os.hostname(),
823
+ user: process.env.USER || process.env.USERNAME || '',
824
+ total_mem_mb: Math.round(os.totalmem() / 1024 / 1024),
825
+ free_mem_mb: Math.round(os.freemem() / 1024 / 1024),
826
+ node_version: process.version,
827
+ cwd: process.cwd(),
828
+ };
829
+ _log(` ${FG_GREEN}✓${RST} ${FG_GRAY}System info: ${info.platform}/${info.arch}${RST}`);
830
+ logToolCall('system_info', {}, true, 'ok');
831
+ return info;
832
+ }
833
+
834
+ logToolCall(action, { action }, false, 'error');
79
835
  return { error: `Unknown action: ${action}` };
80
836
  }
81
837
 
@@ -95,11 +851,7 @@ function extractToolCalls(text) {
95
851
  }
96
852
  }
97
853
 
98
- for (const match of text.matchAll(/<shell>([\s\S]*?)<\/shell>/g)) {
99
- calls.push(['shell', match[1].trim()]);
100
- }
101
-
102
- for (const match of text.matchAll(/<exec>([\s\S]*?)<\/exec>/g)) {
854
+ for (const match of text.matchAll(/<(?:shell|exec|run_command|run)>([\s\S]*?)<\/(?:shell|exec|run_command|run)>/g)) {
103
855
  calls.push(['shell', match[1].trim()]);
104
856
  }
105
857
 
@@ -107,14 +859,123 @@ function extractToolCalls(text) {
107
859
  calls.push(['read', match[1].trim()]);
108
860
  }
109
861
 
862
+ for (const match of text.matchAll(/<read_file\s+path="([^"]+)"\s*\/?>/g)) {
863
+ calls.push(['read', match[1]]);
864
+ }
865
+
110
866
  for (const match of text.matchAll(/<write_file\s+path="([^"]+)">([\s\S]*?)<\/write_file>/g)) {
111
867
  calls.push(['write', match[1], match[2]]);
112
868
  }
113
869
 
870
+ for (const match of text.matchAll(/<create_file\s+path="([^"]+)">([\s\S]*?)<\/create_file>/g)) {
871
+ calls.push(['write', match[1], match[2]]);
872
+ }
873
+
874
+ for (const match of text.matchAll(/<append_file\s+path="([^"]+)">([\s\S]*?)<\/append_file>/g)) {
875
+ calls.push(['append', match[1], match[2]]);
876
+ }
877
+
878
+ for (const match of text.matchAll(/<list_dir>([\s\S]*?)<\/list_dir>/g)) {
879
+ calls.push(['list_dir', match[1].trim()]);
880
+ }
881
+
882
+ for (const match of text.matchAll(/<search_files>([\s\S]*?)<\/search_files>/g)) {
883
+ calls.push(['search_files', match[1].trim(), '.']);
884
+ }
885
+
886
+ for (const match of text.matchAll(/<search_files\s+pattern="([^"]+)"(?:\s+dir="([^"]*)")?\s*(?:><\/search_files>|\/>)/g)) {
887
+ calls.push(['search_files', match[1], match[2] || '.']);
888
+ }
889
+
890
+ for (const match of text.matchAll(/<delete_file>([\s\S]*?)<\/delete_file>/g)) {
891
+ calls.push(['delete_file', match[1].trim()]);
892
+ }
893
+
894
+ for (const match of text.matchAll(/<make_dir>([\s\S]*?)<\/make_dir>/g)) {
895
+ calls.push(['make_dir', match[1].trim()]);
896
+ }
897
+
898
+ for (const match of text.matchAll(/<remove_dir>([\s\S]*?)<\/remove_dir>/g)) {
899
+ calls.push(['remove_dir', match[1].trim()]);
900
+ }
901
+
902
+ for (const match of text.matchAll(/<get_env>([\s\S]*?)<\/get_env>/g)) {
903
+ calls.push(['get_env', match[1].trim()]);
904
+ }
905
+
906
+ for (const match of text.matchAll(/<set_env\s+name="([^"]+)"\s+value="([^"]*)"\s*(?:><\/set_env>|\/>)/g)) {
907
+ calls.push(['set_env', match[1], match[2]]);
908
+ }
909
+
910
+ for (const match of text.matchAll(/<move_file\s+src="([^"]+)"\s+dst="([^"]+)"\s*(?:><\/move_file>|\/>)/g)) {
911
+ calls.push(['move_file', match[1], match[2]]);
912
+ }
913
+
914
+ for (const match of text.matchAll(/<copy_file\s+src="([^"]+)"\s+dst="([^"]+)"\s*(?:><\/copy_file>|\/>)/g)) {
915
+ calls.push(['copy_file', match[1], match[2]]);
916
+ }
917
+
918
+ for (const match of text.matchAll(/<edit_file\s+path="([^"]+)"\s+line="(\d+)">([\s\S]*?)<\/edit_file>/g)) {
919
+ calls.push(['edit_file', match[1], parseInt(match[2], 10), match[3]]);
920
+ }
921
+
922
+ for (const match of text.matchAll(/<search_in_file\s+path="([^"]+)">([\s\S]*?)<\/search_in_file>/g)) {
923
+ calls.push(['search_in_file', match[1], match[2].trim()]);
924
+ }
925
+
926
+ for (const match of text.matchAll(/<replace_in_file\s+path="([^"]+)"\s+search="([^"]*)"\s+replace="([^"]*)">([\s\S]*?)<\/replace_in_file>/g)) {
927
+ calls.push(['replace_in_file', match[1], match[2], match[3], match[4].trim()]);
928
+ }
929
+
930
+ for (const match of text.matchAll(/<download>([\s\S]*?)<\/download>/g)) {
931
+ calls.push(['download', match[1].trim()]);
932
+ }
933
+
934
+ for (const match of text.matchAll(/<upload\s+path="([^"]+)">([\s\S]*?)<\/upload>/g)) {
935
+ calls.push(['upload', match[1], match[2]]);
936
+ }
937
+
938
+ for (const match of text.matchAll(/<file_stat>([\s\S]*?)<\/file_stat>/g)) {
939
+ calls.push(['file_stat', match[1].trim()]);
940
+ }
941
+
942
+ for (const match of text.matchAll(/<http_get\b([^>]*?)(?:><\/http_get>|\/>)/g)) {
943
+ const attrStr = match[1];
944
+ const urlMatch = attrStr.match(/url="([^"]+)"/);
945
+ const rawMatch = attrStr.match(/raw="([^"]+)"/);
946
+ if (urlMatch) calls.push(['http_get', urlMatch[1], rawMatch ? rawMatch[1] : '']);
947
+ }
948
+
949
+ for (const match of text.matchAll(/<http_get_next\s+key="([^"]+)"\s*(?:><\/http_get_next>|\/>)/g)) {
950
+ calls.push(['http_get_next', match[1]]);
951
+ }
952
+
953
+ for (const match of text.matchAll(/<ask_user\s+question="([^"]+)"\s*(?:><\/ask_user>|\/>)/g)) {
954
+ calls.push(['ask_user', match[1]]);
955
+ }
956
+
957
+ for (const match of text.matchAll(/<store_memory\s+key="([^"]+)">([\s\S]*?)<\/store_memory>/g)) {
958
+ calls.push(['store_memory', match[1], match[2]]);
959
+ }
960
+
961
+ for (const match of text.matchAll(/<recall_memory\s+key="([^"]+)"\s*(?:><\/recall_memory>|\/>)/g)) {
962
+ calls.push(['recall_memory', match[1]]);
963
+ }
964
+
965
+ for (const match of text.matchAll(/<list_memories\s*(?:><\/list_memories>|\/>)/g)) {
966
+ calls.push(['list_memories']);
967
+ }
968
+
969
+ for (const match of text.matchAll(/<system_info\s*(?:><\/system_info>|\/>)/g)) {
970
+ calls.push(['system_info']);
971
+ }
972
+
114
973
  return calls;
115
974
  }
116
975
 
117
976
  module.exports = {
118
977
  createToolExecutor,
119
978
  extractToolCalls,
979
+ getSkippedOps,
980
+ setUIActive,
120
981
  };