explorbot 0.1.9 → 0.1.11
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 +27 -1
- package/bin/explorbot-cli.ts +86 -15
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +85 -14
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +2 -2
- 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 +46 -38
- package/dist/src/ai/bosun.js +16 -2
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +320 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +19 -398
- package/dist/src/ai/navigator.js +133 -80
- package/dist/src/ai/pilot.js +254 -13
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +33 -13
- package/dist/src/ai/provider.js +55 -18
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +43 -41
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +18 -10
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/browser-server.js +16 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +12 -9
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +3 -2
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +54 -19
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +11 -10
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +43 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +20 -8
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +174 -83
- package/dist/src/explorbot.js +31 -22
- package/dist/src/explorer.js +12 -5
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +30 -7
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/next-steps.js +37 -0
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +2 -2
- 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 +44 -37
- package/src/ai/bosun.ts +16 -2
- package/src/ai/conversation.ts +37 -0
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +383 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +35 -468
- package/src/ai/navigator.ts +140 -85
- package/src/ai/pilot.ts +259 -14
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +34 -14
- package/src/ai/provider.ts +52 -18
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +47 -42
- package/src/ai/rules.ts +27 -14
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +53 -29
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/browser-server.ts +17 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +13 -9
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +4 -3
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +58 -21
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +11 -10
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +47 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +20 -9
- package/src/commands/rerun-command.ts +5 -0
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/components/App.tsx +16 -5
- package/src/config.ts +6 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +198 -100
- package/src/explorbot.ts +33 -23
- package/src/explorer.ts +14 -5
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +32 -7
- package/src/utils/logger.ts +1 -1
- package/src/utils/next-steps.ts +51 -0
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
|
@@ -1,12 +1,18 @@
|
|
|
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';
|
|
8
10
|
description = 'Plan testing for a feature';
|
|
9
|
-
suggestions = [
|
|
11
|
+
suggestions = [
|
|
12
|
+
{ command: 'test', hint: 'launch first test' },
|
|
13
|
+
{ command: 'test *', hint: 'launch all tests' },
|
|
14
|
+
{ command: 'plan:reload', hint: 'after editing the plan file, reload it' },
|
|
15
|
+
];
|
|
10
16
|
options = [
|
|
11
17
|
{ flags: '--fresh', description: 'Regenerate plan from scratch' },
|
|
12
18
|
{ flags: '--clear', description: 'Clear plan before regenerating' },
|
|
@@ -30,6 +36,36 @@ export class PlanCommand extends BaseCommand {
|
|
|
30
36
|
}
|
|
31
37
|
this.printPlanSummary();
|
|
32
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);
|
|
33
69
|
}
|
|
34
70
|
printPlanSummary() {
|
|
35
71
|
const suite = this.explorBot.getSuite();
|
|
@@ -50,13 +86,16 @@ export class PlanCommand extends BaseCommand {
|
|
|
50
86
|
}
|
|
51
87
|
}
|
|
52
88
|
updateSuggestions() {
|
|
53
|
-
this.suggestions = [
|
|
89
|
+
this.suggestions = [
|
|
90
|
+
{ command: 'test', hint: 'launch first test' },
|
|
91
|
+
{ command: 'test *', hint: 'launch all tests' },
|
|
92
|
+
];
|
|
54
93
|
const suite = this.explorBot.getSuite();
|
|
55
94
|
if (suite && suite.automatedTestCount > 0) {
|
|
56
95
|
for (const f of suite.getAutomatedTestFiles()) {
|
|
57
|
-
this.suggestions.push(
|
|
96
|
+
this.suggestions.push({ command: `rerun ${path.relative(process.cwd(), f)}`, hint: 're-run automated tests' });
|
|
58
97
|
}
|
|
59
98
|
}
|
|
60
|
-
this.suggestions.push(
|
|
99
|
+
this.suggestions.push({ command: 'plan:reload', hint: 'after editing the plan file, reload it' });
|
|
61
100
|
}
|
|
62
101
|
}
|
|
@@ -2,6 +2,6 @@ import { BaseCommand } from './base-command.js';
|
|
|
2
2
|
export class PlanEditCommand extends BaseCommand {
|
|
3
3
|
name = 'plan:edit';
|
|
4
4
|
description = 'Open test plan editor';
|
|
5
|
-
suggestions = ['
|
|
5
|
+
suggestions = [{ command: 'plan:edit', hint: 'toggle tests on/off' }];
|
|
6
6
|
async execute(_args) { }
|
|
7
7
|
}
|
|
@@ -3,7 +3,10 @@ import { BaseCommand } from './base-command.js';
|
|
|
3
3
|
export class PlanLoadCommand extends BaseCommand {
|
|
4
4
|
name = 'plan:load';
|
|
5
5
|
description = 'Load plan from file';
|
|
6
|
-
suggestions = [
|
|
6
|
+
suggestions = [
|
|
7
|
+
{ command: 'test', hint: 'launch first test' },
|
|
8
|
+
{ command: 'test *', hint: 'launch all tests' },
|
|
9
|
+
];
|
|
7
10
|
async execute(args) {
|
|
8
11
|
const filename = args.trim();
|
|
9
12
|
if (!filename) {
|
|
@@ -3,7 +3,10 @@ import { BaseCommand } from './base-command.js';
|
|
|
3
3
|
export class PlanReloadCommand extends BaseCommand {
|
|
4
4
|
name = 'plan:reload';
|
|
5
5
|
description = 'Clear current plan and regenerate';
|
|
6
|
-
suggestions = [
|
|
6
|
+
suggestions = [
|
|
7
|
+
{ command: 'test', hint: 'launch first test' },
|
|
8
|
+
{ command: 'test *', hint: 'launch all tests' },
|
|
9
|
+
];
|
|
7
10
|
async execute(args) {
|
|
8
11
|
const currentPlan = this.explorBot.getCurrentPlan();
|
|
9
12
|
if (!currentPlan) {
|
|
@@ -1,10 +1,10 @@
|
|
|
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';
|
|
6
6
|
description = 'Save current plan to file';
|
|
7
|
-
suggestions = ['
|
|
7
|
+
suggestions = [{ command: 'test', hint: 'launch first test' }];
|
|
8
8
|
async execute(args) {
|
|
9
9
|
const plan = this.explorBot.getCurrentPlan();
|
|
10
10
|
if (!plan) {
|
|
@@ -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
|
}
|
|
@@ -5,7 +5,10 @@ import { BaseCommand } from './base-command.js';
|
|
|
5
5
|
export class ResearchCommand extends BaseCommand {
|
|
6
6
|
name = 'research';
|
|
7
7
|
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.';
|
|
8
|
-
suggestions = [
|
|
8
|
+
suggestions = [
|
|
9
|
+
{ command: 'navigate <page>', hint: 'go to another page' },
|
|
10
|
+
{ command: 'plan <feature>', hint: 'plan testing' },
|
|
11
|
+
];
|
|
9
12
|
options = [
|
|
10
13
|
{ flags: '--data', description: 'Include page data' },
|
|
11
14
|
{ flags: '--deep', description: 'Explore interactive elements by clicking them' },
|
|
@@ -37,7 +40,7 @@ export class ResearchCommand extends BaseCommand {
|
|
|
37
40
|
tag('info').log(`Research file: ${join(outputDir, 'research', `${state.hash}.md`)}`);
|
|
38
41
|
}
|
|
39
42
|
if (!enableDeep) {
|
|
40
|
-
this.suggestions = ['
|
|
43
|
+
this.suggestions = [{ command: 'research <page> --deep', hint: 'analyze page for all expandable elements and interactions' }];
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -3,7 +3,11 @@ import { ExploreCommand } from './explore-command.js';
|
|
|
3
3
|
export class StartCommand extends BaseCommand {
|
|
4
4
|
name = 'start';
|
|
5
5
|
description = 'Start web exploration';
|
|
6
|
-
suggestions = [
|
|
6
|
+
suggestions = [
|
|
7
|
+
{ command: 'navigate <page>', hint: 'go to another page' },
|
|
8
|
+
{ command: 'research', hint: 'analyze current page' },
|
|
9
|
+
{ command: 'plan <feature>', hint: 'plan testing' },
|
|
10
|
+
];
|
|
7
11
|
async execute(args) {
|
|
8
12
|
await new ExploreCommand(this.explorBot).execute(args);
|
|
9
13
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { Stats } from '../stats.js';
|
|
1
2
|
import { Test } from '../test-plan.js';
|
|
2
3
|
import { tag } from '../utils/logger.js';
|
|
3
4
|
import { BaseCommand } from './base-command.js';
|
|
4
5
|
export class TestCommand extends BaseCommand {
|
|
5
6
|
name = 'test';
|
|
6
7
|
description = 'Launch tester agent to execute test scenarios';
|
|
7
|
-
suggestions = [
|
|
8
|
+
suggestions = [
|
|
9
|
+
{ command: 'test', hint: 'run next test' },
|
|
10
|
+
{ command: 'plan', hint: 'create new plan' },
|
|
11
|
+
];
|
|
8
12
|
async execute(args) {
|
|
9
13
|
const plan = this.explorBot.getCurrentPlan();
|
|
14
|
+
Stats.mode = 'test';
|
|
15
|
+
Stats.focus = plan?.title;
|
|
10
16
|
const toExecute = [];
|
|
11
17
|
const requirePlan = () => {
|
|
12
18
|
if (!plan)
|
|
@@ -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();
|
|
@@ -9,6 +9,24 @@ import { mdq } from './utils/markdown-query.js';
|
|
|
9
9
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
10
10
|
const debugLog = createDebug('explorbot:experience');
|
|
11
11
|
const DEFAULT_MAX_EXPERIENCE_LINES = 100;
|
|
12
|
+
export const RECENT_WINDOW_DAYS = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Stores and reads per-page experience files (`./experience/<stateHash>.md`).
|
|
15
|
+
*
|
|
16
|
+
* Two writers, two contracts:
|
|
17
|
+
*
|
|
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().
|
|
25
|
+
*
|
|
26
|
+
* - Always h2. Never h3 for FLOW/ACTION.
|
|
27
|
+
* - On read (getSuccessfulExperience), headings are rendered as
|
|
28
|
+
* `## HOW to <title> (multi-step|single-step)` so prompts get natural phrasing.
|
|
29
|
+
*/
|
|
12
30
|
export class ExperienceTracker {
|
|
13
31
|
experienceDir;
|
|
14
32
|
disabled;
|
|
@@ -122,28 +140,48 @@ export class ExperienceTracker {
|
|
|
122
140
|
isWritingDisabled(state) {
|
|
123
141
|
return this.knowledgeTracker.getRelevantKnowledge(state).some((k) => k.noExperienceWriting === true || k.noExperienceWriting === 'true');
|
|
124
142
|
}
|
|
125
|
-
|
|
143
|
+
writeAction(state, action) {
|
|
126
144
|
if (this.disabled || this.isWritingDisabled(state))
|
|
127
145
|
return;
|
|
146
|
+
if (!action.code?.trim())
|
|
147
|
+
return;
|
|
148
|
+
this.ensureExperienceFile(state);
|
|
149
|
+
const stateHash = state.getStateHash();
|
|
150
|
+
const { content, data } = this.readExperienceFile(stateHash);
|
|
151
|
+
if (content.includes(action.code)) {
|
|
152
|
+
debugLog('Skipping duplicate action', action.code);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const title = normalizeTitle(action.title.split('\n')[0]);
|
|
156
|
+
if (!title)
|
|
157
|
+
return;
|
|
158
|
+
const filteredCode = action.code.replace(/I\.amOnPage\s*\([^)]*\)/gs, '');
|
|
159
|
+
const newEntry = generateActionContent(title, filteredCode, action.explanation);
|
|
160
|
+
const updatedContent = `${newEntry}\n\n${content}`;
|
|
161
|
+
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
162
|
+
tag('substep').log(` Added ACTION to: ${stateHash}.md`);
|
|
163
|
+
}
|
|
164
|
+
writeFlow(state, body, relatedUrls) {
|
|
165
|
+
if (this.disabled || this.isWritingDisabled(state))
|
|
166
|
+
return;
|
|
167
|
+
if (!body?.trim())
|
|
168
|
+
return;
|
|
128
169
|
this.ensureExperienceFile(state);
|
|
129
170
|
const stateHash = state.getStateHash();
|
|
130
171
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
131
|
-
if (content.includes(
|
|
132
|
-
debugLog('Skipping duplicate
|
|
172
|
+
if (content.includes(body)) {
|
|
173
|
+
debugLog('Skipping duplicate flow body');
|
|
133
174
|
return;
|
|
134
175
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
${
|
|
142
|
-
\`\`\`
|
|
143
|
-
`;
|
|
144
|
-
const updatedContent = `${newEntryContent}\n\n${content}`;
|
|
176
|
+
if (relatedUrls?.length) {
|
|
177
|
+
const currentPath = extractStatePath(state.url || '');
|
|
178
|
+
const existingRelated = Array.isArray(data.related) ? data.related : [];
|
|
179
|
+
const allRelated = [...new Set([...existingRelated, ...relatedUrls])];
|
|
180
|
+
data.related = allRelated.filter((url) => url !== currentPath);
|
|
181
|
+
}
|
|
182
|
+
const updatedContent = `${body}\n${content}`;
|
|
145
183
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
146
|
-
tag('substep').log(`
|
|
184
|
+
tag('substep').log(`Added FLOW to: ${stateHash}.md`);
|
|
147
185
|
}
|
|
148
186
|
getAllExperience() {
|
|
149
187
|
const allFiles = [];
|
|
@@ -159,10 +197,12 @@ ${filteredCode}
|
|
|
159
197
|
try {
|
|
160
198
|
const content = readFileSync(file, 'utf8');
|
|
161
199
|
const parsed = matter(content);
|
|
200
|
+
const mtime = statSync(file).mtime;
|
|
162
201
|
allFiles.push({
|
|
163
202
|
filePath: file,
|
|
164
203
|
data: parsed.data,
|
|
165
204
|
content: parsed.content,
|
|
205
|
+
mtime,
|
|
166
206
|
});
|
|
167
207
|
}
|
|
168
208
|
catch (error) {
|
|
@@ -205,71 +245,6 @@ ${filteredCode}
|
|
|
205
245
|
// Clear any in-memory state if needed
|
|
206
246
|
// The actual files will be cleaned up by test cleanup
|
|
207
247
|
}
|
|
208
|
-
saveSessionExperience(state, entry) {
|
|
209
|
-
if (this.disabled || this.isWritingDisabled(state))
|
|
210
|
-
return;
|
|
211
|
-
this.ensureExperienceFile(state);
|
|
212
|
-
const stateHash = state.getStateHash();
|
|
213
|
-
const { content, data } = this.readExperienceFile(stateHash);
|
|
214
|
-
if (entry.relatedUrls?.length) {
|
|
215
|
-
const currentPath = extractStatePath(state.url || '');
|
|
216
|
-
const existingRelated = Array.isArray(data.related) ? data.related : [];
|
|
217
|
-
const allRelated = [...new Set([...existingRelated, ...entry.relatedUrls])];
|
|
218
|
-
data.related = allRelated.filter((url) => url !== currentPath);
|
|
219
|
-
}
|
|
220
|
-
const sessionContent = this.trimSessionContent(this.generateSessionContent(entry));
|
|
221
|
-
if (!sessionContent)
|
|
222
|
-
return;
|
|
223
|
-
const updatedContent = `${sessionContent}\n${content}`;
|
|
224
|
-
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
225
|
-
tag('substep').log(`Added session experience to: ${stateHash}.md`);
|
|
226
|
-
}
|
|
227
|
-
generateSessionContent(entry) {
|
|
228
|
-
let content = `## Successful Flow: ${entry.scenario}\n\n`;
|
|
229
|
-
for (const step of entry.steps) {
|
|
230
|
-
content += `* ${step.message}\n\n`;
|
|
231
|
-
if (step.code) {
|
|
232
|
-
content += '```js\n';
|
|
233
|
-
content += `${step.code}\n`;
|
|
234
|
-
content += '```\n\n';
|
|
235
|
-
}
|
|
236
|
-
if (step.discovery) {
|
|
237
|
-
const discoveries = step.discovery.split('\n').filter((d) => d.trim());
|
|
238
|
-
for (const discovery of discoveries) {
|
|
239
|
-
content += `> ${discovery.trim()}\n\n`;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
content += '---\n';
|
|
244
|
-
return content;
|
|
245
|
-
}
|
|
246
|
-
trimSessionContent(content) {
|
|
247
|
-
const q = mdq(content);
|
|
248
|
-
if (q.query('heading').count() === 0)
|
|
249
|
-
return null;
|
|
250
|
-
if (q.query('code').count() === 0)
|
|
251
|
-
return null;
|
|
252
|
-
let result = content;
|
|
253
|
-
const codeBlocks = q.query('code').each();
|
|
254
|
-
if (codeBlocks.length > 2) {
|
|
255
|
-
for (const block of codeBlocks.slice(2)) {
|
|
256
|
-
result = result.replace(block.text(), '');
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
const blockquotes = mdq(result).query('blockquote').each();
|
|
260
|
-
if (blockquotes.length > 5) {
|
|
261
|
-
for (const bq of blockquotes.slice(5)) {
|
|
262
|
-
result = result.replace(bq.text(), '');
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
const lines = result.split('\n');
|
|
266
|
-
if (lines.length > 40) {
|
|
267
|
-
result = lines.slice(0, 40).join('\n');
|
|
268
|
-
}
|
|
269
|
-
if (!result.trim())
|
|
270
|
-
return null;
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
248
|
getSuccessfulExperience(state, options) {
|
|
274
249
|
const records = this.getRelevantExperience(state, {
|
|
275
250
|
includeDescendantExperience: options?.includeDescendants,
|
|
@@ -278,11 +253,12 @@ ${filteredCode}
|
|
|
278
253
|
for (const record of records) {
|
|
279
254
|
if (!record.content)
|
|
280
255
|
continue;
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
let combined = [
|
|
256
|
+
const flows = mdq(record.content).query('section(~"FLOW:")').text();
|
|
257
|
+
const actions = mdq(record.content).query('section(~"ACTION:")').text();
|
|
258
|
+
let combined = [flows, actions].filter(Boolean).join('\n\n');
|
|
284
259
|
if (!combined.trim())
|
|
285
260
|
continue;
|
|
261
|
+
combined = renderAsHowTo(combined);
|
|
286
262
|
if (options?.stripCode) {
|
|
287
263
|
combined = mdq(combined).query('code').replace('');
|
|
288
264
|
}
|
|
@@ -339,6 +315,69 @@ ${filteredCode}
|
|
|
339
315
|
}
|
|
340
316
|
return null;
|
|
341
317
|
}
|
|
318
|
+
listAllExperienceToc(filter, options) {
|
|
319
|
+
const records = this.getAllExperience();
|
|
320
|
+
if (records.length === 0)
|
|
321
|
+
return [];
|
|
322
|
+
const trimmed = filter?.trim();
|
|
323
|
+
let matching = records;
|
|
324
|
+
if (trimmed) {
|
|
325
|
+
if (trimmed.endsWith('.md')) {
|
|
326
|
+
const bare = trimmed.slice(0, -3);
|
|
327
|
+
const byFilename = records.find((record) => basename(record.filePath, '.md') === bare);
|
|
328
|
+
matching = byFilename ? [byFilename] : [];
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const lower = trimmed.toLowerCase();
|
|
332
|
+
matching = records.filter((record) => {
|
|
333
|
+
const url = (record.data?.url || '').toLowerCase();
|
|
334
|
+
if (!url)
|
|
335
|
+
return false;
|
|
336
|
+
return url.includes(lower);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (options?.recency) {
|
|
341
|
+
const cutoff = Date.now() - RECENT_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
342
|
+
matching = matching.filter((record) => {
|
|
343
|
+
const isRecent = record.mtime.getTime() >= cutoff;
|
|
344
|
+
return options.recency === 'recent' ? isRecent : !isRecent;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const sorted = matching.sort((a, b) => {
|
|
348
|
+
const aUrl = a.data?.url || '';
|
|
349
|
+
const bUrl = b.data?.url || '';
|
|
350
|
+
return aUrl.localeCompare(bUrl);
|
|
351
|
+
});
|
|
352
|
+
const toc = [];
|
|
353
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
354
|
+
const record = sorted[i];
|
|
355
|
+
const sections = listTocHeadings(record.content);
|
|
356
|
+
if (sections.length === 0)
|
|
357
|
+
continue;
|
|
358
|
+
toc.push({
|
|
359
|
+
fileTag: indexToLetters(toc.length),
|
|
360
|
+
fileHash: basename(record.filePath, '.md'),
|
|
361
|
+
url: record.data?.url || '',
|
|
362
|
+
sections,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return toc;
|
|
366
|
+
}
|
|
367
|
+
getExperienceSectionByTag(fileTag, sectionIndex, filter) {
|
|
368
|
+
const toc = this.listAllExperienceToc(filter);
|
|
369
|
+
const entry = toc.find((e) => e.fileTag === fileTag);
|
|
370
|
+
if (!entry)
|
|
371
|
+
return null;
|
|
372
|
+
const filePath = this.findExperienceFileByHash(entry.fileHash);
|
|
373
|
+
if (!filePath)
|
|
374
|
+
return null;
|
|
375
|
+
const { content } = this.readExperienceFile(entry.fileHash);
|
|
376
|
+
const extracted = extractHeadingSection(content, sectionIndex);
|
|
377
|
+
if (!extracted)
|
|
378
|
+
return null;
|
|
379
|
+
return { title: extracted.title, url: entry.url, content: extracted.body, fileHash: entry.fileHash };
|
|
380
|
+
}
|
|
342
381
|
}
|
|
343
382
|
function listTocHeadings(content) {
|
|
344
383
|
const tokens = marked.lexer(content);
|
|
@@ -403,7 +442,11 @@ export function renderExperienceToc(toc) {
|
|
|
403
442
|
return '';
|
|
404
443
|
const lines = [];
|
|
405
444
|
lines.push('<experience>');
|
|
406
|
-
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.');
|
|
448
|
+
lines.push('FLOW: = multi-step recipe (bullets + code + discovery). ACTION: = single-step snippet (one code block).');
|
|
449
|
+
lines.push('Call learn_experience({ fileTag, sectionIndex }) to read a section when it looks relevant to the current step.');
|
|
407
450
|
lines.push('');
|
|
408
451
|
for (const entry of toc) {
|
|
409
452
|
lines.push(`File ${entry.fileTag} ${entry.url}:`);
|
|
@@ -416,3 +459,51 @@ export function renderExperienceToc(toc) {
|
|
|
416
459
|
lines.push('</experience>');
|
|
417
460
|
return lines.join('\n');
|
|
418
461
|
}
|
|
462
|
+
function normalizeTitle(raw) {
|
|
463
|
+
let t = (raw || '').trim();
|
|
464
|
+
for (const p of ['FLOW:', 'ACTION:']) {
|
|
465
|
+
if (t.toLowerCase().startsWith(p.toLowerCase())) {
|
|
466
|
+
t = t.slice(p.length).trim();
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
while (t.length > 0 && '.!?,;:'.includes(t[t.length - 1])) {
|
|
471
|
+
t = t.slice(0, -1);
|
|
472
|
+
}
|
|
473
|
+
if (t.length > 0)
|
|
474
|
+
t = t[0].toLowerCase() + t.slice(1);
|
|
475
|
+
return t;
|
|
476
|
+
}
|
|
477
|
+
function generateActionContent(title, code, explanation) {
|
|
478
|
+
const lines = [];
|
|
479
|
+
lines.push(`## ACTION: ${title}`);
|
|
480
|
+
lines.push('');
|
|
481
|
+
if (explanation) {
|
|
482
|
+
lines.push(`Solution: ${explanation}`);
|
|
483
|
+
lines.push('');
|
|
484
|
+
}
|
|
485
|
+
lines.push('```javascript');
|
|
486
|
+
lines.push(code);
|
|
487
|
+
lines.push('```');
|
|
488
|
+
lines.push('');
|
|
489
|
+
return lines.join('\n');
|
|
490
|
+
}
|
|
491
|
+
function renderAsHowTo(content) {
|
|
492
|
+
const tokens = marked.lexer(content);
|
|
493
|
+
let result = '';
|
|
494
|
+
for (const token of tokens) {
|
|
495
|
+
if (token.type === 'heading' && token.depth === 2) {
|
|
496
|
+
const text = token.text.trim();
|
|
497
|
+
if (text.startsWith('FLOW:')) {
|
|
498
|
+
result += `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (text.startsWith('ACTION:')) {
|
|
502
|
+
result += `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
result += token.raw || '';
|
|
507
|
+
}
|
|
508
|
+
return result;
|
|
509
|
+
}
|