vigthoria-cli 1.6.2 → 1.6.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.
Files changed (46) hide show
  1. package/README.md +52 -1
  2. package/dist/commands/chat.d.ts +31 -45
  3. package/dist/commands/chat.d.ts.map +1 -1
  4. package/dist/commands/chat.js +374 -855
  5. package/dist/commands/chat.js.map +1 -1
  6. package/dist/commands/repo.d.ts +10 -0
  7. package/dist/commands/repo.d.ts.map +1 -1
  8. package/dist/commands/repo.js +215 -97
  9. package/dist/commands/repo.js.map +1 -1
  10. package/dist/index.js +32 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/api.d.ts +8 -0
  13. package/dist/utils/api.d.ts.map +1 -1
  14. package/dist/utils/api.js +183 -42
  15. package/dist/utils/api.js.map +1 -1
  16. package/dist/utils/config.d.ts.map +1 -1
  17. package/dist/utils/config.js +2 -1
  18. package/dist/utils/config.js.map +1 -1
  19. package/dist/utils/tools.d.ts +3 -0
  20. package/dist/utils/tools.d.ts.map +1 -1
  21. package/dist/utils/tools.js +116 -0
  22. package/dist/utils/tools.js.map +1 -1
  23. package/package.json +13 -2
  24. package/install.ps1 +0 -290
  25. package/install.sh +0 -307
  26. package/src/commands/auth.ts +0 -226
  27. package/src/commands/chat.ts +0 -1101
  28. package/src/commands/config.ts +0 -306
  29. package/src/commands/deploy.ts +0 -609
  30. package/src/commands/edit.ts +0 -310
  31. package/src/commands/explain.ts +0 -115
  32. package/src/commands/generate.ts +0 -222
  33. package/src/commands/hub.ts +0 -382
  34. package/src/commands/repo.ts +0 -742
  35. package/src/commands/review.ts +0 -186
  36. package/src/index.ts +0 -601
  37. package/src/types/marked-terminal.d.ts +0 -31
  38. package/src/utils/api.ts +0 -526
  39. package/src/utils/config.ts +0 -241
  40. package/src/utils/files.ts +0 -273
  41. package/src/utils/logger.ts +0 -130
  42. package/src/utils/session.ts +0 -179
  43. package/src/utils/tools.ts +0 -2086
  44. package/test-parse.js +0 -105
  45. package/test-parse2.js +0 -35
  46. package/tsconfig.json +0 -20
@@ -1,2086 +0,0 @@
1
- /**
2
- * Vigthoria CLI Tools - Agentic Tool System
3
- *
4
- * This module provides Vigthoria Autonomous 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, options?: { batchApproval?: boolean }) => Promise<boolean | 'batch'>;
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
- // Session-based tool approvals - remembers which tools user approved for this turn
93
- private sessionApprovedTools: Set<string> = new Set();
94
-
95
- constructor(
96
- logger: Logger,
97
- cwd: string,
98
- permissionCallback: (action: string, options?: { batchApproval?: boolean }) => Promise<boolean | 'batch'>,
99
- autoApprove: boolean = false
100
- ) {
101
- this.logger = logger;
102
- this.cwd = cwd;
103
- this.permissionCallback = permissionCallback;
104
- this.autoApprove = autoApprove;
105
- }
106
-
107
- /**
108
- * Clear session-approved tools (call this at the start of each new AI turn)
109
- */
110
- clearSessionApprovals(): void {
111
- this.sessionApprovedTools.clear();
112
- }
113
-
114
- /**
115
- * Get currently approved tools for this session
116
- */
117
- getSessionApprovedTools(): string[] {
118
- return Array.from(this.sessionApprovedTools);
119
- }
120
-
121
- /**
122
- * Get the undo stack for inspection
123
- */
124
- getUndoStack(): UndoOperation[] {
125
- return [...this.undoStack];
126
- }
127
-
128
- /**
129
- * Undo the last file operation
130
- */
131
- async undo(): Promise<ToolResult> {
132
- const lastOp = this.undoStack.pop();
133
-
134
- if (!lastOp) {
135
- return {
136
- success: false,
137
- error: 'Nothing to undo',
138
- suggestion: 'The undo stack is empty. Only file write/edit operations can be undone.',
139
- };
140
- }
141
-
142
- if (lastOp.filePath) {
143
- try {
144
- if (lastOp.originalContent === null) {
145
- // File was created, so delete it
146
- if (fs.existsSync(lastOp.filePath)) {
147
- fs.unlinkSync(lastOp.filePath);
148
- return {
149
- success: true,
150
- output: `✓ Undone: ${lastOp.description}\n File deleted: ${lastOp.filePath}`,
151
- metadata: { remainingUndos: this.undoStack.length },
152
- };
153
- }
154
- } else if (lastOp.originalContent !== undefined) {
155
- // Restore original content
156
- fs.writeFileSync(lastOp.filePath, lastOp.originalContent, 'utf-8');
157
- return {
158
- success: true,
159
- output: `✓ Undone: ${lastOp.description}\n File restored: ${lastOp.filePath}`,
160
- metadata: { remainingUndos: this.undoStack.length },
161
- };
162
- }
163
- } catch (error) {
164
- return {
165
- success: false,
166
- error: `Failed to undo: ${(error as Error).message}`,
167
- suggestion: 'The file may have been moved or permissions changed.',
168
- };
169
- }
170
- }
171
-
172
- return {
173
- success: false,
174
- error: 'This operation cannot be undone',
175
- };
176
- }
177
-
178
- /**
179
- * List of available tools - Vigthoria's advanced
180
- * Enhanced with risk levels and categories
181
- */
182
- static getToolDefinitions(): ToolDefinition[] {
183
- return [
184
- {
185
- name: 'read_file',
186
- description: 'Read the contents of a file',
187
- parameters: [
188
- { name: 'path', description: 'File path (relative or absolute)', required: true },
189
- { name: 'start_line', description: 'Start line (1-indexed)', required: false },
190
- { name: 'end_line', description: 'End line (1-indexed)', required: false },
191
- ],
192
- requiresPermission: false,
193
- dangerous: false,
194
- riskLevel: 'low',
195
- category: 'read',
196
- },
197
- {
198
- name: 'write_file',
199
- description: 'Write content to a file (creates if not exists)',
200
- parameters: [
201
- { name: 'path', description: 'File path', required: true },
202
- { name: 'content', description: 'Content to write', required: true },
203
- ],
204
- requiresPermission: true,
205
- dangerous: false,
206
- riskLevel: 'medium',
207
- category: 'write',
208
- },
209
- {
210
- name: 'edit_file',
211
- description: 'Make specific edits to a file by replacing text',
212
- parameters: [
213
- { name: 'path', description: 'File path', required: true },
214
- { name: 'old_text', description: 'Text to replace', required: true },
215
- { name: 'new_text', description: 'New text', required: true },
216
- ],
217
- requiresPermission: true,
218
- dangerous: false,
219
- riskLevel: 'medium',
220
- category: 'write',
221
- },
222
- {
223
- name: 'bash',
224
- description: 'Run a shell command',
225
- parameters: [
226
- { name: 'command', description: 'Shell command to execute', required: true },
227
- { name: 'cwd', description: 'Working directory', required: false },
228
- { name: 'timeout', description: 'Timeout in seconds', required: false },
229
- ],
230
- requiresPermission: true,
231
- dangerous: true,
232
- riskLevel: 'high',
233
- category: 'execute',
234
- },
235
- {
236
- name: 'grep',
237
- description: 'Search for patterns in files',
238
- parameters: [
239
- { name: 'pattern', description: 'Search pattern (regex)', required: true },
240
- { name: 'path', description: 'File or directory path', required: false },
241
- { name: 'include', description: 'File pattern to include (e.g., *.ts)', required: false },
242
- ],
243
- requiresPermission: false,
244
- dangerous: false,
245
- riskLevel: 'low',
246
- category: 'search',
247
- },
248
- {
249
- name: 'list_dir',
250
- description: 'List contents of a directory',
251
- parameters: [
252
- { name: 'path', description: 'Directory path', required: true },
253
- { name: 'recursive', description: 'List recursively', required: false },
254
- ],
255
- requiresPermission: false,
256
- dangerous: false,
257
- riskLevel: 'low',
258
- category: 'read',
259
- },
260
- {
261
- name: 'glob',
262
- description: 'Find files matching a pattern',
263
- parameters: [
264
- { name: 'pattern', description: 'Glob pattern (e.g., **/*.ts)', required: true },
265
- ],
266
- requiresPermission: false,
267
- dangerous: false,
268
- riskLevel: 'low',
269
- category: 'search',
270
- },
271
- {
272
- name: 'git',
273
- description: 'Run git commands',
274
- parameters: [
275
- { name: 'args', description: 'Git command arguments', required: true },
276
- ],
277
- requiresPermission: true,
278
- dangerous: false,
279
- riskLevel: 'medium',
280
- category: 'execute',
281
- },
282
- {
283
- name: 'repo',
284
- description: 'Manage projects in Vigthoria Repository - push, pull, list, share, delete, or clone projects',
285
- parameters: [
286
- { name: 'action', description: 'Action: push, pull, list, status, share, delete, clone', required: true },
287
- { name: 'project', description: 'Project name (for push/pull/status/share/delete)', required: false },
288
- { name: 'files', description: 'Files to push as JSON object {filename: content} or array [{path, content}]', required: false },
289
- { name: 'description', description: 'Project description (for push)', required: false },
290
- { name: 'visibility', description: 'Visibility: public or private (for push)', required: false },
291
- { name: 'path', description: 'Directory path (for push) or target path (for clone)', required: false },
292
- { name: 'username', description: 'Username to share with (for share action)', required: false },
293
- { name: 'permission', description: 'Permission level: read, write, admin (for share action)', required: false },
294
- ],
295
- requiresPermission: true,
296
- dangerous: false,
297
- riskLevel: 'medium',
298
- category: 'execute',
299
- },
300
- {
301
- name: 'fetch_url',
302
- description: 'Fetch content from a URL (web page, API endpoint). Works cross-platform on Windows, Mac, and Linux.',
303
- parameters: [
304
- { name: 'url', description: 'URL to fetch (must be http or https)', required: true },
305
- { name: 'method', description: 'HTTP method (GET, POST, etc.)', required: false },
306
- { name: 'headers', description: 'JSON string of headers to send', required: false },
307
- { name: 'body', description: 'Request body for POST/PUT requests', required: false },
308
- { name: 'selector', description: 'CSS selector to extract specific content (for HTML)', required: false },
309
- ],
310
- requiresPermission: true,
311
- dangerous: false,
312
- riskLevel: 'low',
313
- category: 'read',
314
- },
315
- {
316
- name: 'ssh_exec',
317
- description: 'Execute a command on a remote server via SSH (useful for Unix commands from Windows)',
318
- parameters: [
319
- { name: 'command', description: 'Command to execute on remote server', required: true },
320
- { name: 'host', description: 'SSH host (default: vigthoria-server)', required: false },
321
- { name: 'timeout', description: 'Timeout in seconds', required: false },
322
- ],
323
- requiresPermission: true,
324
- dangerous: true,
325
- riskLevel: 'high',
326
- category: 'execute',
327
- },
328
- ];
329
- }
330
-
331
- /**
332
- * Execute a tool call with enhanced error handling and retry logic
333
- */
334
- async execute(call: ToolCall): Promise<ToolResult> {
335
- const tool = AgenticTools.getToolDefinitions().find(t => t.name === call.tool);
336
-
337
- if (!tool) {
338
- return this.createErrorResult(
339
- ToolErrorType.INVALID_ARGS,
340
- `Unknown tool: ${call.tool}`,
341
- `Available tools: ${AgenticTools.getToolDefinitions().map(t => t.name).join(', ')}`
342
- );
343
- }
344
-
345
- // Validate required parameters
346
- const validationError = this.validateParameters(call, tool);
347
- if (validationError) {
348
- return validationError;
349
- }
350
-
351
- // Check permission for dangerous/modifying actions
352
- if (tool.requiresPermission && !this.autoApprove) {
353
- // Check if this tool was already approved for this session/turn
354
- if (this.sessionApprovedTools.has(call.tool)) {
355
- // Already approved - skip permission prompt
356
- this.logger.info(`${call.tool}: Auto-approved (batch)`);
357
- } else {
358
- const approved = await this.permissionCallback(
359
- this.formatPermissionRequest(call, tool),
360
- { batchApproval: true }
361
- );
362
-
363
- if (approved === false) {
364
- return {
365
- success: false,
366
- error: 'Permission denied by user',
367
- canRetry: true,
368
- };
369
- }
370
-
371
- // Only add to session approvals if user chose 'batch' (typed 'a')
372
- // 'y' or 'yes' only approves this single request
373
- if (approved === 'batch') {
374
- this.sessionApprovedTools.add(call.tool);
375
- }
376
- }
377
- }
378
-
379
- // Execute with retry logic for applicable operations
380
- return this.executeWithRetry(call, tool);
381
- }
382
-
383
- /**
384
- * Execute tool with automatic retry for transient failures
385
- */
386
- private async executeWithRetry(call: ToolCall, tool: ToolDefinition): Promise<ToolResult> {
387
- let lastError: ToolResult | null = null;
388
-
389
- for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
390
- try {
391
- const result = await this.executeTool(call);
392
-
393
- if (result.success) {
394
- return result;
395
- }
396
-
397
- // Check if error is retryable
398
- const isRetryable = result.canRetry !== false &&
399
- this.isRetryableError(result.error || '');
400
-
401
- if (!isRetryable || attempt === this.retryConfig.maxRetries) {
402
- return result;
403
- }
404
-
405
- lastError = result;
406
-
407
- // Calculate delay with exponential backoff
408
- const delay = Math.min(
409
- this.retryConfig.baseDelayMs * Math.pow(2, attempt),
410
- this.retryConfig.maxDelayMs
411
- );
412
-
413
- this.logger.warn(`Retrying ${call.tool} in ${delay}ms (attempt ${attempt + 2}/${this.retryConfig.maxRetries + 1})...`);
414
- await this.sleep(delay);
415
-
416
- } catch (error) {
417
- lastError = this.createErrorResult(
418
- ToolErrorType.EXECUTION_FAILED,
419
- (error as Error).message
420
- );
421
-
422
- if (attempt === this.retryConfig.maxRetries) {
423
- return lastError;
424
- }
425
- }
426
- }
427
-
428
- return lastError || this.createErrorResult(
429
- ToolErrorType.EXECUTION_FAILED,
430
- 'Unknown error after retries'
431
- );
432
- }
433
-
434
- /**
435
- * Execute the actual tool operation
436
- */
437
- private async executeTool(call: ToolCall): Promise<ToolResult> {
438
- switch (call.tool) {
439
- case 'read_file':
440
- return this.readFile(call.args);
441
- case 'write_file':
442
- return this.writeFile(call.args);
443
- case 'edit_file':
444
- return this.editFile(call.args);
445
- case 'bash':
446
- return this.bash(call.args);
447
- case 'grep':
448
- return this.grep(call.args);
449
- case 'list_dir':
450
- return this.listDir(call.args);
451
- case 'glob':
452
- return this.glob(call.args);
453
- case 'git':
454
- return this.git(call.args);
455
- case 'repo':
456
- return this.repo(call.args);
457
- case 'fetch_url':
458
- return this.fetchUrl(call.args);
459
- case 'ssh_exec':
460
- return this.sshExec(call.args);
461
- default:
462
- return this.createErrorResult(
463
- ToolErrorType.INVALID_ARGS,
464
- `Tool not implemented: ${call.tool}`
465
- );
466
- }
467
- }
468
-
469
- /**
470
- * Check if an error is retryable
471
- */
472
- private isRetryableError(error: string): boolean {
473
- return this.retryConfig.retryableErrors.some(e => error.includes(e));
474
- }
475
-
476
- /**
477
- * Validate tool parameters
478
- */
479
- private validateParameters(call: ToolCall, tool: ToolDefinition): ToolResult | null {
480
- for (const param of tool.parameters) {
481
- if (param.required && !call.args[param.name]) {
482
- return this.createErrorResult(
483
- ToolErrorType.INVALID_ARGS,
484
- `Missing required parameter: ${param.name}`,
485
- `The ${call.tool} tool requires the '${param.name}' parameter. ${param.description}`
486
- );
487
- }
488
- }
489
- return null;
490
- }
491
-
492
- /**
493
- * Create a standardized error result
494
- */
495
- private createErrorResult(
496
- type: ToolErrorType,
497
- message: string,
498
- suggestion?: string
499
- ): ToolResult {
500
- const suggestions: Record<ToolErrorType, string> = {
501
- [ToolErrorType.FILE_NOT_FOUND]: 'Check if the file path is correct. Use list_dir to see available files.',
502
- [ToolErrorType.PERMISSION_DENIED]: 'You may need elevated permissions. Try using sudo or check file ownership.',
503
- [ToolErrorType.NETWORK_ERROR]: 'Check your internet connection. The operation will retry automatically.',
504
- [ToolErrorType.TIMEOUT]: 'The operation took too long. Try with a shorter timeout or simpler command.',
505
- [ToolErrorType.INVALID_ARGS]: 'Check the tool documentation for correct parameter usage.',
506
- [ToolErrorType.EXECUTION_FAILED]: 'Review the command syntax and try again.',
507
- [ToolErrorType.USER_CANCELLED]: 'Operation cancelled. You can retry if needed.',
508
- };
509
-
510
- return {
511
- success: false,
512
- error: message,
513
- suggestion: suggestion || suggestions[type],
514
- canRetry: type !== ToolErrorType.USER_CANCELLED && type !== ToolErrorType.INVALID_ARGS,
515
- };
516
- }
517
-
518
- /**
519
- * Enhanced permission request with risk visualization
520
- */
521
- private formatPermissionRequest(call: ToolCall, tool: ToolDefinition): string {
522
- const riskColors: Record<RiskLevel, (text: string) => string> = {
523
- low: chalk.green,
524
- medium: chalk.yellow,
525
- high: chalk.red,
526
- critical: chalk.bgRed.white,
527
- };
528
-
529
- const riskIcons: Record<RiskLevel, string> = {
530
- low: '🟢',
531
- medium: '🟡',
532
- high: '🔴',
533
- critical: '⛔',
534
- };
535
-
536
- const riskColor = riskColors[tool.riskLevel];
537
- const riskIcon = riskIcons[tool.riskLevel];
538
-
539
- let msg = `\n${riskIcon} ${riskColor(`${tool.riskLevel.toUpperCase()} RISK`)} - AI wants to use ${chalk.cyan.bold(call.tool)}\n`;
540
- msg += chalk.gray('─'.repeat(50)) + '\n';
541
-
542
- // Tool-specific details
543
- if (call.tool === 'bash') {
544
- msg += `${chalk.gray('├─')} ${chalk.white('Command:')} ${chalk.yellow(call.args.command)}\n`;
545
- if (call.args.cwd) {
546
- msg += `${chalk.gray('├─')} ${chalk.white('Directory:')} ${call.args.cwd}\n`;
547
- }
548
- msg += `${chalk.gray('├─')} ${chalk.white('Timeout:')} ${call.args.timeout || '30'}s\n`;
549
- } else if (call.tool === 'write_file') {
550
- msg += `${chalk.gray('├─')} ${chalk.white('File:')} ${chalk.cyan(call.args.path)}\n`;
551
- const preview = call.args.content.substring(0, 100);
552
- msg += `${chalk.gray('├─')} ${chalk.white('Content preview:')} ${chalk.gray(preview)}${call.args.content.length > 100 ? '...' : ''}\n`;
553
- msg += `${chalk.gray('├─')} ${chalk.white('Size:')} ${call.args.content.length} bytes\n`;
554
- } else if (call.tool === 'edit_file') {
555
- msg += `${chalk.gray('├─')} ${chalk.white('File:')} ${chalk.cyan(call.args.path)}\n`;
556
- msg += `${chalk.gray('├─')} ${chalk.white('Replace:')} ${chalk.red(call.args.old_text.substring(0, 50))}${call.args.old_text.length > 50 ? '...' : ''}\n`;
557
- msg += `${chalk.gray('├─')} ${chalk.white('With:')} ${chalk.green(call.args.new_text.substring(0, 50))}${call.args.new_text.length > 50 ? '...' : ''}\n`;
558
- } else if (call.tool === 'git') {
559
- msg += `${chalk.gray('├─')} ${chalk.white('Command:')} git ${chalk.yellow(call.args.args)}\n`;
560
- }
561
-
562
- // Safety info
563
- msg += chalk.gray('─'.repeat(50)) + '\n';
564
-
565
- const canUndo = ['write_file', 'edit_file'].includes(call.tool);
566
- msg += `${chalk.gray('├─')} ${chalk.white('Undo:')} ${canUndo ? chalk.green('✓ Available (use /undo)') : chalk.gray('✗ Not available')}\n`;
567
- msg += `${chalk.gray('└─')} ${chalk.white('Category:')} ${tool.category}\n`;
568
-
569
- if (tool.dangerous) {
570
- msg += `\n${chalk.red.bold('⚠️ WARNING: This is a potentially dangerous action!')}\n`;
571
- msg += chalk.red(' The AI is requesting to execute commands on your system.\n');
572
- }
573
-
574
- // Add batch approval hint
575
- msg += `\n${chalk.gray('Respond:')} ${chalk.white('[y]es')} ${chalk.gray('/')} ${chalk.white('[n]o')} ${chalk.gray('/')} ${chalk.cyan('[a]pprove all')} ${chalk.cyan(call.tool)} ${chalk.gray('for this turn')}\n`;
576
-
577
- return msg;
578
- }
579
-
580
- private sleep(ms: number): Promise<void> {
581
- return new Promise(resolve => setTimeout(resolve, ms));
582
- }
583
-
584
- // Tool implementations with enhanced error handling and undo support
585
-
586
- /**
587
- * Read file with enhanced error handling and suggestions
588
- */
589
- private readFile(args: Record<string, string>): ToolResult {
590
- const filePath = this.resolvePath(args.path);
591
-
592
- if (!fs.existsSync(filePath)) {
593
- // Try to find similar files
594
- const dir = path.dirname(filePath);
595
- const basename = path.basename(filePath);
596
- let suggestions: string[] = [];
597
-
598
- if (fs.existsSync(dir)) {
599
- const files = fs.readdirSync(dir);
600
- suggestions = files
601
- .filter(f => f.toLowerCase().includes(basename.toLowerCase().slice(0, 3)))
602
- .slice(0, 3);
603
- }
604
-
605
- return this.createErrorResult(
606
- ToolErrorType.FILE_NOT_FOUND,
607
- `File not found: ${args.path}`,
608
- suggestions.length > 0
609
- ? `Did you mean one of these? ${suggestions.join(', ')}`
610
- : 'Use list_dir to see available files in the directory.'
611
- );
612
- }
613
-
614
- try {
615
- const stats = fs.statSync(filePath);
616
- if (stats.size > 1024 * 1024) { // 1MB warning
617
- this.logger.warn(`Large file (${(stats.size / 1024 / 1024).toFixed(2)}MB) - consider using start_line/end_line`);
618
- }
619
-
620
- const content = fs.readFileSync(filePath, 'utf-8');
621
- const lines = content.split('\n');
622
-
623
- const startLine = args.start_line ? parseInt(args.start_line) - 1 : 0;
624
- const endLine = args.end_line ? parseInt(args.end_line) : lines.length;
625
-
626
- // Validate line numbers
627
- if (startLine < 0 || startLine >= lines.length) {
628
- return this.createErrorResult(
629
- ToolErrorType.INVALID_ARGS,
630
- `Invalid start_line: ${args.start_line}. File has ${lines.length} lines.`
631
- );
632
- }
633
-
634
- const selectedLines = lines.slice(startLine, Math.min(endLine, lines.length));
635
-
636
- return {
637
- success: true,
638
- output: selectedLines.join('\n'),
639
- metadata: {
640
- totalLines: lines.length,
641
- linesReturned: selectedLines.length,
642
- filePath: args.path,
643
- },
644
- };
645
- } catch (error: any) {
646
- if (error.code === 'EACCES') {
647
- return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Permission denied: ${args.path}`);
648
- }
649
- return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, error.message);
650
- }
651
- }
652
-
653
- /**
654
- * Write file with undo support
655
- */
656
- private writeFile(args: Record<string, string>): ToolResult {
657
- const filePath = this.resolvePath(args.path);
658
- const dir = path.dirname(filePath);
659
-
660
- // Save original content for undo if file exists
661
- let originalContent: string | null = null;
662
- if (fs.existsSync(filePath)) {
663
- originalContent = fs.readFileSync(filePath, 'utf-8');
664
- }
665
-
666
- try {
667
- // Create directory if needed
668
- if (!fs.existsSync(dir)) {
669
- fs.mkdirSync(dir, { recursive: true });
670
- }
671
-
672
- fs.writeFileSync(filePath, args.content, 'utf-8');
673
-
674
- // Store undo operation
675
- const undoOp: UndoOperation = {
676
- id: `undo-${Date.now()}`,
677
- tool: 'write_file',
678
- timestamp: Date.now(),
679
- filePath: filePath,
680
- originalContent: originalContent,
681
- description: originalContent
682
- ? `Restore ${args.path} to previous version`
683
- : `Delete ${args.path} (was created)`,
684
- };
685
- this.undoStack.push(undoOp);
686
-
687
- // Limit undo stack size
688
- if (this.undoStack.length > 50) {
689
- this.undoStack.shift();
690
- }
691
-
692
- const isNew = originalContent === null;
693
- return {
694
- success: true,
695
- output: `File ${isNew ? 'created' : 'updated'}: ${args.path}`,
696
- metadata: {
697
- bytes: args.content.length,
698
- isNew,
699
- canUndo: true,
700
- },
701
- };
702
- } catch (error: any) {
703
- if (error.code === 'EACCES') {
704
- return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Permission denied writing to: ${args.path}`);
705
- }
706
- if (error.code === 'ENOSPC') {
707
- return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, 'No space left on device');
708
- }
709
- return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, error.message);
710
- }
711
- }
712
-
713
- /**
714
- * Edit file with undo support and helpful error messages
715
- */
716
- private editFile(args: Record<string, string>): ToolResult {
717
- const filePath = this.resolvePath(args.path);
718
-
719
- if (!fs.existsSync(filePath)) {
720
- return this.createErrorResult(
721
- ToolErrorType.FILE_NOT_FOUND,
722
- `File not found: ${args.path}`,
723
- 'Use write_file to create a new file, or check the path.'
724
- );
725
- }
726
-
727
- try {
728
- let content = fs.readFileSync(filePath, 'utf-8');
729
- const originalContent = content;
730
-
731
- if (!content.includes(args.old_text)) {
732
- // Try to help identify the issue
733
- const lines = content.split('\n');
734
- const searchTerms = args.old_text.split('\n')[0].trim().slice(0, 30);
735
- const possibleMatches = lines
736
- .map((line, i) => ({ line: i + 1, content: line }))
737
- .filter(l => l.content.includes(searchTerms.slice(0, 10)))
738
- .slice(0, 3);
739
-
740
- let suggestion = 'The exact text was not found. ';
741
- if (possibleMatches.length > 0) {
742
- suggestion += `Similar content found at lines: ${possibleMatches.map(m => m.line).join(', ')}. `;
743
- }
744
- suggestion += 'Ensure whitespace and indentation match exactly.';
745
-
746
- return this.createErrorResult(
747
- ToolErrorType.EXECUTION_FAILED,
748
- 'Old text not found in file',
749
- suggestion
750
- );
751
- }
752
-
753
- // Check for multiple matches
754
- const matchCount = (content.match(new RegExp(args.old_text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
755
- if (matchCount > 1) {
756
- this.logger.warn(`Found ${matchCount} matches - only replacing first occurrence`);
757
- }
758
-
759
- content = content.replace(args.old_text, args.new_text);
760
- fs.writeFileSync(filePath, content, 'utf-8');
761
-
762
- // Store undo operation
763
- const undoOp: UndoOperation = {
764
- id: `undo-${Date.now()}`,
765
- tool: 'edit_file',
766
- timestamp: Date.now(),
767
- filePath: filePath,
768
- originalContent: originalContent,
769
- description: `Restore ${args.path} (reverts edit)`,
770
- };
771
- this.undoStack.push(undoOp);
772
-
773
- // Limit undo stack size
774
- if (this.undoStack.length > 50) {
775
- this.undoStack.shift();
776
- }
777
-
778
- return {
779
- success: true,
780
- output: `File edited: ${args.path}`,
781
- metadata: {
782
- matchesFound: matchCount,
783
- canUndo: true,
784
- },
785
- };
786
- } catch (error: any) {
787
- if (error.code === 'EACCES') {
788
- return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Permission denied: ${args.path}`);
789
- }
790
- return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, error.message);
791
- }
792
- }
793
-
794
- /**
795
- * Execute bash command with enhanced error handling
796
- * SECURITY: Commands are sandboxed to workspace directory
797
- * WINDOWS: Detects Unix-specific commands and suggests alternatives
798
- */
799
- private bash(args: Record<string, string>): ToolResult {
800
- const cwd = args.cwd ? this.resolvePath(args.cwd) : this.cwd;
801
- const timeout = args.timeout ? parseInt(args.timeout) * 1000 : 30000;
802
- const os = require('os');
803
- const platform = os.platform();
804
-
805
- // Unix-specific commands that don't exist on Windows
806
- const unixOnlyCommands = [
807
- 'head', 'tail', 'grep', 'awk', 'sed', 'wc', 'cut', 'sort', 'uniq',
808
- 'xargs', 'find', 'chmod', 'chown', 'ln', 'df', 'du', 'ps', 'kill',
809
- 'top', 'htop', 'which', 'whereis', 'man', 'less', 'more', 'cat',
810
- ];
811
-
812
- // Check if command uses Unix-only tools on Windows
813
- if (platform === 'win32') {
814
- const cmdParts = args.command.split(/[|&;]/);
815
- for (const part of cmdParts) {
816
- const firstWord = part.trim().split(/\s+/)[0].toLowerCase();
817
- if (unixOnlyCommands.includes(firstWord)) {
818
- return this.createErrorResult(
819
- ToolErrorType.EXECUTION_FAILED,
820
- `Command '${firstWord}' is not available on Windows`,
821
- `Use the 'ssh_exec' tool to run Unix commands on the server, ` +
822
- `or use 'fetch_url' for web requests. ` +
823
- `PowerShell alternatives: dir (ls), type (cat), findstr (grep), select-string (grep)`
824
- );
825
- }
826
- }
827
-
828
- // Check for pipe to Unix command
829
- if (args.command.includes('|')) {
830
- const pipedCommands = args.command.split('|').map(c => c.trim().split(/\s+/)[0].toLowerCase());
831
- for (const cmd of pipedCommands) {
832
- if (unixOnlyCommands.includes(cmd)) {
833
- return this.createErrorResult(
834
- ToolErrorType.EXECUTION_FAILED,
835
- `Piped command '${cmd}' is not available on Windows`,
836
- `Windows doesn't have '${cmd}'. Use 'ssh_exec' tool to run this command on the Vigthoria server instead.`
837
- );
838
- }
839
- }
840
- }
841
- }
842
-
843
- // SECURITY: Block dangerous commands that could access outside workspace
844
- const blockedPatterns = [
845
- /\bcat\s+\/etc\//i, // Reading system files
846
- /\bcat\s+\/var\/(?!log)/i, // Reading /var/ except logs
847
- /\bcat\s+\/root\//i, // Reading root home
848
- /\bcat\s+\/home\/[^\/]+\/\.[^\/]/i, // Reading hidden files in home dirs
849
- /\bcat\s+~\/\./i, // Reading hidden files via ~
850
- /\bcd\s+(\/etc|\/var\/www|\/root|\/home)/i, // CD to sensitive dirs
851
- /\brm\s+-rf?\s+\//i, // Dangerous rm commands
852
- /\b(curl|wget).*\|\s*(bash|sh)\b/i, // Downloading and executing scripts
853
- /\bsudo\b/i, // Sudo commands
854
- /\bsu\s+-/i, // Su to root
855
- /\bchmod\s+[0-7]*777/i, // World-writable permissions
856
- /\/(var\/www|opt)\/[a-z]+-?(database|models|coder|mcp|operator|voice|music)/i, // Vigthoria ecosystem
857
- /vigthoria-(models|database|mcp|operator|voice|music)/i, // Vigthoria project names
858
- ];
859
-
860
- for (const pattern of blockedPatterns) {
861
- if (pattern.test(args.command)) {
862
- this.logger.warn(`Security: Blocked potentially dangerous command: ${args.command.substring(0, 50)}...`);
863
- return this.createErrorResult(
864
- ToolErrorType.PERMISSION_DENIED,
865
- 'This command is blocked for security reasons',
866
- 'Commands must operate within your current project workspace.'
867
- );
868
- }
869
- }
870
-
871
- // Validate working directory
872
- if (!fs.existsSync(cwd)) {
873
- return this.createErrorResult(
874
- ToolErrorType.FILE_NOT_FOUND,
875
- `Working directory not found: ${cwd}`,
876
- 'Check that the directory exists or use an absolute path.'
877
- );
878
- }
879
-
880
- try {
881
- const startTime = Date.now();
882
-
883
- // Build exec options based on platform
884
- const execOptions: any = {
885
- cwd,
886
- timeout,
887
- encoding: 'utf-8',
888
- maxBuffer: 10 * 1024 * 1024, // 10MB
889
- stdio: ['pipe', 'pipe', 'pipe'],
890
- // Ensure consistent environment
891
- env: {
892
- ...process.env,
893
- // Prevent locale issues
894
- LC_ALL: 'C',
895
- LANG: 'C',
896
- },
897
- };
898
-
899
- // Set shell based on platform for consistent behavior
900
- if (platform === 'win32') {
901
- execOptions.shell = true; // Use default cmd.exe on Windows
902
- } else {
903
- // Use /bin/sh on Unix-like systems (more portable than bash)
904
- execOptions.shell = '/bin/sh';
905
- }
906
-
907
- const output = execSync(args.command, execOptions) as string;
908
-
909
- const duration = Date.now() - startTime;
910
-
911
- return {
912
- success: true,
913
- output: output.trim(),
914
- metadata: {
915
- durationMs: duration,
916
- cwd,
917
- },
918
- };
919
- } catch (error: any) {
920
- // Command failed but may have output
921
- const output = error.stdout || '';
922
- const stderr = error.stderr || '';
923
-
924
- // Provide helpful error messages for common cases
925
- if (error.killed) {
926
- return this.createErrorResult(
927
- ToolErrorType.TIMEOUT,
928
- `Command timed out after ${timeout/1000}s`,
929
- 'Try increasing the timeout or breaking the command into smaller parts.'
930
- );
931
- }
932
-
933
- if (stderr.includes('command not found') || stderr.includes('not recognized')) {
934
- const cmd = args.command.split(' ')[0];
935
- return this.createErrorResult(
936
- ToolErrorType.EXECUTION_FAILED,
937
- `Command not found: ${cmd}`,
938
- 'Check that the command is installed and in PATH.'
939
- );
940
- }
941
-
942
- if (stderr.includes('Permission denied')) {
943
- return this.createErrorResult(
944
- ToolErrorType.PERMISSION_DENIED,
945
- 'Permission denied executing command',
946
- 'Try using sudo or check file/directory permissions.'
947
- );
948
- }
949
-
950
- return {
951
- success: false,
952
- output,
953
- error: stderr || error.message,
954
- suggestion: 'Check command syntax and try again.',
955
- canRetry: !error.killed,
956
- };
957
- }
958
- }
959
-
960
- private grep(args: Record<string, string>): ToolResult {
961
- const searchPath = args.path ? this.resolvePath(args.path) : this.cwd;
962
- const include = args.include || '*';
963
- const os = require('os');
964
-
965
- // macOS uses BSD grep which has different options than GNU grep
966
- const isMac = os.platform() === 'darwin';
967
-
968
- let cmd: string;
969
- if (isMac) {
970
- // BSD grep: use -r for recursive, -n for line numbers
971
- // Note: BSD grep doesn't support --color=never, just omit it
972
- if (args.include) {
973
- cmd = `grep -rn --include="${args.include}" "${args.pattern}" "${searchPath}"`;
974
- } else {
975
- cmd = `grep -rn "${args.pattern}" "${searchPath}"`;
976
- }
977
- } else {
978
- // GNU grep (Linux)
979
- if (args.include) {
980
- cmd = `grep -rn --color=never --include="${args.include}" "${args.pattern}" "${searchPath}"`;
981
- } else {
982
- cmd = `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
983
- }
984
- }
985
-
986
- try {
987
- const output = execSync(cmd, {
988
- cwd: this.cwd,
989
- encoding: 'utf-8',
990
- maxBuffer: 5 * 1024 * 1024,
991
- timeout: 30000,
992
- env: { ...process.env, GREP_OPTIONS: '' }, // Prevent user's grep options from interfering
993
- });
994
-
995
- return { success: true, output: output.trim() };
996
- } catch (error: any) {
997
- if (error.status === 1) {
998
- // No matches found
999
- return { success: true, output: 'No matches found' };
1000
- }
1001
- return { success: false, error: error.message };
1002
- }
1003
- }
1004
-
1005
- private listDir(args: Record<string, string>): ToolResult {
1006
- const dirPath = this.resolvePath(args.path);
1007
-
1008
- if (!fs.existsSync(dirPath)) {
1009
- return { success: false, error: `Directory not found: ${args.path}` };
1010
- }
1011
-
1012
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
1013
- const output = entries.map(e => {
1014
- const suffix = e.isDirectory() ? '/' : '';
1015
- return e.name + suffix;
1016
- }).join('\n');
1017
-
1018
- return { success: true, output };
1019
- }
1020
-
1021
- private glob(args: Record<string, string>): ToolResult {
1022
- const { globSync } = require('glob');
1023
-
1024
- try {
1025
- const files = globSync(args.pattern, { cwd: this.cwd });
1026
- return { success: true, output: files.join('\n') };
1027
- } catch (error) {
1028
- return { success: false, error: (error as Error).message };
1029
- }
1030
- }
1031
-
1032
- private git(args: Record<string, string>): ToolResult {
1033
- try {
1034
- const output = execSync(`git ${args.args}`, {
1035
- cwd: this.cwd,
1036
- encoding: 'utf-8',
1037
- timeout: 60000,
1038
- });
1039
- return { success: true, output: output.trim() };
1040
- } catch (error: any) {
1041
- return { success: false, error: error.stderr || error.message };
1042
- }
1043
- }
1044
-
1045
- /**
1046
- * Vigthoria Repository management tool
1047
- * Allows AI to push, pull, list, share, and manage projects in the Vigthoria Repository
1048
- */
1049
- private async repo(args: Record<string, string>): Promise<ToolResult> {
1050
- const action = args.action?.toLowerCase();
1051
- const project = args.project;
1052
- const visibility = args.visibility || 'public';
1053
- const targetPath = args.path || this.cwd;
1054
- const description = args.description || '';
1055
-
1056
- try {
1057
- // Push action uses direct API call to Community server (bypasses interactive CLI)
1058
- if (action === 'push') {
1059
- if (!project) {
1060
- return {
1061
- success: false,
1062
- error: 'Project name is required for push',
1063
- suggestion: 'Provide a project name, e.g., repo action=push project=my-project'
1064
- };
1065
- }
1066
-
1067
- // Load auth config
1068
- const configPath = path.join(process.env.HOME || '/root', '.config', 'vigthoria-cli', 'config.json');
1069
- if (!fs.existsSync(configPath)) {
1070
- return { success: false, error: 'Not logged in. Run vigthoria login first.' };
1071
- }
1072
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1073
- const authToken = config.authToken;
1074
- const apiBase = config.apiUrl || 'https://coder.vigthoria.io';
1075
- if (!authToken) {
1076
- return { success: false, error: 'No auth token found. Run vigthoria login first.' };
1077
- }
1078
-
1079
- // Build files array
1080
- let filesArray: Array<{ path: string; content: string }> = [];
1081
-
1082
- if (args.files) {
1083
- // AI passed files directly
1084
- let filesInput: any;
1085
- if (typeof args.files === 'string') {
1086
- try { filesInput = JSON.parse(args.files); } catch { filesInput = args.files; }
1087
- } else {
1088
- filesInput = args.files;
1089
- }
1090
-
1091
- if (Array.isArray(filesInput)) {
1092
- // Already in [{path, content}] format
1093
- filesArray = filesInput.map((f: any) => ({ path: f.path || f.name || 'file', content: String(f.content || '') }));
1094
- } else if (typeof filesInput === 'object' && filesInput !== null) {
1095
- // {filename: content} format - convert
1096
- for (const [filePath, content] of Object.entries(filesInput)) {
1097
- filesArray.push({ path: filePath, content: String(content) });
1098
- }
1099
- }
1100
- }
1101
-
1102
- // If no files from AI, read from disk
1103
- if (filesArray.length === 0 && fs.existsSync(targetPath)) {
1104
- const readDir = (dir: string, base: string): void => {
1105
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1106
- for (const entry of entries) {
1107
- const fullPath = path.join(dir, entry.name);
1108
- const relPath = path.relative(base, fullPath);
1109
- // Skip hidden dirs, node_modules, common non-essential dirs
1110
- if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__') continue;
1111
- if (entry.isDirectory()) {
1112
- readDir(fullPath, base);
1113
- } else if (entry.isFile()) {
1114
- try {
1115
- const content = fs.readFileSync(fullPath, 'utf-8');
1116
- filesArray.push({ path: relPath, content });
1117
- } catch { /* skip binary/unreadable files */ }
1118
- }
1119
- }
1120
- };
1121
-
1122
- const stat = fs.statSync(targetPath);
1123
- if (stat.isDirectory()) {
1124
- readDir(targetPath, targetPath);
1125
- } else {
1126
- filesArray.push({ path: path.basename(targetPath), content: fs.readFileSync(targetPath, 'utf-8') });
1127
- }
1128
- }
1129
-
1130
- if (filesArray.length === 0) {
1131
- return { success: false, error: 'No files to push. Provide files parameter or a valid path.' };
1132
- }
1133
-
1134
- // POST to Community API via Coder proxy (fallback to direct Community server)
1135
- const body = JSON.stringify({
1136
- projectName: project,
1137
- description: description || `${project} - pushed via Vigthoria AI`,
1138
- visibility,
1139
- files: filesArray,
1140
- commitMessage: `Push from Vigthoria AI: ${filesArray.length} file(s)`
1141
- });
1142
-
1143
- const headers = {
1144
- 'Authorization': `Bearer ${authToken}`,
1145
- 'Content-Type': 'application/json'
1146
- };
1147
-
1148
- // Try proxy first, then direct Community server
1149
- const pushUrls = [
1150
- `${apiBase}/api/community-repo/push`,
1151
- 'http://localhost:9000/api/repo/push'
1152
- ];
1153
-
1154
- let result: any;
1155
- let lastError = '';
1156
- for (const pushUrl of pushUrls) {
1157
- try {
1158
- const response = await fetch(pushUrl, { method: 'POST', headers, body });
1159
- result = await response.json() as any;
1160
- if (response.ok && result.success) break;
1161
- lastError = result.error || result.message || `Status ${response.status}`;
1162
- result = null;
1163
- } catch (e: any) {
1164
- lastError = e.message;
1165
- result = null;
1166
- }
1167
- }
1168
-
1169
- if (!result || !result.success) {
1170
- return {
1171
- success: false,
1172
- error: lastError || 'Push failed on all endpoints',
1173
- suggestion: 'Check your authentication and try again. Run vigthoria login to refresh your token.'
1174
- };
1175
- }
1176
-
1177
- return {
1178
- success: true,
1179
- output: `Successfully pushed "${project}" to Vigthoria Community!\n` +
1180
- `Action: ${result.action || 'created'}\n` +
1181
- `Files: ${result.filesWritten || filesArray.length}\n` +
1182
- `URL: ${result.url || `https://community.vigthoria.io/showcase/${result.projectId}`}`,
1183
- metadata: { action: 'push', project, filesWritten: result.filesWritten || filesArray.length, url: result.url }
1184
- };
1185
- }
1186
-
1187
- // All other actions use CLI commands
1188
- let command: string;
1189
-
1190
- switch (action) {
1191
- case 'pull':
1192
- if (!project) {
1193
- return {
1194
- success: false,
1195
- error: 'Project name is required for pull',
1196
- suggestion: 'Provide a project name, e.g., repo action=pull project=my-project'
1197
- };
1198
- }
1199
- command = `vigthoria repo pull "${project}"`;
1200
- break;
1201
-
1202
- case 'list':
1203
- command = 'vigthoria repo list';
1204
- break;
1205
-
1206
- case 'status':
1207
- if (!project) {
1208
- return {
1209
- success: false,
1210
- error: 'Project name is required for status',
1211
- suggestion: 'Provide a project name, e.g., repo action=status project=my-project'
1212
- };
1213
- }
1214
- command = `vigthoria repo status "${project}"`;
1215
- break;
1216
-
1217
- case 'share':
1218
- if (!project || !args.username) {
1219
- return {
1220
- success: false,
1221
- error: 'Project name and username are required for share',
1222
- suggestion: 'Provide both, e.g., repo action=share project=my-project username=collaborator permission=read'
1223
- };
1224
- }
1225
- const permission = args.permission || 'read';
1226
- command = `vigthoria repo share "${project}" "${args.username}" --permission ${permission}`;
1227
- break;
1228
-
1229
- case 'delete':
1230
- if (!project) {
1231
- return {
1232
- success: false,
1233
- error: 'Project name is required for delete',
1234
- suggestion: 'Provide a project name, e.g., repo action=delete project=my-project'
1235
- };
1236
- }
1237
- command = `vigthoria repo delete "${project}" --force`;
1238
- break;
1239
-
1240
- case 'clone':
1241
- if (!project) {
1242
- return {
1243
- success: false,
1244
- error: 'Project name is required for clone',
1245
- suggestion: 'Provide a project name, e.g., repo action=clone project=my-project path=/path/to/target'
1246
- };
1247
- }
1248
- command = `vigthoria repo clone "${project}" "${targetPath}"`;
1249
- break;
1250
-
1251
- default:
1252
- return {
1253
- success: false,
1254
- error: `Unknown repo action: ${action}`,
1255
- suggestion: 'Available actions: push, pull, list, status, share, delete, clone'
1256
- };
1257
- }
1258
-
1259
- const output = execSync(command, {
1260
- cwd: this.cwd,
1261
- encoding: 'utf-8',
1262
- timeout: 120000,
1263
- env: { ...process.env, FORCE_COLOR: '0' }
1264
- });
1265
-
1266
- return {
1267
- success: true,
1268
- output: output.trim(),
1269
- metadata: { action, project }
1270
- };
1271
- } catch (error: any) {
1272
- return {
1273
- success: false,
1274
- error: error.stderr || error.message,
1275
- suggestion: 'Make sure you are logged in with vigthoria login and have the required permissions.'
1276
- };
1277
- }
1278
- }
1279
-
1280
- /**
1281
- * Fetch URL content - Cross-platform web fetching
1282
- * Uses Node.js native fetch (available in Node 18+)
1283
- */
1284
- private async fetchUrl(args: Record<string, string>): Promise<ToolResult> {
1285
- const url = args.url;
1286
- const method = (args.method || 'GET').toUpperCase();
1287
-
1288
- // Validate URL
1289
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
1290
- return this.createErrorResult(
1291
- ToolErrorType.INVALID_ARGS,
1292
- 'URL must start with http:// or https://',
1293
- 'Provide a valid URL, e.g., https://example.com'
1294
- );
1295
- }
1296
-
1297
- try {
1298
- const fetchOptions: RequestInit = {
1299
- method,
1300
- headers: {
1301
- 'User-Agent': 'Vigthoria-CLI/1.5.7',
1302
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1303
- },
1304
- };
1305
-
1306
- // Parse custom headers if provided
1307
- if (args.headers) {
1308
- try {
1309
- const customHeaders = JSON.parse(args.headers);
1310
- fetchOptions.headers = { ...fetchOptions.headers, ...customHeaders };
1311
- } catch {
1312
- this.logger.warn('Invalid headers JSON, using defaults');
1313
- }
1314
- }
1315
-
1316
- // Add body for POST/PUT requests
1317
- if (args.body && ['POST', 'PUT', 'PATCH'].includes(method)) {
1318
- fetchOptions.body = args.body;
1319
- (fetchOptions.headers as Record<string, string>)['Content-Type'] = 'application/json';
1320
- }
1321
-
1322
- // Use AbortController for timeout
1323
- const controller = new AbortController();
1324
- const timeout = setTimeout(() => controller.abort(), 30000);
1325
- fetchOptions.signal = controller.signal;
1326
-
1327
- const response = await fetch(url, fetchOptions);
1328
- clearTimeout(timeout);
1329
-
1330
- if (!response.ok) {
1331
- return this.createErrorResult(
1332
- ToolErrorType.NETWORK_ERROR,
1333
- `HTTP ${response.status}: ${response.statusText}`,
1334
- `The server returned an error. Status: ${response.status}`
1335
- );
1336
- }
1337
-
1338
- let content = await response.text();
1339
-
1340
- // Extract content based on selector if provided (basic HTML extraction)
1341
- if (args.selector && content.includes('<')) {
1342
- const selector = args.selector.toLowerCase().trim();
1343
-
1344
- if (selector === 'title') {
1345
- const match = content.match(/<title[^>]*>([^<]*)<\/title>/i);
1346
- content = match ? match[1].trim() : 'No title found';
1347
- } else if (selector === 'meta' || selector.includes('meta[')) {
1348
- const matches = content.match(/<meta[^>]+>/gi) || [];
1349
- content = matches.length > 0 ? matches.join('\n') : 'No meta tags found';
1350
- } else if (selector === 'body' || selector === 'text') {
1351
- // Extract readable text from body
1352
- const match = content.match(/<body[^>]*>([\s\S]*)<\/body>/i);
1353
- if (match) {
1354
- content = match[1]
1355
- .replace(/<script[\s\S]*?<\/script>/gi, '')
1356
- .replace(/<style[\s\S]*?<\/style>/gi, '')
1357
- .replace(/<nav[\s\S]*?<\/nav>/gi, '')
1358
- .replace(/<header[\s\S]*?<\/header>/gi, '')
1359
- .replace(/<footer[\s\S]*?<\/footer>/gi, '')
1360
- .replace(/<[^>]+>/g, ' ')
1361
- .replace(/\s+/g, ' ')
1362
- .trim();
1363
- }
1364
- } else if (selector === 'links' || selector === 'a') {
1365
- // Extract all links
1366
- const linkRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
1367
- const links: string[] = [];
1368
- let linkMatch;
1369
- while ((linkMatch = linkRegex.exec(content)) !== null) {
1370
- const href = linkMatch[1];
1371
- const text = linkMatch[2].trim();
1372
- if (text && !href.startsWith('#') && !href.startsWith('javascript:')) {
1373
- links.push(`${text}: ${href}`);
1374
- }
1375
- }
1376
- content = links.length > 0 ? links.join('\n') : 'No links found';
1377
- } else if (selector.includes(',')) {
1378
- // Handle compound selectors like "h1, h2, h3"
1379
- const tags = selector.split(',').map(s => s.trim());
1380
- const allMatches: string[] = [];
1381
- for (const tag of tags) {
1382
- const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'gi');
1383
- let match;
1384
- while ((match = regex.exec(content)) !== null) {
1385
- const text = match[1].replace(/<[^>]+>/g, '').trim();
1386
- if (text) allMatches.push(`[${tag}] ${text}`);
1387
- }
1388
- }
1389
- content = allMatches.length > 0 ? allMatches.join('\n') : `No ${selector} tags found`;
1390
- } else if (/^h[1-6]$/.test(selector)) {
1391
- // Single heading selector
1392
- const regex = new RegExp(`<${selector}[^>]*>([\\s\\S]*?)</${selector}>`, 'gi');
1393
- const matches: string[] = [];
1394
- let match;
1395
- while ((match = regex.exec(content)) !== null) {
1396
- const text = match[1].replace(/<[^>]+>/g, '').trim();
1397
- if (text) matches.push(text);
1398
- }
1399
- content = matches.length > 0 ? matches.join('\n') : `No ${selector} tags found`;
1400
- } else if (selector === 'nav' || selector === 'nav a') {
1401
- // Extract navigation links
1402
- const navMatch = content.match(/<nav[^>]*>([\s\S]*?)<\/nav>/gi);
1403
- if (navMatch) {
1404
- const linkRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
1405
- const links: string[] = [];
1406
- for (const nav of navMatch) {
1407
- let linkMatch;
1408
- while ((linkMatch = linkRegex.exec(nav)) !== null) {
1409
- const text = linkMatch[2].trim();
1410
- if (text) links.push(`${text}: ${linkMatch[1]}`);
1411
- }
1412
- }
1413
- content = links.length > 0 ? links.join('\n') : 'No navigation links found';
1414
- } else {
1415
- content = 'No navigation found';
1416
- }
1417
- } else if (selector === 'footer') {
1418
- const match = content.match(/<footer[^>]*>([\s\S]*?)<\/footer>/i);
1419
- content = match ? match[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() : 'No footer found';
1420
- } else if (selector === 'images' || selector === 'img') {
1421
- const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*(?:alt=["']([^"']*)["'])?[^>]*>/gi;
1422
- const images: string[] = [];
1423
- let imgMatch;
1424
- while ((imgMatch = imgRegex.exec(content)) !== null) {
1425
- const src = imgMatch[1];
1426
- const alt = imgMatch[2] || 'no alt';
1427
- images.push(`${alt}: ${src}`);
1428
- }
1429
- content = images.length > 0 ? images.join('\n') : 'No images found';
1430
- } else {
1431
- // Generic tag extraction
1432
- const regex = new RegExp(`<${selector}[^>]*>([\\s\\S]*?)</${selector}>`, 'gi');
1433
- const matches: string[] = [];
1434
- let match;
1435
- while ((match = regex.exec(content)) !== null) {
1436
- matches.push(match[1].replace(/<[^>]+>/g, ' ').trim());
1437
- }
1438
- content = matches.length > 0 ? matches.join('\n\n---\n\n') : `No ${selector} elements found`;
1439
- }
1440
- }
1441
-
1442
- // Truncate if too long
1443
- if (content.length > 50000) {
1444
- content = content.substring(0, 50000) + '\n... (truncated, content too long)';
1445
- }
1446
-
1447
- return {
1448
- success: true,
1449
- output: content,
1450
- metadata: {
1451
- url,
1452
- status: response.status,
1453
- contentType: response.headers.get('content-type') || 'unknown',
1454
- contentLength: content.length,
1455
- },
1456
- };
1457
- } catch (error: any) {
1458
- if (error.name === 'AbortError') {
1459
- return this.createErrorResult(
1460
- ToolErrorType.TIMEOUT,
1461
- 'Request timed out after 30 seconds',
1462
- 'The server took too long to respond. Try again or use a different URL.'
1463
- );
1464
- }
1465
-
1466
- return this.createErrorResult(
1467
- ToolErrorType.NETWORK_ERROR,
1468
- error.message,
1469
- 'Check your internet connection and ensure the URL is correct.'
1470
- );
1471
- }
1472
- }
1473
-
1474
- /**
1475
- * Execute command via SSH on remote server
1476
- * Useful for running Unix commands from Windows
1477
- */
1478
- private async sshExec(args: Record<string, string>): Promise<ToolResult> {
1479
- const command = args.command;
1480
- const host = args.host || 'vigthoria-server';
1481
- const timeout = args.timeout ? parseInt(args.timeout) * 1000 : 60000;
1482
-
1483
- // Security checks for SSH commands
1484
- const blockedPatterns = [
1485
- /\brm\s+-rf?\s+\//i, // Dangerous rm commands
1486
- /\bsudo\s+rm/i, // Sudo rm
1487
- />\s*\/dev\/sd/i, // Writing to disk devices
1488
- /\bdd\s+.*of=\/dev/i, // dd to devices
1489
- /\bmkfs/i, // Format filesystems
1490
- /shutdown|reboot|halt|poweroff/i, // System control
1491
- ];
1492
-
1493
- for (const pattern of blockedPatterns) {
1494
- if (pattern.test(command)) {
1495
- return this.createErrorResult(
1496
- ToolErrorType.PERMISSION_DENIED,
1497
- 'This command is blocked for security reasons',
1498
- 'Dangerous system commands cannot be executed via SSH.'
1499
- );
1500
- }
1501
- }
1502
-
1503
- try {
1504
- const os = require('os');
1505
- const platform = os.platform();
1506
-
1507
- let sshCommand: string;
1508
- let execOptions: any = {
1509
- encoding: 'utf-8',
1510
- timeout,
1511
- maxBuffer: 10 * 1024 * 1024,
1512
- };
1513
-
1514
- if (platform === 'win32') {
1515
- // On Windows, use the ssh command from OpenSSH
1516
- sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${host} "${command.replace(/"/g, '\\"')}"`;
1517
- execOptions.shell = true;
1518
- } else {
1519
- // On Unix-like systems
1520
- sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${host} '${command.replace(/'/g, "'\\''")}'`;
1521
- execOptions.shell = '/bin/sh';
1522
- }
1523
-
1524
- const output = execSync(sshCommand, execOptions) as string;
1525
-
1526
- return {
1527
- success: true,
1528
- output: output.trim(),
1529
- metadata: { host, command },
1530
- };
1531
- } catch (error: any) {
1532
- // Check for common SSH errors
1533
- const errorMsg = error.stderr || error.message || '';
1534
-
1535
- if (errorMsg.includes('Connection refused') || errorMsg.includes('No route to host')) {
1536
- return this.createErrorResult(
1537
- ToolErrorType.NETWORK_ERROR,
1538
- `Cannot connect to SSH host: ${host}`,
1539
- 'Check that the SSH host is correct and the server is running.'
1540
- );
1541
- }
1542
-
1543
- if (errorMsg.includes('Permission denied') || errorMsg.includes('authentication')) {
1544
- return this.createErrorResult(
1545
- ToolErrorType.PERMISSION_DENIED,
1546
- 'SSH authentication failed',
1547
- 'Make sure you have SSH key authentication set up for this host.'
1548
- );
1549
- }
1550
-
1551
- if (error.killed) {
1552
- return this.createErrorResult(
1553
- ToolErrorType.TIMEOUT,
1554
- `SSH command timed out after ${timeout/1000}s`,
1555
- 'Try a simpler command or increase the timeout.'
1556
- );
1557
- }
1558
-
1559
- return {
1560
- success: false,
1561
- output: error.stdout || '',
1562
- error: errorMsg,
1563
- suggestion: 'Check the command syntax and ensure SSH access is configured.',
1564
- };
1565
- }
1566
- }
1567
-
1568
- /**
1569
- * Resolve and SANITIZE path - prevent path traversal outside workspace
1570
- * SECURITY: All paths MUST stay within the workspace (cwd)
1571
- */
1572
- private resolvePath(p: string): string {
1573
- // Resolve the full path (handles both relative and absolute)
1574
- let resolvedPath: string;
1575
- if (path.isAbsolute(p)) {
1576
- resolvedPath = path.normalize(p);
1577
- } else {
1578
- resolvedPath = path.normalize(path.join(this.cwd, p));
1579
- }
1580
-
1581
- // SECURITY CHECK: Ensure path is within workspace
1582
- const workspaceRoot = path.normalize(this.cwd);
1583
- if (!resolvedPath.startsWith(workspaceRoot + path.sep) && resolvedPath !== workspaceRoot) {
1584
- // Path is outside workspace - force it back to workspace
1585
- this.logger.warn(`Security: Blocked access to path outside workspace: ${p}`);
1586
- // Return the sanitized relative path within workspace
1587
- const basename = path.basename(p);
1588
- return path.join(this.cwd, basename);
1589
- }
1590
-
1591
- return resolvedPath;
1592
- }
1593
-
1594
- /**
1595
- * Check if a path is within the allowed workspace
1596
- */
1597
- private isPathWithinWorkspace(p: string): boolean {
1598
- const resolvedPath = path.normalize(path.isAbsolute(p) ? p : path.join(this.cwd, p));
1599
- const workspaceRoot = path.normalize(this.cwd);
1600
- return resolvedPath.startsWith(workspaceRoot + path.sep) || resolvedPath === workspaceRoot;
1601
- }
1602
-
1603
- /**
1604
- * Parse tool calls from AI response (Vigthoria Agent format)
1605
- * Enhanced to handle various AI output formats including malformed JSON
1606
- */
1607
- static parseToolCalls(text: string): ToolCall[] {
1608
- const calls: ToolCall[] = [];
1609
- let match;
1610
-
1611
- // Helper to extract balanced JSON from a position (handles nested braces)
1612
- const extractBalancedJson = (str: string, startIdx: number): string | null => {
1613
- if (str[startIdx] !== '{') return null;
1614
- let braceCount = 0;
1615
- let inString = false;
1616
- let escapeNext = false;
1617
-
1618
- for (let i = startIdx; i < str.length; i++) {
1619
- const char = str[i];
1620
-
1621
- if (escapeNext) {
1622
- escapeNext = false;
1623
- continue;
1624
- }
1625
-
1626
- if (char === '\\') {
1627
- escapeNext = true;
1628
- continue;
1629
- }
1630
-
1631
- if (char === '"') {
1632
- inString = !inString;
1633
- continue;
1634
- }
1635
-
1636
- if (!inString) {
1637
- if (char === '{') braceCount++;
1638
- else if (char === '}') {
1639
- braceCount--;
1640
- if (braceCount === 0) {
1641
- return str.substring(startIdx, i + 1);
1642
- }
1643
- }
1644
- }
1645
- }
1646
- return null;
1647
- };
1648
-
1649
- // Helper to fix common JSON issues from AI outputs
1650
- // IMPORTANT: Don't blindly replace quotes - it breaks code content
1651
- const fixJson = (jsonStr: string): string => {
1652
- // First, escape newlines and control characters
1653
- let fixed = jsonStr
1654
- .replace(/\r/g, '') // Remove carriage returns
1655
- .replace(/\t/g, '\\t'); // Escape tabs
1656
-
1657
- // Escape literal newlines inside strings (but not \n which is already escaped)
1658
- // We need to be careful - only escape newlines that are inside quoted strings
1659
- const parts: string[] = [];
1660
- let inString = false;
1661
- let currentPart = '';
1662
-
1663
- for (let i = 0; i < fixed.length; i++) {
1664
- const char = fixed[i];
1665
- const prevChar = i > 0 ? fixed[i - 1] : '';
1666
-
1667
- if (char === '"' && prevChar !== '\\') {
1668
- inString = !inString;
1669
- currentPart += char;
1670
- } else if (char === '\n') {
1671
- if (inString) {
1672
- currentPart += '\\n'; // Escape the newline
1673
- } else {
1674
- currentPart += char; // Keep as-is outside strings
1675
- }
1676
- } else {
1677
- currentPart += char;
1678
- }
1679
- }
1680
- fixed = currentPart;
1681
-
1682
- // Quote unquoted keys (only outside strings)
1683
- fixed = fixed.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":');
1684
-
1685
- // Remove trailing commas
1686
- fixed = fixed.replace(/,\s*}/g, '}');
1687
- fixed = fixed.replace(/,\s*]/g, ']');
1688
-
1689
- return fixed;
1690
- };
1691
-
1692
- // Normalize tool name from various formats
1693
- const normalizeToolName = (name: string): string => {
1694
- const normalized = name
1695
- .replace(/^__/, '') // Remove leading underscores
1696
- .replace(/__$/, '') // Remove trailing underscores
1697
- .replace(/^execute_/i, '') // Remove execute_ prefix
1698
- .replace(/_execute$/i, '') // Remove _execute suffix
1699
- .toLowerCase();
1700
-
1701
- // Map common variations
1702
- const toolMap: Record<string, string> = {
1703
- 'bash': 'bash',
1704
- 'shell': 'bash',
1705
- 'run': 'bash',
1706
- 'command': 'bash',
1707
- 'list_dir': 'list_dir',
1708
- 'list_directory': 'list_dir',
1709
- 'ls': 'list_dir',
1710
- 'dir': 'list_dir',
1711
- 'read_file': 'read_file',
1712
- 'readfile': 'read_file',
1713
- 'read': 'read_file',
1714
- 'write_file': 'write_file',
1715
- 'writefile': 'write_file',
1716
- 'write': 'write_file',
1717
- 'edit_file': 'edit_file',
1718
- 'editfile': 'edit_file',
1719
- 'edit': 'edit_file',
1720
- };
1721
-
1722
- return toolMap[normalized] || normalized;
1723
- };
1724
-
1725
- // Match <tool_call>...</tool_call> blocks
1726
- const toolCallRegex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
1727
- while ((match = toolCallRegex.exec(text)) !== null) {
1728
- try {
1729
- const fixed = fixJson(match[1]);
1730
- const parsed = JSON.parse(fixed);
1731
- if (parsed.tool && parsed.args) {
1732
- parsed.tool = normalizeToolName(parsed.tool);
1733
- calls.push(parsed);
1734
- }
1735
- } catch (e) {
1736
- // Invalid JSON, skip
1737
- }
1738
- }
1739
-
1740
- // Match ```tool format
1741
- const codeBlockRegex = /```tool\s*\n([\s\S]*?)\n```/g;
1742
- while ((match = codeBlockRegex.exec(text)) !== null) {
1743
- try {
1744
- const fixed = fixJson(match[1]);
1745
- const parsed = JSON.parse(fixed);
1746
- if (parsed.tool && parsed.args) {
1747
- parsed.tool = normalizeToolName(parsed.tool);
1748
- calls.push(parsed);
1749
- }
1750
- } catch (e) {
1751
- // Invalid JSON, skip
1752
- }
1753
- }
1754
-
1755
- // Match ```json blocks with tool definitions
1756
- const jsonBlockRegex = /```(?:json)?\s*\n?([\s\S]*?"tool"[\s\S]*?)\n?```/g;
1757
- while ((match = jsonBlockRegex.exec(text)) !== null) {
1758
- try {
1759
- const fixed = fixJson(match[1]);
1760
- const parsed = JSON.parse(fixed);
1761
- if (parsed.tool && parsed.args) {
1762
- parsed.tool = normalizeToolName(parsed.tool);
1763
- // Prevent duplicates
1764
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1765
- calls.push(parsed);
1766
- }
1767
- }
1768
- } catch (e) {
1769
- // Invalid JSON, skip
1770
- }
1771
- }
1772
-
1773
- // ROBUST PARSER: Find {"tool": at any position and extract balanced JSON
1774
- // This handles multi-line content in write_file and nested structures
1775
- const toolMarkerRegex = /\{"tool"\s*:/g;
1776
- while ((match = toolMarkerRegex.exec(text)) !== null) {
1777
- const startIdx = match.index;
1778
- const jsonStr = extractBalancedJson(text, startIdx);
1779
-
1780
- if (jsonStr) {
1781
- try {
1782
- const parsed = JSON.parse(jsonStr);
1783
- if (parsed.tool && parsed.args) {
1784
- parsed.tool = normalizeToolName(parsed.tool);
1785
- // Prevent duplicates
1786
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1787
- calls.push(parsed);
1788
- }
1789
- }
1790
- } catch (e) {
1791
- // Invalid JSON, try to fix it
1792
- try {
1793
- const fixed = fixJson(jsonStr);
1794
- const parsed = JSON.parse(fixed);
1795
- if (parsed.tool && parsed.args) {
1796
- parsed.tool = normalizeToolName(parsed.tool);
1797
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1798
- calls.push(parsed);
1799
- }
1800
- }
1801
- } catch (e2) {
1802
- // Still invalid - try more aggressive fixing for write_file
1803
- if (jsonStr.includes('"write_file"') || jsonStr.includes('"content"')) {
1804
- try {
1805
- // More aggressive: escape all control characters
1806
- const aggressiveFix = jsonStr
1807
- .replace(/[\x00-\x1F]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
1808
- .replace(/'/g, '"');
1809
- const parsed = JSON.parse(aggressiveFix);
1810
- if (parsed.tool && parsed.args) {
1811
- parsed.tool = normalizeToolName(parsed.tool);
1812
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1813
- calls.push(parsed);
1814
- }
1815
- }
1816
- } catch (e3) {
1817
- // Still invalid, skip
1818
- }
1819
- }
1820
- }
1821
- }
1822
- }
1823
- }
1824
-
1825
- // Match inline JSON with "tool" key (various formats - tool before args)
1826
- const inlineToolRegex = /\{[^{}]*"?tool"?\s*:\s*["']?([^"',}]+)["']?[^{}]*"?args"?\s*:\s*\{([^{}]*)\}[^{}]*\}/gi;
1827
- while ((match = inlineToolRegex.exec(text)) !== null) {
1828
- try {
1829
- const fixed = fixJson(match[0]);
1830
- const parsed = JSON.parse(fixed);
1831
- if (parsed.tool && parsed.args) {
1832
- parsed.tool = normalizeToolName(parsed.tool);
1833
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1834
- calls.push(parsed);
1835
- }
1836
- }
1837
- } catch (e) {
1838
- // Invalid JSON, skip
1839
- }
1840
- }
1841
-
1842
- // Match inline JSON with "args" BEFORE "tool" (handles reversed order from some AI models)
1843
- const inlineArgsFirstRegex = /\{[^{}]*"?args"?\s*:\s*\{([^{}]*)\}[^{}]*"?tool"?\s*:\s*["']?([^"',}]+)["']?[^{}]*\}/gi;
1844
- while ((match = inlineArgsFirstRegex.exec(text)) !== null) {
1845
- try {
1846
- const fixed = fixJson(match[0]);
1847
- const parsed = JSON.parse(fixed);
1848
- if (parsed.tool && parsed.args) {
1849
- parsed.tool = normalizeToolName(parsed.tool);
1850
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1851
- calls.push(parsed);
1852
- }
1853
- }
1854
- } catch (e) {
1855
- // Invalid JSON, skip
1856
- }
1857
- }
1858
-
1859
- // Universal: Try to find any JSON object with both "tool" and "args" keys
1860
- const universalJsonRegex = /\{[^{}]*(?:"tool"|"args")[^{}]*(?:"tool"|"args")[^{}]*\}/gi;
1861
- while ((match = universalJsonRegex.exec(text)) !== null) {
1862
- try {
1863
- const fixed = fixJson(match[0]);
1864
- const parsed = JSON.parse(fixed);
1865
- if (parsed.tool && parsed.args) {
1866
- parsed.tool = normalizeToolName(parsed.tool);
1867
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1868
- calls.push(parsed);
1869
- }
1870
- }
1871
- } catch (e) {
1872
- // Invalid JSON, skip
1873
- }
1874
- }
1875
-
1876
- // Fallback: Try to parse each line as JSON (for plain JSON output without code blocks)
1877
- // This handles cases where AI outputs just: {"args": {...}, "tool": "..."}
1878
- const lines = text.split('\n');
1879
- for (const line of lines) {
1880
- const trimmed = line.trim();
1881
- if (trimmed.startsWith('{') && trimmed.includes('"tool"') && trimmed.includes('"args"')) {
1882
- try {
1883
- // Try to find balanced braces
1884
- let braceCount = 0;
1885
- let startIdx = -1;
1886
- let endIdx = -1;
1887
- for (let i = 0; i < trimmed.length; i++) {
1888
- if (trimmed[i] === '{') {
1889
- if (startIdx === -1) startIdx = i;
1890
- braceCount++;
1891
- } else if (trimmed[i] === '}') {
1892
- braceCount--;
1893
- if (braceCount === 0 && startIdx !== -1) {
1894
- endIdx = i + 1;
1895
- break;
1896
- }
1897
- }
1898
- }
1899
- if (startIdx !== -1 && endIdx !== -1) {
1900
- const jsonStr = trimmed.substring(startIdx, endIdx);
1901
- const fixed = fixJson(jsonStr);
1902
- const parsed = JSON.parse(fixed);
1903
- if (parsed.tool && parsed.args) {
1904
- parsed.tool = normalizeToolName(parsed.tool);
1905
- if (!calls.some(c => c.tool === parsed.tool && JSON.stringify(c.args) === JSON.stringify(parsed.args))) {
1906
- calls.push(parsed);
1907
- }
1908
- }
1909
- }
1910
- } catch (e) {
1911
- // Invalid JSON, skip
1912
- }
1913
- }
1914
- }
1915
-
1916
- // Parse Vigthoria V2 format: {"tool": "__BASH__", ...}
1917
- const vigV2Regex = /"?tool"?\s*:\s*["']__?([A-Za-z_]+)__?["']/gi;
1918
- while ((match = vigV2Regex.exec(text)) !== null) {
1919
- try {
1920
- const toolName = normalizeToolName(match[1]);
1921
- // Extract args from nearby context
1922
- const pathMatch = text.match(/"?(?:arg_)?path"?\s*:\s*["']([^"']+)["']/i);
1923
- const cmdMatch = text.match(/"?command"?\s*:\s*(?:["']([^"']+)["']|\[\s*["']([^"']+)["']\s*\])/i);
1924
- const contentMatch = text.match(/"?content"?\s*:\s*["']([^"']+)["']/i);
1925
-
1926
- const args: Record<string, string> = {};
1927
- if (pathMatch) args.path = pathMatch[1];
1928
- if (cmdMatch) args.command = cmdMatch[1] || cmdMatch[2];
1929
- if (contentMatch) args.content = contentMatch[1];
1930
-
1931
- if (Object.keys(args).length > 0) {
1932
- // Prevent duplicates
1933
- if (!calls.some(c => c.tool === toolName && JSON.stringify(c.args) === JSON.stringify(args))) {
1934
- calls.push({ tool: toolName, args });
1935
- }
1936
- }
1937
- } catch (e) {
1938
- // Skip
1939
- }
1940
- }
1941
-
1942
- return calls;
1943
- }
1944
-
1945
- /**
1946
- * Get tools formatted for AI system prompt
1947
- */
1948
- static getToolsForPrompt(): string {
1949
- const tools = AgenticTools.getToolDefinitions();
1950
-
1951
- let prompt = `## AGENT MODE - YOU MUST USE TOOLS
1952
-
1953
- ⚠️ CRITICAL: You are in Agent Mode. You MUST use tools to interact with files and the system.
1954
- DO NOT guess or hallucinate file contents. DO NOT make up directory structures.
1955
- ALWAYS use read_file before discussing what's in a file.
1956
- ALWAYS use list_dir before discussing what's in a directory.
1957
-
1958
- You have access to these tools:
1959
-
1960
- `;
1961
-
1962
- for (const tool of tools) {
1963
- prompt += `## ${tool.name}
1964
- ${tool.description}
1965
- Parameters:
1966
- ${tool.parameters.map(p => ` - ${p.name}${p.required ? ' (required)' : ''}: ${p.description}`).join('\n')}
1967
-
1968
- `;
1969
- }
1970
-
1971
- prompt += `
1972
- ## How to Use Tools
1973
-
1974
- To use a tool, output a JSON block in a code fence with "tool" language:
1975
-
1976
- \`\`\`tool
1977
- {"tool": "tool_name", "args": {"param1": "value1"}}
1978
- \`\`\`
1979
-
1980
- ## MANDATORY TOOL USAGE (DO NOT SKIP!)
1981
-
1982
- **When user asks "what's in this folder/directory":**
1983
- → FIRST use list_dir, THEN respond based on actual results
1984
-
1985
- **When user asks "can you see/read/show me file X":**
1986
- → FIRST use read_file, THEN respond based on actual contents
1987
-
1988
- **When user mentions a path like "C:\\some\\path" or "/some/path":**
1989
- → FIRST use list_dir to check if it exists, THEN respond
1990
-
1991
- **NEVER say things like "Here's an overview of the contents..." without first using tools!**
1992
- **NEVER describe files or code you haven't actually read with read_file!**
1993
-
1994
- ### Examples:
1995
-
1996
- 1. List directory contents:
1997
- \`\`\`tool
1998
- {"tool": "list_dir", "args": {"path": "."}}
1999
- \`\`\`
2000
-
2001
- 2. Read a file:
2002
- \`\`\`tool
2003
- {"tool": "read_file", "args": {"path": "src/index.js"}}
2004
- \`\`\`
2005
-
2006
- 3. Fetch a web page (full HTML):
2007
- \`\`\`tool
2008
- {"tool": "fetch_url", "args": {"url": "https://example.com"}}
2009
- \`\`\`
2010
-
2011
- 4. Fetch specific content with selectors:
2012
- \`\`\`tool
2013
- {"tool": "fetch_url", "args": {"url": "https://example.com", "selector": "h1, h2, h3"}}
2014
- \`\`\`
2015
- Available selectors: title, body, text, links, nav, footer, images, h1, h2, h3, h1/h2/h3 (compound)
2016
-
2017
- 5. Run command on YOUR configured SSH server (requires user SSH setup, NOT Vigthoria servers):
2018
- \`\`\`tool
2019
- {"tool": "ssh_exec", "args": {"command": "curl -s https://example.com | head -20", "host": "your-server"}}
2020
- \`\`\`
2021
- Note: ssh_exec is for users who have their own servers configured. It does NOT connect to Vigthoria infrastructure.
2022
-
2023
- 6. Write a file:
2024
- \`\`\`tool
2025
- {"tool": "write_file", "args": {"path": "hello.py", "content": "print('Hello World')"}}
2026
- \`\`\`
2027
-
2028
- ## CRITICAL RULES:
2029
-
2030
- ### ABSOLUTELY NO HALLUCINATION (READ THIS CAREFULLY):
2031
- - ONLY use information from tool results you just received in THIS conversation
2032
- - DO NOT make up names, organizations, or content that wasn't in the fetched data
2033
- - DO NOT read random files from the workspace expecting them to contain relevant data
2034
- - If you fetch a website, your analysis MUST be based ONLY on what you just fetched
2035
- - If the user asks about websites A and B, ONLY discuss A and B - don't invent Site C
2036
- - NEVER create fictional organizations, services, or content
2037
- - If you're unsure about something, say "Based on the fetched content, I can see..." not "This organization does..."
2038
-
2039
- ### Strategic Planning (VERY IMPORTANT):
2040
- - PLAN before acting - don't issue many redundant tool calls
2041
- - For web comparisons: Fetch each URL ONCE with no selector to get full HTML, then analyze locally
2042
- - Do NOT fetch the same URL multiple times with different selectors - fetch once and parse the result
2043
- - Think step-by-step: 1) Gather data, 2) Analyze it, 3) Present findings
2044
- - If comparing two things, fetch both ONCE, then write a REAL comparison with specific differences
2045
- - Maximum 2-4 tool calls per step is usually enough - don't spam 10+ calls at once
2046
- - Do NOT use list_dir or read_file when the task is about fetching websites - those are for LOCAL files only
2047
-
2048
- ### Cross-Platform Compatibility:
2049
- - On Windows, Unix commands (head, tail, grep, awk, sed, wc) are NOT available
2050
- - Use \`fetch_url\` for web requests instead of curl|grep
2051
- - Use \`ssh_exec\` to run Unix commands on the Vigthoria server if needed
2052
- - Use \`read_file\` instead of cat
2053
- - Use \`list_dir\` instead of ls
2054
-
2055
- ### Handling Tool Failures:
2056
- - If a tool fails, REPORT the failure honestly to the user
2057
- - NEVER make up or hallucinate content when a tool fails
2058
- - If you cannot access a URL, say "I was unable to fetch the URL because..."
2059
- - If a command fails, explain what happened and suggest alternatives
2060
- - Do NOT write analysis or comparison reports if you couldn't gather the actual data
2061
-
2062
- ### Comparison Tasks:
2063
- - When asked to compare websites/files, you MUST produce actual specific differences
2064
- - Don't just describe each site separately - show what Site A has that Site B is missing
2065
- - Use concrete examples: "Site A has a Team page at /team.html, Site B does not"
2066
- - If you need sub-pages, fetch them in follow-up steps after analyzing the main page
2067
- - BASE YOUR COMPARISON ONLY ON THE DATA YOU FETCHED - not on random files in the workspace
2068
- - Quote actual text from the fetched content to support your claims
2069
-
2070
- ### File Access:
2071
- - You can ONLY access files within the current project workspace
2072
- - Use relative paths (e.g., "src/file.js", "app.py", "./config.json")
2073
- - Never try to access system files or directories outside the workspace
2074
- - Do NOT read files unrelated to the user's request (e.g., don't read "comparison.md" when asked to fetch websites)
2075
- - When comparing WEBSITES, use fetch_url - do NOT use read_file or list_dir
2076
-
2077
- ### Tool Names:
2078
- - Use ONLY these exact tool names: list_dir, read_file, write_file, edit_file, bash, grep, glob, git, repo, fetch_url, ssh_exec
2079
- - The JSON must be valid with double quotes for all keys and string values
2080
- - After tool execution, you will receive results and can continue with the next step
2081
- - Explain what you're doing before using tools
2082
- `;
2083
-
2084
- return prompt;
2085
- }
2086
- }