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.
@@ -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