vigthoria-cli 1.6.51 → 1.6.52

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.
@@ -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
  }
@@ -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
77
  }) => Promise<boolean | 'batch'>, 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,8 +487,9 @@ 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
  }
@@ -497,6 +598,12 @@ class AgenticTools {
497
598
  return this.fetchUrl(call.args);
498
599
  case 'ssh_exec':
499
600
  return this.sshExec(call.args);
601
+ case 'task':
602
+ return this.task(call.args);
603
+ case 'multi_edit':
604
+ return this.multiEdit(call.args);
605
+ case 'codebase_search':
606
+ return this.codebaseSearch(call.args);
500
607
  default:
501
608
  return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Tool not implemented: ${call.tool}`);
502
609
  }
@@ -772,6 +879,8 @@ class AgenticTools {
772
879
  if (this.undoStack.length > 50) {
773
880
  this.undoStack.shift();
774
881
  }
882
+ // Show syntax-highlighted diff
883
+ this.logger.unifiedDiff(args.path, args.old_text, args.new_text);
775
884
  return {
776
885
  success: true,
777
886
  output: `File edited: ${args.path}`,
@@ -921,20 +1030,45 @@ class AgenticTools {
921
1030
  }
922
1031
  }
923
1032
  // SECURITY: Block dangerous commands that could access outside workspace
1033
+ // HARDENED: Protects Vigthoria ecosystem, server config, and system files
924
1034
  const blockedPatterns = [
1035
+ // === System file access ===
925
1036
  /\bcat\s+\/etc\//i, // Reading system files
926
1037
  /\bcat\s+\/var\/(?!log)/i, // Reading /var/ except logs
927
1038
  /\bcat\s+\/root\//i, // Reading root home
928
1039
  /\bcat\s+\/home\/[^\/]+\/\.[^\/]/i, // Reading hidden files in home dirs
929
1040
  /\bcat\s+~\/\./i, // Reading hidden files via ~
930
1041
  /\bcd\s+(\/etc|\/var\/www|\/root|\/home)/i, // CD to sensitive dirs
1042
+ // === Destructive operations ===
931
1043
  /\brm\s+-rf?\s+\//i, // Dangerous rm commands
1044
+ /\brm\s+-rf?\s+\.\.\//i, // rm going up directories
1045
+ /\brm\s+-rf?\s+~\//i, // rm in home directory
932
1046
  /\b(curl|wget).*\|\s*(bash|sh)\b/i, // Downloading and executing scripts
933
1047
  /\bsudo\b/i, // Sudo commands
934
1048
  /\bsu\s+-/i, // Su to root
935
1049
  /\bchmod\s+[0-7]*777/i, // World-writable permissions
936
1050
  /\/(var\/www|opt)\/[a-z]+-?(database|models|coder|mcp|operator|voice|music)/i, // Vigthoria ecosystem
937
1051
  /vigthoria-(models|database|mcp|operator|voice|music)/i, // Vigthoria project names
1052
+ // === Vigthoria Server Protection (CRITICAL) ===
1053
+ /\/var\/www\/(?!$)/i, // Block ANY access to /var/www/* (server web root)
1054
+ /\/etc\/(caddy|nginx|apache|systemd|pm2)/i, // Server config files
1055
+ /\/etc\/(passwd|shadow|group|sudoers)/i, // System auth files
1056
+ /\b(systemctl|service)\s+(start|stop|restart|enable|disable|reload)/i, // Service control
1057
+ /\bpm2\s+(start|stop|restart|delete|kill|flush)/i, // PM2 process management
1058
+ /\bjournalctl\b/i, // System logs access
1059
+ /\bnpm\s+publish\b/i, // Block npm publish from agent
1060
+ /\bdocker\s+(rm|kill|stop|exec|run)/i, // Docker container manipulation
1061
+ /\biptables\b/i, // Firewall rules
1062
+ /\bufw\s+(allow|deny|delete|disable)/i, // UFW firewall
1063
+ /\bssh\s+.*vigthoria/i, // SSH to Vigthoria servers
1064
+ /\bssh\s+.*78\.46\.154/i, // SSH to known Vigthoria IP
1065
+ /\.env\b/i, // Environment files (may contain secrets)
1066
+ /\bprivate[_-]?key|secret[_-]?key|api[_-]?key|auth[_-]?token/i, // Sensitive variable patterns in commands
1067
+ /\bcrontab\b/i, // Cron manipulation
1068
+ /\bmount\s/i, // Mount filesystems
1069
+ /\bmkfs\b/i, // Format filesystems
1070
+ />\s*\/dev\/sd/i, // Writing to disk devices
1071
+ /\bdd\s+.*of=/i, // dd write operations
938
1072
  ];
939
1073
  for (const pattern of blockedPatterns) {
940
1074
  if (pattern.test(args.command)) {
@@ -1836,6 +1970,347 @@ class AgenticTools {
1836
1970
  const workspaceRoot = path.normalize(this.cwd);
1837
1971
  return resolvedPath.startsWith(workspaceRoot + path.sep) || resolvedPath === workspaceRoot;
1838
1972
  }
1973
+ // ═══════════════════════════════════════════════════════════════
1974
+ // TASK (Sub-Agent) Tool
1975
+ // ═══════════════════════════════════════════════════════════════
1976
+ async task(args) {
1977
+ const description = args.description;
1978
+ const workingDir = args.working_dir
1979
+ ? this.resolvePath(args.working_dir)
1980
+ : this.cwd;
1981
+ if (!this.isPathWithinWorkspace(workingDir)) {
1982
+ return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, 'Sub-agent working directory must be within the project workspace', 'Provide a relative path within the current project.');
1983
+ }
1984
+ this.logger.info(`Spawning sub-agent: ${description.substring(0, 80)}...`);
1985
+ // Create a scoped sub-agent with its own tool instance
1986
+ const subTools = new AgenticTools(this.logger, workingDir, this.permissionCallback, this.autoApprove);
1987
+ // Build a focused system prompt for the sub-agent
1988
+ const systemPrompt = [
1989
+ 'You are a focused sub-agent spawned to complete a specific subtask.',
1990
+ 'You have access to all standard tools (read_file, write_file, edit_file, bash, grep, list_dir, glob, git).',
1991
+ 'Complete the task thoroughly and return a detailed result.',
1992
+ `Working directory: ${workingDir}`,
1993
+ '',
1994
+ AgenticTools.getToolsForPrompt(),
1995
+ ].join('\n');
1996
+ const messages = [
1997
+ { role: 'system', content: systemPrompt },
1998
+ { role: 'user', content: description },
1999
+ ];
2000
+ const maxSubTurns = 8;
2001
+ const results = [];
2002
+ try {
2003
+ for (let turn = 0; turn < maxSubTurns; turn++) {
2004
+ // Import api dynamically to avoid circular dependency
2005
+ const { APIClient } = await import('./api.js');
2006
+ const { Config } = await import('./config.js');
2007
+ const config = new Config();
2008
+ const api = new APIClient(config, this.logger);
2009
+ const response = await api.chat(messages, 'code');
2010
+ const assistantMessage = response.message || '';
2011
+ messages.push({ role: 'assistant', content: assistantMessage });
2012
+ const toolCalls = AgenticTools.parseToolCalls(assistantMessage);
2013
+ if (toolCalls.length === 0) {
2014
+ // Sub-agent finished - extract the final answer
2015
+ const finalAnswer = assistantMessage
2016
+ .replace(/```tool[\s\S]*?```/g, '')
2017
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')
2018
+ .trim();
2019
+ results.push(finalAnswer);
2020
+ break;
2021
+ }
2022
+ // Execute each tool call
2023
+ for (const call of toolCalls) {
2024
+ const result = await subTools.execute(call);
2025
+ const summary = `Tool ${call.tool} ${result.success ? 'succeeded' : 'FAILED'}.` +
2026
+ (call.args.path ? `\nFile: ${call.args.path}` : '') +
2027
+ (result.output ? `\nOutput:\n${result.output.substring(0, 4000)}` : '') +
2028
+ (result.error ? `\nError: ${result.error}` : '');
2029
+ messages.push({ role: 'system', content: summary });
2030
+ results.push(`[${call.tool}] ${result.success ? '✓' : '✗'}`);
2031
+ }
2032
+ messages.push({
2033
+ role: 'system',
2034
+ content: 'Continue with your task. Use more tools if needed, or provide your final answer.',
2035
+ });
2036
+ }
2037
+ return {
2038
+ success: true,
2039
+ output: results.join('\n'),
2040
+ metadata: { subAgentTurns: results.length, workingDir },
2041
+ };
2042
+ }
2043
+ catch (error) {
2044
+ 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.');
2045
+ }
2046
+ }
2047
+ // ═══════════════════════════════════════════════════════════════
2048
+ // MULTI_EDIT Tool - Atomic multi-file edits with rollback
2049
+ // ═══════════════════════════════════════════════════════════════
2050
+ async multiEdit(args) {
2051
+ let edits;
2052
+ try {
2053
+ edits = JSON.parse(args.edits);
2054
+ if (!Array.isArray(edits) || edits.length === 0) {
2055
+ throw new Error('edits must be a non-empty array');
2056
+ }
2057
+ }
2058
+ catch (parseError) {
2059
+ 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"}]');
2060
+ }
2061
+ // Validate all edits can proceed before modifying anything
2062
+ const backups = [];
2063
+ const resolvedEdits = [];
2064
+ for (let i = 0; i < edits.length; i++) {
2065
+ const edit = edits[i];
2066
+ if (!edit.path || typeof edit.old_text !== 'string' || typeof edit.new_text !== 'string') {
2067
+ 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.');
2068
+ }
2069
+ const resolvedPath = this.resolvePath(edit.path);
2070
+ if (!this.isPathWithinWorkspace(resolvedPath)) {
2071
+ return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Edit ${i}: path "${edit.path}" is outside workspace`, 'All files must be within the current project.');
2072
+ }
2073
+ if (!fs.existsSync(resolvedPath)) {
2074
+ return this.createErrorResult(ToolErrorType.FILE_NOT_FOUND, `Edit ${i}: file not found: ${edit.path}`, 'Use write_file to create new files instead.');
2075
+ }
2076
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
2077
+ if (!content.includes(edit.old_text)) {
2078
+ 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.`);
2079
+ }
2080
+ // Check for multiple matches
2081
+ const matchCount = content.split(edit.old_text).length - 1;
2082
+ if (matchCount > 1) {
2083
+ 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.');
2084
+ }
2085
+ backups.push({ path: resolvedPath, content });
2086
+ resolvedEdits.push({ resolvedPath, old_text: edit.old_text, new_text: edit.new_text, content });
2087
+ }
2088
+ // Apply all edits
2089
+ const applied = [];
2090
+ try {
2091
+ for (const edit of resolvedEdits) {
2092
+ const newContent = edit.content.replace(edit.old_text, edit.new_text);
2093
+ fs.writeFileSync(edit.resolvedPath, newContent, 'utf-8');
2094
+ applied.push(path.relative(this.cwd, edit.resolvedPath));
2095
+ }
2096
+ // Push undo operations for all edits
2097
+ for (const backup of backups) {
2098
+ this.undoStack.push({
2099
+ id: `multi_edit_${Date.now()}_${path.basename(backup.path)}`,
2100
+ tool: 'multi_edit',
2101
+ timestamp: Date.now(),
2102
+ filePath: backup.path,
2103
+ originalContent: backup.content,
2104
+ description: `multi_edit: ${path.relative(this.cwd, backup.path)}`,
2105
+ });
2106
+ }
2107
+ return {
2108
+ success: true,
2109
+ output: `${logger_js_1.CH.success} Atomically edited ${applied.length} file(s):\n${applied.map(f => ` ✓ ${f}`).join('\n')}`,
2110
+ undoable: true,
2111
+ metadata: { filesEdited: applied.length, files: applied },
2112
+ };
2113
+ }
2114
+ catch (error) {
2115
+ // ROLLBACK: Restore all files from backups
2116
+ for (const backup of backups) {
2117
+ try {
2118
+ fs.writeFileSync(backup.path, backup.content, 'utf-8');
2119
+ }
2120
+ catch {
2121
+ // Best-effort rollback
2122
+ }
2123
+ }
2124
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Multi-edit failed, all changes rolled back: ${error.message}`, 'Check file permissions and disk space.');
2125
+ }
2126
+ }
2127
+ // ═══════════════════════════════════════════════════════════════
2128
+ // CODEBASE_SEARCH Tool - Deep indexed codebase search
2129
+ // ═══════════════════════════════════════════════════════════════
2130
+ async codebaseSearch(args) {
2131
+ const query = args.query;
2132
+ const scope = args.scope || 'all';
2133
+ const includePattern = args.include || '';
2134
+ const maxResults = Math.min(parseInt(args.max_results || '30', 10), 100);
2135
+ const results = [];
2136
+ const seen = new Set();
2137
+ // Helper: collect files recursively respecting gitignore-like patterns
2138
+ const collectFiles = (dir, pattern) => {
2139
+ const files = [];
2140
+ const ignorePatterns = [
2141
+ 'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
2142
+ '.venv', 'venv', '.tox', 'coverage', '.nyc_output', '.cache',
2143
+ 'vendor', 'target', 'bin', 'obj', '.svn', '.hg',
2144
+ ];
2145
+ const walk = (currentDir, depth) => {
2146
+ if (depth > 12)
2147
+ return; // Prevent infinite recursion
2148
+ let entries;
2149
+ try {
2150
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
2151
+ }
2152
+ catch {
2153
+ return;
2154
+ }
2155
+ for (const entry of entries) {
2156
+ if (ignorePatterns.includes(entry.name) || entry.name.startsWith('.'))
2157
+ continue;
2158
+ const fullPath = path.join(currentDir, entry.name);
2159
+ if (entry.isDirectory()) {
2160
+ walk(fullPath, depth + 1);
2161
+ }
2162
+ else if (entry.isFile()) {
2163
+ if (pattern) {
2164
+ const basename = entry.name;
2165
+ // Simple glob matching for extension patterns like *.ts, *.js
2166
+ const globPattern = pattern.replace(/\*\*/g, '').replace(/\*/g, '.*').replace(/\?/g, '.');
2167
+ if (new RegExp(globPattern, 'i').test(basename) || fullPath.includes(pattern.replace(/\*/g, ''))) {
2168
+ files.push(fullPath);
2169
+ }
2170
+ }
2171
+ else {
2172
+ files.push(fullPath);
2173
+ }
2174
+ }
2175
+ }
2176
+ };
2177
+ walk(dir, 0);
2178
+ return files;
2179
+ };
2180
+ try {
2181
+ const allFiles = collectFiles(this.cwd, includePattern || undefined);
2182
+ // SCOPE: files - search file names/paths
2183
+ if (scope === 'files' || scope === 'all') {
2184
+ const queryLower = query.toLowerCase();
2185
+ const queryParts = queryLower.split(/[\s_\-./]+/).filter(Boolean);
2186
+ for (const filePath of allFiles) {
2187
+ const relativePath = path.relative(this.cwd, filePath);
2188
+ const filenameLower = relativePath.toLowerCase();
2189
+ const matches = queryParts.every(part => filenameLower.includes(part));
2190
+ if (matches && !seen.has(relativePath)) {
2191
+ seen.add(relativePath);
2192
+ results.push(`[file] ${relativePath}`);
2193
+ }
2194
+ if (results.length >= maxResults)
2195
+ break;
2196
+ }
2197
+ }
2198
+ // SCOPE: symbols - extract function/class/variable definitions
2199
+ if ((scope === 'symbols' || scope === 'all') && results.length < maxResults) {
2200
+ 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;
2201
+ const queryLower = query.toLowerCase();
2202
+ for (const filePath of allFiles) {
2203
+ if (results.length >= maxResults)
2204
+ break;
2205
+ const ext = path.extname(filePath).toLowerCase();
2206
+ // Only parse code files
2207
+ if (!['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.c', '.cpp', '.h', '.cs', '.swift', '.kt'].includes(ext))
2208
+ continue;
2209
+ let content;
2210
+ try {
2211
+ const stat = fs.statSync(filePath);
2212
+ if (stat.size > 512 * 1024)
2213
+ continue; // Skip files > 512KB
2214
+ content = fs.readFileSync(filePath, 'utf-8');
2215
+ }
2216
+ catch {
2217
+ continue;
2218
+ }
2219
+ let match;
2220
+ symbolRegex.lastIndex = 0;
2221
+ while ((match = symbolRegex.exec(content)) !== null) {
2222
+ const symbolName = match[1];
2223
+ if (symbolName.toLowerCase().includes(queryLower) || queryLower.includes(symbolName.toLowerCase())) {
2224
+ const relativePath = path.relative(this.cwd, filePath);
2225
+ const lineNum = content.substring(0, match.index).split('\n').length;
2226
+ const key = `${relativePath}:${symbolName}`;
2227
+ if (!seen.has(key)) {
2228
+ seen.add(key);
2229
+ results.push(`[symbol] ${symbolName} → ${relativePath}:${lineNum}`);
2230
+ }
2231
+ }
2232
+ if (results.length >= maxResults)
2233
+ break;
2234
+ }
2235
+ }
2236
+ }
2237
+ // SCOPE: content - full-text search using ripgrep or fallback
2238
+ if ((scope === 'content' || scope === 'all') && results.length < maxResults) {
2239
+ try {
2240
+ // Try ripgrep first (fast)
2241
+ const rgArgs = [
2242
+ '-i', '--no-heading', '--line-number',
2243
+ '--max-count', '3',
2244
+ '--max-filesize', '512K',
2245
+ '-g', '!node_modules', '-g', '!.git', '-g', '!dist', '-g', '!build',
2246
+ ];
2247
+ if (includePattern)
2248
+ rgArgs.push('-g', includePattern);
2249
+ rgArgs.push('--', query, this.cwd);
2250
+ const rgOutput = (0, child_process_1.execSync)(`rg ${rgArgs.map(a => `'${a}'`).join(' ')}`, {
2251
+ encoding: 'utf-8',
2252
+ timeout: 15000,
2253
+ maxBuffer: 5 * 1024 * 1024,
2254
+ }).trim();
2255
+ for (const line of rgOutput.split('\n').slice(0, maxResults - results.length)) {
2256
+ if (!line.trim())
2257
+ continue;
2258
+ const relativeLine = line.replace(this.cwd + path.sep, '').replace(this.cwd + '/', '');
2259
+ const lineKey = relativeLine.substring(0, 200);
2260
+ if (!seen.has(lineKey)) {
2261
+ seen.add(lineKey);
2262
+ results.push(`[content] ${relativeLine}`);
2263
+ }
2264
+ }
2265
+ }
2266
+ catch {
2267
+ // Fallback: Node-native grep
2268
+ const queryLower = query.toLowerCase();
2269
+ for (const filePath of allFiles) {
2270
+ if (results.length >= maxResults)
2271
+ break;
2272
+ try {
2273
+ const stat = fs.statSync(filePath);
2274
+ if (stat.size > 256 * 1024)
2275
+ continue;
2276
+ const content = fs.readFileSync(filePath, 'utf-8');
2277
+ const lines = content.split('\n');
2278
+ for (let i = 0; i < lines.length; i++) {
2279
+ if (lines[i].toLowerCase().includes(queryLower)) {
2280
+ const relativePath = path.relative(this.cwd, filePath);
2281
+ const lineKey = `${relativePath}:${i + 1}`;
2282
+ if (!seen.has(lineKey)) {
2283
+ seen.add(lineKey);
2284
+ results.push(`[content] ${relativePath}:${i + 1}: ${lines[i].trim().substring(0, 120)}`);
2285
+ }
2286
+ if (results.length >= maxResults)
2287
+ break;
2288
+ }
2289
+ }
2290
+ }
2291
+ catch {
2292
+ continue;
2293
+ }
2294
+ }
2295
+ }
2296
+ }
2297
+ if (results.length === 0) {
2298
+ return {
2299
+ success: true,
2300
+ output: `No results found for "${query}" in scope "${scope}".`,
2301
+ metadata: { searchStatus: 'search_no_matches', totalFiles: allFiles.length },
2302
+ };
2303
+ }
2304
+ return {
2305
+ success: true,
2306
+ output: `Found ${results.length} result(s) across ${allFiles.length} files:\n\n${results.join('\n')}`,
2307
+ metadata: { searchStatus: 'search_matches_found', resultCount: results.length, totalFiles: allFiles.length },
2308
+ };
2309
+ }
2310
+ catch (error) {
2311
+ return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Codebase search failed: ${error.message}`, 'Try a simpler query or use grep for specific text patterns.');
2312
+ }
2313
+ }
1839
2314
  /**
1840
2315
  * Parse tool calls from AI response (Vigthoria Agent format)
1841
2316
  * Enhanced to handle various AI output formats including malformed JSON
@@ -2306,10 +2781,36 @@ To use a tool, output a JSON block in a code fence with "tool" language:
2306
2781
  - When comparing WEBSITES, use fetch_url - do NOT use read_file or list_dir
2307
2782
 
2308
2783
  ### 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
2784
+ - 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
2785
  - The JSON must be valid with double quotes for all keys and string values
2311
2786
  - After tool execution, you will receive results and can continue with the next step
2312
2787
  - Explain what you're doing before using tools
2788
+
2789
+ ### Sub-Agent Delegation (task tool):
2790
+ - Use the task tool to delegate complex subtasks to an independent sub-agent
2791
+ - The sub-agent has its own tools and context, runs autonomously, and returns results
2792
+ - Great for: parallel research, investigating side questions, exploring unfamiliar code
2793
+ - Example:
2794
+ \`\`\`tool
2795
+ {"tool": "task", "args": {"description": "Find all API endpoints in the backend and list their HTTP methods and paths"}}
2796
+ \`\`\`
2797
+
2798
+ ### Atomic Multi-File Edits (multi_edit tool):
2799
+ - Use multi_edit to apply multiple edits atomically - all succeed or all roll back
2800
+ - Pass edits as a JSON array in the edits parameter
2801
+ - Example:
2802
+ \`\`\`tool
2803
+ {"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)\\"}]"}}
2804
+ \`\`\`
2805
+
2806
+ ### Deep Codebase Search (codebase_search tool):
2807
+ - Use codebase_search for large projects to find symbols, files, or content across the ENTIRE codebase
2808
+ - Not limited by workspace snapshot - searches all files recursively
2809
+ - Scopes: "symbols" (functions/classes), "files" (filenames), "content" (full-text), "all" (default)
2810
+ - Example:
2811
+ \`\`\`tool
2812
+ {"tool": "codebase_search", "args": {"query": "handleAuthentication", "scope": "symbols"}}
2813
+ \`\`\`
2313
2814
  `;
2314
2815
  return prompt;
2315
2816
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.51",
3
+ "version": "1.6.52",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [