@darbotlabs/darbot-browser-mcp 0.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +222 -161
  3. package/cli.js +1 -1
  4. package/config.d.ts +77 -1
  5. package/index.d.ts +1 -1
  6. package/index.js +1 -1
  7. package/lib/ai/context.js +150 -0
  8. package/lib/ai/guardrails.js +382 -0
  9. package/lib/ai/integration.js +397 -0
  10. package/lib/ai/intent.js +237 -0
  11. package/lib/ai/manualPromise.js +111 -0
  12. package/lib/ai/memory.js +273 -0
  13. package/lib/ai/ml-scorer.js +265 -0
  14. package/lib/ai/orchestrator-tools.js +292 -0
  15. package/lib/ai/orchestrator.js +473 -0
  16. package/lib/ai/planner.js +300 -0
  17. package/lib/ai/reporter.js +493 -0
  18. package/lib/ai/workflow.js +407 -0
  19. package/lib/auth/apiKeyAuth.js +46 -0
  20. package/lib/auth/entraAuth.js +110 -0
  21. package/lib/auth/entraJwtVerifier.js +117 -0
  22. package/lib/auth/index.js +210 -0
  23. package/lib/auth/managedIdentityAuth.js +175 -0
  24. package/lib/auth/mcpOAuthProvider.js +186 -0
  25. package/lib/auth/tunnelAuth.js +120 -0
  26. package/lib/browserContextFactory.js +1 -1
  27. package/lib/browserServer.js +1 -1
  28. package/lib/cdpRelay.js +2 -2
  29. package/lib/common.js +68 -0
  30. package/lib/config.js +62 -3
  31. package/lib/connection.js +1 -1
  32. package/lib/context.js +1 -1
  33. package/lib/fileUtils.js +1 -1
  34. package/lib/guardrails.js +382 -0
  35. package/lib/health.js +178 -0
  36. package/lib/httpServer.js +1 -1
  37. package/lib/index.js +1 -1
  38. package/lib/javascript.js +1 -1
  39. package/lib/manualPromise.js +1 -1
  40. package/lib/memory.js +273 -0
  41. package/lib/openapi.js +373 -0
  42. package/lib/orchestrator.js +473 -0
  43. package/lib/package.js +1 -1
  44. package/lib/pageSnapshot.js +17 -2
  45. package/lib/planner.js +302 -0
  46. package/lib/program.js +17 -5
  47. package/lib/reporter.js +493 -0
  48. package/lib/resources/resource.js +1 -1
  49. package/lib/server.js +5 -3
  50. package/lib/tab.js +1 -1
  51. package/lib/tools/ai-native.js +298 -0
  52. package/lib/tools/autonomous.js +147 -0
  53. package/lib/tools/clock.js +183 -0
  54. package/lib/tools/common.js +1 -1
  55. package/lib/tools/console.js +1 -1
  56. package/lib/tools/diagnostics.js +132 -0
  57. package/lib/tools/dialogs.js +1 -1
  58. package/lib/tools/emulation.js +155 -0
  59. package/lib/tools/files.js +1 -1
  60. package/lib/tools/install.js +1 -1
  61. package/lib/tools/keyboard.js +1 -1
  62. package/lib/tools/navigate.js +1 -1
  63. package/lib/tools/network.js +1 -1
  64. package/lib/tools/pageSnapshot.js +58 -0
  65. package/lib/tools/pdf.js +1 -1
  66. package/lib/tools/profiles.js +76 -25
  67. package/lib/tools/screenshot.js +1 -1
  68. package/lib/tools/scroll.js +93 -0
  69. package/lib/tools/snapshot.js +1 -1
  70. package/lib/tools/storage.js +328 -0
  71. package/lib/tools/tab.js +16 -0
  72. package/lib/tools/tabs.js +1 -1
  73. package/lib/tools/testing.js +1 -1
  74. package/lib/tools/tool.js +1 -1
  75. package/lib/tools/utils.js +1 -1
  76. package/lib/tools/vision.js +1 -1
  77. package/lib/tools/wait.js +1 -1
  78. package/lib/tools.js +22 -1
  79. package/lib/transport.js +251 -31
  80. package/package.json +28 -22
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Copyright (c) DarbotLabs.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * AI Tool Integration - Executes parsed intents using real browser automation tools
18
+ */
19
+ export class AIToolIntegration {
20
+ /**
21
+ * Execute a parsed intent by calling the appropriate browser automation tool
22
+ */
23
+ async executeIntent(context, intent) {
24
+ try {
25
+ switch (intent.action) {
26
+ case 'navigate':
27
+ return await this.executeNavigate(context, intent);
28
+ case 'click':
29
+ return await this.executeClick(context, intent);
30
+ case 'type':
31
+ return await this.executeType(context, intent);
32
+ case 'submit_form':
33
+ return await this.executeSubmitForm(context, intent);
34
+ case 'search':
35
+ return await this.executeSearch(context, intent);
36
+ case 'github_create_issue':
37
+ return await this.executeGitHubCreateIssue(context, intent);
38
+ case 'github_review_pr':
39
+ return await this.executeGitHubReviewPR(context, intent);
40
+ case 'login':
41
+ return await this.executeLogin(context, intent);
42
+ case 'wait_for':
43
+ return await this.executeWaitFor(context, intent);
44
+ case 'screenshot':
45
+ return await this.executeScreenshot(context, intent);
46
+ default:
47
+ return await this.executeGeneric(context, intent);
48
+ }
49
+ }
50
+ catch (error) {
51
+ // Return error result instead of throwing
52
+ return {
53
+ success: false,
54
+ action: intent.action,
55
+ target: intent.parameters.url || intent.parameters.element,
56
+ error: error instanceof Error ? error.message : 'Unknown error',
57
+ };
58
+ }
59
+ }
60
+ /**
61
+ * Execute recovery strategy when primary intent fails
62
+ */
63
+ async executeRecovery(context, intent, primaryError) {
64
+ const strategy = intent.fallbackStrategy || 'analyze_page_context';
65
+ try {
66
+ switch (strategy) {
67
+ case 'search_for_url':
68
+ return await this.recoverSearchForUrl(context, intent);
69
+ case 'auto_detect_clickable_elements':
70
+ return await this.recoverAutoDetectClickable(context, intent);
71
+ case 'auto_detect_input_fields':
72
+ return await this.recoverAutoDetectInput(context, intent);
73
+ case 'find_submit_buttons':
74
+ return await this.recoverFindSubmitButton(context, intent);
75
+ case 'find_search_input':
76
+ return await this.recoverFindSearchInput(context, intent);
77
+ case 'analyze_page_context':
78
+ return await this.recoverAnalyzeContext(context, intent);
79
+ default:
80
+ throw new Error(`Unknown recovery strategy: ${strategy}`);
81
+ }
82
+ }
83
+ catch (recoveryError) {
84
+ return {
85
+ success: false,
86
+ action: intent.action,
87
+ error: `Primary error: ${primaryError.message}. Recovery failed: ${recoveryError instanceof Error ? recoveryError.message : 'Unknown error'}`,
88
+ recoveryUsed: true,
89
+ recoveryStrategy: strategy,
90
+ };
91
+ }
92
+ }
93
+ // ==================== Action Implementations ====================
94
+ async executeNavigate(context, intent) {
95
+ const tab = await context.ensureTab();
96
+ const url = intent.parameters.url;
97
+ await tab.navigate(url);
98
+ return {
99
+ success: true,
100
+ action: 'navigate',
101
+ target: url,
102
+ };
103
+ }
104
+ async executeClick(context, intent) {
105
+ const tab = context.currentTabOrDie();
106
+ const element = intent.parameters.element;
107
+ // Find element by accessibility text or role
108
+ const locator = await this.findElementByDescription(tab, element);
109
+ await locator.click();
110
+ return {
111
+ success: true,
112
+ action: 'click',
113
+ target: element,
114
+ };
115
+ }
116
+ async executeType(context, intent) {
117
+ const tab = context.currentTabOrDie();
118
+ const text = intent.parameters.text;
119
+ const element = intent.parameters.element;
120
+ // Find input element
121
+ const locator = await this.findInputByDescription(tab, element);
122
+ await locator.fill(text);
123
+ return {
124
+ success: true,
125
+ action: 'type',
126
+ target: element,
127
+ result: { text },
128
+ };
129
+ }
130
+ async executeSubmitForm(context, intent) {
131
+ const tab = context.currentTabOrDie();
132
+ // Find and click submit button, or press Enter on focused element
133
+ try {
134
+ const submitButton = tab.page.locator('button[type="submit"], input[type="submit"]').first();
135
+ await submitButton.click();
136
+ }
137
+ catch {
138
+ // Fallback: press Enter
139
+ await tab.page.keyboard.press('Enter');
140
+ }
141
+ return {
142
+ success: true,
143
+ action: 'submit_form',
144
+ };
145
+ }
146
+ async executeSearch(context, intent) {
147
+ const tab = await context.ensureTab();
148
+ const query = intent.parameters.query;
149
+ // Navigate to Google search
150
+ const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
151
+ await tab.navigate(searchUrl);
152
+ return {
153
+ success: true,
154
+ action: 'search',
155
+ target: query,
156
+ result: { url: searchUrl },
157
+ };
158
+ }
159
+ async executeGitHubCreateIssue(context, intent) {
160
+ const tab = context.currentTabOrDie();
161
+ const currentUrl = tab.page.url();
162
+ // Extract repository from current URL if on GitHub
163
+ if (!currentUrl.includes('github.com'))
164
+ throw new Error('Not on GitHub. Navigate to a repository first.');
165
+ // Navigate to issues page
166
+ const repoMatch = currentUrl.match(/github\.com\/([^/]+\/[^/]+)/);
167
+ if (!repoMatch)
168
+ throw new Error('Could not determine repository from URL');
169
+ const issuesUrl = `https://github.com/${repoMatch[1]}/issues/new`;
170
+ await tab.navigate(issuesUrl);
171
+ return {
172
+ success: true,
173
+ action: 'github_create_issue',
174
+ target: issuesUrl,
175
+ };
176
+ }
177
+ async executeGitHubReviewPR(context, intent) {
178
+ const tab = context.currentTabOrDie();
179
+ const currentUrl = tab.page.url();
180
+ if (!currentUrl.includes('github.com'))
181
+ throw new Error('Not on GitHub. Navigate to a repository first.');
182
+ // Navigate to pull requests
183
+ const repoMatch = currentUrl.match(/github\.com\/([^/]+\/[^/]+)/);
184
+ if (!repoMatch)
185
+ throw new Error('Could not determine repository from URL');
186
+ const prsUrl = `https://github.com/${repoMatch[1]}/pulls`;
187
+ await tab.navigate(prsUrl);
188
+ return {
189
+ success: true,
190
+ action: 'github_review_pr',
191
+ target: prsUrl,
192
+ };
193
+ }
194
+ async executeLogin(context, intent) {
195
+ const tab = context.currentTabOrDie();
196
+ // Detect login form on current page
197
+ const loginInputs = tab.page.locator('input[type="email"], input[type="text"], input[name*="user"], input[name*="email"]');
198
+ const passwordInputs = tab.page.locator('input[type="password"]');
199
+ const loginCount = await loginInputs.count();
200
+ const passwordCount = await passwordInputs.count();
201
+ if (loginCount === 0 || passwordCount === 0)
202
+ throw new Error('No login form detected on current page');
203
+ return {
204
+ success: true,
205
+ action: 'login',
206
+ target: 'login_form_detected',
207
+ result: {
208
+ message: 'Login form detected. Use browser_type tool to enter credentials.',
209
+ },
210
+ };
211
+ }
212
+ async executeWaitFor(context, intent) {
213
+ const tab = context.currentTabOrDie();
214
+ const target = intent.parameters.target;
215
+ // Wait for element or text to appear
216
+ try {
217
+ // Try as text content first
218
+ await tab.page.waitForSelector(`:text("${target}")`, { timeout: 10000 });
219
+ }
220
+ catch {
221
+ // Try as selector
222
+ await tab.page.waitForSelector(target, { timeout: 10000 });
223
+ }
224
+ return {
225
+ success: true,
226
+ action: 'wait_for',
227
+ target,
228
+ };
229
+ }
230
+ async executeScreenshot(context, intent) {
231
+ const tab = context.currentTabOrDie();
232
+ const timestamp = Date.now();
233
+ const filename = `screenshot-${timestamp}.png`;
234
+ await tab.page.screenshot({ path: filename, fullPage: true });
235
+ return {
236
+ success: true,
237
+ action: 'screenshot',
238
+ result: { filename, path: filename },
239
+ };
240
+ }
241
+ async executeGeneric(context, intent) {
242
+ throw new Error(`Action '${intent.action}' is not yet implemented in the integration layer`);
243
+ }
244
+ // ==================== Recovery Strategies ====================
245
+ async recoverSearchForUrl(context, intent) {
246
+ // If direct navigation failed, try Google search
247
+ const searchQuery = intent.parameters.url || intent.parameters.element;
248
+ const searchIntent = {
249
+ action: 'search',
250
+ parameters: { query: searchQuery },
251
+ confidence: 0.6,
252
+ fallbackStrategy: 'analyze_page_context',
253
+ };
254
+ const result = await this.executeSearch(context, searchIntent);
255
+ result.recoveryUsed = true;
256
+ result.recoveryStrategy = 'search_for_url';
257
+ return result;
258
+ }
259
+ async recoverAutoDetectClickable(context, intent) {
260
+ const tab = context.currentTabOrDie();
261
+ // Find all clickable elements
262
+ const clickableElements = await tab.page.locator('button, a, [role="button"], [role="link"], input[type="submit"]').all();
263
+ if (clickableElements.length === 0)
264
+ throw new Error('No clickable elements found on page');
265
+ // Click the first visible one
266
+ await clickableElements[0].click();
267
+ return {
268
+ success: true,
269
+ action: 'click',
270
+ target: 'auto_detected_element',
271
+ recoveryUsed: true,
272
+ recoveryStrategy: 'auto_detect_clickable_elements',
273
+ };
274
+ }
275
+ async recoverAutoDetectInput(context, intent) {
276
+ const tab = context.currentTabOrDie();
277
+ // Find visible input fields
278
+ const inputs = await tab.page.locator('input[type="text"], input[type="email"], input[type="search"], textarea').all();
279
+ if (inputs.length === 0)
280
+ throw new Error('No input fields found on page');
281
+ // Fill the first visible one
282
+ const text = intent.parameters.text || '';
283
+ await inputs[0].fill(text);
284
+ return {
285
+ success: true,
286
+ action: 'type',
287
+ target: 'auto_detected_input',
288
+ recoveryUsed: true,
289
+ recoveryStrategy: 'auto_detect_input_fields',
290
+ };
291
+ }
292
+ async recoverFindSubmitButton(context, intent) {
293
+ const tab = context.currentTabOrDie();
294
+ // Find submit buttons by various selectors
295
+ const submitButton = tab.page.locator('button[type="submit"], input[type="submit"], button:has-text("Submit"), button:has-text("Send")').first();
296
+ await submitButton.click();
297
+ return {
298
+ success: true,
299
+ action: 'submit_form',
300
+ target: 'auto_detected_submit',
301
+ recoveryUsed: true,
302
+ recoveryStrategy: 'find_submit_buttons',
303
+ };
304
+ }
305
+ async recoverFindSearchInput(context, intent) {
306
+ const tab = context.currentTabOrDie();
307
+ // Find search input by name, placeholder, or type
308
+ const searchInput = tab.page.locator('input[type="search"], input[name*="search"], input[placeholder*="search" i]').first();
309
+ const query = intent.parameters.query || '';
310
+ await searchInput.fill(query);
311
+ await searchInput.press('Enter');
312
+ return {
313
+ success: true,
314
+ action: 'search',
315
+ target: 'auto_detected_search_input',
316
+ recoveryUsed: true,
317
+ recoveryStrategy: 'find_search_input',
318
+ };
319
+ }
320
+ async recoverAnalyzeContext(context, intent) {
321
+ const tab = context.currentTabOrDie();
322
+ // Capture page snapshot for intelligent recovery
323
+ const title = await tab.page.title();
324
+ const url = tab.page.url();
325
+ // Provide context-aware suggestions
326
+ const suggestions = [];
327
+ if (url.includes('github.com'))
328
+ suggestions.push('Try navigating to issues or pull requests');
329
+ if (title.toLowerCase().includes('login'))
330
+ suggestions.push('Page appears to be a login form');
331
+ throw new Error(`Could not execute action. Suggestions: ${suggestions.join(', ') || 'None'}`);
332
+ }
333
+ // ==================== Helper Methods ====================
334
+ /**
335
+ * Find element by natural language description
336
+ */
337
+ async findElementByDescription(tab, description) {
338
+ // Try various strategies to find element
339
+ const strategies = [
340
+ // 1. Exact text match
341
+ tab.page.locator(`:text("${description}")`),
342
+ // 2. Partial text match (case insensitive)
343
+ tab.page.locator(`:text-is("${description}")`),
344
+ // 3. Button with text
345
+ tab.page.locator(`button:has-text("${description}")`),
346
+ // 4. Link with text
347
+ tab.page.locator(`a:has-text("${description}")`),
348
+ // 5. Accessible name
349
+ tab.page.locator(`[aria-label="${description}"]`),
350
+ // 6. Placeholder
351
+ tab.page.locator(`[placeholder="${description}"]`),
352
+ // 7. Title attribute
353
+ tab.page.locator(`[title="${description}"]`),
354
+ ];
355
+ // Try each strategy
356
+ for (const locator of strategies) {
357
+ const count = await locator.count();
358
+ if (count > 0)
359
+ return locator.first();
360
+ }
361
+ // If all strategies fail, throw error
362
+ throw new Error(`Could not find element matching: ${description}`);
363
+ }
364
+ /**
365
+ * Find input field by natural language description
366
+ */
367
+ async findInputByDescription(tab, description) {
368
+ // Try various input-specific strategies
369
+ const strategies = [
370
+ // 1. Label text
371
+ tab.page.locator(`input:near(:text("${description}"))`),
372
+ // 2. Placeholder
373
+ tab.page.locator(`input[placeholder*="${description}" i]`),
374
+ // 3. Name attribute
375
+ tab.page.locator(`input[name*="${description}" i]`),
376
+ // 4. Accessible label
377
+ tab.page.locator(`input[aria-label*="${description}" i]`),
378
+ // 5. ID
379
+ tab.page.locator(`input[id*="${description}" i]`),
380
+ // 6. Any input (fallback)
381
+ tab.page.locator('input[type="text"], input[type="email"], input[type="search"], textarea').first(),
382
+ ];
383
+ for (const locator of strategies) {
384
+ try {
385
+ const count = await locator.count();
386
+ if (count > 0)
387
+ return locator.first();
388
+ }
389
+ catch {
390
+ continue;
391
+ }
392
+ }
393
+ throw new Error(`Could not find input field matching: ${description}`);
394
+ }
395
+ }
396
+ // Global integration instance
397
+ export const aiToolIntegration = new AIToolIntegration();
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Copyright (c) DarbotLabs.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Intent parser for natural language automation commands
18
+ */
19
+ export class IntentParser {
20
+ patterns = [
21
+ // Navigation intents
22
+ {
23
+ pattern: /(?:go to|navigate to|visit|open)\s+(.+)/i,
24
+ action: 'navigate',
25
+ parameterExtractors: {
26
+ url: match => this.normalizeUrl(match[1].trim()),
27
+ },
28
+ confidence: 0.9,
29
+ },
30
+ // Form submission intents
31
+ {
32
+ pattern: /(?:submit|send|complete)\s+(?:the\s+)?form/i,
33
+ action: 'submit_form',
34
+ parameterExtractors: {},
35
+ confidence: 0.85,
36
+ },
37
+ // Click intents
38
+ {
39
+ pattern: /(?:click|press|tap)\s+(?:on\s+)?(?:the\s+)?(.+?)(?:\s+button|\s+link)?$/i,
40
+ action: 'click',
41
+ parameterExtractors: {
42
+ element: match => match[1].trim(),
43
+ },
44
+ confidence: 0.8,
45
+ },
46
+ // Text input intents
47
+ {
48
+ pattern: /(?:type|enter|input|fill)\s+['""](.+?)['""]\s+(?:in|into)\s+(?:the\s+)?(.+)/i,
49
+ action: 'type',
50
+ parameterExtractors: {
51
+ text: match => match[1],
52
+ element: match => match[2].trim(),
53
+ },
54
+ confidence: 0.85,
55
+ },
56
+ // Search intents
57
+ {
58
+ pattern: /search\s+for\s+['""]?(.+?)['""]?/i,
59
+ action: 'search',
60
+ parameterExtractors: {
61
+ query: match => match[1].trim(),
62
+ },
63
+ confidence: 0.8,
64
+ },
65
+ // GitHub-specific intents
66
+ {
67
+ pattern: /(?:create|open)\s+(?:a\s+)?(?:new\s+)?(?:github\s+)?issue/i,
68
+ action: 'github_create_issue',
69
+ parameterExtractors: {},
70
+ confidence: 0.9,
71
+ },
72
+ {
73
+ pattern: /(?:review|check)\s+(?:the\s+)?(?:github\s+)?(?:pull\s+)?(?:request|pr)/i,
74
+ action: 'github_review_pr',
75
+ parameterExtractors: {},
76
+ confidence: 0.9,
77
+ },
78
+ // Login intents
79
+ {
80
+ pattern: /(?:log\s*in|sign\s*in|login)\s+(?:to\s+)?(.+)/i,
81
+ action: 'login',
82
+ parameterExtractors: {
83
+ service: match => match[1].trim(),
84
+ },
85
+ confidence: 0.85,
86
+ },
87
+ // Wait intents
88
+ {
89
+ pattern: /wait\s+(?:for\s+)?(?:the\s+)?(.+?)(?:\s+to\s+(?:appear|load|show))?/i,
90
+ action: 'wait_for',
91
+ parameterExtractors: {
92
+ target: match => match[1].trim(),
93
+ },
94
+ confidence: 0.8,
95
+ },
96
+ // Screenshot intents
97
+ {
98
+ pattern: /(?:take|capture)\s+(?:a\s+)?screenshot/i,
99
+ action: 'screenshot',
100
+ parameterExtractors: {},
101
+ confidence: 0.9,
102
+ },
103
+ ];
104
+ /**
105
+ * Parse natural language intent into structured action
106
+ */
107
+ parseIntent(description) {
108
+ const normalizedDescription = description.trim();
109
+ for (const pattern of this.patterns) {
110
+ const match = normalizedDescription.match(pattern.pattern);
111
+ if (match) {
112
+ const parameters = {};
113
+ // Extract parameters using pattern extractors
114
+ for (const [key, extractor] of Object.entries(pattern.parameterExtractors)) {
115
+ try {
116
+ parameters[key] = extractor(match);
117
+ }
118
+ catch (error) {
119
+ // Skip parameter if extraction fails
120
+ continue;
121
+ }
122
+ }
123
+ return {
124
+ action: pattern.action,
125
+ parameters,
126
+ confidence: pattern.confidence,
127
+ fallbackStrategy: this.getFallbackStrategy(pattern.action),
128
+ };
129
+ }
130
+ }
131
+ // Fallback to generic action detection
132
+ return this.parseGenericIntent(normalizedDescription);
133
+ }
134
+ /**
135
+ * Parse generic intents that don't match specific patterns
136
+ */
137
+ parseGenericIntent(description) {
138
+ const lowercaseDesc = description.toLowerCase();
139
+ // Common action keywords
140
+ if (lowercaseDesc.includes('click') || lowercaseDesc.includes('press')) {
141
+ return {
142
+ action: 'click',
143
+ parameters: { element: description },
144
+ confidence: 0.6,
145
+ fallbackStrategy: 'auto_detect_clickable_elements',
146
+ };
147
+ }
148
+ if (lowercaseDesc.includes('type') || lowercaseDesc.includes('enter')) {
149
+ return {
150
+ action: 'type',
151
+ parameters: { text: description },
152
+ confidence: 0.5,
153
+ fallbackStrategy: 'auto_detect_input_fields',
154
+ };
155
+ }
156
+ if (lowercaseDesc.includes('navigate') || lowercaseDesc.includes('go')) {
157
+ return {
158
+ action: 'navigate',
159
+ parameters: { url: description },
160
+ confidence: 0.5,
161
+ fallbackStrategy: 'search_for_url',
162
+ };
163
+ }
164
+ // Default to generic interaction
165
+ return {
166
+ action: 'interact',
167
+ parameters: { description },
168
+ confidence: 0.3,
169
+ fallbackStrategy: 'analyze_page_context',
170
+ };
171
+ }
172
+ /**
173
+ * Get fallback strategy for action type
174
+ */
175
+ getFallbackStrategy(action) {
176
+ const strategies = {
177
+ 'navigate': 'search_for_url',
178
+ 'click': 'auto_detect_clickable_elements',
179
+ 'type': 'auto_detect_input_fields',
180
+ 'submit_form': 'find_submit_buttons',
181
+ 'search': 'find_search_input',
182
+ 'github_create_issue': 'navigate_to_github_issues',
183
+ 'github_review_pr': 'navigate_to_github_prs',
184
+ 'login': 'find_login_form',
185
+ 'wait_for': 'intelligent_wait',
186
+ 'screenshot': 'capture_full_page',
187
+ };
188
+ return strategies[action] || 'analyze_page_context';
189
+ }
190
+ /**
191
+ * Normalize URL for navigation
192
+ */
193
+ normalizeUrl(url) {
194
+ // Remove quotes and trim
195
+ const cleaned = url.replace(/['"]/g, '').trim();
196
+ // Add protocol if missing
197
+ if (!cleaned.startsWith('http://') && !cleaned.startsWith('https://')) {
198
+ // Check if it looks like a domain
199
+ if (cleaned.includes('.') && !cleaned.includes(' '))
200
+ return `https://${cleaned}`;
201
+ // Otherwise treat as search query
202
+ return `https://www.google.com/search?q=${encodeURIComponent(cleaned)}`;
203
+ }
204
+ return cleaned;
205
+ }
206
+ /**
207
+ * Enhance intent with context information
208
+ */
209
+ enhanceWithContext(intent, context) {
210
+ // Add context-aware enhancements
211
+ const enhanced = { ...intent };
212
+ // Enhance based on current page context
213
+ if (context.pageIntent)
214
+ enhanced.parameters.pageContext = context.pageIntent;
215
+ // Adjust confidence based on context
216
+ if (context.successfulActions) {
217
+ const similarActions = context.successfulActions.filter((action) => action.action === intent.action);
218
+ if (similarActions.length > 0)
219
+ enhanced.confidence = Math.min(enhanced.confidence + 0.1, 1.0);
220
+ }
221
+ return enhanced;
222
+ }
223
+ /**
224
+ * Extract workflow intentions from complex descriptions
225
+ */
226
+ parseWorkflowIntent(description) {
227
+ // Split complex descriptions into steps
228
+ const stepSeparators = /(?:then|next|after that|and then|,\s*)/i;
229
+ const steps = description.split(stepSeparators).map(s => s.trim()).filter(s => s.length > 0);
230
+ if (steps.length <= 1)
231
+ return [this.parseIntent(description)];
232
+ // Parse each step as individual intent
233
+ return steps.map(step => this.parseIntent(step));
234
+ }
235
+ }
236
+ // Global intent parser instance
237
+ export const intentParser = new IntentParser();