explorbot 0.1.9 → 0.1.10
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/bin/explorbot-cli.ts +70 -8
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +70 -7
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +1 -1
- package/dist/src/ai/bosun.js +5 -1
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian.js +13 -6
- package/dist/src/ai/navigator.js +62 -62
- package/dist/src/ai/pilot.js +22 -0
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +4 -4
- package/dist/src/ai/provider.js +1 -1
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +4 -11
- package/dist/src/ai/tools.js +5 -3
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +1 -1
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +1 -1
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +28 -5
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +2 -2
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +11 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +1 -1
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/experience-tracker.js +191 -56
- package/dist/src/explorbot.js +26 -14
- package/dist/src/explorer.js +3 -3
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/utils/error-page.js +10 -0
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +1 -1
- package/src/ai/bosun.ts +5 -1
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian.ts +12 -7
- package/src/ai/navigator.ts +68 -66
- package/src/ai/pilot.ts +22 -0
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +5 -5
- package/src/ai/provider.ts +1 -1
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +4 -11
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tools.ts +6 -4
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +2 -2
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +2 -2
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +30 -7
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +2 -2
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +12 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +2 -2
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/experience-tracker.ts +220 -71
- package/src/explorbot.ts +28 -15
- package/src/explorer.ts +3 -3
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/utils/error-page.ts +10 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
package/dist/src/ai/navigator.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../action-result.js';
|
|
3
|
-
import { ExperienceTracker } from '../experience-tracker.js';
|
|
3
|
+
import { ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
|
|
4
4
|
import { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
5
5
|
import { normalizeUrl } from '../state-manager.js';
|
|
6
6
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
7
7
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
8
8
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
9
9
|
import { loop, pause } from '../utils/loop.js';
|
|
10
|
+
import { RulesLoader } from "../utils/rules-loader.js";
|
|
10
11
|
import { Researcher } from "./researcher.js";
|
|
11
12
|
import { actionRule, locatorRule } from './rules.js';
|
|
12
|
-
import { RulesLoader } from "../utils/rules-loader.js";
|
|
13
13
|
import { isInteractive } from './task-agent.js';
|
|
14
|
+
import { createAgentTools } from "./tools.js";
|
|
14
15
|
const debugLog = createDebug('explorbot:navigator');
|
|
15
16
|
class Navigator {
|
|
16
17
|
emoji = '🧭';
|
|
@@ -18,8 +19,6 @@ class Navigator {
|
|
|
18
19
|
experienceCompactor;
|
|
19
20
|
knowledgeTracker;
|
|
20
21
|
experienceTracker;
|
|
21
|
-
currentAction = null;
|
|
22
|
-
currentUrl = null;
|
|
23
22
|
hooksRunner;
|
|
24
23
|
MAX_ATTEMPTS = Number.parseInt(process.env.MAX_ATTEMPTS || '5');
|
|
25
24
|
systemPrompt = dedent `
|
|
@@ -85,9 +84,7 @@ class Navigator {
|
|
|
85
84
|
const actualPath = action.stateManager.getCurrentState()?.url || '';
|
|
86
85
|
const actionResult = action.actionResult || ActionResult.fromState(action.stateManager.getCurrentState());
|
|
87
86
|
const originalMessage = `Navigate to: ${url}. Current page: ${actualPath}`;
|
|
88
|
-
this.
|
|
89
|
-
this.currentUrl = url;
|
|
90
|
-
const resolved = await this.resolveState(originalMessage, actionResult);
|
|
87
|
+
const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
|
|
91
88
|
if (!resolved) {
|
|
92
89
|
throw new Error(`Navigation to ${url} failed: redirected to ${actualPath} and could not resolve`);
|
|
93
90
|
}
|
|
@@ -99,9 +96,7 @@ class Navigator {
|
|
|
99
96
|
And I expected to see the URL in the browser
|
|
100
97
|
But I got error: ${action.lastError?.message || 'Navigation failed'}.
|
|
101
98
|
`.trim();
|
|
102
|
-
this.
|
|
103
|
-
this.currentUrl = url;
|
|
104
|
-
const resolved = await this.resolveState(originalMessage, actionResult);
|
|
99
|
+
const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
|
|
105
100
|
if (!resolved) {
|
|
106
101
|
throw new Error(`Navigation to ${url} failed: ${action.lastError?.message}`);
|
|
107
102
|
}
|
|
@@ -119,9 +114,11 @@ class Navigator {
|
|
|
119
114
|
throw error;
|
|
120
115
|
}
|
|
121
116
|
}
|
|
122
|
-
async resolveState(message, actionResult) {
|
|
117
|
+
async resolveState(message, actionResult, opts) {
|
|
123
118
|
tag('info').log('AI Navigator resolving state at', actionResult.url);
|
|
124
119
|
debugLog('Resolution message:', message);
|
|
120
|
+
const action = opts?.action ?? this.explorer.createAction();
|
|
121
|
+
const expectedUrl = opts?.expectedUrl;
|
|
125
122
|
let knowledge = '';
|
|
126
123
|
let experience = '';
|
|
127
124
|
const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
|
|
@@ -133,24 +130,13 @@ class Navigator {
|
|
|
133
130
|
${knowledgeContent}
|
|
134
131
|
</hint>`;
|
|
135
132
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Here is the experience of interacting with the page.
|
|
144
|
-
Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
|
|
145
|
-
If there was found successful solution to an issue, propose it as a first solution.
|
|
146
|
-
If there are no successful solutions, analyze failed intentions and actions to avoid them.
|
|
147
|
-
Do not try again same failed solutions
|
|
148
|
-
|
|
149
|
-
Focus on successful solutions and avoid actions and locators that caused errors in past.
|
|
150
|
-
|
|
151
|
-
${experienceContent}
|
|
152
|
-
|
|
153
|
-
</experience>`;
|
|
133
|
+
if (!actionResult.isInsideIframe) {
|
|
134
|
+
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
135
|
+
if (toc.length > 0) {
|
|
136
|
+
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
137
|
+
tag('substep').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
|
|
138
|
+
experience = renderExperienceToc(toc);
|
|
139
|
+
}
|
|
154
140
|
}
|
|
155
141
|
const prompt = dedent `
|
|
156
142
|
<message>
|
|
@@ -185,6 +171,7 @@ class Navigator {
|
|
|
185
171
|
`;
|
|
186
172
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
187
173
|
conversation.addUserText(prompt);
|
|
174
|
+
const tools = this.buildExperienceTools();
|
|
188
175
|
let codeBlocks = [];
|
|
189
176
|
let htmlContextAdded = false;
|
|
190
177
|
let codeBlockIndex = 0;
|
|
@@ -192,12 +179,12 @@ class Navigator {
|
|
|
192
179
|
let resolved = false;
|
|
193
180
|
await loop(async ({ stop }) => {
|
|
194
181
|
if (codeBlocks.length === 0) {
|
|
195
|
-
const result = await this.provider.invokeConversation(conversation);
|
|
182
|
+
const result = await this.provider.invokeConversation(conversation, tools);
|
|
196
183
|
if (!result)
|
|
197
184
|
return;
|
|
198
185
|
const aiResponse = result?.response?.text;
|
|
199
186
|
debugLog('AI:', aiResponse?.split('\n')[0]);
|
|
200
|
-
debugLog('Received AI response:', aiResponse
|
|
187
|
+
debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
|
|
201
188
|
codeBlocks = extractCodeBlocks(aiResponse ?? '');
|
|
202
189
|
codeBlockIndex = 0;
|
|
203
190
|
}
|
|
@@ -227,22 +214,23 @@ class Navigator {
|
|
|
227
214
|
}
|
|
228
215
|
codeBlockIndex++;
|
|
229
216
|
totalAttempts++;
|
|
217
|
+
await this.explorer.switchToMainFrame();
|
|
230
218
|
debugLog(`Attempting resolution: ${codeBlock}`);
|
|
231
|
-
resolved = await
|
|
232
|
-
if (
|
|
233
|
-
await
|
|
234
|
-
const freshState = await
|
|
235
|
-
if (normalizeUrl(freshState.url || '') === normalizeUrl(
|
|
219
|
+
resolved = await action.attempt(codeBlock, message);
|
|
220
|
+
if (expectedUrl) {
|
|
221
|
+
await action.getActor().wait(2);
|
|
222
|
+
const freshState = await action.capturePageState();
|
|
223
|
+
if (normalizeUrl(freshState.url || '') === normalizeUrl(expectedUrl)) {
|
|
236
224
|
resolved = true;
|
|
237
225
|
}
|
|
238
226
|
else if (resolved) {
|
|
239
|
-
tag('warning').log(`URL verification failed: expected ${
|
|
227
|
+
tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${freshState.url}`);
|
|
240
228
|
resolved = false;
|
|
241
229
|
}
|
|
242
230
|
}
|
|
243
231
|
if (resolved) {
|
|
244
232
|
tag('success').log('Navigation resolved successfully');
|
|
245
|
-
|
|
233
|
+
this.experienceTracker.writeAction(actionResult, { title: message, code: codeBlock });
|
|
246
234
|
stop();
|
|
247
235
|
return;
|
|
248
236
|
}
|
|
@@ -256,9 +244,9 @@ class Navigator {
|
|
|
256
244
|
resolved = false;
|
|
257
245
|
},
|
|
258
246
|
});
|
|
259
|
-
if (!resolved &&
|
|
260
|
-
await
|
|
261
|
-
if (this.isOnExpectedPage(
|
|
247
|
+
if (!resolved && expectedUrl) {
|
|
248
|
+
await action.getActor().wait(1);
|
|
249
|
+
if (this.isOnExpectedPage(expectedUrl, action.stateManager)) {
|
|
262
250
|
resolved = true;
|
|
263
251
|
tag('success').log('Navigation resolved after delayed redirect');
|
|
264
252
|
}
|
|
@@ -267,12 +255,12 @@ class Navigator {
|
|
|
267
255
|
tag('error').log(`Navigation failed after ${totalAttempts} attempts`);
|
|
268
256
|
}
|
|
269
257
|
if (!resolved && isInteractive()) {
|
|
270
|
-
const userInput = await pause(`Navigator failed to resolve. Current: ${
|
|
258
|
+
const userInput = await pause(`Navigator failed to resolve. Current: ${action.stateManager.getCurrentState()?.url}\n` + `Target: ${expectedUrl ?? '(none)'}\nEnter CodeceptJS commands (or press Enter to skip):`);
|
|
271
259
|
if (userInput?.trim()) {
|
|
272
|
-
resolved = await
|
|
273
|
-
if (resolved &&
|
|
274
|
-
await
|
|
275
|
-
if (!this.isOnExpectedPage(
|
|
260
|
+
resolved = await action.attempt(userInput, message);
|
|
261
|
+
if (resolved && expectedUrl) {
|
|
262
|
+
await action.getActor().wait(1);
|
|
263
|
+
if (!this.isOnExpectedPage(expectedUrl, action.stateManager)) {
|
|
276
264
|
resolved = false;
|
|
277
265
|
}
|
|
278
266
|
}
|
|
@@ -280,6 +268,23 @@ class Navigator {
|
|
|
280
268
|
}
|
|
281
269
|
return resolved;
|
|
282
270
|
}
|
|
271
|
+
buildExperienceTools() {
|
|
272
|
+
const stateManager = this.explorer.getStateManager();
|
|
273
|
+
const getState = () => {
|
|
274
|
+
const s = stateManager.getCurrentState();
|
|
275
|
+
return s ? ActionResult.fromState(s) : null;
|
|
276
|
+
};
|
|
277
|
+
const { learn_experience } = createAgentTools({
|
|
278
|
+
explorer: this.explorer,
|
|
279
|
+
researcher: null,
|
|
280
|
+
navigator: this,
|
|
281
|
+
experienceTracker: this.experienceTracker,
|
|
282
|
+
getState,
|
|
283
|
+
});
|
|
284
|
+
if (!learn_experience)
|
|
285
|
+
return undefined;
|
|
286
|
+
return { learn_experience };
|
|
287
|
+
}
|
|
283
288
|
async freeSail(opts, actionResult) {
|
|
284
289
|
const stateManager = this.explorer.getStateManager();
|
|
285
290
|
const state = stateManager.getCurrentState();
|
|
@@ -403,20 +408,13 @@ class Navigator {
|
|
|
403
408
|
${knowledgeContent}
|
|
404
409
|
</hint>`;
|
|
405
410
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
Here is the experience of interacting with the page.
|
|
414
|
-
Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
|
|
415
|
-
If there was found successful solution to an issue, propose it as a first solution.
|
|
416
|
-
|
|
417
|
-
${experienceContent}
|
|
418
|
-
|
|
419
|
-
</experience>`;
|
|
411
|
+
if (!actionResult.isInsideIframe) {
|
|
412
|
+
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
413
|
+
if (toc.length > 0) {
|
|
414
|
+
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
415
|
+
tag('substep').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
|
|
416
|
+
experience = renderExperienceToc(toc);
|
|
417
|
+
}
|
|
420
418
|
}
|
|
421
419
|
const prompt = dedent `
|
|
422
420
|
<message>
|
|
@@ -454,16 +452,17 @@ class Navigator {
|
|
|
454
452
|
tag('debug').log('Prompt:', prompt);
|
|
455
453
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
456
454
|
conversation.addUserText(prompt);
|
|
455
|
+
const tools = this.buildExperienceTools();
|
|
457
456
|
let codeBlocks = [];
|
|
458
457
|
const successfulCodes = [];
|
|
459
458
|
const action = this.explorer.createAction();
|
|
460
459
|
await loop(async ({ stop, iteration }) => {
|
|
461
460
|
if (codeBlocks.length === 0) {
|
|
462
|
-
const result = await this.provider.invokeConversation(conversation);
|
|
461
|
+
const result = await this.provider.invokeConversation(conversation, tools);
|
|
463
462
|
if (!result)
|
|
464
463
|
return;
|
|
465
464
|
const aiResponse = result?.response?.text;
|
|
466
|
-
debugLog('Received AI response:', aiResponse
|
|
465
|
+
debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
|
|
467
466
|
tag('step').log('Verifying assertion...');
|
|
468
467
|
codeBlocks = extractCodeBlocks(aiResponse ?? '');
|
|
469
468
|
}
|
|
@@ -475,6 +474,7 @@ class Navigator {
|
|
|
475
474
|
stop();
|
|
476
475
|
return;
|
|
477
476
|
}
|
|
477
|
+
await this.explorer.switchToMainFrame();
|
|
478
478
|
const verified = await action.attempt(codeBlock, message, false);
|
|
479
479
|
if (verified) {
|
|
480
480
|
tag('success').log('Verification passed');
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -418,6 +418,28 @@ export class Pilot {
|
|
|
418
418
|
const verifyLines = verifications.map(([a, v]) => `${v ? 'PASS' : 'FAIL'}: ${a}`);
|
|
419
419
|
lines.push(`verifications: ${verifyLines.join(', ')}`);
|
|
420
420
|
}
|
|
421
|
+
const consoleErrors = (state.browserLogs ?? []).filter((l) => (l.type || l.level) === 'error');
|
|
422
|
+
if (consoleErrors.length > 0) {
|
|
423
|
+
const sample = consoleErrors
|
|
424
|
+
.slice(0, 3)
|
|
425
|
+
.map((e) => e.text || e.message || String(e))
|
|
426
|
+
.join(' | ');
|
|
427
|
+
lines.push(`console errors: ${consoleErrors.length} (${sample})`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
lines.push('console errors: none');
|
|
431
|
+
}
|
|
432
|
+
const failedRequests = this.explorer.getRequestStore()?.getFailedRequests() ?? [];
|
|
433
|
+
if (failedRequests.length > 0) {
|
|
434
|
+
const sample = failedRequests
|
|
435
|
+
.slice(-5)
|
|
436
|
+
.map((r) => `${r.method} ${r.path} → ${r.status}`)
|
|
437
|
+
.join(', ');
|
|
438
|
+
lines.push(`network errors: ${sample}`);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
lines.push('network errors: none');
|
|
442
|
+
}
|
|
421
443
|
const interactiveNodes = collectInteractiveNodes(state.ariaSnapshot);
|
|
422
444
|
const disabledButtons = interactiveNodes.filter((n) => n.role === 'button' && n.disabled === true && n.name).map((n) => n.name);
|
|
423
445
|
lines.push(`disabled buttons: ${disabledButtons.length > 0 ? disabledButtons.join(', ') : 'none'}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { ConfigParser } from "../../config.js";
|
|
4
3
|
import { normalizeUrl } from "../../state-manager.js";
|
|
4
|
+
import { isDynamicSegment } from "../../utils/url-matcher.js";
|
|
5
5
|
const planRegistry = new Map();
|
|
6
6
|
export function registerPlan(url, plan, feature, stateHash) {
|
|
7
7
|
const key = buildKey(url, feature);
|
|
@@ -27,35 +27,6 @@ function buildKey(url, feature) {
|
|
|
27
27
|
return `${normalized}::${feature}`;
|
|
28
28
|
return normalized;
|
|
29
29
|
}
|
|
30
|
-
export function isDynamicSegment(segment) {
|
|
31
|
-
try {
|
|
32
|
-
const configRegex = ConfigParser.getInstance().getConfig().dynamicPageRegex;
|
|
33
|
-
if (configRegex)
|
|
34
|
-
return new RegExp(configRegex, 'i').test(segment);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
/* config not loaded yet */
|
|
38
|
-
}
|
|
39
|
-
// numeric: /users/123
|
|
40
|
-
if (/^\d+$/.test(segment))
|
|
41
|
-
return true;
|
|
42
|
-
// UUID: /items/550e8400-e29b-41d4-a716-446655440000
|
|
43
|
-
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment))
|
|
44
|
-
return true;
|
|
45
|
-
// ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
46
|
-
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment))
|
|
47
|
-
return true;
|
|
48
|
-
// hex ID (4+ chars): /suite/70dae98a
|
|
49
|
-
if (/^[a-f0-9]{4,}$/i.test(segment))
|
|
50
|
-
return true;
|
|
51
|
-
// hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
|
|
52
|
-
if (/^[a-f0-9]{8,}-/i.test(segment))
|
|
53
|
-
return true;
|
|
54
|
-
// short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
|
|
55
|
-
if (segment.length <= 8 && !segment.includes('-') && /\d/.test(segment) && /[a-z]/i.test(segment))
|
|
56
|
-
return true;
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
30
|
export function isTemplateMatch(urlA, urlB) {
|
|
60
31
|
const partsA = normalizeUrl(urlA).split('/');
|
|
61
32
|
const partsB = normalizeUrl(urlB).split('/');
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -6,19 +6,19 @@ import { ConfigParser } from "../config.js";
|
|
|
6
6
|
import { ExperienceTracker } from "../experience-tracker.js";
|
|
7
7
|
import { Observability } from "../observability.js";
|
|
8
8
|
import { Stats } from "../stats.js";
|
|
9
|
+
import { Suite } from "../suite.js";
|
|
9
10
|
import { Plan, Test } from "../test-plan.js";
|
|
10
|
-
import { planToCompactAiContext } from "../utils/test-plan-markdown.js";
|
|
11
11
|
import { createDebug, tag } from '../utils/logger.js';
|
|
12
12
|
import { jsonToTable } from "../utils/markdown-parser.js";
|
|
13
13
|
import { mdq } from '../utils/markdown-query.js';
|
|
14
|
+
import { planToCompactAiContext } from "../utils/test-plan-markdown.js";
|
|
14
15
|
import { Conversation } from "./conversation.js";
|
|
15
|
-
import { getActiveStyle, getStyles } from "./planner/styles.js";
|
|
16
16
|
import { WithSessionDedup } from "./planner/session-dedup.js";
|
|
17
|
+
import { getActiveStyle, getStyles } from "./planner/styles.js";
|
|
17
18
|
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from "./planner/subpages.js";
|
|
19
|
+
import { POSSIBLE_SECTIONS, Researcher } from "./researcher.js";
|
|
18
20
|
import { findSimilarStateHash } from "./researcher/cache.js";
|
|
19
21
|
import { hasFocusedSection } from "./researcher/focus.js";
|
|
20
|
-
import { POSSIBLE_SECTIONS, Researcher } from "./researcher.js";
|
|
21
|
-
import { Suite } from "../suite.js";
|
|
22
22
|
import { fileUploadRule, protectionRule } from "./rules.js";
|
|
23
23
|
const debugLog = createDebug('explorbot:planner');
|
|
24
24
|
const TasksSchema = z.object({
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -2,12 +2,12 @@ import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
|
2
2
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
3
3
|
import { generateObject, generateText } from 'ai';
|
|
4
4
|
import { clearActivity, setActivity } from "../activity.js";
|
|
5
|
-
import { RulesLoader } from "../utils/rules-loader.js";
|
|
6
5
|
import { executionController } from "../execution-controller.js";
|
|
7
6
|
import { Observability } from "../observability.js";
|
|
8
7
|
import { Stats } from "../stats.js";
|
|
9
8
|
import { createDebug, tag } from '../utils/logger.js';
|
|
10
9
|
import { withRetry } from '../utils/retry.js';
|
|
10
|
+
import { RulesLoader } from "../utils/rules-loader.js";
|
|
11
11
|
import { Conversation } from './conversation.js';
|
|
12
12
|
const debugLog = createDebug('explorbot:provider');
|
|
13
13
|
const promptLog = createDebug('explorbot:provider:out');
|
package/dist/src/ai/rerunner.js
CHANGED
|
@@ -7,8 +7,8 @@ import { highlight } from 'cli-highlight';
|
|
|
7
7
|
import * as codeceptjs from 'codeceptjs';
|
|
8
8
|
import heal from 'codeceptjs/lib/heal';
|
|
9
9
|
import aiTracePlugin from 'codeceptjs/lib/plugin/aiTrace';
|
|
10
|
-
import figureSet from 'figures';
|
|
11
10
|
import dedent from 'dedent';
|
|
11
|
+
import figureSet from 'figures';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { ActionResult } from "../action-result.js";
|
|
14
14
|
import { setActivity } from "../activity.js";
|
|
@@ -16,11 +16,11 @@ import { Stats } from "../stats.js";
|
|
|
16
16
|
import { Task, Test, TestResult } from "../test-plan.js";
|
|
17
17
|
import { createDebug, tag } from "../utils/logger.js";
|
|
18
18
|
import { loop } from "../utils/loop.js";
|
|
19
|
+
import { RulesLoader } from "../utils/rules-loader.js";
|
|
19
20
|
import { loadTestSuites, printTestList } from "../utils/test-files.js";
|
|
20
21
|
import { toolExecutionLabel } from "./conversation.js";
|
|
21
|
-
import {
|
|
22
|
+
import { actionRule, locatorRule, sectionContextRule } from "./rules.js";
|
|
22
23
|
import { TaskAgent } from "./task-agent.js";
|
|
23
|
-
import { RulesLoader } from "../utils/rules-loader.js";
|
|
24
24
|
import { createCodeceptJSTools } from "./tools.js";
|
|
25
25
|
const debugLog = createDebug('explorbot:rerunner');
|
|
26
26
|
export class Rerunner extends TaskAgent {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../../action-result.js';
|
|
3
|
-
import { detectFocusArea, diffAriaSnapshots } from "../../utils/aria.js";
|
|
4
3
|
import { executionController } from "../../execution-controller.js";
|
|
4
|
+
import { detectFocusArea, diffAriaSnapshots } from "../../utils/aria.js";
|
|
5
5
|
import { tag } from '../../utils/logger.js';
|
|
6
6
|
import { mdq } from "../../utils/markdown-query.js";
|
|
7
7
|
import { getCachedResearch, saveResearch } from "./cache.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parentPort } from 'node:worker_threads';
|
|
4
4
|
import { computeHtmlFingerprint } from "../../utils/html-diff.js";
|
|
@@ -4,8 +4,8 @@ import { parseAriaLocator } from "../../utils/aria.js";
|
|
|
4
4
|
import { tag } from '../../utils/logger.js';
|
|
5
5
|
import { mdq } from "../../utils/markdown-query.js";
|
|
6
6
|
import { WebElement } from "../../utils/web-element.js";
|
|
7
|
-
import { FOCUSED_MARKER } from "./focus.js";
|
|
8
7
|
import { locatorRule as generalLocatorRuleText } from '../rules.js';
|
|
8
|
+
import { FOCUSED_MARKER } from "./focus.js";
|
|
9
9
|
import { debugLog } from "./mixin.js";
|
|
10
10
|
import { parseResearchSections } from "./parser.js";
|
|
11
11
|
function firstCssSegment(css) {
|
|
@@ -22,7 +22,14 @@ export function WithSections(Base) {
|
|
|
22
22
|
for (const [name, description] of targets) {
|
|
23
23
|
if (executionController.isInterrupted())
|
|
24
24
|
break;
|
|
25
|
-
|
|
25
|
+
let text = '';
|
|
26
|
+
try {
|
|
27
|
+
text = await this._researchSingleSection(name, description, ariaSnapshot, focusCss);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
tag('warning').log(`Section "${name}" research failed, skipping: ${err instanceof Error ? err.message : err}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
26
33
|
if (!text)
|
|
27
34
|
continue;
|
|
28
35
|
const trimmed = text.trim();
|
|
@@ -2,16 +2,17 @@ import dedent from 'dedent';
|
|
|
2
2
|
import { ActionResult } from '../action-result.js';
|
|
3
3
|
import { setActivity } from "../activity.js";
|
|
4
4
|
import { outputPath } from "../config.js";
|
|
5
|
+
import { executionController } from "../execution-controller.js";
|
|
5
6
|
import { Observability } from "../observability.js";
|
|
6
7
|
import { Stats } from "../stats.js";
|
|
7
8
|
import { diffAriaSnapshots } from "../utils/aria.js";
|
|
8
|
-
import { isErrorPage } from "../utils/error-page.js";
|
|
9
|
+
import { ErrorPageError, isErrorPage } from "../utils/error-page.js";
|
|
9
10
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
10
11
|
import { isBodyEmpty } from "../utils/html.js";
|
|
11
12
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
12
13
|
import { mdq } from "../utils/markdown-query.js";
|
|
13
14
|
import { withRetry } from "../utils/retry.js";
|
|
14
|
-
import {
|
|
15
|
+
import { RulesLoader } from "../utils/rules-loader.js";
|
|
15
16
|
import { ContextLengthError } from './provider.js';
|
|
16
17
|
import { findSimilarResearch, getCachedResearch, saveResearch } from "./researcher/cache.js";
|
|
17
18
|
import { WithCoordinates } from "./researcher/coordinates.js";
|
|
@@ -22,7 +23,6 @@ import { extractValidContainers, formatResearchSummary, parseResearchSections }
|
|
|
22
23
|
import { ResearchResult } from "./researcher/research-result.js";
|
|
23
24
|
import { WithSections } from "./researcher/sections.js";
|
|
24
25
|
import { locatorRule as generalLocatorRuleText } from './rules.js';
|
|
25
|
-
import { RulesLoader } from "../utils/rules-loader.js";
|
|
26
26
|
import { TaskAgent } from "./task-agent.js";
|
|
27
27
|
const debugLog = createDebug('explorbot:researcher');
|
|
28
28
|
export const POSSIBLE_SECTIONS = {
|
|
@@ -102,14 +102,7 @@ export class Researcher extends ResearcherBase {
|
|
|
102
102
|
const recovered = await this.waitForPageLoad(screenshot);
|
|
103
103
|
if (!recovered) {
|
|
104
104
|
tag('warning').log(`Detected error page at ${state.url}`);
|
|
105
|
-
|
|
106
|
-
## Error Page Detected
|
|
107
|
-
|
|
108
|
-
URL: ${state.url}
|
|
109
|
-
Title: ${this.actionResult.title || 'N/A'}
|
|
110
|
-
|
|
111
|
-
Research skipped. Navigate to a valid page to continue.
|
|
112
|
-
`;
|
|
105
|
+
throw new ErrorPageError(state.url, this.actionResult.title);
|
|
113
106
|
}
|
|
114
107
|
}
|
|
115
108
|
debugLog('Researching web page:', this.actionResult.url);
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -252,14 +252,16 @@ export function createCodeceptJSTools(explorer, task) {
|
|
|
252
252
|
form: tool({
|
|
253
253
|
description: dedent `
|
|
254
254
|
Execute raw CodeceptJS code block with multiple commands.
|
|
255
|
-
USE THIS TOOL for
|
|
255
|
+
USE THIS TOOL for typing text into fields: I.fillField, I.type
|
|
256
256
|
|
|
257
257
|
Follow <actions> from system prompt for available commands.
|
|
258
258
|
Follow <locator_priority> from system prompt for locator selection.
|
|
259
259
|
|
|
260
|
+
I.type(text) types the literal characters of its argument into the focused element.
|
|
261
|
+
To press key combination or special keys (Ctrl, Meta, Esc) use I.pressKey instead.
|
|
262
|
+
|
|
260
263
|
Use cases:
|
|
261
264
|
- Typing into input fields (I.fillField, I.type)
|
|
262
|
-
- Pressing keyboard keys (I.pressKey)
|
|
263
265
|
- Working with iframes (switch context with I.switchTo)
|
|
264
266
|
- Performing multiple form actions in a single batch
|
|
265
267
|
- Complex interactions requiring sequential commands
|
|
@@ -834,7 +836,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
|
|
|
834
836
|
tools.learn_experience = tool({
|
|
835
837
|
description: dedent `
|
|
836
838
|
Read the full body of a specific experience section listed in <experience>.
|
|
837
|
-
The TOC shows entries like "A.1 ##
|
|
839
|
+
The TOC shows entries like "A.1 ## FLOW: ..." or "A.2 ## ACTION: ...". Pass the fileTag and sectionIndex.
|
|
838
840
|
Only call when a TOC entry looks directly relevant to the current step.
|
|
839
841
|
`,
|
|
840
842
|
inputSchema: z.object({
|
|
@@ -5,6 +5,8 @@ const AUTH_HEADERS = ['authorization', 'cookie', 'x-api-key', 'x-csrf-token'];
|
|
|
5
5
|
export class RequestStore {
|
|
6
6
|
capturedRequests = [];
|
|
7
7
|
madeRequests = [];
|
|
8
|
+
failedRequests = [];
|
|
9
|
+
onFailedListeners = [];
|
|
8
10
|
outputDir;
|
|
9
11
|
constructor(outputDir) {
|
|
10
12
|
this.outputDir = outputDir;
|
|
@@ -13,6 +15,23 @@ export class RequestStore {
|
|
|
13
15
|
this.capturedRequests.push(result);
|
|
14
16
|
result.save(this.outputDir);
|
|
15
17
|
}
|
|
18
|
+
addFailedRequest(result) {
|
|
19
|
+
this.failedRequests.push(result);
|
|
20
|
+
for (const cb of this.onFailedListeners) {
|
|
21
|
+
cb(result);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
getFailedRequests() {
|
|
25
|
+
return this.failedRequests;
|
|
26
|
+
}
|
|
27
|
+
onFailedRequest(cb) {
|
|
28
|
+
this.onFailedListeners.push(cb);
|
|
29
|
+
return () => {
|
|
30
|
+
const idx = this.onFailedListeners.indexOf(cb);
|
|
31
|
+
if (idx !== -1)
|
|
32
|
+
this.onFailedListeners.splice(idx, 1);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
16
35
|
addMadeRequest(result) {
|
|
17
36
|
this.madeRequests.push(result);
|
|
18
37
|
result.save(this.outputDir);
|
|
@@ -101,6 +120,7 @@ export class RequestStore {
|
|
|
101
120
|
clear() {
|
|
102
121
|
this.capturedRequests = [];
|
|
103
122
|
this.madeRequests = [];
|
|
123
|
+
this.failedRequests = [];
|
|
104
124
|
}
|
|
105
125
|
}
|
|
106
126
|
function normalizePathPattern(urlPath) {
|
|
@@ -32,15 +32,31 @@ export class XhrCapture {
|
|
|
32
32
|
if (resourceType !== 'xhr' && resourceType !== 'fetch')
|
|
33
33
|
return;
|
|
34
34
|
const method = request.method();
|
|
35
|
-
if (!WRITE_METHODS.has(method))
|
|
36
|
-
return;
|
|
37
35
|
const url = request.url();
|
|
38
36
|
if (!url.startsWith(this.baseOrigin))
|
|
39
37
|
return;
|
|
38
|
+
const status = response.status();
|
|
39
|
+
if (status >= 400) {
|
|
40
|
+
const failedUrl = new URL(url);
|
|
41
|
+
const failure = new RequestResult({
|
|
42
|
+
id: generateRequestId(method, failedUrl.pathname, 'fail_'),
|
|
43
|
+
method,
|
|
44
|
+
path: failedUrl.pathname,
|
|
45
|
+
fullUrl: failedUrl.pathname + failedUrl.search,
|
|
46
|
+
requestHeaders: {},
|
|
47
|
+
status,
|
|
48
|
+
statusText: response.statusText(),
|
|
49
|
+
responseHeaders: {},
|
|
50
|
+
timing: 0,
|
|
51
|
+
timestamp: new Date(),
|
|
52
|
+
});
|
|
53
|
+
this.store.addFailedRequest(failure);
|
|
54
|
+
}
|
|
55
|
+
if (!WRITE_METHODS.has(method))
|
|
56
|
+
return;
|
|
40
57
|
const contentType = response.headers()['content-type'] || '';
|
|
41
58
|
if (!JSON_CONTENT_TYPES.test(contentType))
|
|
42
59
|
return;
|
|
43
|
-
const status = response.status();
|
|
44
60
|
if (status === 304)
|
|
45
61
|
return;
|
|
46
62
|
const parsedUrl = new URL(url);
|
|
@@ -105,7 +105,7 @@ export class CommandHandler {
|
|
|
105
105
|
this.runningCommands.add(command.name);
|
|
106
106
|
try {
|
|
107
107
|
await command.execute(argsString);
|
|
108
|
-
command.
|
|
108
|
+
command.printSuggestions();
|
|
109
109
|
}
|
|
110
110
|
catch (error) {
|
|
111
111
|
if (error?.name === 'AbortError')
|
|
@@ -7,7 +7,7 @@ import { BaseCommand } from './base-command.js';
|
|
|
7
7
|
export class AddRuleCommand extends BaseCommand {
|
|
8
8
|
name = 'add-rule';
|
|
9
9
|
description = 'Create a rule file for an agent';
|
|
10
|
-
suggestions = ['
|
|
10
|
+
suggestions = [{ command: 'add-rule researcher check-tooltips', hint: 'example — add a rule for the researcher agent' }];
|
|
11
11
|
async execute(args) {
|
|
12
12
|
const parts = args.trim().split(/\s+/);
|
|
13
13
|
const agentName = parts[0] || '';
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { Command } from 'commander';
|
|
3
|
+
import { isInteractive } from '../ai/task-agent.js';
|
|
4
|
+
import { getCliName } from '../utils/cli-name.js';
|
|
5
|
+
import { tag } from '../utils/logger.js';
|
|
2
6
|
export class BaseCommand {
|
|
3
7
|
aliases = [];
|
|
4
8
|
options = [];
|
|
@@ -11,6 +15,22 @@ export class BaseCommand {
|
|
|
11
15
|
matches(commandName) {
|
|
12
16
|
return this.name === commandName || this.aliases.includes(commandName);
|
|
13
17
|
}
|
|
18
|
+
printSuggestions() {
|
|
19
|
+
if (this.suggestions.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
const prefix = isInteractive() ? '/' : `${getCliName()} `;
|
|
22
|
+
tag('info').log('');
|
|
23
|
+
tag('info').log(chalk.bold('Suggested:'));
|
|
24
|
+
for (const { command, hint } of this.suggestions) {
|
|
25
|
+
tag('info').log('');
|
|
26
|
+
if (!command) {
|
|
27
|
+
tag('info').log(chalk.dim(hint));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
tag('info').log(chalk.dim(`${hint}:`));
|
|
31
|
+
tag('info').log(` ${chalk.yellow(`${prefix}${command}`)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
14
34
|
parseArgs(args) {
|
|
15
35
|
const cmd = new Command();
|
|
16
36
|
cmd.exitOverride();
|