@push.rocks/smartagent 1.2.2 → 1.2.4

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
  // ================================
@@ -46,17 +46,25 @@ export class FilesystemTool extends BaseToolWrapper {
46
46
  public actions: interfaces.IToolAction[] = [
47
47
  {
48
48
  name: 'read',
49
- description: 'Read the contents of a file',
49
+ description: 'Read file contents (full or specific line range)',
50
50
  parameters: {
51
51
  type: 'object',
52
52
  properties: {
53
- path: { type: 'string', description: 'Absolute path to the file' },
53
+ path: { type: 'string', description: 'Path to the file' },
54
54
  encoding: {
55
55
  type: 'string',
56
56
  enum: ['utf8', 'binary', 'base64'],
57
57
  default: 'utf8',
58
58
  description: 'File encoding',
59
59
  },
60
+ startLine: {
61
+ type: 'number',
62
+ description: 'First line to read (1-indexed, inclusive). If omitted, reads from beginning.',
63
+ },
64
+ endLine: {
65
+ type: 'number',
66
+ description: 'Last line to read (1-indexed, inclusive). If omitted, reads to end.',
67
+ },
60
68
  },
61
69
  required: ['path'],
62
70
  },
@@ -182,6 +190,55 @@ export class FilesystemTool extends BaseToolWrapper {
182
190
  required: ['path'],
183
191
  },
184
192
  },
193
+ {
194
+ name: 'tree',
195
+ description: 'Show directory structure as a tree (no file contents)',
196
+ parameters: {
197
+ type: 'object',
198
+ properties: {
199
+ path: { type: 'string', description: 'Root directory path' },
200
+ maxDepth: {
201
+ type: 'number',
202
+ default: 3,
203
+ description: 'Maximum depth to traverse (default: 3)',
204
+ },
205
+ filter: {
206
+ type: 'string',
207
+ description: 'Glob pattern to filter files (e.g., "*.ts")',
208
+ },
209
+ showSizes: {
210
+ type: 'boolean',
211
+ default: false,
212
+ description: 'Include file sizes in output',
213
+ },
214
+ format: {
215
+ type: 'string',
216
+ enum: ['string', 'json'],
217
+ default: 'string',
218
+ description: 'Output format: "string" for human-readable tree, "json" for structured array',
219
+ },
220
+ },
221
+ required: ['path'],
222
+ },
223
+ },
224
+ {
225
+ name: 'glob',
226
+ description: 'Find files matching a glob pattern',
227
+ parameters: {
228
+ type: 'object',
229
+ properties: {
230
+ pattern: {
231
+ type: 'string',
232
+ description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.js")',
233
+ },
234
+ path: {
235
+ type: 'string',
236
+ description: 'Base path to search from (defaults to current directory)',
237
+ },
238
+ },
239
+ required: ['pattern'],
240
+ },
241
+ },
185
242
  ];
186
243
 
187
244
  private smartfs!: plugins.smartfs.SmartFs;
@@ -207,16 +264,61 @@ export class FilesystemTool extends BaseToolWrapper {
207
264
  case 'read': {
208
265
  const validatedPath = this.validatePath(params.path as string);
209
266
  const encoding = (params.encoding as string) || 'utf8';
210
- const content = await this.smartfs
267
+ const startLine = params.startLine as number | undefined;
268
+ const endLine = params.endLine as number | undefined;
269
+
270
+ const fullContent = await this.smartfs
211
271
  .file(validatedPath)
212
272
  .encoding(encoding as 'utf8' | 'binary' | 'base64')
213
273
  .read();
274
+
275
+ const contentStr = fullContent.toString();
276
+ const lines = contentStr.split('\n');
277
+ const totalLines = lines.length;
278
+
279
+ // Apply line range if specified
280
+ let resultContent: string;
281
+ let resultStartLine = 1;
282
+ let resultEndLine = totalLines;
283
+
284
+ if (startLine !== undefined || endLine !== undefined) {
285
+ const start = Math.max(1, startLine ?? 1);
286
+ const end = Math.min(totalLines, endLine ?? totalLines);
287
+ resultStartLine = start;
288
+ resultEndLine = end;
289
+
290
+ // Convert to 0-indexed for array slicing
291
+ const selectedLines = lines.slice(start - 1, end);
292
+
293
+ // Add line numbers to output for context
294
+ resultContent = selectedLines
295
+ .map((line, idx) => `${String(start + idx).padStart(5)}│ ${line}`)
296
+ .join('\n');
297
+ } else {
298
+ // No range specified - return full content but warn if large
299
+ const MAX_LINES_WITHOUT_RANGE = 500;
300
+ if (totalLines > MAX_LINES_WITHOUT_RANGE) {
301
+ // Return first portion with warning
302
+ const selectedLines = lines.slice(0, MAX_LINES_WITHOUT_RANGE);
303
+ resultContent = selectedLines
304
+ .map((line, idx) => `${String(idx + 1).padStart(5)}│ ${line}`)
305
+ .join('\n');
306
+ resultContent += `\n\n[... ${totalLines - MAX_LINES_WITHOUT_RANGE} more lines. Use startLine/endLine to read specific ranges.]`;
307
+ resultEndLine = MAX_LINES_WITHOUT_RANGE;
308
+ } else {
309
+ resultContent = contentStr;
310
+ }
311
+ }
312
+
214
313
  return {
215
314
  success: true,
216
315
  result: {
217
316
  path: params.path,
218
- content: content.toString(),
317
+ content: resultContent,
219
318
  encoding,
319
+ totalLines,
320
+ startLine: resultStartLine,
321
+ endLine: resultEndLine,
220
322
  },
221
323
  };
222
324
  }
@@ -364,6 +466,160 @@ export class FilesystemTool extends BaseToolWrapper {
364
466
  };
365
467
  }
366
468
 
469
+ case 'tree': {
470
+ const validatedPath = this.validatePath(params.path as string);
471
+ const maxDepth = (params.maxDepth as number) ?? 3;
472
+ const filter = params.filter as string | undefined;
473
+ const showSizes = (params.showSizes as boolean) ?? false;
474
+ const format = (params.format as 'string' | 'json') ?? 'string';
475
+
476
+ // Collect all entries recursively up to maxDepth
477
+ interface ITreeEntry {
478
+ path: string;
479
+ relativePath: string;
480
+ isDir: boolean;
481
+ depth: number;
482
+ size?: number;
483
+ }
484
+
485
+ const entries: ITreeEntry[] = [];
486
+
487
+ const collectEntries = async (dirPath: string, depth: number, relativePath: string) => {
488
+ if (depth > maxDepth) return;
489
+
490
+ let dir = this.smartfs.directory(dirPath);
491
+ if (filter) {
492
+ dir = dir.filter(filter);
493
+ }
494
+ const items = await dir.list();
495
+
496
+ for (const item of items) {
497
+ // item is IDirectoryEntry with name, path, isFile, isDirectory properties
498
+ const itemPath = item.path;
499
+ const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;
500
+ const isDir = item.isDirectory;
501
+
502
+ const entry: ITreeEntry = {
503
+ path: itemPath,
504
+ relativePath: itemRelPath,
505
+ isDir,
506
+ depth,
507
+ };
508
+
509
+ if (showSizes && !isDir && item.stats) {
510
+ entry.size = item.stats.size;
511
+ }
512
+
513
+ entries.push(entry);
514
+
515
+ // Recurse into directories
516
+ if (isDir && depth < maxDepth) {
517
+ await collectEntries(itemPath, depth + 1, itemRelPath);
518
+ }
519
+ }
520
+ };
521
+
522
+ await collectEntries(validatedPath, 0, '');
523
+
524
+ // Sort entries by path for consistent output
525
+ entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
526
+
527
+ if (format === 'json') {
528
+ return {
529
+ success: true,
530
+ result: {
531
+ path: params.path,
532
+ entries: entries.map((e) => ({
533
+ path: e.relativePath,
534
+ isDir: e.isDir,
535
+ depth: e.depth,
536
+ ...(e.size !== undefined ? { size: e.size } : {}),
537
+ })),
538
+ count: entries.length,
539
+ },
540
+ };
541
+ }
542
+
543
+ // Format as string tree
544
+ const formatSize = (bytes: number): string => {
545
+ if (bytes < 1024) return `${bytes}B`;
546
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
547
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
548
+ };
549
+
550
+ // Build tree string with proper indentation
551
+ let treeStr = `${params.path}/\n`;
552
+ const pathParts = new Map<string, number>(); // Track which paths are last in their parent
553
+
554
+ // Group by parent to determine last child
555
+ const parentChildCount = new Map<string, number>();
556
+ const parentCurrentChild = new Map<string, number>();
557
+
558
+ for (const entry of entries) {
559
+ const parentPath = entry.relativePath.includes('/')
560
+ ? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
561
+ : '';
562
+ parentChildCount.set(parentPath, (parentChildCount.get(parentPath) || 0) + 1);
563
+ }
564
+
565
+ for (const entry of entries) {
566
+ const parentPath = entry.relativePath.includes('/')
567
+ ? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
568
+ : '';
569
+ parentCurrentChild.set(parentPath, (parentCurrentChild.get(parentPath) || 0) + 1);
570
+ const isLast = parentCurrentChild.get(parentPath) === parentChildCount.get(parentPath);
571
+
572
+ // Build prefix based on depth
573
+ let prefix = '';
574
+ const parts = entry.relativePath.split('/');
575
+ for (let i = 0; i < parts.length - 1; i++) {
576
+ prefix += '│ ';
577
+ }
578
+ prefix += isLast ? '└── ' : '├── ';
579
+
580
+ const name = parts[parts.length - 1];
581
+ const suffix = entry.isDir ? '/' : '';
582
+ const sizeStr = showSizes && entry.size !== undefined ? ` (${formatSize(entry.size)})` : '';
583
+
584
+ treeStr += `${prefix}${name}${suffix}${sizeStr}\n`;
585
+ }
586
+
587
+ return {
588
+ success: true,
589
+ result: {
590
+ path: params.path,
591
+ tree: treeStr,
592
+ count: entries.length,
593
+ },
594
+ };
595
+ }
596
+
597
+ case 'glob': {
598
+ const pattern = params.pattern as string;
599
+ const basePath = params.path ? this.validatePath(params.path as string) : (this.basePath || process.cwd());
600
+
601
+ // Use smartfs to list with filter
602
+ const dir = this.smartfs.directory(basePath).recursive().filter(pattern);
603
+ const matches = await dir.list();
604
+
605
+ // Return file paths relative to base path for readability
606
+ const files = matches.map((entry) => ({
607
+ path: entry.path,
608
+ relativePath: plugins.path.relative(basePath, entry.path),
609
+ isDirectory: entry.isDirectory,
610
+ }));
611
+
612
+ return {
613
+ success: true,
614
+ result: {
615
+ pattern,
616
+ basePath,
617
+ files,
618
+ count: files.length,
619
+ },
620
+ };
621
+ }
622
+
367
623
  default:
368
624
  return {
369
625
  success: false,
@@ -380,8 +636,12 @@ export class FilesystemTool extends BaseToolWrapper {
380
636
 
381
637
  public getCallSummary(action: string, params: Record<string, unknown>): string {
382
638
  switch (action) {
383
- case 'read':
384
- return `Read file "${params.path}" with encoding ${params.encoding || 'utf8'}`;
639
+ case 'read': {
640
+ const lineRange = params.startLine || params.endLine
641
+ ? ` lines ${params.startLine || 1}-${params.endLine || 'end'}`
642
+ : '';
643
+ return `Read file "${params.path}"${lineRange}`;
644
+ }
385
645
 
386
646
  case 'write': {
387
647
  const content = params.content as string;
@@ -416,6 +676,12 @@ export class FilesystemTool extends BaseToolWrapper {
416
676
  case 'mkdir':
417
677
  return `Create directory "${params.path}"${params.recursive !== false ? ' (with parents)' : ''}`;
418
678
 
679
+ case 'tree':
680
+ return `Show tree of "${params.path}" (depth: ${params.maxDepth ?? 3}, format: ${params.format ?? 'string'})`;
681
+
682
+ case 'glob':
683
+ return `Find files matching "${params.pattern}"${params.path ? ` in "${params.path}"` : ''}`;
684
+
419
685
  default:
420
686
  return `Unknown action: ${action}`;
421
687
  }