@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,1430 @@
1
+ /**
2
+ * WebTool - Web browsing and automation with Puppeteer
3
+ *
4
+ * Purpose:
5
+ * - Search the web using known search engines
6
+ * - Fetch web content in various formats
7
+ * - Interactive browser automation with command chaining
8
+ * - Tab management with agent isolation
9
+ * - Screenshot capture and AI-powered analysis
10
+ * - Mouse and keyboard event simulation
11
+ */
12
+
13
+ import { BaseTool } from './baseTool.js';
14
+ import TagParser from '../utilities/tagParser.js';
15
+ import puppeteer from 'puppeteer';
16
+ import path from 'path';
17
+ import fs from 'fs/promises';
18
+ import os from 'os';
19
+
20
+ import {
21
+ TOOL_STATUS,
22
+ SYSTEM_DEFAULTS
23
+ } from '../utilities/constants.js';
24
+
25
+ class WebTool extends BaseTool {
26
+ constructor(config = {}, logger = null) {
27
+ super(config, logger);
28
+
29
+ // Tool metadata
30
+ this.requiresProject = false;
31
+ this.isAsync = true;
32
+
33
+ // Browser instance (singleton per system)
34
+ this.browser = null;
35
+ this.browserInitializing = false;
36
+
37
+ // Tab tracking: Map<agentId, Map<tabName, tabInfo>>
38
+ this.agentTabs = new Map();
39
+
40
+ // Known search engines
41
+ this.searchEngines = [
42
+ {
43
+ name: 'google',
44
+ url: 'https://www.google.com/search?q=',
45
+ searchSelector: 'input[name="q"]',
46
+ submitSelector: 'input[type="submit"], button[type="submit"]',
47
+ resultsSelector: '#search .g a, #search a[href]',
48
+ waitSelector: '#search'
49
+ },
50
+ {
51
+ name: 'bing',
52
+ url: 'https://www.bing.com/search?q=',
53
+ searchSelector: 'input[name="q"]',
54
+ submitSelector: 'input[type="submit"]',
55
+ resultsSelector: '.b_algo a',
56
+ waitSelector: '#b_results'
57
+ },
58
+ {
59
+ name: 'duckduckgo',
60
+ url: 'https://duckduckgo.com/?q=',
61
+ searchSelector: 'input[name="q"]',
62
+ submitSelector: 'button[type="submit"]',
63
+ resultsSelector: '.result__a, a[data-testid="result-title-a"]',
64
+ waitSelector: '#links, [data-testid="mainline"]'
65
+ }
66
+ ];
67
+
68
+ // Configuration
69
+ this.TAB_IDLE_TIMEOUT = config.tabIdleTimeout || 60 * 60 * 1000; // 1 hour
70
+ this.CLEANUP_INTERVAL = config.cleanupInterval || 5 * 60 * 1000; // 5 minutes
71
+ this.DEFAULT_TIMEOUT = config.defaultTimeout || 60000; // 60 seconds
72
+ this.TEMP_DIR = config.tempDir || path.join(os.tmpdir(), 'webtool-screenshots');
73
+
74
+ // Start cleanup timer
75
+ this.cleanupTimer = null;
76
+ this.startCleanupTimer();
77
+
78
+ // Ensure temp directory exists
79
+ this.ensureTempDir();
80
+ }
81
+
82
+ /**
83
+ * Get tool description for LLM consumption
84
+ * @returns {string} Tool description
85
+ */
86
+ getDescription() {
87
+ return `
88
+ Web Tool: Browse, search, and automate web interactions using a real browser (Puppeteer).
89
+
90
+ IMPORTANT: This tool supports command chaining - nest multiple actions together to execute them sequentially without waiting for responses between each action.
91
+
92
+ USAGE:
93
+ [tool id="web"]
94
+ <operation>search|fetch|interactive</operation>
95
+ <!-- Operation-specific content -->
96
+ [/tool]
97
+
98
+ ALTERNATIVE JSON FORMAT:
99
+ \`\`\`json
100
+ {
101
+ "toolId": "web",
102
+ "operation": "search|fetch|interactive",
103
+ "parameters": { ... }
104
+ }
105
+ \`\`\`
106
+
107
+ ═══════════════════════════════════════════════════════════════
108
+ OPERATION 1: SEARCH THE WEB
109
+ ═══════════════════════════════════════════════════════════════
110
+
111
+ Search using real browsers on known search engines (Google, Bing, DuckDuckGo).
112
+
113
+ XML SYNTAX:
114
+ [tool id="web"]
115
+ <operation>search</operation>
116
+ <query>puppeteer web scraping tutorial</query>
117
+ <engine>google</engine> <!-- Optional: google|bing|duckduckgo, default: google -->
118
+ <max-results>10</max-results> <!-- Optional, default: 10 -->
119
+ [/tool]
120
+
121
+ JSON SYNTAX:
122
+ \`\`\`json
123
+ {
124
+ "toolId": "web",
125
+ "operation": "search",
126
+ "query": "puppeteer web scraping tutorial",
127
+ "engine": "google",
128
+ "maxResults": 10
129
+ }
130
+ \`\`\`
131
+
132
+ OUTPUT: List of URLs with titles and descriptions
133
+
134
+ ═══════════════════════════════════════════════════════════════
135
+ OPERATION 2: FETCH WEB CONTENT
136
+ ═══════════════════════════════════════════════════════════════
137
+
138
+ Fetch content from a URL in various formats.
139
+
140
+ XML SYNTAX:
141
+ [tool id="web"]
142
+ <operation>fetch</operation>
143
+ <url>https://example.com</url>
144
+ <format>title,text,links</format> <!-- Options: title|text|links|html|console -->
145
+ [/tool]
146
+
147
+ JSON SYNTAX:
148
+ \`\`\`json
149
+ {
150
+ "toolId": "web",
151
+ "operation": "fetch",
152
+ "url": "https://example.com",
153
+ "formats": ["title", "text", "links", "html", "console"]
154
+ }
155
+ \`\`\`
156
+
157
+ FORMAT OPTIONS:
158
+ - title: Page title
159
+ - text: Plain text content (no HTML)
160
+ - links: All links on page
161
+ - html: Full HTML source
162
+ - console: Browser console messages
163
+
164
+ OUTPUT: Object with requested content formats
165
+
166
+ ═══════════════════════════════════════════════════════════════
167
+ OPERATION 3: INTERACTIVE BROWSER AUTOMATION
168
+ ═══════════════════════════════════════════════════════════════
169
+
170
+ Control a real browser with command chaining for complex workflows.
171
+
172
+ XML SYNTAX (RECOMMENDED FOR CHAINING):
173
+ [tool id="web"]
174
+ <operation>interactive</operation>
175
+ <headless>true</headless> <!-- true|false, default: true -->
176
+ <actions>
177
+ <open-tab name="search">
178
+ <navigate>https://github.com/trending</navigate>
179
+ <wait-for selector=".Box-row" timeout="5000" />
180
+ <click selector=".Box-row:first-child a" />
181
+ <wait-for selector="#readme" />
182
+ <screenshot format="file" path="readme.png" />
183
+ <analyze-screenshot>What is the main topic of this README?</analyze-screenshot>
184
+ <extract-text selector="#readme" />
185
+ <get-source />
186
+ </open-tab>
187
+ <open-tab name="docs">
188
+ <navigate>https://docs.example.com</navigate>
189
+ <type selector="input.search" text="installation" />
190
+ <press key="Enter" />
191
+ <extract-links selector="a.doc-link" />
192
+ </open-tab>
193
+ <list-tabs />
194
+ <close-tab name="search" />
195
+ </actions>
196
+ [/tool]
197
+
198
+ JSON SYNTAX (ALTERNATIVE):
199
+ \`\`\`json
200
+ {
201
+ "toolId": "web",
202
+ "operation": "interactive",
203
+ "headless": true,
204
+ "actions": [
205
+ {
206
+ "type": "open-tab",
207
+ "name": "search",
208
+ "url": "https://github.com/trending",
209
+ "nestedActions": [
210
+ {"type": "wait-for", "selector": ".Box-row", "timeout": 5000},
211
+ {"type": "click", "selector": ".Box-row:first-child a"},
212
+ {"type": "screenshot", "format": "base64"},
213
+ {"type": "extract-text", "selector": "#readme"}
214
+ ]
215
+ },
216
+ {"type": "list-tabs"},
217
+ {"type": "close-tab", "name": "search"}
218
+ ]
219
+ }
220
+ \`\`\`
221
+
222
+ SUPPORTED ACTIONS:
223
+ - open-tab: Open new tab with nested actions
224
+ - close-tab: Close specific tab
225
+ - switch-tab: Switch to existing tab
226
+ - list-tabs: List all active tabs for this agent
227
+ - navigate: Go to URL
228
+ - click: Click element (left|right|middle)
229
+ - type: Type text into element
230
+ - press: Press keyboard key
231
+ - wait-for: Wait for element to appear
232
+ - screenshot: Capture screenshot (file|base64)
233
+ - analyze-screenshot: AI analysis of current page
234
+ - extract-text: Extract text from selector
235
+ - extract-links: Extract all links
236
+ - get-source: Get HTML source
237
+ - get-console: Get console messages
238
+ - scroll: Scroll page
239
+ - hover: Hover over element
240
+
241
+ MOUSE EVENTS:
242
+ <click selector=".button" button="left" /> <!-- left|right|middle -->
243
+ <hover selector=".menu" />
244
+ <mouse-move selector=".element" />
245
+
246
+ KEYBOARD EVENTS:
247
+ <type selector="input" text="Hello World" />
248
+ <press key="Enter" />
249
+ <press key="Control+C" /> <!-- Supports modifier keys -->
250
+
251
+ SCREENSHOT OPTIONS:
252
+ <screenshot format="file" path="screenshot.png" /> <!-- Save to project dir -->
253
+ <screenshot format="file" /> <!-- Save to temp dir -->
254
+ <screenshot format="base64" /> <!-- Return as base64 string -->
255
+
256
+ AI SCREENSHOT ANALYSIS:
257
+ <analyze-screenshot>What products are visible on this page?</analyze-screenshot>
258
+ <analyze-screenshot model="gpt-4-vision">Describe the layout</analyze-screenshot>
259
+
260
+ TAB MANAGEMENT:
261
+ - Tabs are agent-isolated (each agent has its own tabs)
262
+ - Tabs auto-close after 1 hour of inactivity
263
+ - Tab names must be unique per agent
264
+ - Use descriptive names for easy identification
265
+
266
+ ═══════════════════════════════════════════════════════════════
267
+ COMMAND CHAINING BENEFITS
268
+ ═══════════════════════════════════════════════════════════════
269
+
270
+ Instead of:
271
+ 1. Open tab → wait for response
272
+ 2. Navigate → wait for response
273
+ 3. Click → wait for response
274
+ 4. Screenshot → wait for response
275
+
276
+ Do this (ONE REQUEST):
277
+ <open-tab name="task">
278
+ <navigate>URL</navigate>
279
+ <click selector=".button" />
280
+ <screenshot />
281
+ </open-tab>
282
+
283
+ All actions execute sequentially in one operation!
284
+
285
+ ═══════════════════════════════════════════════════════════════
286
+ EXAMPLES
287
+ ═══════════════════════════════════════════════════════════════
288
+
289
+ EXAMPLE 1: Search and analyze results
290
+ [tool id="web"]
291
+ <operation>interactive</operation>
292
+ <headless>true</headless>
293
+ <actions>
294
+ <open-tab name="search">
295
+ <navigate>https://google.com</navigate>
296
+ <type selector="input[name=q]">best web scraping tools 2025</type>
297
+ <press key="Enter" />
298
+ <wait-for selector="#search" />
299
+ <screenshot format="file" path="search-results.png" />
300
+ <analyze-screenshot>List the top 3 tools mentioned</analyze-screenshot>
301
+ <extract-links selector="#search .g a" />
302
+ </open-tab>
303
+ </actions>
304
+ [/tool]
305
+
306
+ EXAMPLE 2: Multi-tab workflow
307
+ [tool id="web"]
308
+ <operation>interactive</operation>
309
+ <actions>
310
+ <open-tab name="github">
311
+ <navigate>https://github.com/trending</navigate>
312
+ <extract-links selector=".Box-row a" />
313
+ </open-tab>
314
+ <open-tab name="npm">
315
+ <navigate>https://npmjs.com/package/puppeteer</navigate>
316
+ <extract-text selector=".package-description" />
317
+ </open-tab>
318
+ <list-tabs />
319
+ </actions>
320
+ [/tool]
321
+
322
+ EXAMPLE 3: Form interaction
323
+ [tool id="web"]
324
+ <operation>interactive</operation>
325
+ <headless>false</headless> <!-- Visible browser for debugging -->
326
+ <actions>
327
+ <open-tab name="form">
328
+ <navigate>https://example.com/contact</navigate>
329
+ <type selector="#name" text="John Doe" />
330
+ <type selector="#email" text="john@example.com" />
331
+ <type selector="#message" text="Hello!" />
332
+ <click selector="#submit" />
333
+ <wait-for selector=".success-message" />
334
+ <screenshot format="base64" />
335
+ </open-tab>
336
+ </actions>
337
+ [/tool]
338
+
339
+ EXAMPLE 4: Simple fetch
340
+ [tool id="web"]
341
+ <operation>fetch</operation>
342
+ <url>https://example.com</url>
343
+ <format>title,text,links</format>
344
+ [/tool]
345
+
346
+ EXAMPLE 5: Quick search
347
+ [tool id="web"]
348
+ <operation>search</operation>
349
+ <query>openai gpt-4 api documentation</query>
350
+ <engine>google</engine>
351
+ <max-results>5</max-results>
352
+ [/tool]
353
+
354
+ SECURITY NOTES:
355
+ - Browser runs in isolated context per agent
356
+ - Tabs auto-close after 1 hour of inactivity
357
+ - Screenshots stored in temp directory with auto-cleanup
358
+ - No access to local file system beyond allowed directories
359
+
360
+ BEST PRACTICES:
361
+ - Use command chaining to minimize round-trips
362
+ - Use descriptive tab names
363
+ - Close tabs when done to free resources
364
+ - Use headless mode for better performance
365
+ - Use visible mode only for debugging
366
+ `;
367
+ }
368
+
369
+ /**
370
+ * Parse parameters from tool command content
371
+ * @param {string} content - Raw tool command content
372
+ * @returns {Object} Parsed parameters
373
+ */
374
+ parseParameters(content) {
375
+ try {
376
+ // Try JSON first
377
+ if (content.trim().startsWith('{')) {
378
+ return JSON.parse(content);
379
+ }
380
+
381
+ // Parse XML-style tags
382
+ const params = {};
383
+
384
+ // Extract operation
385
+ const operationMatches = TagParser.extractContent(content, 'operation');
386
+ if (operationMatches.length > 0) {
387
+ params.operation = operationMatches[0].trim();
388
+ }
389
+
390
+ // Extract based on operation
391
+ switch (params.operation) {
392
+ case 'search':
393
+ params.query = TagParser.extractContent(content, 'query')[0]?.trim();
394
+ params.engine = TagParser.extractContent(content, 'engine')[0]?.trim() || 'google';
395
+ const maxResults = TagParser.extractContent(content, 'max-results')[0]?.trim();
396
+ params.maxResults = maxResults ? parseInt(maxResults, 10) : 10;
397
+ break;
398
+
399
+ case 'fetch':
400
+ params.url = TagParser.extractContent(content, 'url')[0]?.trim();
401
+ const formatStr = TagParser.extractContent(content, 'format')[0]?.trim();
402
+ params.formats = formatStr ? formatStr.split(',').map(f => f.trim()) : ['title', 'text'];
403
+ break;
404
+
405
+ case 'interactive':
406
+ const headlessStr = TagParser.extractContent(content, 'headless')[0]?.trim();
407
+ params.headless = headlessStr !== 'false'; // Default true
408
+
409
+ // Extract actions block
410
+ const actionsContent = TagParser.extractContent(content, 'actions')[0];
411
+ if (actionsContent) {
412
+ params.actions = this.parseActions(actionsContent);
413
+ }
414
+ break;
415
+ }
416
+
417
+ params.rawContent = content.trim();
418
+ return params;
419
+
420
+ } catch (error) {
421
+ throw new Error(`Failed to parse web tool parameters: ${error.message}`);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Parse actions from XML content
427
+ * @param {string} content - Actions XML content
428
+ * @returns {Array} Parsed actions
429
+ * @private
430
+ */
431
+ parseActions(content) {
432
+ const actions = [];
433
+
434
+ // Parse open-tab actions
435
+ const openTabRegex = /<open-tab[^>]*name="([^"]+)"[^>]*>([\s\S]*?)<\/open-tab>/g;
436
+ let match;
437
+
438
+ while ((match = openTabRegex.exec(content)) !== null) {
439
+ const [, name, nestedContent] = match;
440
+ const url = TagParser.extractContent(nestedContent, 'navigate')[0]?.trim();
441
+
442
+ actions.push({
443
+ type: 'open-tab',
444
+ name,
445
+ url,
446
+ nestedActions: this.parseNestedActions(nestedContent)
447
+ });
448
+ }
449
+
450
+ // Parse other actions (close-tab, switch-tab, list-tabs, etc.)
451
+ const simpleActions = [
452
+ 'close-tab', 'switch-tab', 'list-tabs', 'navigate',
453
+ 'click', 'type', 'press', 'wait-for', 'screenshot',
454
+ 'analyze-screenshot', 'extract-text', 'extract-links',
455
+ 'get-source', 'get-console', 'scroll', 'hover', 'mouse-move'
456
+ ];
457
+
458
+ for (const actionType of simpleActions) {
459
+ const regex = new RegExp(`<${actionType}([^>]*)>([^<]*)<\/${actionType}>`, 'g');
460
+ let actionMatch;
461
+
462
+ while ((actionMatch = regex.exec(content)) !== null) {
463
+ const [, attrs, value] = actionMatch;
464
+ const action = { type: actionType };
465
+
466
+ // Parse attributes
467
+ const attrRegex = /(\w+(?:-\w+)*)="([^"]*)"/g;
468
+ let attrMatch;
469
+ while ((attrMatch = attrRegex.exec(attrs)) !== null) {
470
+ action[attrMatch[1]] = attrMatch[2];
471
+ }
472
+
473
+ // Add value if present
474
+ if (value && value.trim()) {
475
+ action.value = value.trim();
476
+ }
477
+
478
+ actions.push(action);
479
+ }
480
+ }
481
+
482
+ return actions;
483
+ }
484
+
485
+ /**
486
+ * Parse nested actions within a tab
487
+ * @param {string} content - Nested actions content
488
+ * @returns {Array} Parsed nested actions
489
+ * @private
490
+ */
491
+ parseNestedActions(content) {
492
+ const actions = [];
493
+
494
+ const actionTypes = [
495
+ 'navigate', 'click', 'type', 'press', 'wait-for', 'screenshot',
496
+ 'analyze-screenshot', 'extract-text', 'extract-links',
497
+ 'get-source', 'get-console', 'scroll', 'hover', 'mouse-move'
498
+ ];
499
+
500
+ for (const actionType of actionTypes) {
501
+ const regex = new RegExp(`<${actionType}([^>]*)>([^<]*)<\/${actionType}>|<${actionType}([^>]*)\/>`, 'g');
502
+ let match;
503
+
504
+ while ((match = regex.exec(content)) !== null) {
505
+ const [, attrs1, value, attrs2] = match;
506
+ const attrs = attrs1 || attrs2 || '';
507
+ const action = { type: actionType };
508
+
509
+ // Parse attributes
510
+ const attrRegex = /(\w+(?:-\w+)*)="([^"]*)"/g;
511
+ let attrMatch;
512
+ while ((attrMatch = attrRegex.exec(attrs)) !== null) {
513
+ action[attrMatch[1]] = attrMatch[2];
514
+ }
515
+
516
+ // Add value if present
517
+ if (value && value.trim()) {
518
+ action.value = value.trim();
519
+ }
520
+
521
+ actions.push(action);
522
+ }
523
+ }
524
+
525
+ return actions;
526
+ }
527
+
528
+ /**
529
+ * Get required parameters based on operation
530
+ * @returns {Array<string>} Array of required parameter names
531
+ */
532
+ getRequiredParameters() {
533
+ return ['operation'];
534
+ }
535
+
536
+ /**
537
+ * Custom parameter validation
538
+ * @param {Object} params - Parameters to validate
539
+ * @returns {Object} Validation result
540
+ */
541
+ customValidateParameters(params) {
542
+ const errors = [];
543
+
544
+ if (!['search', 'fetch', 'interactive'].includes(params.operation)) {
545
+ errors.push('operation must be one of: search, fetch, interactive');
546
+ return { valid: false, errors };
547
+ }
548
+
549
+ switch (params.operation) {
550
+ case 'search':
551
+ if (!params.query) {
552
+ errors.push('query is required for search operation');
553
+ }
554
+ break;
555
+
556
+ case 'fetch':
557
+ if (!params.url) {
558
+ errors.push('url is required for fetch operation');
559
+ }
560
+ break;
561
+
562
+ case 'interactive':
563
+ if (!params.actions || !Array.isArray(params.actions) || params.actions.length === 0) {
564
+ errors.push('actions array is required for interactive operation');
565
+ }
566
+ break;
567
+ }
568
+
569
+ return {
570
+ valid: errors.length === 0,
571
+ errors
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Execute tool with parsed parameters
577
+ * @param {Object} params - Parsed parameters
578
+ * @param {Object} context - Execution context
579
+ * @returns {Promise<Object>} Execution result
580
+ */
581
+ async execute(params, context) {
582
+ const { operation } = params;
583
+ const { agentId } = context;
584
+
585
+ try {
586
+ // Ensure browser is initialized
587
+ await this.ensureBrowser();
588
+
589
+ let result;
590
+
591
+ switch (operation) {
592
+ case 'search':
593
+ result = await this.search(params.query, {
594
+ engine: params.engine || 'google',
595
+ maxResults: params.maxResults || 10,
596
+ agentId
597
+ });
598
+ break;
599
+
600
+ case 'fetch':
601
+ result = await this.fetch(params.url, {
602
+ formats: params.formats || ['title', 'text'],
603
+ agentId
604
+ });
605
+ break;
606
+
607
+ case 'interactive':
608
+ result = await this.interactive(params.actions, {
609
+ headless: params.headless !== false, // Default true
610
+ agentId,
611
+ context
612
+ });
613
+ break;
614
+
615
+ default:
616
+ throw new Error(`Unknown operation: ${operation}`);
617
+ }
618
+
619
+ return {
620
+ success: true,
621
+ operation,
622
+ result,
623
+ toolUsed: 'web'
624
+ };
625
+
626
+ } catch (error) {
627
+ this.logger?.error('Web tool execution failed', {
628
+ operation,
629
+ error: error.message,
630
+ agentId
631
+ });
632
+
633
+ return {
634
+ success: false,
635
+ operation,
636
+ error: error.message,
637
+ toolUsed: 'web'
638
+ };
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Ensure browser is initialized
644
+ * @private
645
+ */
646
+ async ensureBrowser() {
647
+ if (this.browser && this.browser.isConnected()) {
648
+ return;
649
+ }
650
+
651
+ if (this.browserInitializing) {
652
+ // Wait for browser to finish initializing
653
+ while (this.browserInitializing) {
654
+ await new Promise(resolve => setTimeout(resolve, 100));
655
+ }
656
+ return;
657
+ }
658
+
659
+ this.browserInitializing = true;
660
+
661
+ try {
662
+ this.logger?.info('Initializing Puppeteer browser');
663
+
664
+ this.browser = await puppeteer.launch({
665
+ headless: 'new', // Use new headless mode
666
+ args: [
667
+ '--no-sandbox',
668
+ '--disable-setuid-sandbox',
669
+ '--disable-dev-shm-usage',
670
+ '--disable-accelerated-2d-canvas',
671
+ '--disable-gpu',
672
+ '--disable-blink-features=AutomationControlled',
673
+ '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
674
+ ]
675
+ });
676
+
677
+ this.logger?.info('Puppeteer browser initialized successfully');
678
+
679
+ } catch (error) {
680
+ this.logger?.error('Failed to initialize browser', { error: error.message });
681
+ throw new Error(`Browser initialization failed: ${error.message}`);
682
+ } finally {
683
+ this.browserInitializing = false;
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Search the web using a known search engine
689
+ * @param {string} query - Search query
690
+ * @param {Object} options - Search options
691
+ * @returns {Promise<Object>} Search results
692
+ */
693
+ async search(query, options = {}) {
694
+ const { engine = 'google', maxResults = 10, agentId } = options;
695
+
696
+ // Validate query
697
+ if (!query || typeof query !== 'string' || query.trim().length === 0) {
698
+ throw new Error('Search query is required and must be a non-empty string');
699
+ }
700
+
701
+ const searchEngine = this.searchEngines.find(e => e.name === engine);
702
+ if (!searchEngine) {
703
+ throw new Error(`Unknown search engine: ${engine}. Available: ${this.searchEngines.map(e => e.name).join(', ')}`);
704
+ }
705
+
706
+ // Ensure browser is initialized
707
+ await this.ensureBrowser();
708
+
709
+ this.logger?.info('Performing web search', { query, engine, agentId });
710
+
711
+ // Create temporary page for search
712
+ const page = await this.browser.newPage();
713
+
714
+ try {
715
+ // Navigate to search engine
716
+ const searchUrl = `${searchEngine.url}${encodeURIComponent(query)}`;
717
+ await page.goto(searchUrl, { waitUntil: 'networkidle2', timeout: this.DEFAULT_TIMEOUT });
718
+
719
+ // Wait for results
720
+ await page.waitForSelector(searchEngine.waitSelector, { timeout: this.DEFAULT_TIMEOUT });
721
+
722
+ // Extract results
723
+ const results = await page.evaluate((selector, max) => {
724
+ const links = Array.from(document.querySelectorAll(selector));
725
+ return links.slice(0, max).map(link => ({
726
+ url: link.href,
727
+ title: link.textContent.trim(),
728
+ description: link.closest('.g, .b_algo, .result')?.textContent.trim() || ''
729
+ })).filter(result => result.url && result.url.startsWith('http'));
730
+ }, searchEngine.resultsSelector, maxResults);
731
+
732
+ this.logger?.info('Search completed', { resultsCount: results.length, agentId });
733
+
734
+ return {
735
+ success: true,
736
+ query,
737
+ engine,
738
+ resultsCount: results.length,
739
+ results
740
+ };
741
+
742
+ } finally {
743
+ await page.close();
744
+ }
745
+ }
746
+
747
+ /**
748
+ * Fetch web content in various formats
749
+ * @param {string} url - URL to fetch
750
+ * @param {Object} options - Fetch options
751
+ * @returns {Promise<Object>} Fetched content
752
+ */
753
+ async fetch(url, options = {}) {
754
+ const { formats = ['title', 'text'], agentId } = options;
755
+
756
+ // Ensure browser is initialized
757
+ await this.ensureBrowser();
758
+
759
+ this.logger?.info('Fetching web content', { url, formats, agentId });
760
+
761
+ // Create temporary page
762
+ const page = await this.browser.newPage();
763
+
764
+ try {
765
+ // Listen for console messages if requested
766
+ const consoleMessages = [];
767
+ if (formats.includes('console')) {
768
+ page.on('console', msg => {
769
+ consoleMessages.push({
770
+ type: msg.type(),
771
+ text: msg.text()
772
+ });
773
+ });
774
+ }
775
+
776
+ // Navigate to URL
777
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: this.DEFAULT_TIMEOUT });
778
+
779
+ const result = { url };
780
+
781
+ // Extract requested formats
782
+ for (const format of formats) {
783
+ switch (format) {
784
+ case 'title':
785
+ result.title = await page.title();
786
+ break;
787
+
788
+ case 'text':
789
+ result.text = await page.evaluate(() => document.body.innerText);
790
+ break;
791
+
792
+ case 'links':
793
+ result.links = await page.evaluate(() => {
794
+ return Array.from(document.querySelectorAll('a[href]')).map(a => ({
795
+ href: a.href,
796
+ text: a.textContent.trim()
797
+ }));
798
+ });
799
+ break;
800
+
801
+ case 'html':
802
+ result.html = await page.content();
803
+ break;
804
+
805
+ case 'console':
806
+ result.consoleMessages = consoleMessages;
807
+ break;
808
+ }
809
+ }
810
+
811
+ this.logger?.info('Fetch completed', { url, formats, agentId });
812
+
813
+ return {
814
+ success: true,
815
+ ...result
816
+ };
817
+
818
+ } finally {
819
+ await page.close();
820
+ }
821
+ }
822
+
823
+ /**
824
+ * Interactive browser automation with command chaining
825
+ * @param {Array} actions - Array of actions to execute
826
+ * @param {Object} options - Options
827
+ * @returns {Promise<Object>} Results of all actions
828
+ */
829
+ async interactive(actions, options = {}) {
830
+ const { headless = true, agentId, context } = options;
831
+
832
+ // Ensure browser is initialized
833
+ await this.ensureBrowser();
834
+
835
+ this.logger?.info('Starting interactive session', {
836
+ actionsCount: actions.length,
837
+ headless,
838
+ agentId
839
+ });
840
+
841
+ const results = [];
842
+
843
+ // Initialize agent tabs if not exists
844
+ if (!this.agentTabs.has(agentId)) {
845
+ this.agentTabs.set(agentId, new Map());
846
+ }
847
+
848
+ const agentTabsMap = this.agentTabs.get(agentId);
849
+
850
+ for (const action of actions) {
851
+ try {
852
+ let actionResult;
853
+
854
+ switch (action.type) {
855
+ case 'open-tab':
856
+ actionResult = await this.openTab(agentId, action.name, action.url, headless, action.nestedActions, context);
857
+ break;
858
+
859
+ case 'close-tab':
860
+ actionResult = await this.closeTab(agentId, action.name);
861
+ break;
862
+
863
+ case 'switch-tab':
864
+ actionResult = await this.switchTab(agentId, action.name);
865
+ break;
866
+
867
+ case 'list-tabs':
868
+ actionResult = await this.listTabs(agentId);
869
+ break;
870
+
871
+ default:
872
+ // For actions that need a tab context, we need to specify which tab
873
+ // For now, we'll skip these at the top level
874
+ actionResult = {
875
+ success: false,
876
+ error: `Action ${action.type} must be executed within a tab context (use open-tab with nestedActions)`
877
+ };
878
+ }
879
+
880
+ results.push({
881
+ action: action.type,
882
+ ...actionResult
883
+ });
884
+
885
+ } catch (error) {
886
+ this.logger?.error('Action failed', {
887
+ action: action.type,
888
+ error: error.message,
889
+ agentId
890
+ });
891
+
892
+ results.push({
893
+ action: action.type,
894
+ success: false,
895
+ error: error.message
896
+ });
897
+ }
898
+ }
899
+
900
+ return {
901
+ success: results.every(r => r.success !== false),
902
+ actionsExecuted: results.length,
903
+ results
904
+ };
905
+ }
906
+
907
+ /**
908
+ * Open a new tab with nested actions
909
+ * @param {string} agentId - Agent identifier
910
+ * @param {string} tabName - Unique tab name
911
+ * @param {string} url - Initial URL
912
+ * @param {boolean} headless - Headless mode
913
+ * @param {Array} nestedActions - Actions to execute in this tab
914
+ * @param {Object} context - Execution context
915
+ * @returns {Promise<Object>} Result
916
+ */
917
+ async openTab(agentId, tabName, url, headless, nestedActions = [], context = {}) {
918
+ // Initialize agent tabs if not exists
919
+ if (!this.agentTabs.has(agentId)) {
920
+ this.agentTabs.set(agentId, new Map());
921
+ }
922
+
923
+ const agentTabsMap = this.agentTabs.get(agentId);
924
+
925
+ // Check if tab already exists
926
+ if (agentTabsMap.has(tabName)) {
927
+ throw new Error(`Tab '${tabName}' already exists for agent ${agentId}`);
928
+ }
929
+
930
+ this.logger?.info('Opening tab', { agentId, tabName, url, headless });
931
+
932
+ // Create new page
933
+ const page = await this.browser.newPage();
934
+
935
+ // Set viewport
936
+ await page.setViewport({ width: 1280, height: 720 });
937
+
938
+ // Track console messages
939
+ const consoleMessages = [];
940
+ page.on('console', msg => {
941
+ consoleMessages.push({
942
+ type: msg.type(),
943
+ text: msg.text(),
944
+ timestamp: Date.now()
945
+ });
946
+ });
947
+
948
+ // Store tab info
949
+ const tabInfo = {
950
+ page,
951
+ url,
952
+ lastActivity: Date.now(),
953
+ headless,
954
+ consoleMessages,
955
+ name: tabName
956
+ };
957
+
958
+ agentTabsMap.set(tabName, tabInfo);
959
+
960
+ const results = [];
961
+
962
+ try {
963
+ // Navigate to initial URL if provided
964
+ if (url) {
965
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: this.DEFAULT_TIMEOUT });
966
+ tabInfo.url = url;
967
+ tabInfo.lastActivity = Date.now();
968
+ }
969
+
970
+ // Execute nested actions
971
+ for (const action of nestedActions) {
972
+ const actionResult = await this.executeTabAction(page, action, tabInfo, context);
973
+ results.push({
974
+ action: action.type,
975
+ ...actionResult
976
+ });
977
+ tabInfo.lastActivity = Date.now();
978
+ }
979
+
980
+ return {
981
+ success: true,
982
+ tabName,
983
+ url: tabInfo.url,
984
+ actionsExecuted: results.length,
985
+ results
986
+ };
987
+
988
+ } catch (error) {
989
+ this.logger?.error('Failed to open tab', {
990
+ agentId,
991
+ tabName,
992
+ error: error.message
993
+ });
994
+
995
+ // Clean up on error
996
+ await page.close();
997
+ agentTabsMap.delete(tabName);
998
+
999
+ throw error;
1000
+ }
1001
+ }
1002
+
1003
+ /**
1004
+ * Execute an action in a tab context
1005
+ * @param {Page} page - Puppeteer page
1006
+ * @param {Object} action - Action to execute
1007
+ * @param {Object} tabInfo - Tab information
1008
+ * @param {Object} context - Execution context
1009
+ * @returns {Promise<Object>} Action result
1010
+ * @private
1011
+ */
1012
+ async executeTabAction(page, action, tabInfo, context) {
1013
+ switch (action.type) {
1014
+ case 'navigate':
1015
+ await page.goto(action.value || action.url, {
1016
+ waitUntil: 'networkidle2',
1017
+ timeout: this.DEFAULT_TIMEOUT
1018
+ });
1019
+ tabInfo.url = page.url();
1020
+ return { success: true, url: tabInfo.url };
1021
+
1022
+ case 'click':
1023
+ await page.click(action.selector, {
1024
+ button: action.button || 'left'
1025
+ });
1026
+ return { success: true, selector: action.selector };
1027
+
1028
+ case 'type':
1029
+ await page.type(action.selector, action.text || action.value);
1030
+ return { success: true, selector: action.selector, text: action.text };
1031
+
1032
+ case 'press':
1033
+ await page.keyboard.press(action.key || action.value);
1034
+ return { success: true, key: action.key };
1035
+
1036
+ case 'wait-for':
1037
+ const timeout = action.timeout ? parseInt(action.timeout, 10) : this.DEFAULT_TIMEOUT;
1038
+ await page.waitForSelector(action.selector, { timeout });
1039
+ return { success: true, selector: action.selector };
1040
+
1041
+ case 'screenshot':
1042
+ return await this.takeScreenshot(page, action, context);
1043
+
1044
+ case 'analyze-screenshot':
1045
+ return await this.analyzeScreenshot(page, action.value, context);
1046
+
1047
+ case 'extract-text':
1048
+ const text = await page.evaluate((sel) => {
1049
+ const element = document.querySelector(sel);
1050
+ return element ? element.innerText : null;
1051
+ }, action.selector);
1052
+ return { success: true, selector: action.selector, text };
1053
+
1054
+ case 'extract-links':
1055
+ const links = await page.evaluate((sel) => {
1056
+ const elements = document.querySelectorAll(sel);
1057
+ return Array.from(elements).map(a => ({
1058
+ href: a.href,
1059
+ text: a.textContent.trim()
1060
+ }));
1061
+ }, action.selector);
1062
+ return { success: true, selector: action.selector, links };
1063
+
1064
+ case 'get-source':
1065
+ const html = await page.content();
1066
+ return { success: true, html };
1067
+
1068
+ case 'get-console':
1069
+ return {
1070
+ success: true,
1071
+ consoleMessages: [...tabInfo.consoleMessages]
1072
+ };
1073
+
1074
+ case 'scroll':
1075
+ await page.evaluate((sel) => {
1076
+ if (sel) {
1077
+ document.querySelector(sel)?.scrollIntoView();
1078
+ } else {
1079
+ window.scrollTo(0, document.body.scrollHeight);
1080
+ }
1081
+ }, action.selector);
1082
+ return { success: true };
1083
+
1084
+ case 'hover':
1085
+ await page.hover(action.selector);
1086
+ return { success: true, selector: action.selector };
1087
+
1088
+ case 'mouse-move':
1089
+ await page.hover(action.selector);
1090
+ return { success: true, selector: action.selector };
1091
+
1092
+ default:
1093
+ throw new Error(`Unknown action type: ${action.type}`);
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * Take screenshot of page
1099
+ * @param {Page} page - Puppeteer page
1100
+ * @param {Object} options - Screenshot options
1101
+ * @param {Object} context - Execution context
1102
+ * @returns {Promise<Object>} Screenshot result
1103
+ * @private
1104
+ */
1105
+ async takeScreenshot(page, options, context) {
1106
+ const format = options.format || 'file';
1107
+ const screenshotPath = options.path;
1108
+
1109
+ if (format === 'base64') {
1110
+ const screenshot = await page.screenshot({ encoding: 'base64' });
1111
+ return {
1112
+ success: true,
1113
+ format: 'base64',
1114
+ screenshot
1115
+ };
1116
+ }
1117
+
1118
+ // File format
1119
+ let filePath;
1120
+
1121
+ if (screenshotPath) {
1122
+ // Save to project directory if path is provided
1123
+ const projectDir = context.directoryAccess?.workingDirectory || context.projectDir || process.cwd();
1124
+ filePath = path.isAbsolute(screenshotPath)
1125
+ ? screenshotPath
1126
+ : path.join(projectDir, screenshotPath);
1127
+ } else {
1128
+ // Save to temp directory
1129
+ const filename = `screenshot-${Date.now()}.png`;
1130
+ filePath = path.join(this.TEMP_DIR, filename);
1131
+ }
1132
+
1133
+ await page.screenshot({ path: filePath });
1134
+
1135
+ return {
1136
+ success: true,
1137
+ format: 'file',
1138
+ path: filePath
1139
+ };
1140
+ }
1141
+
1142
+ /**
1143
+ * Analyze screenshot using AI vision model
1144
+ * @param {Page} page - Puppeteer page
1145
+ * @param {string} question - Question for AI
1146
+ * @param {Object} context - Execution context
1147
+ * @returns {Promise<Object>} Analysis result
1148
+ * @private
1149
+ */
1150
+ async analyzeScreenshot(page, question, context) {
1151
+ // Take screenshot as base64
1152
+ const screenshot = await page.screenshot({ encoding: 'base64' });
1153
+
1154
+ // Get AI service from context
1155
+ const aiService = context.aiService;
1156
+ if (!aiService) {
1157
+ throw new Error('AI service not available for screenshot analysis');
1158
+ }
1159
+
1160
+ this.logger?.info('Analyzing screenshot with AI', {
1161
+ question: question.substring(0, 100),
1162
+ agentId: context.agentId
1163
+ });
1164
+
1165
+ try {
1166
+ // Use vision model (prefer o3 if available, fallback to gpt-4-vision)
1167
+ const model = 'o3'; // Will be mapped by AI service
1168
+
1169
+ // Create message with image
1170
+ const response = await aiService.sendMessage(
1171
+ model,
1172
+ question,
1173
+ {
1174
+ agentId: context.agentId,
1175
+ images: [`data:image/png;base64,${screenshot}`],
1176
+ apiKey: context.apiKey,
1177
+ customApiKeys: context.customApiKeys,
1178
+ platformProvided: context.platformProvided
1179
+ }
1180
+ );
1181
+
1182
+ return {
1183
+ success: true,
1184
+ question,
1185
+ analysis: response.content,
1186
+ model: response.model || model
1187
+ };
1188
+
1189
+ } catch (error) {
1190
+ this.logger?.error('Screenshot analysis failed', {
1191
+ error: error.message,
1192
+ agentId: context.agentId
1193
+ });
1194
+
1195
+ throw new Error(`Screenshot analysis failed: ${error.message}`);
1196
+ }
1197
+ }
1198
+
1199
+ /**
1200
+ * Close a tab
1201
+ * @param {string} agentId - Agent identifier
1202
+ * @param {string} tabName - Tab name to close
1203
+ * @returns {Promise<Object>} Result
1204
+ */
1205
+ async closeTab(agentId, tabName) {
1206
+ const agentTabsMap = this.agentTabs.get(agentId);
1207
+ if (!agentTabsMap || !agentTabsMap.has(tabName)) {
1208
+ throw new Error(`Tab '${tabName}' not found for agent ${agentId}`);
1209
+ }
1210
+
1211
+ const tabInfo = agentTabsMap.get(tabName);
1212
+
1213
+ this.logger?.info('Closing tab', { agentId, tabName });
1214
+
1215
+ await tabInfo.page.close();
1216
+ agentTabsMap.delete(tabName);
1217
+
1218
+ return {
1219
+ success: true,
1220
+ tabName,
1221
+ message: `Tab '${tabName}' closed`
1222
+ };
1223
+ }
1224
+
1225
+ /**
1226
+ * Switch to an existing tab
1227
+ * @param {string} agentId - Agent identifier
1228
+ * @param {string} tabName - Tab name to switch to
1229
+ * @returns {Promise<Object>} Result
1230
+ */
1231
+ async switchTab(agentId, tabName) {
1232
+ const agentTabsMap = this.agentTabs.get(agentId);
1233
+ if (!agentTabsMap || !agentTabsMap.has(tabName)) {
1234
+ throw new Error(`Tab '${tabName}' not found for agent ${agentId}`);
1235
+ }
1236
+
1237
+ const tabInfo = agentTabsMap.get(tabName);
1238
+ tabInfo.lastActivity = Date.now();
1239
+
1240
+ return {
1241
+ success: true,
1242
+ tabName,
1243
+ url: tabInfo.url,
1244
+ message: `Switched to tab '${tabName}'`
1245
+ };
1246
+ }
1247
+
1248
+ /**
1249
+ * List all active tabs for an agent
1250
+ * @param {string} agentId - Agent identifier
1251
+ * @returns {Promise<Object>} List of tabs
1252
+ */
1253
+ async listTabs(agentId) {
1254
+ const agentTabsMap = this.agentTabs.get(agentId);
1255
+
1256
+ if (!agentTabsMap || agentTabsMap.size === 0) {
1257
+ return {
1258
+ success: true,
1259
+ tabCount: 0,
1260
+ tabs: [],
1261
+ message: 'No active tabs'
1262
+ };
1263
+ }
1264
+
1265
+ const tabs = [];
1266
+ for (const [name, info] of agentTabsMap.entries()) {
1267
+ tabs.push({
1268
+ name,
1269
+ url: info.url,
1270
+ idleTime: Date.now() - info.lastActivity,
1271
+ headless: info.headless
1272
+ });
1273
+ }
1274
+
1275
+ return {
1276
+ success: true,
1277
+ tabCount: tabs.length,
1278
+ tabs
1279
+ };
1280
+ }
1281
+
1282
+ /**
1283
+ * Start cleanup timer for idle tabs
1284
+ * @private
1285
+ */
1286
+ startCleanupTimer() {
1287
+ if (this.cleanupTimer) {
1288
+ clearInterval(this.cleanupTimer);
1289
+ }
1290
+
1291
+ this.cleanupTimer = setInterval(() => {
1292
+ this.cleanupIdleTabs();
1293
+ }, this.CLEANUP_INTERVAL);
1294
+ }
1295
+
1296
+ /**
1297
+ * Cleanup idle tabs (1-hour timeout)
1298
+ * @private
1299
+ */
1300
+ async cleanupIdleTabs() {
1301
+ const now = Date.now();
1302
+ const tabsToClose = [];
1303
+
1304
+ for (const [agentId, agentTabsMap] of this.agentTabs.entries()) {
1305
+ for (const [tabName, tabInfo] of agentTabsMap.entries()) {
1306
+ const idleTime = now - tabInfo.lastActivity;
1307
+
1308
+ if (idleTime > this.TAB_IDLE_TIMEOUT) {
1309
+ tabsToClose.push({ agentId, tabName, tabInfo });
1310
+ }
1311
+ }
1312
+ }
1313
+
1314
+ if (tabsToClose.length > 0) {
1315
+ this.logger?.info('Cleaning up idle tabs', {
1316
+ count: tabsToClose.length
1317
+ });
1318
+
1319
+ for (const { agentId, tabName, tabInfo } of tabsToClose) {
1320
+ try {
1321
+ await tabInfo.page.close();
1322
+ this.agentTabs.get(agentId).delete(tabName);
1323
+ this.logger?.debug('Closed idle tab', { agentId, tabName });
1324
+ } catch (error) {
1325
+ this.logger?.error('Failed to close idle tab', {
1326
+ agentId,
1327
+ tabName,
1328
+ error: error.message
1329
+ });
1330
+ }
1331
+ }
1332
+ }
1333
+ }
1334
+
1335
+ /**
1336
+ * Cleanup all tabs for an agent (called when agent is deleted)
1337
+ * @param {string} agentId - Agent identifier
1338
+ * @returns {Promise<Object>} Cleanup result
1339
+ */
1340
+ async cleanupAgent(agentId) {
1341
+ const agentTabsMap = this.agentTabs.get(agentId);
1342
+
1343
+ if (!agentTabsMap) {
1344
+ return {
1345
+ success: true,
1346
+ agentId,
1347
+ closedTabs: 0,
1348
+ message: 'No tabs to clean up'
1349
+ };
1350
+ }
1351
+
1352
+ this.logger?.info('Cleaning up agent tabs', {
1353
+ agentId,
1354
+ tabCount: agentTabsMap.size
1355
+ });
1356
+
1357
+ let closedCount = 0;
1358
+
1359
+ for (const [tabName, tabInfo] of agentTabsMap.entries()) {
1360
+ try {
1361
+ await tabInfo.page.close();
1362
+ closedCount++;
1363
+ } catch (error) {
1364
+ this.logger?.error('Failed to close tab during cleanup', {
1365
+ agentId,
1366
+ tabName,
1367
+ error: error.message
1368
+ });
1369
+ }
1370
+ }
1371
+
1372
+ this.agentTabs.delete(agentId);
1373
+
1374
+ return {
1375
+ success: true,
1376
+ agentId,
1377
+ closedTabs: closedCount,
1378
+ message: `Closed ${closedCount} tabs for agent ${agentId}`
1379
+ };
1380
+ }
1381
+
1382
+ /**
1383
+ * Ensure temp directory exists
1384
+ * @private
1385
+ */
1386
+ async ensureTempDir() {
1387
+ try {
1388
+ await fs.mkdir(this.TEMP_DIR, { recursive: true });
1389
+ } catch (error) {
1390
+ this.logger?.warn('Failed to create temp directory', {
1391
+ path: this.TEMP_DIR,
1392
+ error: error.message
1393
+ });
1394
+ }
1395
+ }
1396
+
1397
+ /**
1398
+ * Cleanup resources
1399
+ */
1400
+ async cleanup() {
1401
+ // Stop cleanup timer
1402
+ if (this.cleanupTimer) {
1403
+ clearInterval(this.cleanupTimer);
1404
+ this.cleanupTimer = null;
1405
+ }
1406
+
1407
+ // Close all tabs
1408
+ for (const [agentId] of this.agentTabs.entries()) {
1409
+ await this.cleanupAgent(agentId);
1410
+ }
1411
+
1412
+ // Close browser
1413
+ if (this.browser) {
1414
+ await this.browser.close();
1415
+ this.browser = null;
1416
+ }
1417
+
1418
+ // Clean temp directory
1419
+ try {
1420
+ await fs.rm(this.TEMP_DIR, { recursive: true, force: true });
1421
+ } catch (error) {
1422
+ this.logger?.warn('Failed to clean temp directory', {
1423
+ path: this.TEMP_DIR,
1424
+ error: error.message
1425
+ });
1426
+ }
1427
+ }
1428
+ }
1429
+
1430
+ export default WebTool;