@loxia-labs/loxia-autopilot-one 1.0.1

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.
Files changed (80) hide show
  1. package/LICENSE +267 -0
  2. package/README.md +509 -0
  3. package/bin/cli.js +117 -0
  4. package/package.json +94 -0
  5. package/scripts/install-scanners.js +236 -0
  6. package/src/analyzers/CSSAnalyzer.js +297 -0
  7. package/src/analyzers/ConfigValidator.js +690 -0
  8. package/src/analyzers/ESLintAnalyzer.js +320 -0
  9. package/src/analyzers/JavaScriptAnalyzer.js +261 -0
  10. package/src/analyzers/PrettierFormatter.js +247 -0
  11. package/src/analyzers/PythonAnalyzer.js +266 -0
  12. package/src/analyzers/SecurityAnalyzer.js +729 -0
  13. package/src/analyzers/TypeScriptAnalyzer.js +247 -0
  14. package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
  15. package/src/analyzers/codeCloneDetector/detector.js +203 -0
  16. package/src/analyzers/codeCloneDetector/index.js +160 -0
  17. package/src/analyzers/codeCloneDetector/parser.js +199 -0
  18. package/src/analyzers/codeCloneDetector/reporter.js +148 -0
  19. package/src/analyzers/codeCloneDetector/scanner.js +59 -0
  20. package/src/core/agentPool.js +1474 -0
  21. package/src/core/agentScheduler.js +2147 -0
  22. package/src/core/contextManager.js +709 -0
  23. package/src/core/messageProcessor.js +732 -0
  24. package/src/core/orchestrator.js +548 -0
  25. package/src/core/stateManager.js +877 -0
  26. package/src/index.js +631 -0
  27. package/src/interfaces/cli.js +549 -0
  28. package/src/interfaces/webServer.js +2162 -0
  29. package/src/modules/fileExplorer/controller.js +280 -0
  30. package/src/modules/fileExplorer/index.js +37 -0
  31. package/src/modules/fileExplorer/middleware.js +92 -0
  32. package/src/modules/fileExplorer/routes.js +125 -0
  33. package/src/modules/fileExplorer/types.js +44 -0
  34. package/src/services/aiService.js +1232 -0
  35. package/src/services/apiKeyManager.js +164 -0
  36. package/src/services/benchmarkService.js +366 -0
  37. package/src/services/budgetService.js +539 -0
  38. package/src/services/contextInjectionService.js +247 -0
  39. package/src/services/conversationCompactionService.js +637 -0
  40. package/src/services/errorHandler.js +810 -0
  41. package/src/services/fileAttachmentService.js +544 -0
  42. package/src/services/modelRouterService.js +366 -0
  43. package/src/services/modelsService.js +322 -0
  44. package/src/services/qualityInspector.js +796 -0
  45. package/src/services/tokenCountingService.js +536 -0
  46. package/src/tools/agentCommunicationTool.js +1344 -0
  47. package/src/tools/agentDelayTool.js +485 -0
  48. package/src/tools/asyncToolManager.js +604 -0
  49. package/src/tools/baseTool.js +800 -0
  50. package/src/tools/browserTool.js +920 -0
  51. package/src/tools/cloneDetectionTool.js +621 -0
  52. package/src/tools/dependencyResolverTool.js +1215 -0
  53. package/src/tools/fileContentReplaceTool.js +875 -0
  54. package/src/tools/fileSystemTool.js +1107 -0
  55. package/src/tools/fileTreeTool.js +853 -0
  56. package/src/tools/imageTool.js +901 -0
  57. package/src/tools/importAnalyzerTool.js +1060 -0
  58. package/src/tools/jobDoneTool.js +248 -0
  59. package/src/tools/seekTool.js +956 -0
  60. package/src/tools/staticAnalysisTool.js +1778 -0
  61. package/src/tools/taskManagerTool.js +2873 -0
  62. package/src/tools/terminalTool.js +2304 -0
  63. package/src/tools/webTool.js +1430 -0
  64. package/src/types/agent.js +519 -0
  65. package/src/types/contextReference.js +972 -0
  66. package/src/types/conversation.js +730 -0
  67. package/src/types/toolCommand.js +747 -0
  68. package/src/utilities/attachmentValidator.js +292 -0
  69. package/src/utilities/configManager.js +582 -0
  70. package/src/utilities/constants.js +722 -0
  71. package/src/utilities/directoryAccessManager.js +535 -0
  72. package/src/utilities/fileProcessor.js +307 -0
  73. package/src/utilities/logger.js +436 -0
  74. package/src/utilities/tagParser.js +1246 -0
  75. package/src/utilities/toolConstants.js +317 -0
  76. package/web-ui/build/index.html +15 -0
  77. package/web-ui/build/logo.png +0 -0
  78. package/web-ui/build/logo2.png +0 -0
  79. package/web-ui/build/static/index-CjkkcnFA.js +344 -0
  80. package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
@@ -0,0 +1,2304 @@
1
+ /**
2
+ * TerminalTool - Execute terminal/command line operations
3
+ *
4
+ * Purpose:
5
+ * - Execute system commands safely
6
+ * - Handle directory navigation
7
+ * - Manage command output and errors
8
+ * - Support both synchronous and asynchronous execution
9
+ */
10
+
11
+ import { BaseTool } from './baseTool.js';
12
+ import TagParser from '../utilities/tagParser.js';
13
+ import DirectoryAccessManager from '../utilities/directoryAccessManager.js';
14
+ import { spawn, exec } from 'child_process';
15
+ import path from 'path';
16
+ import fs from 'fs/promises';
17
+
18
+ import {
19
+ TOOL_STATUS,
20
+ SYSTEM_DEFAULTS
21
+ } from '../utilities/constants.js';
22
+
23
+ /**
24
+ * PromptDetector - Detects interactive prompts in command output
25
+ * Phase 2: Prompt Detection System
26
+ */
27
+ class PromptDetector {
28
+ constructor() {
29
+ // Common prompt patterns (case-insensitive)
30
+ this.promptPatterns = [
31
+ // Yes/No prompts
32
+ { pattern: /\(y\/n\)/i, type: 'yes-no', description: 'Yes/No question' },
33
+ { pattern: /\(Y\/n\)/i, type: 'yes-no-default-yes', description: 'Yes/No (default Yes)' },
34
+ { pattern: /\(y\/N\)/i, type: 'yes-no-default-no', description: 'Yes/No (default No)' },
35
+ { pattern: /\[y\/n\]/i, type: 'yes-no', description: 'Yes/No question' },
36
+ { pattern: /\[Y\/n\]/i, type: 'yes-no-default-yes', description: 'Yes/No (default Yes)' },
37
+ { pattern: /\[y\/N\]/i, type: 'yes-no-default-no', description: 'Yes/No (default No)' },
38
+
39
+ // Continue prompts
40
+ { pattern: /continue\?/i, type: 'continue', description: 'Continue prompt' },
41
+ { pattern: /proceed\?/i, type: 'continue', description: 'Proceed prompt' },
42
+ { pattern: /press any key to continue/i, type: 'keypress', description: 'Press any key' },
43
+ { pattern: /press enter to continue/i, type: 'keypress', description: 'Press enter' },
44
+ { pattern: /hit enter to continue/i, type: 'keypress', description: 'Hit enter' },
45
+
46
+ // Password/Authentication prompts
47
+ { pattern: /password:/i, type: 'password', description: 'Password prompt' },
48
+ { pattern: /enter password/i, type: 'password', description: 'Password prompt' },
49
+ { pattern: /passphrase:/i, type: 'password', description: 'Passphrase prompt' },
50
+ { pattern: /username:/i, type: 'username', description: 'Username prompt' },
51
+ { pattern: /enter username/i, type: 'username', description: 'Username prompt' },
52
+
53
+ // Input prompts
54
+ { pattern: /enter\s+\w+:/i, type: 'input', description: 'Generic input prompt' },
55
+ { pattern: /please enter/i, type: 'input', description: 'Generic input prompt' },
56
+ { pattern: /input:/i, type: 'input', description: 'Generic input prompt' },
57
+
58
+ // Confirmation prompts
59
+ { pattern: /are you sure\?/i, type: 'confirmation', description: 'Confirmation prompt' },
60
+ { pattern: /do you want to/i, type: 'confirmation', description: 'Confirmation prompt' },
61
+ { pattern: /would you like to/i, type: 'confirmation', description: 'Confirmation prompt' },
62
+
63
+ // Selection prompts
64
+ { pattern: /select an option/i, type: 'selection', description: 'Selection prompt' },
65
+ { pattern: /choose/i, type: 'selection', description: 'Selection prompt' },
66
+ { pattern: /\d+\)\s+\w+/g, type: 'menu', description: 'Menu selection' } // Matches: 1) Option
67
+ ];
68
+ }
69
+
70
+ /**
71
+ * Analyze output for prompt patterns
72
+ * @param {string} output - Output text to analyze (stdout or stderr)
73
+ * @param {string} source - Source of output ('stdout' or 'stderr')
74
+ * @returns {Object|null} Prompt detection result or null
75
+ */
76
+ detectPrompt(output, source = 'stdout') {
77
+ if (!output || output.trim().length === 0) {
78
+ return null;
79
+ }
80
+
81
+ // Get the last few lines (prompts are usually at the end)
82
+ const lines = output.split('\n');
83
+ const lastLines = lines.slice(-5).join('\n'); // Check last 5 lines
84
+
85
+ // Check each pattern
86
+ for (const promptDef of this.promptPatterns) {
87
+ const match = lastLines.match(promptDef.pattern);
88
+ if (match) {
89
+ return {
90
+ detected: true,
91
+ type: promptDef.type,
92
+ description: promptDef.description,
93
+ matchedText: match[0],
94
+ matchIndex: match.index,
95
+ source: source,
96
+ fullContext: lastLines,
97
+ timestamp: Date.now()
98
+ };
99
+ }
100
+ }
101
+
102
+ // Check for generic prompt indicators
103
+ // Look for lines ending with : or ? without newline after
104
+ const lastLine = lines[lines.length - 1] || '';
105
+ if (lastLine.trim().length > 0) {
106
+ const endsWithColon = /:\s*$/.test(lastLine);
107
+ const endsWithQuestion = /\?\s*$/.test(lastLine);
108
+
109
+ if (endsWithColon || endsWithQuestion) {
110
+ // Might be a prompt - check if it's asking for input
111
+ const looksLikePrompt = /\b(enter|type|provide|specify|input)\b/i.test(lastLine);
112
+ if (looksLikePrompt) {
113
+ return {
114
+ detected: true,
115
+ type: 'generic',
116
+ description: 'Generic input prompt detected',
117
+ matchedText: lastLine.trim(),
118
+ source: source,
119
+ fullContext: lastLines,
120
+ timestamp: Date.now(),
121
+ confidence: 0.7 // Lower confidence for generic detection
122
+ };
123
+ }
124
+ }
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Check if output indicates command is waiting (no prompt but no output)
132
+ * @param {number} lastOutputTime - Timestamp of last output
133
+ * @param {number} hangThresholdMs - Milliseconds to consider as hanging
134
+ * @returns {Object} Hang detection result
135
+ */
136
+ detectHang(lastOutputTime, hangThresholdMs = 30000) {
137
+ const now = Date.now();
138
+ const timeSinceLastOutput = now - lastOutputTime;
139
+
140
+ return {
141
+ isHanging: timeSinceLastOutput >= hangThresholdMs,
142
+ timeSinceLastOutput: timeSinceLastOutput,
143
+ threshold: hangThresholdMs,
144
+ likelyWaiting: timeSinceLastOutput >= hangThresholdMs / 2 // 50% threshold
145
+ };
146
+ }
147
+ }
148
+
149
+ class TerminalTool extends BaseTool {
150
+ constructor(config = {}, logger = null) {
151
+ super(config, logger);
152
+
153
+ // Tool metadata
154
+ this.requiresProject = false;
155
+ this.isAsync = false; // Most commands are quick, use sync execution
156
+ this.timeout = config.timeout || 120000; // 2 minutes default
157
+ this.maxConcurrentOperations = config.maxConcurrentOperations || 3;
158
+
159
+ // Current working directories per context
160
+ this.workingDirectories = new Map();
161
+
162
+ // Command history
163
+ this.commandHistory = [];
164
+
165
+ // Security settings
166
+ this.allowedCommands = config.allowedCommands || null; // null = all allowed
167
+ this.blockedCommands = config.blockedCommands || [
168
+ 'rm -rf /',
169
+ 'format',
170
+ 'del /f /q',
171
+ 'shutdown',
172
+ 'reboot',
173
+ 'halt'
174
+ ];
175
+
176
+ // Directory access manager
177
+ this.directoryAccessManager = new DirectoryAccessManager(config, logger);
178
+
179
+ // Prompt detector (Phase 2)
180
+ this.promptDetector = new PromptDetector();
181
+
182
+ // Phase 3 & 4: Background command tracking
183
+ this.commandTracker = new Map(); // commandId -> { agentId, pid, process, state, buffers, timestamps }
184
+ this.commandIdCounter = 0;
185
+
186
+ // Resource limits
187
+ this.MAX_BACKGROUND_COMMANDS_PER_AGENT = config.maxBackgroundCommandsPerAgent || 5;
188
+ this.MAX_BACKGROUND_COMMANDS_GLOBAL = config.maxBackgroundCommandsGlobal || 20;
189
+ this.MAX_COMMAND_AGE_MINUTES = config.maxCommandAgeMinutes || 60;
190
+
191
+ // Terminal detection
192
+ this.detectedTerminal = null;
193
+ this.platformType = null;
194
+ this.initializeTerminalDetection();
195
+ }
196
+
197
+ /**
198
+ * Get tool description for LLM consumption
199
+ * @returns {string} Tool description
200
+ */
201
+ getDescription() {
202
+ return `
203
+ Terminal Tool: Execute system commands and manage terminal operations safely.
204
+
205
+ IMPORTANT: For file and directory creation, prefer using the FileSystem tool.
206
+ Reserve the Terminal tool for command-line operations like npm, git, curl, etc.
207
+
208
+ USAGE:
209
+ [tool id="terminal"]
210
+ <run-command>npm install express</run-command>
211
+ <change-directory>project/backend</change-directory>
212
+ [/tool]
213
+
214
+ ALTERNATIVE JSON FORMAT:
215
+ \`\`\`json
216
+ {
217
+ "toolId": "terminal",
218
+ "actions": [
219
+ {
220
+ "type": "run-command",
221
+ "command": "npm install express"
222
+ },
223
+ {
224
+ "type": "change-directory",
225
+ "directory": "project/backend"
226
+ }
227
+ ]
228
+ }
229
+ \`\`\`
230
+
231
+ SUPPORTED ACTIONS:
232
+ - run-command: Execute a command in the current directory
233
+ - change-directory: Change current working directory
234
+ - list-directory: List contents of current or specified directory
235
+ - create-directory: Create a new directory (prefer FileSystem tool)
236
+ - get-working-directory: Get current working directory
237
+
238
+ PARAMETERS:
239
+ - command: The command to execute
240
+ - directory: Directory path for navigation/operations
241
+ - timeout: Optional timeout in milliseconds (max ${this.timeout}ms)
242
+ - async: Whether to run command asynchronously (true/false)
243
+
244
+ BACKGROUND COMMAND MANAGEMENT (Advanced):
245
+ For long-running commands (npm install, git operations, builds), use background command tracking:
246
+
247
+ **startBackgroundCommand(command, workingDir, {agentId, context})**
248
+ - Starts command in background with stdin kept open
249
+ - Returns commandId for tracking
250
+ - Max ${this.MAX_BACKGROUND_COMMANDS_PER_AGENT} per agent, ${this.MAX_BACKGROUND_COMMANDS_GLOBAL} global
251
+ - Auto-detects prompts and updates state to 'waiting_for_input'
252
+
253
+ **getCommandStatus(commandId, agentId)**
254
+ - Get real-time status: running, waiting_for_input, completed, failed
255
+ - Returns stdout/stderr buffers, exit code, timestamps
256
+ - Shows prompt detection info if interactive prompt detected
257
+
258
+ **sendInput(commandId, input, agentId)**
259
+ - Send input to command's stdin (answers prompts)
260
+ - Automatically adds newline if not present
261
+ - Updates state from 'waiting_for_input' to 'running'
262
+
263
+ **killCommand(commandId, agentId)**
264
+ - Terminate background command (SIGTERM → SIGKILL)
265
+ - Safe to call on already-completed commands
266
+
267
+ **listAgentCommands(agentId)**
268
+ - List all commands (running and completed) for agent
269
+ - Useful for monitoring multiple background processes
270
+
271
+ BACKGROUND COMMAND WORKFLOW:
272
+ 1. Start: const {commandId} = await terminal.startBackgroundCommand('npm install', dir, {agentId})
273
+ 2. Wait: Use agent-delay tool to wait while command runs
274
+ 3. Check: const status = terminal.getCommandStatus(commandId, agentId)
275
+ 4. If prompt detected: terminal.sendInput(commandId, 'y', agentId)
276
+ 5. Verify: Check status.state === 'completed' && status.exitCode === 0
277
+
278
+ EXAMPLES:
279
+ Basic command execution:
280
+ [tool id="terminal"]
281
+ <run-command>npm install</run-command>
282
+ [/tool]
283
+
284
+ Execute curl command:
285
+ [tool id="terminal"]
286
+ <run-command>curl -s https://api.github.com/zen</run-command>
287
+ [/tool]
288
+
289
+ Change directory and run command:
290
+ [tool id="terminal"]
291
+ <change-directory>../frontend</change-directory>
292
+ <run-command>npm run build</run-command>
293
+ [/tool]
294
+
295
+ Package management:
296
+ [tool id="terminal"]
297
+ <run-command>pip install -r requirements.txt</run-command>
298
+ [/tool]
299
+
300
+ Git operations:
301
+ [tool id="terminal"]
302
+ <run-command>git status</run-command>
303
+ [/tool]
304
+
305
+ [tool id="terminal"]
306
+ <run-command>git add .</run-command>
307
+ <run-command>git commit -m "Update files"</run-command>
308
+ [/tool]
309
+
310
+ Directory operations (if FileSystem tool unavailable):
311
+ [tool id="terminal"]
312
+ <create-directory>backend</create-directory>
313
+ [/tool]
314
+
315
+ SECURITY NOTES:
316
+ - Dangerous commands are blocked for safety
317
+ - Commands execute in isolated environment
318
+ - Output is captured and returned safely
319
+ - Long-running commands may time out - use agent-delay tool to wait
320
+
321
+ BEST PRACTICES:
322
+ - Use FileSystem tool for file/directory creation and management
323
+ - Use Terminal tool for command-line utilities (npm, git, curl, grep, etc.)
324
+ - After starting long-running commands, use agent-delay tool to pause checking
325
+ - Always check command output to verify success
326
+
327
+ CURRENT DIRECTORY:
328
+ The tool maintains a current working directory context per agent.
329
+ Use change-directory to navigate, and subsequent commands will execute from that location.
330
+ `;
331
+ }
332
+
333
+ /**
334
+ * Parse parameters from tool command content
335
+ * @param {string} content - Raw tool command content
336
+ * @returns {Object} Parsed parameters
337
+ */
338
+ parseParameters(content) {
339
+ try {
340
+ const params = {};
341
+
342
+ // Extract individual action parameters
343
+ const runCommandMatches = TagParser.extractContent(content, 'run-command');
344
+ const changeDirMatches = TagParser.extractContent(content, 'change-directory');
345
+ const listDirMatches = TagParser.extractContent(content, 'list-directory');
346
+ const createDirMatches = TagParser.extractContent(content, 'create-directory');
347
+ const getWdMatches = TagParser.extractContent(content, 'get-working-directory');
348
+ const timeoutMatches = TagParser.extractContent(content, 'timeout');
349
+ const asyncMatches = TagParser.extractContent(content, 'async');
350
+
351
+ // Build actions array
352
+ const actions = [];
353
+
354
+ if (runCommandMatches.length > 0) {
355
+ actions.push({
356
+ type: 'run-command',
357
+ command: runCommandMatches[0].trim()
358
+ });
359
+ }
360
+
361
+ if (changeDirMatches.length > 0) {
362
+ actions.push({
363
+ type: 'change-directory',
364
+ directory: changeDirMatches[0].trim()
365
+ });
366
+ }
367
+
368
+ if (listDirMatches.length > 0) {
369
+ actions.push({
370
+ type: 'list-directory',
371
+ directory: listDirMatches[0].trim() || '.'
372
+ });
373
+ }
374
+
375
+ if (createDirMatches.length > 0) {
376
+ actions.push({
377
+ type: 'create-directory',
378
+ directory: createDirMatches[0].trim()
379
+ });
380
+ }
381
+
382
+ if (getWdMatches.length > 0) {
383
+ actions.push({
384
+ type: 'get-working-directory'
385
+ });
386
+ }
387
+
388
+ params.actions = actions;
389
+
390
+ // Parse additional options
391
+ if (timeoutMatches.length > 0) {
392
+ params.timeout = parseInt(timeoutMatches[0], 10);
393
+ }
394
+
395
+ if (asyncMatches.length > 0) {
396
+ params.async = asyncMatches[0].toLowerCase() === 'true';
397
+ }
398
+
399
+ params.rawContent = content.trim();
400
+
401
+ return params;
402
+
403
+ } catch (error) {
404
+ throw new Error(`Failed to parse terminal parameters: ${error.message}`);
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Get required parameters
410
+ * @returns {Array<string>} Array of required parameter names
411
+ */
412
+ getRequiredParameters() {
413
+ return ['actions'];
414
+ }
415
+
416
+ /**
417
+ * Custom parameter validation
418
+ * @param {Object} params - Parameters to validate
419
+ * @returns {Object} Validation result
420
+ */
421
+ customValidateParameters(params) {
422
+ const errors = [];
423
+
424
+ if (!params.actions || !Array.isArray(params.actions) || params.actions.length === 0) {
425
+ errors.push('At least one action is required');
426
+ } else {
427
+ // Validate each action
428
+ for (const [index, action] of params.actions.entries()) {
429
+ if (!action.type) {
430
+ errors.push(`Action ${index + 1}: type is required`);
431
+ continue;
432
+ }
433
+
434
+ switch (action.type) {
435
+ case 'run-command':
436
+ if (!action.command || !action.command.trim()) {
437
+ errors.push(`Action ${index + 1}: command is required for run-command`);
438
+ } else if (this.isBlockedCommand(action.command)) {
439
+ errors.push(`Action ${index + 1}: command is blocked for security: ${action.command}`);
440
+ } else if (this.allowedCommands && !this.isAllowedCommand(action.command)) {
441
+ errors.push(`Action ${index + 1}: command is not in allowed list: ${action.command}`);
442
+ }
443
+ break;
444
+
445
+ case 'change-directory':
446
+ case 'list-directory':
447
+ case 'create-directory':
448
+ if (!action.directory || !action.directory.trim()) {
449
+ errors.push(`Action ${index + 1}: directory is required for ${action.type}`);
450
+ }
451
+ break;
452
+
453
+ case 'get-working-directory':
454
+ // No additional validation needed
455
+ break;
456
+
457
+ default:
458
+ errors.push(`Action ${index + 1}: unknown action type: ${action.type}`);
459
+ }
460
+ }
461
+ }
462
+
463
+ if (params.timeout && (params.timeout < 1000 || params.timeout > this.timeout)) {
464
+ errors.push(`Timeout must be between 1000 and ${this.timeout} milliseconds`);
465
+ }
466
+
467
+ return {
468
+ valid: errors.length === 0,
469
+ errors
470
+ };
471
+ }
472
+
473
+ /**
474
+ * Execute tool with parsed parameters
475
+ * @param {Object} params - Parsed parameters
476
+ * @param {Object} context - Execution context
477
+ * @returns {Promise<Object>} Execution result
478
+ */
479
+ async execute(params, context) {
480
+ const { actions, timeout: customTimeout, async: forceAsync } = params;
481
+ const { agentId, projectDir, directoryAccess } = context;
482
+
483
+ // Get directory access configuration from agent or create default
484
+ const accessConfig = directoryAccess ||
485
+ this.directoryAccessManager.createDirectoryAccess({
486
+ workingDirectory: projectDir || process.cwd(),
487
+ writeEnabledDirectories: [projectDir || process.cwd()],
488
+ restrictToProject: true
489
+ });
490
+
491
+ // IMPORTANT: If the agent has directoryAccess configured, use its workingDirectory
492
+ // This ensures UI-configured project directories are respected
493
+ if (directoryAccess && directoryAccess.workingDirectory) {
494
+ // Agent has explicitly configured working directory from UI - use it
495
+ console.log('Terminal DEBUG: Using agent configured working directory:', directoryAccess.workingDirectory);
496
+ } else {
497
+ // Using fallback to projectDir or process.cwd()
498
+ console.log('Terminal DEBUG: Using fallback working directory:', projectDir || process.cwd());
499
+ }
500
+
501
+ // Get or set current working directory for this agent
502
+ const contextKey = `${agentId}-${projectDir || 'default'}`;
503
+ let currentWorkingDir = this.workingDirectories.get(contextKey) ||
504
+ this.directoryAccessManager.getWorkingDirectory(accessConfig);
505
+
506
+ const results = [];
507
+
508
+ for (const action of actions) {
509
+ try {
510
+ let result;
511
+
512
+ switch (action.type) {
513
+ case 'run-command':
514
+ result = await this.executeCommand(action.command, currentWorkingDir, {
515
+ timeout: customTimeout || this.timeout,
516
+ async: forceAsync || false,
517
+ agentId,
518
+ context: {
519
+ toolsRegistry: context.toolsRegistry,
520
+ aiService: context.aiService,
521
+ apiKey: context.apiKey,
522
+ customApiKeys: context.customApiKeys,
523
+ platformProvided: context.platformProvided
524
+ }
525
+ });
526
+ break;
527
+
528
+ case 'change-directory':
529
+ result = await this.changeDirectory(action.directory, currentWorkingDir, accessConfig);
530
+ currentWorkingDir = result.newDirectory;
531
+ this.workingDirectories.set(contextKey, currentWorkingDir);
532
+ break;
533
+
534
+ case 'list-directory':
535
+ result = await this.listDirectory(action.directory === '.' ? currentWorkingDir : action.directory);
536
+ break;
537
+
538
+ case 'create-directory':
539
+ result = await this.createDirectory(action.directory, currentWorkingDir);
540
+ break;
541
+
542
+ case 'get-working-directory':
543
+ result = {
544
+ success: true,
545
+ action: 'get-working-directory',
546
+ workingDirectory: currentWorkingDir,
547
+ message: `Current working directory: ${currentWorkingDir}`
548
+ };
549
+ break;
550
+
551
+ default:
552
+ throw new Error(`Unknown action type: ${action.type}`);
553
+ }
554
+
555
+ results.push(result);
556
+
557
+ // Add to command history
558
+ this.addToHistory(action, result, agentId);
559
+
560
+ } catch (error) {
561
+ const errorResult = {
562
+ success: false,
563
+ action: action.type,
564
+ error: error.message,
565
+ command: action.command || action.directory,
566
+ workingDirectory: currentWorkingDir
567
+ };
568
+
569
+ results.push(errorResult);
570
+ this.addToHistory(action, errorResult, agentId);
571
+ }
572
+ }
573
+
574
+ // Determine overall success based on individual action results
575
+ const overallSuccess = results.every(result => result.success);
576
+ const failedActions = results.filter(result => !result.success);
577
+
578
+ return {
579
+ success: overallSuccess,
580
+ actions: results,
581
+ workingDirectory: currentWorkingDir,
582
+ executedActions: actions.length,
583
+ failedActions: failedActions.length,
584
+ toolUsed: 'terminal',
585
+ message: overallSuccess
586
+ ? `All ${actions.length} actions completed successfully`
587
+ : `${failedActions.length} of ${actions.length} actions failed`
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Execute a command in the specified directory
593
+ * @private
594
+ */
595
+ async executeCommand(command, workingDir, options = {}) {
596
+ const { timeout = this.timeout, async: isAsync = false, agentId, context } = options;
597
+
598
+ // Translate command for current terminal (now async with AI support)
599
+ const originalCommand = command;
600
+ let translatedCommand;
601
+
602
+ try {
603
+ translatedCommand = await this.translateCommand(command, {
604
+ agentId,
605
+ toolsRegistry: context?.toolsRegistry,
606
+ messageProcessor: context?.messageProcessor,
607
+ aiService: context?.aiService,
608
+ apiKey: context?.apiKey,
609
+ customApiKeys: context?.customApiKeys,
610
+ platformProvided: context?.platformProvided
611
+ });
612
+ } catch (error) {
613
+ this.logger?.warn('Command translation failed, using original command', {
614
+ originalCommand,
615
+ error: error.message
616
+ });
617
+ translatedCommand = command;
618
+ }
619
+
620
+ return new Promise((resolve, reject) => {
621
+ const startTime = Date.now();
622
+
623
+ this.logger?.info(`Executing command: ${translatedCommand}`, {
624
+ originalCommand,
625
+ translatedCommand,
626
+ terminal: this.detectedTerminal,
627
+ workingDirectory: workingDir,
628
+ timeout,
629
+ agentId
630
+ });
631
+
632
+ const childProcess = exec(translatedCommand, {
633
+ cwd: workingDir,
634
+ timeout,
635
+ maxBuffer: 1024 * 1024, // 1MB buffer
636
+ env: { ...process.env }
637
+ }, (error, stdout, stderr) => {
638
+ const executionTime = Date.now() - startTime;
639
+
640
+ if (error) {
641
+ this.logger?.error(`Command failed: ${translatedCommand}`, {
642
+ originalCommand,
643
+ translatedCommand,
644
+ error: error.message,
645
+ workingDirectory: workingDir,
646
+ executionTime
647
+ });
648
+
649
+ resolve({
650
+ success: false,
651
+ action: 'run-command',
652
+ command: originalCommand,
653
+ translatedCommand: translatedCommand !== originalCommand ? translatedCommand : undefined,
654
+ error: error.message,
655
+ stderr: stderr.trim(),
656
+ stdout: stdout.trim(),
657
+ exitCode: error.code,
658
+ executionTime,
659
+ workingDirectory: workingDir
660
+ });
661
+ return;
662
+ }
663
+
664
+ this.logger?.info(`Command completed: ${translatedCommand}`, {
665
+ originalCommand,
666
+ translatedCommand,
667
+ executionTime,
668
+ stdoutLength: stdout.length,
669
+ stderrLength: stderr.length
670
+ });
671
+
672
+ resolve({
673
+ success: true,
674
+ action: 'run-command',
675
+ command: originalCommand,
676
+ translatedCommand: translatedCommand !== originalCommand ? translatedCommand : undefined,
677
+ stdout: stdout.trim(),
678
+ stderr: stderr.trim(),
679
+ exitCode: 0,
680
+ executionTime,
681
+ workingDirectory: workingDir,
682
+ message: `Command executed successfully in ${executionTime}ms`
683
+ });
684
+ });
685
+
686
+ // Handle timeout
687
+ setTimeout(() => {
688
+ if (!childProcess.killed) {
689
+ childProcess.kill('SIGTERM');
690
+ reject(new Error(`Command timed out after ${timeout}ms: ${translatedCommand} (original: ${originalCommand})`));
691
+ }
692
+ }, timeout);
693
+ });
694
+ }
695
+
696
+ /**
697
+ * Execute a command using spawn() for streaming output
698
+ * @param {string} command - Command to execute
699
+ * @param {string} workingDir - Working directory
700
+ * @param {Object} options - Execution options
701
+ * @returns {Promise<Object>} Execution result
702
+ * @private
703
+ */
704
+ async executeCommandWithSpawn(command, workingDir, options = {}) {
705
+ const { timeout = this.timeout, agentId, context } = options;
706
+
707
+ // Translate command for current terminal
708
+ const originalCommand = command;
709
+ let translatedCommand;
710
+
711
+ try {
712
+ translatedCommand = await this.translateCommand(command, {
713
+ agentId,
714
+ toolsRegistry: context?.toolsRegistry,
715
+ messageProcessor: context?.messageProcessor,
716
+ aiService: context?.aiService,
717
+ apiKey: context?.apiKey,
718
+ customApiKeys: context?.customApiKeys,
719
+ platformProvided: context?.platformProvided
720
+ });
721
+ } catch (error) {
722
+ this.logger?.warn('Command translation failed, using original command', {
723
+ originalCommand,
724
+ error: error.message
725
+ });
726
+ translatedCommand = command;
727
+ }
728
+
729
+ return new Promise((resolve, reject) => {
730
+ const startTime = Date.now();
731
+
732
+ this.logger?.info(`Executing command with spawn: ${translatedCommand}`, {
733
+ originalCommand,
734
+ translatedCommand,
735
+ terminal: this.detectedTerminal,
736
+ workingDirectory: workingDir,
737
+ timeout,
738
+ agentId
739
+ });
740
+
741
+ // Parse command into program and args
742
+ // For shell commands, we need to run them through a shell
743
+ let childProcess;
744
+
745
+ if (this.detectedTerminal === 'cmd' || this.detectedTerminal === 'powershell') {
746
+ // Windows: Use cmd /c or powershell -Command
747
+ const shell = this.detectedTerminal === 'powershell' ? 'powershell' : 'cmd';
748
+ const shellArgs = this.detectedTerminal === 'powershell'
749
+ ? ['-Command', translatedCommand]
750
+ : ['/c', translatedCommand];
751
+
752
+ childProcess = spawn(shell, shellArgs, {
753
+ cwd: workingDir,
754
+ env: { ...process.env },
755
+ windowsHide: true
756
+ });
757
+ } else {
758
+ // Unix: Use sh -c
759
+ childProcess = spawn('sh', ['-c', translatedCommand], {
760
+ cwd: workingDir,
761
+ env: { ...process.env }
762
+ });
763
+ }
764
+
765
+ // Buffers for stdout and stderr
766
+ let stdoutData = '';
767
+ let stderrData = '';
768
+ let isTimedOut = false;
769
+ let exitCode = null;
770
+
771
+ // Phase 2: Prompt detection tracking
772
+ let lastOutputTime = Date.now();
773
+ let promptDetectionResult = null;
774
+
775
+ // Set up timeout
776
+ const timeoutId = setTimeout(() => {
777
+ if (!childProcess.killed && exitCode === null) {
778
+ isTimedOut = true;
779
+ this.logger?.warn(`Command timed out, killing process: ${translatedCommand}`, {
780
+ timeout,
781
+ agentId
782
+ });
783
+ childProcess.kill('SIGTERM');
784
+
785
+ // If SIGTERM doesn't work, try SIGKILL after 5s
786
+ setTimeout(() => {
787
+ if (!childProcess.killed) {
788
+ childProcess.kill('SIGKILL');
789
+ }
790
+ }, 5000);
791
+ }
792
+ }, timeout);
793
+
794
+ // Stream stdout
795
+ childProcess.stdout.on('data', (chunk) => {
796
+ const data = chunk.toString();
797
+ stdoutData += data;
798
+ lastOutputTime = Date.now(); // Update last output time
799
+
800
+ this.logger?.debug(`Command output chunk: ${data.substring(0, 100)}`, {
801
+ agentId,
802
+ command: originalCommand.substring(0, 50)
803
+ });
804
+
805
+ // Phase 2: Check for prompts in stdout
806
+ if (!promptDetectionResult) {
807
+ const detection = this.promptDetector.detectPrompt(stdoutData, 'stdout');
808
+ if (detection) {
809
+ promptDetectionResult = detection;
810
+ this.logger?.info('Prompt detected in stdout', {
811
+ type: detection.type,
812
+ description: detection.description,
813
+ matchedText: detection.matchedText,
814
+ agentId,
815
+ command: originalCommand.substring(0, 50)
816
+ });
817
+ }
818
+ }
819
+ });
820
+
821
+ // Stream stderr
822
+ childProcess.stderr.on('data', (chunk) => {
823
+ const data = chunk.toString();
824
+ stderrData += data;
825
+ lastOutputTime = Date.now(); // Update last output time
826
+
827
+ this.logger?.debug(`Command error chunk: ${data.substring(0, 100)}`, {
828
+ agentId,
829
+ command: originalCommand.substring(0, 50)
830
+ });
831
+
832
+ // Phase 2: Check for prompts in stderr
833
+ if (!promptDetectionResult) {
834
+ const detection = this.promptDetector.detectPrompt(stderrData, 'stderr');
835
+ if (detection) {
836
+ promptDetectionResult = detection;
837
+ this.logger?.info('Prompt detected in stderr', {
838
+ type: detection.type,
839
+ description: detection.description,
840
+ matchedText: detection.matchedText,
841
+ agentId,
842
+ command: originalCommand.substring(0, 50)
843
+ });
844
+ }
845
+ }
846
+ });
847
+
848
+ // Handle process exit
849
+ childProcess.on('exit', (code, signal) => {
850
+ clearTimeout(timeoutId);
851
+ exitCode = code;
852
+ const executionTime = Date.now() - startTime;
853
+
854
+ // Phase 2: Calculate time since last output
855
+ const timeSinceLastOutput = Date.now() - lastOutputTime;
856
+
857
+ this.logger?.info(`Command process exited: ${translatedCommand}`, {
858
+ exitCode: code,
859
+ signal,
860
+ executionTime,
861
+ timedOut: isTimedOut,
862
+ stdoutLength: stdoutData.length,
863
+ stderrLength: stderrData.length,
864
+ promptDetected: !!promptDetectionResult,
865
+ timeSinceLastOutput
866
+ });
867
+
868
+ // Build common result object with Phase 2 additions
869
+ const baseResult = {
870
+ action: 'run-command',
871
+ command: originalCommand,
872
+ translatedCommand: translatedCommand !== originalCommand ? translatedCommand : undefined,
873
+ stdout: stdoutData.trim(),
874
+ stderr: stderrData.trim(),
875
+ exitCode: code,
876
+ executionTime,
877
+ workingDirectory: workingDir,
878
+ // Phase 2: Prompt detection fields
879
+ promptDetected: !!promptDetectionResult,
880
+ promptInfo: promptDetectionResult || undefined,
881
+ lastOutputTime: lastOutputTime,
882
+ timeSinceLastOutput: timeSinceLastOutput
883
+ };
884
+
885
+ // If timed out, reject
886
+ if (isTimedOut) {
887
+ resolve({
888
+ ...baseResult,
889
+ success: false,
890
+ error: `Command timed out after ${timeout}ms`,
891
+ exitCode: code || -1,
892
+ timedOut: true
893
+ });
894
+ return;
895
+ }
896
+
897
+ // If exit code is not 0, consider it a failure
898
+ if (code !== 0) {
899
+ this.logger?.error(`Command failed with exit code ${code}: ${translatedCommand}`, {
900
+ originalCommand,
901
+ translatedCommand,
902
+ exitCode: code,
903
+ stderr: stderrData.substring(0, 200),
904
+ executionTime,
905
+ promptDetected: !!promptDetectionResult
906
+ });
907
+
908
+ resolve({
909
+ ...baseResult,
910
+ success: false,
911
+ error: `Command exited with code ${code}`
912
+ });
913
+ return;
914
+ }
915
+
916
+ // Success
917
+ this.logger?.info(`Command completed successfully: ${translatedCommand}`, {
918
+ originalCommand,
919
+ executionTime,
920
+ stdoutLength: stdoutData.length,
921
+ stderrLength: stderrData.length,
922
+ promptDetected: !!promptDetectionResult
923
+ });
924
+
925
+ resolve({
926
+ ...baseResult,
927
+ success: true,
928
+ exitCode: 0,
929
+ message: `Command executed successfully in ${executionTime}ms`
930
+ });
931
+ });
932
+
933
+ // Handle spawn errors
934
+ childProcess.on('error', (error) => {
935
+ clearTimeout(timeoutId);
936
+ const executionTime = Date.now() - startTime;
937
+ const timeSinceLastOutput = Date.now() - lastOutputTime;
938
+
939
+ this.logger?.error(`Command spawn error: ${translatedCommand}`, {
940
+ originalCommand,
941
+ error: error.message,
942
+ executionTime,
943
+ promptDetected: !!promptDetectionResult
944
+ });
945
+
946
+ resolve({
947
+ success: false,
948
+ action: 'run-command',
949
+ command: originalCommand,
950
+ translatedCommand: translatedCommand !== originalCommand ? translatedCommand : undefined,
951
+ error: error.message,
952
+ stderr: stderrData.trim(),
953
+ stdout: stdoutData.trim(),
954
+ exitCode: -1,
955
+ executionTime,
956
+ workingDirectory: workingDir,
957
+ // Phase 2: Prompt detection fields
958
+ promptDetected: !!promptDetectionResult,
959
+ promptInfo: promptDetectionResult || undefined,
960
+ lastOutputTime: lastOutputTime,
961
+ timeSinceLastOutput: timeSinceLastOutput
962
+ });
963
+ });
964
+ });
965
+ }
966
+
967
+ /**
968
+ * Change current working directory
969
+ * @private
970
+ */
971
+ async changeDirectory(targetDir, currentDir, accessConfig) {
972
+ try {
973
+ let newDirectory;
974
+
975
+ if (path.isAbsolute(targetDir)) {
976
+ newDirectory = targetDir;
977
+ } else {
978
+ newDirectory = path.resolve(currentDir, targetDir);
979
+ }
980
+
981
+ // Verify directory exists
982
+ const stats = await fs.stat(newDirectory);
983
+ if (!stats.isDirectory()) {
984
+ throw new Error(`Not a directory: ${newDirectory}`);
985
+ }
986
+
987
+ // Security check: validate directory access using DirectoryAccessManager
988
+ const accessResult = this.directoryAccessManager.validateReadAccess(newDirectory, accessConfig);
989
+ if (!accessResult.allowed) {
990
+ this.logger?.warn(`Directory change blocked: ${accessResult.reason}`, {
991
+ targetDirectory: newDirectory,
992
+ reason: accessResult.reason,
993
+ category: accessResult.category
994
+ });
995
+ throw new Error(`Access denied: ${accessResult.reason}`);
996
+ }
997
+
998
+ return {
999
+ success: true,
1000
+ action: 'change-directory',
1001
+ previousDirectory: currentDir,
1002
+ newDirectory,
1003
+ message: `Changed directory to ${newDirectory}`
1004
+ };
1005
+
1006
+ } catch (error) {
1007
+ throw new Error(`Failed to change directory to ${targetDir}: ${error.message}`);
1008
+ }
1009
+ }
1010
+
1011
+ /**
1012
+ * List directory contents
1013
+ * @private
1014
+ */
1015
+ async listDirectory(dirPath) {
1016
+ try {
1017
+ const files = await fs.readdir(dirPath, { withFileTypes: true });
1018
+
1019
+ const contents = files.map(file => ({
1020
+ name: file.name,
1021
+ type: file.isDirectory() ? 'directory' : 'file',
1022
+ isSymlink: file.isSymbolicLink()
1023
+ }));
1024
+
1025
+ return {
1026
+ success: true,
1027
+ action: 'list-directory',
1028
+ directory: dirPath,
1029
+ contents,
1030
+ totalItems: contents.length,
1031
+ directories: contents.filter(item => item.type === 'directory').length,
1032
+ files: contents.filter(item => item.type === 'file').length,
1033
+ message: `Listed ${contents.length} items in ${dirPath}`
1034
+ };
1035
+
1036
+ } catch (error) {
1037
+ throw new Error(`Failed to list directory ${dirPath}: ${error.message}`);
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Create directory
1043
+ * @private
1044
+ */
1045
+ async createDirectory(dirPath, currentDir) {
1046
+ try {
1047
+ let fullPath;
1048
+
1049
+ if (path.isAbsolute(dirPath)) {
1050
+ fullPath = dirPath;
1051
+ } else {
1052
+ fullPath = path.resolve(currentDir, dirPath);
1053
+ }
1054
+
1055
+ await fs.mkdir(fullPath, { recursive: true });
1056
+
1057
+ return {
1058
+ success: true,
1059
+ action: 'create-directory',
1060
+ directory: fullPath,
1061
+ relativePath: path.relative(currentDir, fullPath),
1062
+ message: `Created directory: ${fullPath}`
1063
+ };
1064
+
1065
+ } catch (error) {
1066
+ throw new Error(`Failed to create directory ${dirPath}: ${error.message}`);
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Check if command is blocked for security
1072
+ * @private
1073
+ */
1074
+ isBlockedCommand(command) {
1075
+ const cmdLower = command.toLowerCase().trim();
1076
+
1077
+ return this.blockedCommands.some(blocked => {
1078
+ const blockedLower = blocked.toLowerCase();
1079
+ return cmdLower === blockedLower || cmdLower.startsWith(blockedLower + ' ');
1080
+ });
1081
+ }
1082
+
1083
+ /**
1084
+ * Check if command is in allowed list
1085
+ * @private
1086
+ */
1087
+ isAllowedCommand(command) {
1088
+ if (!this.allowedCommands) return true;
1089
+
1090
+ const cmdLower = command.toLowerCase().trim();
1091
+ const cmdBase = cmdLower.split(' ')[0];
1092
+
1093
+ return this.allowedCommands.some(allowed =>
1094
+ allowed.toLowerCase() === cmdBase ||
1095
+ cmdLower.startsWith(allowed.toLowerCase() + ' ')
1096
+ );
1097
+ }
1098
+
1099
+ /**
1100
+ * Add command to history
1101
+ * @private
1102
+ */
1103
+ addToHistory(action, result, agentId) {
1104
+ const historyEntry = {
1105
+ timestamp: new Date().toISOString(),
1106
+ agentId,
1107
+ action: action.type,
1108
+ command: action.command || action.directory,
1109
+ success: result.success,
1110
+ executionTime: result.executionTime || 0,
1111
+ workingDirectory: result.workingDirectory
1112
+ };
1113
+
1114
+ this.commandHistory.push(historyEntry);
1115
+
1116
+ // Keep only last 100 entries
1117
+ if (this.commandHistory.length > 100) {
1118
+ this.commandHistory = this.commandHistory.slice(-100);
1119
+ }
1120
+ }
1121
+
1122
+ /**
1123
+ * Get supported actions for this tool
1124
+ * @returns {Array<string>} Array of supported action names
1125
+ */
1126
+ getSupportedActions() {
1127
+ return ['run-command', 'change-directory', 'list-directory', 'create-directory', 'get-working-directory'];
1128
+ }
1129
+
1130
+ /**
1131
+ * Get parameter schema for validation
1132
+ * @returns {Object} Parameter schema
1133
+ */
1134
+ getParameterSchema() {
1135
+ return {
1136
+ type: 'object',
1137
+ properties: {
1138
+ actions: {
1139
+ type: 'array',
1140
+ minItems: 1,
1141
+ items: {
1142
+ type: 'object',
1143
+ properties: {
1144
+ type: {
1145
+ type: 'string',
1146
+ enum: this.getSupportedActions()
1147
+ },
1148
+ command: { type: 'string' },
1149
+ directory: { type: 'string' }
1150
+ },
1151
+ required: ['type']
1152
+ }
1153
+ },
1154
+ timeout: {
1155
+ type: 'integer',
1156
+ minimum: 1000,
1157
+ maximum: this.timeout
1158
+ },
1159
+ async: {
1160
+ type: 'boolean'
1161
+ }
1162
+ },
1163
+ required: ['actions']
1164
+ };
1165
+ }
1166
+
1167
+ /**
1168
+ * Get command history for debugging
1169
+ * @returns {Array} Command history
1170
+ */
1171
+ getCommandHistory(agentId = null) {
1172
+ if (agentId) {
1173
+ return this.commandHistory.filter(entry => entry.agentId === agentId);
1174
+ }
1175
+ return [...this.commandHistory];
1176
+ }
1177
+
1178
+ /**
1179
+ * Clear working directory context for agent
1180
+ * @param {string} agentId - Agent identifier
1181
+ */
1182
+ clearWorkingDirectory(agentId) {
1183
+ for (const [key] of this.workingDirectories) {
1184
+ if (key.startsWith(`${agentId}-`)) {
1185
+ this.workingDirectories.delete(key);
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ /**
1191
+ * Get current working directory for agent
1192
+ * @param {string} agentId - Agent identifier
1193
+ * @param {string} projectDir - Project directory
1194
+ * @returns {string} Current working directory
1195
+ */
1196
+ getCurrentWorkingDirectory(agentId, projectDir = null) {
1197
+ const contextKey = `${agentId}-${projectDir || 'default'}`;
1198
+ return this.workingDirectories.get(contextKey) || projectDir || process.cwd();
1199
+ }
1200
+
1201
+ /**
1202
+ * Initialize terminal detection
1203
+ * @private
1204
+ */
1205
+ initializeTerminalDetection() {
1206
+ // Detect platform
1207
+ this.platformType = process.platform;
1208
+
1209
+ // Detect terminal type based on environment
1210
+ if (process.platform === 'win32') {
1211
+ // Windows detection
1212
+ if (process.env.PSModulePath) {
1213
+ this.detectedTerminal = 'powershell';
1214
+ } else if (process.env.SHELL && process.env.SHELL.includes('bash')) {
1215
+ this.detectedTerminal = 'bash'; // Git Bash or WSL
1216
+ } else {
1217
+ this.detectedTerminal = 'cmd'; // Windows Command Prompt
1218
+ }
1219
+ } else if (process.platform === 'darwin') {
1220
+ this.detectedTerminal = 'bash'; // macOS
1221
+ } else {
1222
+ this.detectedTerminal = 'bash'; // Linux/Unix
1223
+ }
1224
+
1225
+ this.logger?.info('Terminal detected', {
1226
+ platform: this.platformType,
1227
+ terminal: this.detectedTerminal,
1228
+ shell: process.env.SHELL
1229
+ });
1230
+ }
1231
+
1232
+ /**
1233
+ * Translate command for current terminal using AI if needed
1234
+ * @param {string} command - Original command
1235
+ * @param {Object} context - Execution context with aiService access
1236
+ * @returns {Promise<string>} Translated command
1237
+ * @private
1238
+ */
1239
+ async translateCommand(command, context = {}) {
1240
+ const trimmedCommand = command.trim();
1241
+
1242
+ // Perform comprehensive command analysis
1243
+ const analysis = this.analyzeCommandCompatibility(trimmedCommand);
1244
+
1245
+ this.logger?.info('Command compatibility analysis', {
1246
+ command: trimmedCommand,
1247
+ detectedTerminal: analysis.detectedTerminal,
1248
+ commandType: analysis.commandType,
1249
+ commandCategory: analysis.commandCategory,
1250
+ compatible: analysis.compatible,
1251
+ issues: analysis.specificIssues,
1252
+ suggestedAction: analysis.suggestedAction,
1253
+ confidence: analysis.confidence
1254
+ });
1255
+
1256
+ if (analysis.compatible) {
1257
+ return command;
1258
+ }
1259
+
1260
+ // Try simple translations first (fast path)
1261
+ let simpleTranslation = null;
1262
+ if (this.detectedTerminal === 'cmd') {
1263
+ simpleTranslation = this.translateToWindowsCmd(trimmedCommand);
1264
+ } else if (this.detectedTerminal === 'powershell') {
1265
+ simpleTranslation = this.translateToPowerShell(trimmedCommand);
1266
+ } else if (this.detectedTerminal === 'bash') {
1267
+ simpleTranslation = this.translateToBash(trimmedCommand);
1268
+ }
1269
+
1270
+ // If simple translation looks sufficient and command is simple, use it
1271
+ if (simpleTranslation && this.isSimpleCommand(trimmedCommand)) {
1272
+ this.logger?.info('Using simple translation', {
1273
+ original: command,
1274
+ translated: simpleTranslation,
1275
+ method: 'simple'
1276
+ });
1277
+ return simpleTranslation;
1278
+ }
1279
+
1280
+ // For complex commands, use AI translation
1281
+ if (context.aiService || (context.toolsRegistry && context.agentId)) {
1282
+ this.logger?.info('Attempting AI translation for complex command', {
1283
+ command: command.substring(0, 100) + '...',
1284
+ hasAiService: !!context.aiService,
1285
+ hasToolsRegistry: !!context.toolsRegistry,
1286
+ agentId: context.agentId
1287
+ });
1288
+
1289
+ try {
1290
+ const aiTranslation = await this.translateCommandWithAI(command, context);
1291
+ if (aiTranslation && aiTranslation.trim() !== command.trim()) {
1292
+ this.logger?.info('Using AI translation', {
1293
+ original: command,
1294
+ translated: aiTranslation,
1295
+ method: 'ai'
1296
+ });
1297
+ return aiTranslation;
1298
+ } else {
1299
+ this.logger?.warn('AI translation returned same command', {
1300
+ original: command,
1301
+ translated: aiTranslation
1302
+ });
1303
+ }
1304
+ } catch (error) {
1305
+ this.logger?.warn('AI translation failed, falling back to simple translation', {
1306
+ original: command,
1307
+ error: error.message,
1308
+ stack: error.stack
1309
+ });
1310
+ }
1311
+ } else {
1312
+ this.logger?.warn('AI translation not available - missing context', {
1313
+ hasAiService: !!context.aiService,
1314
+ hasToolsRegistry: !!context.toolsRegistry,
1315
+ hasAgentId: !!context.agentId,
1316
+ contextKeys: Object.keys(context)
1317
+ });
1318
+ }
1319
+
1320
+ // Fallback to simple translation or original command
1321
+ return simpleTranslation || command;
1322
+ }
1323
+
1324
+ /**
1325
+ * Check if command is simple enough for regex translation
1326
+ * @param {string} command - Command to check
1327
+ * @returns {boolean} Whether command is simple
1328
+ * @private
1329
+ */
1330
+ isSimpleCommand(command) {
1331
+ // Check for complex command patterns using simple string methods
1332
+ const cmd = command.toLowerCase();
1333
+
1334
+ // Multi-line or escape sequences
1335
+ if (command.includes('\\n') || command.includes('\\r') || command.includes('\\t')) return false;
1336
+ if (command.includes('\n') || command.includes('\r')) return false;
1337
+
1338
+ // Code content
1339
+ if (command.includes('#include') || command.includes('printf') || command.includes('main()')) return false;
1340
+ if (command.includes('def ') || command.includes('function ') || command.includes('class ')) return false;
1341
+
1342
+ // Single quotes (problematic on Windows CMD)
1343
+ if (command.includes("echo '") && !command.includes('echo "')) return false;
1344
+
1345
+ // Long complex strings (likely contain complex content)
1346
+ const singleQuoteMatch = command.match(/'([^']*)'/);
1347
+ if (singleQuoteMatch && singleQuoteMatch[1].length > 50) return false;
1348
+
1349
+ // Unix-style paths in redirections on Windows
1350
+ if (this.detectedTerminal === 'cmd' && command.includes('>') && command.includes('/') && !command.includes('\\')) {
1351
+ return false;
1352
+ }
1353
+
1354
+ // Multiple commands
1355
+ if (command.includes(' && ') || command.includes(' || ') || command.includes('; ')) return false;
1356
+
1357
+ // Command substitution or complex shell features
1358
+ if (command.includes('$(') || command.includes('`') || command.includes('{') || command.includes('[')) return false;
1359
+
1360
+ // Loops and conditionals
1361
+ if (cmd.includes('for ') || cmd.includes('while ') || cmd.includes('if ')) return false;
1362
+ if (cmd.includes('export ') || cmd.includes('source ')) return false;
1363
+
1364
+ // Pipes and redirections
1365
+ if (command.includes(' | ') || command.includes(' > &')) return false;
1366
+
1367
+ return true; // Command is simple
1368
+ }
1369
+
1370
+ /**
1371
+ * Translate command using AI service
1372
+ * @param {string} command - Original command
1373
+ * @param {Object} context - Execution context
1374
+ * @returns {Promise<string>} AI-translated command
1375
+ * @private
1376
+ */
1377
+ async translateCommandWithAI(command, context) {
1378
+ // Get AI service - either directly provided or through tools registry
1379
+ let aiService = context.aiService;
1380
+ if (!aiService && context.toolsRegistry && context.agentId) {
1381
+ // Try to get AI service through the system (this would need to be passed through context)
1382
+ const messageProcessor = context.messageProcessor; // Would need to be passed in context
1383
+ if (messageProcessor && messageProcessor.aiService) {
1384
+ aiService = messageProcessor.aiService;
1385
+ }
1386
+ }
1387
+
1388
+ if (!aiService) {
1389
+ throw new Error('AI service not available for command translation');
1390
+ }
1391
+
1392
+ const translationPrompt = this.buildTranslationPrompt(command);
1393
+
1394
+ // Use a lightweight model for translation
1395
+ const model = 'gpt-4-mini'; // Fast and cost-effective for simple translations
1396
+
1397
+ try {
1398
+ const response = await aiService.sendMessage(model, translationPrompt, {
1399
+ agentId: context.agentId,
1400
+ temperature: 0.1, // Low temperature for consistent translations
1401
+ maxTokens: 200, // Short response expected
1402
+ apiKey: context.apiKey,
1403
+ customApiKeys: context.customApiKeys,
1404
+ platformProvided: context.platformProvided
1405
+ });
1406
+
1407
+ // Extract the translated command from the response
1408
+ const translatedCommand = this.extractTranslatedCommand(response.content);
1409
+
1410
+ this.logger?.debug('AI command translation completed', {
1411
+ original: command,
1412
+ translated: translatedCommand,
1413
+ model: model,
1414
+ terminal: this.detectedTerminal
1415
+ });
1416
+
1417
+ return translatedCommand;
1418
+
1419
+ } catch (error) {
1420
+ this.logger?.error('AI command translation failed', {
1421
+ command,
1422
+ terminal: this.detectedTerminal,
1423
+ error: error.message
1424
+ });
1425
+ throw error;
1426
+ }
1427
+ }
1428
+
1429
+ /**
1430
+ * Build translation prompt for AI service
1431
+ * @param {string} command - Command to translate
1432
+ * @returns {string} Translation prompt
1433
+ * @private
1434
+ */
1435
+ buildTranslationPrompt(command) {
1436
+ const terminalInfo = {
1437
+ 'cmd': 'Windows Command Prompt (cmd.exe)',
1438
+ 'powershell': 'Windows PowerShell',
1439
+ 'bash': 'Bash shell (Linux/Unix/macOS)'
1440
+ };
1441
+
1442
+ const currentTerminal = terminalInfo[this.detectedTerminal] || this.detectedTerminal;
1443
+
1444
+ // Get detailed analysis for better translation context
1445
+ const analysis = this.analyzeCommandCompatibility(command);
1446
+
1447
+ let prompt = `Translate this command to work correctly in ${currentTerminal}:
1448
+
1449
+ ORIGINAL COMMAND:
1450
+ \`\`\`
1451
+ ${command}
1452
+ \`\`\`
1453
+
1454
+ COMMAND ANALYSIS:
1455
+ - Command Type: ${analysis.commandType}
1456
+ - Category: ${analysis.commandCategory}
1457
+ - Target Terminal: ${currentTerminal}
1458
+ - Compatibility Issues: ${analysis.specificIssues.join(', ') || 'None detected'}`;
1459
+
1460
+ // Add alternative suggestions if available
1461
+ if (analysis.commandType === 'unix' && analysis.alternatives && analysis.alternatives[this.detectedTerminal]) {
1462
+ prompt += `\n- Suggested Alternative: ${analysis.alternatives[this.detectedTerminal]}`;
1463
+ }
1464
+
1465
+ prompt += `
1466
+
1467
+ REQUIREMENTS:
1468
+ 1. Make the command work correctly in ${currentTerminal}
1469
+ 2. Preserve the original intent and functionality
1470
+ 3. Handle file paths, quoting, and syntax correctly
1471
+ 4. If creating files, ensure proper encoding and line endings
1472
+ 5. Address the specific compatibility issues identified above
1473
+ 6. Return ONLY the translated command, nothing else
1474
+
1475
+ TRANSLATED COMMAND:`;
1476
+
1477
+ return prompt;
1478
+ }
1479
+
1480
+ /**
1481
+ * Extract translated command from AI response
1482
+ * @param {string} response - AI response content
1483
+ * @returns {string} Extracted command
1484
+ * @private
1485
+ */
1486
+ extractTranslatedCommand(response) {
1487
+ // Remove markdown code blocks if present
1488
+ let extracted = response.replace(/```[a-z]*\n?(.*?)\n?```/s, '$1');
1489
+
1490
+ // Remove common prefixes
1491
+ extracted = extracted.replace(/^(TRANSLATED COMMAND:|Command:|Result:)\s*/i, '');
1492
+
1493
+ // Take first line if multiple lines (unless it's intentionally multiline)
1494
+ const lines = extracted.trim().split('\n');
1495
+ if (lines.length > 1 && !extracted.includes('&&') && !extracted.includes('||')) {
1496
+ extracted = lines[0].trim();
1497
+ }
1498
+
1499
+ return extracted.trim();
1500
+ }
1501
+
1502
+ /**
1503
+ * Analyze command compatibility with current terminal
1504
+ * @param {string} command - Command to check
1505
+ * @returns {Object} Compatibility analysis result
1506
+ * @private
1507
+ */
1508
+ analyzeCommandCompatibility(command) {
1509
+ const analysis = this.classifyCommand(command);
1510
+ const isCompatible = this.isCommandCompatibleWithTerminal(analysis, this.detectedTerminal);
1511
+
1512
+ return {
1513
+ compatible: isCompatible,
1514
+ detectedTerminal: this.detectedTerminal,
1515
+ commandType: analysis.type,
1516
+ commandCategory: analysis.category,
1517
+ specificIssues: analysis.issues,
1518
+ suggestedAction: isCompatible ? 'execute' : 'translate',
1519
+ confidence: analysis.confidence
1520
+ };
1521
+ }
1522
+
1523
+ /**
1524
+ * Legacy wrapper for backward compatibility
1525
+ * @param {string} command - Command to check
1526
+ * @returns {boolean} Whether command is compatible
1527
+ * @private
1528
+ */
1529
+ isCommandCompatible(command) {
1530
+ return this.analyzeCommandCompatibility(command).compatible;
1531
+ }
1532
+
1533
+ /**
1534
+ * Classify command type and detect potential issues
1535
+ * @param {string} command - Command to analyze
1536
+ * @returns {Object} Command classification
1537
+ * @private
1538
+ */
1539
+ classifyCommand(command) {
1540
+ const cmd = command.toLowerCase().trim();
1541
+ const firstWord = cmd.split(' ')[0];
1542
+ const issues = [];
1543
+ let confidence = 0.9;
1544
+
1545
+ // Unix/Linux commands
1546
+ const unixCommands = {
1547
+ // File operations
1548
+ 'ls': { category: 'file-listing', alternatives: { cmd: 'dir', powershell: 'Get-ChildItem' }},
1549
+ 'cat': { category: 'file-viewing', alternatives: { cmd: 'type', powershell: 'Get-Content' }},
1550
+ 'grep': { category: 'text-search', alternatives: { cmd: 'findstr', powershell: 'Select-String' }},
1551
+ 'find': { category: 'file-search', alternatives: { cmd: 'dir /s', powershell: 'Get-ChildItem -Recurse' }},
1552
+ 'head': { category: 'file-viewing', alternatives: { cmd: 'more', powershell: 'Get-Content -Head' }},
1553
+ 'tail': { category: 'file-viewing', alternatives: { cmd: 'more +n', powershell: 'Get-Content -Tail' }},
1554
+ 'wc': { category: 'text-analysis', alternatives: { cmd: 'find /c', powershell: 'Measure-Object' }},
1555
+
1556
+ // File manipulation
1557
+ 'cp': { category: 'file-copy', alternatives: { cmd: 'copy', powershell: 'Copy-Item' }},
1558
+ 'mv': { category: 'file-move', alternatives: { cmd: 'move', powershell: 'Move-Item' }},
1559
+ 'rm': { category: 'file-delete', alternatives: { cmd: 'del', powershell: 'Remove-Item' }},
1560
+ 'mkdir': { category: 'directory-create', alternatives: { cmd: 'mkdir', powershell: 'New-Item -Type Directory' }},
1561
+ 'rmdir': { category: 'directory-delete', alternatives: { cmd: 'rmdir', powershell: 'Remove-Item' }},
1562
+ 'touch': { category: 'file-create', alternatives: { cmd: 'type nul >', powershell: 'New-Item -Type File' }},
1563
+
1564
+ // Permissions and ownership
1565
+ 'chmod': { category: 'permissions', alternatives: { cmd: 'icacls', powershell: 'Set-Acl' }},
1566
+ 'chown': { category: 'ownership', alternatives: { cmd: 'takeown', powershell: 'Set-Acl' }},
1567
+
1568
+ // Process management
1569
+ 'ps': { category: 'process-list', alternatives: { cmd: 'tasklist', powershell: 'Get-Process' }},
1570
+ 'kill': { category: 'process-kill', alternatives: { cmd: 'taskkill', powershell: 'Stop-Process' }},
1571
+ 'killall': { category: 'process-kill', alternatives: { cmd: 'taskkill /f /im', powershell: 'Get-Process | Stop-Process' }},
1572
+ 'top': { category: 'process-monitor', alternatives: { cmd: 'tasklist', powershell: 'Get-Process | Sort-Object CPU' }},
1573
+
1574
+ // Network
1575
+ 'wget': { category: 'download', alternatives: { cmd: 'curl', powershell: 'Invoke-WebRequest' }},
1576
+ 'curl': { category: 'http-client', alternatives: { cmd: 'curl', powershell: 'Invoke-RestMethod' }},
1577
+ 'ping': { category: 'network-test', alternatives: { cmd: 'ping', powershell: 'Test-NetConnection' }},
1578
+
1579
+ // Text processing
1580
+ 'awk': { category: 'text-processing', alternatives: { cmd: 'for /f', powershell: 'ForEach-Object' }},
1581
+ 'sed': { category: 'text-edit', alternatives: { cmd: 'powershell -c', powershell: 'native' }},
1582
+ 'sort': { category: 'text-sort', alternatives: { cmd: 'sort', powershell: 'Sort-Object' }},
1583
+ 'uniq': { category: 'text-dedupe', alternatives: { cmd: 'sort /unique', powershell: 'Sort-Object -Unique' }},
1584
+
1585
+ // Environment
1586
+ 'pwd': { category: 'directory-current', alternatives: { cmd: 'cd', powershell: 'Get-Location' }},
1587
+ 'whoami': { category: 'user-info', alternatives: { cmd: 'echo %USERNAME%', powershell: 'whoami' }},
1588
+ 'env': { category: 'environment', alternatives: { cmd: 'set', powershell: 'Get-ChildItem Env:' }},
1589
+ 'export': { category: 'environment', alternatives: { cmd: 'set', powershell: '$env:' }},
1590
+ 'source': { category: 'script-execute', alternatives: { cmd: 'call', powershell: '. ' }},
1591
+
1592
+ // Archives
1593
+ 'tar': { category: 'archive', alternatives: { cmd: '7z', powershell: 'Compress-Archive' }},
1594
+ 'zip': { category: 'archive', alternatives: { cmd: 'powershell Compress-Archive', powershell: 'Compress-Archive' }},
1595
+ 'unzip': { category: 'archive', alternatives: { cmd: 'powershell Expand-Archive', powershell: 'Expand-Archive' }},
1596
+
1597
+ // System info
1598
+ 'df': { category: 'disk-info', alternatives: { cmd: 'fsutil volume diskfree', powershell: 'Get-WmiObject -Class Win32_LogicalDisk' }},
1599
+ 'du': { category: 'disk-usage', alternatives: { cmd: 'dir /s', powershell: 'Get-ChildItem -Recurse | Measure-Object' }},
1600
+ 'free': { category: 'memory-info', alternatives: { cmd: 'systeminfo', powershell: 'Get-WmiObject -Class Win32_PhysicalMemory' }},
1601
+ 'uname': { category: 'system-info', alternatives: { cmd: 'systeminfo', powershell: 'Get-ComputerInfo' }}
1602
+ };
1603
+
1604
+ // Windows CMD commands
1605
+ const cmdCommands = {
1606
+ 'dir': { category: 'file-listing', alternatives: { unix: 'ls', powershell: 'Get-ChildItem' }},
1607
+ 'type': { category: 'file-viewing', alternatives: { unix: 'cat', powershell: 'Get-Content' }},
1608
+ 'copy': { category: 'file-copy', alternatives: { unix: 'cp', powershell: 'Copy-Item' }},
1609
+ 'move': { category: 'file-move', alternatives: { unix: 'mv', powershell: 'Move-Item' }},
1610
+ 'del': { category: 'file-delete', alternatives: { unix: 'rm', powershell: 'Remove-Item' }},
1611
+ 'tasklist': { category: 'process-list', alternatives: { unix: 'ps', powershell: 'Get-Process' }},
1612
+ 'taskkill': { category: 'process-kill', alternatives: { unix: 'kill', powershell: 'Stop-Process' }},
1613
+ 'findstr': { category: 'text-search', alternatives: { unix: 'grep', powershell: 'Select-String' }}
1614
+ };
1615
+
1616
+ // PowerShell commands
1617
+ const powershellCommands = {
1618
+ 'get-childitem': { category: 'file-listing', alternatives: { unix: 'ls', cmd: 'dir' }},
1619
+ 'get-content': { category: 'file-viewing', alternatives: { unix: 'cat', cmd: 'type' }},
1620
+ 'copy-item': { category: 'file-copy', alternatives: { unix: 'cp', cmd: 'copy' }},
1621
+ 'move-item': { category: 'file-move', alternatives: { unix: 'mv', cmd: 'move' }},
1622
+ 'remove-item': { category: 'file-delete', alternatives: { unix: 'rm', cmd: 'del' }},
1623
+ 'get-process': { category: 'process-list', alternatives: { unix: 'ps', cmd: 'tasklist' }},
1624
+ 'stop-process': { category: 'process-kill', alternatives: { unix: 'kill', cmd: 'taskkill' }}
1625
+ };
1626
+
1627
+ // Determine command type
1628
+ let commandType = 'unknown';
1629
+ let category = 'unknown';
1630
+ let alternatives = {};
1631
+
1632
+ if (unixCommands[firstWord]) {
1633
+ commandType = 'unix';
1634
+ category = unixCommands[firstWord].category;
1635
+ alternatives = unixCommands[firstWord].alternatives;
1636
+ } else if (cmdCommands[firstWord]) {
1637
+ commandType = 'windows-cmd';
1638
+ category = cmdCommands[firstWord].category;
1639
+ alternatives = cmdCommands[firstWord].alternatives;
1640
+ } else if (powershellCommands[firstWord]) {
1641
+ commandType = 'powershell';
1642
+ category = powershellCommands[firstWord].category;
1643
+ alternatives = powershellCommands[firstWord].alternatives;
1644
+ } else {
1645
+ // Check for built-in commands that work everywhere
1646
+ const universalCommands = ['echo', 'cd', 'exit', 'help'];
1647
+ if (universalCommands.includes(firstWord)) {
1648
+ commandType = 'universal';
1649
+ category = 'builtin';
1650
+ } else {
1651
+ commandType = 'unknown';
1652
+ confidence = 0.3;
1653
+ }
1654
+ }
1655
+
1656
+ // Check for syntax issues
1657
+ if (command.includes("'") && !command.includes('"')) {
1658
+ issues.push('single-quotes-problematic');
1659
+ }
1660
+ if (command.includes('/') && !command.includes('\\') && !command.includes('http') && command.includes('>')) {
1661
+ issues.push('unix-style-paths');
1662
+ }
1663
+ if (command.includes('\\n') || command.includes('\\t')) {
1664
+ issues.push('escape-sequences');
1665
+ }
1666
+ if (command.includes(' && ') || command.includes(' || ') || command.includes(';')) {
1667
+ issues.push('command-chaining');
1668
+ }
1669
+
1670
+ return {
1671
+ type: commandType,
1672
+ category: category,
1673
+ alternatives: alternatives,
1674
+ issues: issues,
1675
+ confidence: confidence,
1676
+ firstWord: firstWord,
1677
+ fullCommand: command
1678
+ };
1679
+ }
1680
+
1681
+ /**
1682
+ * Check if classified command is compatible with terminal
1683
+ * @param {Object} analysis - Command analysis
1684
+ * @param {string} terminal - Target terminal type
1685
+ * @returns {boolean} Whether compatible
1686
+ * @private
1687
+ */
1688
+ isCommandCompatibleWithTerminal(analysis, terminal) {
1689
+ switch (terminal) {
1690
+ case 'cmd':
1691
+ if (analysis.type === 'unix') return false;
1692
+ if (analysis.issues.includes('single-quotes-problematic')) return false;
1693
+ if (analysis.issues.includes('unix-style-paths')) return false;
1694
+ return analysis.type === 'windows-cmd' || analysis.type === 'universal';
1695
+
1696
+ case 'powershell':
1697
+ if (analysis.type === 'unix' && !analysis.alternatives.powershell) return false;
1698
+ return true; // PowerShell is quite compatible
1699
+
1700
+ case 'bash':
1701
+ if (analysis.type === 'windows-cmd') return false;
1702
+ return analysis.type === 'unix' || analysis.type === 'universal';
1703
+
1704
+ default:
1705
+ return false;
1706
+ }
1707
+ }
1708
+
1709
+ /**
1710
+ * Translate command to Windows CMD syntax
1711
+ * @param {string} command - Original command
1712
+ * @returns {string} Windows CMD equivalent
1713
+ * @private
1714
+ */
1715
+ translateToWindowsCmd(command) {
1716
+ let translated = command;
1717
+
1718
+ // Common Unix to Windows translations
1719
+ const translations = new Map([
1720
+ // Directory listing
1721
+ [/^ls\s*$/, 'dir'],
1722
+ [/^ls\s+-la?$/, 'dir'],
1723
+ [/^ls\s+-l$/, 'dir'],
1724
+ [/^ls\s+-a$/, 'dir /a'],
1725
+
1726
+ // File operations
1727
+ [/^cat\s+(.+)$/, 'type $1'],
1728
+ [/^cp\s+(.+)\s+(.+)$/, 'copy $1 $2'],
1729
+ [/^mv\s+(.+)\s+(.+)$/, 'move $1 $2'],
1730
+ [/^rm\s+(.+)$/, 'del $1'],
1731
+ [/^mkdir\s+(.+)$/, 'mkdir $1'],
1732
+ [/^rmdir\s+(.+)$/, 'rmdir $1'],
1733
+
1734
+ // Process management
1735
+ [/^ps\s*$/, 'tasklist'],
1736
+ [/^kill\s+(.+)$/, 'taskkill /PID $1'],
1737
+
1738
+ // Environment
1739
+ [/^pwd\s*$/, 'cd'],
1740
+ [/^whoami\s*$/, 'echo %USERNAME%'],
1741
+
1742
+ // Network
1743
+ [/^ping\s+(.+)$/, 'ping $1'],
1744
+ [/^wget\s+(.+)$/, 'curl -O $1'],
1745
+ [/^curl\s+(.+)$/, 'curl $1']
1746
+ ]);
1747
+
1748
+ // Apply translations
1749
+ for (const [regex, replacement] of translations) {
1750
+ if (regex.test(translated)) {
1751
+ translated = translated.replace(regex, replacement);
1752
+ break;
1753
+ }
1754
+ }
1755
+
1756
+ // Fix echo command with single quotes
1757
+ translated = translated.replace(/echo\s+'([^']+)'\s*>/g, 'echo "$1" >');
1758
+ translated = translated.replace(/echo\s+'([^']+)'$/g, 'echo "$1"');
1759
+
1760
+ // Fix multi-line echo commands
1761
+ if (translated.includes("echo '") && translated.includes("\\n")) {
1762
+ // Convert multi-line echo to multiple echo commands or use different approach
1763
+ const match = translated.match(/echo\s+'(.+?)'\s*>\s*(.+)$/);
1764
+ if (match) {
1765
+ const content = match[1].replace(/\\n/g, '\n');
1766
+ const filename = match[2];
1767
+ const lines = content.split('\n');
1768
+
1769
+ // Create a batch file approach for multi-line content
1770
+ translated = `(${lines.map(line => `echo ${line}`).join(' & ')}) > ${filename}`;
1771
+ }
1772
+ }
1773
+
1774
+ this.logger?.info('Command translated for Windows CMD', {
1775
+ original: command,
1776
+ translated: translated
1777
+ });
1778
+
1779
+ return translated;
1780
+ }
1781
+
1782
+ /**
1783
+ * Translate command to PowerShell syntax
1784
+ * @param {string} command - Original command
1785
+ * @returns {string} PowerShell equivalent
1786
+ * @private
1787
+ */
1788
+ translateToPowerShell(command) {
1789
+ let translated = command;
1790
+
1791
+ const translations = new Map([
1792
+ [/^ls\s*$/, 'Get-ChildItem'],
1793
+ [/^ls\s+-la?$/, 'Get-ChildItem -Force'],
1794
+ [/^cat\s+(.+)$/, 'Get-Content $1'],
1795
+ [/^cp\s+(.+)\s+(.+)$/, 'Copy-Item $1 $2'],
1796
+ [/^mv\s+(.+)\s+(.+)$/, 'Move-Item $1 $2'],
1797
+ [/^rm\s+(.+)$/, 'Remove-Item $1'],
1798
+ [/^pwd\s*$/, 'Get-Location'],
1799
+ [/^ps\s*$/, 'Get-Process']
1800
+ ]);
1801
+
1802
+ for (const [regex, replacement] of translations) {
1803
+ if (regex.test(translated)) {
1804
+ translated = translated.replace(regex, replacement);
1805
+ break;
1806
+ }
1807
+ }
1808
+
1809
+ this.logger?.info('Command translated for PowerShell', {
1810
+ original: command,
1811
+ translated: translated
1812
+ });
1813
+
1814
+ return translated;
1815
+ }
1816
+
1817
+ /**
1818
+ * Translate command to Bash syntax
1819
+ * @param {string} command - Original command
1820
+ * @returns {string} Bash equivalent
1821
+ * @private
1822
+ */
1823
+ translateToBash(command) {
1824
+ // For bash, mainly fix Windows-specific commands
1825
+ let translated = command;
1826
+
1827
+ const translations = new Map([
1828
+ [/^dir\s*$/, 'ls'],
1829
+ [/^dir\s+\/a$/, 'ls -a'],
1830
+ [/^type\s+(.+)$/, 'cat $1'],
1831
+ [/^copy\s+(.+)\s+(.+)$/, 'cp $1 $2'],
1832
+ [/^move\s+(.+)\s+(.+)$/, 'mv $1 $2'],
1833
+ [/^del\s+(.+)$/, 'rm $1'],
1834
+ [/^tasklist\s*$/, 'ps'],
1835
+ [/^cd\s*$/, 'pwd']
1836
+ ]);
1837
+
1838
+ for (const [regex, replacement] of translations) {
1839
+ if (regex.test(translated)) {
1840
+ translated = translated.replace(regex, replacement);
1841
+ break;
1842
+ }
1843
+ }
1844
+
1845
+ return translated;
1846
+ }
1847
+
1848
+ /**
1849
+ * Resource cleanup
1850
+ * @param {string} operationId - Operation identifier
1851
+ */
1852
+ async cleanup(operationId) {
1853
+ // Clean up any hanging processes or temporary resources
1854
+ // This would be expanded based on specific needs
1855
+ }
1856
+
1857
+ /**
1858
+ * Phase 3 & 4: Start a background command
1859
+ * @param {string} command - Command to execute
1860
+ * @param {string} workingDir - Working directory
1861
+ * @param {Object} options - Execution options
1862
+ * @returns {Promise<Object>} Command info with commandId
1863
+ */
1864
+ async startBackgroundCommand(command, workingDir, options = {}) {
1865
+ const { agentId, context } = options;
1866
+
1867
+ if (!agentId) {
1868
+ throw new Error('agentId is required for background commands');
1869
+ }
1870
+
1871
+ // Check agent resource limits (only count active commands: running or waiting_for_input)
1872
+ const agentCommands = this.getAgentCommands(agentId);
1873
+ const activeAgentCommands = agentCommands.filter(cmd =>
1874
+ cmd.state === 'running' || cmd.state === 'waiting_for_input'
1875
+ );
1876
+ if (activeAgentCommands.length >= this.MAX_BACKGROUND_COMMANDS_PER_AGENT) {
1877
+ throw new Error(`Maximum background commands per agent exceeded (${this.MAX_BACKGROUND_COMMANDS_PER_AGENT})`);
1878
+ }
1879
+
1880
+ // Check global resource limits (only count active commands)
1881
+ const allCommands = Array.from(this.commandTracker.values());
1882
+ const activeGlobalCommands = allCommands.filter(cmd =>
1883
+ cmd.state === 'running' || cmd.state === 'waiting_for_input'
1884
+ );
1885
+ if (activeGlobalCommands.length >= this.MAX_BACKGROUND_COMMANDS_GLOBAL) {
1886
+ throw new Error(`Maximum global background commands exceeded (${this.MAX_BACKGROUND_COMMANDS_GLOBAL})`);
1887
+ }
1888
+
1889
+ // Generate unique command ID
1890
+ const commandId = `${agentId}-cmd-${Date.now()}-${++this.commandIdCounter}`;
1891
+
1892
+ // Translate command
1893
+ const originalCommand = command;
1894
+ let translatedCommand;
1895
+
1896
+ try {
1897
+ translatedCommand = await this.translateCommand(command, {
1898
+ agentId,
1899
+ toolsRegistry: context?.toolsRegistry,
1900
+ messageProcessor: context?.messageProcessor,
1901
+ aiService: context?.aiService,
1902
+ apiKey: context?.apiKey,
1903
+ customApiKeys: context?.customApiKeys,
1904
+ platformProvided: context?.platformProvided
1905
+ });
1906
+ } catch (error) {
1907
+ this.logger?.warn('Command translation failed, using original command', {
1908
+ originalCommand,
1909
+ error: error.message
1910
+ });
1911
+ translatedCommand = command;
1912
+ }
1913
+
1914
+ this.logger?.info(`Starting background command: ${translatedCommand}`, {
1915
+ commandId,
1916
+ agentId,
1917
+ originalCommand,
1918
+ workingDirectory: workingDir
1919
+ });
1920
+
1921
+ // Spawn process with stdin kept open
1922
+ let childProcess;
1923
+
1924
+ if (this.detectedTerminal === 'cmd' || this.detectedTerminal === 'powershell') {
1925
+ const shell = this.detectedTerminal === 'powershell' ? 'powershell' : 'cmd';
1926
+ const shellArgs = this.detectedTerminal === 'powershell'
1927
+ ? ['-Command', translatedCommand]
1928
+ : ['/c', translatedCommand];
1929
+
1930
+ childProcess = spawn(shell, shellArgs, {
1931
+ cwd: workingDir,
1932
+ env: { ...process.env },
1933
+ windowsHide: true,
1934
+ stdio: ['pipe', 'pipe', 'pipe'] // Keep stdin open
1935
+ });
1936
+ } else {
1937
+ childProcess = spawn('sh', ['-c', translatedCommand], {
1938
+ cwd: workingDir,
1939
+ env: { ...process.env },
1940
+ stdio: ['pipe', 'pipe', 'pipe'] // Keep stdin open
1941
+ });
1942
+ }
1943
+
1944
+ // Initialize command tracking
1945
+ const commandInfo = {
1946
+ commandId,
1947
+ agentId,
1948
+ pid: childProcess.pid,
1949
+ command: originalCommand,
1950
+ translatedCommand,
1951
+ workingDirectory: workingDir,
1952
+ startTime: new Date().toISOString(),
1953
+ state: 'running',
1954
+ exitCode: null,
1955
+ stdoutBuffer: '',
1956
+ stderrBuffer: '',
1957
+ lastOutputTime: Date.now(),
1958
+ promptDetected: null,
1959
+ process: childProcess
1960
+ };
1961
+
1962
+ this.commandTracker.set(commandId, commandInfo);
1963
+
1964
+ // Set up stream handlers
1965
+ childProcess.stdout.on('data', (chunk) => {
1966
+ const data = chunk.toString();
1967
+ commandInfo.stdoutBuffer += data;
1968
+ commandInfo.lastOutputTime = Date.now();
1969
+
1970
+ // Check for prompts
1971
+ if (!commandInfo.promptDetected) {
1972
+ const detection = this.promptDetector.detectPrompt(commandInfo.stdoutBuffer, 'stdout');
1973
+ if (detection) {
1974
+ commandInfo.promptDetected = detection;
1975
+ commandInfo.state = 'waiting_for_input';
1976
+ this.logger?.info('Prompt detected in background command', {
1977
+ commandId,
1978
+ agentId,
1979
+ type: detection.type,
1980
+ matchedText: detection.matchedText
1981
+ });
1982
+ }
1983
+ }
1984
+ });
1985
+
1986
+ childProcess.stderr.on('data', (chunk) => {
1987
+ const data = chunk.toString();
1988
+ commandInfo.stderrBuffer += data;
1989
+ commandInfo.lastOutputTime = Date.now();
1990
+
1991
+ // Check for prompts in stderr too
1992
+ if (!commandInfo.promptDetected) {
1993
+ const detection = this.promptDetector.detectPrompt(commandInfo.stderrBuffer, 'stderr');
1994
+ if (detection) {
1995
+ commandInfo.promptDetected = detection;
1996
+ commandInfo.state = 'waiting_for_input';
1997
+ this.logger?.info('Prompt detected in background command stderr', {
1998
+ commandId,
1999
+ agentId,
2000
+ type: detection.type,
2001
+ matchedText: detection.matchedText
2002
+ });
2003
+ }
2004
+ }
2005
+ });
2006
+
2007
+ childProcess.on('exit', (code, signal) => {
2008
+ commandInfo.exitCode = code;
2009
+ commandInfo.state = code === 0 ? 'completed' : 'failed';
2010
+ commandInfo.endTime = new Date().toISOString();
2011
+
2012
+ this.logger?.info('Background command exited', {
2013
+ commandId,
2014
+ agentId,
2015
+ exitCode: code,
2016
+ signal,
2017
+ state: commandInfo.state
2018
+ });
2019
+ });
2020
+
2021
+ childProcess.on('error', (error) => {
2022
+ commandInfo.state = 'failed';
2023
+ commandInfo.error = error.message;
2024
+ commandInfo.endTime = new Date().toISOString();
2025
+
2026
+ this.logger?.error('Background command error', {
2027
+ commandId,
2028
+ agentId,
2029
+ error: error.message
2030
+ });
2031
+ });
2032
+
2033
+ // Return command info
2034
+ return {
2035
+ success: true,
2036
+ commandId,
2037
+ pid: childProcess.pid,
2038
+ command: originalCommand,
2039
+ translatedCommand,
2040
+ workingDirectory: workingDir,
2041
+ message: `Background command started with ID: ${commandId}`
2042
+ };
2043
+ }
2044
+
2045
+ /**
2046
+ * Phase 3: Send input to a background command (stdin)
2047
+ * @param {string} commandId - Command identifier
2048
+ * @param {string} input - Input to send (will add newline automatically)
2049
+ * @param {string} agentId - Agent identifier for ownership validation
2050
+ * @returns {Object} Result
2051
+ */
2052
+ sendInput(commandId, input, agentId) {
2053
+ // Validate ownership
2054
+ const commandInfo = this.validateCommandOwnership(commandId, agentId);
2055
+
2056
+ if (commandInfo.state === 'completed' || commandInfo.state === 'failed') {
2057
+ throw new Error(`Cannot send input to ${commandInfo.state} command`);
2058
+ }
2059
+
2060
+ if (!commandInfo.process || commandInfo.process.killed) {
2061
+ throw new Error('Command process is not running');
2062
+ }
2063
+
2064
+ // Send input with newline
2065
+ const inputWithNewline = input.endsWith('\n') ? input : input + '\n';
2066
+ commandInfo.process.stdin.write(inputWithNewline);
2067
+
2068
+ this.logger?.info('Input sent to background command', {
2069
+ commandId,
2070
+ agentId,
2071
+ inputLength: input.length
2072
+ });
2073
+
2074
+ // Update state if it was waiting
2075
+ if (commandInfo.state === 'waiting_for_input') {
2076
+ commandInfo.state = 'running';
2077
+ commandInfo.promptDetected = null; // Clear prompt after answering
2078
+ }
2079
+
2080
+ return {
2081
+ success: true,
2082
+ commandId,
2083
+ message: 'Input sent successfully'
2084
+ };
2085
+ }
2086
+
2087
+ /**
2088
+ * Phase 4: Get status of a background command
2089
+ * @param {string} commandId - Command identifier
2090
+ * @param {string} agentId - Agent identifier for ownership validation
2091
+ * @returns {Object} Command status
2092
+ */
2093
+ getCommandStatus(commandId, agentId) {
2094
+ const commandInfo = this.validateCommandOwnership(commandId, agentId);
2095
+
2096
+ const timeSinceLastOutput = Date.now() - commandInfo.lastOutputTime;
2097
+
2098
+ return {
2099
+ success: true,
2100
+ commandId,
2101
+ pid: commandInfo.pid,
2102
+ command: commandInfo.command,
2103
+ state: commandInfo.state,
2104
+ exitCode: commandInfo.exitCode,
2105
+ startTime: commandInfo.startTime,
2106
+ endTime: commandInfo.endTime,
2107
+ workingDirectory: commandInfo.workingDirectory,
2108
+ stdout: commandInfo.stdoutBuffer,
2109
+ stderr: commandInfo.stderrBuffer,
2110
+ stdoutLength: commandInfo.stdoutBuffer.length,
2111
+ stderrLength: commandInfo.stderrBuffer.length,
2112
+ lastOutputTime: commandInfo.lastOutputTime,
2113
+ timeSinceLastOutput,
2114
+ promptDetected: !!commandInfo.promptDetected,
2115
+ promptInfo: commandInfo.promptDetected || undefined
2116
+ };
2117
+ }
2118
+
2119
+ /**
2120
+ * Phase 4: Kill a background command
2121
+ * @param {string} commandId - Command identifier
2122
+ * @param {string} agentId - Agent identifier for ownership validation
2123
+ * @returns {Object} Result
2124
+ */
2125
+ killCommand(commandId, agentId) {
2126
+ const commandInfo = this.validateCommandOwnership(commandId, agentId);
2127
+
2128
+ if (commandInfo.state === 'completed' || commandInfo.state === 'failed') {
2129
+ return {
2130
+ success: true,
2131
+ commandId,
2132
+ message: 'Command already terminated',
2133
+ state: commandInfo.state
2134
+ };
2135
+ }
2136
+
2137
+ if (commandInfo.process && !commandInfo.process.killed) {
2138
+ commandInfo.process.kill('SIGTERM');
2139
+
2140
+ // If SIGTERM doesn't work, try SIGKILL after 5s
2141
+ setTimeout(() => {
2142
+ if (commandInfo.process && !commandInfo.process.killed) {
2143
+ commandInfo.process.kill('SIGKILL');
2144
+ }
2145
+ }, 5000);
2146
+
2147
+ this.logger?.info('Background command killed', {
2148
+ commandId,
2149
+ agentId
2150
+ });
2151
+
2152
+ return {
2153
+ success: true,
2154
+ commandId,
2155
+ message: 'Command killed successfully'
2156
+ };
2157
+ }
2158
+
2159
+ return {
2160
+ success: false,
2161
+ commandId,
2162
+ error: 'Command process not found or already killed'
2163
+ };
2164
+ }
2165
+
2166
+ /**
2167
+ * Phase 4: List all commands for an agent
2168
+ * @param {string} agentId - Agent identifier
2169
+ * @returns {Array} List of command info objects
2170
+ */
2171
+ listAgentCommands(agentId) {
2172
+ const commands = this.getAgentCommands(agentId);
2173
+
2174
+ return commands.map(cmd => ({
2175
+ commandId: cmd.commandId,
2176
+ command: cmd.command,
2177
+ state: cmd.state,
2178
+ pid: cmd.pid,
2179
+ exitCode: cmd.exitCode,
2180
+ startTime: cmd.startTime,
2181
+ endTime: cmd.endTime,
2182
+ promptDetected: !!cmd.promptDetected,
2183
+ timeSinceLastOutput: Date.now() - cmd.lastOutputTime
2184
+ }));
2185
+ }
2186
+
2187
+ /**
2188
+ * Phase 4: Get agent's commands (helper method)
2189
+ * @param {string} agentId - Agent identifier
2190
+ * @returns {Array} Array of command info objects
2191
+ * @private
2192
+ */
2193
+ getAgentCommands(agentId) {
2194
+ return Array.from(this.commandTracker.values())
2195
+ .filter(cmd => cmd.agentId === agentId);
2196
+ }
2197
+
2198
+ /**
2199
+ * Phase 4: Validate command ownership
2200
+ * @param {string} commandId - Command identifier
2201
+ * @param {string} agentId - Agent identifier
2202
+ * @returns {Object} Command info if valid
2203
+ * @throws {Error} If command not found or access denied
2204
+ * @private
2205
+ */
2206
+ validateCommandOwnership(commandId, agentId) {
2207
+ const commandInfo = this.commandTracker.get(commandId);
2208
+
2209
+ if (!commandInfo) {
2210
+ throw new Error(`Command not found: ${commandId}`);
2211
+ }
2212
+
2213
+ if (commandInfo.agentId !== agentId) {
2214
+ throw new Error(`Access denied: Command belongs to agent ${commandInfo.agentId}`);
2215
+ }
2216
+
2217
+ return commandInfo;
2218
+ }
2219
+
2220
+ /**
2221
+ * Phase 4: Cleanup all commands for an agent (called when agent is deleted)
2222
+ * @param {string} agentId - Agent identifier
2223
+ * @returns {Promise<Object>} Cleanup result
2224
+ */
2225
+ async cleanupAgent(agentId) {
2226
+ const commands = this.getAgentCommands(agentId);
2227
+
2228
+ let killedCount = 0;
2229
+ let removedCount = 0;
2230
+
2231
+ for (const commandInfo of commands) {
2232
+ // Kill process if still running
2233
+ if (commandInfo.process && !commandInfo.process.killed) {
2234
+ commandInfo.process.kill('SIGTERM');
2235
+ killedCount++;
2236
+
2237
+ // Force kill after delay
2238
+ setTimeout(() => {
2239
+ if (commandInfo.process && !commandInfo.process.killed) {
2240
+ commandInfo.process.kill('SIGKILL');
2241
+ }
2242
+ }, 3000);
2243
+ }
2244
+
2245
+ // Remove from tracker
2246
+ this.commandTracker.delete(commandInfo.commandId);
2247
+ removedCount++;
2248
+ }
2249
+
2250
+ this.logger?.info('Agent commands cleaned up', {
2251
+ agentId,
2252
+ killedCount,
2253
+ removedCount
2254
+ });
2255
+
2256
+ return {
2257
+ success: true,
2258
+ agentId,
2259
+ killedCount,
2260
+ removedCount,
2261
+ message: `Cleaned up ${removedCount} commands for agent ${agentId}`
2262
+ };
2263
+ }
2264
+
2265
+ /**
2266
+ * Phase 4: Auto-cleanup stale completed commands
2267
+ * @returns {Object} Cleanup result
2268
+ */
2269
+ cleanupStaleCommands() {
2270
+ const now = Date.now();
2271
+ let removedCount = 0;
2272
+
2273
+ for (const [commandId, commandInfo] of this.commandTracker.entries()) {
2274
+ // Only clean up completed/failed commands
2275
+ if (commandInfo.state !== 'completed' && commandInfo.state !== 'failed') {
2276
+ continue;
2277
+ }
2278
+
2279
+ // Check age
2280
+ const startTime = new Date(commandInfo.startTime).getTime();
2281
+ const ageMinutes = (now - startTime) / 1000 / 60;
2282
+
2283
+ if (ageMinutes > this.MAX_COMMAND_AGE_MINUTES) {
2284
+ this.commandTracker.delete(commandId);
2285
+ removedCount++;
2286
+ }
2287
+ }
2288
+
2289
+ if (removedCount > 0) {
2290
+ this.logger?.info('Stale commands cleaned up', {
2291
+ removedCount,
2292
+ ageThresholdMinutes: this.MAX_COMMAND_AGE_MINUTES
2293
+ });
2294
+ }
2295
+
2296
+ return {
2297
+ success: true,
2298
+ removedCount,
2299
+ message: `Cleaned up ${removedCount} stale commands`
2300
+ };
2301
+ }
2302
+ }
2303
+
2304
+ export default TerminalTool;