explorbot 0.4.10 → 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/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 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- 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/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/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/knowledge-tracker.js +1 -1
- 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 +8 -2
- 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/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/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/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 +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +51 -7
- 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/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/knowledge-tracker.ts +1 -1
- 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 +6 -1
- 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/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
|
@@ -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/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/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
|
*/
|
package/src/test-plan.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import figures from 'figures';
|
|
5
5
|
import type { ActionResult } from './action-result.ts';
|
|
6
6
|
import { listSitePlanDirs } from './global-config.ts';
|
|
7
7
|
import { WebPageState } from './state-manager.ts';
|
|
8
|
-
import { tag } from './utils/logger.ts';
|
|
8
|
+
import { createDebug, pluralize, tag } from './utils/logger.ts';
|
|
9
|
+
import { truncateMiddle } from './utils/strings.ts';
|
|
9
10
|
import { parsePlanFromMarkdown, planToAiContext, savePlanToMarkdown, savePlansToMarkdown } from './utils/test-plan-markdown.ts';
|
|
10
11
|
import { uniqSessionName } from './utils/unique-names.ts';
|
|
12
|
+
import { extractStatePath } from './utils/url-matcher.ts';
|
|
11
13
|
|
|
12
14
|
export const TestResult = {
|
|
13
15
|
PASSED: 'passed',
|
|
@@ -241,6 +243,7 @@ export class Test extends Task {
|
|
|
241
243
|
endTime?: number;
|
|
242
244
|
resetCount = 0;
|
|
243
245
|
appliedExperience: AppliedExperience[] = [];
|
|
246
|
+
preparedData: string[] = [];
|
|
244
247
|
|
|
245
248
|
constructor(scenario: string, priority: 'critical' | 'important' | 'high' | 'normal' | 'low', expectedOutcome: string | string[], startUrl: string, plannedSteps: string[] = []) {
|
|
246
249
|
super(scenario, startUrl);
|
|
@@ -477,6 +480,39 @@ export class Plan {
|
|
|
477
480
|
|
|
478
481
|
updateStatus(): void {}
|
|
479
482
|
|
|
483
|
+
static listFiles(plansDir: string): PlanFile[] {
|
|
484
|
+
if (!existsSync(plansDir)) return [];
|
|
485
|
+
|
|
486
|
+
const files = readdirSync(plansDir)
|
|
487
|
+
.filter((file) => file.endsWith('.md'))
|
|
488
|
+
.map((file) => {
|
|
489
|
+
const filePath = path.join(plansDir, file);
|
|
490
|
+
const modifiedAt = statSync(filePath).mtimeMs;
|
|
491
|
+
const plan = readPlanFile(filePath, modifiedAt);
|
|
492
|
+
return {
|
|
493
|
+
name: file,
|
|
494
|
+
path: filePath,
|
|
495
|
+
modifiedAt,
|
|
496
|
+
title: plan.title,
|
|
497
|
+
url: extractStatePath(plan.startUrl || ''),
|
|
498
|
+
testCount: plan.tests.length,
|
|
499
|
+
label: '',
|
|
500
|
+
};
|
|
501
|
+
})
|
|
502
|
+
.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
503
|
+
|
|
504
|
+
if (files.length === 0) return files;
|
|
505
|
+
|
|
506
|
+
const nameWidth = Math.min(MAX_PLAN_NAME_WIDTH, Math.max(...files.map((file) => file.name.length)));
|
|
507
|
+
const urlWidth = Math.min(MAX_PLAN_URL_WIDTH, Math.max(...files.map((file) => file.url.length)));
|
|
508
|
+
for (const file of files) {
|
|
509
|
+
const name = truncateMiddle(file.name, nameWidth).padEnd(nameWidth);
|
|
510
|
+
const url = truncateMiddle(file.url, urlWidth).padEnd(urlWidth);
|
|
511
|
+
file.label = `${name} ${url} ${file.testCount} ${pluralize(file.testCount, 'test')}`;
|
|
512
|
+
}
|
|
513
|
+
return files;
|
|
514
|
+
}
|
|
515
|
+
|
|
480
516
|
static resolveFile(file: string, plansDir?: string): string | null {
|
|
481
517
|
const names = [file];
|
|
482
518
|
if (!file.endsWith('.md')) names.push(`${file}.md`);
|
|
@@ -579,3 +615,32 @@ interface AppliedExperience {
|
|
|
579
615
|
url: string;
|
|
580
616
|
content: string;
|
|
581
617
|
}
|
|
618
|
+
|
|
619
|
+
export interface PlanFile {
|
|
620
|
+
name: string;
|
|
621
|
+
path: string;
|
|
622
|
+
modifiedAt: number;
|
|
623
|
+
title: string;
|
|
624
|
+
url: string;
|
|
625
|
+
testCount: number;
|
|
626
|
+
label: string;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const debugLog = createDebug('explorbot:test-plan');
|
|
630
|
+
const MAX_PLAN_NAME_WIDTH = 38;
|
|
631
|
+
const MAX_PLAN_URL_WIDTH = 26;
|
|
632
|
+
const planFileCache = new Map<string, { modifiedAt: number; plan: Plan }>();
|
|
633
|
+
|
|
634
|
+
function readPlanFile(filePath: string, modifiedAt: number): Plan {
|
|
635
|
+
const cached = planFileCache.get(filePath);
|
|
636
|
+
if (cached?.modifiedAt === modifiedAt) return cached.plan;
|
|
637
|
+
|
|
638
|
+
let plan = new Plan(path.basename(filePath));
|
|
639
|
+
try {
|
|
640
|
+
plan = parsePlanFromMarkdown(filePath);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
debugLog(`Failed to read plan file ${filePath}:`, error);
|
|
643
|
+
}
|
|
644
|
+
planFileCache.set(filePath, { modifiedAt, plan });
|
|
645
|
+
return plan;
|
|
646
|
+
}
|
package/src/utils/aria-ref.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebElement } from './web-element.ts';
|
|
2
2
|
|
|
3
|
-
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
3
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=((?:f\d+)?e\d+)\]/;
|
|
4
4
|
const ARIA_REF_PATTERN = /^(f\d+)?e\d+$/i;
|
|
5
5
|
|
|
6
6
|
const REF_ROLES: Record<string, string> = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|
package/src/utils/logger.ts
CHANGED
|
@@ -103,7 +103,12 @@ class ConsoleDestination implements LogDestination {
|
|
|
103
103
|
if (entry.type === 'debug') return;
|
|
104
104
|
if (entry.type === 'html') return;
|
|
105
105
|
if (entry.type === 'operation' && !this.verboseMode) return;
|
|
106
|
-
if (entry.type === 'step' && !this.verboseMode
|
|
106
|
+
if (entry.type === 'step' && !this.verboseMode) {
|
|
107
|
+
const step = entry.originalArgs?.[0];
|
|
108
|
+
const error = entry.originalArgs?.[1];
|
|
109
|
+
if (error || step?.failed === true || step?.status === 'failed') return;
|
|
110
|
+
if (this.recentSteps.shouldSuppress(entry.content)) return;
|
|
111
|
+
}
|
|
107
112
|
let content = entry.content;
|
|
108
113
|
if (entry.type === 'multiline' || entry.type === 'details') {
|
|
109
114
|
const cleaned = stripAnsi(dedent(entry.content));
|