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