vigthoria-cli 1.8.19 → 1.9.5

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.
@@ -52,12 +52,234 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
52
52
  };
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
54
  exports.AgenticTools = exports.ToolErrorType = void 0;
55
+ exports.installUpdateWindows = installUpdateWindows;
56
+ exports.robustifyStreamResponse = robustifyStreamResponse;
55
57
  const fs = __importStar(require("fs"));
56
58
  const path = __importStar(require("path"));
57
59
  const child_process_1 = require("child_process");
58
60
  const chalk_1 = __importDefault(require("chalk"));
59
61
  const logger_js_1 = require("./logger.js");
60
62
  const api_js_1 = require("./api.js");
63
+ const STREAM_RESPONSE_MAX_YIELD_CHARS = 32 * 1024;
64
+ function isNodeError(error) {
65
+ return error instanceof Error && 'code' in error;
66
+ }
67
+ function resolveWindowsInstallerPath() {
68
+ const configuredPath = process.env.VIGTHORIA_WINDOWS_INSTALLER || process.env.VIGTHORIA_UPDATE_INSTALLER;
69
+ if (configuredPath && configuredPath.trim().length > 0) {
70
+ return path.resolve(configuredPath);
71
+ }
72
+ const executableDir = path.dirname(process.execPath);
73
+ const cwd = process.cwd();
74
+ const candidates = [
75
+ path.resolve(cwd, 'dist', 'VigthoriaSetup.exe'),
76
+ path.resolve(cwd, 'release', 'VigthoriaSetup.exe'),
77
+ path.resolve(cwd, 'VigthoriaSetup.exe'),
78
+ path.resolve(executableDir, 'VigthoriaSetup.exe'),
79
+ path.resolve(executableDir, '..', 'VigthoriaSetup.exe'),
80
+ ];
81
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
82
+ }
83
+ async function installUpdateWindows() {
84
+ const installerPath = resolveWindowsInstallerPath();
85
+ try {
86
+ if (!fs.existsSync(installerPath)) {
87
+ return { success: false, platform: 'windows', error: 'ENOENT' };
88
+ }
89
+ await fs.promises.access(installerPath, fs.constants.F_OK);
90
+ await new Promise((resolve, reject) => {
91
+ const child = (0, child_process_1.spawn)(installerPath, [], {
92
+ cwd: path.dirname(installerPath),
93
+ detached: true,
94
+ stdio: 'ignore',
95
+ windowsHide: false,
96
+ });
97
+ child.once('error', reject);
98
+ child.once('spawn', () => {
99
+ child.unref();
100
+ resolve();
101
+ });
102
+ });
103
+ return { success: true, platform: 'windows' };
104
+ }
105
+ catch (error) {
106
+ if (isNodeError(error) && error.code === 'ENOENT') {
107
+ return { success: false, platform: 'windows', error: 'ENOENT' };
108
+ }
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ return { success: false, platform: 'windows', error: message };
111
+ }
112
+ }
113
+ async function* robustifyStreamResponse(res) {
114
+ const MAX_YIELD_CHARS = STREAM_RESPONSE_MAX_YIELD_CHARS;
115
+ const MAX_BUFFER_CHARS = 256 * 1024;
116
+ const body = res?.body ?? res;
117
+ const decoder = new TextDecoder('utf-8');
118
+ let reader = null;
119
+ let lineBuffer = '';
120
+ const emitContent = async function* (type, content) {
121
+ for (let offset = 0; offset < content.length; offset += MAX_YIELD_CHARS) {
122
+ const part = content.slice(offset, offset + MAX_YIELD_CHARS);
123
+ if (part.length > 0) {
124
+ yield { type, content: part };
125
+ }
126
+ }
127
+ };
128
+ const textFromObject = (value) => {
129
+ if (!value || typeof value !== 'object')
130
+ return '';
131
+ if (typeof value.content === 'string')
132
+ return value.content;
133
+ if (typeof value.text === 'string')
134
+ return value.text;
135
+ if (typeof value.delta === 'string')
136
+ return value.delta;
137
+ if (typeof value.delta?.text === 'string')
138
+ return value.delta.text;
139
+ if (typeof value.message?.content === 'string')
140
+ return value.message.content;
141
+ if (typeof value.choices?.[0]?.delta?.content === 'string')
142
+ return value.choices[0].delta.content;
143
+ if (typeof value.choices?.[0]?.message?.content === 'string')
144
+ return value.choices[0].message.content;
145
+ if (typeof value.error === 'string')
146
+ return value.error;
147
+ if (typeof value.error?.message === 'string')
148
+ return value.error.message;
149
+ return '';
150
+ };
151
+ const stringifyChunk = (chunk) => {
152
+ if (typeof chunk === 'string')
153
+ return chunk;
154
+ if (Buffer.isBuffer(chunk))
155
+ return chunk.toString('utf8');
156
+ if (chunk instanceof Uint8Array)
157
+ return Buffer.from(chunk).toString('utf8');
158
+ if (chunk instanceof ArrayBuffer)
159
+ return Buffer.from(chunk).toString('utf8');
160
+ if (chunk === null || chunk === undefined)
161
+ return '';
162
+ const objectText = textFromObject(chunk);
163
+ return objectText || String(chunk);
164
+ };
165
+ const decodeLine = (line) => {
166
+ const trimmed = line.trim();
167
+ if (!trimmed || trimmed === 'event: ping' || trimmed === ':' || trimmed === 'data: [DONE]' || trimmed === '[DONE]') {
168
+ return null;
169
+ }
170
+ const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trimStart() : trimmed;
171
+ if (!payload || payload === '[DONE]')
172
+ return null;
173
+ if ((payload.startsWith('{') && payload.endsWith('}')) || (payload.startsWith('[') && payload.endsWith(']'))) {
174
+ try {
175
+ const parsed = JSON.parse(payload);
176
+ const parsedText = textFromObject(parsed);
177
+ if (parsedText) {
178
+ const type = parsed.error ? 'error' : 'delta';
179
+ return { type, content: parsedText };
180
+ }
181
+ }
182
+ catch (error) {
183
+ console.debug(`Stream JSON fragment preserved as text: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+ }
186
+ if (payload.startsWith('event:') || payload.startsWith('id:') || payload.startsWith('retry:')) {
187
+ return null;
188
+ }
189
+ return { type: 'delta', content: payload };
190
+ };
191
+ const consumeText = async function* (text, flush = false) {
192
+ if (text) {
193
+ lineBuffer += text;
194
+ }
195
+ while (lineBuffer.length > MAX_BUFFER_CHARS) {
196
+ const newlineIndex = lineBuffer.indexOf('\n', Math.max(0, MAX_BUFFER_CHARS - MAX_YIELD_CHARS));
197
+ const cutIndex = newlineIndex >= 0 ? newlineIndex + 1 : MAX_BUFFER_CHARS;
198
+ const overflow = lineBuffer.slice(0, cutIndex);
199
+ lineBuffer = lineBuffer.slice(cutIndex);
200
+ const decoded = decodeLine(overflow);
201
+ if (decoded) {
202
+ yield* emitContent(decoded.type, decoded.content);
203
+ }
204
+ else {
205
+ yield* emitContent('delta', overflow);
206
+ }
207
+ }
208
+ const lines = lineBuffer.split(/\r?\n/);
209
+ const partialLine = lines.pop() ?? '';
210
+ for (const line of lines) {
211
+ const decoded = decodeLine(line);
212
+ if (decoded) {
213
+ yield* emitContent(decoded.type, decoded.content);
214
+ }
215
+ }
216
+ if (flush) {
217
+ lineBuffer = '';
218
+ if (partialLine) {
219
+ const decoded = decodeLine(partialLine);
220
+ if (decoded) {
221
+ yield* emitContent(decoded.type, decoded.content);
222
+ }
223
+ }
224
+ }
225
+ else {
226
+ lineBuffer = partialLine;
227
+ }
228
+ };
229
+ try {
230
+ if (!body) {
231
+ return;
232
+ }
233
+ if (typeof body.getReader === 'function') {
234
+ reader = body.getReader();
235
+ const streamReader = reader;
236
+ const decoder = new TextDecoder();
237
+ while (true) {
238
+ const { done, value } = await streamReader.read();
239
+ if (done)
240
+ break;
241
+ const content = decoder.decode(value, { stream: true });
242
+ if (content.length > 0) {
243
+ yield* consumeText(content);
244
+ }
245
+ }
246
+ const tail = decoder.decode();
247
+ if (tail.length > 0) {
248
+ yield* consumeText(tail);
249
+ }
250
+ yield* consumeText('', true);
251
+ return;
252
+ }
253
+ if (typeof body[Symbol.asyncIterator] === 'function') {
254
+ for await (const chunk of body) {
255
+ const content = stringifyChunk(chunk);
256
+ if (content.length > 0) {
257
+ yield* consumeText(content);
258
+ }
259
+ }
260
+ yield* consumeText('', true);
261
+ return;
262
+ }
263
+ const content = stringifyChunk(body);
264
+ if (content.length > 0) {
265
+ yield* emitContent('text', content);
266
+ }
267
+ }
268
+ catch (error) {
269
+ const content = error instanceof Error ? error.message : String(error);
270
+ yield { type: 'error', content };
271
+ }
272
+ finally {
273
+ lineBuffer = '';
274
+ try {
275
+ reader?.releaseLock();
276
+ }
277
+ catch (error) {
278
+ const message = error instanceof Error ? error.message : String(error);
279
+ console.debug(`Stream reader release failed: ${message}`);
280
+ }
281
+ }
282
+ }
61
283
  const TOOL_ARG_ALIASES = {
62
284
  read_file: {
63
285
  path: ['file', 'filePath', 'filepath', 'target', 'targetPath'],
@@ -141,16 +363,154 @@ class AgenticTools {
141
363
  maxDelayMs: 10000,
142
364
  retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN'],
143
365
  };
366
+ formatExternalToolError(toolName, operation, error) {
367
+ if (error instanceof Error) {
368
+ const processError = error;
369
+ const details = [
370
+ `External tool call failed in ${toolName} during ${operation}: ${error.message}`,
371
+ processError.code !== undefined ? `code=${processError.code}` : undefined,
372
+ processError.status !== undefined ? `status=${processError.status}` : undefined,
373
+ processError.signal ? `signal=${processError.signal}` : undefined,
374
+ processError.stderr ? `stderr=${processError.stderr.toString().trim()}` : undefined,
375
+ processError.stdout ? `stdout=${processError.stdout.toString().trim()}` : undefined,
376
+ ].filter(Boolean);
377
+ return details.join(' | ');
378
+ }
379
+ return `External tool call failed in ${toolName} during ${operation}: ${String(error)}`;
380
+ }
381
+ externalToolFailure(toolName, operation, error, suggestion) {
382
+ const message = this.formatExternalToolError(toolName, operation, error);
383
+ this.logger.debug(message);
384
+ this.logger.warn(message);
385
+ return {
386
+ success: false,
387
+ error: message,
388
+ suggestion: suggestion || 'Review the command, arguments, permissions, and environment, then retry.',
389
+ canRetry: true,
390
+ };
391
+ }
392
+ cleanupAfterToolError(toolName, operation, cleanup) {
393
+ try {
394
+ cleanup();
395
+ }
396
+ catch (cleanupError) {
397
+ const message = this.formatExternalToolError(toolName, `${operation} cleanup`, cleanupError);
398
+ this.logger.debug(message);
399
+ this.logger.warn(message);
400
+ }
401
+ }
402
+ runExternalCommand(toolName, operation, command, options, cleanup) {
403
+ if (!this.isNonEmptyString(toolName)) {
404
+ throw new Error('Invalid tool execution: toolName must be a non-empty string.');
405
+ }
406
+ if (!this.isNonEmptyString(operation)) {
407
+ throw new Error(`Invalid external command for ${toolName}: operation must be a non-empty string.`);
408
+ }
409
+ if (!this.isNonEmptyString(command)) {
410
+ throw new Error(`Invalid external command for ${toolName} during ${operation}: command must be a non-empty string.`);
411
+ }
412
+ try {
413
+ return (0, child_process_1.execSync)(command, options);
414
+ }
415
+ catch (error) {
416
+ if (cleanup) {
417
+ this.cleanupAfterToolError(toolName, operation, cleanup);
418
+ }
419
+ const message = this.formatExternalToolError(toolName, operation, error);
420
+ const wrapped = new Error(message);
421
+ wrapped.cause = error;
422
+ throw wrapped;
423
+ }
424
+ }
425
+ isNonEmptyString(value) {
426
+ return typeof value === 'string' && value.trim().length > 0;
427
+ }
428
+ describeInvalidValue(value) {
429
+ if (value === null)
430
+ return 'null';
431
+ if (value === undefined)
432
+ return 'undefined';
433
+ if (Array.isArray(value))
434
+ return 'array';
435
+ return typeof value;
436
+ }
437
+ assertStringRecord(value, context) {
438
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
439
+ throw new Error(`${context} must be an object with string values; received ${this.describeInvalidValue(value)}.`);
440
+ }
441
+ for (const [key, entryValue] of Object.entries(value)) {
442
+ if (!this.isNonEmptyString(key)) {
443
+ throw new Error(`${context} contains an invalid empty parameter name.`);
444
+ }
445
+ if (typeof entryValue !== 'string') {
446
+ throw new Error(`${context}.${key} must be a string; received ${this.describeInvalidValue(entryValue)}.`);
447
+ }
448
+ }
449
+ }
450
+ requireNonEmptyString(value, fieldName, toolName) {
451
+ if (!this.isNonEmptyString(value)) {
452
+ throw new Error(`Invalid arguments for ${toolName}: ${fieldName} is required and must be a non-empty string.`);
453
+ }
454
+ return value;
455
+ }
456
+ requireArgsObject(args, toolName) {
457
+ this.assertStringRecord(args, `Invalid arguments for ${toolName}: args`);
458
+ }
144
459
  // Session-based tool approvals - remembers which tools user approved for this turn
145
460
  sessionApprovedTools = new Set();
146
461
  // Persistent permissions - tool allowlists per project
147
462
  static permissionsFile = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.vigthoria', 'permissions.json');
148
463
  constructor(logger, cwd, permissionCallback, autoApprove = false) {
464
+ if (!logger) {
465
+ throw new Error('AgenticTools initialization failed: logger is required.');
466
+ }
467
+ if (typeof cwd !== 'string' || cwd.trim().length === 0) {
468
+ throw new Error('AgenticTools initialization failed: cwd must be a non-empty string.');
469
+ }
470
+ if (typeof permissionCallback !== 'function') {
471
+ throw new Error('AgenticTools initialization failed: permissionCallback must be a function.');
472
+ }
473
+ if (typeof autoApprove !== 'boolean') {
474
+ throw new Error('AgenticTools initialization failed: autoApprove must be a boolean.');
475
+ }
149
476
  this.logger = logger;
150
477
  this.cwd = cwd;
151
478
  this.permissionCallback = permissionCallback;
152
479
  this.autoApprove = autoApprove;
153
480
  }
481
+ getErrorMessage(error) {
482
+ if (error instanceof Error) {
483
+ return error.message;
484
+ }
485
+ if (typeof error === 'string') {
486
+ return error;
487
+ }
488
+ try {
489
+ return JSON.stringify(error);
490
+ }
491
+ catch {
492
+ return String(error);
493
+ }
494
+ }
495
+ assertToolCall(call) {
496
+ if (!call || typeof call !== 'object') {
497
+ throw new Error('Invalid tool call: call must be an object.');
498
+ }
499
+ if (typeof call.tool !== 'string' || call.tool.trim().length === 0) {
500
+ throw new Error('Invalid tool call: tool name must be a non-empty string.');
501
+ }
502
+ if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
503
+ throw new Error(`Invalid tool call for ${call.tool}: args must be an object.`);
504
+ }
505
+ for (const [key, value] of Object.entries(call.args)) {
506
+ if (typeof key !== 'string' || key.trim().length === 0) {
507
+ throw new Error(`Invalid tool call for ${call.tool}: argument names must be non-empty strings.`);
508
+ }
509
+ if (typeof value !== 'string') {
510
+ throw new Error(`Invalid argument "${key}" for ${call.tool}: expected string, received ${typeof value}.`);
511
+ }
512
+ }
513
+ }
154
514
  /**
155
515
  * Load persistent permissions for the current project
156
516
  */
@@ -160,8 +520,10 @@ class AgenticTools {
160
520
  return JSON.parse(fs.readFileSync(AgenticTools.permissionsFile, 'utf-8'));
161
521
  }
162
522
  }
163
- catch {
164
- // Corrupted file - ignore
523
+ catch (error) {
524
+ const message = this.formatExternalToolError('permissions', `read ${AgenticTools.permissionsFile}`, error);
525
+ this.logger.debug(message);
526
+ this.logger.warn(message);
165
527
  }
166
528
  return {};
167
529
  }
@@ -184,8 +546,10 @@ class AgenticTools {
184
546
  fs.mkdirSync(dir, { recursive: true });
185
547
  fs.writeFileSync(AgenticTools.permissionsFile, JSON.stringify(permissions, null, 2), 'utf-8');
186
548
  }
187
- catch {
188
- // Non-critical - permission won't persist
549
+ catch (error) {
550
+ const message = this.formatExternalToolError('permissions', `write ${AgenticTools.permissionsFile}`, error);
551
+ this.logger.debug(message);
552
+ this.logger.warn(message);
189
553
  }
190
554
  }
191
555
  /**
@@ -468,9 +832,21 @@ class AgenticTools {
468
832
  * Execute a tool call with enhanced error handling and retry logic
469
833
  */
470
834
  async execute(call) {
471
- // Guard against malformed tool calls with undefined or empty tool names
835
+ // Guard against malformed tool calls before accessing fields.
836
+ if (!call || typeof call !== 'object') {
837
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid tool call: expected an object, received ${call === null ? 'null' : typeof call}`, 'Provide a valid ToolCall object with a non-empty tool name and args object.');
838
+ }
472
839
  if (!call.tool || typeof call.tool !== 'string' || !call.tool.trim()) {
473
- return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid tool call: tool name is ${call.tool === undefined ? 'undefined' : 'empty'}`, `Provide a valid tool name. Available tools: ${AgenticTools.getToolDefinitions().map(t => t.name).join(', ')}`);
840
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid tool call: tool name is ${call.tool === undefined ? 'undefined' : typeof call.tool === 'string' ? 'empty' : typeof call.tool}`, `Provide a valid tool name. Available tools: ${AgenticTools.getToolDefinitions().map(t => t.name).join(', ')}`);
841
+ }
842
+ if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
843
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid tool call for '${call.tool}': args must be a key/value object`, 'Provide args as an object with string values, for example { path: "src/index.ts" }.');
844
+ }
845
+ try {
846
+ this.assertStringRecord(call.args, `Invalid tool call for '${call.tool}': args`);
847
+ }
848
+ catch (error) {
849
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, this.getErrorMessage(error), 'All tool argument names must be non-empty strings and every argument value must be a string.');
474
850
  }
475
851
  const normalizedCall = this.normalizeToolCall(call);
476
852
  const tool = AgenticTools.getToolDefinitions().find(t => t.name === normalizedCall.tool);
@@ -594,39 +970,57 @@ class AgenticTools {
594
970
  * Execute the actual tool operation
595
971
  */
596
972
  async executeTool(call) {
597
- switch (call.tool) {
598
- case 'read_file':
599
- return this.readFile(call.args);
600
- case 'write_file':
601
- return this.writeFile(call.args);
602
- case 'edit_file':
603
- return this.editFile(call.args);
604
- case 'bash':
605
- return this.bash(call.args);
606
- case 'grep':
607
- return this.grep(call.args);
608
- case 'list_dir':
609
- return this.listDir(call.args);
610
- case 'glob':
611
- return this.glob(call.args);
612
- case 'git':
613
- return this.git(call.args);
614
- case 'repo':
615
- return this.repo(call.args);
616
- case 'fetch_url':
617
- return this.fetchUrl(call.args);
618
- case 'browser':
619
- return this.browserTool(call.args);
620
- case 'ssh_exec':
621
- return this.sshExec(call.args);
622
- case 'task':
623
- return this.task(call.args);
624
- case 'multi_edit':
625
- return this.multiEdit(call.args);
626
- case 'codebase_search':
627
- return this.codebaseSearch(call.args);
628
- default:
629
- return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Tool not implemented: ${call.tool}`);
973
+ try {
974
+ switch (call.tool) {
975
+ case 'read_file':
976
+ return this.readFile(call.args);
977
+ case 'write_file':
978
+ return this.writeFile(call.args);
979
+ case 'edit_file':
980
+ return this.editFile(call.args);
981
+ case 'bash':
982
+ return this.bash(call.args);
983
+ case 'grep':
984
+ return this.grep(call.args);
985
+ case 'list_dir':
986
+ return this.listDir(call.args);
987
+ case 'glob':
988
+ return this.glob(call.args);
989
+ case 'git':
990
+ return this.git(call.args);
991
+ case 'repo':
992
+ return this.repo(call.args);
993
+ case 'fetch_url':
994
+ return this.fetchUrl(call.args);
995
+ case 'browser':
996
+ return this.browserTool(call.args);
997
+ case 'ssh_exec':
998
+ return this.sshExec(call.args);
999
+ case 'task':
1000
+ return this.task(call.args);
1001
+ case 'multi_edit':
1002
+ return this.multiEdit(call.args);
1003
+ case 'codebase_search':
1004
+ return this.codebaseSearch(call.args);
1005
+ default:
1006
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Tool not implemented: ${call.tool}`);
1007
+ }
1008
+ }
1009
+ catch (error) {
1010
+ const context = `Tool '${call.tool}' failed while executing with args ${this.safeStringifyArgs(call.args)}`;
1011
+ const stderr = error?.stderr?.toString?.().trim();
1012
+ const stdout = error?.stdout?.toString?.().trim();
1013
+ const status = error?.status !== undefined ? `exit status ${error.status}` : undefined;
1014
+ const signal = error?.signal ? `signal ${error.signal}` : undefined;
1015
+ const details = [context, status, signal, stderr || error?.message || String(error)].filter(Boolean).join(': ');
1016
+ this.logger.error(details);
1017
+ return {
1018
+ success: false,
1019
+ output: stdout,
1020
+ error: details,
1021
+ suggestion: 'Review the tool arguments and subprocess output above, then retry with corrected input.',
1022
+ canRetry: this.isRetryableError(details),
1023
+ };
630
1024
  }
631
1025
  }
632
1026
  /**
@@ -635,13 +1029,108 @@ class AgenticTools {
635
1029
  isRetryableError(error) {
636
1030
  return this.retryConfig.retryableErrors.some(e => error.includes(e));
637
1031
  }
1032
+ /**
1033
+ * Safely stringify tool arguments without dumping very large payloads into logs.
1034
+ */
1035
+ safeStringifyArgs(args) {
1036
+ try {
1037
+ const json = JSON.stringify(args ?? {}, (_key, value) => {
1038
+ if (typeof value === 'string' && value.length > 500) {
1039
+ return `${value.slice(0, 500)}...<truncated ${value.length - 500} chars>`;
1040
+ }
1041
+ return value;
1042
+ });
1043
+ return json || '{}';
1044
+ }
1045
+ catch (error) {
1046
+ return `[unserializable args: ${error instanceof Error ? error.message : String(error)}]`;
1047
+ }
1048
+ }
638
1049
  /**
639
1050
  * Validate tool parameters
640
1051
  */
641
1052
  validateParameters(call, tool) {
1053
+ if (!call || typeof call !== 'object') {
1054
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Invalid tool call: expected an object with tool and args fields', 'Pass a ToolCall object shaped like { tool: "read_file", args: { path: "file.ts" } }.');
1055
+ }
1056
+ if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
1057
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid arguments for ${tool.name}: args must be a key/value object`, `Pass ${tool.name} arguments as an object. Received: ${this.safeStringifyArgs(call.args)}`);
1058
+ }
1059
+ for (const [key, value] of Object.entries(call.args)) {
1060
+ if (!key || typeof key !== 'string') {
1061
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid argument key for ${tool.name}: keys must be non-empty strings`, 'Use documented parameter names for this tool.');
1062
+ }
1063
+ if (typeof value !== 'string') {
1064
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid argument '${key}' for ${tool.name}: expected string value, received ${Array.isArray(value) ? 'array' : typeof value}`, `Convert '${key}' to a string before invoking ${tool.name}.`);
1065
+ }
1066
+ }
642
1067
  for (const param of tool.parameters) {
643
- if (param.required && !call.args[param.name]) {
644
- return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Missing required parameter: ${param.name}`, `The ${call.tool} tool requires the '${param.name}' parameter. ${param.description}`);
1068
+ const value = call.args[param.name];
1069
+ if (param.required && (value === undefined || value.trim() === '')) {
1070
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Missing required parameter '${param.name}' for tool '${call.tool}'`, `The ${call.tool} tool requires the '${param.name}' parameter. ${param.description}`);
1071
+ }
1072
+ }
1073
+ const pathLikeParams = ['path', 'working_dir', 'cwd'];
1074
+ for (const paramName of pathLikeParams) {
1075
+ const value = call.args[paramName];
1076
+ if (typeof value === 'string' && value.includes('\0')) {
1077
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid ${paramName} for ${tool.name}: paths cannot contain null bytes`, 'Remove null bytes from the path and retry.');
1078
+ }
1079
+ }
1080
+ if (call.args.start_line !== undefined && (!/^\d+$/.test(call.args.start_line) || Number(call.args.start_line) < 1)) {
1081
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid start_line for ${tool.name}: expected a positive integer, received '${call.args.start_line}'`, 'Use a 1-based positive integer for start_line.');
1082
+ }
1083
+ if (call.args.end_line !== undefined && (!/^\d+$/.test(call.args.end_line) || Number(call.args.end_line) < 0)) {
1084
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid end_line for ${tool.name}: expected a non-negative integer, received '${call.args.end_line}'`, 'Use 0 for end-of-file or a positive 1-based line number for end_line.');
1085
+ }
1086
+ if (call.args.start_line !== undefined && call.args.end_line !== undefined) {
1087
+ const startLine = Number(call.args.start_line);
1088
+ const endLine = Number(call.args.end_line);
1089
+ if (endLine !== 0 && endLine < startLine) {
1090
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid line range for ${tool.name}: end_line (${endLine}) is before start_line (${startLine})`, 'Use an end_line greater than or equal to start_line, or 0 to read through EOF.');
1091
+ }
1092
+ }
1093
+ if (call.args.timeout !== undefined && (!/^\d+$/.test(call.args.timeout) || Number(call.args.timeout) < 1)) {
1094
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid timeout for ${tool.name}: expected a positive integer number of seconds, received '${call.args.timeout}'`, 'Use a positive integer timeout in seconds.');
1095
+ }
1096
+ if (call.args.max_results !== undefined && (!/^\d+$/.test(call.args.max_results) || Number(call.args.max_results) < 1)) {
1097
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid max_results for ${tool.name}: expected a positive integer, received '${call.args.max_results}'`, 'Use a positive integer for max_results.');
1098
+ }
1099
+ if (call.tool === 'list_dir' && call.args.recursive !== undefined && !['true', 'false', '1', '0', 'yes', 'no'].includes(call.args.recursive.toLowerCase())) {
1100
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid recursive value for list_dir: expected true or false, received '${call.args.recursive}'`, 'Use recursive="true" or recursive="false".');
1101
+ }
1102
+ if (call.tool === 'fetch_url' && call.args.url !== undefined) {
1103
+ try {
1104
+ const parsed = new URL(call.args.url);
1105
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
1106
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid URL protocol for fetch_url: '${parsed.protocol}'`, 'Use an http:// or https:// URL.');
1107
+ }
1108
+ }
1109
+ catch (error) {
1110
+ const message = this.getErrorMessage(error);
1111
+ this.logger.debug(`Invalid URL for fetch_url '${call.args.url}': ${message}`);
1112
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid URL for fetch_url: '${call.args.url}'`, 'Provide a fully-qualified http:// or https:// URL.');
1113
+ }
1114
+ }
1115
+ if (call.tool === 'multi_edit' && call.args.edits !== undefined) {
1116
+ try {
1117
+ const edits = JSON.parse(call.args.edits);
1118
+ if (!Array.isArray(edits) || edits.length === 0) {
1119
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Invalid edits for multi_edit: expected a non-empty JSON array', 'Provide edits as a JSON array of { path, old_text, new_text } objects.');
1120
+ }
1121
+ for (const [index, edit] of edits.entries()) {
1122
+ if (!edit || typeof edit !== 'object' || Array.isArray(edit)) {
1123
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid edit at index ${index}: expected an object`);
1124
+ }
1125
+ for (const field of ['path', 'old_text', 'new_text']) {
1126
+ if (typeof edit[field] !== 'string') {
1127
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid edit at index ${index}: '${field}' must be a string`);
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ catch (error) {
1133
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid edits JSON for multi_edit: ${error instanceof Error ? error.message : String(error)}`, 'Pass a valid JSON array string for the edits parameter.');
645
1134
  }
646
1135
  }
647
1136
  return null;
@@ -954,18 +1443,27 @@ class AgenticTools {
954
1443
  validateJavaScriptSyntax(source) {
955
1444
  const tempFile = path.join(this.cwd, `.vigthoria-temp-${Date.now()}.js`);
956
1445
  try {
957
- fs.writeFileSync(tempFile, source, 'utf-8');
958
- (0, child_process_1.execSync)(`node --check "${tempFile}"`, { stdio: 'pipe' });
959
- return null;
960
- }
961
- catch (error) {
962
- const stderr = error.stderr?.toString()?.trim();
963
- return stderr || error.message || 'JavaScript syntax check failed.';
1446
+ try {
1447
+ fs.writeFileSync(tempFile, source, 'utf-8');
1448
+ }
1449
+ catch (writeError) {
1450
+ return this.formatExternalToolError('syntax_check', `write temporary file ${tempFile}`, writeError);
1451
+ }
1452
+ try {
1453
+ this.runExternalCommand('syntax_check', `node --check ${tempFile}`, `node --check "${tempFile}"`, { stdio: 'pipe' });
1454
+ return null;
1455
+ }
1456
+ catch (error) {
1457
+ const stderr = error.stderr?.toString()?.trim();
1458
+ return stderr || error.message || this.formatExternalToolError('syntax_check', `node --check ${tempFile}`, error);
1459
+ }
964
1460
  }
965
1461
  finally {
966
- if (fs.existsSync(tempFile)) {
967
- fs.unlinkSync(tempFile);
968
- }
1462
+ this.cleanupAfterToolError('syntax_check', `remove temporary file ${tempFile}`, () => {
1463
+ if (fs.existsSync(tempFile)) {
1464
+ fs.unlinkSync(tempFile);
1465
+ }
1466
+ });
969
1467
  }
970
1468
  }
971
1469
  validateHtmlContent(content) {
@@ -1131,7 +1629,7 @@ class AgenticTools {
1131
1629
  // Use /bin/sh on Unix-like systems (more portable than bash)
1132
1630
  execOptions.shell = '/bin/sh';
1133
1631
  }
1134
- const output = (0, child_process_1.execSync)(args.command, execOptions);
1632
+ const output = this.runExternalCommand('bash', args.command, args.command, execOptions);
1135
1633
  const duration = Date.now() - startTime;
1136
1634
  return {
1137
1635
  success: true,
@@ -1189,12 +1687,16 @@ class AgenticTools {
1189
1687
  (0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1190
1688
  return this.grepWithRg(args, searchPath);
1191
1689
  }
1192
- catch { /* rg not available */ }
1690
+ catch (error) {
1691
+ this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Windows', error));
1692
+ }
1193
1693
  // 2. Try PowerShell Select-String
1194
1694
  try {
1195
1695
  return this.grepWithSelectString(args, searchPath);
1196
1696
  }
1197
- catch { /* Select-String failed */ }
1697
+ catch (error) {
1698
+ this.logger.debug(this.formatExternalToolError('grep', 'running PowerShell Select-String fallback', error));
1699
+ }
1198
1700
  // 3. Fall back to Node-native recursive file scanner
1199
1701
  return this.grepNodeNative(args, searchPath);
1200
1702
  }
@@ -1204,10 +1706,13 @@ class AgenticTools {
1204
1706
  grepUnix(args, searchPath, platform) {
1205
1707
  // Try ripgrep first if available
1206
1708
  try {
1207
- (0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1709
+ this.runExternalCommand('grep', 'checking ripgrep availability on Unix', 'rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1208
1710
  return this.grepWithRg(args, searchPath);
1209
1711
  }
1210
- catch { /* rg not available, fall through to system grep */ }
1712
+ catch (error) {
1713
+ this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Unix', error));
1714
+ // rg not available, fall through to system grep
1715
+ }
1211
1716
  const isMac = platform === 'darwin';
1212
1717
  let cmd;
1213
1718
  if (isMac) {
@@ -1221,7 +1726,7 @@ class AgenticTools {
1221
1726
  : `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
1222
1727
  }
1223
1728
  try {
1224
- const output = (0, child_process_1.execSync)(cmd, {
1729
+ const output = this.runExternalCommand('grep', 'system grep search', cmd, {
1225
1730
  cwd: this.cwd,
1226
1731
  encoding: 'utf-8',
1227
1732
  maxBuffer: 5 * 1024 * 1024,
@@ -1264,7 +1769,7 @@ class AgenticTools {
1264
1769
  }
1265
1770
  cmd += ` "${args.pattern}" "${searchPath}"`;
1266
1771
  try {
1267
- const output = (0, child_process_1.execSync)(cmd, {
1772
+ const output = this.runExternalCommand('grep', 'ripgrep search', cmd, {
1268
1773
  cwd: this.cwd,
1269
1774
  encoding: 'utf-8',
1270
1775
  maxBuffer: 5 * 1024 * 1024,