vigthoria-cli 1.6.50 → 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.
- package/dist/commands/chat.js +36 -5
- package/dist/utils/api.js +8 -11
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.js +51 -1
- package/dist/utils/tools.d.ts +16 -0
- package/dist/utils/tools.js +506 -5
- package/package.json +2 -2
package/dist/commands/chat.js
CHANGED
|
@@ -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
|
-
|
|
1655
|
-
|
|
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
|
}
|
package/dist/utils/api.js
CHANGED
|
@@ -425,11 +425,12 @@ class APIClient {
|
|
|
425
425
|
}
|
|
426
426
|
getOperatorBaseUrls() {
|
|
427
427
|
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
428
|
+
const allowLocal = process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
428
429
|
const urls = [
|
|
429
430
|
process.env.VIGTHORIA_OPERATOR_URL,
|
|
430
431
|
process.env.OPERATOR_URL,
|
|
431
|
-
'http://127.0.0.1:4009',
|
|
432
432
|
configuredModelsApiUrl,
|
|
433
|
+
...(allowLocal ? ['http://127.0.0.1:4009'] : []),
|
|
433
434
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
434
435
|
return [...new Set(urls)];
|
|
435
436
|
}
|
|
@@ -438,39 +439,35 @@ class APIClient {
|
|
|
438
439
|
}
|
|
439
440
|
getMcpBaseUrls() {
|
|
440
441
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
442
|
+
const allowLocal = process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
441
443
|
const urls = [
|
|
442
444
|
process.env.VIGTHORIA_MCP_URL,
|
|
443
445
|
process.env.MCP_SERVER_URL,
|
|
444
|
-
'http://127.0.0.1:4008',
|
|
445
446
|
configuredApiUrl,
|
|
447
|
+
...(allowLocal ? ['http://127.0.0.1:4008'] : []),
|
|
446
448
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
447
449
|
return [...new Set(urls)];
|
|
448
450
|
}
|
|
449
451
|
getVigFlowBaseUrls() {
|
|
450
452
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
451
|
-
|
|
452
|
-
// rarely running for end-user CLI installations. This avoids
|
|
453
|
-
// wasting connection-attempt time on 127.0.0.1 and hitting the
|
|
454
|
-
// remote gateway only after the local attempts have already
|
|
455
|
-
// errored — which surfaces as a confusing "last error" 404 in
|
|
456
|
-
// some setups.
|
|
453
|
+
const allowLocal = process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
457
454
|
const urls = [
|
|
458
455
|
process.env.VIGTHORIA_VIGFLOW_URL,
|
|
459
456
|
process.env.VIGFLOW_URL,
|
|
460
457
|
process.env.WORKFLOW_BUILDER_URL,
|
|
461
458
|
`${configuredApiUrl}/api/vigflow`,
|
|
462
|
-
'http://127.0.0.1:5060',
|
|
463
|
-
'http://127.0.0.1:5050',
|
|
459
|
+
...(allowLocal ? ['http://127.0.0.1:5060', 'http://127.0.0.1:5050'] : []),
|
|
464
460
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
465
461
|
return [...new Set(urls)];
|
|
466
462
|
}
|
|
467
463
|
getTemplateServiceBaseUrls() {
|
|
468
464
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
465
|
+
const allowLocal = process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
469
466
|
const urls = [
|
|
470
467
|
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
|
|
471
468
|
process.env.TEMPLATE_SERVICE_URL,
|
|
472
|
-
'http://127.0.0.1:4011',
|
|
473
469
|
`${configuredApiUrl}/api/template-service`,
|
|
470
|
+
...(allowLocal ? ['http://127.0.0.1:4011'] : []),
|
|
474
471
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
475
472
|
return [...new Set(urls)];
|
|
476
473
|
}
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/logger.js
CHANGED
|
@@ -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();
|
package/dist/utils/tools.d.ts
CHANGED
|
@@ -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
|
package/dist/utils/tools.js
CHANGED
|
@@ -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
|
|
377
|
-
if (this.
|
|
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
|
-
//
|
|
391
|
-
// '
|
|
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.
|
|
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": [
|
|
@@ -86,4 +86,4 @@
|
|
|
86
86
|
"engines": {
|
|
87
87
|
"node": ">=18.0.0"
|
|
88
88
|
}
|
|
89
|
-
}
|
|
89
|
+
}
|