@darbotlabs/darbot-browser-mcp 0.1.1 → 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.
- package/LICENSE +1 -1
- package/README.md +249 -158
- package/cli.js +1 -1
- package/config.d.ts +77 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/ai/context.js +150 -0
- package/lib/ai/guardrails.js +382 -0
- package/lib/ai/integration.js +397 -0
- package/lib/ai/intent.js +237 -0
- package/lib/ai/manualPromise.js +111 -0
- package/lib/ai/memory.js +273 -0
- package/lib/ai/ml-scorer.js +265 -0
- package/lib/ai/orchestrator-tools.js +292 -0
- package/lib/ai/orchestrator.js +473 -0
- package/lib/ai/planner.js +300 -0
- package/lib/ai/reporter.js +493 -0
- package/lib/ai/workflow.js +407 -0
- package/lib/auth/apiKeyAuth.js +46 -0
- package/lib/auth/entraAuth.js +110 -0
- package/lib/auth/entraJwtVerifier.js +117 -0
- package/lib/auth/index.js +210 -0
- package/lib/auth/managedIdentityAuth.js +175 -0
- package/lib/auth/mcpOAuthProvider.js +186 -0
- package/lib/auth/tunnelAuth.js +120 -0
- package/lib/browserContextFactory.js +1 -1
- package/lib/browserServer.js +1 -1
- package/lib/cdpRelay.js +2 -2
- package/lib/common.js +68 -0
- package/lib/config.js +62 -3
- package/lib/connection.js +1 -1
- package/lib/context.js +1 -1
- package/lib/fileUtils.js +1 -1
- package/lib/guardrails.js +382 -0
- package/lib/health.js +178 -0
- package/lib/httpServer.js +1 -1
- package/lib/index.js +1 -1
- package/lib/javascript.js +1 -1
- package/lib/manualPromise.js +1 -1
- package/lib/memory.js +273 -0
- package/lib/openapi.js +373 -0
- package/lib/orchestrator.js +473 -0
- package/lib/package.js +1 -1
- package/lib/pageSnapshot.js +17 -2
- package/lib/planner.js +302 -0
- package/lib/program.js +17 -5
- package/lib/reporter.js +493 -0
- package/lib/resources/resource.js +1 -1
- package/lib/server.js +5 -3
- package/lib/tab.js +1 -1
- package/lib/tools/ai-native.js +298 -0
- package/lib/tools/autonomous.js +147 -0
- package/lib/tools/clock.js +183 -0
- package/lib/tools/common.js +1 -1
- package/lib/tools/console.js +1 -1
- package/lib/tools/diagnostics.js +132 -0
- package/lib/tools/dialogs.js +1 -1
- package/lib/tools/emulation.js +155 -0
- package/lib/tools/files.js +1 -1
- package/lib/tools/install.js +1 -1
- package/lib/tools/keyboard.js +1 -1
- package/lib/tools/navigate.js +1 -1
- package/lib/tools/network.js +1 -1
- package/lib/tools/pageSnapshot.js +58 -0
- package/lib/tools/pdf.js +1 -1
- package/lib/tools/profiles.js +76 -25
- package/lib/tools/screenshot.js +1 -1
- package/lib/tools/scroll.js +93 -0
- package/lib/tools/snapshot.js +1 -1
- package/lib/tools/storage.js +328 -0
- package/lib/tools/tab.js +16 -0
- package/lib/tools/tabs.js +1 -1
- package/lib/tools/testing.js +1 -1
- package/lib/tools/tool.js +1 -1
- package/lib/tools/utils.js +1 -1
- package/lib/tools/vision.js +1 -1
- package/lib/tools/wait.js +1 -1
- package/lib/tools.js +22 -1
- package/lib/transport.js +251 -31
- package/package.json +54 -21
|
@@ -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();
|
package/lib/ai/intent.js
ADDED
|
@@ -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();
|