vigthoria-cli 1.8.15 → 1.9.2

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.
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * Vigthoria CLI — Multi-Step Terminal Task Display
4
+ *
5
+ * Renders a live, updating task progress list in the terminal using ANSI
6
+ * escape codes and chalk — no external UI framework required.
7
+ *
8
+ * Only activates when stderr is a real TTY; JSON mode and piped output stay clean.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.TaskDisplay = void 0;
15
+ const chalk_1 = __importDefault(require("chalk"));
16
+ const ICONS = {
17
+ pending: chalk_1.default.gray('○'),
18
+ running: chalk_1.default.cyan('⟳'),
19
+ done: chalk_1.default.green('✓'),
20
+ error: chalk_1.default.red('✗'),
21
+ skipped: chalk_1.default.gray('\u2013'),
22
+ };
23
+ const LABEL_FN = {
24
+ pending: (s) => chalk_1.default.gray(s),
25
+ running: (s) => chalk_1.default.cyan.bold(s),
26
+ done: (s) => chalk_1.default.gray(s),
27
+ error: (s) => chalk_1.default.red(s),
28
+ skipped: (s) => chalk_1.default.gray(s),
29
+ };
30
+ class TaskDisplay {
31
+ tasks = [];
32
+ linesRendered = 0;
33
+ enabled;
34
+ constructor(labels, enabled = true) {
35
+ this.enabled = enabled && process.stderr.isTTY === true;
36
+ this.tasks = labels.map((label) => ({ label, status: 'pending' }));
37
+ }
38
+ clearLines() {
39
+ if (this.linesRendered > 0) {
40
+ process.stderr.write(`\u001b[${this.linesRendered}A\u001b[0J`);
41
+ }
42
+ }
43
+ renderLines() {
44
+ return this.tasks.map((t) => {
45
+ const icon = ICONS[t.status];
46
+ const label = LABEL_FN[t.status](t.label);
47
+ const detail = t.detail ? chalk_1.default.gray(` \u2014 ${t.detail.slice(0, 60)}`) : '';
48
+ return ` ${icon} ${label}${detail}`;
49
+ });
50
+ }
51
+ render() {
52
+ if (!this.enabled)
53
+ return;
54
+ this.clearLines();
55
+ const lines = this.renderLines();
56
+ process.stderr.write(lines.join('\n') + '\n');
57
+ this.linesRendered = lines.length;
58
+ }
59
+ start(index, detail) {
60
+ if (!this.enabled || index < 0 || index >= this.tasks.length)
61
+ return;
62
+ this.tasks[index].status = 'running';
63
+ if (detail !== undefined)
64
+ this.tasks[index].detail = detail;
65
+ this.render();
66
+ }
67
+ complete(index, detail) {
68
+ if (!this.enabled || index < 0 || index >= this.tasks.length)
69
+ return;
70
+ this.tasks[index].status = 'done';
71
+ if (detail !== undefined)
72
+ this.tasks[index].detail = detail;
73
+ this.render();
74
+ }
75
+ fail(index, detail) {
76
+ if (!this.enabled || index < 0 || index >= this.tasks.length)
77
+ return;
78
+ this.tasks[index].status = 'error';
79
+ if (detail !== undefined)
80
+ this.tasks[index].detail = detail;
81
+ this.render();
82
+ }
83
+ skip(index, detail) {
84
+ if (!this.enabled || index < 0 || index >= this.tasks.length)
85
+ return;
86
+ this.tasks[index].status = 'skipped';
87
+ if (detail !== undefined)
88
+ this.tasks[index].detail = detail;
89
+ this.render();
90
+ }
91
+ setDetail(index, detail) {
92
+ if (!this.enabled || index < 0 || index >= this.tasks.length)
93
+ return;
94
+ this.tasks[index].detail = detail;
95
+ this.render();
96
+ }
97
+ clear() {
98
+ if (!this.enabled)
99
+ return;
100
+ this.clearLines();
101
+ this.linesRendered = 0;
102
+ }
103
+ finalize() {
104
+ if (!this.enabled)
105
+ return;
106
+ this.clearLines();
107
+ const lines = this.renderLines();
108
+ process.stderr.write(lines.join('\n') + '\n');
109
+ this.linesRendered = 0;
110
+ }
111
+ get isEnabled() {
112
+ return this.enabled;
113
+ }
114
+ }
115
+ exports.TaskDisplay = TaskDisplay;
@@ -70,11 +70,22 @@ export declare class AgenticTools {
70
70
  private undoStack;
71
71
  private maxUndoStack;
72
72
  private retryConfig;
73
+ private formatExternalToolError;
74
+ private externalToolFailure;
75
+ private cleanupAfterToolError;
76
+ private runExternalCommand;
77
+ private isNonEmptyString;
78
+ private describeInvalidValue;
79
+ private assertStringRecord;
80
+ private requireNonEmptyString;
81
+ private requireArgsObject;
73
82
  private sessionApprovedTools;
74
83
  private static permissionsFile;
75
84
  constructor(logger: Logger, cwd: string, permissionCallback: (action: string, options?: {
76
85
  batchApproval?: boolean;
77
86
  }) => Promise<boolean | 'batch' | 'persist'>, autoApprove?: boolean);
87
+ private getErrorMessage;
88
+ private assertToolCall;
78
89
  /**
79
90
  * Load persistent permissions for the current project
80
91
  */
@@ -125,6 +136,10 @@ export declare class AgenticTools {
125
136
  * Check if an error is retryable
126
137
  */
127
138
  private isRetryableError;
139
+ /**
140
+ * Safely stringify tool arguments without dumping very large payloads into logs.
141
+ */
142
+ private safeStringifyArgs;
128
143
  /**
129
144
  * Validate tool parameters
130
145
  */
@@ -141,16 +141,154 @@ class AgenticTools {
141
141
  maxDelayMs: 10000,
142
142
  retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN'],
143
143
  };
144
+ formatExternalToolError(toolName, operation, error) {
145
+ if (error instanceof Error) {
146
+ const processError = error;
147
+ const details = [
148
+ `External tool call failed in ${toolName} during ${operation}: ${error.message}`,
149
+ processError.code !== undefined ? `code=${processError.code}` : undefined,
150
+ processError.status !== undefined ? `status=${processError.status}` : undefined,
151
+ processError.signal ? `signal=${processError.signal}` : undefined,
152
+ processError.stderr ? `stderr=${processError.stderr.toString().trim()}` : undefined,
153
+ processError.stdout ? `stdout=${processError.stdout.toString().trim()}` : undefined,
154
+ ].filter(Boolean);
155
+ return details.join(' | ');
156
+ }
157
+ return `External tool call failed in ${toolName} during ${operation}: ${String(error)}`;
158
+ }
159
+ externalToolFailure(toolName, operation, error, suggestion) {
160
+ const message = this.formatExternalToolError(toolName, operation, error);
161
+ this.logger.debug(message);
162
+ this.logger.warn(message);
163
+ return {
164
+ success: false,
165
+ error: message,
166
+ suggestion: suggestion || 'Review the command, arguments, permissions, and environment, then retry.',
167
+ canRetry: true,
168
+ };
169
+ }
170
+ cleanupAfterToolError(toolName, operation, cleanup) {
171
+ try {
172
+ cleanup();
173
+ }
174
+ catch (cleanupError) {
175
+ const message = this.formatExternalToolError(toolName, `${operation} cleanup`, cleanupError);
176
+ this.logger.debug(message);
177
+ this.logger.warn(message);
178
+ }
179
+ }
180
+ runExternalCommand(toolName, operation, command, options, cleanup) {
181
+ if (!this.isNonEmptyString(toolName)) {
182
+ throw new Error('Invalid tool execution: toolName must be a non-empty string.');
183
+ }
184
+ if (!this.isNonEmptyString(operation)) {
185
+ throw new Error(`Invalid external command for ${toolName}: operation must be a non-empty string.`);
186
+ }
187
+ if (!this.isNonEmptyString(command)) {
188
+ throw new Error(`Invalid external command for ${toolName} during ${operation}: command must be a non-empty string.`);
189
+ }
190
+ try {
191
+ return (0, child_process_1.execSync)(command, options);
192
+ }
193
+ catch (error) {
194
+ if (cleanup) {
195
+ this.cleanupAfterToolError(toolName, operation, cleanup);
196
+ }
197
+ const message = this.formatExternalToolError(toolName, operation, error);
198
+ const wrapped = new Error(message);
199
+ wrapped.cause = error;
200
+ throw wrapped;
201
+ }
202
+ }
203
+ isNonEmptyString(value) {
204
+ return typeof value === 'string' && value.trim().length > 0;
205
+ }
206
+ describeInvalidValue(value) {
207
+ if (value === null)
208
+ return 'null';
209
+ if (value === undefined)
210
+ return 'undefined';
211
+ if (Array.isArray(value))
212
+ return 'array';
213
+ return typeof value;
214
+ }
215
+ assertStringRecord(value, context) {
216
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
217
+ throw new Error(`${context} must be an object with string values; received ${this.describeInvalidValue(value)}.`);
218
+ }
219
+ for (const [key, entryValue] of Object.entries(value)) {
220
+ if (!this.isNonEmptyString(key)) {
221
+ throw new Error(`${context} contains an invalid empty parameter name.`);
222
+ }
223
+ if (typeof entryValue !== 'string') {
224
+ throw new Error(`${context}.${key} must be a string; received ${this.describeInvalidValue(entryValue)}.`);
225
+ }
226
+ }
227
+ }
228
+ requireNonEmptyString(value, fieldName, toolName) {
229
+ if (!this.isNonEmptyString(value)) {
230
+ throw new Error(`Invalid arguments for ${toolName}: ${fieldName} is required and must be a non-empty string.`);
231
+ }
232
+ return value;
233
+ }
234
+ requireArgsObject(args, toolName) {
235
+ this.assertStringRecord(args, `Invalid arguments for ${toolName}: args`);
236
+ }
144
237
  // Session-based tool approvals - remembers which tools user approved for this turn
145
238
  sessionApprovedTools = new Set();
146
239
  // Persistent permissions - tool allowlists per project
147
240
  static permissionsFile = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.vigthoria', 'permissions.json');
148
241
  constructor(logger, cwd, permissionCallback, autoApprove = false) {
242
+ if (!logger) {
243
+ throw new Error('AgenticTools initialization failed: logger is required.');
244
+ }
245
+ if (typeof cwd !== 'string' || cwd.trim().length === 0) {
246
+ throw new Error('AgenticTools initialization failed: cwd must be a non-empty string.');
247
+ }
248
+ if (typeof permissionCallback !== 'function') {
249
+ throw new Error('AgenticTools initialization failed: permissionCallback must be a function.');
250
+ }
251
+ if (typeof autoApprove !== 'boolean') {
252
+ throw new Error('AgenticTools initialization failed: autoApprove must be a boolean.');
253
+ }
149
254
  this.logger = logger;
150
255
  this.cwd = cwd;
151
256
  this.permissionCallback = permissionCallback;
152
257
  this.autoApprove = autoApprove;
153
258
  }
259
+ getErrorMessage(error) {
260
+ if (error instanceof Error) {
261
+ return error.message;
262
+ }
263
+ if (typeof error === 'string') {
264
+ return error;
265
+ }
266
+ try {
267
+ return JSON.stringify(error);
268
+ }
269
+ catch {
270
+ return String(error);
271
+ }
272
+ }
273
+ assertToolCall(call) {
274
+ if (!call || typeof call !== 'object') {
275
+ throw new Error('Invalid tool call: call must be an object.');
276
+ }
277
+ if (typeof call.tool !== 'string' || call.tool.trim().length === 0) {
278
+ throw new Error('Invalid tool call: tool name must be a non-empty string.');
279
+ }
280
+ if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
281
+ throw new Error(`Invalid tool call for ${call.tool}: args must be an object.`);
282
+ }
283
+ for (const [key, value] of Object.entries(call.args)) {
284
+ if (typeof key !== 'string' || key.trim().length === 0) {
285
+ throw new Error(`Invalid tool call for ${call.tool}: argument names must be non-empty strings.`);
286
+ }
287
+ if (typeof value !== 'string') {
288
+ throw new Error(`Invalid argument "${key}" for ${call.tool}: expected string, received ${typeof value}.`);
289
+ }
290
+ }
291
+ }
154
292
  /**
155
293
  * Load persistent permissions for the current project
156
294
  */
@@ -160,8 +298,10 @@ class AgenticTools {
160
298
  return JSON.parse(fs.readFileSync(AgenticTools.permissionsFile, 'utf-8'));
161
299
  }
162
300
  }
163
- catch {
164
- // Corrupted file - ignore
301
+ catch (error) {
302
+ const message = this.formatExternalToolError('permissions', `read ${AgenticTools.permissionsFile}`, error);
303
+ this.logger.debug(message);
304
+ this.logger.warn(message);
165
305
  }
166
306
  return {};
167
307
  }
@@ -184,8 +324,10 @@ class AgenticTools {
184
324
  fs.mkdirSync(dir, { recursive: true });
185
325
  fs.writeFileSync(AgenticTools.permissionsFile, JSON.stringify(permissions, null, 2), 'utf-8');
186
326
  }
187
- catch {
188
- // Non-critical - permission won't persist
327
+ catch (error) {
328
+ const message = this.formatExternalToolError('permissions', `write ${AgenticTools.permissionsFile}`, error);
329
+ this.logger.debug(message);
330
+ this.logger.warn(message);
189
331
  }
190
332
  }
191
333
  /**
@@ -468,9 +610,21 @@ class AgenticTools {
468
610
  * Execute a tool call with enhanced error handling and retry logic
469
611
  */
470
612
  async execute(call) {
471
- // Guard against malformed tool calls with undefined or empty tool names
613
+ // Guard against malformed tool calls before accessing fields.
614
+ if (!call || typeof call !== 'object') {
615
+ 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.');
616
+ }
472
617
  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(', ')}`);
618
+ 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(', ')}`);
619
+ }
620
+ if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
621
+ 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" }.');
622
+ }
623
+ try {
624
+ this.assertStringRecord(call.args, `Invalid tool call for '${call.tool}': args`);
625
+ }
626
+ catch (error) {
627
+ 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
628
  }
475
629
  const normalizedCall = this.normalizeToolCall(call);
476
630
  const tool = AgenticTools.getToolDefinitions().find(t => t.name === normalizedCall.tool);
@@ -594,39 +748,57 @@ class AgenticTools {
594
748
  * Execute the actual tool operation
595
749
  */
596
750
  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}`);
751
+ try {
752
+ switch (call.tool) {
753
+ case 'read_file':
754
+ return this.readFile(call.args);
755
+ case 'write_file':
756
+ return this.writeFile(call.args);
757
+ case 'edit_file':
758
+ return this.editFile(call.args);
759
+ case 'bash':
760
+ return this.bash(call.args);
761
+ case 'grep':
762
+ return this.grep(call.args);
763
+ case 'list_dir':
764
+ return this.listDir(call.args);
765
+ case 'glob':
766
+ return this.glob(call.args);
767
+ case 'git':
768
+ return this.git(call.args);
769
+ case 'repo':
770
+ return this.repo(call.args);
771
+ case 'fetch_url':
772
+ return this.fetchUrl(call.args);
773
+ case 'browser':
774
+ return this.browserTool(call.args);
775
+ case 'ssh_exec':
776
+ return this.sshExec(call.args);
777
+ case 'task':
778
+ return this.task(call.args);
779
+ case 'multi_edit':
780
+ return this.multiEdit(call.args);
781
+ case 'codebase_search':
782
+ return this.codebaseSearch(call.args);
783
+ default:
784
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Tool not implemented: ${call.tool}`);
785
+ }
786
+ }
787
+ catch (error) {
788
+ const context = `Tool '${call.tool}' failed while executing with args ${this.safeStringifyArgs(call.args)}`;
789
+ const stderr = error?.stderr?.toString?.().trim();
790
+ const stdout = error?.stdout?.toString?.().trim();
791
+ const status = error?.status !== undefined ? `exit status ${error.status}` : undefined;
792
+ const signal = error?.signal ? `signal ${error.signal}` : undefined;
793
+ const details = [context, status, signal, stderr || error?.message || String(error)].filter(Boolean).join(': ');
794
+ this.logger.error(details);
795
+ return {
796
+ success: false,
797
+ output: stdout,
798
+ error: details,
799
+ suggestion: 'Review the tool arguments and subprocess output above, then retry with corrected input.',
800
+ canRetry: this.isRetryableError(details),
801
+ };
630
802
  }
631
803
  }
632
804
  /**
@@ -635,13 +807,108 @@ class AgenticTools {
635
807
  isRetryableError(error) {
636
808
  return this.retryConfig.retryableErrors.some(e => error.includes(e));
637
809
  }
810
+ /**
811
+ * Safely stringify tool arguments without dumping very large payloads into logs.
812
+ */
813
+ safeStringifyArgs(args) {
814
+ try {
815
+ const json = JSON.stringify(args ?? {}, (_key, value) => {
816
+ if (typeof value === 'string' && value.length > 500) {
817
+ return `${value.slice(0, 500)}...<truncated ${value.length - 500} chars>`;
818
+ }
819
+ return value;
820
+ });
821
+ return json || '{}';
822
+ }
823
+ catch (error) {
824
+ return `[unserializable args: ${error instanceof Error ? error.message : String(error)}]`;
825
+ }
826
+ }
638
827
  /**
639
828
  * Validate tool parameters
640
829
  */
641
830
  validateParameters(call, tool) {
831
+ if (!call || typeof call !== 'object') {
832
+ 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" } }.');
833
+ }
834
+ if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
835
+ 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)}`);
836
+ }
837
+ for (const [key, value] of Object.entries(call.args)) {
838
+ if (!key || typeof key !== 'string') {
839
+ 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.');
840
+ }
841
+ if (typeof value !== 'string') {
842
+ 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}.`);
843
+ }
844
+ }
642
845
  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}`);
846
+ const value = call.args[param.name];
847
+ if (param.required && (value === undefined || value.trim() === '')) {
848
+ 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}`);
849
+ }
850
+ }
851
+ const pathLikeParams = ['path', 'working_dir', 'cwd'];
852
+ for (const paramName of pathLikeParams) {
853
+ const value = call.args[paramName];
854
+ if (typeof value === 'string' && value.includes('\0')) {
855
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid ${paramName} for ${tool.name}: paths cannot contain null bytes`, 'Remove null bytes from the path and retry.');
856
+ }
857
+ }
858
+ if (call.args.start_line !== undefined && (!/^\d+$/.test(call.args.start_line) || Number(call.args.start_line) < 1)) {
859
+ 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.');
860
+ }
861
+ if (call.args.end_line !== undefined && (!/^\d+$/.test(call.args.end_line) || Number(call.args.end_line) < 0)) {
862
+ 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.');
863
+ }
864
+ if (call.args.start_line !== undefined && call.args.end_line !== undefined) {
865
+ const startLine = Number(call.args.start_line);
866
+ const endLine = Number(call.args.end_line);
867
+ if (endLine !== 0 && endLine < startLine) {
868
+ 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.');
869
+ }
870
+ }
871
+ if (call.args.timeout !== undefined && (!/^\d+$/.test(call.args.timeout) || Number(call.args.timeout) < 1)) {
872
+ 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.');
873
+ }
874
+ if (call.args.max_results !== undefined && (!/^\d+$/.test(call.args.max_results) || Number(call.args.max_results) < 1)) {
875
+ 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.');
876
+ }
877
+ if (call.tool === 'list_dir' && call.args.recursive !== undefined && !['true', 'false', '1', '0', 'yes', 'no'].includes(call.args.recursive.toLowerCase())) {
878
+ 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".');
879
+ }
880
+ if (call.tool === 'fetch_url' && call.args.url !== undefined) {
881
+ try {
882
+ const parsed = new URL(call.args.url);
883
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
884
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid URL protocol for fetch_url: '${parsed.protocol}'`, 'Use an http:// or https:// URL.');
885
+ }
886
+ }
887
+ catch (error) {
888
+ const message = this.getErrorMessage(error);
889
+ this.logger.debug(`Invalid URL for fetch_url '${call.args.url}': ${message}`);
890
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid URL for fetch_url: '${call.args.url}'`, 'Provide a fully-qualified http:// or https:// URL.');
891
+ }
892
+ }
893
+ if (call.tool === 'multi_edit' && call.args.edits !== undefined) {
894
+ try {
895
+ const edits = JSON.parse(call.args.edits);
896
+ if (!Array.isArray(edits) || edits.length === 0) {
897
+ 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.');
898
+ }
899
+ for (const [index, edit] of edits.entries()) {
900
+ if (!edit || typeof edit !== 'object' || Array.isArray(edit)) {
901
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid edit at index ${index}: expected an object`);
902
+ }
903
+ for (const field of ['path', 'old_text', 'new_text']) {
904
+ if (typeof edit[field] !== 'string') {
905
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid edit at index ${index}: '${field}' must be a string`);
906
+ }
907
+ }
908
+ }
909
+ }
910
+ catch (error) {
911
+ 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
912
  }
646
913
  }
647
914
  return null;
@@ -954,18 +1221,27 @@ class AgenticTools {
954
1221
  validateJavaScriptSyntax(source) {
955
1222
  const tempFile = path.join(this.cwd, `.vigthoria-temp-${Date.now()}.js`);
956
1223
  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.';
1224
+ try {
1225
+ fs.writeFileSync(tempFile, source, 'utf-8');
1226
+ }
1227
+ catch (writeError) {
1228
+ return this.formatExternalToolError('syntax_check', `write temporary file ${tempFile}`, writeError);
1229
+ }
1230
+ try {
1231
+ this.runExternalCommand('syntax_check', `node --check ${tempFile}`, `node --check "${tempFile}"`, { stdio: 'pipe' });
1232
+ return null;
1233
+ }
1234
+ catch (error) {
1235
+ const stderr = error.stderr?.toString()?.trim();
1236
+ return stderr || error.message || this.formatExternalToolError('syntax_check', `node --check ${tempFile}`, error);
1237
+ }
964
1238
  }
965
1239
  finally {
966
- if (fs.existsSync(tempFile)) {
967
- fs.unlinkSync(tempFile);
968
- }
1240
+ this.cleanupAfterToolError('syntax_check', `remove temporary file ${tempFile}`, () => {
1241
+ if (fs.existsSync(tempFile)) {
1242
+ fs.unlinkSync(tempFile);
1243
+ }
1244
+ });
969
1245
  }
970
1246
  }
971
1247
  validateHtmlContent(content) {
@@ -1131,7 +1407,7 @@ class AgenticTools {
1131
1407
  // Use /bin/sh on Unix-like systems (more portable than bash)
1132
1408
  execOptions.shell = '/bin/sh';
1133
1409
  }
1134
- const output = (0, child_process_1.execSync)(args.command, execOptions);
1410
+ const output = this.runExternalCommand('bash', args.command, args.command, execOptions);
1135
1411
  const duration = Date.now() - startTime;
1136
1412
  return {
1137
1413
  success: true,
@@ -1189,12 +1465,16 @@ class AgenticTools {
1189
1465
  (0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1190
1466
  return this.grepWithRg(args, searchPath);
1191
1467
  }
1192
- catch { /* rg not available */ }
1468
+ catch (error) {
1469
+ this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Windows', error));
1470
+ }
1193
1471
  // 2. Try PowerShell Select-String
1194
1472
  try {
1195
1473
  return this.grepWithSelectString(args, searchPath);
1196
1474
  }
1197
- catch { /* Select-String failed */ }
1475
+ catch (error) {
1476
+ this.logger.debug(this.formatExternalToolError('grep', 'running PowerShell Select-String fallback', error));
1477
+ }
1198
1478
  // 3. Fall back to Node-native recursive file scanner
1199
1479
  return this.grepNodeNative(args, searchPath);
1200
1480
  }
@@ -1204,10 +1484,13 @@ class AgenticTools {
1204
1484
  grepUnix(args, searchPath, platform) {
1205
1485
  // Try ripgrep first if available
1206
1486
  try {
1207
- (0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1487
+ this.runExternalCommand('grep', 'checking ripgrep availability on Unix', 'rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1208
1488
  return this.grepWithRg(args, searchPath);
1209
1489
  }
1210
- catch { /* rg not available, fall through to system grep */ }
1490
+ catch (error) {
1491
+ this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Unix', error));
1492
+ // rg not available, fall through to system grep
1493
+ }
1211
1494
  const isMac = platform === 'darwin';
1212
1495
  let cmd;
1213
1496
  if (isMac) {
@@ -1221,7 +1504,7 @@ class AgenticTools {
1221
1504
  : `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
1222
1505
  }
1223
1506
  try {
1224
- const output = (0, child_process_1.execSync)(cmd, {
1507
+ const output = this.runExternalCommand('grep', 'system grep search', cmd, {
1225
1508
  cwd: this.cwd,
1226
1509
  encoding: 'utf-8',
1227
1510
  maxBuffer: 5 * 1024 * 1024,
@@ -1264,7 +1547,7 @@ class AgenticTools {
1264
1547
  }
1265
1548
  cmd += ` "${args.pattern}" "${searchPath}"`;
1266
1549
  try {
1267
- const output = (0, child_process_1.execSync)(cmd, {
1550
+ const output = this.runExternalCommand('grep', 'ripgrep search', cmd, {
1268
1551
  cwd: this.cwd,
1269
1552
  encoding: 'utf-8',
1270
1553
  maxBuffer: 5 * 1024 * 1024,