@semalt-ai/code 1.7.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/.claude/settings.local.json +8 -0
- package/ARCHITECTURE.md +99 -0
- package/CLAUDE.md +349 -0
- package/index.js +69 -7
- package/lib/agent.js +508 -39
- package/lib/api.js +225 -79
- package/lib/args.js +31 -0
- package/lib/audit.js +31 -0
- package/lib/commands.js +959 -307
- package/lib/config.js +51 -5
- package/lib/constants.js +56 -0
- package/lib/context.js +2 -6
- package/lib/metrics.js +94 -0
- package/lib/permissions.js +180 -49
- package/lib/prompts.js +89 -13
- package/lib/storage.js +96 -0
- package/lib/tools.js +896 -35
- package/lib/ui/ansi.js +64 -0
- package/lib/ui/chat-history.js +217 -0
- package/lib/ui/create-ui.js +474 -0
- package/lib/ui/diff.js +243 -0
- package/lib/ui/input-field.js +1176 -0
- package/lib/ui/layout.js +53 -0
- package/lib/ui/legacy.js +130 -0
- package/lib/ui/status-bar.js +130 -0
- package/lib/ui/stream.js +158 -0
- package/lib/ui/utils.js +45 -0
- package/lib/ui.js +42 -598
- package/package.json +1 -1
- package/path +1 -0
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
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,
|
|
40
|
-
|
|
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
|
-
|
|
97
|
+
_log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath} (${lines} lines, ${data.length} chars)${RST}`);
|
|
49
98
|
} else {
|
|
50
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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(/ /gi, ' ')
|
|
615
|
+
.replace(/&/gi, '&')
|
|
616
|
+
.replace(/</gi, '<')
|
|
617
|
+
.replace(/>/gi, '>')
|
|
618
|
+
.replace(/"/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
|
};
|