explorbot 0.1.10 → 0.1.12
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/README.md +37 -1
- package/bin/explorbot-cli.ts +27 -18
- package/dist/bin/explorbot-cli.js +26 -18
- package/dist/package.json +3 -3
- package/dist/rules/navigator/output.md +9 -0
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +23 -1
- package/dist/src/action.js +51 -42
- package/dist/src/ai/bosun.js +11 -1
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +321 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/screencast.js +121 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +21 -405
- package/dist/src/ai/navigator.js +82 -29
- package/dist/src/ai/pilot.js +232 -13
- package/dist/src/ai/planner.js +29 -9
- package/dist/src/ai/provider.js +54 -17
- package/dist/src/ai/researcher.js +41 -32
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +13 -7
- package/dist/src/browser-server.js +16 -3
- package/dist/src/commands/add-rule-command.js +11 -8
- package/dist/src/commands/clean-command.js +2 -1
- package/dist/src/commands/explore-command.js +43 -15
- package/dist/src/commands/init-command.js +9 -8
- package/dist/src/commands/plan-command.js +32 -0
- package/dist/src/commands/plan-save-command.js +19 -7
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +20 -64
- package/dist/src/explorbot.js +8 -8
- package/dist/src/explorer.js +11 -3
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/reporter.js +4 -1
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +20 -7
- package/dist/src/utils/next-steps.js +37 -0
- package/dist/src/utils/strings.js +15 -0
- package/package.json +3 -3
- package/rules/navigator/output.md +9 -0
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +26 -1
- package/src/action.ts +49 -41
- package/src/ai/bosun.ts +11 -1
- package/src/ai/conversation.ts +37 -0
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +384 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/screencast.ts +133 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +37 -473
- package/src/ai/navigator.ts +82 -29
- package/src/ai/pilot.ts +237 -14
- package/src/ai/planner.ts +29 -9
- package/src/ai/provider.ts +51 -17
- package/src/ai/researcher.ts +45 -33
- package/src/ai/rules.ts +27 -14
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +47 -25
- package/src/browser-server.ts +17 -3
- package/src/commands/add-rule-command.ts +11 -7
- package/src/commands/clean-command.ts +2 -1
- package/src/commands/explore-command.ts +46 -14
- package/src/commands/init-command.ts +9 -8
- package/src/commands/plan-command.ts +35 -0
- package/src/commands/plan-save-command.ts +18 -7
- package/src/commands/rerun-command.ts +5 -0
- package/src/components/App.tsx +16 -5
- package/src/config.ts +12 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +21 -72
- package/src/explorbot.ts +8 -8
- package/src/explorer.ts +13 -3
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/reporter.ts +4 -1
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +22 -7
- package/src/utils/next-steps.ts +51 -0
- package/src/utils/strings.ts +17 -0
|
@@ -7,6 +7,7 @@ export const CLEAN_TARGETS = {
|
|
|
7
7
|
states: { description: 'page states', getDir: () => outputPath('states') },
|
|
8
8
|
research: { description: 'research cache', getDir: () => outputPath('research') },
|
|
9
9
|
plans: { description: 'test plans', getDir: () => outputPath('plans') },
|
|
10
|
+
tests: { description: 'generated tests', getDir: () => outputPath('tests') },
|
|
10
11
|
experiences: { description: 'experience files', getDir: () => getExperienceDir() },
|
|
11
12
|
output: { description: 'all output files', getDir: () => outputPath() },
|
|
12
13
|
};
|
|
@@ -38,7 +39,7 @@ function cleanDirectoryContents(dirPath) {
|
|
|
38
39
|
}
|
|
39
40
|
export class CleanCommand extends BaseCommand {
|
|
40
41
|
name = 'clean';
|
|
41
|
-
description = 'Clean files: clean [states|research|plans|experiences|output]';
|
|
42
|
+
description = 'Clean files: clean [states|research|plans|tests|experiences|output]';
|
|
42
43
|
suggestions = Object.entries(CLEAN_TARGETS).map(([name, target]) => ({ command: `clean ${name}`, hint: `clean ${target.description}` }));
|
|
43
44
|
async execute(args) {
|
|
44
45
|
const target = args.trim().toLowerCase();
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import figureSet from 'figures';
|
|
3
2
|
import { getStyles } from '../ai/planner/styles.js';
|
|
3
|
+
import { outputPath } from '../config.js';
|
|
4
4
|
import { Stats } from '../stats.js';
|
|
5
5
|
import { getCliName } from "../utils/cli-name.js";
|
|
6
6
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
7
7
|
import { tag } from '../utils/logger.js';
|
|
8
8
|
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
9
|
+
import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
|
|
10
|
+
import { safeFilename } from "../utils/strings.js";
|
|
9
11
|
import { BaseCommand } from './base-command.js';
|
|
10
12
|
export class ExploreCommand extends BaseCommand {
|
|
11
13
|
name = 'explore';
|
|
@@ -65,8 +67,8 @@ export class ExploreCommand extends BaseCommand {
|
|
|
65
67
|
if (mainUrl)
|
|
66
68
|
await this.explorBot.visit(mainUrl);
|
|
67
69
|
const savedPath = this.explorBot.savePlans(this.completedPlans);
|
|
68
|
-
this.printResults(
|
|
69
|
-
this.
|
|
70
|
+
this.printResults();
|
|
71
|
+
this.printNextSteps(savedPath);
|
|
70
72
|
}
|
|
71
73
|
async runAllStyles(pageUrl, feature, parentPlan, completedPlans) {
|
|
72
74
|
let fresh = true;
|
|
@@ -97,7 +99,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
97
99
|
tag('warning').log(`Planning style '${opts.style}' failed after retry, skipping`);
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
|
-
printResults(
|
|
102
|
+
printResults() {
|
|
101
103
|
const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title })));
|
|
102
104
|
if (allTests.length === 0)
|
|
103
105
|
return;
|
|
@@ -128,20 +130,46 @@ export class ExploreCommand extends BaseCommand {
|
|
|
128
130
|
columns.push('Plan');
|
|
129
131
|
tag('multiline').log(jsonToTable(rows, columns));
|
|
130
132
|
tag('info').log(`${figureSet.tick} ${allTests.length} tests completed`);
|
|
131
|
-
if (savedPath) {
|
|
132
|
-
const relativePath = path.relative(process.cwd(), savedPath);
|
|
133
|
-
tag('info').log(`Re-run tests: ${getCliName()} test ${relativePath} <index>`);
|
|
134
|
-
}
|
|
135
133
|
}
|
|
136
|
-
|
|
134
|
+
printNextSteps(savedPlanPath) {
|
|
135
|
+
const cli = getCliName();
|
|
136
|
+
const sections = [];
|
|
137
|
+
if (savedPlanPath) {
|
|
138
|
+
const relPlan = relativeToCwd(savedPlanPath);
|
|
139
|
+
sections.push({
|
|
140
|
+
label: 'Plan',
|
|
141
|
+
path: savedPlanPath,
|
|
142
|
+
commands: [
|
|
143
|
+
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
144
|
+
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
145
|
+
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
137
149
|
const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
const screencasts = savedFiles.filter((f) => f.endsWith('.webm'));
|
|
151
|
+
const testFiles = savedFiles.filter((f) => !f.endsWith('.webm'));
|
|
152
|
+
if (testFiles.length > 0) {
|
|
153
|
+
const commands = testFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
|
|
154
|
+
commands.push({ label: 'List tests', command: `${cli} runs` });
|
|
155
|
+
sections.push({
|
|
156
|
+
label: `Generated tests (${testFiles.length})`,
|
|
157
|
+
commands,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (screencasts.length > 0) {
|
|
161
|
+
const commands = screencasts.map((f) => ({ label: '', command: relativeToCwd(f) }));
|
|
162
|
+
const screencastDir = relativeToCwd(outputPath('screencasts'));
|
|
163
|
+
const planSlugs = [...new Set(this.completedPlans.map((p) => safeFilename(p.title)).filter(Boolean))];
|
|
164
|
+
for (const slug of planSlugs) {
|
|
165
|
+
commands.push({ label: 'Browse plan', command: `ls ${screencastDir}/${slug}-*` });
|
|
166
|
+
}
|
|
167
|
+
sections.push({
|
|
168
|
+
label: `Screencasts (${screencasts.length})`,
|
|
169
|
+
commands,
|
|
170
|
+
});
|
|
142
171
|
}
|
|
143
|
-
|
|
144
|
-
tag('info').log(`Re-run with healing: ${getCliName()} rerun <filename> [index]`);
|
|
172
|
+
printNextSteps(sections);
|
|
145
173
|
}
|
|
146
174
|
isLimitReached() {
|
|
147
175
|
return this.maxTests != null && this.testsRun >= this.maxTests;
|
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import dedent from 'dedent';
|
|
5
5
|
import { getCliName } from "../utils/cli-name.js";
|
|
6
6
|
import { log, tag } from '../utils/logger.js';
|
|
7
|
+
import { relativeToCwd } from "../utils/next-steps.js";
|
|
7
8
|
const DEFAULT_CONFIG_TEMPLATE = `import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
8
9
|
// import { '<your provider here>' } from '<your provider package here>';
|
|
9
10
|
|
|
@@ -57,10 +58,10 @@ export function runInitCommand(options) {
|
|
|
57
58
|
const dir = resolve(customPath);
|
|
58
59
|
if (!existsSync(dir)) {
|
|
59
60
|
mkdirSync(dir, { recursive: true });
|
|
60
|
-
log(`Created directory: ${dir}`);
|
|
61
|
+
log(`Created directory: ${relativeToCwd(dir)}`);
|
|
61
62
|
}
|
|
62
63
|
process.chdir(dir);
|
|
63
|
-
log(`Working in directory: ${dir}`);
|
|
64
|
+
log(`Working in directory: ${relativeToCwd(dir)}`);
|
|
64
65
|
}
|
|
65
66
|
try {
|
|
66
67
|
let outPath = resolve(configPath);
|
|
@@ -73,22 +74,22 @@ export function runInitCommand(options) {
|
|
|
73
74
|
const dir = dirname(outPath);
|
|
74
75
|
if (!existsSync(dir)) {
|
|
75
76
|
mkdirSync(dir, { recursive: true });
|
|
76
|
-
log(`Created directory: ${dir}`);
|
|
77
|
+
log(`Created directory: ${relativeToCwd(dir)}`);
|
|
77
78
|
}
|
|
78
79
|
if (existsSync(outPath) && !force) {
|
|
79
|
-
log(`Config file already exists: ${outPath}`);
|
|
80
|
+
log(`Config file already exists: ${relativeToCwd(outPath)}`);
|
|
80
81
|
log('Use --force to overwrite existing file');
|
|
81
82
|
process.exit(1);
|
|
82
83
|
}
|
|
83
84
|
writeFileSync(outPath, DEFAULT_CONFIG_TEMPLATE, 'utf8');
|
|
84
|
-
log(`Created config file: ${outPath}`);
|
|
85
|
+
log(`Created config file: ${relativeToCwd(outPath)}`);
|
|
85
86
|
const envPath = resolve(process.cwd(), '.env');
|
|
86
87
|
if (!existsSync(envPath)) {
|
|
87
88
|
writeFileSync(envPath, `${DEFAULT_ENV_TEMPLATE}\n`, 'utf8');
|
|
88
|
-
log(`Created env file: ${envPath}`);
|
|
89
|
+
log(`Created env file: ${relativeToCwd(envPath)}`);
|
|
89
90
|
}
|
|
90
91
|
else {
|
|
91
|
-
log(`Env file already exists: ${envPath}`);
|
|
92
|
+
log(`Env file already exists: ${relativeToCwd(envPath)}`);
|
|
92
93
|
}
|
|
93
94
|
log('');
|
|
94
95
|
log('Next steps:');
|
|
@@ -102,7 +103,7 @@ export function runInitCommand(options) {
|
|
|
102
103
|
tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
|
|
103
104
|
if (!existsSync('./output')) {
|
|
104
105
|
mkdirSync('./output', { recursive: true });
|
|
105
|
-
log('Created directory:
|
|
106
|
+
log('Created directory: output');
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
catch (error) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import figureSet from 'figures';
|
|
4
|
+
import { getCliName } from "../utils/cli-name.js";
|
|
4
5
|
import { tag } from '../utils/logger.js';
|
|
6
|
+
import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
|
|
5
7
|
import { BaseCommand } from './base-command.js';
|
|
6
8
|
export class PlanCommand extends BaseCommand {
|
|
7
9
|
name = 'plan';
|
|
@@ -34,6 +36,36 @@ export class PlanCommand extends BaseCommand {
|
|
|
34
36
|
}
|
|
35
37
|
this.printPlanSummary();
|
|
36
38
|
this.updateSuggestions();
|
|
39
|
+
this.printNextSteps();
|
|
40
|
+
}
|
|
41
|
+
printNextSteps() {
|
|
42
|
+
const savedPath = this.explorBot.lastSavedPlanPath;
|
|
43
|
+
if (!savedPath)
|
|
44
|
+
return;
|
|
45
|
+
const cli = getCliName();
|
|
46
|
+
const relPlan = relativeToCwd(savedPath);
|
|
47
|
+
const sections = [
|
|
48
|
+
{
|
|
49
|
+
label: 'Plan',
|
|
50
|
+
path: savedPath,
|
|
51
|
+
commands: [
|
|
52
|
+
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
53
|
+
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
54
|
+
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
55
|
+
{ label: 'Reload', command: `/plan:load ${relPlan}` },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
const suite = this.explorBot.getSuite();
|
|
60
|
+
const files = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
61
|
+
if (files.length > 0) {
|
|
62
|
+
const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
|
|
63
|
+
sections.push({
|
|
64
|
+
label: `Automated tests (${files.length})`,
|
|
65
|
+
commands,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
printNextSteps(sections);
|
|
37
69
|
}
|
|
38
70
|
printPlanSummary() {
|
|
39
71
|
const suite = this.explorBot.getSuite();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { getCliName } from "../utils/cli-name.js";
|
|
2
|
+
import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
|
|
3
3
|
import { BaseCommand } from './base-command.js';
|
|
4
4
|
export class PlanSaveCommand extends BaseCommand {
|
|
5
5
|
name = 'plan:save';
|
|
@@ -12,10 +12,22 @@ export class PlanSaveCommand extends BaseCommand {
|
|
|
12
12
|
}
|
|
13
13
|
const filename = args.trim() || undefined;
|
|
14
14
|
const savedPath = this.explorBot.savePlan(filename);
|
|
15
|
-
if (savedPath)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (!savedPath)
|
|
16
|
+
return;
|
|
17
|
+
const cli = getCliName();
|
|
18
|
+
const relPlan = relativeToCwd(savedPath);
|
|
19
|
+
const sections = [
|
|
20
|
+
{
|
|
21
|
+
label: 'Plan',
|
|
22
|
+
path: savedPath,
|
|
23
|
+
commands: [
|
|
24
|
+
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
25
|
+
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
26
|
+
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
27
|
+
{ label: 'Reload', command: `/plan:load ${relPlan}` },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
printNextSteps(sections);
|
|
20
32
|
}
|
|
21
33
|
}
|
|
@@ -19,6 +19,10 @@ export class RerunCommand extends BaseCommand {
|
|
|
19
19
|
if (!existsSync(filePath)) {
|
|
20
20
|
filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
|
|
21
21
|
}
|
|
22
|
+
if (filePath.endsWith('.spec.ts') || filePath.endsWith('.spec.js')) {
|
|
23
|
+
tag('error').log(`Rerun does not support Playwright tests. Run them with: npx playwright test ${filePath}`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
23
27
|
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
24
28
|
}
|
|
@@ -72,16 +72,27 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
72
72
|
setInterruptPrompt(prompt);
|
|
73
73
|
setShowInput(true);
|
|
74
74
|
return new Promise((resolve) => {
|
|
75
|
-
interruptResolveRef.current =
|
|
75
|
+
interruptResolveRef.current = (value) => {
|
|
76
|
+
interruptResolveRef.current = null;
|
|
77
|
+
setInterruptPrompt(null);
|
|
78
|
+
resolve(value);
|
|
79
|
+
};
|
|
76
80
|
});
|
|
77
81
|
});
|
|
78
82
|
const handleIdle = () => {
|
|
79
83
|
setShowInput(true);
|
|
80
84
|
};
|
|
85
|
+
const handleInterrupt = () => {
|
|
86
|
+
if (interruptResolveRef.current) {
|
|
87
|
+
interruptResolveRef.current(null);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
81
90
|
executionController.on('idle', handleIdle);
|
|
91
|
+
executionController.on('interrupt', handleInterrupt);
|
|
82
92
|
setInputCallbackReady(true);
|
|
83
93
|
return () => {
|
|
84
94
|
executionController.off('idle', handleIdle);
|
|
95
|
+
executionController.off('interrupt', handleInterrupt);
|
|
85
96
|
executionController.reset();
|
|
86
97
|
};
|
|
87
98
|
}, []);
|
|
@@ -244,9 +255,10 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
244
255
|
return;
|
|
245
256
|
}
|
|
246
257
|
if (isCommand) {
|
|
247
|
-
|
|
258
|
+
if (interruptResolveRef.current) {
|
|
259
|
+
interruptResolveRef.current(null);
|
|
260
|
+
}
|
|
248
261
|
setShowInput(false);
|
|
249
|
-
interruptResolveRef.current = null;
|
|
250
262
|
executionController.startExecution();
|
|
251
263
|
try {
|
|
252
264
|
await commandHandler.executeCommand(trimmed);
|
|
@@ -264,8 +276,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
264
276
|
}
|
|
265
277
|
if (interruptResolveRef.current) {
|
|
266
278
|
interruptResolveRef.current(input);
|
|
267
|
-
interruptResolveRef.current = null;
|
|
268
|
-
setInterruptPrompt(null);
|
|
269
279
|
setShowInput(false);
|
|
270
280
|
return;
|
|
271
281
|
}
|
|
@@ -7,6 +7,7 @@ export class ExecutionController extends EventEmitter {
|
|
|
7
7
|
inputCallback = null;
|
|
8
8
|
interruptResolvers = [];
|
|
9
9
|
abortController = null;
|
|
10
|
+
awaitingInput = false;
|
|
10
11
|
constructor() {
|
|
11
12
|
super();
|
|
12
13
|
}
|
|
@@ -39,6 +40,9 @@ export class ExecutionController extends EventEmitter {
|
|
|
39
40
|
this.interruptResolvers = [];
|
|
40
41
|
this.emit('idle');
|
|
41
42
|
}
|
|
43
|
+
isAwaitingInput() {
|
|
44
|
+
return this.awaitingInput;
|
|
45
|
+
}
|
|
42
46
|
isInterrupted() {
|
|
43
47
|
return this.interrupted;
|
|
44
48
|
}
|
|
@@ -64,10 +68,16 @@ export class ExecutionController extends EventEmitter {
|
|
|
64
68
|
return userInput;
|
|
65
69
|
}
|
|
66
70
|
async requestInput(prompt) {
|
|
67
|
-
if (this.inputCallback) {
|
|
71
|
+
if (!this.inputCallback) {
|
|
72
|
+
return await this.readlineInput(prompt);
|
|
73
|
+
}
|
|
74
|
+
this.awaitingInput = true;
|
|
75
|
+
try {
|
|
68
76
|
return await this.inputCallback(prompt);
|
|
69
77
|
}
|
|
70
|
-
|
|
78
|
+
finally {
|
|
79
|
+
this.awaitingInput = false;
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
async readlineInput(prompt) {
|
|
73
83
|
const rl = readline.createInterface({
|
|
@@ -86,6 +96,7 @@ export class ExecutionController extends EventEmitter {
|
|
|
86
96
|
this.interrupted = false;
|
|
87
97
|
this.interruptResolvers = [];
|
|
88
98
|
this.abortController = null;
|
|
99
|
+
this.awaitingInput = false;
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
export const executionController = ExecutionController.getInstance();
|
|
@@ -13,15 +13,17 @@ export const RECENT_WINDOW_DAYS = 30;
|
|
|
13
13
|
/**
|
|
14
14
|
* Stores and reads per-page experience files (`./experience/<stateHash>.md`).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* Two writers, two contracts:
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* writeFlow(state, body, relatedUrls?) — caller hands in a fully-formatted
|
|
19
|
+
* `## FLOW: <imperative title>` block (multi-step,
|
|
20
|
+
* `*` bullets + optional ```js``` + `>` discovery,
|
|
21
|
+
* ends with `---`). Tracker dedups + prepends.
|
|
22
|
+
* writeAction(state, ActionInput) — `## ACTION: <imperative title>`, single-step,
|
|
23
|
+
* optional `Solution:` line + one ```js``` code block.
|
|
24
|
+
* Title normalized via normalizeTitle().
|
|
20
25
|
*
|
|
21
26
|
* - Always h2. Never h3 for FLOW/ACTION.
|
|
22
|
-
* - Title is an imperative verb phrase, lowercase-first, no trailing punctuation.
|
|
23
|
-
* Writers normalize automatically (strip own `FLOW:` / `ACTION:` prefix if the caller included it,
|
|
24
|
-
* lowercase first char, trim trailing `.!?,;:`).
|
|
25
27
|
* - On read (getSuccessfulExperience), headings are rendered as
|
|
26
28
|
* `## HOW to <title> (multi-step|single-step)` so prompts get natural phrasing.
|
|
27
29
|
*/
|
|
@@ -159,27 +161,25 @@ export class ExperienceTracker {
|
|
|
159
161
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
160
162
|
tag('substep').log(` Added ACTION to: ${stateHash}.md`);
|
|
161
163
|
}
|
|
162
|
-
writeFlow(state,
|
|
164
|
+
writeFlow(state, body, relatedUrls) {
|
|
163
165
|
if (this.disabled || this.isWritingDisabled(state))
|
|
164
166
|
return;
|
|
165
|
-
if (!
|
|
167
|
+
if (!body?.trim())
|
|
166
168
|
return;
|
|
167
169
|
this.ensureExperienceFile(state);
|
|
168
170
|
const stateHash = state.getStateHash();
|
|
169
171
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
170
|
-
if (
|
|
172
|
+
if (content.includes(body)) {
|
|
173
|
+
debugLog('Skipping duplicate flow body');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (relatedUrls?.length) {
|
|
171
177
|
const currentPath = extractStatePath(state.url || '');
|
|
172
178
|
const existingRelated = Array.isArray(data.related) ? data.related : [];
|
|
173
|
-
const allRelated = [...new Set([...existingRelated, ...
|
|
179
|
+
const allRelated = [...new Set([...existingRelated, ...relatedUrls])];
|
|
174
180
|
data.related = allRelated.filter((url) => url !== currentPath);
|
|
175
181
|
}
|
|
176
|
-
const
|
|
177
|
-
if (!title)
|
|
178
|
-
return;
|
|
179
|
-
const sessionContent = this.trimSessionContent(generateFlowContent(title, flow.steps));
|
|
180
|
-
if (!sessionContent)
|
|
181
|
-
return;
|
|
182
|
-
const updatedContent = `${sessionContent}\n${content}`;
|
|
182
|
+
const updatedContent = `${body}\n${content}`;
|
|
183
183
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
184
184
|
tag('substep').log(`Added FLOW to: ${stateHash}.md`);
|
|
185
185
|
}
|
|
@@ -245,33 +245,6 @@ export class ExperienceTracker {
|
|
|
245
245
|
// Clear any in-memory state if needed
|
|
246
246
|
// The actual files will be cleaned up by test cleanup
|
|
247
247
|
}
|
|
248
|
-
trimSessionContent(content) {
|
|
249
|
-
const q = mdq(content);
|
|
250
|
-
if (q.query('heading').count() === 0)
|
|
251
|
-
return null;
|
|
252
|
-
if (q.query('code').count() === 0)
|
|
253
|
-
return null;
|
|
254
|
-
let result = content;
|
|
255
|
-
const codeBlocks = q.query('code').each();
|
|
256
|
-
if (codeBlocks.length > 2) {
|
|
257
|
-
for (const block of codeBlocks.slice(2)) {
|
|
258
|
-
result = result.replace(block.text(), '');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const blockquotes = mdq(result).query('blockquote').each();
|
|
262
|
-
if (blockquotes.length > 5) {
|
|
263
|
-
for (const bq of blockquotes.slice(5)) {
|
|
264
|
-
result = result.replace(bq.text(), '');
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const lines = result.split('\n');
|
|
268
|
-
if (lines.length > 40) {
|
|
269
|
-
result = lines.slice(0, 40).join('\n');
|
|
270
|
-
}
|
|
271
|
-
if (!result.trim())
|
|
272
|
-
return null;
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
248
|
getSuccessfulExperience(state, options) {
|
|
276
249
|
const records = this.getRelevantExperience(state, {
|
|
277
250
|
includeDescendantExperience: options?.includeDescendants,
|
|
@@ -469,7 +442,9 @@ export function renderExperienceToc(toc) {
|
|
|
469
442
|
return '';
|
|
470
443
|
const lines = [];
|
|
471
444
|
lines.push('<experience>');
|
|
472
|
-
lines.push('Past experience for this page —
|
|
445
|
+
lines.push('Past experience for this page — recipes recorded from prior successful runs.');
|
|
446
|
+
lines.push('Locators and step ordering worked then; the page may have changed since.');
|
|
447
|
+
lines.push('Treat as a starting hypothesis, not ground truth. If a step fails, fall back to ARIA/UI-map.');
|
|
473
448
|
lines.push('FLOW: = multi-step recipe (bullets + code + discovery). ACTION: = single-step snippet (one code block).');
|
|
474
449
|
lines.push('Call learn_experience({ fileTag, sectionIndex }) to read a section when it looks relevant to the current step.');
|
|
475
450
|
lines.push('');
|
|
@@ -513,25 +488,6 @@ function generateActionContent(title, code, explanation) {
|
|
|
513
488
|
lines.push('');
|
|
514
489
|
return lines.join('\n');
|
|
515
490
|
}
|
|
516
|
-
function generateFlowContent(title, steps) {
|
|
517
|
-
let content = `## FLOW: ${title}\n\n`;
|
|
518
|
-
for (const step of steps) {
|
|
519
|
-
content += `* ${step.message}\n\n`;
|
|
520
|
-
if (step.code) {
|
|
521
|
-
content += '```js\n';
|
|
522
|
-
content += `${step.code}\n`;
|
|
523
|
-
content += '```\n\n';
|
|
524
|
-
}
|
|
525
|
-
if (step.discovery) {
|
|
526
|
-
const discoveries = step.discovery.split('\n').filter((d) => d.trim());
|
|
527
|
-
for (const discovery of discoveries) {
|
|
528
|
-
content += `> ${discovery.trim()}\n\n`;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
content += '---\n';
|
|
533
|
-
return content;
|
|
534
|
-
}
|
|
535
491
|
function renderAsHowTo(content) {
|
|
536
492
|
const tokens = marked.lexer(content);
|
|
537
493
|
let result = '';
|
package/dist/src/explorbot.js
CHANGED
|
@@ -36,6 +36,7 @@ export class ExplorBot {
|
|
|
36
36
|
currentPlan;
|
|
37
37
|
planFeature;
|
|
38
38
|
lastPlanError = null;
|
|
39
|
+
lastSavedPlanPath = null;
|
|
39
40
|
agents = {};
|
|
40
41
|
constructor(options = {}) {
|
|
41
42
|
this.options = options;
|
|
@@ -214,10 +215,13 @@ export class ExplorBot {
|
|
|
214
215
|
return this.agents.quartermaster;
|
|
215
216
|
}
|
|
216
217
|
agentHistorian() {
|
|
217
|
-
return (this.agents.historian ||= this.createAgent(({ ai, explorer }) => {
|
|
218
|
+
return (this.agents.historian ||= this.createAgent(({ ai, explorer, config }) => {
|
|
218
219
|
const experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
219
220
|
const reporter = explorer.getReporter();
|
|
220
|
-
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager()
|
|
221
|
+
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager(), config, {
|
|
222
|
+
recorder: explorer.getPlaywrightRecorder(),
|
|
223
|
+
helper: explorer.playwrightHelper,
|
|
224
|
+
});
|
|
221
225
|
}));
|
|
222
226
|
}
|
|
223
227
|
agentRerunner() {
|
|
@@ -314,12 +318,7 @@ export class ExplorBot {
|
|
|
314
318
|
return undefined;
|
|
315
319
|
return this.currentPlan;
|
|
316
320
|
}
|
|
317
|
-
|
|
318
|
-
if (savedPath) {
|
|
319
|
-
const relativePath = path.relative(process.cwd(), savedPath);
|
|
320
|
-
tag('info').log(`Plan saved to: ${relativePath}`);
|
|
321
|
-
tag('info').log(`Edit the plan file and run /plan:load ${relativePath} to reload it`);
|
|
322
|
-
}
|
|
321
|
+
this.savePlan();
|
|
323
322
|
return this.currentPlan;
|
|
324
323
|
}
|
|
325
324
|
getPlansDir() {
|
|
@@ -341,6 +340,7 @@ export class ExplorBot {
|
|
|
341
340
|
const planFilename = filename || this.generatePlanFilename();
|
|
342
341
|
const planPath = path.join(plansDir, planFilename);
|
|
343
342
|
Plan.saveMultipleToMarkdown(plans, planPath);
|
|
343
|
+
this.lastSavedPlanPath = planPath;
|
|
344
344
|
return planPath;
|
|
345
345
|
}
|
|
346
346
|
generatePlanFilename() {
|
package/dist/src/explorer.js
CHANGED
|
@@ -12,6 +12,7 @@ import { RequestStore } from "./api/request-store.js";
|
|
|
12
12
|
import { XhrCapture } from "./api/xhr-capture.js";
|
|
13
13
|
import { ConfigParser, outputPath } from './config.js';
|
|
14
14
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
15
|
+
import { PlaywrightRecorder } from "./playwright-recorder.js";
|
|
15
16
|
import { Reporter } from "./reporter.js";
|
|
16
17
|
import { StateManager } from './state-manager.js';
|
|
17
18
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
@@ -35,6 +36,7 @@ class Explorer {
|
|
|
35
36
|
_activeTest = null;
|
|
36
37
|
xhrCapture = null;
|
|
37
38
|
requestStore = null;
|
|
39
|
+
playwrightRecorder = new PlaywrightRecorder();
|
|
38
40
|
constructor(config, aiProvider, options) {
|
|
39
41
|
this.config = config;
|
|
40
42
|
this.aiProvider = aiProvider;
|
|
@@ -89,7 +91,7 @@ class Explorer {
|
|
|
89
91
|
tag('substep').log(debugInfo);
|
|
90
92
|
}
|
|
91
93
|
const PlaywrightConfig = {
|
|
92
|
-
timeout:
|
|
94
|
+
timeout: 3000,
|
|
93
95
|
highlightElement: true,
|
|
94
96
|
waitForAction: 500,
|
|
95
97
|
...playwrightConfig,
|
|
@@ -188,6 +190,7 @@ class Explorer {
|
|
|
188
190
|
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
189
191
|
const contextOptions = hasSession ? { storageState: this.options.session } : undefined;
|
|
190
192
|
await this.playwrightHelper._createContextPage(contextOptions);
|
|
193
|
+
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
191
194
|
this.setupXhrCapture();
|
|
192
195
|
if (hasSession) {
|
|
193
196
|
tag('info').log(`Session restored from ${path.relative(process.cwd(), this.options.session)}`);
|
|
@@ -216,7 +219,10 @@ class Explorer {
|
|
|
216
219
|
await this.playwrightHelper._startBrowser();
|
|
217
220
|
}
|
|
218
221
|
createAction() {
|
|
219
|
-
return new Action(this.actor, this.stateManager);
|
|
222
|
+
return new Action(this.actor, this.stateManager, this.playwrightRecorder);
|
|
223
|
+
}
|
|
224
|
+
getPlaywrightRecorder() {
|
|
225
|
+
return this.playwrightRecorder;
|
|
220
226
|
}
|
|
221
227
|
async visit(url) {
|
|
222
228
|
await this.closeOtherTabs();
|
|
@@ -411,6 +417,7 @@ class Explorer {
|
|
|
411
417
|
if (this.xhrCapture && this.playwrightHelper?.page) {
|
|
412
418
|
this.xhrCapture.detach(this.playwrightHelper.page);
|
|
413
419
|
}
|
|
420
|
+
await this.playwrightRecorder.stop();
|
|
414
421
|
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
415
422
|
const dir = path.dirname(this.options.session);
|
|
416
423
|
if (!existsSync(dir))
|
|
@@ -605,6 +612,7 @@ function toCodeceptjsTest(test) {
|
|
|
605
612
|
codeceptjsTest.fullTitle = () => `${parent.title} ${test.scenario}`;
|
|
606
613
|
codeceptjsTest.state = 'pending';
|
|
607
614
|
codeceptjsTest.notes = test.getPrintableNotes();
|
|
615
|
+
codeceptjsTest._explorbotTest = test;
|
|
608
616
|
return codeceptjsTest;
|
|
609
617
|
}
|
|
610
618
|
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
@@ -622,7 +630,7 @@ function parseAriaRefs(ariaSnapshot) {
|
|
|
622
630
|
return entries;
|
|
623
631
|
}
|
|
624
632
|
export async function annotatePageElements(page) {
|
|
625
|
-
const ariaSnapshot = await page.locator('body').ariaSnapshot({
|
|
633
|
+
const ariaSnapshot = await page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
626
634
|
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
627
635
|
const byRole = new Map();
|
|
628
636
|
for (const { role, name, ref } of refEntries) {
|