explorbot 0.4.9 → 0.5.0
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 +4 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +3 -2
- package/dist/src/action.js +4 -2
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -1
- package/dist/src/ai/navigator.js +24 -28
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +67 -9
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +52 -7
- package/dist/src/ai/rerunner.js +7 -0
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/api/xhr-capture.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/explorer.js +2 -3
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/reporter.js +8 -4
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +9 -3
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/step-analyzer.d.ts +3 -0
- package/dist/src/utils/step-analyzer.js +7 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +7 -0
- package/docs/api-testing/planning.md +1 -1
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/package.json +3 -2
- package/src/action.ts +4 -2
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +24 -27
- package/src/ai/pilot.ts +68 -9
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +51 -7
- package/src/ai/rerunner.ts +4 -0
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/api/xhr-capture.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/explorer.ts +2 -2
- package/src/knowledge-tracker.ts +1 -1
- package/src/reporter.ts +8 -4
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +7 -2
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/step-analyzer.ts +8 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
- package/src/utils/url-matcher.ts +7 -0
|
@@ -10,7 +10,7 @@ import { ErrorPageError, getStateErrorPageError } from '../utils/error-page.ts';
|
|
|
10
10
|
import { tag } from '../utils/logger.js';
|
|
11
11
|
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
12
12
|
import { safeFilename } from '../utils/strings.ts';
|
|
13
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
13
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
14
14
|
|
|
15
15
|
const MAX_SUB_PAGE_ATTEMPTS = 30;
|
|
16
16
|
const PRIORITY_ORDER: Record<string, number> = { critical: 0, important: 1, high: 2, normal: 3, low: 4 };
|
|
@@ -19,7 +19,7 @@ export const DEADLINE_TEST_ALLOWANCE_MS = 5 * 60_000;
|
|
|
19
19
|
|
|
20
20
|
export class ExploreCommand extends BaseCommand {
|
|
21
21
|
name = 'explore';
|
|
22
|
-
description = 'Start web exploration';
|
|
22
|
+
description = 'Start web exploration on the current page, or on the URL given as the first argument';
|
|
23
23
|
options = [
|
|
24
24
|
{ flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
|
|
25
25
|
{ flags: '--max-duration <minutes>', description: 'Wall-clock budget in minutes; wraps up the session before the limit is hit' },
|
|
@@ -61,6 +61,12 @@ export class ExploreCommand extends BaseCommand {
|
|
|
61
61
|
this.hardDeadlineAt = Date.now() + this.maxDurationMinutes * 60_000 - DEADLINE_RESERVE_MS;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const [target] = remaining;
|
|
65
|
+
if (target?.startsWith('/') || target?.startsWith('http://') || target?.startsWith('https://')) {
|
|
66
|
+
remaining.shift();
|
|
67
|
+
await this.explorBot.visit(target);
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
const feature = (opts.focus as string) || remaining.join(' ') || undefined;
|
|
65
71
|
const cfg = this.parseConfigure(opts.configure as string | undefined);
|
|
66
72
|
if (cfg.priorities) this.priorityFilter = new Set(cfg.priorities);
|
|
@@ -97,6 +103,13 @@ export class ExploreCommand extends BaseCommand {
|
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
completeArguments(): ArgumentCompletion[] {
|
|
107
|
+
return this.explorBot
|
|
108
|
+
.stateManager()
|
|
109
|
+
.getKnownUrls()
|
|
110
|
+
.map((url) => ({ value: url }));
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
private originLabel(test: Test): string {
|
|
101
114
|
return this.oldTestRefs.has(test) ? 'OLD' : 'NEW';
|
|
102
115
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Planner } from '../ai/planner.js';
|
|
2
2
|
import { Researcher } from '../ai/researcher.js';
|
|
3
|
+
import { normalizeUrl } from '../state-manager.js';
|
|
3
4
|
import { Stats } from '../stats.js';
|
|
4
5
|
import { tag } from '../utils/logger.js';
|
|
5
6
|
import { loop } from '../utils/loop.js';
|
|
@@ -67,12 +68,18 @@ export class FreesailCommand extends BaseCommand {
|
|
|
67
68
|
const suggestion = await navigator.freeSail({ strategy, scope, visitedUrls });
|
|
68
69
|
if (!suggestion) {
|
|
69
70
|
tag('info').log('No navigation suggestion available');
|
|
70
|
-
|
|
71
|
+
ctx.stop();
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
if (scope && !suggestion.target.startsWith(scope)) {
|
|
74
75
|
tag('warning').log(`Suggestion ${suggestion.target} is outside scope ${scope}, skipping`);
|
|
75
|
-
|
|
76
|
+
ctx.stop();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const currentUrl = stateManager.getCurrentState()?.url;
|
|
80
|
+
if (currentUrl && normalizeUrl(suggestion.target) === normalizeUrl(currentUrl)) {
|
|
81
|
+
tag('info').log(`No new navigation target available after ${currentUrl}; stopping exploration`);
|
|
82
|
+
ctx.stop();
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
tag('info').log(`Navigating to: ${suggestion.target} - ${suggestion.reason}`);
|
|
@@ -163,7 +163,7 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
163
163
|
log('2. Set AI models config file');
|
|
164
164
|
log('3. Set web application URL in the config file');
|
|
165
165
|
log('4. Add initial knowledge (how to authorize to the application, etc.)');
|
|
166
|
-
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
166
|
+
tag('substep').log(chalk.yellow(`${getCliName()} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
167
167
|
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
168
168
|
|
|
169
169
|
log('5. Launch application on a relative URL');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tag } from '../utils/logger.js';
|
|
2
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
2
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
3
3
|
|
|
4
4
|
export class NavigateCommand extends BaseCommand {
|
|
5
5
|
name = 'navigate';
|
|
@@ -18,4 +18,11 @@ export class NavigateCommand extends BaseCommand {
|
|
|
18
18
|
await this.explorBot.visit(destination);
|
|
19
19
|
tag('success').log(`Navigation requested: ${destination}`);
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
completeArguments(): ArgumentCompletion[] {
|
|
23
|
+
return this.explorBot
|
|
24
|
+
.stateManager()
|
|
25
|
+
.getKnownUrls()
|
|
26
|
+
.map((url) => ({ value: url }));
|
|
27
|
+
}
|
|
21
28
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Plan } from '../test-plan.js';
|
|
1
2
|
import { tag } from '../utils/logger.js';
|
|
2
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
3
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
3
4
|
|
|
4
5
|
export class PlanLoadCommand extends BaseCommand {
|
|
5
6
|
name = 'plan:load';
|
|
@@ -18,4 +19,8 @@ export class PlanLoadCommand extends BaseCommand {
|
|
|
18
19
|
const plan = this.explorBot.loadPlan(filename);
|
|
19
20
|
tag('success').log(`Plan loaded: ${plan.title} with ${plan.tests.length} tests`);
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
completeArguments(): ArgumentCompletion[] {
|
|
24
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
25
|
+
}
|
|
21
26
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { Plan } from '../test-plan.js';
|
|
3
|
+
import { Plan, type PlanFile } from '../test-plan.js';
|
|
4
4
|
import { getCliName } from '../utils/cli-name.js';
|
|
5
5
|
import { tag } from '../utils/logger.js';
|
|
6
6
|
import { relativeToCwd } from '../utils/next-steps.js';
|
|
7
|
-
import { BaseCommand } from './base-command.js';
|
|
7
|
+
import { type ArgumentCompletion, BaseCommand } from './base-command.js';
|
|
8
8
|
|
|
9
9
|
export class PlansCommand extends BaseCommand {
|
|
10
10
|
name = 'plans';
|
|
@@ -13,7 +13,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
13
13
|
|
|
14
14
|
async execute(args: string): Promise<void> {
|
|
15
15
|
const { opts, args: remaining } = this.parseArgs(args);
|
|
16
|
-
const files = this.
|
|
16
|
+
const files = Plan.listFiles(this.explorBot.getPlansDir());
|
|
17
17
|
const target = String(opts.fromPlan || remaining[0] || '').trim();
|
|
18
18
|
|
|
19
19
|
if (!target) {
|
|
@@ -25,22 +25,8 @@ export class PlansCommand extends BaseCommand {
|
|
|
25
25
|
this.printPlanDetails(plan, file);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!existsSync(plansDir)) return [];
|
|
31
|
-
|
|
32
|
-
return readdirSync(plansDir)
|
|
33
|
-
.filter((file) => file.endsWith('.md'))
|
|
34
|
-
.map((file) => {
|
|
35
|
-
const filePath = path.join(plansDir, file);
|
|
36
|
-
const stat = statSync(filePath);
|
|
37
|
-
return {
|
|
38
|
-
name: file,
|
|
39
|
-
path: filePath,
|
|
40
|
-
modifiedAt: stat.mtimeMs,
|
|
41
|
-
};
|
|
42
|
-
})
|
|
43
|
-
.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
28
|
+
completeArguments(): ArgumentCompletion[] {
|
|
29
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
44
30
|
}
|
|
45
31
|
|
|
46
32
|
private printPlans(files: PlanFile[]): void {
|
|
@@ -52,8 +38,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
52
38
|
tag('info').log('Saved plans:');
|
|
53
39
|
for (let i = 0; i < files.length; i++) {
|
|
54
40
|
const file = files[i];
|
|
55
|
-
|
|
56
|
-
tag('info').log(`${i + 1}. ${plan.title} (${plan.tests.length} tests) - ${file.name}`);
|
|
41
|
+
tag('info').log(`${i + 1}. ${file.title} (${file.testCount} tests) - ${file.name}`);
|
|
57
42
|
}
|
|
58
43
|
tag('info').log('');
|
|
59
44
|
tag('info').log(`View plan tests: ${getCliName()} plans <number>`);
|
|
@@ -83,17 +68,16 @@ export class PlansCommand extends BaseCommand {
|
|
|
83
68
|
throw new Error(`Plan file not found: ${target}`);
|
|
84
69
|
}
|
|
85
70
|
|
|
71
|
+
const name = path.basename(plan.filePath);
|
|
86
72
|
const file = {
|
|
87
|
-
name
|
|
73
|
+
name,
|
|
88
74
|
path: plan.filePath,
|
|
89
75
|
modifiedAt: statSync(plan.filePath).mtimeMs,
|
|
76
|
+
title: plan.title,
|
|
77
|
+
url: plan.startUrl || '',
|
|
78
|
+
testCount: plan.tests.length,
|
|
79
|
+
label: name,
|
|
90
80
|
};
|
|
91
81
|
return { plan, file };
|
|
92
82
|
}
|
|
93
83
|
}
|
|
94
|
-
|
|
95
|
-
interface PlanFile {
|
|
96
|
-
name: string;
|
|
97
|
-
path: string;
|
|
98
|
-
modifiedAt: number;
|
|
99
|
-
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
3
|
import { ConfigParser } from '../config.ts';
|
|
4
4
|
import { tag } from '../utils/logger.ts';
|
|
5
|
-
import {
|
|
5
|
+
import { listTestFiles } from '../utils/test-files.ts';
|
|
6
|
+
import { type ArgumentCompletion, BaseCommand } from './base-command.js';
|
|
6
7
|
|
|
7
8
|
export class RerunCommand extends BaseCommand {
|
|
8
9
|
name = 'rerun';
|
|
@@ -32,6 +33,10 @@ export class RerunCommand extends BaseCommand {
|
|
|
32
33
|
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
33
34
|
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
completeArguments(): ArgumentCompletion[] {
|
|
38
|
+
return listTestFiles(ConfigParser.getInstance().getTestsDir()).map((file) => ({ value: basename(file) }));
|
|
39
|
+
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function parseTestIndices(input: string): number[] {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { ConfigParser } from '../config.ts';
|
|
3
3
|
import { tag } from '../utils/logger.ts';
|
|
4
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
4
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
5
5
|
|
|
6
6
|
export class ResearchCommand extends BaseCommand {
|
|
7
7
|
name = 'research';
|
|
@@ -51,4 +51,11 @@ export class ResearchCommand extends BaseCommand {
|
|
|
51
51
|
this.suggestions = [{ command: 'research <page> --deep', hint: 'analyze page for all expandable elements and interactions' }];
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
completeArguments(): ArgumentCompletion[] {
|
|
56
|
+
return this.explorBot
|
|
57
|
+
.stateManager()
|
|
58
|
+
.getKnownUrls()
|
|
59
|
+
.map((url) => ({ value: url }));
|
|
60
|
+
}
|
|
54
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Stats } from '../stats.js';
|
|
2
|
-
import { Test } from '../test-plan.js';
|
|
2
|
+
import { Plan, Test } from '../test-plan.js';
|
|
3
3
|
import { tag } from '../utils/logger.js';
|
|
4
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
4
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
5
5
|
|
|
6
6
|
export class TestCommand extends BaseCommand {
|
|
7
7
|
name = 'test';
|
|
@@ -75,6 +75,10 @@ export class TestCommand extends BaseCommand {
|
|
|
75
75
|
}
|
|
76
76
|
tag('success').log('Test execution finished');
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
completeArguments(): ArgumentCompletion[] {
|
|
80
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: `--from-plan ${file.name}`, display: file.label }));
|
|
81
|
+
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function parseTestIndices(input: string, total: number): number[] {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
1
|
+
import { Box, Text, useStdout } from 'ink';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { truncate } from '../utils/strings.js';
|
|
3
4
|
import { useAutocompleteState } from './autocomplete-store.js';
|
|
4
5
|
|
|
6
|
+
const MAX_HEIGHT = 7;
|
|
7
|
+
const COLUMN_WIDTH = 24;
|
|
8
|
+
|
|
5
9
|
const Autocomplete: React.FC = () => {
|
|
6
10
|
const { suggestions, selectedIndex, visible, argumentHint } = useAutocompleteState();
|
|
11
|
+
const { stdout } = useStdout();
|
|
7
12
|
|
|
8
13
|
if (!visible) {
|
|
9
14
|
if (!argumentHint) {
|
|
@@ -20,24 +25,49 @@ const Autocomplete: React.FC = () => {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
const effectiveSelectedIndex = Math.min(selectedIndex, suggestions.length - 1);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
28
|
+
const rowsPerColumn = Math.min(Math.max(1, MAX_HEIGHT - 2), suggestions.length);
|
|
29
|
+
const widest = Math.max(...suggestions.map((suggestion) => suggestion.display.length));
|
|
30
|
+
|
|
31
|
+
if (widest > COLUMN_WIDTH) {
|
|
32
|
+
const width = Math.max(COLUMN_WIDTH, (stdout?.columns || 80) - 4);
|
|
33
|
+
let firstRow = 0;
|
|
34
|
+
if (effectiveSelectedIndex >= rowsPerColumn) {
|
|
35
|
+
firstRow = effectiveSelectedIndex - rowsPerColumn + 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Box position="absolute" top={0} left={0} width="100%" maxHeight={MAX_HEIGHT} overflow="hidden" paddingX={1} paddingY={1} backgroundColor="#2a2a2a" flexDirection="column">
|
|
40
|
+
{suggestions.slice(firstRow, firstRow + rowsPerColumn).map((suggestion, rowIndex) => {
|
|
41
|
+
const isSelected = firstRow + rowIndex === effectiveSelectedIndex;
|
|
42
|
+
let color = 'white';
|
|
43
|
+
let backgroundColor = '#2a2a2a';
|
|
44
|
+
if (isSelected) {
|
|
45
|
+
color = 'black';
|
|
46
|
+
backgroundColor = '#e6e6e6';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Text key={suggestion.value} color={color} backgroundColor={backgroundColor}>
|
|
51
|
+
{` ${truncate(suggestion.display, width).padEnd(width)} `}
|
|
52
|
+
</Text>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
const columns: (typeof suggestions)[] = [];
|
|
26
60
|
for (let index = 0; index < suggestions.length; index += rowsPerColumn) {
|
|
27
61
|
columns.push(suggestions.slice(index, index + rowsPerColumn));
|
|
28
62
|
}
|
|
29
63
|
|
|
30
64
|
return (
|
|
31
|
-
<Box position="absolute" top={0} left={0} width="100%" maxHeight={
|
|
65
|
+
<Box position="absolute" top={0} left={0} width="100%" maxHeight={MAX_HEIGHT} overflow="hidden" paddingX={1} paddingY={1} backgroundColor="#2a2a2a" flexDirection="row" columnGap={2}>
|
|
32
66
|
{columns.map((column, columnIndex) => (
|
|
33
67
|
<Box key={columnIndex} flexDirection="column">
|
|
34
68
|
{column.map((suggestion, rowIndex) => {
|
|
35
69
|
const index = columnIndex * rowsPerColumn + rowIndex;
|
|
36
70
|
const isSelected = index === effectiveSelectedIndex;
|
|
37
|
-
let display = suggestion.display;
|
|
38
|
-
if (display.length > 24) {
|
|
39
|
-
display = `${display.slice(0, 21)}...`;
|
|
40
|
-
}
|
|
41
71
|
let color = 'white';
|
|
42
72
|
let backgroundColor = '#2a2a2a';
|
|
43
73
|
if (isSelected) {
|
|
@@ -48,8 +78,7 @@ const Autocomplete: React.FC = () => {
|
|
|
48
78
|
return (
|
|
49
79
|
<Box key={suggestion.display} marginBottom={1}>
|
|
50
80
|
<Text color={color} backgroundColor={backgroundColor}>
|
|
51
|
-
{
|
|
52
|
-
{display}{' '}
|
|
81
|
+
{` ${suggestion.display} `}
|
|
53
82
|
</Text>
|
|
54
83
|
</Box>
|
|
55
84
|
);
|
|
@@ -43,6 +43,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
43
43
|
const cursorRef = useRef(cursorPosition);
|
|
44
44
|
const showAutocompleteRef = useRef(showAutocomplete);
|
|
45
45
|
const selectedIndexRef = useRef(selectedIndex);
|
|
46
|
+
const navigatedRef = useRef(false);
|
|
46
47
|
const historyRef = useRef<string[]>([]);
|
|
47
48
|
const historyIndexRef = useRef(-1);
|
|
48
49
|
const historyDraftRef = useRef('');
|
|
@@ -85,6 +86,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
85
86
|
cursorRef.current = 0;
|
|
86
87
|
showAutocompleteRef.current = false;
|
|
87
88
|
selectedIndexRef.current = 0;
|
|
89
|
+
navigatedRef.current = false;
|
|
88
90
|
historyIndexRef.current = -1;
|
|
89
91
|
historyDraftRef.current = '';
|
|
90
92
|
setInputState({ value: '', cursor: 0, showAutocomplete: false, selectedIndex: 0 });
|
|
@@ -141,6 +143,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
141
143
|
cursorRef.current = safeCursor;
|
|
142
144
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
143
145
|
selectedIndexRef.current = 0;
|
|
146
|
+
navigatedRef.current = false;
|
|
144
147
|
historyIndexRef.current = -1;
|
|
145
148
|
historyDraftRef.current = '';
|
|
146
149
|
|
|
@@ -235,6 +238,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
235
238
|
if (showAutocompleteRef.current) {
|
|
236
239
|
showAutocompleteRef.current = false;
|
|
237
240
|
selectedIndexRef.current = 0;
|
|
241
|
+
navigatedRef.current = false;
|
|
238
242
|
setInputState((prev) => ({ ...prev, showAutocomplete: false, selectedIndex: 0 }));
|
|
239
243
|
}
|
|
240
244
|
return;
|
|
@@ -247,7 +251,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
247
251
|
}
|
|
248
252
|
if (commandHandler && showAutocompleteRef.current) {
|
|
249
253
|
const autocomplete = getAutocomplete(inputRef.current, cursorRef.current);
|
|
250
|
-
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
254
|
+
if (autocomplete.visible && autocomplete.suggestions.length > 0 && (!autocomplete.completesArgument || navigatedRef.current)) {
|
|
251
255
|
const chosen = applyAutocomplete(selectedIndexRef.current);
|
|
252
256
|
if (chosen) {
|
|
253
257
|
handleSubmit(chosen);
|
|
@@ -381,6 +385,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
381
385
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
382
386
|
const nextIndex = selectedIndexRef.current > 0 ? selectedIndexRef.current - 1 : autocomplete.suggestions.length - 1;
|
|
383
387
|
selectedIndexRef.current = nextIndex;
|
|
388
|
+
navigatedRef.current = true;
|
|
384
389
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
385
390
|
return;
|
|
386
391
|
}
|
|
@@ -391,6 +396,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
391
396
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
392
397
|
const nextIndex = selectedIndexRef.current < autocomplete.suggestions.length - 1 ? selectedIndexRef.current + 1 : 0;
|
|
393
398
|
selectedIndexRef.current = nextIndex;
|
|
399
|
+
navigatedRef.current = true;
|
|
394
400
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
395
401
|
return;
|
|
396
402
|
}
|
|
@@ -449,6 +455,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
449
455
|
inputRef.current = nextValue;
|
|
450
456
|
cursorRef.current = nextValue.length;
|
|
451
457
|
selectedIndexRef.current = 0;
|
|
458
|
+
navigatedRef.current = false;
|
|
452
459
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
453
460
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
454
461
|
return;
|
|
@@ -470,6 +477,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
470
477
|
inputRef.current = nextValue;
|
|
471
478
|
cursorRef.current = nextValue.length;
|
|
472
479
|
selectedIndexRef.current = 0;
|
|
480
|
+
navigatedRef.current = false;
|
|
473
481
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
474
482
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
475
483
|
return;
|
|
@@ -481,6 +489,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
481
489
|
inputRef.current = nextValue;
|
|
482
490
|
cursorRef.current = nextValue.length;
|
|
483
491
|
selectedIndexRef.current = 0;
|
|
492
|
+
navigatedRef.current = false;
|
|
484
493
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
485
494
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
486
495
|
return;
|
package/src/config.ts
CHANGED
|
@@ -185,6 +185,7 @@ interface AIConfig {
|
|
|
185
185
|
vision?: boolean;
|
|
186
186
|
visionModel?: any;
|
|
187
187
|
agenticModel?: any;
|
|
188
|
+
decisionModel?: { provider: string; model: string; tool?: boolean; direct?: boolean };
|
|
188
189
|
retryAttempts?: number;
|
|
189
190
|
retryDelay?: number;
|
|
190
191
|
maxParallelRequests?: number;
|
|
@@ -262,7 +262,7 @@ export class ExperienceTracker {
|
|
|
262
262
|
while (content.split('\n').length > maxLines) {
|
|
263
263
|
const sections = mdq(content).query('section2').each();
|
|
264
264
|
if (sections.length <= 1) break;
|
|
265
|
-
content = sections[sections.length - 1].replace('');
|
|
265
|
+
content = sections[sections.length - 1].replace('').toString();
|
|
266
266
|
}
|
|
267
267
|
return { ...experience, content };
|
|
268
268
|
});
|
|
@@ -286,7 +286,7 @@ export class ExperienceTracker {
|
|
|
286
286
|
combined = renderAsHowTo(combined);
|
|
287
287
|
|
|
288
288
|
if (options?.stripCode) {
|
|
289
|
-
combined = mdq(combined).query('code').replace('');
|
|
289
|
+
combined = mdq(combined).query('code').replace('').toString();
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
if (combined.trim()) results.push(combined.trim());
|
|
@@ -515,7 +515,8 @@ function renderAsHowTo(content: string): string {
|
|
|
515
515
|
if (text.startsWith('FLOW:')) return `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
|
|
516
516
|
if (text.startsWith('ACTION:')) return `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
517
517
|
return heading.text();
|
|
518
|
-
})
|
|
518
|
+
})
|
|
519
|
+
.toString();
|
|
519
520
|
}
|
|
520
521
|
|
|
521
522
|
export interface ExperienceFile {
|
package/src/explorbot.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Driller } from './ai/driller.ts';
|
|
|
7
7
|
import { ExperienceCompactor } from './ai/experience-compactor.ts';
|
|
8
8
|
import { Fisherman } from './ai/fisherman.ts';
|
|
9
9
|
import { Historian } from './ai/historian.ts';
|
|
10
|
+
import { Judge } from './ai/judge.ts';
|
|
10
11
|
import { Navigator } from './ai/navigator.ts';
|
|
11
12
|
import { Pilot } from './ai/pilot.ts';
|
|
12
13
|
import { Planner } from './ai/planner.ts';
|
|
@@ -80,6 +81,7 @@ export class ExplorBot {
|
|
|
80
81
|
private _reporter?: Reporter;
|
|
81
82
|
private _requestStore?: RequestStore;
|
|
82
83
|
private _playwrightRecorder?: PlaywrightRecorder;
|
|
84
|
+
private judgeInstance: Judge | null | undefined;
|
|
83
85
|
|
|
84
86
|
constructor(options: ExplorBotOptions = {}) {
|
|
85
87
|
this.options = options;
|
|
@@ -192,6 +194,11 @@ export class ExplorBot {
|
|
|
192
194
|
return (this._playwrightRecorder ||= new PlaywrightRecorder());
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
judge(): Judge | null {
|
|
198
|
+
if (this.judgeInstance === undefined) this.judgeInstance = Judge.fromConfig(this.config.ai?.decisionModel);
|
|
199
|
+
return this.judgeInstance;
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
getConfig(): ExplorbotConfig {
|
|
196
203
|
return this.config;
|
|
197
204
|
}
|
|
@@ -213,6 +220,7 @@ export class ExplorBot {
|
|
|
213
220
|
knowledgeTracker: this.knowledgeTracker(),
|
|
214
221
|
requestStore: this.requestStore(),
|
|
215
222
|
playwrightRecorder: this.playwrightRecorder(),
|
|
223
|
+
judge: this.judge() || undefined,
|
|
216
224
|
});
|
|
217
225
|
|
|
218
226
|
const agentName = (agent as any).constructor.name.toLowerCase();
|
package/src/explorer.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { Test, TestResult } from './test-plan.ts';
|
|
|
21
21
|
import { BrowserRecoveryError, browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
|
|
22
22
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
23
23
|
import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
|
|
24
|
+
import { isInternalStep } from './utils/step-analyzer.ts';
|
|
24
25
|
|
|
25
26
|
declare global {
|
|
26
27
|
namespace NodeJS {
|
|
@@ -206,8 +207,7 @@ class Explorer {
|
|
|
206
207
|
|
|
207
208
|
const stepHandler = (step: any, status?: string, error?: string, log?: string) => {
|
|
208
209
|
if (!step.toCode) return;
|
|
209
|
-
if (step
|
|
210
|
-
if (step?.name?.startsWith('save')) return;
|
|
210
|
+
if (isInternalStep(step)) return;
|
|
211
211
|
|
|
212
212
|
test.addStep(step.toCode(), step.duration, status, error, log);
|
|
213
213
|
|
package/src/knowledge-tracker.ts
CHANGED
|
@@ -214,7 +214,7 @@ export class KnowledgeTracker {
|
|
|
214
214
|
|
|
215
215
|
return this.knowledgeFiles.map((knowledge) => {
|
|
216
216
|
const content = knowledge.content.trim();
|
|
217
|
-
const firstLine = mdq(content).
|
|
217
|
+
const firstLine = mdq(content).nodes()[0]?.text.split('\n')[0]?.trim() || '';
|
|
218
218
|
return {
|
|
219
219
|
url: knowledge.url || knowledge.endpoint || '',
|
|
220
220
|
firstLine,
|
package/src/reporter.ts
CHANGED
|
@@ -138,6 +138,7 @@ export class Reporter {
|
|
|
138
138
|
|
|
139
139
|
protected combineStepsAndNotes(test: Test, lastScreenshotFile?: string): Step[] {
|
|
140
140
|
const noteEntries = Object.entries(test.notes)
|
|
141
|
+
.filter(([, note]) => !note.observation)
|
|
141
142
|
.map(([timestampKey, note]) => ({
|
|
142
143
|
startTime: note.startTime,
|
|
143
144
|
endTime: note.endTime,
|
|
@@ -272,9 +273,12 @@ export class Reporter {
|
|
|
272
273
|
description: test.description,
|
|
273
274
|
code: test.generatedCode || '',
|
|
274
275
|
steps,
|
|
275
|
-
logs:
|
|
276
|
-
.map((stepData) => stepData.text)
|
|
277
|
-
.
|
|
276
|
+
logs: [
|
|
277
|
+
...Object.values(test.steps).map((stepData) => stepData.text),
|
|
278
|
+
...Object.values(test.notes)
|
|
279
|
+
.filter((note) => note.observation)
|
|
280
|
+
.map((note) => note.message),
|
|
281
|
+
].join('\n'),
|
|
278
282
|
files: Object.values(test.artifacts) || [],
|
|
279
283
|
message: test.summary || this.extractLastNoteMessage(test) || '',
|
|
280
284
|
meta,
|
|
@@ -341,7 +345,7 @@ export class Reporter {
|
|
|
341
345
|
}
|
|
342
346
|
|
|
343
347
|
private extractLastNoteMessage(test: Test): string {
|
|
344
|
-
const notes = Object.values(test.notes);
|
|
348
|
+
const notes = Object.values(test.notes).filter((note) => !note.observation);
|
|
345
349
|
if (notes.length === 0) return '';
|
|
346
350
|
return notes[notes.length - 1].message;
|
|
347
351
|
}
|
package/src/state-manager.ts
CHANGED
|
@@ -86,6 +86,7 @@ export class StateManager {
|
|
|
86
86
|
private currentState: WebPageState | null = null;
|
|
87
87
|
private stateHistory: StateTransition[] = [];
|
|
88
88
|
private allVisitedUrls: Set<string> = new Set();
|
|
89
|
+
private experienceUrls?: string[];
|
|
89
90
|
private stateChangeListeners: StateChangeListener[] = [];
|
|
90
91
|
private experienceTracker!: ExperienceTracker;
|
|
91
92
|
private knowledgeTracker: KnowledgeTracker;
|
|
@@ -359,6 +360,21 @@ export class StateManager {
|
|
|
359
360
|
return this.allVisitedUrls;
|
|
360
361
|
}
|
|
361
362
|
|
|
363
|
+
getKnownUrls(): string[] {
|
|
364
|
+
this.experienceUrls ||= this.experienceTracker
|
|
365
|
+
.getAllExperience()
|
|
366
|
+
.sort((left, right) => right.mtime.getTime() - left.mtime.getTime())
|
|
367
|
+
.flatMap((experience) => experience.data.url || []);
|
|
368
|
+
const sessionUrls = this.stateHistory.map((transition) => transition.toState.url).reverse();
|
|
369
|
+
const urls = new Map<string, string>();
|
|
370
|
+
for (const url of [...sessionUrls, ...this.experienceUrls]) {
|
|
371
|
+
const key = normalizeUrl(url);
|
|
372
|
+
if (urls.has(key)) continue;
|
|
373
|
+
urls.set(key, url);
|
|
374
|
+
}
|
|
375
|
+
return [...urls.values()];
|
|
376
|
+
}
|
|
377
|
+
|
|
362
378
|
/**
|
|
363
379
|
* Get how many times we've visited a specific path
|
|
364
380
|
*/
|