openpaean 0.3.2 → 0.4.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/README.md +8 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +25 -0
- package/dist/commands/agent.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +34 -1
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +59 -11
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/system.d.ts +41 -0
- package/dist/mcp/system.d.ts.map +1 -0
- package/dist/mcp/system.js +740 -0
- package/dist/mcp/system.js.map +1 -0
- package/dist/mcp/tools.d.ts +73 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +175 -2
- package/dist/mcp/tools.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System/Shell MCP Tools (Open Source)
|
|
3
|
+
*
|
|
4
|
+
* Provides controlled shell execution, filesystem operations, and process
|
|
5
|
+
* management capabilities for the OpenPaean CLI.
|
|
6
|
+
*
|
|
7
|
+
* This is the open-source foundation for local tool execution.
|
|
8
|
+
* For advanced features (autonomous worker, CLI agent orchestration),
|
|
9
|
+
* see the commercial Paean CLI.
|
|
10
|
+
*
|
|
11
|
+
* Security:
|
|
12
|
+
* - Command whitelist for autonomous/safe execution
|
|
13
|
+
* - Dangerous pattern detection
|
|
14
|
+
* - System path write protection
|
|
15
|
+
* - Input sanitization for process names
|
|
16
|
+
*/
|
|
17
|
+
import { spawn, exec } from 'child_process';
|
|
18
|
+
import { promisify } from 'util';
|
|
19
|
+
import { createWriteStream } from 'fs';
|
|
20
|
+
import { writeFile, readFile, mkdir, readdir, stat, appendFile } from 'fs/promises';
|
|
21
|
+
import { pipeline } from 'stream/promises';
|
|
22
|
+
import { Readable } from 'stream';
|
|
23
|
+
import { basename, join, resolve, dirname } from 'path';
|
|
24
|
+
const execAsync = promisify(exec);
|
|
25
|
+
/**
|
|
26
|
+
* Command whitelist for autonomous mode
|
|
27
|
+
* These commands are considered safe to execute without user confirmation
|
|
28
|
+
*/
|
|
29
|
+
const COMMAND_WHITELIST = new Set([
|
|
30
|
+
// Package managers
|
|
31
|
+
'npm', 'bun', 'bunx', 'npx', 'pnpm', 'yarn',
|
|
32
|
+
// Runtime
|
|
33
|
+
'node', 'deno', 'tsx',
|
|
34
|
+
// Version control
|
|
35
|
+
'git',
|
|
36
|
+
// Build tools
|
|
37
|
+
'tsc', 'esbuild', 'vite', 'webpack',
|
|
38
|
+
// Testing
|
|
39
|
+
'vitest', 'jest', 'mocha',
|
|
40
|
+
// Basic utilities (read-only)
|
|
41
|
+
'echo', 'cat', 'ls', 'pwd', 'which', 'head', 'tail', 'grep', 'find', 'wc',
|
|
42
|
+
// Process inspection
|
|
43
|
+
'ps', 'pgrep', 'lsof',
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* Dangerous command patterns that should never be executed
|
|
47
|
+
*/
|
|
48
|
+
const DANGEROUS_PATTERNS = [
|
|
49
|
+
/rm\s+-rf?\s+[\/~]/, // rm -rf / or ~
|
|
50
|
+
/:(){ :|:& };:/, // Fork bomb
|
|
51
|
+
/>\s*\/dev\/sd/, // Write to disk
|
|
52
|
+
/mkfs\./, // Format disk
|
|
53
|
+
/dd\s+if=/, // Direct disk write
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Check if a command is in the whitelist
|
|
57
|
+
*/
|
|
58
|
+
export function isCommandWhitelisted(command) {
|
|
59
|
+
const baseCommand = command.trim().split(/\s+/)[0];
|
|
60
|
+
// Handle path prefixes like /usr/bin/node
|
|
61
|
+
const cmdName = baseCommand.split('/').pop() || baseCommand;
|
|
62
|
+
return COMMAND_WHITELIST.has(cmdName);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if a command contains dangerous patterns
|
|
66
|
+
*/
|
|
67
|
+
export function isDangerousCommand(command) {
|
|
68
|
+
return DANGEROUS_PATTERNS.some(pattern => pattern.test(command));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* System MCP Tools definition (open-source shell & filesystem tools)
|
|
72
|
+
*/
|
|
73
|
+
export function getSystemTools() {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
name: 'paean_execute_shell',
|
|
77
|
+
description: 'Execute a shell command on the local machine. ' +
|
|
78
|
+
'In autonomous mode, only whitelisted commands (npm, bun, git, node, etc.) are allowed. ' +
|
|
79
|
+
'Use this to run tests, build projects, or inspect the system.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
command: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'The command to execute (e.g., "npm test", "bun run build")',
|
|
86
|
+
},
|
|
87
|
+
cwd: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Working directory for the command (optional)',
|
|
90
|
+
},
|
|
91
|
+
background: {
|
|
92
|
+
type: 'boolean',
|
|
93
|
+
description: 'Run in background (detached mode). Useful for starting long-running services.',
|
|
94
|
+
},
|
|
95
|
+
timeout: {
|
|
96
|
+
type: 'number',
|
|
97
|
+
description: 'Timeout in milliseconds (default: 60000, max: 300000)',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ['command'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'paean_check_process',
|
|
105
|
+
description: 'Check if a process is running by name or PID. ' +
|
|
106
|
+
'Use this to verify if a service or dev server is running.',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
name: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'Process name to search for (e.g., "node", "vite")',
|
|
113
|
+
},
|
|
114
|
+
pid: {
|
|
115
|
+
type: 'number',
|
|
116
|
+
description: 'Process ID to check',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'paean_kill_process',
|
|
123
|
+
description: 'Terminate a process by PID. Use SIGTERM for graceful shutdown or SIGKILL for force kill.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
pid: {
|
|
128
|
+
type: 'number',
|
|
129
|
+
description: 'Process ID to terminate',
|
|
130
|
+
},
|
|
131
|
+
signal: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
enum: ['SIGTERM', 'SIGKILL', 'SIGINT'],
|
|
134
|
+
description: 'Signal to send (default: SIGTERM)',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ['pid'],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'paean_download_file',
|
|
142
|
+
description: 'Download a file from a URL to the local filesystem. ' +
|
|
143
|
+
'Supports HTTPS URLs. Useful for downloading assets, documents, or other files.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
url: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'The URL to download from (supports HTTPS)',
|
|
150
|
+
},
|
|
151
|
+
filename: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'Optional filename for the downloaded file.',
|
|
154
|
+
},
|
|
155
|
+
directory: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: 'Optional target directory path. Defaults to current working directory.',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['url'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'paean_write_file',
|
|
165
|
+
description: 'Write content to a file on the local filesystem. ' +
|
|
166
|
+
'Creates parent directories automatically. Supports append mode.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
filePath: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Absolute or relative path to write to',
|
|
173
|
+
},
|
|
174
|
+
content: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'The text content to write to the file',
|
|
177
|
+
},
|
|
178
|
+
append: {
|
|
179
|
+
type: 'boolean',
|
|
180
|
+
description: 'If true, append to file instead of overwriting (default: false)',
|
|
181
|
+
},
|
|
182
|
+
encoding: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'File encoding (default: utf-8)',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ['filePath', 'content'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'paean_read_file',
|
|
192
|
+
description: 'Read the contents of a file from the local filesystem. ' +
|
|
193
|
+
'Supports offset and limit for reading large files in chunks.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
filePath: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'Absolute or relative path to read from',
|
|
200
|
+
},
|
|
201
|
+
offset: {
|
|
202
|
+
type: 'number',
|
|
203
|
+
description: 'Line number to start reading from (0-based, default: 0)',
|
|
204
|
+
},
|
|
205
|
+
limit: {
|
|
206
|
+
type: 'number',
|
|
207
|
+
description: 'Maximum number of lines to read (default: all)',
|
|
208
|
+
},
|
|
209
|
+
encoding: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'File encoding (default: utf-8)',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
required: ['filePath'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'paean_list_directory',
|
|
219
|
+
description: 'List files and directories at a given path. ' +
|
|
220
|
+
'Returns names, types (file/directory), and sizes. Supports recursive listing and glob patterns.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
dirPath: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: 'Directory path to list (default: current working directory)',
|
|
227
|
+
},
|
|
228
|
+
recursive: {
|
|
229
|
+
type: 'boolean',
|
|
230
|
+
description: 'If true, list recursively (default: false, max depth: 3)',
|
|
231
|
+
},
|
|
232
|
+
pattern: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
description: 'Glob pattern to filter entries (e.g., "*.md", "*.ts")',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Execute a system tool
|
|
243
|
+
*/
|
|
244
|
+
export async function executeSystemTool(toolName, args, options) {
|
|
245
|
+
const { autonomousMode = false, debug = false } = options || {};
|
|
246
|
+
switch (toolName) {
|
|
247
|
+
case 'paean_execute_shell':
|
|
248
|
+
return executeShell(args, { autonomousMode, debug });
|
|
249
|
+
case 'paean_check_process':
|
|
250
|
+
return checkProcess(args);
|
|
251
|
+
case 'paean_kill_process':
|
|
252
|
+
return killProcess(args);
|
|
253
|
+
case 'paean_download_file':
|
|
254
|
+
return downloadFile(args);
|
|
255
|
+
case 'paean_write_file':
|
|
256
|
+
return writeLocalFile(args);
|
|
257
|
+
case 'paean_read_file':
|
|
258
|
+
return readLocalFile(args);
|
|
259
|
+
case 'paean_list_directory':
|
|
260
|
+
return listDirectory(args);
|
|
261
|
+
default:
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: `Unknown system tool: ${toolName}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Execute a shell command
|
|
270
|
+
*/
|
|
271
|
+
async function executeShell(args, options) {
|
|
272
|
+
const command = args.command;
|
|
273
|
+
const cwd = args.cwd;
|
|
274
|
+
const background = args.background;
|
|
275
|
+
const timeout = Math.min(args.timeout || 60000, 300000); // Max 5 minutes
|
|
276
|
+
if (!command) {
|
|
277
|
+
return { success: false, error: 'Command is required' };
|
|
278
|
+
}
|
|
279
|
+
// Security checks
|
|
280
|
+
if (isDangerousCommand(command)) {
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
error: 'Command contains dangerous patterns and cannot be executed',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// In autonomous mode, only allow whitelisted commands
|
|
287
|
+
if (options.autonomousMode && !isCommandWhitelisted(command)) {
|
|
288
|
+
return {
|
|
289
|
+
success: false,
|
|
290
|
+
error: `Command "${command.split(/\s+/)[0]}" is not in the whitelist. ` +
|
|
291
|
+
`Allowed: ${Array.from(COMMAND_WHITELIST).join(', ')}`,
|
|
292
|
+
requiresConfirmation: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
if (background) {
|
|
297
|
+
// Detached background process
|
|
298
|
+
const parts = command.split(/\s+/);
|
|
299
|
+
const cmd = parts[0];
|
|
300
|
+
const cmdArgs = parts.slice(1);
|
|
301
|
+
const subprocess = spawn(cmd, cmdArgs, {
|
|
302
|
+
cwd: cwd || process.cwd(),
|
|
303
|
+
detached: true,
|
|
304
|
+
stdio: 'ignore',
|
|
305
|
+
shell: true,
|
|
306
|
+
});
|
|
307
|
+
subprocess.unref();
|
|
308
|
+
return {
|
|
309
|
+
success: true,
|
|
310
|
+
message: 'Process started in background',
|
|
311
|
+
pid: subprocess.pid,
|
|
312
|
+
background: true,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// Synchronous execution with timeout
|
|
317
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
318
|
+
cwd: cwd || process.cwd(),
|
|
319
|
+
timeout,
|
|
320
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
stdout: stdout.trim(),
|
|
325
|
+
stderr: stderr.trim(),
|
|
326
|
+
exitCode: 0,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
const err = error;
|
|
332
|
+
if (err.killed) {
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
error: `Command timed out after ${timeout}ms`,
|
|
336
|
+
timedOut: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
success: false,
|
|
341
|
+
error: err.message || 'Command execution failed',
|
|
342
|
+
exitCode: typeof err.code === 'number' ? err.code : 1,
|
|
343
|
+
stdout: err.stdout?.trim(),
|
|
344
|
+
stderr: err.stderr?.trim(),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Check if a process is running
|
|
350
|
+
*/
|
|
351
|
+
async function checkProcess(args) {
|
|
352
|
+
const name = args.name;
|
|
353
|
+
const pid = args.pid;
|
|
354
|
+
if (!name && !pid) {
|
|
355
|
+
return { success: false, error: 'Either name or pid is required' };
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
if (pid) {
|
|
359
|
+
try {
|
|
360
|
+
process.kill(pid, 0); // Signal 0 = check existence
|
|
361
|
+
return { success: true, running: true, pid };
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return { success: true, running: false, pid };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else if (name) {
|
|
368
|
+
// Sanitize name to prevent command injection
|
|
369
|
+
const sanitizedName = name.replace(/[^a-zA-Z0-9\-_. ]/g, '');
|
|
370
|
+
if (sanitizedName !== name) {
|
|
371
|
+
return {
|
|
372
|
+
success: false,
|
|
373
|
+
error: 'Process name contains invalid characters. Only alphanumeric, dash, underscore, dot, and space are allowed.',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const { stdout } = await execAsync(`pgrep -f "${sanitizedName}"`);
|
|
378
|
+
const pids = stdout.trim().split('\n').filter(Boolean).map(Number);
|
|
379
|
+
return {
|
|
380
|
+
success: true,
|
|
381
|
+
running: pids.length > 0,
|
|
382
|
+
processName: name,
|
|
383
|
+
pids,
|
|
384
|
+
count: pids.length,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return {
|
|
389
|
+
success: true,
|
|
390
|
+
running: false,
|
|
391
|
+
processName: name,
|
|
392
|
+
pids: [],
|
|
393
|
+
count: 0,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return { success: false, error: 'Invalid arguments' };
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
error: error instanceof Error ? error.message : 'Failed to check process',
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Kill a process by PID
|
|
408
|
+
*/
|
|
409
|
+
async function killProcess(args) {
|
|
410
|
+
const pid = args.pid;
|
|
411
|
+
const signal = args.signal || 'SIGTERM';
|
|
412
|
+
if (!pid) {
|
|
413
|
+
return { success: false, error: 'PID is required' };
|
|
414
|
+
}
|
|
415
|
+
const validSignals = ['SIGTERM', 'SIGKILL', 'SIGINT'];
|
|
416
|
+
if (!validSignals.includes(signal)) {
|
|
417
|
+
return { success: false, error: `Invalid signal: ${signal}. Use: ${validSignals.join(', ')}` };
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
try {
|
|
421
|
+
process.kill(pid, 0);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return { success: false, error: `Process ${pid} not found` };
|
|
425
|
+
}
|
|
426
|
+
process.kill(pid, signal);
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
message: `Sent ${signal} to process ${pid}`,
|
|
430
|
+
pid,
|
|
431
|
+
signal,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
error: error instanceof Error ? error.message : 'Failed to kill process',
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Download a file from a URL to the local filesystem
|
|
443
|
+
*/
|
|
444
|
+
async function downloadFile(args) {
|
|
445
|
+
const url = args.url;
|
|
446
|
+
const filename = args.filename;
|
|
447
|
+
const directory = args.directory;
|
|
448
|
+
if (!url) {
|
|
449
|
+
return { success: false, error: 'URL is required' };
|
|
450
|
+
}
|
|
451
|
+
let parsedUrl;
|
|
452
|
+
try {
|
|
453
|
+
parsedUrl = new URL(url);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return { success: false, error: 'Invalid URL format' };
|
|
457
|
+
}
|
|
458
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
459
|
+
return { success: false, error: `Unsupported protocol: ${parsedUrl.protocol}. Only HTTP/HTTPS are supported.` };
|
|
460
|
+
}
|
|
461
|
+
const targetDir = directory ? resolve(directory) : process.cwd();
|
|
462
|
+
try {
|
|
463
|
+
await mkdir(targetDir, { recursive: true });
|
|
464
|
+
const response = await fetch(url, {
|
|
465
|
+
headers: { 'User-Agent': 'OpenPaean-CLI/1.0' },
|
|
466
|
+
signal: AbortSignal.timeout(120_000),
|
|
467
|
+
});
|
|
468
|
+
if (!response.ok) {
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
error: `Download failed: HTTP ${response.status} ${response.statusText}`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
// Determine filename
|
|
475
|
+
let resolvedFilename = filename;
|
|
476
|
+
if (!resolvedFilename) {
|
|
477
|
+
const contentDisposition = response.headers.get('content-disposition');
|
|
478
|
+
if (contentDisposition) {
|
|
479
|
+
const match = contentDisposition.match(/filename[^;=\n]*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/i);
|
|
480
|
+
if (match) {
|
|
481
|
+
resolvedFilename = (match[2] || match[3])?.trim();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (!resolvedFilename) {
|
|
485
|
+
const urlPath = parsedUrl.pathname;
|
|
486
|
+
const urlFilename = basename(urlPath);
|
|
487
|
+
resolvedFilename = decodeURIComponent(urlFilename.split('?')[0]);
|
|
488
|
+
}
|
|
489
|
+
if (!resolvedFilename || resolvedFilename === '/' || resolvedFilename === '') {
|
|
490
|
+
const contentType = response.headers.get('content-type') || '';
|
|
491
|
+
const ext = contentType.includes('png') ? '.png'
|
|
492
|
+
: contentType.includes('jpeg') || contentType.includes('jpg') ? '.jpg'
|
|
493
|
+
: contentType.includes('gif') ? '.gif'
|
|
494
|
+
: contentType.includes('webp') ? '.webp'
|
|
495
|
+
: contentType.includes('svg') ? '.svg'
|
|
496
|
+
: contentType.includes('pdf') ? '.pdf'
|
|
497
|
+
: '';
|
|
498
|
+
resolvedFilename = `download-${Date.now()}${ext}`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Sanitize filename
|
|
502
|
+
resolvedFilename = resolvedFilename.replace(/[/\\:\0]/g, '_');
|
|
503
|
+
const filePath = join(targetDir, resolvedFilename);
|
|
504
|
+
if (response.body) {
|
|
505
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
506
|
+
const writeStream = createWriteStream(filePath);
|
|
507
|
+
await pipeline(nodeStream, writeStream);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
511
|
+
await writeFile(filePath, buffer);
|
|
512
|
+
}
|
|
513
|
+
const contentLength = response.headers.get('content-length');
|
|
514
|
+
const contentType = response.headers.get('content-type');
|
|
515
|
+
return {
|
|
516
|
+
success: true,
|
|
517
|
+
message: 'File downloaded successfully',
|
|
518
|
+
filePath,
|
|
519
|
+
filename: resolvedFilename,
|
|
520
|
+
directory: targetDir,
|
|
521
|
+
size: contentLength ? parseInt(contentLength, 10) : undefined,
|
|
522
|
+
contentType: contentType || undefined,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
527
|
+
return { success: false, error: 'Download timed out after 2 minutes', timedOut: true };
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
success: false,
|
|
531
|
+
error: error instanceof Error ? error.message : 'Download failed',
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Get the whitelist for display/debugging
|
|
537
|
+
*/
|
|
538
|
+
export function getCommandWhitelist() {
|
|
539
|
+
return Array.from(COMMAND_WHITELIST);
|
|
540
|
+
}
|
|
541
|
+
// ============================================
|
|
542
|
+
// Filesystem Tools
|
|
543
|
+
// ============================================
|
|
544
|
+
/**
|
|
545
|
+
* Write content to a local file
|
|
546
|
+
*/
|
|
547
|
+
async function writeLocalFile(args) {
|
|
548
|
+
const filePath = args.filePath;
|
|
549
|
+
const content = args.content;
|
|
550
|
+
const shouldAppend = args.append;
|
|
551
|
+
const encoding = args.encoding || 'utf-8';
|
|
552
|
+
if (!filePath) {
|
|
553
|
+
return { success: false, error: 'filePath is required' };
|
|
554
|
+
}
|
|
555
|
+
if (content === undefined || content === null) {
|
|
556
|
+
return { success: false, error: 'content is required' };
|
|
557
|
+
}
|
|
558
|
+
const resolvedPath = resolve(filePath);
|
|
559
|
+
// Security: block writing to critical system paths
|
|
560
|
+
const blockedPrefixes = ['/etc/', '/usr/', '/bin/', '/sbin/', '/System/', '/Library/'];
|
|
561
|
+
if (blockedPrefixes.some(p => resolvedPath.startsWith(p))) {
|
|
562
|
+
return {
|
|
563
|
+
success: false,
|
|
564
|
+
error: `Writing to system path is not allowed: ${resolvedPath}`,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
569
|
+
if (shouldAppend) {
|
|
570
|
+
await appendFile(resolvedPath, content, { encoding });
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
await writeFile(resolvedPath, content, { encoding });
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
success: true,
|
|
577
|
+
message: shouldAppend ? 'Content appended to file' : 'File written successfully',
|
|
578
|
+
filePath: resolvedPath,
|
|
579
|
+
bytesWritten: Buffer.byteLength(content, encoding),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
return {
|
|
584
|
+
success: false,
|
|
585
|
+
error: error instanceof Error ? error.message : 'Failed to write file',
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Read content from a local file
|
|
591
|
+
*/
|
|
592
|
+
async function readLocalFile(args) {
|
|
593
|
+
const filePath = args.filePath;
|
|
594
|
+
const offset = args.offset;
|
|
595
|
+
const limit = args.limit;
|
|
596
|
+
const encoding = args.encoding || 'utf-8';
|
|
597
|
+
if (!filePath) {
|
|
598
|
+
return { success: false, error: 'filePath is required' };
|
|
599
|
+
}
|
|
600
|
+
const resolvedPath = resolve(filePath);
|
|
601
|
+
try {
|
|
602
|
+
const content = await readFile(resolvedPath, { encoding });
|
|
603
|
+
const lines = content.split('\n');
|
|
604
|
+
const startLine = offset || 0;
|
|
605
|
+
const endLine = limit ? startLine + limit : lines.length;
|
|
606
|
+
const slicedLines = lines.slice(startLine, endLine);
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
filePath: resolvedPath,
|
|
610
|
+
content: slicedLines.join('\n'),
|
|
611
|
+
totalLines: lines.length,
|
|
612
|
+
linesReturned: slicedLines.length,
|
|
613
|
+
startLine,
|
|
614
|
+
endLine: Math.min(endLine, lines.length),
|
|
615
|
+
truncated: endLine < lines.length,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
const err = error;
|
|
620
|
+
if (err.code === 'ENOENT') {
|
|
621
|
+
return { success: false, error: `File not found: ${resolvedPath}` };
|
|
622
|
+
}
|
|
623
|
+
if (err.code === 'EISDIR') {
|
|
624
|
+
return { success: false, error: `Path is a directory, not a file: ${resolvedPath}` };
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
success: false,
|
|
628
|
+
error: error instanceof Error ? error.message : 'Failed to read file',
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* List directory contents
|
|
634
|
+
*/
|
|
635
|
+
async function listDirectory(args) {
|
|
636
|
+
const dirPath = args.dirPath;
|
|
637
|
+
const recursive = args.recursive;
|
|
638
|
+
const pattern = args.pattern;
|
|
639
|
+
const resolvedPath = resolve(dirPath || process.cwd());
|
|
640
|
+
try {
|
|
641
|
+
if (recursive) {
|
|
642
|
+
const entries = await listDirectoryRecursive(resolvedPath, 0, 3, pattern);
|
|
643
|
+
return {
|
|
644
|
+
success: true,
|
|
645
|
+
dirPath: resolvedPath,
|
|
646
|
+
entries,
|
|
647
|
+
count: entries.length,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
const dirEntries = await readdir(resolvedPath, { withFileTypes: true });
|
|
652
|
+
let entries = dirEntries.map(e => ({
|
|
653
|
+
name: e.name,
|
|
654
|
+
type: e.isDirectory() ? 'directory' : 'file',
|
|
655
|
+
path: join(resolvedPath, e.name),
|
|
656
|
+
}));
|
|
657
|
+
if (pattern) {
|
|
658
|
+
const regex = globToRegex(pattern);
|
|
659
|
+
entries = entries.filter(e => regex.test(e.name));
|
|
660
|
+
}
|
|
661
|
+
const enriched = await Promise.all(entries.map(async (e) => {
|
|
662
|
+
if (e.type === 'file') {
|
|
663
|
+
try {
|
|
664
|
+
const s = await stat(e.path);
|
|
665
|
+
return { ...e, size: s.size };
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return e;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return e;
|
|
672
|
+
}));
|
|
673
|
+
return {
|
|
674
|
+
success: true,
|
|
675
|
+
dirPath: resolvedPath,
|
|
676
|
+
entries: enriched,
|
|
677
|
+
count: enriched.length,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
const err = error;
|
|
683
|
+
if (err.code === 'ENOENT') {
|
|
684
|
+
return { success: false, error: `Directory not found: ${resolvedPath}` };
|
|
685
|
+
}
|
|
686
|
+
if (err.code === 'ENOTDIR') {
|
|
687
|
+
return { success: false, error: `Path is not a directory: ${resolvedPath}` };
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: error instanceof Error ? error.message : 'Failed to list directory',
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Recursively list directory up to maxDepth
|
|
697
|
+
*/
|
|
698
|
+
async function listDirectoryRecursive(dirPath, depth, maxDepth, pattern) {
|
|
699
|
+
if (depth > maxDepth)
|
|
700
|
+
return [];
|
|
701
|
+
const results = [];
|
|
702
|
+
const dirEntries = await readdir(dirPath, { withFileTypes: true });
|
|
703
|
+
const regex = pattern ? globToRegex(pattern) : null;
|
|
704
|
+
for (const entry of dirEntries) {
|
|
705
|
+
// Skip hidden directories and node_modules in recursive mode
|
|
706
|
+
if (entry.name.startsWith('.') && entry.isDirectory())
|
|
707
|
+
continue;
|
|
708
|
+
if (entry.name === 'node_modules')
|
|
709
|
+
continue;
|
|
710
|
+
const fullPath = join(dirPath, entry.name);
|
|
711
|
+
if (entry.isDirectory()) {
|
|
712
|
+
results.push({ name: entry.name, type: 'directory', path: fullPath });
|
|
713
|
+
const children = await listDirectoryRecursive(fullPath, depth + 1, maxDepth, pattern);
|
|
714
|
+
results.push(...children);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
if (!regex || regex.test(entry.name)) {
|
|
718
|
+
try {
|
|
719
|
+
const s = await stat(fullPath);
|
|
720
|
+
results.push({ name: entry.name, type: 'file', path: fullPath, size: s.size });
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
results.push({ name: entry.name, type: 'file', path: fullPath });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return results;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Convert a simple glob pattern to regex
|
|
732
|
+
*/
|
|
733
|
+
function globToRegex(pattern) {
|
|
734
|
+
const escaped = pattern
|
|
735
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
736
|
+
.replace(/\*/g, '.*')
|
|
737
|
+
.replace(/\?/g, '.');
|
|
738
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
739
|
+
}
|
|
740
|
+
//# sourceMappingURL=system.js.map
|