@push.rocks/smartagent 1.2.3 → 1.2.5

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.
@@ -26,6 +26,14 @@ export interface IDualAgentOptions extends plugins.smartai.ISmartAiOptions {
26
26
  maxConsecutiveRejections?: number;
27
27
  /** Enable verbose logging */
28
28
  verbose?: boolean;
29
+ /** Maximum characters for tool result output before truncation (default: 15000). Set to 0 to disable. */
30
+ maxResultChars?: number;
31
+ /** Maximum history messages to pass to API (default: 20). Set to 0 for unlimited. */
32
+ maxHistoryMessages?: number;
33
+ /** Optional callback for live progress updates during execution */
34
+ onProgress?: (event: IProgressEvent) => void;
35
+ /** Prefix for log messages (e.g., "[README]", "[Commit]"). Default: empty */
36
+ logPrefix?: string;
29
37
  }
30
38
 
31
39
  // ================================
@@ -84,6 +92,8 @@ export interface IToolExecutionResult {
84
92
  success: boolean;
85
93
  result?: unknown;
86
94
  error?: string;
95
+ /** Optional human-readable summary for history (if provided, used instead of full result) */
96
+ summary?: string;
87
97
  }
88
98
 
89
99
  /**
@@ -195,6 +205,58 @@ export interface IDualAgentRunResult {
195
205
  error?: string;
196
206
  }
197
207
 
208
+ // ================================
209
+ // Progress Event Interfaces
210
+ // ================================
211
+
212
+ /**
213
+ * Progress event types for live feedback during agent execution
214
+ */
215
+ export type TProgressEventType =
216
+ | 'task_started'
217
+ | 'iteration_started'
218
+ | 'tool_proposed'
219
+ | 'guardian_evaluating'
220
+ | 'tool_approved'
221
+ | 'tool_rejected'
222
+ | 'tool_executing'
223
+ | 'tool_completed'
224
+ | 'task_completed'
225
+ | 'clarification_needed'
226
+ | 'max_iterations'
227
+ | 'max_rejections';
228
+
229
+ /**
230
+ * Log level for progress events
231
+ */
232
+ export type TLogLevel = 'info' | 'warn' | 'error' | 'success';
233
+
234
+ /**
235
+ * Progress event for live feedback during agent execution
236
+ */
237
+ export interface IProgressEvent {
238
+ /** Type of progress event */
239
+ type: TProgressEventType;
240
+ /** Current iteration number */
241
+ iteration?: number;
242
+ /** Maximum iterations configured */
243
+ maxIterations?: number;
244
+ /** Name of the tool being used */
245
+ toolName?: string;
246
+ /** Action being performed */
247
+ action?: string;
248
+ /** Reason for rejection or other explanation */
249
+ reason?: string;
250
+ /** Human-readable message about the event */
251
+ message?: string;
252
+ /** Timestamp of the event */
253
+ timestamp: Date;
254
+ /** Log level for this event (info, warn, error, success) */
255
+ logLevel: TLogLevel;
256
+ /** Pre-formatted log message ready for output */
257
+ logMessage: string;
258
+ }
259
+
198
260
  // ================================
199
261
  // Utility Types
200
262
  // ================================
@@ -8,6 +8,8 @@ import { BaseToolWrapper } from './smartagent.tools.base.js';
8
8
  export interface IFilesystemToolOptions {
9
9
  /** Base path to scope all operations to. If set, all paths must be within this directory. */
10
10
  basePath?: string;
11
+ /** Glob patterns to exclude from listings (e.g., ['.nogit/**', 'node_modules/**']) */
12
+ excludePatterns?: string[];
11
13
  }
12
14
 
13
15
  /**
@@ -20,12 +22,25 @@ export class FilesystemTool extends BaseToolWrapper {
20
22
 
21
23
  /** Base path to scope all operations to */
22
24
  private basePath?: string;
25
+ /** Glob patterns to exclude from listings */
26
+ private excludePatterns: string[];
23
27
 
24
28
  constructor(options?: IFilesystemToolOptions) {
25
29
  super();
26
30
  if (options?.basePath) {
27
31
  this.basePath = plugins.path.resolve(options.basePath);
28
32
  }
33
+ this.excludePatterns = options?.excludePatterns || [];
34
+ }
35
+
36
+ /**
37
+ * Check if a relative path should be excluded based on exclude patterns
38
+ */
39
+ private isExcluded(relativePath: string): boolean {
40
+ if (this.excludePatterns.length === 0) return false;
41
+ return this.excludePatterns.some(pattern =>
42
+ plugins.minimatch(relativePath, pattern, { dot: true })
43
+ );
29
44
  }
30
45
 
31
46
  /**
@@ -46,17 +61,25 @@ export class FilesystemTool extends BaseToolWrapper {
46
61
  public actions: interfaces.IToolAction[] = [
47
62
  {
48
63
  name: 'read',
49
- description: 'Read the contents of a file',
64
+ description: 'Read file contents (full or specific line range)',
50
65
  parameters: {
51
66
  type: 'object',
52
67
  properties: {
53
- path: { type: 'string', description: 'Absolute path to the file' },
68
+ path: { type: 'string', description: 'Path to the file' },
54
69
  encoding: {
55
70
  type: 'string',
56
71
  enum: ['utf8', 'binary', 'base64'],
57
72
  default: 'utf8',
58
73
  description: 'File encoding',
59
74
  },
75
+ startLine: {
76
+ type: 'number',
77
+ description: 'First line to read (1-indexed, inclusive). If omitted, reads from beginning.',
78
+ },
79
+ endLine: {
80
+ type: 'number',
81
+ description: 'Last line to read (1-indexed, inclusive). If omitted, reads to end.',
82
+ },
60
83
  },
61
84
  required: ['path'],
62
85
  },
@@ -182,6 +205,55 @@ export class FilesystemTool extends BaseToolWrapper {
182
205
  required: ['path'],
183
206
  },
184
207
  },
208
+ {
209
+ name: 'tree',
210
+ description: 'Show directory structure as a tree (no file contents)',
211
+ parameters: {
212
+ type: 'object',
213
+ properties: {
214
+ path: { type: 'string', description: 'Root directory path' },
215
+ maxDepth: {
216
+ type: 'number',
217
+ default: 3,
218
+ description: 'Maximum depth to traverse (default: 3)',
219
+ },
220
+ filter: {
221
+ type: 'string',
222
+ description: 'Glob pattern to filter files (e.g., "*.ts")',
223
+ },
224
+ showSizes: {
225
+ type: 'boolean',
226
+ default: false,
227
+ description: 'Include file sizes in output',
228
+ },
229
+ format: {
230
+ type: 'string',
231
+ enum: ['string', 'json'],
232
+ default: 'string',
233
+ description: 'Output format: "string" for human-readable tree, "json" for structured array',
234
+ },
235
+ },
236
+ required: ['path'],
237
+ },
238
+ },
239
+ {
240
+ name: 'glob',
241
+ description: 'Find files matching a glob pattern',
242
+ parameters: {
243
+ type: 'object',
244
+ properties: {
245
+ pattern: {
246
+ type: 'string',
247
+ description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.js")',
248
+ },
249
+ path: {
250
+ type: 'string',
251
+ description: 'Base path to search from (defaults to current directory)',
252
+ },
253
+ },
254
+ required: ['pattern'],
255
+ },
256
+ },
185
257
  ];
186
258
 
187
259
  private smartfs!: plugins.smartfs.SmartFs;
@@ -207,16 +279,61 @@ export class FilesystemTool extends BaseToolWrapper {
207
279
  case 'read': {
208
280
  const validatedPath = this.validatePath(params.path as string);
209
281
  const encoding = (params.encoding as string) || 'utf8';
210
- const content = await this.smartfs
282
+ const startLine = params.startLine as number | undefined;
283
+ const endLine = params.endLine as number | undefined;
284
+
285
+ const fullContent = await this.smartfs
211
286
  .file(validatedPath)
212
287
  .encoding(encoding as 'utf8' | 'binary' | 'base64')
213
288
  .read();
289
+
290
+ const contentStr = fullContent.toString();
291
+ const lines = contentStr.split('\n');
292
+ const totalLines = lines.length;
293
+
294
+ // Apply line range if specified
295
+ let resultContent: string;
296
+ let resultStartLine = 1;
297
+ let resultEndLine = totalLines;
298
+
299
+ if (startLine !== undefined || endLine !== undefined) {
300
+ const start = Math.max(1, startLine ?? 1);
301
+ const end = Math.min(totalLines, endLine ?? totalLines);
302
+ resultStartLine = start;
303
+ resultEndLine = end;
304
+
305
+ // Convert to 0-indexed for array slicing
306
+ const selectedLines = lines.slice(start - 1, end);
307
+
308
+ // Add line numbers to output for context
309
+ resultContent = selectedLines
310
+ .map((line, idx) => `${String(start + idx).padStart(5)}│ ${line}`)
311
+ .join('\n');
312
+ } else {
313
+ // No range specified - return full content but warn if large
314
+ const MAX_LINES_WITHOUT_RANGE = 500;
315
+ if (totalLines > MAX_LINES_WITHOUT_RANGE) {
316
+ // Return first portion with warning
317
+ const selectedLines = lines.slice(0, MAX_LINES_WITHOUT_RANGE);
318
+ resultContent = selectedLines
319
+ .map((line, idx) => `${String(idx + 1).padStart(5)}│ ${line}`)
320
+ .join('\n');
321
+ resultContent += `\n\n[... ${totalLines - MAX_LINES_WITHOUT_RANGE} more lines. Use startLine/endLine to read specific ranges.]`;
322
+ resultEndLine = MAX_LINES_WITHOUT_RANGE;
323
+ } else {
324
+ resultContent = contentStr;
325
+ }
326
+ }
327
+
214
328
  return {
215
329
  success: true,
216
330
  result: {
217
331
  path: params.path,
218
- content: content.toString(),
332
+ content: resultContent,
219
333
  encoding,
334
+ totalLines,
335
+ startLine: resultStartLine,
336
+ endLine: resultEndLine,
220
337
  },
221
338
  };
222
339
  }
@@ -259,7 +376,16 @@ export class FilesystemTool extends BaseToolWrapper {
259
376
  if (params.filter) {
260
377
  dir = dir.filter(params.filter as string);
261
378
  }
262
- const entries = await dir.list();
379
+ let entries = await dir.list();
380
+
381
+ // Filter out excluded paths
382
+ if (this.excludePatterns.length > 0) {
383
+ entries = entries.filter(entry => {
384
+ const relativePath = plugins.path.relative(validatedPath, entry.path);
385
+ return !this.isExcluded(relativePath) && !this.isExcluded(entry.name);
386
+ });
387
+ }
388
+
263
389
  return {
264
390
  success: true,
265
391
  result: {
@@ -364,6 +490,168 @@ export class FilesystemTool extends BaseToolWrapper {
364
490
  };
365
491
  }
366
492
 
493
+ case 'tree': {
494
+ const validatedPath = this.validatePath(params.path as string);
495
+ const maxDepth = (params.maxDepth as number) ?? 3;
496
+ const filter = params.filter as string | undefined;
497
+ const showSizes = (params.showSizes as boolean) ?? false;
498
+ const format = (params.format as 'string' | 'json') ?? 'string';
499
+
500
+ // Collect all entries recursively up to maxDepth
501
+ interface ITreeEntry {
502
+ path: string;
503
+ relativePath: string;
504
+ isDir: boolean;
505
+ depth: number;
506
+ size?: number;
507
+ }
508
+
509
+ const entries: ITreeEntry[] = [];
510
+
511
+ const collectEntries = async (dirPath: string, depth: number, relativePath: string) => {
512
+ if (depth > maxDepth) return;
513
+
514
+ let dir = this.smartfs.directory(dirPath);
515
+ if (filter) {
516
+ dir = dir.filter(filter);
517
+ }
518
+ const items = await dir.list();
519
+
520
+ for (const item of items) {
521
+ // item is IDirectoryEntry with name, path, isFile, isDirectory properties
522
+ const itemPath = item.path;
523
+ const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;
524
+ const isDir = item.isDirectory;
525
+
526
+ // Skip excluded paths
527
+ if (this.isExcluded(itemRelPath) || this.isExcluded(item.name)) {
528
+ continue;
529
+ }
530
+
531
+ const entry: ITreeEntry = {
532
+ path: itemPath,
533
+ relativePath: itemRelPath,
534
+ isDir,
535
+ depth,
536
+ };
537
+
538
+ if (showSizes && !isDir && item.stats) {
539
+ entry.size = item.stats.size;
540
+ }
541
+
542
+ entries.push(entry);
543
+
544
+ // Recurse into directories
545
+ if (isDir && depth < maxDepth) {
546
+ await collectEntries(itemPath, depth + 1, itemRelPath);
547
+ }
548
+ }
549
+ };
550
+
551
+ await collectEntries(validatedPath, 0, '');
552
+
553
+ // Sort entries by path for consistent output
554
+ entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
555
+
556
+ if (format === 'json') {
557
+ return {
558
+ success: true,
559
+ result: {
560
+ path: params.path,
561
+ entries: entries.map((e) => ({
562
+ path: e.relativePath,
563
+ isDir: e.isDir,
564
+ depth: e.depth,
565
+ ...(e.size !== undefined ? { size: e.size } : {}),
566
+ })),
567
+ count: entries.length,
568
+ },
569
+ };
570
+ }
571
+
572
+ // Format as string tree
573
+ const formatSize = (bytes: number): string => {
574
+ if (bytes < 1024) return `${bytes}B`;
575
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
576
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
577
+ };
578
+
579
+ // Build tree string with proper indentation
580
+ let treeStr = `${params.path}/\n`;
581
+ const pathParts = new Map<string, number>(); // Track which paths are last in their parent
582
+
583
+ // Group by parent to determine last child
584
+ const parentChildCount = new Map<string, number>();
585
+ const parentCurrentChild = new Map<string, number>();
586
+
587
+ for (const entry of entries) {
588
+ const parentPath = entry.relativePath.includes('/')
589
+ ? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
590
+ : '';
591
+ parentChildCount.set(parentPath, (parentChildCount.get(parentPath) || 0) + 1);
592
+ }
593
+
594
+ for (const entry of entries) {
595
+ const parentPath = entry.relativePath.includes('/')
596
+ ? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
597
+ : '';
598
+ parentCurrentChild.set(parentPath, (parentCurrentChild.get(parentPath) || 0) + 1);
599
+ const isLast = parentCurrentChild.get(parentPath) === parentChildCount.get(parentPath);
600
+
601
+ // Build prefix based on depth
602
+ let prefix = '';
603
+ const parts = entry.relativePath.split('/');
604
+ for (let i = 0; i < parts.length - 1; i++) {
605
+ prefix += '│ ';
606
+ }
607
+ prefix += isLast ? '└── ' : '├── ';
608
+
609
+ const name = parts[parts.length - 1];
610
+ const suffix = entry.isDir ? '/' : '';
611
+ const sizeStr = showSizes && entry.size !== undefined ? ` (${formatSize(entry.size)})` : '';
612
+
613
+ treeStr += `${prefix}${name}${suffix}${sizeStr}\n`;
614
+ }
615
+
616
+ return {
617
+ success: true,
618
+ result: {
619
+ path: params.path,
620
+ tree: treeStr,
621
+ count: entries.length,
622
+ },
623
+ };
624
+ }
625
+
626
+ case 'glob': {
627
+ const pattern = params.pattern as string;
628
+ const basePath = params.path ? this.validatePath(params.path as string) : (this.basePath || process.cwd());
629
+
630
+ // Use smartfs to list with filter
631
+ const dir = this.smartfs.directory(basePath).recursive().filter(pattern);
632
+ const matches = await dir.list();
633
+
634
+ // Return file paths relative to base path for readability
635
+ // Filter out excluded paths
636
+ const files = matches
637
+ .map((entry) => ({
638
+ path: entry.path,
639
+ relativePath: plugins.path.relative(basePath, entry.path),
640
+ isDirectory: entry.isDirectory,
641
+ }))
642
+ .filter((file) => !this.isExcluded(file.relativePath));
643
+
644
+ return {
645
+ success: true,
646
+ result: {
647
+ pattern,
648
+ basePath,
649
+ files,
650
+ count: files.length,
651
+ },
652
+ };
653
+ }
654
+
367
655
  default:
368
656
  return {
369
657
  success: false,
@@ -380,8 +668,12 @@ export class FilesystemTool extends BaseToolWrapper {
380
668
 
381
669
  public getCallSummary(action: string, params: Record<string, unknown>): string {
382
670
  switch (action) {
383
- case 'read':
384
- return `Read file "${params.path}" with encoding ${params.encoding || 'utf8'}`;
671
+ case 'read': {
672
+ const lineRange = params.startLine || params.endLine
673
+ ? ` lines ${params.startLine || 1}-${params.endLine || 'end'}`
674
+ : '';
675
+ return `Read file "${params.path}"${lineRange}`;
676
+ }
385
677
 
386
678
  case 'write': {
387
679
  const content = params.content as string;
@@ -416,6 +708,12 @@ export class FilesystemTool extends BaseToolWrapper {
416
708
  case 'mkdir':
417
709
  return `Create directory "${params.path}"${params.recursive !== false ? ' (with parents)' : ''}`;
418
710
 
711
+ case 'tree':
712
+ return `Show tree of "${params.path}" (depth: ${params.maxDepth ?? 3}, format: ${params.format ?? 'string'})`;
713
+
714
+ case 'glob':
715
+ return `Find files matching "${params.pattern}"${params.path ? ` in "${params.path}"` : ''}`;
716
+
419
717
  default:
420
718
  return `Unknown action: ${action}`;
421
719
  }