explorbot 0.0.5 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +97 -39
- package/dist/bin/explorbot-cli.js +75 -19
- package/dist/rules/rerunner/healing-approach.md +19 -0
- package/dist/src/action.js +8 -7
- package/dist/src/ai/historian.js +34 -3
- package/dist/src/ai/navigator.js +35 -28
- package/dist/src/ai/pilot.js +33 -9
- package/dist/src/ai/planner/subpages.js +42 -6
- package/dist/src/ai/planner.js +44 -13
- package/dist/src/ai/rerunner.js +472 -0
- package/dist/src/ai/researcher/cache.js +13 -8
- package/dist/src/ai/researcher/coordinates.js +4 -2
- package/dist/src/ai/researcher/deep-analysis.js +16 -19
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/parser.js +4 -3
- package/dist/src/ai/researcher/research-result.js +2 -0
- package/dist/src/ai/researcher.js +3 -3
- package/dist/src/ai/rules.js +2 -2
- package/dist/src/ai/tools.js +6 -2
- package/dist/src/commands/add-rule-command.js +1 -2
- package/dist/src/commands/base-command.js +12 -0
- package/dist/src/commands/context-command.js +10 -3
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/explore-command.js +21 -6
- package/dist/src/commands/freesail-command.js +8 -22
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/init-command.js +7 -5
- package/dist/src/commands/path-command.js +2 -1
- package/dist/src/commands/plan-command.js +38 -11
- package/dist/src/commands/rerun-command.js +42 -0
- package/dist/src/commands/research-command.js +10 -4
- package/dist/src/commands/runs-command.js +22 -0
- package/dist/src/commands/start-command.js +0 -1
- package/dist/src/commands/test-command.js +3 -3
- package/dist/src/components/App.js +8 -0
- package/dist/src/config.js +3 -0
- package/dist/src/explorbot.js +20 -1
- package/dist/src/explorer.js +59 -16
- package/dist/src/suite.js +115 -0
- package/dist/src/utils/html.js +2 -5
- package/dist/src/utils/rules-loader.js +33 -17
- package/dist/src/utils/test-files.js +103 -0
- package/dist/src/utils/web-element.js +6 -4
- package/package.json +3 -2
- package/rules/rerunner/healing-approach.md +19 -0
- package/src/action.ts +8 -6
- package/src/ai/historian.ts +37 -3
- package/src/ai/navigator.ts +35 -28
- package/src/ai/pilot.ts +33 -9
- package/src/ai/planner/subpages.ts +37 -7
- package/src/ai/planner.ts +44 -12
- package/src/ai/rerunner.ts +532 -0
- package/src/ai/researcher/cache.ts +14 -8
- package/src/ai/researcher/coordinates.ts +8 -7
- package/src/ai/researcher/deep-analysis.ts +18 -21
- package/src/ai/researcher/locators.ts +3 -3
- package/src/ai/researcher/parser.ts +4 -4
- package/src/ai/researcher/research-result.ts +1 -0
- package/src/ai/researcher.ts +3 -3
- package/src/ai/rules.ts +2 -2
- package/src/ai/tools.ts +7 -2
- package/src/commands/add-rule-command.ts +1 -2
- package/src/commands/base-command.ts +13 -0
- package/src/commands/context-command.ts +10 -3
- package/src/commands/drill-command.ts +0 -1
- package/src/commands/explore-command.ts +22 -6
- package/src/commands/freesail-command.ts +6 -23
- package/src/commands/index.ts +4 -0
- package/src/commands/init-command.ts +8 -5
- package/src/commands/path-command.ts +2 -1
- package/src/commands/plan-command.ts +46 -12
- package/src/commands/rerun-command.ts +46 -0
- package/src/commands/research-command.ts +10 -4
- package/src/commands/runs-command.ts +27 -0
- package/src/commands/start-command.ts +0 -1
- package/src/commands/test-command.ts +3 -3
- package/src/components/App.tsx +8 -0
- package/src/config.ts +24 -0
- package/src/explorbot.ts +22 -1
- package/src/explorer.ts +68 -20
- package/src/suite.ts +135 -0
- package/src/utils/html.ts +1 -5
- package/src/utils/rules-loader.ts +35 -17
- package/src/utils/test-files.ts +122 -0
- package/src/utils/web-element.ts +12 -10
|
@@ -77,7 +77,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
private async _discoverExpandables(researchText: string): Promise<ExpandableElement[]> {
|
|
80
|
-
const allElements = new Map<
|
|
80
|
+
const allElements = new Map<string, ExpandableElement>();
|
|
81
81
|
for (const section of parseResearchSections(researchText)) {
|
|
82
82
|
for (const el of section.elements) {
|
|
83
83
|
if (el.eidx != null) allElements.set(el.eidx, { ...el, container: section.containerCss });
|
|
@@ -91,7 +91,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
91
91
|
From this UI research, identify elements that could reveal hidden UI when clicked
|
|
92
92
|
(dropdown menus, popups, expandable panels, accordion sections, overflow menus, tab switches).
|
|
93
93
|
|
|
94
|
-
Available eidx
|
|
94
|
+
Available eidx refs: ${eidxList}
|
|
95
95
|
|
|
96
96
|
${researchText}
|
|
97
97
|
|
|
@@ -99,7 +99,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
99
99
|
- Only pick elements that HIDE content until clicked (menus, dropdowns, accordions, tabs)
|
|
100
100
|
- Skip regular links, data items, and navigation
|
|
101
101
|
- For repeated elements (same expand button on every row), pick only the FIRST one
|
|
102
|
-
- Respond with comma-separated eidx
|
|
102
|
+
- Respond with comma-separated eidx refs only, e.g.: e3, e7, e15
|
|
103
103
|
`;
|
|
104
104
|
|
|
105
105
|
const model = this.provider.getModelForAgent('researcher');
|
|
@@ -112,7 +112,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
112
112
|
const screenshot = this.actionResult?.screenshot;
|
|
113
113
|
if (screenshot && this.provider.hasVision()) {
|
|
114
114
|
const visionPrompt = dedent`
|
|
115
|
-
This screenshot has interactive elements labeled with eidx
|
|
115
|
+
This screenshot has interactive elements labeled with eidx refs (solid bordered boxes with labels).
|
|
116
116
|
Identify elements that could reveal hidden UI when clicked.
|
|
117
117
|
|
|
118
118
|
Look for: overflow/ellipsis menus, chevron dropdowns, hamburger menus,
|
|
@@ -121,33 +121,30 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
121
121
|
Rules:
|
|
122
122
|
- For repeated icons (same icon on every list row), pick only the FIRST one
|
|
123
123
|
- Skip regular text buttons, links, and navigation items
|
|
124
|
-
- Respond with comma-separated eidx
|
|
124
|
+
- Respond with comma-separated eidx refs only, e.g.: e3, e7, e15
|
|
125
125
|
`;
|
|
126
126
|
visionCall = this.provider.processImage(visionPrompt, screenshot.toString('base64'));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const [textRes, visionRes] = await Promise.all([textCall, visionCall]);
|
|
130
130
|
|
|
131
|
-
const eidxSet = new Set<
|
|
131
|
+
const eidxSet = new Set<string>();
|
|
132
|
+
const parseRefs = (text: string | undefined) => {
|
|
133
|
+
if (!text) return [];
|
|
134
|
+
const matches = text.match(/e?\d+/g) || [];
|
|
135
|
+
const refs = matches.map((m) => (m.startsWith('e') ? m : `e${m}`));
|
|
136
|
+
return refs.filter((r) => allElements.has(r));
|
|
137
|
+
};
|
|
138
|
+
|
|
132
139
|
for (const res of [textRes, visionRes]) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
for (const n of nums) {
|
|
136
|
-
if (allElements.has(n)) eidxSet.add(n);
|
|
140
|
+
for (const ref of parseRefs(res?.text)) {
|
|
141
|
+
eidxSet.add(ref);
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
?.map(Number)
|
|
144
|
-
.filter((n) => allElements.has(n)) || [];
|
|
145
|
-
const visionNums =
|
|
146
|
-
visionRes?.text
|
|
147
|
-
?.match(/\d+/g)
|
|
148
|
-
?.map(Number)
|
|
149
|
-
.filter((n) => allElements.has(n)) || [];
|
|
150
|
-
debugLog(`Text model picked eidx: [${textNums.join(', ')}], Vision model picked eidx: [${visionNums.join(', ')}]`);
|
|
145
|
+
const textRefs = parseRefs(textRes?.text);
|
|
146
|
+
const visionRefs = parseRefs(visionRes?.text);
|
|
147
|
+
debugLog(`Text model picked eidx: [${textRefs.join(', ')}], Vision model picked eidx: [${visionRefs.join(', ')}]`);
|
|
151
148
|
|
|
152
149
|
return [...eidxSet].map((eidx) => allElements.get(eidx)!);
|
|
153
150
|
}
|
|
@@ -146,7 +146,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
|
|
|
146
146
|
for (const fixedSection of fixedSections) {
|
|
147
147
|
const originalSections = parseResearchSections(result.text);
|
|
148
148
|
const original = originalSections.find((s) => s.name === fixedSection.name);
|
|
149
|
-
if (!original) continue;
|
|
149
|
+
if (!original || original.elements.length === 0) continue;
|
|
150
150
|
|
|
151
151
|
if (fixedSection.containerCss && fixedSection.containerCss !== original.containerCss) {
|
|
152
152
|
debugLog(`Fixed container for "${fixedSection.name}": '${original.containerCss}' → '${fixedSection.containerCss}'`);
|
|
@@ -177,8 +177,8 @@ export function WithLocators<T extends Constructor>(Base: T) {
|
|
|
177
177
|
const sections = parseResearchSections(result.text);
|
|
178
178
|
const brokenCss = new Set(result.locators.filter((l) => l.type === 'css' && l.valid === false).map((l) => `${l.section}::${l.element}`));
|
|
179
179
|
|
|
180
|
-
const needsXpath:
|
|
181
|
-
const needsXpathEls = new Map<
|
|
180
|
+
const needsXpath: string[] = [];
|
|
181
|
+
const needsXpathEls = new Map<string, { section: (typeof sections)[0]; el: (typeof sections)[0]['elements'][0] }>();
|
|
182
182
|
|
|
183
183
|
for (const section of sections) {
|
|
184
184
|
for (const el of section.elements) {
|
|
@@ -13,7 +13,7 @@ export interface ResearchElement {
|
|
|
13
13
|
coordinates: string | null;
|
|
14
14
|
color: string | null;
|
|
15
15
|
icon: string | null;
|
|
16
|
-
eidx:
|
|
16
|
+
eidx: string | null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface ResearchSection {
|
|
@@ -62,8 +62,8 @@ export function mapRowToElement(row: Record<string, string>): ResearchElement |
|
|
|
62
62
|
const name = stripQuotes(colMap.element || '');
|
|
63
63
|
if (!name) return null;
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
let eidxRaw = (colMap.eidx || '').trim();
|
|
66
|
+
if (eidxRaw && /^\d+$/.test(eidxRaw)) eidxRaw = `e${eidxRaw}`;
|
|
67
67
|
|
|
68
68
|
const aria = parseAriaLocator(colMap.aria || '-');
|
|
69
69
|
|
|
@@ -76,7 +76,7 @@ export function mapRowToElement(row: Record<string, string>): ResearchElement |
|
|
|
76
76
|
coordinates: (colMap.coordinates || '-').trim() === '-' ? null : colMap.coordinates.trim(),
|
|
77
77
|
color: (colMap.color || '-').trim() === '-' || (colMap.color || '').trim() === '' ? null : colMap.color.trim(),
|
|
78
78
|
icon: (colMap.icon || '-').trim() === '-' || (colMap.icon || '').trim() === '' ? null : colMap.icon.trim(),
|
|
79
|
-
eidx:
|
|
79
|
+
eidx: eidxRaw && eidxRaw !== '-' ? eidxRaw : null,
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -65,6 +65,7 @@ export class ResearchResult {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
rebuildSectionInText(section: ResearchSection): void {
|
|
68
|
+
if (section.elements.length === 0) return;
|
|
68
69
|
const newTable = rebuildSectionMarkdown(section);
|
|
69
70
|
const escaped = section.name.replace(/"/g, '\\"');
|
|
70
71
|
let sectionQuery = mdq(this.text).query(`section2(~"${escaped}")`);
|
package/src/ai/researcher.ts
CHANGED
|
@@ -131,8 +131,8 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
131
131
|
await this.ensureNavigated(state.url, screenshot && this.provider.hasVision());
|
|
132
132
|
await this.hooksRunner.runBeforeHook('researcher', state.url);
|
|
133
133
|
|
|
134
|
-
const
|
|
135
|
-
debugLog(`Annotated ${
|
|
134
|
+
const annotatedElements = await this.explorer.annotateElements();
|
|
135
|
+
debugLog(`Annotated ${annotatedElements.length} interactive elements with eidx`);
|
|
136
136
|
this.actionResult = await this.explorer.createAction().capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
|
|
137
137
|
|
|
138
138
|
if (isErrorPage(this.actionResult!)) {
|
|
@@ -154,7 +154,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
154
154
|
|
|
155
155
|
const combinedHtml = await this.actionResult!.combinedHtml();
|
|
156
156
|
|
|
157
|
-
if (!deep) {
|
|
157
|
+
if (!deep && !force) {
|
|
158
158
|
const similar = await findSimilarResearch(combinedHtml);
|
|
159
159
|
if (similar) {
|
|
160
160
|
tag('info').log('Similar research found, reusing cached result');
|
package/src/ai/rules.ts
CHANGED
|
@@ -266,7 +266,7 @@ export const actionRule = dedent`
|
|
|
266
266
|
I.fillField('Username', 'John', '.login-form'); // fills Username inside .login-form
|
|
267
267
|
I.fillField('Username', 'John'); // fills the field located by name or placeholder or label "Username" with the text "John"
|
|
268
268
|
I.fillField('//user/input', 'John'); // fills the field located by XPath "//user/input" with the text "John"
|
|
269
|
-
</example>
|
|
269
|
+
</example>
|
|
270
270
|
|
|
271
271
|
### I.type
|
|
272
272
|
|
|
@@ -303,7 +303,7 @@ export const actionRule = dedent`
|
|
|
303
303
|
</example>
|
|
304
304
|
|
|
305
305
|
IMPORTANT: Requires an active/focused element for most keys.
|
|
306
|
-
Commonly used after I.type() to submit forms or navigate dropdowns.
|
|
306
|
+
Commonly used after I.type() or I.fillField() to submit forms or navigate dropdowns.
|
|
307
307
|
|
|
308
308
|
### I.switchTo
|
|
309
309
|
|
package/src/ai/tools.ts
CHANGED
|
@@ -310,7 +310,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
|
|
|
310
310
|
I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
|
|
311
311
|
|
|
312
312
|
Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
|
|
313
|
-
Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
|
|
313
|
+
Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
|
|
314
314
|
`,
|
|
315
315
|
inputSchema: z.object({
|
|
316
316
|
codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
|
|
@@ -340,8 +340,13 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
|
|
|
340
340
|
const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
|
|
341
341
|
const formLocator = codeLines[0] || 'form';
|
|
342
342
|
const action = explorer.createAction();
|
|
343
|
+
const wasInIframe = await explorer.isInsideIframe();
|
|
343
344
|
await action.attempt(codeBlock, explanation);
|
|
344
345
|
|
|
346
|
+
if (action.lastError && !wasInIframe && (await explorer.isInsideIframe())) {
|
|
347
|
+
await explorer.switchToMainFrame();
|
|
348
|
+
}
|
|
349
|
+
|
|
345
350
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, formLocator);
|
|
346
351
|
|
|
347
352
|
if (action.lastError) {
|
|
@@ -380,7 +385,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
|
|
|
380
385
|
message: `Form completed successfully with ${lines.length} commands.`,
|
|
381
386
|
commandsExecuted: lines.length,
|
|
382
387
|
code: codeBlock,
|
|
383
|
-
suggestion: 'Verify the form was filled in correctly using see() tool.
|
|
388
|
+
suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
|
|
384
389
|
});
|
|
385
390
|
} catch (error) {
|
|
386
391
|
activeNote.commit(TestResult.FAILED);
|
|
@@ -6,8 +6,7 @@ import { tag } from '../utils/logger.js';
|
|
|
6
6
|
import { BaseCommand } from './base-command.js';
|
|
7
7
|
|
|
8
8
|
export class AddRuleCommand extends BaseCommand {
|
|
9
|
-
name = '
|
|
10
|
-
aliases = ['add-rule'];
|
|
9
|
+
name = 'add-rule';
|
|
11
10
|
description = 'Create a rule file for an agent';
|
|
12
11
|
suggestions = ['/add-rule researcher check-tooltips'];
|
|
13
12
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import type { ExplorBot } from '../explorbot.js';
|
|
2
3
|
|
|
3
4
|
export interface CommandOption {
|
|
@@ -24,4 +25,16 @@ export abstract class BaseCommand {
|
|
|
24
25
|
matches(commandName: string): boolean {
|
|
25
26
|
return this.name === commandName || this.aliases.includes(commandName);
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
protected parseArgs(args: string): { opts: Record<string, string | boolean>; args: string[] } {
|
|
30
|
+
const cmd = new Command();
|
|
31
|
+
cmd.exitOverride();
|
|
32
|
+
for (const opt of this.options) {
|
|
33
|
+
cmd.option(opt.flags, opt.description);
|
|
34
|
+
}
|
|
35
|
+
cmd.argument('[args...]');
|
|
36
|
+
const argv = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).map((s) => s.replace(/^["']|["']$/g, ''));
|
|
37
|
+
cmd.parse(argv, { from: 'user' });
|
|
38
|
+
return { opts: cmd.opts(), args: cmd.args };
|
|
39
|
+
}
|
|
27
40
|
}
|
|
@@ -10,6 +10,12 @@ export class ContextCommand extends BaseCommand {
|
|
|
10
10
|
name = 'context';
|
|
11
11
|
description = 'Show page context summary (URL, headings, experience, knowledge, ARIA, HTML, research)';
|
|
12
12
|
suggestions = ['context:aria', 'context:html', 'context:knowledge', 'context:experience', 'context:data'];
|
|
13
|
+
options = [
|
|
14
|
+
{ flags: '--visual', description: 'Include annotated screenshot' },
|
|
15
|
+
{ flags: '--screenshot', description: 'Include annotated screenshot' },
|
|
16
|
+
{ flags: '--full', description: 'Show full context with HTML' },
|
|
17
|
+
{ flags: '--attached', description: 'Show attached context mode' },
|
|
18
|
+
];
|
|
13
19
|
|
|
14
20
|
async execute(args: string): Promise<void> {
|
|
15
21
|
const explorer = this.explorBot.getExplorer();
|
|
@@ -19,7 +25,8 @@ export class ContextCommand extends BaseCommand {
|
|
|
19
25
|
throw new Error('No active page to show context for');
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
const
|
|
28
|
+
const { opts } = this.parseArgs(args);
|
|
29
|
+
const isVisual = !!(opts.visual || opts.screenshot);
|
|
23
30
|
|
|
24
31
|
await explorer.annotateElements();
|
|
25
32
|
|
|
@@ -34,9 +41,9 @@ export class ContextCommand extends BaseCommand {
|
|
|
34
41
|
const knowledgeTracker = this.explorBot.getKnowledgeTracker();
|
|
35
42
|
|
|
36
43
|
let mode: ContextMode = 'compact';
|
|
37
|
-
if (
|
|
44
|
+
if (opts.full) {
|
|
38
45
|
mode = 'full';
|
|
39
|
-
} else if (
|
|
46
|
+
} else if (opts.attached) {
|
|
40
47
|
mode = 'attached';
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -3,7 +3,6 @@ import { BaseCommand } from './base-command.js';
|
|
|
3
3
|
export class DrillCommand extends BaseCommand {
|
|
4
4
|
name = 'drill';
|
|
5
5
|
description = 'Drill all components on current page to learn interactions';
|
|
6
|
-
aliases = ['bosun'];
|
|
7
6
|
suggestions = ['/research - to see UI map first', '/navigate <page> - to go to another page'];
|
|
8
7
|
|
|
9
8
|
async execute(args: string): Promise<void> {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
1
2
|
import figureSet from 'figures';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { getStyles } from '../ai/planner/styles.js';
|
|
5
|
+
import { ConfigParser } from '../config.ts';
|
|
4
6
|
import { getCliName } from '../utils/cli-name.ts';
|
|
5
7
|
import type { Plan } from '../test-plan.js';
|
|
6
8
|
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
@@ -18,13 +20,12 @@ export class ExploreCommand extends BaseCommand {
|
|
|
18
20
|
private completedPlans: Plan[] = [];
|
|
19
21
|
|
|
20
22
|
async execute(args: string): Promise<void> {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
this.maxTests = Number.parseInt(
|
|
24
|
-
args = args.replace(/--max-tests\s+\d+/, '').trim();
|
|
23
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
24
|
+
if (opts.maxTests) {
|
|
25
|
+
this.maxTests = Number.parseInt(opts.maxTests as string, 10);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const feature =
|
|
28
|
+
const feature = remaining.join(' ') || undefined;
|
|
28
29
|
const mainUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
|
|
29
30
|
|
|
30
31
|
await this.runAllStyles(mainUrl, feature);
|
|
@@ -61,6 +62,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
61
62
|
if (mainUrl) await this.explorBot.visit(mainUrl);
|
|
62
63
|
const savedPath = this.explorBot.savePlans(this.completedPlans);
|
|
63
64
|
this.printResults(savedPath);
|
|
65
|
+
this.printRerunSuggestions();
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, completedPlans?: Plan[]): Promise<void> {
|
|
@@ -78,7 +80,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
private printResults(savedPath?: string | null): void {
|
|
81
|
-
const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.
|
|
83
|
+
const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title })));
|
|
82
84
|
|
|
83
85
|
if (allTests.length === 0) return;
|
|
84
86
|
|
|
@@ -113,6 +115,20 @@ export class ExploreCommand extends BaseCommand {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
private printRerunSuggestions(): void {
|
|
119
|
+
const testsDir = ConfigParser.getInstance().getTestsDir();
|
|
120
|
+
if (!existsSync(testsDir)) return;
|
|
121
|
+
|
|
122
|
+
const testFiles = readdirSync(testsDir).filter((f) => f.endsWith('.js'));
|
|
123
|
+
if (testFiles.length === 0) return;
|
|
124
|
+
|
|
125
|
+
for (const file of testFiles) {
|
|
126
|
+
tag('info').log(`Generated: ${file}`);
|
|
127
|
+
}
|
|
128
|
+
tag('info').log(`List tests: ${getCliName()} runs`);
|
|
129
|
+
tag('info').log(`Re-run with healing: ${getCliName()} rerun <filename> [index]`);
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
private isLimitReached(): boolean {
|
|
117
133
|
return this.maxTests != null && this.testsRun >= this.maxTests;
|
|
118
134
|
}
|
|
@@ -18,7 +18,12 @@ export class FreesailCommand extends BaseCommand {
|
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
async execute(args: string): Promise<void> {
|
|
21
|
-
const {
|
|
21
|
+
const { opts } = this.parseArgs(args);
|
|
22
|
+
let strategy: 'deep' | 'shallow' | undefined;
|
|
23
|
+
if (opts.deep) strategy = 'deep';
|
|
24
|
+
if (opts.shallow) strategy = 'shallow';
|
|
25
|
+
const scope = opts.scope as string | undefined;
|
|
26
|
+
const maxTests = opts.maxTests ? Number.parseInt(opts.maxTests as string, 10) : undefined;
|
|
22
27
|
|
|
23
28
|
await this.explorBot.visitInitialState();
|
|
24
29
|
|
|
@@ -71,25 +76,3 @@ export class FreesailCommand extends BaseCommand {
|
|
|
71
76
|
);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
|
-
|
|
75
|
-
function parseArgs(args: string): { strategy: 'deep' | 'shallow' | undefined; scope: string | undefined; maxTests: number | undefined } {
|
|
76
|
-
const parts = args.trim().split(/\s+/);
|
|
77
|
-
let strategy: 'deep' | 'shallow' | undefined;
|
|
78
|
-
let scope: string | undefined;
|
|
79
|
-
let maxTests: number | undefined;
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < parts.length; i++) {
|
|
82
|
-
if (parts[i] === '--deep') strategy = 'deep';
|
|
83
|
-
if (parts[i] === '--shallow') strategy = 'shallow';
|
|
84
|
-
if (parts[i] === '--scope' && parts[i + 1]) {
|
|
85
|
-
scope = parts[i + 1];
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
if (parts[i] === '--max-tests' && parts[i + 1]) {
|
|
89
|
-
maxTests = Number.parseInt(parts[i + 1], 10);
|
|
90
|
-
i++;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { strategy, scope, maxTests };
|
|
95
|
-
}
|
package/src/commands/index.ts
CHANGED
|
@@ -24,7 +24,9 @@ import { PlanEditCommand } from './plan-edit-command.js';
|
|
|
24
24
|
import { PlanLoadCommand } from './plan-load-command.js';
|
|
25
25
|
import { PlanReloadCommand } from './plan-reload-command.js';
|
|
26
26
|
import { PlanSaveCommand } from './plan-save-command.js';
|
|
27
|
+
import { RerunCommand } from './rerun-command.js';
|
|
27
28
|
import { ResearchCommand } from './research-command.js';
|
|
29
|
+
import { RunsCommand } from './runs-command.js';
|
|
28
30
|
import { StartCommand } from './start-command.js';
|
|
29
31
|
import { StatusCommand } from './status-command.tsx';
|
|
30
32
|
import { TestCommand } from './test-command.js';
|
|
@@ -59,6 +61,8 @@ const commandClasses: CommandClass[] = [
|
|
|
59
61
|
ContextExperienceCommand,
|
|
60
62
|
ContextDataCommand,
|
|
61
63
|
TestCommand,
|
|
64
|
+
RunsCommand,
|
|
65
|
+
RerunCommand,
|
|
62
66
|
StatusCommand,
|
|
63
67
|
DebugCommand,
|
|
64
68
|
ExitCommand,
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import { log, tag } from '../utils/logger.js';
|
|
4
4
|
import dedent from 'dedent';
|
|
5
|
+
import chalk from 'chalk';
|
|
5
6
|
import { getCliName } from '../utils/cli-name.ts';
|
|
6
7
|
|
|
7
8
|
const DEFAULT_CONFIG_TEMPLATE = `import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
@@ -22,11 +23,11 @@ const config = {
|
|
|
22
23
|
|
|
23
24
|
ai: {
|
|
24
25
|
// fast model with tool calling capabilities
|
|
25
|
-
model: openrouter('
|
|
26
|
+
model: openrouter('openai/gpt-oss-20b:nitro'),
|
|
26
27
|
// vision model for screenshot analysis
|
|
27
|
-
visionModel: openrouter('
|
|
28
|
+
visionModel: openrouter('meta-llama/llama-4-scout-17b-16e-instruct'),
|
|
28
29
|
// agentic model for decision making
|
|
29
|
-
agenticModel: openrouter('
|
|
30
|
+
agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
|
|
30
31
|
},
|
|
31
32
|
};
|
|
32
33
|
|
|
@@ -103,9 +104,11 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
103
104
|
log('2. Set AI models config file');
|
|
104
105
|
log('3. Set web application URL in the config file');
|
|
105
106
|
log('4. Add initial knowledge (how to authorize to the application, etc.)');
|
|
106
|
-
tag('substep').log(`${getCliName()} learn * 'to aurhorize use these credentials: admin@example.com / secret123'`);
|
|
107
|
+
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to aurhorize use these credentials: admin@example.com / secret123'`));
|
|
108
|
+
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
109
|
+
|
|
107
110
|
log('5. Launch application on a relative URL');
|
|
108
|
-
tag('substep').log(`${getCliName()} start /dashboard`);
|
|
111
|
+
tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
|
|
109
112
|
|
|
110
113
|
if (!existsSync('./output')) {
|
|
111
114
|
mkdirSync('./output', { recursive: true });
|
|
@@ -14,7 +14,8 @@ export class PathCommand extends BaseCommand {
|
|
|
14
14
|
options = [{ flags: '--links', description: 'Show outgoing links from each page' }];
|
|
15
15
|
|
|
16
16
|
async execute(args: string): Promise<void> {
|
|
17
|
-
const
|
|
17
|
+
const { opts } = this.parseArgs(args);
|
|
18
|
+
const showLinks = !!opts.links;
|
|
18
19
|
const stateManager = this.explorBot.getExplorer().getStateManager();
|
|
19
20
|
const history = stateManager.getStateHistory();
|
|
20
21
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import figureSet from 'figures';
|
|
1
4
|
import { tag } from '../utils/logger.js';
|
|
2
5
|
import { BaseCommand } from './base-command.js';
|
|
3
6
|
|
|
@@ -9,20 +12,14 @@ export class PlanCommand extends BaseCommand {
|
|
|
9
12
|
{ flags: '--fresh', description: 'Regenerate plan from scratch' },
|
|
10
13
|
{ flags: '--clear', description: 'Clear plan before regenerating' },
|
|
11
14
|
{ flags: '--style <name>', description: 'Planning style (normal, curious, psycho, performer)' },
|
|
15
|
+
{ flags: '--focus <feature>', description: 'Focus area for test planning' },
|
|
12
16
|
];
|
|
13
17
|
|
|
14
18
|
async execute(args: string): Promise<void> {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const focus = args
|
|
20
|
-
.replace('--clear', '')
|
|
21
|
-
.replace('--fresh', '')
|
|
22
|
-
.replace(/--style\s+\S+/, '')
|
|
23
|
-
.trim();
|
|
24
|
-
|
|
25
|
-
if (clear) {
|
|
19
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
20
|
+
const focus = (opts.focus as string) || remaining.join(' ') || undefined;
|
|
21
|
+
|
|
22
|
+
if (opts.clear) {
|
|
26
23
|
this.explorBot.clearPlan();
|
|
27
24
|
tag('success').log('Plan cleared');
|
|
28
25
|
}
|
|
@@ -31,11 +28,48 @@ export class PlanCommand extends BaseCommand {
|
|
|
31
28
|
tag('info').log(`Planning focus: ${focus}`);
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
await this.explorBot.plan(focus
|
|
31
|
+
await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style as string });
|
|
35
32
|
|
|
36
33
|
const plan = this.explorBot.getCurrentPlan();
|
|
37
34
|
if (!plan?.tests.length) {
|
|
38
35
|
throw new Error('No test scenarios in the current plan.');
|
|
39
36
|
}
|
|
37
|
+
|
|
38
|
+
this.printPlanSummary();
|
|
39
|
+
this.updateSuggestions();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private printPlanSummary(): void {
|
|
43
|
+
const suite = this.explorBot.getSuite();
|
|
44
|
+
const plan = this.explorBot.getCurrentPlan();
|
|
45
|
+
|
|
46
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
47
|
+
const names = suite.getAutomatedTestNames();
|
|
48
|
+
console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
|
|
49
|
+
for (let i = 0; i < names.length; i++) {
|
|
50
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (plan?.tests.length) {
|
|
55
|
+
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
|
|
56
|
+
for (let i = 0; i < plan.tests.length; i++) {
|
|
57
|
+
const t = plan.tests[i];
|
|
58
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private updateSuggestions(): void {
|
|
64
|
+
this.suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
|
|
65
|
+
|
|
66
|
+
const suite = this.explorBot.getSuite();
|
|
67
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
68
|
+
for (const f of suite.getAutomatedTestFiles()) {
|
|
69
|
+
this.suggestions.push(`/rerun ${path.relative(process.cwd(), f)} - re-run automated tests`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.suggestions.push('Edit the plan in file and call /plan:reload to update it');
|
|
40
74
|
}
|
|
41
75
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { ConfigParser } from '../config.ts';
|
|
4
|
+
import { tag } from '../utils/logger.ts';
|
|
5
|
+
import { BaseCommand } from './base-command.js';
|
|
6
|
+
|
|
7
|
+
export class RerunCommand extends BaseCommand {
|
|
8
|
+
name = 'rerun';
|
|
9
|
+
description = 'Re-run generated tests with AI auto-healing';
|
|
10
|
+
tuiEnabled = true;
|
|
11
|
+
|
|
12
|
+
async execute(args: string): Promise<void> {
|
|
13
|
+
const { args: remaining } = this.parseArgs(args);
|
|
14
|
+
const filename = remaining[0];
|
|
15
|
+
const indexArg = remaining[1];
|
|
16
|
+
|
|
17
|
+
if (!filename) {
|
|
18
|
+
tag('error').log('Usage: /rerun <filename> [index]');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let filePath = resolve(filename);
|
|
23
|
+
if (!existsSync(filePath)) {
|
|
24
|
+
filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
28
|
+
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseTestIndices(input: string): number[] {
|
|
33
|
+
if (input === '*' || input === 'all') return [];
|
|
34
|
+
|
|
35
|
+
const indices = new Set<number>();
|
|
36
|
+
for (const part of input.split(',')) {
|
|
37
|
+
const trimmed = part.trim();
|
|
38
|
+
const range = trimmed.match(/^(\d+)-(\d+)$/);
|
|
39
|
+
if (range) {
|
|
40
|
+
for (let i = Number.parseInt(range[1]); i <= Number.parseInt(range[2]); i++) indices.add(i - 1);
|
|
41
|
+
} else {
|
|
42
|
+
indices.add(Number.parseInt(trimmed) - 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...indices].sort((a, b) => a - b);
|
|
46
|
+
}
|
|
@@ -7,12 +7,18 @@ export class ResearchCommand extends BaseCommand {
|
|
|
7
7
|
name = 'research';
|
|
8
8
|
description = 'Research current page or navigate to URI and research. Use --deep to explore interactive elements by clicking them. Use --data to include page data.';
|
|
9
9
|
suggestions = ['/navigate <page> - to go to another page', '/plan <feature> - to plan testing'];
|
|
10
|
+
options = [
|
|
11
|
+
{ flags: '--data', description: 'Include page data' },
|
|
12
|
+
{ flags: '--deep', description: 'Explore interactive elements by clicking them' },
|
|
13
|
+
{ flags: '--no-fix', description: 'Skip fixing research issues' },
|
|
14
|
+
];
|
|
10
15
|
|
|
11
16
|
async execute(args: string): Promise<void> {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
17
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
18
|
+
const includeData = !!opts.data;
|
|
19
|
+
const enableDeep = !!opts.deep;
|
|
20
|
+
const noFix = !!opts.noFix;
|
|
21
|
+
const target = remaining.join(' ');
|
|
16
22
|
|
|
17
23
|
if (target) {
|
|
18
24
|
await this.explorBot.agentNavigator().visit(target);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as codeceptjs from 'codeceptjs';
|
|
2
|
+
import { ConfigParser } from '../config.ts';
|
|
3
|
+
import { dryRunTestFile, loadTestSuites, printTestList } from '../utils/test-files.ts';
|
|
4
|
+
import { BaseCommand } from './base-command.js';
|
|
5
|
+
|
|
6
|
+
export class RunsCommand extends BaseCommand {
|
|
7
|
+
name = 'runs';
|
|
8
|
+
description = 'List generated test files and their scenarios';
|
|
9
|
+
tuiEnabled = true;
|
|
10
|
+
|
|
11
|
+
async execute(args: string): Promise<void> {
|
|
12
|
+
if (!this.explorBot.isExploring) {
|
|
13
|
+
codeceptjs.container.create({ helpers: {} }, {});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { args: remaining } = this.parseArgs(args);
|
|
17
|
+
const filePath = remaining[0];
|
|
18
|
+
|
|
19
|
+
if (filePath) {
|
|
20
|
+
await dryRunTestFile(filePath);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const suites = loadTestSuites(ConfigParser.getInstance().getTestsDir());
|
|
25
|
+
printTestList(suites);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -3,7 +3,6 @@ import { ExploreCommand } from './explore-command.js';
|
|
|
3
3
|
|
|
4
4
|
export class StartCommand extends BaseCommand {
|
|
5
5
|
name = 'start';
|
|
6
|
-
aliases = ['sail'];
|
|
7
6
|
description = 'Start web exploration';
|
|
8
7
|
suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
|
|
9
8
|
|