vigthoria-cli 1.0.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.
Files changed (75) hide show
  1. package/README.md +413 -0
  2. package/dist/commands/auth.d.ts +24 -0
  3. package/dist/commands/auth.d.ts.map +1 -0
  4. package/dist/commands/auth.js +194 -0
  5. package/dist/commands/auth.js.map +1 -0
  6. package/dist/commands/chat.d.ts +64 -0
  7. package/dist/commands/chat.d.ts.map +1 -0
  8. package/dist/commands/chat.js +596 -0
  9. package/dist/commands/chat.js.map +1 -0
  10. package/dist/commands/config.d.ts +25 -0
  11. package/dist/commands/config.d.ts.map +1 -0
  12. package/dist/commands/config.js +291 -0
  13. package/dist/commands/config.js.map +1 -0
  14. package/dist/commands/edit.d.ts +28 -0
  15. package/dist/commands/edit.d.ts.map +1 -0
  16. package/dist/commands/edit.js +257 -0
  17. package/dist/commands/edit.js.map +1 -0
  18. package/dist/commands/explain.d.ts +21 -0
  19. package/dist/commands/explain.d.ts.map +1 -0
  20. package/dist/commands/explain.js +98 -0
  21. package/dist/commands/explain.js.map +1 -0
  22. package/dist/commands/generate.d.ts +25 -0
  23. package/dist/commands/generate.d.ts.map +1 -0
  24. package/dist/commands/generate.js +155 -0
  25. package/dist/commands/generate.js.map +1 -0
  26. package/dist/commands/review.d.ts +24 -0
  27. package/dist/commands/review.d.ts.map +1 -0
  28. package/dist/commands/review.js +153 -0
  29. package/dist/commands/review.js.map +1 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +205 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/utils/api.d.ts +88 -0
  35. package/dist/utils/api.d.ts.map +1 -0
  36. package/dist/utils/api.js +431 -0
  37. package/dist/utils/api.js.map +1 -0
  38. package/dist/utils/config.d.ts +57 -0
  39. package/dist/utils/config.d.ts.map +1 -0
  40. package/dist/utils/config.js +167 -0
  41. package/dist/utils/config.js.map +1 -0
  42. package/dist/utils/files.d.ts +31 -0
  43. package/dist/utils/files.d.ts.map +1 -0
  44. package/dist/utils/files.js +217 -0
  45. package/dist/utils/files.js.map +1 -0
  46. package/dist/utils/logger.d.ts +23 -0
  47. package/dist/utils/logger.d.ts.map +1 -0
  48. package/dist/utils/logger.js +104 -0
  49. package/dist/utils/logger.js.map +1 -0
  50. package/dist/utils/session.d.ts +61 -0
  51. package/dist/utils/session.d.ts.map +1 -0
  52. package/dist/utils/session.js +172 -0
  53. package/dist/utils/session.js.map +1 -0
  54. package/dist/utils/tools.d.ts +145 -0
  55. package/dist/utils/tools.d.ts.map +1 -0
  56. package/dist/utils/tools.js +781 -0
  57. package/dist/utils/tools.js.map +1 -0
  58. package/install.sh +248 -0
  59. package/package.json +52 -0
  60. package/src/commands/auth.ts +225 -0
  61. package/src/commands/chat.ts +690 -0
  62. package/src/commands/config.ts +297 -0
  63. package/src/commands/edit.ts +310 -0
  64. package/src/commands/explain.ts +115 -0
  65. package/src/commands/generate.ts +177 -0
  66. package/src/commands/review.ts +186 -0
  67. package/src/index.ts +221 -0
  68. package/src/types/marked-terminal.d.ts +31 -0
  69. package/src/utils/api.ts +531 -0
  70. package/src/utils/config.ts +224 -0
  71. package/src/utils/files.ts +212 -0
  72. package/src/utils/logger.ts +125 -0
  73. package/src/utils/session.ts +167 -0
  74. package/src/utils/tools.ts +933 -0
  75. package/tsconfig.json +20 -0
@@ -0,0 +1,933 @@
1
+ /**
2
+ * Vigthoria CLI Tools - Agentic Tool System
3
+ *
4
+ * This module provides Claude Code-like autonomous tool execution.
5
+ * Tools can be called by the AI to perform actions.
6
+ *
7
+ * Enhanced with:
8
+ * - Risk-based permission system
9
+ * - Automatic retry logic with exponential backoff
10
+ * - Undo functionality for file operations
11
+ * - Detailed error messages with suggestions
12
+ *
13
+ * @version 1.1.0
14
+ * @author Vigthoria Labs
15
+ */
16
+
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import { execSync, spawn } from 'child_process';
20
+ import chalk from 'chalk';
21
+ import { Logger } from './logger.js';
22
+
23
+ // Risk levels for operations
24
+ export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
25
+
26
+ export interface ToolResult {
27
+ success: boolean;
28
+ output?: string;
29
+ error?: string;
30
+ suggestion?: string; // Actionable suggestion for errors
31
+ canRetry?: boolean; // Whether operation can be retried
32
+ undoable?: boolean; // Whether operation can be undone
33
+ metadata?: { // Additional tool-specific data
34
+ [key: string]: any;
35
+ };
36
+ }
37
+
38
+ export interface ToolCall {
39
+ tool: string;
40
+ args: Record<string, string>;
41
+ }
42
+
43
+ export interface ToolDefinition {
44
+ name: string;
45
+ description: string;
46
+ parameters: {
47
+ name: string;
48
+ description: string;
49
+ required: boolean;
50
+ }[];
51
+ requiresPermission: boolean;
52
+ dangerous: boolean;
53
+ riskLevel: RiskLevel; // NEW: Risk classification
54
+ category: 'read' | 'write' | 'execute' | 'search'; // NEW: Tool category
55
+ }
56
+
57
+ // Undo operation tracking
58
+ interface UndoOperation {
59
+ id: string;
60
+ tool: string;
61
+ timestamp: number;
62
+ filePath?: string;
63
+ originalContent?: string | null; // null means file was created (delete on undo)
64
+ description: string;
65
+ }
66
+
67
+ // Error types for better handling
68
+ export enum ToolErrorType {
69
+ FILE_NOT_FOUND = 'FILE_NOT_FOUND',
70
+ PERMISSION_DENIED = 'PERMISSION_DENIED',
71
+ NETWORK_ERROR = 'NETWORK_ERROR',
72
+ TIMEOUT = 'TIMEOUT',
73
+ INVALID_ARGS = 'INVALID_ARGS',
74
+ EXECUTION_FAILED = 'EXECUTION_FAILED',
75
+ USER_CANCELLED = 'USER_CANCELLED',
76
+ }
77
+
78
+ export class AgenticTools {
79
+ private logger: Logger;
80
+ private cwd: string;
81
+ private permissionCallback: (action: string) => Promise<boolean>;
82
+ private autoApprove: boolean;
83
+ private undoStack: UndoOperation[] = [];
84
+ private maxUndoStack: number = 50;
85
+ private retryConfig = {
86
+ maxRetries: 3,
87
+ baseDelayMs: 1000,
88
+ maxDelayMs: 10000,
89
+ retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN'],
90
+ };
91
+
92
+ constructor(
93
+ logger: Logger,
94
+ cwd: string,
95
+ permissionCallback: (action: string) => Promise<boolean>,
96
+ autoApprove: boolean = false
97
+ ) {
98
+ this.logger = logger;
99
+ this.cwd = cwd;
100
+ this.permissionCallback = permissionCallback;
101
+ this.autoApprove = autoApprove;
102
+ }
103
+
104
+ /**
105
+ * Get the undo stack for inspection
106
+ */
107
+ getUndoStack(): UndoOperation[] {
108
+ return [...this.undoStack];
109
+ }
110
+
111
+ /**
112
+ * Undo the last file operation
113
+ */
114
+ async undo(): Promise<ToolResult> {
115
+ const lastOp = this.undoStack.pop();
116
+
117
+ if (!lastOp) {
118
+ return {
119
+ success: false,
120
+ error: 'Nothing to undo',
121
+ suggestion: 'The undo stack is empty. Only file write/edit operations can be undone.',
122
+ };
123
+ }
124
+
125
+ if (lastOp.filePath) {
126
+ try {
127
+ if (lastOp.originalContent === null) {
128
+ // File was created, so delete it
129
+ if (fs.existsSync(lastOp.filePath)) {
130
+ fs.unlinkSync(lastOp.filePath);
131
+ return {
132
+ success: true,
133
+ output: `✓ Undone: ${lastOp.description}\n File deleted: ${lastOp.filePath}`,
134
+ metadata: { remainingUndos: this.undoStack.length },
135
+ };
136
+ }
137
+ } else if (lastOp.originalContent !== undefined) {
138
+ // Restore original content
139
+ fs.writeFileSync(lastOp.filePath, lastOp.originalContent, 'utf-8');
140
+ return {
141
+ success: true,
142
+ output: `✓ Undone: ${lastOp.description}\n File restored: ${lastOp.filePath}`,
143
+ metadata: { remainingUndos: this.undoStack.length },
144
+ };
145
+ }
146
+ } catch (error) {
147
+ return {
148
+ success: false,
149
+ error: `Failed to undo: ${(error as Error).message}`,
150
+ suggestion: 'The file may have been moved or permissions changed.',
151
+ };
152
+ }
153
+ }
154
+
155
+ return {
156
+ success: false,
157
+ error: 'This operation cannot be undone',
158
+ };
159
+ }
160
+
161
+ /**
162
+ * List of available tools - similar to Claude Code
163
+ * Enhanced with risk levels and categories
164
+ */
165
+ static getToolDefinitions(): ToolDefinition[] {
166
+ return [
167
+ {
168
+ name: 'read_file',
169
+ description: 'Read the contents of a file',
170
+ parameters: [
171
+ { name: 'path', description: 'File path (relative or absolute)', required: true },
172
+ { name: 'start_line', description: 'Start line (1-indexed)', required: false },
173
+ { name: 'end_line', description: 'End line (1-indexed)', required: false },
174
+ ],
175
+ requiresPermission: false,
176
+ dangerous: false,
177
+ riskLevel: 'low',
178
+ category: 'read',
179
+ },
180
+ {
181
+ name: 'write_file',
182
+ description: 'Write content to a file (creates if not exists)',
183
+ parameters: [
184
+ { name: 'path', description: 'File path', required: true },
185
+ { name: 'content', description: 'Content to write', required: true },
186
+ ],
187
+ requiresPermission: true,
188
+ dangerous: false,
189
+ riskLevel: 'medium',
190
+ category: 'write',
191
+ },
192
+ {
193
+ name: 'edit_file',
194
+ description: 'Make specific edits to a file by replacing text',
195
+ parameters: [
196
+ { name: 'path', description: 'File path', required: true },
197
+ { name: 'old_text', description: 'Text to replace', required: true },
198
+ { name: 'new_text', description: 'New text', required: true },
199
+ ],
200
+ requiresPermission: true,
201
+ dangerous: false,
202
+ riskLevel: 'medium',
203
+ category: 'write',
204
+ },
205
+ {
206
+ name: 'bash',
207
+ description: 'Run a shell command',
208
+ parameters: [
209
+ { name: 'command', description: 'Shell command to execute', required: true },
210
+ { name: 'cwd', description: 'Working directory', required: false },
211
+ { name: 'timeout', description: 'Timeout in seconds', required: false },
212
+ ],
213
+ requiresPermission: true,
214
+ dangerous: true,
215
+ riskLevel: 'high',
216
+ category: 'execute',
217
+ },
218
+ {
219
+ name: 'grep',
220
+ description: 'Search for patterns in files',
221
+ parameters: [
222
+ { name: 'pattern', description: 'Search pattern (regex)', required: true },
223
+ { name: 'path', description: 'File or directory path', required: false },
224
+ { name: 'include', description: 'File pattern to include (e.g., *.ts)', required: false },
225
+ ],
226
+ requiresPermission: false,
227
+ dangerous: false,
228
+ riskLevel: 'low',
229
+ category: 'search',
230
+ },
231
+ {
232
+ name: 'list_dir',
233
+ description: 'List contents of a directory',
234
+ parameters: [
235
+ { name: 'path', description: 'Directory path', required: true },
236
+ { name: 'recursive', description: 'List recursively', required: false },
237
+ ],
238
+ requiresPermission: false,
239
+ dangerous: false,
240
+ riskLevel: 'low',
241
+ category: 'read',
242
+ },
243
+ {
244
+ name: 'glob',
245
+ description: 'Find files matching a pattern',
246
+ parameters: [
247
+ { name: 'pattern', description: 'Glob pattern (e.g., **/*.ts)', required: true },
248
+ ],
249
+ requiresPermission: false,
250
+ dangerous: false,
251
+ riskLevel: 'low',
252
+ category: 'search',
253
+ },
254
+ {
255
+ name: 'git',
256
+ description: 'Run git commands',
257
+ parameters: [
258
+ { name: 'args', description: 'Git command arguments', required: true },
259
+ ],
260
+ requiresPermission: true,
261
+ dangerous: false,
262
+ riskLevel: 'medium',
263
+ category: 'execute',
264
+ },
265
+ ];
266
+ }
267
+
268
+ /**
269
+ * Execute a tool call with enhanced error handling and retry logic
270
+ */
271
+ async execute(call: ToolCall): Promise<ToolResult> {
272
+ const tool = AgenticTools.getToolDefinitions().find(t => t.name === call.tool);
273
+
274
+ if (!tool) {
275
+ return this.createErrorResult(
276
+ ToolErrorType.INVALID_ARGS,
277
+ `Unknown tool: ${call.tool}`,
278
+ `Available tools: ${AgenticTools.getToolDefinitions().map(t => t.name).join(', ')}`
279
+ );
280
+ }
281
+
282
+ // Validate required parameters
283
+ const validationError = this.validateParameters(call, tool);
284
+ if (validationError) {
285
+ return validationError;
286
+ }
287
+
288
+ // Check permission for dangerous/modifying actions
289
+ if (tool.requiresPermission && !this.autoApprove) {
290
+ const approved = await this.permissionCallback(
291
+ this.formatPermissionRequest(call, tool)
292
+ );
293
+
294
+ if (!approved) {
295
+ return {
296
+ success: false,
297
+ error: 'Permission denied by user',
298
+ canRetry: true,
299
+ };
300
+ }
301
+ }
302
+
303
+ // Execute with retry logic for applicable operations
304
+ return this.executeWithRetry(call, tool);
305
+ }
306
+
307
+ /**
308
+ * Execute tool with automatic retry for transient failures
309
+ */
310
+ private async executeWithRetry(call: ToolCall, tool: ToolDefinition): Promise<ToolResult> {
311
+ let lastError: ToolResult | null = null;
312
+
313
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
314
+ try {
315
+ const result = await this.executeTool(call);
316
+
317
+ if (result.success) {
318
+ return result;
319
+ }
320
+
321
+ // Check if error is retryable
322
+ const isRetryable = result.canRetry !== false &&
323
+ this.isRetryableError(result.error || '');
324
+
325
+ if (!isRetryable || attempt === this.retryConfig.maxRetries) {
326
+ return result;
327
+ }
328
+
329
+ lastError = result;
330
+
331
+ // Calculate delay with exponential backoff
332
+ const delay = Math.min(
333
+ this.retryConfig.baseDelayMs * Math.pow(2, attempt),
334
+ this.retryConfig.maxDelayMs
335
+ );
336
+
337
+ this.logger.warn(`Retrying ${call.tool} in ${delay}ms (attempt ${attempt + 2}/${this.retryConfig.maxRetries + 1})...`);
338
+ await this.sleep(delay);
339
+
340
+ } catch (error) {
341
+ lastError = this.createErrorResult(
342
+ ToolErrorType.EXECUTION_FAILED,
343
+ (error as Error).message
344
+ );
345
+
346
+ if (attempt === this.retryConfig.maxRetries) {
347
+ return lastError;
348
+ }
349
+ }
350
+ }
351
+
352
+ return lastError || this.createErrorResult(
353
+ ToolErrorType.EXECUTION_FAILED,
354
+ 'Unknown error after retries'
355
+ );
356
+ }
357
+
358
+ /**
359
+ * Execute the actual tool operation
360
+ */
361
+ private async executeTool(call: ToolCall): Promise<ToolResult> {
362
+ switch (call.tool) {
363
+ case 'read_file':
364
+ return this.readFile(call.args);
365
+ case 'write_file':
366
+ return this.writeFile(call.args);
367
+ case 'edit_file':
368
+ return this.editFile(call.args);
369
+ case 'bash':
370
+ return this.bash(call.args);
371
+ case 'grep':
372
+ return this.grep(call.args);
373
+ case 'list_dir':
374
+ return this.listDir(call.args);
375
+ case 'glob':
376
+ return this.glob(call.args);
377
+ case 'git':
378
+ return this.git(call.args);
379
+ default:
380
+ return this.createErrorResult(
381
+ ToolErrorType.INVALID_ARGS,
382
+ `Tool not implemented: ${call.tool}`
383
+ );
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Check if an error is retryable
389
+ */
390
+ private isRetryableError(error: string): boolean {
391
+ return this.retryConfig.retryableErrors.some(e => error.includes(e));
392
+ }
393
+
394
+ /**
395
+ * Validate tool parameters
396
+ */
397
+ private validateParameters(call: ToolCall, tool: ToolDefinition): ToolResult | null {
398
+ for (const param of tool.parameters) {
399
+ if (param.required && !call.args[param.name]) {
400
+ return this.createErrorResult(
401
+ ToolErrorType.INVALID_ARGS,
402
+ `Missing required parameter: ${param.name}`,
403
+ `The ${call.tool} tool requires the '${param.name}' parameter. ${param.description}`
404
+ );
405
+ }
406
+ }
407
+ return null;
408
+ }
409
+
410
+ /**
411
+ * Create a standardized error result
412
+ */
413
+ private createErrorResult(
414
+ type: ToolErrorType,
415
+ message: string,
416
+ suggestion?: string
417
+ ): ToolResult {
418
+ const suggestions: Record<ToolErrorType, string> = {
419
+ [ToolErrorType.FILE_NOT_FOUND]: 'Check if the file path is correct. Use list_dir to see available files.',
420
+ [ToolErrorType.PERMISSION_DENIED]: 'You may need elevated permissions. Try using sudo or check file ownership.',
421
+ [ToolErrorType.NETWORK_ERROR]: 'Check your internet connection. The operation will retry automatically.',
422
+ [ToolErrorType.TIMEOUT]: 'The operation took too long. Try with a shorter timeout or simpler command.',
423
+ [ToolErrorType.INVALID_ARGS]: 'Check the tool documentation for correct parameter usage.',
424
+ [ToolErrorType.EXECUTION_FAILED]: 'Review the command syntax and try again.',
425
+ [ToolErrorType.USER_CANCELLED]: 'Operation cancelled. You can retry if needed.',
426
+ };
427
+
428
+ return {
429
+ success: false,
430
+ error: message,
431
+ suggestion: suggestion || suggestions[type],
432
+ canRetry: type !== ToolErrorType.USER_CANCELLED && type !== ToolErrorType.INVALID_ARGS,
433
+ };
434
+ }
435
+
436
+ /**
437
+ * Enhanced permission request with risk visualization
438
+ */
439
+ private formatPermissionRequest(call: ToolCall, tool: ToolDefinition): string {
440
+ const riskColors: Record<RiskLevel, (text: string) => string> = {
441
+ low: chalk.green,
442
+ medium: chalk.yellow,
443
+ high: chalk.red,
444
+ critical: chalk.bgRed.white,
445
+ };
446
+
447
+ const riskIcons: Record<RiskLevel, string> = {
448
+ low: '🟢',
449
+ medium: '🟡',
450
+ high: '🔴',
451
+ critical: '⛔',
452
+ };
453
+
454
+ const riskColor = riskColors[tool.riskLevel];
455
+ const riskIcon = riskIcons[tool.riskLevel];
456
+
457
+ let msg = `\n${riskIcon} ${riskColor(`${tool.riskLevel.toUpperCase()} RISK`)} - AI wants to use ${chalk.cyan.bold(call.tool)}\n`;
458
+ msg += chalk.gray('─'.repeat(50)) + '\n';
459
+
460
+ // Tool-specific details
461
+ if (call.tool === 'bash') {
462
+ msg += `${chalk.gray('├─')} ${chalk.white('Command:')} ${chalk.yellow(call.args.command)}\n`;
463
+ if (call.args.cwd) {
464
+ msg += `${chalk.gray('├─')} ${chalk.white('Directory:')} ${call.args.cwd}\n`;
465
+ }
466
+ msg += `${chalk.gray('├─')} ${chalk.white('Timeout:')} ${call.args.timeout || '30'}s\n`;
467
+ } else if (call.tool === 'write_file') {
468
+ msg += `${chalk.gray('├─')} ${chalk.white('File:')} ${chalk.cyan(call.args.path)}\n`;
469
+ const preview = call.args.content.substring(0, 100);
470
+ msg += `${chalk.gray('├─')} ${chalk.white('Content preview:')} ${chalk.gray(preview)}${call.args.content.length > 100 ? '...' : ''}\n`;
471
+ msg += `${chalk.gray('├─')} ${chalk.white('Size:')} ${call.args.content.length} bytes\n`;
472
+ } else if (call.tool === 'edit_file') {
473
+ msg += `${chalk.gray('├─')} ${chalk.white('File:')} ${chalk.cyan(call.args.path)}\n`;
474
+ msg += `${chalk.gray('├─')} ${chalk.white('Replace:')} ${chalk.red(call.args.old_text.substring(0, 50))}${call.args.old_text.length > 50 ? '...' : ''}\n`;
475
+ msg += `${chalk.gray('├─')} ${chalk.white('With:')} ${chalk.green(call.args.new_text.substring(0, 50))}${call.args.new_text.length > 50 ? '...' : ''}\n`;
476
+ } else if (call.tool === 'git') {
477
+ msg += `${chalk.gray('├─')} ${chalk.white('Command:')} git ${chalk.yellow(call.args.args)}\n`;
478
+ }
479
+
480
+ // Safety info
481
+ msg += chalk.gray('─'.repeat(50)) + '\n';
482
+
483
+ const canUndo = ['write_file', 'edit_file'].includes(call.tool);
484
+ msg += `${chalk.gray('├─')} ${chalk.white('Undo:')} ${canUndo ? chalk.green('✓ Available (use /undo)') : chalk.gray('✗ Not available')}\n`;
485
+ msg += `${chalk.gray('└─')} ${chalk.white('Category:')} ${tool.category}\n`;
486
+
487
+ if (tool.dangerous) {
488
+ msg += `\n${chalk.red.bold('⚠️ WARNING: This is a potentially dangerous action!')}\n`;
489
+ msg += chalk.red(' The AI is requesting to execute commands on your system.\n');
490
+ }
491
+
492
+ return msg;
493
+ }
494
+
495
+ private sleep(ms: number): Promise<void> {
496
+ return new Promise(resolve => setTimeout(resolve, ms));
497
+ }
498
+
499
+ // Tool implementations with enhanced error handling and undo support
500
+
501
+ /**
502
+ * Read file with enhanced error handling and suggestions
503
+ */
504
+ private readFile(args: Record<string, string>): ToolResult {
505
+ const filePath = this.resolvePath(args.path);
506
+
507
+ if (!fs.existsSync(filePath)) {
508
+ // Try to find similar files
509
+ const dir = path.dirname(filePath);
510
+ const basename = path.basename(filePath);
511
+ let suggestions: string[] = [];
512
+
513
+ if (fs.existsSync(dir)) {
514
+ const files = fs.readdirSync(dir);
515
+ suggestions = files
516
+ .filter(f => f.toLowerCase().includes(basename.toLowerCase().slice(0, 3)))
517
+ .slice(0, 3);
518
+ }
519
+
520
+ return this.createErrorResult(
521
+ ToolErrorType.FILE_NOT_FOUND,
522
+ `File not found: ${args.path}`,
523
+ suggestions.length > 0
524
+ ? `Did you mean one of these? ${suggestions.join(', ')}`
525
+ : 'Use list_dir to see available files in the directory.'
526
+ );
527
+ }
528
+
529
+ try {
530
+ const stats = fs.statSync(filePath);
531
+ if (stats.size > 1024 * 1024) { // 1MB warning
532
+ this.logger.warn(`Large file (${(stats.size / 1024 / 1024).toFixed(2)}MB) - consider using start_line/end_line`);
533
+ }
534
+
535
+ const content = fs.readFileSync(filePath, 'utf-8');
536
+ const lines = content.split('\n');
537
+
538
+ const startLine = args.start_line ? parseInt(args.start_line) - 1 : 0;
539
+ const endLine = args.end_line ? parseInt(args.end_line) : lines.length;
540
+
541
+ // Validate line numbers
542
+ if (startLine < 0 || startLine >= lines.length) {
543
+ return this.createErrorResult(
544
+ ToolErrorType.INVALID_ARGS,
545
+ `Invalid start_line: ${args.start_line}. File has ${lines.length} lines.`
546
+ );
547
+ }
548
+
549
+ const selectedLines = lines.slice(startLine, Math.min(endLine, lines.length));
550
+
551
+ return {
552
+ success: true,
553
+ output: selectedLines.join('\n'),
554
+ metadata: {
555
+ totalLines: lines.length,
556
+ linesReturned: selectedLines.length,
557
+ filePath: args.path,
558
+ },
559
+ };
560
+ } catch (error: any) {
561
+ if (error.code === 'EACCES') {
562
+ return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Permission denied: ${args.path}`);
563
+ }
564
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, error.message);
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Write file with undo support
570
+ */
571
+ private writeFile(args: Record<string, string>): ToolResult {
572
+ const filePath = this.resolvePath(args.path);
573
+ const dir = path.dirname(filePath);
574
+
575
+ // Save original content for undo if file exists
576
+ let originalContent: string | null = null;
577
+ if (fs.existsSync(filePath)) {
578
+ originalContent = fs.readFileSync(filePath, 'utf-8');
579
+ }
580
+
581
+ try {
582
+ // Create directory if needed
583
+ if (!fs.existsSync(dir)) {
584
+ fs.mkdirSync(dir, { recursive: true });
585
+ }
586
+
587
+ fs.writeFileSync(filePath, args.content, 'utf-8');
588
+
589
+ // Store undo operation
590
+ const undoOp: UndoOperation = {
591
+ id: `undo-${Date.now()}`,
592
+ tool: 'write_file',
593
+ timestamp: Date.now(),
594
+ filePath: filePath,
595
+ originalContent: originalContent,
596
+ description: originalContent
597
+ ? `Restore ${args.path} to previous version`
598
+ : `Delete ${args.path} (was created)`,
599
+ };
600
+ this.undoStack.push(undoOp);
601
+
602
+ // Limit undo stack size
603
+ if (this.undoStack.length > 50) {
604
+ this.undoStack.shift();
605
+ }
606
+
607
+ const isNew = originalContent === null;
608
+ return {
609
+ success: true,
610
+ output: `File ${isNew ? 'created' : 'updated'}: ${args.path}`,
611
+ metadata: {
612
+ bytes: args.content.length,
613
+ isNew,
614
+ canUndo: true,
615
+ },
616
+ };
617
+ } catch (error: any) {
618
+ if (error.code === 'EACCES') {
619
+ return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Permission denied writing to: ${args.path}`);
620
+ }
621
+ if (error.code === 'ENOSPC') {
622
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, 'No space left on device');
623
+ }
624
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, error.message);
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Edit file with undo support and helpful error messages
630
+ */
631
+ private editFile(args: Record<string, string>): ToolResult {
632
+ const filePath = this.resolvePath(args.path);
633
+
634
+ if (!fs.existsSync(filePath)) {
635
+ return this.createErrorResult(
636
+ ToolErrorType.FILE_NOT_FOUND,
637
+ `File not found: ${args.path}`,
638
+ 'Use write_file to create a new file, or check the path.'
639
+ );
640
+ }
641
+
642
+ try {
643
+ let content = fs.readFileSync(filePath, 'utf-8');
644
+ const originalContent = content;
645
+
646
+ if (!content.includes(args.old_text)) {
647
+ // Try to help identify the issue
648
+ const lines = content.split('\n');
649
+ const searchTerms = args.old_text.split('\n')[0].trim().slice(0, 30);
650
+ const possibleMatches = lines
651
+ .map((line, i) => ({ line: i + 1, content: line }))
652
+ .filter(l => l.content.includes(searchTerms.slice(0, 10)))
653
+ .slice(0, 3);
654
+
655
+ let suggestion = 'The exact text was not found. ';
656
+ if (possibleMatches.length > 0) {
657
+ suggestion += `Similar content found at lines: ${possibleMatches.map(m => m.line).join(', ')}. `;
658
+ }
659
+ suggestion += 'Ensure whitespace and indentation match exactly.';
660
+
661
+ return this.createErrorResult(
662
+ ToolErrorType.EXECUTION_FAILED,
663
+ 'Old text not found in file',
664
+ suggestion
665
+ );
666
+ }
667
+
668
+ // Check for multiple matches
669
+ const matchCount = (content.match(new RegExp(args.old_text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
670
+ if (matchCount > 1) {
671
+ this.logger.warn(`Found ${matchCount} matches - only replacing first occurrence`);
672
+ }
673
+
674
+ content = content.replace(args.old_text, args.new_text);
675
+ fs.writeFileSync(filePath, content, 'utf-8');
676
+
677
+ // Store undo operation
678
+ const undoOp: UndoOperation = {
679
+ id: `undo-${Date.now()}`,
680
+ tool: 'edit_file',
681
+ timestamp: Date.now(),
682
+ filePath: filePath,
683
+ originalContent: originalContent,
684
+ description: `Restore ${args.path} (reverts edit)`,
685
+ };
686
+ this.undoStack.push(undoOp);
687
+
688
+ // Limit undo stack size
689
+ if (this.undoStack.length > 50) {
690
+ this.undoStack.shift();
691
+ }
692
+
693
+ return {
694
+ success: true,
695
+ output: `File edited: ${args.path}`,
696
+ metadata: {
697
+ matchesFound: matchCount,
698
+ canUndo: true,
699
+ },
700
+ };
701
+ } catch (error: any) {
702
+ if (error.code === 'EACCES') {
703
+ return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Permission denied: ${args.path}`);
704
+ }
705
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, error.message);
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Execute bash command with enhanced error handling
711
+ */
712
+ private bash(args: Record<string, string>): ToolResult {
713
+ const cwd = args.cwd ? this.resolvePath(args.cwd) : this.cwd;
714
+ const timeout = args.timeout ? parseInt(args.timeout) * 1000 : 30000;
715
+
716
+ // Validate working directory
717
+ if (!fs.existsSync(cwd)) {
718
+ return this.createErrorResult(
719
+ ToolErrorType.FILE_NOT_FOUND,
720
+ `Working directory not found: ${cwd}`,
721
+ 'Check that the directory exists or use an absolute path.'
722
+ );
723
+ }
724
+
725
+ try {
726
+ const startTime = Date.now();
727
+ const output = execSync(args.command, {
728
+ cwd,
729
+ timeout,
730
+ encoding: 'utf-8',
731
+ maxBuffer: 10 * 1024 * 1024, // 10MB
732
+ stdio: ['pipe', 'pipe', 'pipe'],
733
+ });
734
+
735
+ const duration = Date.now() - startTime;
736
+
737
+ return {
738
+ success: true,
739
+ output: output.trim(),
740
+ metadata: {
741
+ durationMs: duration,
742
+ cwd,
743
+ },
744
+ };
745
+ } catch (error: any) {
746
+ // Command failed but may have output
747
+ const output = error.stdout || '';
748
+ const stderr = error.stderr || '';
749
+
750
+ // Provide helpful error messages for common cases
751
+ if (error.killed) {
752
+ return this.createErrorResult(
753
+ ToolErrorType.TIMEOUT,
754
+ `Command timed out after ${timeout/1000}s`,
755
+ 'Try increasing the timeout or breaking the command into smaller parts.'
756
+ );
757
+ }
758
+
759
+ if (stderr.includes('command not found') || stderr.includes('not recognized')) {
760
+ const cmd = args.command.split(' ')[0];
761
+ return this.createErrorResult(
762
+ ToolErrorType.EXECUTION_FAILED,
763
+ `Command not found: ${cmd}`,
764
+ 'Check that the command is installed and in PATH.'
765
+ );
766
+ }
767
+
768
+ if (stderr.includes('Permission denied')) {
769
+ return this.createErrorResult(
770
+ ToolErrorType.PERMISSION_DENIED,
771
+ 'Permission denied executing command',
772
+ 'Try using sudo or check file/directory permissions.'
773
+ );
774
+ }
775
+
776
+ return {
777
+ success: false,
778
+ output,
779
+ error: stderr || error.message,
780
+ suggestion: 'Check command syntax and try again.',
781
+ canRetry: !error.killed,
782
+ };
783
+ }
784
+ }
785
+
786
+ private grep(args: Record<string, string>): ToolResult {
787
+ const searchPath = args.path ? this.resolvePath(args.path) : this.cwd;
788
+ const include = args.include || '*';
789
+
790
+ let cmd = `grep -rn --color=never "${args.pattern}" ${searchPath}`;
791
+ if (args.include) {
792
+ cmd = `grep -rn --color=never --include="${args.include}" "${args.pattern}" ${searchPath}`;
793
+ }
794
+
795
+ try {
796
+ const output = execSync(cmd, {
797
+ cwd: this.cwd,
798
+ encoding: 'utf-8',
799
+ maxBuffer: 5 * 1024 * 1024,
800
+ timeout: 30000,
801
+ });
802
+
803
+ return { success: true, output: output.trim() };
804
+ } catch (error: any) {
805
+ if (error.status === 1) {
806
+ // No matches found
807
+ return { success: true, output: 'No matches found' };
808
+ }
809
+ return { success: false, error: error.message };
810
+ }
811
+ }
812
+
813
+ private listDir(args: Record<string, string>): ToolResult {
814
+ const dirPath = this.resolvePath(args.path);
815
+
816
+ if (!fs.existsSync(dirPath)) {
817
+ return { success: false, error: `Directory not found: ${args.path}` };
818
+ }
819
+
820
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
821
+ const output = entries.map(e => {
822
+ const suffix = e.isDirectory() ? '/' : '';
823
+ return e.name + suffix;
824
+ }).join('\n');
825
+
826
+ return { success: true, output };
827
+ }
828
+
829
+ private glob(args: Record<string, string>): ToolResult {
830
+ const { globSync } = require('glob');
831
+
832
+ try {
833
+ const files = globSync(args.pattern, { cwd: this.cwd });
834
+ return { success: true, output: files.join('\n') };
835
+ } catch (error) {
836
+ return { success: false, error: (error as Error).message };
837
+ }
838
+ }
839
+
840
+ private git(args: Record<string, string>): ToolResult {
841
+ try {
842
+ const output = execSync(`git ${args.args}`, {
843
+ cwd: this.cwd,
844
+ encoding: 'utf-8',
845
+ timeout: 60000,
846
+ });
847
+ return { success: true, output: output.trim() };
848
+ } catch (error: any) {
849
+ return { success: false, error: error.stderr || error.message };
850
+ }
851
+ }
852
+
853
+ private resolvePath(p: string): string {
854
+ if (path.isAbsolute(p)) {
855
+ return p;
856
+ }
857
+ return path.join(this.cwd, p);
858
+ }
859
+
860
+ /**
861
+ * Parse tool calls from AI response (Claude Code format)
862
+ */
863
+ static parseToolCalls(text: string): ToolCall[] {
864
+ const calls: ToolCall[] = [];
865
+
866
+ // Match <tool_call>...</tool_call> blocks
867
+ const toolCallRegex = /<tool_call>\s*(\{[\s\S]*?\})\s*<\/tool_call>/g;
868
+ let match;
869
+
870
+ while ((match = toolCallRegex.exec(text)) !== null) {
871
+ try {
872
+ const parsed = JSON.parse(match[1]);
873
+ if (parsed.tool && parsed.args) {
874
+ calls.push(parsed);
875
+ }
876
+ } catch (e) {
877
+ // Invalid JSON, skip
878
+ }
879
+ }
880
+
881
+ // Also match ```tool format
882
+ const codeBlockRegex = /```tool\s*\n(\{[\s\S]*?\})\n```/g;
883
+ while ((match = codeBlockRegex.exec(text)) !== null) {
884
+ try {
885
+ const parsed = JSON.parse(match[1]);
886
+ if (parsed.tool && parsed.args) {
887
+ calls.push(parsed);
888
+ }
889
+ } catch (e) {
890
+ // Invalid JSON, skip
891
+ }
892
+ }
893
+
894
+ return calls;
895
+ }
896
+
897
+ /**
898
+ * Get tools formatted for AI system prompt
899
+ */
900
+ static getToolsForPrompt(): string {
901
+ const tools = AgenticTools.getToolDefinitions();
902
+
903
+ let prompt = `You have access to these tools to help accomplish tasks:
904
+
905
+ `;
906
+
907
+ for (const tool of tools) {
908
+ prompt += `## ${tool.name}
909
+ ${tool.description}
910
+ Parameters:
911
+ ${tool.parameters.map(p => ` - ${p.name}${p.required ? ' (required)' : ''}: ${p.description}`).join('\n')}
912
+
913
+ `;
914
+ }
915
+
916
+ prompt += `
917
+ To use a tool, respond with a tool_call block:
918
+ \`\`\`tool
919
+ {
920
+ "tool": "tool_name",
921
+ "args": {
922
+ "param1": "value1"
923
+ }
924
+ }
925
+ \`\`\`
926
+
927
+ You can use multiple tool calls in one response. After tool execution, you'll receive the results and can continue.
928
+ Always explain what you're doing before using tools.
929
+ `;
930
+
931
+ return prompt;
932
+ }
933
+ }