vigthoria-cli 1.6.51 → 1.6.53

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.
package/README.md CHANGED
@@ -24,6 +24,11 @@ AI-powered terminal coding assistant integrated with Vigthoria's AI models and s
24
24
  - 🔐 **Secure Auth** - Integration with Vigthoria accounts
25
25
  - 📦 **Project Context** - Understands your codebase
26
26
  - 🗂️ **Vigthoria Repo** - Push/Pull projects to your personal cloud repository
27
+ - 🔍 **Deep Codebase Search** - Indexed search across files, symbols, and content via ripgrep
28
+ - ✏️ **Atomic Multi-File Edits** - Coordinated changes across multiple files with rollback
29
+ - 🧩 **Sub-Agent Delegation** - Spawn focused sub-agents for parallel investigation
30
+ - 🔒 **Persistent Permissions** - Per-project tool approvals saved across sessions
31
+ - 📝 **Multi-Line Input** - Block mode ({{{ }}}) and line continuation (\\) for complex prompts
27
32
 
28
33
  ## Installation
29
34
 
@@ -96,7 +101,7 @@ If you see `ENOTFOUND registry.npmjs.org`, try these solutions:
96
101
  4. **Direct tarball download:**
97
102
  ```bash
98
103
  # Download directly
99
- npm install -g https://cli.vigthoria.io/downloads/vigthoria-cli-1.6.12.tgz
104
+ npm install -g https://cli.vigthoria.io/downloads/vigthoria-cli-1.6.52.tgz
100
105
  ```
101
106
 
102
107
  5. **Use Git clone method (no npm registry needed):**
@@ -1644,6 +1644,7 @@ class ChatCommand {
1644
1644
  : 'Interactive Chat';
1645
1645
  this.logger.section(this.workflowTarget ? `${chatTitle} Via Workflow Target` : chatTitle);
1646
1646
  console.log(chalk_1.default.gray('Type /help for commands. Type /exit to quit.'));
1647
+ console.log(chalk_1.default.gray('Multi-line: end a line with \\ or start a block with {{{ and end with }}}'));
1647
1648
  if (this.workflowTarget) {
1648
1649
  console.log(chalk_1.default.gray(`Workflow target: ${this.workflowTarget}`));
1649
1650
  }
@@ -1651,10 +1652,40 @@ class ChatCommand {
1651
1652
  input: process.stdin,
1652
1653
  output: process.stdout,
1653
1654
  });
1654
- while (true) {
1655
- const input = await new Promise((resolve) => {
1655
+ // Multi-line input helper
1656
+ const readMultiLineInput = async () => {
1657
+ const lines = [];
1658
+ let firstLine = await new Promise((resolve) => {
1656
1659
  rl.question(chalk_1.default.blue('> '), resolve);
1657
1660
  });
1661
+ // Check for {{{ block mode
1662
+ if (firstLine.trim() === '{{{' || firstLine.trim().endsWith('{{{')) {
1663
+ if (firstLine.trim() !== '{{{') {
1664
+ lines.push(firstLine.trim().replace(/\{\{\{$/, '').trim());
1665
+ }
1666
+ console.log(chalk_1.default.gray(' (multi-line mode: type }}} on its own line to finish)'));
1667
+ while (true) {
1668
+ const line = await new Promise((resolve) => {
1669
+ rl.question(chalk_1.default.gray(' '), resolve);
1670
+ });
1671
+ if (line.trim() === '}}}')
1672
+ break;
1673
+ lines.push(line);
1674
+ }
1675
+ return lines.join('\n');
1676
+ }
1677
+ // Check for backslash continuation
1678
+ while (firstLine.endsWith('\\')) {
1679
+ lines.push(firstLine.slice(0, -1));
1680
+ firstLine = await new Promise((resolve) => {
1681
+ rl.question(chalk_1.default.gray(' '), resolve);
1682
+ });
1683
+ }
1684
+ lines.push(firstLine);
1685
+ return lines.join('\n');
1686
+ };
1687
+ while (true) {
1688
+ const input = await readMultiLineInput();
1658
1689
  const trimmed = input.trim();
1659
1690
  if (!trimmed) {
1660
1691
  continue;
@@ -2193,7 +2224,7 @@ class ChatCommand {
2193
2224
  // Matches "Tool <name> succeeded/FAILED." through the next blank line,
2194
2225
  // next tool header, or end-of-string. The DOTALL-like [\s\S]*? is
2195
2226
  // terminated by whichever boundary comes first.
2196
- cleaned = cleaned.replace(/Tool (?:read_file|grep|list_dir|glob|bash|write_file|edit_file|ssh_exec) (?:succeeded|FAILED)\.[\s\S]*?(?=\nTool |\n\n|$)/g, '');
2227
+ cleaned = cleaned.replace(/Tool (?:read_file|grep|list_dir|glob|bash|write_file|edit_file|ssh_exec|task|multi_edit|codebase_search) (?:succeeded|FAILED)\.[\s\S]*?(?=\nTool |\n\n|$)/g, '');
2197
2228
  // ── Phase 2: Strip echoed system-prompt / grounding lines ──
2198
2229
  const contaminationPatterns = [
2199
2230
  /^\[Agent recovered from backend failure[^\]]*\]\s*/m,
@@ -2382,10 +2413,10 @@ class ChatCommand {
2382
2413
  if (finalStatus === 'search_failed') {
2383
2414
  this.agentToolEvidence.searchFailed += 1;
2384
2415
  }
2385
- else if (/^(read_file|list_dir|glob|grep|git|repo|fetch_url)$/.test(call.tool)) {
2416
+ else if (/^(read_file|list_dir|glob|grep|git|repo|fetch_url|codebase_search)$/.test(call.tool)) {
2386
2417
  this.agentToolEvidence.discovery += 1;
2387
2418
  }
2388
- else if (/^(write_file|edit_file|bash|ssh_exec)$/.test(call.tool)) {
2419
+ else if (/^(write_file|edit_file|bash|ssh_exec|multi_edit|task)$/.test(call.tool)) {
2389
2420
  this.agentToolEvidence.mutation += 1;
2390
2421
  }
2391
2422
  }
@@ -2442,13 +2473,16 @@ class ChatCommand {
2442
2473
  });
2443
2474
  console.log(action);
2444
2475
  const answer = await new Promise((resolve) => {
2445
- rl.question(chalk_1.default.yellow('Approve? [y]es / [n]o / [a]ll this turn: '), resolve);
2476
+ rl.question(chalk_1.default.yellow('Approve? [y]es / [n]o / [a]ll this turn / [p]ersist: '), resolve);
2446
2477
  });
2447
2478
  rl.close();
2448
2479
  const normalized = answer.trim().toLowerCase();
2449
2480
  if (normalized === 'a' || normalized === 'all') {
2450
2481
  return 'batch';
2451
2482
  }
2483
+ if (normalized === 'p' || normalized === 'persist') {
2484
+ return 'persist';
2485
+ }
2452
2486
  return normalized === 'y' || normalized === 'yes';
2453
2487
  }
2454
2488
  getCurrentSessionInfo() {
@@ -59,6 +59,7 @@ export declare class Logger {
59
59
  user(message: string): void;
60
60
  code(code: string, language?: string): void;
61
61
  diff(added: string[], removed: string[]): void;
62
+ unifiedDiff(filePath: string, oldText: string, newText: string): void;
62
63
  section(title: string): void;
63
64
  progress(message: string): void;
64
65
  clearLine(): void;
@@ -107,11 +107,61 @@ class Logger {
107
107
  console.log(chalk_1.default.yellow(code));
108
108
  console.log(chalk_1.default.gray(exports.CH.hLine.repeat(60)));
109
109
  }
110
- // Diff output
110
+ // Diff output - enhanced with syntax-highlighted unified diff
111
111
  diff(added, removed) {
112
112
  removed.forEach(line => console.log(chalk_1.default.red(`- ${line}`)));
113
113
  added.forEach(line => console.log(chalk_1.default.green(`+ ${line}`)));
114
114
  }
115
+ // Unified diff display with file context
116
+ unifiedDiff(filePath, oldText, newText) {
117
+ const oldLines = oldText.split('\n');
118
+ const newLines = newText.split('\n');
119
+ const fileName = filePath.split('/').pop() || filePath;
120
+ console.log(chalk_1.default.bold.white(`\n ${exports.CH.hLine.repeat(3)} ${fileName} ${exports.CH.hLine.repeat(Math.max(1, 50 - fileName.length))}`));
121
+ console.log(chalk_1.default.gray(` --- a/${filePath}`));
122
+ console.log(chalk_1.default.gray(` +++ b/${filePath}`));
123
+ // Find diff ranges with context
124
+ const contextLines = 3;
125
+ let i = 0;
126
+ let j = 0;
127
+ while (i < oldLines.length || j < newLines.length) {
128
+ if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
129
+ i++;
130
+ j++;
131
+ continue;
132
+ }
133
+ // Found a difference - show context
134
+ const startOld = Math.max(0, i - contextLines);
135
+ const startNew = Math.max(0, j - contextLines);
136
+ // Context before
137
+ for (let c = startOld; c < i; c++) {
138
+ console.log(chalk_1.default.gray(` ${String(c + 1).padStart(4)} │ ${oldLines[c] || ''}`));
139
+ }
140
+ // Removed lines
141
+ while (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
142
+ console.log(chalk_1.default.red(` ${String(i + 1).padStart(4)} │-${oldLines[i]}`));
143
+ i++;
144
+ if (j < newLines.length && (i >= oldLines.length || oldLines[i] === newLines[j]))
145
+ break;
146
+ }
147
+ // Added lines
148
+ while (j < newLines.length && (i >= oldLines.length || oldLines[i] !== newLines[j])) {
149
+ console.log(chalk_1.default.green(` ${String(j + 1).padStart(4)} │+${newLines[j]}`));
150
+ j++;
151
+ if (i < oldLines.length && oldLines[i] === newLines[j])
152
+ break;
153
+ }
154
+ // Context after
155
+ const endCtx = Math.min(oldLines.length, i + contextLines);
156
+ for (let c = i; c < endCtx && c < oldLines.length && j < newLines.length && oldLines[c] === newLines[j]; c++) {
157
+ console.log(chalk_1.default.gray(` ${String(c + 1).padStart(4)} │ ${oldLines[c] || ''}`));
158
+ i = c + 1;
159
+ j++;
160
+ }
161
+ console.log(chalk_1.default.gray(` ${exports.CH.hLine.repeat(55)}`));
162
+ }
163
+ console.log();
164
+ }
115
165
  // Section header
116
166
  section(title) {
117
167
  console.log();
@@ -71,9 +71,22 @@ export declare class AgenticTools {
71
71
  private maxUndoStack;
72
72
  private retryConfig;
73
73
  private sessionApprovedTools;
74
+ private static permissionsFile;
74
75
  constructor(logger: Logger, cwd: string, permissionCallback: (action: string, options?: {
75
76
  batchApproval?: boolean;
76
- }) => Promise<boolean | 'batch'>, autoApprove?: boolean);
77
+ }) => Promise<boolean | 'batch' | 'persist'>, autoApprove?: boolean);
78
+ /**
79
+ * Load persistent permissions for the current project
80
+ */
81
+ private loadPersistentPermissions;
82
+ /**
83
+ * Save a persistent permission for a tool in the current project
84
+ */
85
+ private savePersistentPermission;
86
+ /**
87
+ * Check if a tool has persistent permission for the current project
88
+ */
89
+ private hasPersistentPermission;
77
90
  /**
78
91
  * Clear session-approved tools (call this at the start of each new AI turn)
79
92
  */
@@ -194,6 +207,9 @@ export declare class AgenticTools {
194
207
  * Check if a path is within the allowed workspace
195
208
  */
196
209
  private isPathWithinWorkspace;
210
+ private task;
211
+ private multiEdit;
212
+ private codebaseSearch;
197
213
  /**
198
214
  * Parse tool calls from AI response (Vigthoria Agent format)
199
215
  * Enhanced to handle various AI output formats including malformed JSON
@@ -102,6 +102,19 @@ const TOOL_ARG_ALIASES = {
102
102
  command: ['cmd', 'script'],
103
103
  host: ['server'],
104
104
  },
105
+ task: {
106
+ description: ['prompt', 'task', 'query', 'instructions'],
107
+ working_dir: ['cwd', 'dir', 'directory', 'workDir'],
108
+ },
109
+ multi_edit: {
110
+ edits: ['changes', 'operations', 'replacements'],
111
+ },
112
+ codebase_search: {
113
+ query: ['search', 'pattern', 'text', 'symbol'],
114
+ scope: ['type', 'mode'],
115
+ include: ['includePattern', 'filePattern', 'glob'],
116
+ max_results: ['limit', 'maxResults', 'count'],
117
+ },
105
118
  };
106
119
  // Error types for better handling
107
120
  var ToolErrorType;
@@ -129,12 +142,59 @@ class AgenticTools {
129
142
  };
130
143
  // Session-based tool approvals - remembers which tools user approved for this turn
131
144
  sessionApprovedTools = new Set();
145
+ // Persistent permissions - tool allowlists per project
146
+ static permissionsFile = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.vigthoria', 'permissions.json');
132
147
  constructor(logger, cwd, permissionCallback, autoApprove = false) {
133
148
  this.logger = logger;
134
149
  this.cwd = cwd;
135
150
  this.permissionCallback = permissionCallback;
136
151
  this.autoApprove = autoApprove;
137
152
  }
153
+ /**
154
+ * Load persistent permissions for the current project
155
+ */
156
+ loadPersistentPermissions() {
157
+ try {
158
+ if (fs.existsSync(AgenticTools.permissionsFile)) {
159
+ return JSON.parse(fs.readFileSync(AgenticTools.permissionsFile, 'utf-8'));
160
+ }
161
+ }
162
+ catch {
163
+ // Corrupted file - ignore
164
+ }
165
+ return {};
166
+ }
167
+ /**
168
+ * Save a persistent permission for a tool in the current project
169
+ */
170
+ savePersistentPermission(toolName) {
171
+ const permissions = this.loadPersistentPermissions();
172
+ const projectKey = this.cwd;
173
+ if (!permissions[projectKey]) {
174
+ permissions[projectKey] = { tools: [], updatedAt: new Date().toISOString() };
175
+ }
176
+ if (!permissions[projectKey].tools.includes(toolName)) {
177
+ permissions[projectKey].tools.push(toolName);
178
+ permissions[projectKey].updatedAt = new Date().toISOString();
179
+ }
180
+ try {
181
+ const dir = path.dirname(AgenticTools.permissionsFile);
182
+ if (!fs.existsSync(dir))
183
+ fs.mkdirSync(dir, { recursive: true });
184
+ fs.writeFileSync(AgenticTools.permissionsFile, JSON.stringify(permissions, null, 2), 'utf-8');
185
+ }
186
+ catch {
187
+ // Non-critical - permission won't persist
188
+ }
189
+ }
190
+ /**
191
+ * Check if a tool has persistent permission for the current project
192
+ */
193
+ hasPersistentPermission(toolName) {
194
+ const permissions = this.loadPersistentPermissions();
195
+ const projectKey = this.cwd;
196
+ return permissions[projectKey]?.tools?.includes(toolName) || false;
197
+ }
138
198
  /**
139
199
  * Clear session-approved tools (call this at the start of each new AI turn)
140
200
  */
@@ -351,6 +411,43 @@ class AgenticTools {
351
411
  riskLevel: 'high',
352
412
  category: 'execute',
353
413
  },
414
+ {
415
+ name: 'task',
416
+ description: 'Launch an independent sub-agent to handle a complex subtask. The sub-agent has its own context and tool access, runs autonomously, and returns a single result. Use this to parallelize work or delegate research.',
417
+ parameters: [
418
+ { name: 'description', description: 'A detailed description of the subtask for the sub-agent to perform', required: true },
419
+ { name: 'working_dir', description: 'Working directory for the sub-agent (relative to project root)', required: false },
420
+ ],
421
+ requiresPermission: true,
422
+ dangerous: false,
423
+ riskLevel: 'medium',
424
+ category: 'execute',
425
+ },
426
+ {
427
+ name: 'multi_edit',
428
+ description: 'Apply multiple file edits atomically. All edits succeed or all are rolled back. Each edit specifies a file, old_text to find, and new_text to replace it with.',
429
+ parameters: [
430
+ { name: 'edits', description: 'JSON array of edits: [{"path": "file.ts", "old_text": "find this", "new_text": "replace with"}]', required: true },
431
+ ],
432
+ requiresPermission: true,
433
+ dangerous: false,
434
+ riskLevel: 'medium',
435
+ category: 'write',
436
+ },
437
+ {
438
+ name: 'codebase_search',
439
+ description: 'Perform a deep semantic search across the entire codebase. Searches file names, symbol names (functions, classes, variables), and content across all files - not limited to the workspace snapshot. Use for large projects.',
440
+ parameters: [
441
+ { name: 'query', description: 'Search query - can be a symbol name, concept, or natural language description', required: true },
442
+ { name: 'scope', description: 'Search scope: "symbols" (functions/classes), "files" (filenames), "content" (full-text), or "all" (default)', required: false },
443
+ { name: 'include', description: 'File pattern to include (e.g., "*.ts", "src/**/*.js")', required: false },
444
+ { name: 'max_results', description: 'Maximum results to return (default: 30)', required: false },
445
+ ],
446
+ requiresPermission: false,
447
+ dangerous: false,
448
+ riskLevel: 'low',
449
+ category: 'search',
450
+ },
354
451
  ];
355
452
  }
356
453
  /**
@@ -373,8 +470,11 @@ class AgenticTools {
373
470
  }
374
471
  // Check permission for dangerous/modifying actions
375
472
  if (tool.requiresPermission && !this.autoApprove) {
376
- // Check if this tool was already approved for this session/turn
377
- if (this.sessionApprovedTools.has(normalizedCall.tool)) {
473
+ // Check persistent permissions first (project-scoped), then session
474
+ if (this.hasPersistentPermission(normalizedCall.tool)) {
475
+ this.logger.info(`${call.tool}: Auto-approved (persistent)`);
476
+ }
477
+ else if (this.sessionApprovedTools.has(normalizedCall.tool)) {
378
478
  // Already approved - skip permission prompt
379
479
  this.logger.info(`${call.tool}: Auto-approved (batch)`);
380
480
  }
@@ -387,11 +487,17 @@ class AgenticTools {
387
487
  canRetry: true,
388
488
  };
389
489
  }
390
- // Only add to session approvals if user chose 'batch' (typed 'a')
391
- // 'y' or 'yes' only approves this single request
490
+ // 'batch' (typed 'a') = approve for this session
491
+ // 'persist' (typed 'p') = approve permanently for this project
492
+ // 'y' or 'yes' = approve this single request only
392
493
  if (approved === 'batch') {
393
494
  this.sessionApprovedTools.add(normalizedCall.tool);
394
495
  }
496
+ else if (approved === 'persist') {
497
+ this.sessionApprovedTools.add(normalizedCall.tool);
498
+ this.savePersistentPermission(normalizedCall.tool);
499
+ this.logger.info(`${normalizedCall.tool}: Saved as persistent permission`);
500
+ }
395
501
  }
396
502
  }
397
503
  // Execute with retry logic for applicable operations
@@ -497,6 +603,12 @@ class AgenticTools {
497
603
  return this.fetchUrl(call.args);
498
604
  case 'ssh_exec':
499
605
  return this.sshExec(call.args);
606
+ case 'task':
607
+ return this.task(call.args);
608
+ case 'multi_edit':
609
+ return this.multiEdit(call.args);
610
+ case 'codebase_search':
611
+ return this.codebaseSearch(call.args);
500
612
  default:
501
613
  return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Tool not implemented: ${call.tool}`);
502
614
  }
@@ -772,6 +884,8 @@ class AgenticTools {
772
884
  if (this.undoStack.length > 50) {
773
885
  this.undoStack.shift();
774
886
  }
887
+ // Show syntax-highlighted diff
888
+ this.logger.unifiedDiff(args.path, args.old_text, args.new_text);
775
889
  return {
776
890
  success: true,
777
891
  output: `File edited: ${args.path}`,
@@ -921,20 +1035,50 @@ class AgenticTools {
921
1035
  }
922
1036
  }
923
1037
  // SECURITY: Block dangerous commands that could access outside workspace
1038
+ // HARDENED: Protects Vigthoria ecosystem, server config, and system files
924
1039
  const blockedPatterns = [
1040
+ // === System file access ===
925
1041
  /\bcat\s+\/etc\//i, // Reading system files
926
1042
  /\bcat\s+\/var\/(?!log)/i, // Reading /var/ except logs
927
1043
  /\bcat\s+\/root\//i, // Reading root home
928
1044
  /\bcat\s+\/home\/[^\/]+\/\.[^\/]/i, // Reading hidden files in home dirs
929
1045
  /\bcat\s+~\/\./i, // Reading hidden files via ~
930
1046
  /\bcd\s+(\/etc|\/var\/www|\/root|\/home)/i, // CD to sensitive dirs
1047
+ // === Destructive operations ===
931
1048
  /\brm\s+-rf?\s+\//i, // Dangerous rm commands
1049
+ /\brm\s+-rf?\s+\.\.\//i, // rm going up directories
1050
+ /\brm\s+-rf?\s+~\//i, // rm in home directory
932
1051
  /\b(curl|wget).*\|\s*(bash|sh)\b/i, // Downloading and executing scripts
933
1052
  /\bsudo\b/i, // Sudo commands
934
1053
  /\bsu\s+-/i, // Su to root
935
1054
  /\bchmod\s+[0-7]*777/i, // World-writable permissions
936
1055
  /\/(var\/www|opt)\/[a-z]+-?(database|models|coder|mcp|operator|voice|music)/i, // Vigthoria ecosystem
937
1056
  /vigthoria-(models|database|mcp|operator|voice|music)/i, // Vigthoria project names
1057
+ // === Vigthoria Server Protection (CRITICAL) ===
1058
+ /\/var\/www\/(?!$)/i, // Block ANY access to /var/www/* (server web root)
1059
+ /\/etc\/(caddy|nginx|apache|systemd|pm2)/i, // Server config files
1060
+ /\/etc\/(passwd|shadow|group|sudoers)/i, // System auth files
1061
+ /\b(systemctl|service)\s+(start|stop|restart|enable|disable|reload)/i, // Service control
1062
+ /\bpm2\s+(start|stop|restart|delete|kill|flush)/i, // PM2 process management
1063
+ /\bjournalctl\b/i, // System logs access
1064
+ /\bnpm\s+publish\b/i, // Block npm publish from agent
1065
+ /\bdocker\s+(rm|kill|stop|exec|run)/i, // Docker container manipulation
1066
+ /\biptables\b/i, // Firewall rules
1067
+ /\bufw\s+(allow|deny|delete|disable)/i, // UFW firewall
1068
+ /\bssh\s+.*vigthoria/i, // SSH to Vigthoria servers
1069
+ /\bssh\s+.*78\.46\.154/i, // SSH to known Vigthoria IP
1070
+ /\.env\b/i, // Environment files (may contain secrets)
1071
+ /\bprivate[_-]?key|secret[_-]?key|api[_-]?key|auth[_-]?token/i, // Sensitive variable patterns in commands
1072
+ /\bcrontab\b/i, // Cron manipulation
1073
+ /\bmount\s/i, // Mount filesystems
1074
+ /\bmkfs\b/i, // Format filesystems
1075
+ />\s*\/dev\/sd/i, // Writing to disk devices
1076
+ /\bdd\s+.*of=/i, // dd write operations
1077
+ /\bnode\s+-e\b/i, // Node eval (sandbox bypass)
1078
+ /\bnode\s+--eval\b/i, // Node eval long form
1079
+ /\bpython3?\s+-c\b/i, // Python exec (sandbox bypass)
1080
+ /\bruby\s+-e\b/i, // Ruby eval (sandbox bypass)
1081
+ /\bperl\s+-e\b/i, // Perl eval (sandbox bypass)
938
1082
  ];
939
1083
  for (const pattern of blockedPatterns) {
940
1084
  if (pattern.test(args.command)) {
@@ -1836,6 +1980,357 @@ class AgenticTools {
1836
1980
  const workspaceRoot = path.normalize(this.cwd);
1837
1981
  return resolvedPath.startsWith(workspaceRoot + path.sep) || resolvedPath === workspaceRoot;
1838
1982
  }
1983
+ // ═══════════════════════════════════════════════════════════════
1984
+ // TASK (Sub-Agent) Tool
1985
+ // ═══════════════════════════════════════════════════════════════
1986
+ async task(args) {
1987
+ const description = args.description;
1988
+ const workingDir = args.working_dir
1989
+ ? this.resolvePath(args.working_dir)
1990
+ : this.cwd;
1991
+ if (!this.isPathWithinWorkspace(workingDir)) {
1992
+ return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, 'Sub-agent working directory must be within the project workspace', 'Provide a relative path within the current project.');
1993
+ }
1994
+ this.logger.info(`Spawning sub-agent: ${description.substring(0, 80)}...`);
1995
+ // Create a scoped sub-agent with its own tool instance
1996
+ const subTools = new AgenticTools(this.logger, workingDir, this.permissionCallback, this.autoApprove);
1997
+ // Build a focused system prompt for the sub-agent
1998
+ const systemPrompt = [
1999
+ 'You are a focused sub-agent spawned to complete a specific subtask.',
2000
+ 'You have access to standard tools (read_file, write_file, edit_file, bash, grep, list_dir, glob, git). You cannot spawn sub-agents.',
2001
+ 'Complete the task thoroughly and return a detailed result.',
2002
+ `Working directory: ${workingDir}`,
2003
+ '',
2004
+ AgenticTools.getToolsForPrompt().replace(/### task[\s\S]*?(?=###|$)/, ''), // Strip task from sub-agent
2005
+ ].join('\n');
2006
+ const messages = [
2007
+ { role: 'system', content: systemPrompt },
2008
+ { role: 'user', content: description },
2009
+ ];
2010
+ const maxSubTurns = 8;
2011
+ const results = [];
2012
+ try {
2013
+ for (let turn = 0; turn < maxSubTurns; turn++) {
2014
+ // Import api dynamically to avoid circular dependency
2015
+ const { APIClient } = await import('./api.js');
2016
+ const { Config } = await import('./config.js');
2017
+ const config = new Config();
2018
+ const api = new APIClient(config, this.logger);
2019
+ const response = await api.chat(messages, 'code');
2020
+ const assistantMessage = response.message || '';
2021
+ messages.push({ role: 'assistant', content: assistantMessage });
2022
+ const toolCalls = AgenticTools.parseToolCalls(assistantMessage);
2023
+ if (toolCalls.length === 0) {
2024
+ // Sub-agent finished - extract the final answer
2025
+ const finalAnswer = assistantMessage
2026
+ .replace(/```tool[\s\S]*?```/g, '')
2027
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')
2028
+ .trim();
2029
+ results.push(finalAnswer);
2030
+ break;
2031
+ }
2032
+ // Execute each tool call
2033
+ for (const call of toolCalls) {
2034
+ const result = await subTools.execute(call);
2035
+ const summary = `Tool ${call.tool} ${result.success ? 'succeeded' : 'FAILED'}.` +
2036
+ (call.args.path ? `\nFile: ${call.args.path}` : '') +
2037
+ (result.output ? `\nOutput:\n${result.output.substring(0, 4000)}` : '') +
2038
+ (result.error ? `\nError: ${result.error}` : '');
2039
+ messages.push({ role: 'system', content: summary });
2040
+ results.push(`[${call.tool}] ${result.success ? '✓' : '✗'}`);
2041
+ }
2042
+ messages.push({
2043
+ role: 'system',
2044
+ content: 'Continue with your task. Use more tools if needed, or provide your final answer.',
2045
+ });
2046
+ }
2047
+ return {
2048
+ success: true,
2049
+ output: results.join('\n'),
2050
+ metadata: { subAgentTurns: results.length, workingDir },
2051
+ };
2052
+ }
2053
+ catch (error) {
2054
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Sub-agent failed: ${error.message}`, 'The sub-agent encountered an error. Try simplifying the task or running it directly.');
2055
+ }
2056
+ }
2057
+ // ═══════════════════════════════════════════════════════════════
2058
+ // MULTI_EDIT Tool - Atomic multi-file edits with rollback
2059
+ // ═══════════════════════════════════════════════════════════════
2060
+ async multiEdit(args) {
2061
+ let edits;
2062
+ try {
2063
+ edits = JSON.parse(args.edits);
2064
+ if (!Array.isArray(edits) || edits.length === 0) {
2065
+ throw new Error('edits must be a non-empty array');
2066
+ }
2067
+ }
2068
+ catch (parseError) {
2069
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid edits JSON: ${parseError.message}`, 'Provide edits as a JSON array: [{"path": "file.ts", "old_text": "find", "new_text": "replace"}]');
2070
+ }
2071
+ // Validate all edits can proceed before modifying anything
2072
+ const contentMap = new Map();
2073
+ const backups = [];
2074
+ const resolvedEdits = [];
2075
+ for (let i = 0; i < edits.length; i++) {
2076
+ const edit = edits[i];
2077
+ if (!edit.path || typeof edit.old_text !== 'string' || typeof edit.new_text !== 'string') {
2078
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Edit ${i}: missing required fields (path, old_text, new_text)`, 'Each edit must have path, old_text, and new_text fields.');
2079
+ }
2080
+ const resolvedPath = this.resolvePath(edit.path);
2081
+ if (!this.isPathWithinWorkspace(resolvedPath)) {
2082
+ return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Edit ${i}: path "${edit.path}" is outside workspace`, 'All files must be within the current project.');
2083
+ }
2084
+ if (!fs.existsSync(resolvedPath)) {
2085
+ return this.createErrorResult(ToolErrorType.FILE_NOT_FOUND, `Edit ${i}: file not found: ${edit.path}`, 'Use write_file to create new files instead.');
2086
+ }
2087
+ // Use contentMap to track cumulative edits to the same file
2088
+ if (!contentMap.has(resolvedPath)) {
2089
+ const diskContent = fs.readFileSync(resolvedPath, 'utf-8');
2090
+ contentMap.set(resolvedPath, diskContent);
2091
+ backups.push({ path: resolvedPath, content: diskContent });
2092
+ }
2093
+ const content = contentMap.get(resolvedPath);
2094
+ if (!content.includes(edit.old_text)) {
2095
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Edit ${i}: old_text not found in ${edit.path}`, `The text to replace was not found. Use read_file to verify the file contents.`);
2096
+ }
2097
+ // Check for multiple matches
2098
+ const matchCount = content.split(edit.old_text).length - 1;
2099
+ if (matchCount > 1) {
2100
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Edit ${i}: old_text matches ${matchCount} locations in ${edit.path}`, 'Make old_text more specific to match exactly one location. Include surrounding context.');
2101
+ }
2102
+ // Apply edit to contentMap so subsequent edits to same file see updated content
2103
+ contentMap.set(resolvedPath, content.replace(edit.old_text, edit.new_text));
2104
+ resolvedEdits.push({ resolvedPath, old_text: edit.old_text, new_text: edit.new_text, content });
2105
+ }
2106
+ // Apply all edits
2107
+ const applied = [];
2108
+ try {
2109
+ for (const edit of resolvedEdits) {
2110
+ const finalContent = contentMap.get(edit.resolvedPath) || edit.content.replace(edit.old_text, edit.new_text);
2111
+ fs.writeFileSync(edit.resolvedPath, finalContent, 'utf-8');
2112
+ applied.push(path.relative(this.cwd, edit.resolvedPath));
2113
+ }
2114
+ // Push undo operations for all edits
2115
+ for (const backup of backups) {
2116
+ this.undoStack.push({
2117
+ id: `multi_edit_${Date.now()}_${path.basename(backup.path)}`,
2118
+ tool: 'multi_edit',
2119
+ timestamp: Date.now(),
2120
+ filePath: backup.path,
2121
+ originalContent: backup.content,
2122
+ description: `multi_edit: ${path.relative(this.cwd, backup.path)}`,
2123
+ });
2124
+ }
2125
+ return {
2126
+ success: true,
2127
+ output: `${logger_js_1.CH.success} Atomically edited ${applied.length} file(s):\n${applied.map(f => ` ✓ ${f}`).join('\n')}`,
2128
+ undoable: true,
2129
+ metadata: { filesEdited: applied.length, files: applied },
2130
+ };
2131
+ }
2132
+ catch (error) {
2133
+ // ROLLBACK: Restore all files from backups
2134
+ for (const backup of backups) {
2135
+ try {
2136
+ fs.writeFileSync(backup.path, backup.content, 'utf-8');
2137
+ }
2138
+ catch {
2139
+ // Best-effort rollback
2140
+ }
2141
+ }
2142
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Multi-edit failed, all changes rolled back: ${error.message}`, 'Check file permissions and disk space.');
2143
+ }
2144
+ }
2145
+ // ═══════════════════════════════════════════════════════════════
2146
+ // CODEBASE_SEARCH Tool - Deep indexed codebase search
2147
+ // ═══════════════════════════════════════════════════════════════
2148
+ async codebaseSearch(args) {
2149
+ const query = args.query;
2150
+ const scope = args.scope || 'all';
2151
+ const includePattern = args.include || '';
2152
+ const maxResults = Math.min(parseInt(args.max_results || '30', 10), 100);
2153
+ const results = [];
2154
+ const seen = new Set();
2155
+ // Helper: collect files recursively respecting gitignore-like patterns
2156
+ const collectFiles = (dir, pattern) => {
2157
+ const files = [];
2158
+ const ignorePatterns = [
2159
+ 'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
2160
+ '.venv', 'venv', '.tox', 'coverage', '.nyc_output', '.cache',
2161
+ 'vendor', 'target', 'bin', 'obj', '.svn', '.hg',
2162
+ ];
2163
+ const walk = (currentDir, depth) => {
2164
+ if (depth > 12)
2165
+ return; // Prevent infinite recursion
2166
+ let entries;
2167
+ try {
2168
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
2169
+ }
2170
+ catch {
2171
+ return;
2172
+ }
2173
+ for (const entry of entries) {
2174
+ if (ignorePatterns.includes(entry.name) || entry.name.startsWith('.'))
2175
+ continue;
2176
+ const fullPath = path.join(currentDir, entry.name);
2177
+ if (entry.isDirectory()) {
2178
+ walk(fullPath, depth + 1);
2179
+ }
2180
+ else if (entry.isFile()) {
2181
+ if (pattern) {
2182
+ const basename = entry.name;
2183
+ // Simple glob matching for extension patterns like *.ts, *.js
2184
+ const globPattern = pattern.replace(/\*\*\//g, '(.*/)?').replace(/\*/g, '[^/]*').replace(/\?/g, '.');
2185
+ if (new RegExp(globPattern, 'i').test(basename) || fullPath.includes(pattern.replace(/\*/g, ''))) {
2186
+ files.push(fullPath);
2187
+ }
2188
+ }
2189
+ else {
2190
+ files.push(fullPath);
2191
+ }
2192
+ }
2193
+ }
2194
+ };
2195
+ walk(dir, 0);
2196
+ return files;
2197
+ };
2198
+ try {
2199
+ const allFiles = collectFiles(this.cwd, includePattern || undefined);
2200
+ // SCOPE: files - search file names/paths
2201
+ if (scope === 'files' || scope === 'all') {
2202
+ const queryLower = query.toLowerCase();
2203
+ const queryParts = queryLower.split(/[\s_\-./]+/).filter(Boolean);
2204
+ for (const filePath of allFiles) {
2205
+ const relativePath = path.relative(this.cwd, filePath);
2206
+ const filenameLower = relativePath.toLowerCase();
2207
+ const matches = queryParts.every(part => filenameLower.includes(part));
2208
+ if (matches && !seen.has(relativePath)) {
2209
+ seen.add(relativePath);
2210
+ results.push(`[file] ${relativePath}`);
2211
+ }
2212
+ if (results.length >= maxResults)
2213
+ break;
2214
+ }
2215
+ }
2216
+ // SCOPE: symbols - extract function/class/variable definitions
2217
+ if ((scope === 'symbols' || scope === 'all') && results.length < maxResults) {
2218
+ const symbolRegex = /(?:(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|enum|const|let|var|def|class)\s+)([A-Za-z_$][A-Za-z0-9_$]*)/g;
2219
+ const queryLower = query.toLowerCase();
2220
+ for (const filePath of allFiles) {
2221
+ if (results.length >= maxResults)
2222
+ break;
2223
+ const ext = path.extname(filePath).toLowerCase();
2224
+ // Only parse code files
2225
+ if (!['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.c', '.cpp', '.h', '.cs', '.swift', '.kt'].includes(ext))
2226
+ continue;
2227
+ let content;
2228
+ try {
2229
+ const stat = fs.statSync(filePath);
2230
+ if (stat.size > 512 * 1024)
2231
+ continue; // Skip files > 512KB
2232
+ content = fs.readFileSync(filePath, 'utf-8');
2233
+ }
2234
+ catch {
2235
+ continue;
2236
+ }
2237
+ let match;
2238
+ symbolRegex.lastIndex = 0;
2239
+ while ((match = symbolRegex.exec(content)) !== null) {
2240
+ const symbolName = match[1];
2241
+ if (symbolName.toLowerCase().includes(queryLower) || queryLower.includes(symbolName.toLowerCase())) {
2242
+ const relativePath = path.relative(this.cwd, filePath);
2243
+ const lineNum = content.substring(0, match.index).split('\n').length;
2244
+ const key = `${relativePath}:${symbolName}`;
2245
+ if (!seen.has(key)) {
2246
+ seen.add(key);
2247
+ results.push(`[symbol] ${symbolName} → ${relativePath}:${lineNum}`);
2248
+ }
2249
+ }
2250
+ if (results.length >= maxResults)
2251
+ break;
2252
+ }
2253
+ }
2254
+ }
2255
+ // SCOPE: content - full-text search using ripgrep or fallback
2256
+ if ((scope === 'content' || scope === 'all') && results.length < maxResults) {
2257
+ try {
2258
+ // Try ripgrep first (fast)
2259
+ const rgArgs = [
2260
+ '-i', '--no-heading', '--line-number',
2261
+ '--max-count', '3',
2262
+ '--max-filesize', '512K',
2263
+ '-g', '!node_modules', '-g', '!.git', '-g', '!dist', '-g', '!build',
2264
+ ];
2265
+ if (includePattern)
2266
+ rgArgs.push('-g', includePattern);
2267
+ rgArgs.push('--', query, this.cwd);
2268
+ const isWin = process.platform === 'win32';
2269
+ const quote = (s) => isWin ? `"${s}"` : `'${s}'`;
2270
+ const rgOutput = (0, child_process_1.execSync)(`rg ${rgArgs.map(a => quote(a)).join(' ')}`, {
2271
+ encoding: 'utf-8',
2272
+ timeout: 15000,
2273
+ maxBuffer: 5 * 1024 * 1024,
2274
+ }).trim();
2275
+ for (const line of rgOutput.split('\n').slice(0, maxResults - results.length)) {
2276
+ if (!line.trim())
2277
+ continue;
2278
+ const relativeLine = line.replace(this.cwd + path.sep, '').replace(this.cwd + '/', '');
2279
+ const lineKey = relativeLine.substring(0, 200);
2280
+ if (!seen.has(lineKey)) {
2281
+ seen.add(lineKey);
2282
+ results.push(`[content] ${relativeLine}`);
2283
+ }
2284
+ }
2285
+ }
2286
+ catch {
2287
+ // Fallback: Node-native grep
2288
+ const queryLower = query.toLowerCase();
2289
+ for (const filePath of allFiles) {
2290
+ if (results.length >= maxResults)
2291
+ break;
2292
+ try {
2293
+ const stat = fs.statSync(filePath);
2294
+ if (stat.size > 256 * 1024)
2295
+ continue;
2296
+ const content = fs.readFileSync(filePath, 'utf-8');
2297
+ const lines = content.split('\n');
2298
+ for (let i = 0; i < lines.length; i++) {
2299
+ if (lines[i].toLowerCase().includes(queryLower)) {
2300
+ const relativePath = path.relative(this.cwd, filePath);
2301
+ const lineKey = `${relativePath}:${i + 1}`;
2302
+ if (!seen.has(lineKey)) {
2303
+ seen.add(lineKey);
2304
+ results.push(`[content] ${relativePath}:${i + 1}: ${lines[i].trim().substring(0, 120)}`);
2305
+ }
2306
+ if (results.length >= maxResults)
2307
+ break;
2308
+ }
2309
+ }
2310
+ }
2311
+ catch {
2312
+ continue;
2313
+ }
2314
+ }
2315
+ }
2316
+ }
2317
+ if (results.length === 0) {
2318
+ return {
2319
+ success: true,
2320
+ output: `No results found for "${query}" in scope "${scope}".`,
2321
+ metadata: { searchStatus: 'search_no_matches', totalFiles: allFiles.length },
2322
+ };
2323
+ }
2324
+ return {
2325
+ success: true,
2326
+ output: `Found ${results.length} result(s) across ${allFiles.length} files:\n\n${results.join('\n')}`,
2327
+ metadata: { searchStatus: 'search_matches_found', resultCount: results.length, totalFiles: allFiles.length },
2328
+ };
2329
+ }
2330
+ catch (error) {
2331
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Codebase search failed: ${error.message}`, 'Try a simpler query or use grep for specific text patterns.');
2332
+ }
2333
+ }
1839
2334
  /**
1840
2335
  * Parse tool calls from AI response (Vigthoria Agent format)
1841
2336
  * Enhanced to handle various AI output formats including malformed JSON
@@ -2306,10 +2801,36 @@ To use a tool, output a JSON block in a code fence with "tool" language:
2306
2801
  - When comparing WEBSITES, use fetch_url - do NOT use read_file or list_dir
2307
2802
 
2308
2803
  ### Tool Names:
2309
- - Use ONLY these exact tool names: list_dir, read_file, write_file, edit_file, bash, grep, glob, git, repo, fetch_url, ssh_exec
2804
+ - Use ONLY these exact tool names: list_dir, read_file, write_file, edit_file, bash, grep, glob, git, repo, fetch_url, ssh_exec, task, multi_edit, codebase_search
2310
2805
  - The JSON must be valid with double quotes for all keys and string values
2311
2806
  - After tool execution, you will receive results and can continue with the next step
2312
2807
  - Explain what you're doing before using tools
2808
+
2809
+ ### Sub-Agent Delegation (task tool):
2810
+ - Use the task tool to delegate complex subtasks to an independent sub-agent
2811
+ - The sub-agent has its own tools and context, runs autonomously, and returns results
2812
+ - Great for: parallel research, investigating side questions, exploring unfamiliar code
2813
+ - Example:
2814
+ \`\`\`tool
2815
+ {"tool": "task", "args": {"description": "Find all API endpoints in the backend and list their HTTP methods and paths"}}
2816
+ \`\`\`
2817
+
2818
+ ### Atomic Multi-File Edits (multi_edit tool):
2819
+ - Use multi_edit to apply multiple edits atomically - all succeed or all roll back
2820
+ - Pass edits as a JSON array in the edits parameter
2821
+ - Example:
2822
+ \`\`\`tool
2823
+ {"tool": "multi_edit", "args": {"edits": "[{\\"path\\": \\"src/config.ts\\", \\"old_text\\": \\"port: 3000\\", \\"new_text\\": \\"port: 8080\\"}, {\\"path\\": \\"src/server.ts\\", \\"old_text\\": \\"listen(3000)\\", \\"new_text\\": \\"listen(8080)\\"}]"}}
2824
+ \`\`\`
2825
+
2826
+ ### Deep Codebase Search (codebase_search tool):
2827
+ - Use codebase_search for large projects to find symbols, files, or content across the ENTIRE codebase
2828
+ - Not limited by workspace snapshot - searches all files recursively
2829
+ - Scopes: "symbols" (functions/classes), "files" (filenames), "content" (full-text), "all" (default)
2830
+ - Example:
2831
+ \`\`\`tool
2832
+ {"tool": "codebase_search", "args": {"query": "handleAuthentication", "scope": "symbols"}}
2833
+ \`\`\`
2313
2834
  `;
2314
2835
  return prompt;
2315
2836
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.51",
3
+ "version": "1.6.53",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [