explorbot 0.3.1 → 0.3.4
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 +16 -5
- package/dist/bin/explorbot-cli.js +14 -4
- package/dist/models.json +2 -0
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +4 -0
- package/dist/src/action-result.js +20 -12
- package/dist/src/action.d.ts +11 -0
- package/dist/src/action.js +28 -11
- package/dist/src/ai/conversation.d.ts +2 -1
- package/dist/src/ai/conversation.js +9 -4
- package/dist/src/ai/driller.js +3 -1
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +20 -4
- package/dist/src/ai/pilot.js +9 -2
- package/dist/src/ai/provider.d.ts +2 -0
- package/dist/src/ai/provider.js +18 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -3
- package/dist/src/ai/researcher/sections.js +0 -1
- package/dist/src/ai/researcher.js +0 -1
- package/dist/src/ai/rules.js +0 -1
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +9 -11
- package/dist/src/ai/tools.d.ts +2 -0
- package/dist/src/ai/tools.js +21 -4
- package/dist/src/commands/exit-command.js +1 -1
- package/dist/src/commands/init-command.js +74 -24
- package/dist/src/components/InitWizard.d.ts +2 -1
- package/dist/src/components/InitWizard.js +8 -4
- package/dist/src/explorbot.js +1 -0
- package/dist/src/explorer.js +1 -0
- package/dist/src/knowledge-tracker.d.ts +3 -1
- package/dist/src/knowledge-tracker.js +4 -4
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +3 -3
- package/dist/src/utils/aria.js +1 -1
- package/dist/src/utils/html.d.ts +2 -1
- package/dist/src/utils/html.js +10 -4
- package/dist/src/utils/overlay.d.ts +24 -0
- package/dist/src/utils/overlay.js +43 -0
- package/docs/basics/providers.md +2 -4
- package/models.json +2 -0
- package/package.json +1 -1
- package/src/action-result.ts +25 -15
- package/src/action.ts +36 -13
- package/src/ai/conversation.ts +11 -5
- package/src/ai/driller.ts +3 -1
- package/src/ai/navigator.ts +22 -4
- package/src/ai/pilot.ts +9 -2
- package/src/ai/provider.ts +19 -1
- package/src/ai/researcher/deep-analysis.ts +8 -3
- package/src/ai/researcher/sections.ts +0 -1
- package/src/ai/researcher.ts +0 -1
- package/src/ai/rules.ts +0 -1
- package/src/ai/tester.ts +9 -9
- package/src/ai/tools.ts +20 -4
- package/src/commands/exit-command.ts +1 -1
- package/src/commands/init-command.ts +81 -22
- package/src/components/InitWizard.tsx +8 -4
- package/src/explorbot.ts +1 -0
- package/src/explorer.ts +1 -0
- package/src/knowledge-tracker.ts +4 -4
- package/src/state-manager.ts +4 -3
- package/src/utils/aria.ts +1 -1
- package/src/utils/html.ts +13 -4
- package/src/utils/overlay.ts +51 -0
package/dist/src/ai/provider.js
CHANGED
|
@@ -31,6 +31,15 @@ function createHarmonyChannelFallbackTool() {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
let telemetryRegistered = false;
|
|
34
|
+
let beforeExitFlushHooked = false;
|
|
35
|
+
let activeOtelSdk = null;
|
|
36
|
+
export async function flushTelemetry() {
|
|
37
|
+
const sdk = activeOtelSdk;
|
|
38
|
+
activeOtelSdk = null;
|
|
39
|
+
if (!sdk)
|
|
40
|
+
return;
|
|
41
|
+
await sdk.shutdown().catch((error) => debugLog(`Telemetry flush failed: ${error instanceof Error ? error.message : error}`));
|
|
42
|
+
}
|
|
34
43
|
const CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
|
|
35
44
|
function extractCachedTokens(usage) {
|
|
36
45
|
if (!usage)
|
|
@@ -105,6 +114,9 @@ export class Provider {
|
|
|
105
114
|
throw new AiError(`AI connection failed: ${error.message}`);
|
|
106
115
|
}
|
|
107
116
|
}
|
|
117
|
+
async stop() {
|
|
118
|
+
await flushTelemetry();
|
|
119
|
+
}
|
|
108
120
|
getModelForAgent(agentName) {
|
|
109
121
|
if (!agentName) {
|
|
110
122
|
return this.config.model;
|
|
@@ -251,7 +263,12 @@ export class Provider {
|
|
|
251
263
|
spanProcessors: [processor],
|
|
252
264
|
instrumentations: [],
|
|
253
265
|
});
|
|
266
|
+
activeOtelSdk = this.otelSdk;
|
|
254
267
|
void this.otelSdk.start();
|
|
268
|
+
if (!beforeExitFlushHooked) {
|
|
269
|
+
process.on('beforeExit', () => void flushTelemetry());
|
|
270
|
+
beforeExitFlushHooked = true;
|
|
271
|
+
}
|
|
255
272
|
if (!telemetryRegistered) {
|
|
256
273
|
registerTelemetry(new OpenTelemetry());
|
|
257
274
|
telemetryRegistered = true;
|
|
@@ -293,7 +310,7 @@ export class Provider {
|
|
|
293
310
|
}
|
|
294
311
|
async invokeConversation(conversation, tools, options = {}) {
|
|
295
312
|
const response = tools ? await this.generateWithTools(conversation.messages, conversation.model, tools, options) : await this.chat(conversation.messages, conversation.model, options);
|
|
296
|
-
const responseMessages = response.
|
|
313
|
+
const responseMessages = response.responseMessages || [];
|
|
297
314
|
if (responseMessages.length > 0) {
|
|
298
315
|
conversation.messages.push(...responseMessages);
|
|
299
316
|
tag('debug').log('Added', responseMessages.length, 'messages from response');
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../../action-result.js';
|
|
3
3
|
import { executionController } from "../../execution-controller.js";
|
|
4
|
-
import {
|
|
4
|
+
import { diffAriaSnapshots } from "../../utils/aria.js";
|
|
5
5
|
import { extractCodeBlocks } from "../../utils/code-extractor.js";
|
|
6
6
|
import { tag } from '../../utils/logger.js';
|
|
7
7
|
import { mdq } from "../../utils/markdown-query.js";
|
|
8
|
+
import { truncate } from "../../utils/strings.js";
|
|
8
9
|
import { getCachedResearch, getPreviousResearch, saveResearch } from "./cache.js";
|
|
9
10
|
import { debugLog } from "./mixin.js";
|
|
10
11
|
import { parseResearchSections } from "./parser.js";
|
|
11
12
|
const DEFAULT_MAX_EXPANDABLE_CLICKS = 10;
|
|
13
|
+
const MAX_HTML_DIFF_CHARS = 20_000;
|
|
12
14
|
export function WithDeepAnalysis(Base) {
|
|
13
15
|
return class extends Base {
|
|
14
16
|
async performDeepAnalysis(state, result) {
|
|
@@ -60,7 +62,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
60
62
|
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
61
63
|
}
|
|
62
64
|
async researchOverlay(current, previous, pageStateHash) {
|
|
63
|
-
const focusArea =
|
|
65
|
+
const focusArea = current.overlay;
|
|
64
66
|
if (!focusArea.detected || !focusArea.name)
|
|
65
67
|
return null;
|
|
66
68
|
if (focusArea.type !== 'dialog' && focusArea.type !== 'modal')
|
|
@@ -430,6 +432,8 @@ export function WithDeepAnalysis(Base) {
|
|
|
430
432
|
|
|
431
433
|
`;
|
|
432
434
|
}
|
|
435
|
+
const cleanedParts = await diff.cleanedHtmlParts();
|
|
436
|
+
const htmlChanges = truncate(cleanedParts.map((p) => `[Container: ${p.container}]\n${p.subtree}`).join('\n\n'), MAX_HTML_DIFF_CHARS);
|
|
433
437
|
const prompt = dedent `
|
|
434
438
|
${intro}
|
|
435
439
|
Analyze the changes and produce a UI map section.
|
|
@@ -438,7 +442,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
438
442
|
${diff.ariaChanged || 'none'}
|
|
439
443
|
|
|
440
444
|
HTML changes:
|
|
441
|
-
${
|
|
445
|
+
${htmlChanges || 'none'}
|
|
442
446
|
${alreadyHint}
|
|
443
447
|
|
|
444
448
|
Respond with a SINGLE section in this format:
|
|
@@ -93,7 +93,6 @@ export function WithSections(Base) {
|
|
|
93
93
|
- Do not copy global toolbar, navigation, list, or detail elements into this section unless they are descendants of this section container.
|
|
94
94
|
- Every element with eidx inside this section's container MUST appear in the table.
|
|
95
95
|
- Every row needs CSS; ARIA may be "-" for icon-only buttons.
|
|
96
|
-
- ARIA locator JSON uses keys "role" and "text" (NOT "name").
|
|
97
96
|
- Elements marked data-explorbot-hit="covered" or "offscreen" are not directly actionable; describe the covering or focused UI first.
|
|
98
97
|
- In split-pane pages, entity detail panels are active detail context; include close/back/pin controls in the detail panel section when present.
|
|
99
98
|
</rules>
|
|
@@ -345,7 +345,6 @@ export class Researcher extends ResearcherBase {
|
|
|
345
345
|
- If an element has data-explorbot-hit="covered" or "offscreen", do not present it as directly actionable. Prefer the overlay, drawer, dialog, or focused section covering it, and mention what must be dismissed or revealed first.
|
|
346
346
|
- Every element with an eidx attribute MUST appear in exactly one matching UI map section — describe icon-only buttons by their visual role.
|
|
347
347
|
- Every UI map row needs a CSS selector; ARIA may be "-" for icon-only buttons, CSS must never be "-".
|
|
348
|
-
- ARIA locator JSON uses keys "role" and "text" (NOT "name").
|
|
349
348
|
- Mark elements with likely hover interactions (title, aria-describedby, menu items with submenus) as "(hover)".
|
|
350
349
|
</rules>
|
|
351
350
|
|
package/dist/src/ai/rules.js
CHANGED
|
@@ -67,7 +67,6 @@ const locatorStrategyRule = dedent `
|
|
|
67
67
|
|
|
68
68
|
<bad_aria_locator_example>
|
|
69
69
|
{ "role": "button", "text": "" } // INVALID - empty text is useless, use "-" instead
|
|
70
|
-
{ "role": "button", "name": "Save" } // WRONG key - use "text", not "name"
|
|
71
70
|
</bad_aria_locator_example>
|
|
72
71
|
|
|
73
72
|
NEVER include \`eidx\` attribute in any locator (ARIA, CSS, XPath). It is an internal annotation.
|
package/dist/src/ai/tester.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
33
33
|
lastAnalyzedStateHash: string | null;
|
|
34
34
|
stalledIterations: number;
|
|
35
35
|
readonly MAX_STALLED_ITERATIONS = 3;
|
|
36
|
+
skipResearch: (err: Error) => string;
|
|
36
37
|
constructor(deps: AgentDeps, researcher: Researcher, navigator: Navigator, agentTools?: any);
|
|
37
38
|
getNavigator(): Navigator;
|
|
38
39
|
setPilot(pilot: Pilot): void;
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -9,7 +9,6 @@ import { Observability } from "../observability.js";
|
|
|
9
9
|
import { normalizeUrl } from "../state-manager.js";
|
|
10
10
|
import { Stats } from "../stats.js";
|
|
11
11
|
import { TestResult } from "../test-plan.js";
|
|
12
|
-
import { detectFocusArea } from "../utils/aria.js";
|
|
13
12
|
import { ErrorPageError, isErrorPage } from "../utils/error-page.js";
|
|
14
13
|
import { createDebug, tag } from "../utils/logger.js";
|
|
15
14
|
import { loop } from "../utils/loop.js";
|
|
@@ -52,6 +51,12 @@ export class Tester extends TaskAgent {
|
|
|
52
51
|
lastAnalyzedStateHash = null;
|
|
53
52
|
stalledIterations = 0;
|
|
54
53
|
MAX_STALLED_ITERATIONS = 3;
|
|
54
|
+
skipResearch = (err) => {
|
|
55
|
+
if (err.name === 'AbortError')
|
|
56
|
+
throw err;
|
|
57
|
+
tag('warning').log(`Research skipped: ${err.message}`);
|
|
58
|
+
return '';
|
|
59
|
+
};
|
|
55
60
|
constructor(deps, researcher, navigator, agentTools) {
|
|
56
61
|
super(deps);
|
|
57
62
|
this.requestStore = deps.requestStore;
|
|
@@ -466,7 +471,7 @@ export class Tester extends TaskAgent {
|
|
|
466
471
|
this.previousUrl = currentUrl;
|
|
467
472
|
this.previousStateHash = currentStateHash;
|
|
468
473
|
let context = '';
|
|
469
|
-
const focusArea =
|
|
474
|
+
const focusArea = currentState.overlay;
|
|
470
475
|
const focusedElement = currentState.focusedElement;
|
|
471
476
|
if (focusedElement) {
|
|
472
477
|
const isTextInput = ['textbox', 'combobox', 'searchbox'].includes(focusedElement.role);
|
|
@@ -513,14 +518,7 @@ export class Tester extends TaskAgent {
|
|
|
513
518
|
const alreadySeenUiMap = this.seenUiMapUrls.has(currentUrl);
|
|
514
519
|
let research = '';
|
|
515
520
|
if (!alreadySeenUiMap) {
|
|
516
|
-
|
|
517
|
-
research = await this.researcher.research(currentState);
|
|
518
|
-
}
|
|
519
|
-
catch (err) {
|
|
520
|
-
if (!(err instanceof ErrorPageError))
|
|
521
|
-
throw err;
|
|
522
|
-
tag('warning').log(`Research skipped: ${err.message}`);
|
|
523
|
-
}
|
|
521
|
+
research = await this.researcher.research(currentState).catch(this.skipResearch);
|
|
524
522
|
}
|
|
525
523
|
this.pageStateHash = currentStateHash;
|
|
526
524
|
this.pageActionResult = currentState;
|
|
@@ -560,7 +558,7 @@ export class Tester extends TaskAgent {
|
|
|
560
558
|
return context;
|
|
561
559
|
}
|
|
562
560
|
if (focusArea.detected && focusArea.name && this.pageStateHash && this.pageActionResult) {
|
|
563
|
-
const overlaySection = await this.researcher.researchOverlay(currentState, this.pageActionResult, this.pageStateHash);
|
|
561
|
+
const overlaySection = await this.researcher.researchOverlay(currentState, this.pageActionResult, this.pageStateHash).catch(this.skipResearch);
|
|
564
562
|
if (overlaySection) {
|
|
565
563
|
context += dedent `
|
|
566
564
|
|
package/dist/src/ai/tools.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExecutedStep } from '../action.js';
|
|
1
2
|
import { ActionResult, type PageDiff } from '../action-result.js';
|
|
2
3
|
import { type ExperienceTracker } from '../experience-tracker.js';
|
|
3
4
|
import { type Task } from '../test-plan.js';
|
|
@@ -62,6 +63,7 @@ export declare function successToolResult(action: string, data?: Record<string,
|
|
|
62
63
|
}): Record<string, any>;
|
|
63
64
|
export declare function isMajorPageChange(pageDiff: PageDiff): boolean;
|
|
64
65
|
export declare function hasFailedRequest(pageDiff: PageDiff): boolean;
|
|
66
|
+
export declare function formatExecutedSteps(steps: ExecutedStep[], requestedCount?: number): string;
|
|
65
67
|
export declare function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null): Promise<Record<string, any>>;
|
|
66
68
|
export declare function withdrawVisionTools(tools: Record<string, any>): void;
|
|
67
69
|
export declare function clickFailureSuggestion(attempts: Array<{
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -343,16 +343,17 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
343
343
|
if (action.lastError) {
|
|
344
344
|
const message = errorText(action.lastError);
|
|
345
345
|
await commitNote(activeNote, TestResult.FAILED, toolResult, action);
|
|
346
|
-
let formSuggestion = '
|
|
346
|
+
let formSuggestion = 'Commands after the failing one never ran. Retry only those, using click() or form().';
|
|
347
347
|
if (message.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN)) {
|
|
348
348
|
const disambiguated = await disambiguateElements(action.lastError, explanation, ai);
|
|
349
349
|
if (disambiguated) {
|
|
350
350
|
formSuggestion = `Multiple elements matched. Add step.opts({ elementIndex: ${disambiguated.position} }) to the failing command. Fallback locator: ${disambiguated.xpath}`;
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
|
-
return failedToolResult('form', `Form execution FAILED! ${message}`, {
|
|
353
|
+
return failedToolResult('form', `Form execution FAILED! ${message}\n${formatExecutedSteps(action.executedSteps, codeLines.length)}`, {
|
|
354
354
|
...toolResult,
|
|
355
355
|
code: codeBlock,
|
|
356
|
+
attempts: action.executedSteps,
|
|
356
357
|
suggestion: formSuggestion,
|
|
357
358
|
}, action.lastError);
|
|
358
359
|
}
|
|
@@ -369,6 +370,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
369
370
|
...toolResult,
|
|
370
371
|
message: `Form completed successfully with ${lines.length} commands.`,
|
|
371
372
|
commandsExecuted: lines.length,
|
|
373
|
+
attempts: action.executedSteps,
|
|
372
374
|
code: codeBlock,
|
|
373
375
|
suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
|
|
374
376
|
}, action);
|
|
@@ -698,18 +700,24 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
698
700
|
const actionResult = ActionResult.fromState(currentState);
|
|
699
701
|
const experience = renderExperienceRecipes(explorer.activeTest?.getAppliedExperience(actionResult) ?? []);
|
|
700
702
|
const success = await navigator.resolveState(instruction, actionResult, { experience });
|
|
703
|
+
const attempts = navigator.executedSteps;
|
|
704
|
+
let stepReport = '';
|
|
705
|
+
if (attempts.length)
|
|
706
|
+
stepReport = `\n${formatExecutedSteps(attempts)}`;
|
|
701
707
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, instruction);
|
|
702
708
|
if (success) {
|
|
703
709
|
return successToolResult('interact', {
|
|
704
710
|
...toolResult,
|
|
705
|
-
message: `Successfully executed: ${instruction}`,
|
|
711
|
+
message: `Successfully executed: ${instruction}${stepReport}`,
|
|
712
|
+
attempts,
|
|
706
713
|
});
|
|
707
714
|
}
|
|
708
715
|
let reason = '';
|
|
709
716
|
if (navigator.lastFailureReason)
|
|
710
717
|
reason = `: ${navigator.lastFailureReason}`;
|
|
711
|
-
return failedToolResult('interact', `Failed to execute: ${instruction}${reason}`, {
|
|
718
|
+
return failedToolResult('interact', `Failed to execute: ${instruction}${reason}${stepReport}`, {
|
|
712
719
|
...toolResult,
|
|
720
|
+
attempts,
|
|
713
721
|
suggestion: 'The action could not be completed. Try a different instruction or use more specific element descriptions.',
|
|
714
722
|
});
|
|
715
723
|
}
|
|
@@ -1071,6 +1079,15 @@ export function isMajorPageChange(pageDiff) {
|
|
|
1071
1079
|
export function hasFailedRequest(pageDiff) {
|
|
1072
1080
|
return (pageDiff.requests ?? []).some((request) => request.status >= 400);
|
|
1073
1081
|
}
|
|
1082
|
+
export function formatExecutedSteps(steps, requestedCount = steps.length) {
|
|
1083
|
+
if (!steps.length)
|
|
1084
|
+
return `No command ran of ${requestedCount} requested.`;
|
|
1085
|
+
const lines = steps.map((step) => ` ${step.success ? 'OK' : 'FAILED'} ${step.command}`);
|
|
1086
|
+
const notRun = requestedCount - steps.length;
|
|
1087
|
+
if (notRun > 0)
|
|
1088
|
+
lines.push(` NOT RUN ${notRun} more`);
|
|
1089
|
+
return lines.join('\n');
|
|
1090
|
+
}
|
|
1074
1091
|
function hasObservablePageChange(data) {
|
|
1075
1092
|
if (!data?.pageDiff)
|
|
1076
1093
|
return false;
|
|
@@ -9,7 +9,7 @@ export class ExitCommand extends BaseCommand {
|
|
|
9
9
|
aliases = ['quit'];
|
|
10
10
|
async execute(_args) {
|
|
11
11
|
await this.explorBot.printSessionAnalysis();
|
|
12
|
-
await this.explorBot.
|
|
12
|
+
await this.explorBot.stop();
|
|
13
13
|
if (Stats.hasActivity()) {
|
|
14
14
|
await new Promise((resolve) => {
|
|
15
15
|
const { unmount } = render(React.createElement(StatusPane, {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import dedent from 'dedent';
|
|
5
4
|
import { ConfigParser, PROVIDERS } from "../config.js";
|
|
6
5
|
import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "../global-config.js";
|
|
7
6
|
import { getCliName } from "../utils/cli-name.js";
|
|
8
7
|
import { log, tag } from '../utils/logger.js';
|
|
9
8
|
import { relativeToCwd } from "../utils/next-steps.js";
|
|
10
|
-
function defaultConfigTemplate() {
|
|
9
|
+
function defaultConfigTemplate(provider, esm) {
|
|
10
|
+
let moduleExport = 'module.exports = config;';
|
|
11
|
+
if (esm)
|
|
12
|
+
moduleExport = 'export default config;';
|
|
11
13
|
return `// 'provider/model-id' uses a bundled provider.
|
|
12
|
-
// It is also possible to import provider as a module from Vercel AI SDK.
|
|
14
|
+
// It is also possible to import provider as a module from Vercel AI SDK.
|
|
13
15
|
// https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
|
|
14
16
|
|
|
15
17
|
const config = {
|
|
@@ -19,7 +21,7 @@ const config = {
|
|
|
19
21
|
},
|
|
20
22
|
|
|
21
23
|
ai: {
|
|
22
|
-
${modelLines(
|
|
24
|
+
${modelLines(provider)}
|
|
23
25
|
},
|
|
24
26
|
|
|
25
27
|
reporter: {
|
|
@@ -32,16 +34,17 @@ ${modelLines('openrouter')}
|
|
|
32
34
|
},
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
${moduleExport}
|
|
36
38
|
`;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
function envTemplate(provider) {
|
|
41
|
+
const keyLines = Object.entries(PROVIDERS).map(([name, { envKey }]) => {
|
|
42
|
+
if (name === provider)
|
|
43
|
+
return `${envKey}=`;
|
|
44
|
+
return `# ${envKey}=`;
|
|
45
|
+
});
|
|
46
|
+
return `# AI provider API keys
|
|
47
|
+
${keyLines.join('\n')}
|
|
45
48
|
|
|
46
49
|
# Langfuse Tracing
|
|
47
50
|
LANGFUSE_SECRET_KEY=
|
|
@@ -49,10 +52,11 @@ LANGFUSE_PUBLIC_KEY=
|
|
|
49
52
|
LANGFUSE_BASE_URL=
|
|
50
53
|
|
|
51
54
|
# Testomat.io API key to publish run results
|
|
52
|
-
TESTOMATIO
|
|
53
|
-
|
|
55
|
+
TESTOMATIO=`;
|
|
56
|
+
}
|
|
54
57
|
export async function runInit(options) {
|
|
55
|
-
|
|
58
|
+
const localRequested = !!(options.configPath || options.path);
|
|
59
|
+
if (options.global || (options.provider && !localRequested)) {
|
|
56
60
|
await runGlobalInit(options);
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
@@ -61,8 +65,12 @@ export async function runInit(options) {
|
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
63
67
|
const choice = await renderInitWizard('choose');
|
|
64
|
-
if (choice
|
|
65
|
-
|
|
68
|
+
if (choice !== 'local')
|
|
69
|
+
return;
|
|
70
|
+
const provider = await renderLocalProviderWizard();
|
|
71
|
+
if (!provider)
|
|
72
|
+
return;
|
|
73
|
+
runInitCommand({ ...options, provider });
|
|
66
74
|
}
|
|
67
75
|
export function writeGlobalConfig(provider, apiKey) {
|
|
68
76
|
if (!PROVIDERS[provider]) {
|
|
@@ -87,7 +95,7 @@ export function writeGlobalConfig(provider, apiKey) {
|
|
|
87
95
|
tag('substep').log(chalk.yellow(`${getCliName()} sites`));
|
|
88
96
|
}
|
|
89
97
|
export function runInitCommand(options) {
|
|
90
|
-
const
|
|
98
|
+
const provider = options.provider || 'openrouter';
|
|
91
99
|
const force = options.force ?? false;
|
|
92
100
|
const customPath = options.path;
|
|
93
101
|
const originalCwd = process.cwd();
|
|
@@ -100,13 +108,15 @@ export function runInitCommand(options) {
|
|
|
100
108
|
process.chdir(dir);
|
|
101
109
|
log(`Working in directory: ${relativeToCwd(dir)}`);
|
|
102
110
|
}
|
|
111
|
+
const configName = 'explorbot.config.js';
|
|
112
|
+
const configPath = options.configPath ?? `./${configName}`;
|
|
103
113
|
try {
|
|
104
114
|
let outPath = resolve(configPath);
|
|
105
115
|
if (existsSync(outPath) && statSync(outPath).isDirectory()) {
|
|
106
|
-
outPath = join(outPath,
|
|
116
|
+
outPath = join(outPath, configName);
|
|
107
117
|
}
|
|
108
118
|
else if (!extname(outPath)) {
|
|
109
|
-
outPath = join(outPath,
|
|
119
|
+
outPath = join(outPath, configName);
|
|
110
120
|
}
|
|
111
121
|
const dir = dirname(outPath);
|
|
112
122
|
if (!existsSync(dir)) {
|
|
@@ -118,23 +128,28 @@ export function runInitCommand(options) {
|
|
|
118
128
|
log('Use --force to overwrite existing file');
|
|
119
129
|
process.exit(1);
|
|
120
130
|
}
|
|
121
|
-
|
|
131
|
+
const esm = extname(outPath) !== '.js' || isModuleProject(dirname(outPath));
|
|
132
|
+
writeFileSync(outPath, defaultConfigTemplate(provider, esm), 'utf8');
|
|
122
133
|
log(`Created config file: ${relativeToCwd(outPath)}`);
|
|
123
134
|
const envPath = resolve(process.cwd(), '.env');
|
|
124
135
|
if (!existsSync(envPath)) {
|
|
125
|
-
writeFileSync(envPath, `${
|
|
136
|
+
writeFileSync(envPath, `${envTemplate(provider)}\n`, 'utf8');
|
|
126
137
|
log(`Created env file: ${relativeToCwd(envPath)}`);
|
|
127
138
|
}
|
|
128
139
|
else {
|
|
129
140
|
log(`Env file already exists: ${relativeToCwd(envPath)}`);
|
|
130
141
|
}
|
|
142
|
+
const missing = missingRoles(provider);
|
|
143
|
+
if (missing.length) {
|
|
144
|
+
tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${relativeToCwd(outPath)}`);
|
|
145
|
+
}
|
|
131
146
|
log('');
|
|
132
147
|
log('Next steps:');
|
|
133
148
|
log('1. Configure AI provider in .env');
|
|
134
149
|
log('2. Set AI models config file');
|
|
135
150
|
log('3. Set web application URL in the config file');
|
|
136
151
|
log('4. Add initial knowledge (how to authorize to the application, etc.)');
|
|
137
|
-
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to
|
|
152
|
+
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
138
153
|
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
139
154
|
log('5. Launch application on a relative URL');
|
|
140
155
|
tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
|
|
@@ -187,6 +202,23 @@ async function renderInitWizard(mode) {
|
|
|
187
202
|
}), { exitOnCtrlC: false, patchConsole: false });
|
|
188
203
|
});
|
|
189
204
|
}
|
|
205
|
+
async function renderLocalProviderWizard() {
|
|
206
|
+
const [{ render }, React, InitWizard] = await Promise.all([import('ink'), import('react'), import('../components/InitWizard.js').then((m) => m.default)]);
|
|
207
|
+
return new Promise((resolve) => {
|
|
208
|
+
const finish = (provider) => {
|
|
209
|
+
unmount();
|
|
210
|
+
resolve(provider);
|
|
211
|
+
};
|
|
212
|
+
const { unmount } = render(React.createElement(InitWizard, {
|
|
213
|
+
mode: 'local',
|
|
214
|
+
globalConfigExists: !!findGlobalConfig(),
|
|
215
|
+
onLocal: () => finish(null),
|
|
216
|
+
onComplete: () => finish(null),
|
|
217
|
+
onCancel: () => finish(null),
|
|
218
|
+
onLocalProvider: (provider) => finish(provider),
|
|
219
|
+
}), { exitOnCtrlC: false, patchConsole: false });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
190
222
|
function modelLines(provider) {
|
|
191
223
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
192
224
|
const roles = [
|
|
@@ -216,9 +248,27 @@ ${modelLines(provider)}
|
|
|
216
248
|
},
|
|
217
249
|
};
|
|
218
250
|
|
|
219
|
-
|
|
251
|
+
module.exports = config;
|
|
220
252
|
`;
|
|
221
253
|
}
|
|
254
|
+
function isModuleProject(configDir) {
|
|
255
|
+
let currentDir = resolve(configDir);
|
|
256
|
+
while (true) {
|
|
257
|
+
const packagePath = join(currentDir, 'package.json');
|
|
258
|
+
if (existsSync(packagePath)) {
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(readFileSync(packagePath, 'utf8')).type === 'module';
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const parentDir = dirname(currentDir);
|
|
267
|
+
if (parentDir === currentDir)
|
|
268
|
+
return false;
|
|
269
|
+
currentDir = parentDir;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
222
272
|
function missingRoles(provider) {
|
|
223
273
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
224
274
|
return ['model', 'visionModel', 'agenticModel'].filter((role) => !recommended[role]);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface InitWizardProps {
|
|
3
|
-
mode: 'choose' | 'global';
|
|
3
|
+
mode: 'choose' | 'global' | 'local';
|
|
4
4
|
globalConfigExists: boolean;
|
|
5
5
|
onLocal: () => void;
|
|
6
6
|
onComplete: () => void;
|
|
7
7
|
onCancel: () => void;
|
|
8
|
+
onLocalProvider?: (provider: string) => void;
|
|
8
9
|
}
|
|
9
10
|
declare const InitWizard: React.FC<InitWizardProps>;
|
|
10
11
|
export default InitWizard;
|
|
@@ -7,7 +7,7 @@ import { ConfigParser, PROVIDERS, createModel } from '../config.js';
|
|
|
7
7
|
import { globalDir } from '../global-config.js';
|
|
8
8
|
import InputReadline from './InputReadline.js';
|
|
9
9
|
const PROVIDER_NAMES = Object.keys(PROVIDERS);
|
|
10
|
-
const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel }) => {
|
|
10
|
+
const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, onLocalProvider }) => {
|
|
11
11
|
const [step, setStep] = useState(mode === 'choose' ? 'target' : 'provider');
|
|
12
12
|
const [targetIndex, setTargetIndex] = useState(0);
|
|
13
13
|
const [providerIndex, setProviderIndex] = useState(0);
|
|
@@ -67,8 +67,12 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel })
|
|
|
67
67
|
setProviderIndex((index) => Math.max(0, index - 1));
|
|
68
68
|
if (key.downArrow)
|
|
69
69
|
setProviderIndex((index) => Math.min(PROVIDER_NAMES.length - 1, index + 1));
|
|
70
|
-
if (key.return)
|
|
71
|
-
|
|
70
|
+
if (key.return) {
|
|
71
|
+
if (mode === 'local')
|
|
72
|
+
onLocalProvider?.(provider);
|
|
73
|
+
else
|
|
74
|
+
setStep('key');
|
|
75
|
+
}
|
|
72
76
|
return;
|
|
73
77
|
}
|
|
74
78
|
if (status)
|
|
@@ -121,7 +125,7 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel })
|
|
|
121
125
|
React.createElement(Box, { marginTop: 1 },
|
|
122
126
|
React.createElement(Text, { dimColor: true },
|
|
123
127
|
"Config goes to ",
|
|
124
|
-
globalDir(),
|
|
128
|
+
mode === 'local' ? 'the current directory' : globalDir(),
|
|
125
129
|
" | ",
|
|
126
130
|
step === 'key' ? 'Enter: continue' : '↑↓: select | Enter: confirm',
|
|
127
131
|
" | Ctrl+C: exit"))));
|
package/dist/src/explorbot.js
CHANGED
package/dist/src/explorer.js
CHANGED
|
@@ -203,6 +203,7 @@ class Explorer {
|
|
|
203
203
|
const projectRoot = configParser.getProjectRoot();
|
|
204
204
|
global.output_dir = configParser.getStatesDir();
|
|
205
205
|
global.codecept_dir = projectRoot;
|
|
206
|
+
global.codeceptjs = codeceptjs;
|
|
206
207
|
configParser.validateConfig(this.config);
|
|
207
208
|
codeceptjs.container.create(this.convertToCodeceptConfig(this.config), {});
|
|
208
209
|
}
|
|
@@ -17,7 +17,9 @@ export declare class KnowledgeTracker {
|
|
|
17
17
|
renderRelevantKnowledge(state: ActionResult): string;
|
|
18
18
|
renderRelevantContext(state: ActionResult): string;
|
|
19
19
|
renderApplicationSpec(state: ActionResult): string;
|
|
20
|
-
addKnowledge(urlPattern: string, description: string
|
|
20
|
+
addKnowledge(urlPattern: string, description: string, opts?: {
|
|
21
|
+
replace?: boolean;
|
|
22
|
+
}): {
|
|
21
23
|
filename: string;
|
|
22
24
|
filePath: string;
|
|
23
25
|
isNewFile: boolean;
|
|
@@ -73,7 +73,7 @@ export class KnowledgeTracker {
|
|
|
73
73
|
renderApplicationSpec(state) {
|
|
74
74
|
return this.applicationSpec?.renderFor(state) || '';
|
|
75
75
|
}
|
|
76
|
-
addKnowledge(urlPattern, description) {
|
|
76
|
+
addKnowledge(urlPattern, description, opts) {
|
|
77
77
|
const configParser = ConfigParser.getInstance();
|
|
78
78
|
const configPath = configParser.getConfigPath();
|
|
79
79
|
if (!configPath) {
|
|
@@ -102,11 +102,11 @@ export class KnowledgeTracker {
|
|
|
102
102
|
const existingDescription = parsed.content.trim();
|
|
103
103
|
// Append new knowledge with separator
|
|
104
104
|
let newContent;
|
|
105
|
-
if (existingDescription) {
|
|
106
|
-
newContent =
|
|
105
|
+
if (opts?.replace || !existingDescription) {
|
|
106
|
+
newContent = description;
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
|
-
newContent = description
|
|
109
|
+
newContent = `${existingDescription}\n\n---\n\n${description}`;
|
|
110
110
|
}
|
|
111
111
|
const fileContent = matter.stringify(newContent, frontmatter);
|
|
112
112
|
writeFileSync(filePath, fileContent, 'utf8');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
2
2
|
import type { ExperienceTracker } from './experience-tracker.js';
|
|
3
3
|
import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
|
|
4
|
+
import { Overlay } from './utils/overlay.js';
|
|
4
5
|
export interface Link {
|
|
5
6
|
title: string;
|
|
6
7
|
url: string;
|
|
@@ -40,6 +41,7 @@ export interface WebPageState {
|
|
|
40
41
|
focusedElement?: FocusedElement | null;
|
|
41
42
|
links?: Link[];
|
|
42
43
|
verifications?: Record<string, boolean>;
|
|
44
|
+
overlay?: Overlay;
|
|
43
45
|
}
|
|
44
46
|
export interface StateTransition {
|
|
45
47
|
/** Previous state (null if this is the first state) */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ActionResult } from './action-result.js';
|
|
2
|
-
import { detectFocusArea } from './utils/aria.js';
|
|
3
2
|
import { createDebug, tag } from './utils/logger.js';
|
|
3
|
+
import { Overlay } from './utils/overlay.js';
|
|
4
4
|
import { slugify } from './utils/strings.js';
|
|
5
5
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
6
6
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -114,8 +114,8 @@ export class StateManager {
|
|
|
114
114
|
return newState;
|
|
115
115
|
}
|
|
116
116
|
hasDialogAppeared(previousState, newState) {
|
|
117
|
-
const prevFocus =
|
|
118
|
-
const newFocus =
|
|
117
|
+
const prevFocus = previousState?.overlay ?? Overlay.fromAria(previousState?.ariaSnapshot ?? null);
|
|
118
|
+
const newFocus = newState.overlay ?? Overlay.fromAria(newState.ariaSnapshot ?? null);
|
|
119
119
|
return !prevFocus.detected && newFocus.detected;
|
|
120
120
|
}
|
|
121
121
|
/**
|
package/dist/src/utils/aria.js
CHANGED
|
@@ -569,7 +569,7 @@ export function parseAriaLocator(ariaStr) {
|
|
|
569
569
|
const trimmed = ariaStr.trim();
|
|
570
570
|
if (trimmed === '-' || trimmed === '' || trimmed === '"-"')
|
|
571
571
|
return null;
|
|
572
|
-
const match = trimmed.match(/\{\s*["']?role["']?\s*:\s*['"]([^'"]+)['"]\s*,\s*["']?text["']?\s*:\s*['"]([^'"]*)['"]\s*\}/);
|
|
572
|
+
const match = trimmed.match(/\{\s*["']?role["']?\s*:\s*['"]([^'"]+)['"]\s*,\s*["']?(?:text|name)["']?\s*:\s*['"]([^'"]*)['"]\s*\}/);
|
|
573
573
|
if (!match)
|
|
574
574
|
return null;
|
|
575
575
|
return { role: match[1], text: match[2] };
|
package/dist/src/utils/html.d.ts
CHANGED
|
@@ -14,7 +14,6 @@ export declare const HTML_SELECTORS: {
|
|
|
14
14
|
readonly interactiveControl: "button, a[href], input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [role=\"tab\"], [role=\"menuitem\"]";
|
|
15
15
|
readonly labelLike: "h1, h2, h3, h4, h5, h6, legend, caption, label, [role=\"heading\"], [class*=\"title\"], [class*=\"label\"], [class*=\"header\"], [class*=\"name\"]";
|
|
16
16
|
readonly semanticContextContainer: "section, article, form, fieldset, li, tr, td, th, [role=\"group\"], [role=\"tabpanel\"], [role=\"region\"], [class*=\"card\"], [class*=\"panel\"], [class*=\"item\"], [class*=\"usage\"], [class*=\"group\"]";
|
|
17
|
-
readonly semanticOverlays: readonly ["[role=\"dialog\"]", "[role=\"listbox\"]", "[role=\"menu\"]", "[role=\"tooltip\"]:not([style*=\"display: none\"]):not([style*=\"visibility: hidden\"])"];
|
|
18
17
|
};
|
|
19
18
|
export declare const HTML_VISIBILITY_LIMITS: {
|
|
20
19
|
readonly maxViewportOverlayRatio: 0.95;
|
|
@@ -67,7 +66,9 @@ export type VisibleOverlayExtractionConfig = {
|
|
|
67
66
|
interactiveContentSelector: string;
|
|
68
67
|
limits: typeof HTML_EXTRACTION_LIMITS;
|
|
69
68
|
overlaySelectors: readonly string[];
|
|
69
|
+
overlaySemanticSelector: string;
|
|
70
70
|
visibilityLimits: typeof HTML_VISIBILITY_LIMITS;
|
|
71
|
+
geometryFallback?: boolean;
|
|
71
72
|
};
|
|
72
73
|
export type ComponentScopeExtractionConfig = {
|
|
73
74
|
eidxAttr: string;
|