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
|
@@ -43,4 +43,10 @@ export class ResearchCommand extends BaseCommand {
|
|
|
43
43
|
this.suggestions = [{ command: 'research <page> --deep', hint: 'analyze page for all expandable elements and interactions' }];
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
completeArguments() {
|
|
47
|
+
return this.explorBot
|
|
48
|
+
.stateManager()
|
|
49
|
+
.getKnownUrls()
|
|
50
|
+
.map((url) => ({ value: url }));
|
|
51
|
+
}
|
|
46
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
1
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
2
2
|
export declare class TestCommand extends BaseCommand {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
@@ -8,4 +8,5 @@ export declare class TestCommand extends BaseCommand {
|
|
|
8
8
|
}[];
|
|
9
9
|
suggestions: Suggestion[];
|
|
10
10
|
execute(args: string): Promise<void>;
|
|
11
|
+
completeArguments(): ArgumentCompletion[];
|
|
11
12
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
import { BaseCommand } from './base-command.js';
|
|
5
5
|
export class TestCommand extends BaseCommand {
|
|
@@ -74,6 +74,9 @@ export class TestCommand extends BaseCommand {
|
|
|
74
74
|
}
|
|
75
75
|
tag('success').log('Test execution finished');
|
|
76
76
|
}
|
|
77
|
+
completeArguments() {
|
|
78
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: `--from-plan ${file.name}`, display: file.label }));
|
|
79
|
+
}
|
|
77
80
|
}
|
|
78
81
|
function parseTestIndices(input, total) {
|
|
79
82
|
const indices = new Set();
|
|
@@ -1,8 +1,12 @@
|
|
|
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';
|
|
5
|
+
const MAX_HEIGHT = 7;
|
|
6
|
+
const COLUMN_WIDTH = 24;
|
|
4
7
|
const Autocomplete = () => {
|
|
5
8
|
const { suggestions, selectedIndex, visible, argumentHint } = useAutocompleteState();
|
|
9
|
+
const { stdout } = useStdout();
|
|
6
10
|
if (!visible) {
|
|
7
11
|
if (!argumentHint) {
|
|
8
12
|
return null;
|
|
@@ -13,19 +17,32 @@ const Autocomplete = () => {
|
|
|
13
17
|
React.createElement(Text, { dimColor: true }, argumentHint)));
|
|
14
18
|
}
|
|
15
19
|
const effectiveSelectedIndex = Math.min(selectedIndex, suggestions.length - 1);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
20
|
+
const rowsPerColumn = Math.min(Math.max(1, MAX_HEIGHT - 2), suggestions.length);
|
|
21
|
+
const widest = Math.max(...suggestions.map((suggestion) => suggestion.display.length));
|
|
22
|
+
if (widest > COLUMN_WIDTH) {
|
|
23
|
+
const width = Math.max(COLUMN_WIDTH, (stdout?.columns || 80) - 4);
|
|
24
|
+
let firstRow = 0;
|
|
25
|
+
if (effectiveSelectedIndex >= rowsPerColumn) {
|
|
26
|
+
firstRow = effectiveSelectedIndex - rowsPerColumn + 1;
|
|
27
|
+
}
|
|
28
|
+
return (React.createElement(Box, { position: "absolute", top: 0, left: 0, width: "100%", maxHeight: MAX_HEIGHT, overflow: "hidden", paddingX: 1, paddingY: 1, backgroundColor: "#2a2a2a", flexDirection: "column" }, suggestions.slice(firstRow, firstRow + rowsPerColumn).map((suggestion, rowIndex) => {
|
|
29
|
+
const isSelected = firstRow + rowIndex === effectiveSelectedIndex;
|
|
30
|
+
let color = 'white';
|
|
31
|
+
let backgroundColor = '#2a2a2a';
|
|
32
|
+
if (isSelected) {
|
|
33
|
+
color = 'black';
|
|
34
|
+
backgroundColor = '#e6e6e6';
|
|
35
|
+
}
|
|
36
|
+
return (React.createElement(Text, { key: suggestion.value, color: color, backgroundColor: backgroundColor }, ` ${truncate(suggestion.display, width).padEnd(width)} `));
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
18
39
|
const columns = [];
|
|
19
40
|
for (let index = 0; index < suggestions.length; index += rowsPerColumn) {
|
|
20
41
|
columns.push(suggestions.slice(index, index + rowsPerColumn));
|
|
21
42
|
}
|
|
22
|
-
return (React.createElement(Box, { position: "absolute", top: 0, left: 0, width: "100%", maxHeight:
|
|
43
|
+
return (React.createElement(Box, { position: "absolute", top: 0, left: 0, width: "100%", maxHeight: MAX_HEIGHT, overflow: "hidden", paddingX: 1, paddingY: 1, backgroundColor: "#2a2a2a", flexDirection: "row", columnGap: 2 }, columns.map((column, columnIndex) => (React.createElement(Box, { key: columnIndex, flexDirection: "column" }, column.map((suggestion, rowIndex) => {
|
|
23
44
|
const index = columnIndex * rowsPerColumn + rowIndex;
|
|
24
45
|
const isSelected = index === effectiveSelectedIndex;
|
|
25
|
-
let display = suggestion.display;
|
|
26
|
-
if (display.length > 24) {
|
|
27
|
-
display = `${display.slice(0, 21)}...`;
|
|
28
|
-
}
|
|
29
46
|
let color = 'white';
|
|
30
47
|
let backgroundColor = '#2a2a2a';
|
|
31
48
|
if (isSelected) {
|
|
@@ -33,10 +50,7 @@ const Autocomplete = () => {
|
|
|
33
50
|
backgroundColor = '#e6e6e6';
|
|
34
51
|
}
|
|
35
52
|
return (React.createElement(Box, { key: suggestion.display, marginBottom: 1 },
|
|
36
|
-
React.createElement(Text, { color: color, backgroundColor: backgroundColor },
|
|
37
|
-
' ',
|
|
38
|
-
display,
|
|
39
|
-
' ')));
|
|
53
|
+
React.createElement(Text, { color: color, backgroundColor: backgroundColor }, ` ${suggestion.display} `)));
|
|
40
54
|
}))))));
|
|
41
55
|
};
|
|
42
56
|
export default Autocomplete;
|
|
@@ -17,6 +17,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
17
17
|
const cursorRef = useRef(cursorPosition);
|
|
18
18
|
const showAutocompleteRef = useRef(showAutocomplete);
|
|
19
19
|
const selectedIndexRef = useRef(selectedIndex);
|
|
20
|
+
const navigatedRef = useRef(false);
|
|
20
21
|
const historyRef = useRef([]);
|
|
21
22
|
const historyIndexRef = useRef(-1);
|
|
22
23
|
const historyDraftRef = useRef('');
|
|
@@ -55,6 +56,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
55
56
|
cursorRef.current = 0;
|
|
56
57
|
showAutocompleteRef.current = false;
|
|
57
58
|
selectedIndexRef.current = 0;
|
|
59
|
+
navigatedRef.current = false;
|
|
58
60
|
historyIndexRef.current = -1;
|
|
59
61
|
historyDraftRef.current = '';
|
|
60
62
|
setInputState({ value: '', cursor: 0, showAutocomplete: false, selectedIndex: 0 });
|
|
@@ -101,6 +103,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
101
103
|
cursorRef.current = safeCursor;
|
|
102
104
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
103
105
|
selectedIndexRef.current = 0;
|
|
106
|
+
navigatedRef.current = false;
|
|
104
107
|
historyIndexRef.current = -1;
|
|
105
108
|
historyDraftRef.current = '';
|
|
106
109
|
if (isControlled) {
|
|
@@ -173,6 +176,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
173
176
|
if (showAutocompleteRef.current) {
|
|
174
177
|
showAutocompleteRef.current = false;
|
|
175
178
|
selectedIndexRef.current = 0;
|
|
179
|
+
navigatedRef.current = false;
|
|
176
180
|
setInputState((prev) => ({ ...prev, showAutocomplete: false, selectedIndex: 0 }));
|
|
177
181
|
}
|
|
178
182
|
return;
|
|
@@ -184,7 +188,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
184
188
|
}
|
|
185
189
|
if (commandHandler && showAutocompleteRef.current) {
|
|
186
190
|
const autocomplete = getAutocomplete(inputRef.current, cursorRef.current);
|
|
187
|
-
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
191
|
+
if (autocomplete.visible && autocomplete.suggestions.length > 0 && (!autocomplete.completesArgument || navigatedRef.current)) {
|
|
188
192
|
const chosen = applyAutocomplete(selectedIndexRef.current);
|
|
189
193
|
if (chosen) {
|
|
190
194
|
handleSubmit(chosen);
|
|
@@ -321,6 +325,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
321
325
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
322
326
|
const nextIndex = selectedIndexRef.current > 0 ? selectedIndexRef.current - 1 : autocomplete.suggestions.length - 1;
|
|
323
327
|
selectedIndexRef.current = nextIndex;
|
|
328
|
+
navigatedRef.current = true;
|
|
324
329
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
325
330
|
return;
|
|
326
331
|
}
|
|
@@ -330,6 +335,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
330
335
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
331
336
|
const nextIndex = selectedIndexRef.current < autocomplete.suggestions.length - 1 ? selectedIndexRef.current + 1 : 0;
|
|
332
337
|
selectedIndexRef.current = nextIndex;
|
|
338
|
+
navigatedRef.current = true;
|
|
333
339
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
334
340
|
return;
|
|
335
341
|
}
|
|
@@ -388,6 +394,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
388
394
|
inputRef.current = nextValue;
|
|
389
395
|
cursorRef.current = nextValue.length;
|
|
390
396
|
selectedIndexRef.current = 0;
|
|
397
|
+
navigatedRef.current = false;
|
|
391
398
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
392
399
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
393
400
|
return;
|
|
@@ -410,6 +417,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
410
417
|
inputRef.current = nextValue;
|
|
411
418
|
cursorRef.current = nextValue.length;
|
|
412
419
|
selectedIndexRef.current = 0;
|
|
420
|
+
navigatedRef.current = false;
|
|
413
421
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
414
422
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
415
423
|
return;
|
|
@@ -421,6 +429,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
421
429
|
inputRef.current = nextValue;
|
|
422
430
|
cursorRef.current = nextValue.length;
|
|
423
431
|
selectedIndexRef.current = 0;
|
|
432
|
+
navigatedRef.current = false;
|
|
424
433
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
425
434
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
426
435
|
return;
|
package/dist/src/config.d.ts
CHANGED
|
@@ -157,6 +157,12 @@ interface AIConfig {
|
|
|
157
157
|
vision?: boolean;
|
|
158
158
|
visionModel?: any;
|
|
159
159
|
agenticModel?: any;
|
|
160
|
+
decisionModel?: {
|
|
161
|
+
provider: string;
|
|
162
|
+
model: string;
|
|
163
|
+
tool?: boolean;
|
|
164
|
+
direct?: boolean;
|
|
165
|
+
};
|
|
160
166
|
retryAttempts?: number;
|
|
161
167
|
retryDelay?: number;
|
|
162
168
|
maxParallelRequests?: number;
|
|
@@ -224,7 +224,7 @@ export class ExperienceTracker {
|
|
|
224
224
|
const sections = mdq(content).query('section2').each();
|
|
225
225
|
if (sections.length <= 1)
|
|
226
226
|
break;
|
|
227
|
-
content = sections[sections.length - 1].replace('');
|
|
227
|
+
content = sections[sections.length - 1].replace('').toString();
|
|
228
228
|
}
|
|
229
229
|
return { ...experience, content };
|
|
230
230
|
});
|
|
@@ -244,7 +244,7 @@ export class ExperienceTracker {
|
|
|
244
244
|
continue;
|
|
245
245
|
combined = renderAsHowTo(combined);
|
|
246
246
|
if (options?.stripCode) {
|
|
247
|
-
combined = mdq(combined).query('code').replace('');
|
|
247
|
+
combined = mdq(combined).query('code').replace('').toString();
|
|
248
248
|
}
|
|
249
249
|
if (combined.trim())
|
|
250
250
|
results.push(combined.trim());
|
|
@@ -459,5 +459,6 @@ function renderAsHowTo(content) {
|
|
|
459
459
|
if (text.startsWith('ACTION:'))
|
|
460
460
|
return `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
461
461
|
return heading.text();
|
|
462
|
-
})
|
|
462
|
+
})
|
|
463
|
+
.toString();
|
|
463
464
|
}
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Driller } from './ai/driller.js';
|
|
|
5
5
|
import { ExperienceCompactor } from './ai/experience-compactor.js';
|
|
6
6
|
import { Fisherman } from './ai/fisherman.js';
|
|
7
7
|
import { Historian } from './ai/historian.js';
|
|
8
|
+
import { Judge } from './ai/judge.js';
|
|
8
9
|
import { Navigator } from './ai/navigator.js';
|
|
9
10
|
import { Pilot } from './ai/pilot.js';
|
|
10
11
|
import { Planner } from './ai/planner.js';
|
|
@@ -64,6 +65,7 @@ export declare class ExplorBot {
|
|
|
64
65
|
_reporter?: Reporter;
|
|
65
66
|
_requestStore?: RequestStore;
|
|
66
67
|
_playwrightRecorder?: PlaywrightRecorder;
|
|
68
|
+
judgeInstance: Judge | null | undefined;
|
|
67
69
|
constructor(options?: ExplorBotOptions);
|
|
68
70
|
get isExploring(): boolean;
|
|
69
71
|
setUserResolve(fn: UserResolveFunction): void;
|
|
@@ -83,6 +85,7 @@ export declare class ExplorBot {
|
|
|
83
85
|
reporter(): Reporter;
|
|
84
86
|
requestStore(): RequestStore;
|
|
85
87
|
playwrightRecorder(): PlaywrightRecorder;
|
|
88
|
+
judge(): Judge | null;
|
|
86
89
|
getConfig(): ExplorbotConfig;
|
|
87
90
|
getOptions(): ExplorBotOptions;
|
|
88
91
|
getProvider(): AIProvider;
|
package/dist/src/explorbot.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Driller } from "./ai/driller.js";
|
|
|
5
5
|
import { ExperienceCompactor } from "./ai/experience-compactor.js";
|
|
6
6
|
import { Fisherman } from "./ai/fisherman.js";
|
|
7
7
|
import { Historian } from "./ai/historian.js";
|
|
8
|
+
import { Judge } from "./ai/judge.js";
|
|
8
9
|
import { Navigator } from "./ai/navigator.js";
|
|
9
10
|
import { Pilot } from "./ai/pilot.js";
|
|
10
11
|
import { Planner } from "./ai/planner.js";
|
|
@@ -56,6 +57,7 @@ export class ExplorBot {
|
|
|
56
57
|
_reporter;
|
|
57
58
|
_requestStore;
|
|
58
59
|
_playwrightRecorder;
|
|
60
|
+
judgeInstance;
|
|
59
61
|
constructor(options = {}) {
|
|
60
62
|
this.options = options;
|
|
61
63
|
this.configParser = ConfigParser.getInstance();
|
|
@@ -152,6 +154,11 @@ export class ExplorBot {
|
|
|
152
154
|
playwrightRecorder() {
|
|
153
155
|
return (this._playwrightRecorder ||= new PlaywrightRecorder());
|
|
154
156
|
}
|
|
157
|
+
judge() {
|
|
158
|
+
if (this.judgeInstance === undefined)
|
|
159
|
+
this.judgeInstance = Judge.fromConfig(this.config.ai?.decisionModel);
|
|
160
|
+
return this.judgeInstance;
|
|
161
|
+
}
|
|
155
162
|
getConfig() {
|
|
156
163
|
return this.config;
|
|
157
164
|
}
|
|
@@ -170,6 +177,7 @@ export class ExplorBot {
|
|
|
170
177
|
knowledgeTracker: this.knowledgeTracker(),
|
|
171
178
|
requestStore: this.requestStore(),
|
|
172
179
|
playwrightRecorder: this.playwrightRecorder(),
|
|
180
|
+
judge: this.judge() || undefined,
|
|
173
181
|
});
|
|
174
182
|
const agentName = agent.constructor.name.toLowerCase();
|
|
175
183
|
tag('debug').log(`Created ${agentName} agent`);
|
|
@@ -171,7 +171,7 @@ export class KnowledgeTracker {
|
|
|
171
171
|
this.loadKnowledgeFiles();
|
|
172
172
|
return this.knowledgeFiles.map((knowledge) => {
|
|
173
173
|
const content = knowledge.content.trim();
|
|
174
|
-
const firstLine = mdq(content).
|
|
174
|
+
const firstLine = mdq(content).nodes()[0]?.text.split('\n')[0]?.trim() || '';
|
|
175
175
|
return {
|
|
176
176
|
url: knowledge.url || knowledge.endpoint || '',
|
|
177
177
|
firstLine,
|
|
@@ -72,6 +72,7 @@ export declare class StateManager {
|
|
|
72
72
|
currentState: WebPageState | null;
|
|
73
73
|
stateHistory: StateTransition[];
|
|
74
74
|
allVisitedUrls: Set<string>;
|
|
75
|
+
experienceUrls?: string[];
|
|
75
76
|
stateChangeListeners: StateChangeListener[];
|
|
76
77
|
experienceTracker: ExperienceTracker;
|
|
77
78
|
knowledgeTracker: KnowledgeTracker;
|
|
@@ -137,6 +138,7 @@ export declare class StateManager {
|
|
|
137
138
|
*/
|
|
138
139
|
hasVisitedState(path: string): boolean;
|
|
139
140
|
getAllVisitedUrls(): Set<string>;
|
|
141
|
+
getKnownUrls(): string[];
|
|
140
142
|
/**
|
|
141
143
|
* Get how many times we've visited a specific path
|
|
142
144
|
*/
|
|
@@ -9,6 +9,7 @@ export class StateManager {
|
|
|
9
9
|
currentState = null;
|
|
10
10
|
stateHistory = [];
|
|
11
11
|
allVisitedUrls = new Set();
|
|
12
|
+
experienceUrls;
|
|
12
13
|
stateChangeListeners = [];
|
|
13
14
|
experienceTracker;
|
|
14
15
|
knowledgeTracker;
|
|
@@ -243,6 +244,21 @@ export class StateManager {
|
|
|
243
244
|
getAllVisitedUrls() {
|
|
244
245
|
return this.allVisitedUrls;
|
|
245
246
|
}
|
|
247
|
+
getKnownUrls() {
|
|
248
|
+
this.experienceUrls ||= this.experienceTracker
|
|
249
|
+
.getAllExperience()
|
|
250
|
+
.sort((left, right) => right.mtime.getTime() - left.mtime.getTime())
|
|
251
|
+
.flatMap((experience) => experience.data.url || []);
|
|
252
|
+
const sessionUrls = this.stateHistory.map((transition) => transition.toState.url).reverse();
|
|
253
|
+
const urls = new Map();
|
|
254
|
+
for (const url of [...sessionUrls, ...this.experienceUrls]) {
|
|
255
|
+
const key = normalizeUrl(url);
|
|
256
|
+
if (urls.has(key))
|
|
257
|
+
continue;
|
|
258
|
+
urls.set(key, url);
|
|
259
|
+
}
|
|
260
|
+
return [...urls.values()];
|
|
261
|
+
}
|
|
246
262
|
/**
|
|
247
263
|
* Get how many times we've visited a specific path
|
|
248
264
|
*/
|
package/dist/src/test-plan.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export declare class Test extends Task {
|
|
|
96
96
|
endTime?: number;
|
|
97
97
|
resetCount: number;
|
|
98
98
|
appliedExperience: AppliedExperience[];
|
|
99
|
+
preparedData: string[];
|
|
99
100
|
constructor(scenario: string, priority: 'critical' | 'important' | 'high' | 'normal' | 'low', expectedOutcome: string | string[], startUrl: string, plannedSteps?: string[]);
|
|
100
101
|
getVisitedUrls({ localOnly }?: {
|
|
101
102
|
localOnly?: boolean;
|
|
@@ -147,6 +148,7 @@ export declare class Plan {
|
|
|
147
148
|
get allSuccessful(): boolean;
|
|
148
149
|
get allFailed(): boolean;
|
|
149
150
|
updateStatus(): void;
|
|
151
|
+
static listFiles(plansDir: string): PlanFile[];
|
|
150
152
|
static resolveFile(file: string, plansDir?: string): string | null;
|
|
151
153
|
static loadFromFile(file: string, plansDir?: string): Plan | null;
|
|
152
154
|
static fromMarkdown(filePath: string): Plan;
|
|
@@ -178,4 +180,13 @@ interface AppliedExperience {
|
|
|
178
180
|
url: string;
|
|
179
181
|
content: string;
|
|
180
182
|
}
|
|
183
|
+
export interface PlanFile {
|
|
184
|
+
name: string;
|
|
185
|
+
path: string;
|
|
186
|
+
modifiedAt: number;
|
|
187
|
+
title: string;
|
|
188
|
+
url: string;
|
|
189
|
+
testCount: number;
|
|
190
|
+
label: string;
|
|
191
|
+
}
|
|
181
192
|
export {};
|
package/dist/src/test-plan.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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 { listSitePlanDirs } from "./global-config.js";
|
|
5
|
-
import { tag } from "./utils/logger.js";
|
|
5
|
+
import { createDebug, pluralize, tag } from "./utils/logger.js";
|
|
6
|
+
import { truncateMiddle } from "./utils/strings.js";
|
|
6
7
|
import { parsePlanFromMarkdown, planToAiContext, savePlanToMarkdown, savePlansToMarkdown } from "./utils/test-plan-markdown.js";
|
|
7
8
|
import { uniqSessionName } from "./utils/unique-names.js";
|
|
9
|
+
import { extractStatePath } from "./utils/url-matcher.js";
|
|
8
10
|
export const TestResult = {
|
|
9
11
|
PASSED: 'passed',
|
|
10
12
|
FAILED: 'failed',
|
|
@@ -194,6 +196,7 @@ export class Test extends Task {
|
|
|
194
196
|
endTime;
|
|
195
197
|
resetCount = 0;
|
|
196
198
|
appliedExperience = [];
|
|
199
|
+
preparedData = [];
|
|
197
200
|
constructor(scenario, priority, expectedOutcome, startUrl, plannedSteps = []) {
|
|
198
201
|
super(scenario, startUrl);
|
|
199
202
|
this.scenario = scenario;
|
|
@@ -397,6 +400,37 @@ export class Plan {
|
|
|
397
400
|
return this.tests.length > 0 && this.tests.every((test) => test.hasFailed);
|
|
398
401
|
}
|
|
399
402
|
updateStatus() { }
|
|
403
|
+
static listFiles(plansDir) {
|
|
404
|
+
if (!existsSync(plansDir))
|
|
405
|
+
return [];
|
|
406
|
+
const files = readdirSync(plansDir)
|
|
407
|
+
.filter((file) => file.endsWith('.md'))
|
|
408
|
+
.map((file) => {
|
|
409
|
+
const filePath = path.join(plansDir, file);
|
|
410
|
+
const modifiedAt = statSync(filePath).mtimeMs;
|
|
411
|
+
const plan = readPlanFile(filePath, modifiedAt);
|
|
412
|
+
return {
|
|
413
|
+
name: file,
|
|
414
|
+
path: filePath,
|
|
415
|
+
modifiedAt,
|
|
416
|
+
title: plan.title,
|
|
417
|
+
url: extractStatePath(plan.startUrl || ''),
|
|
418
|
+
testCount: plan.tests.length,
|
|
419
|
+
label: '',
|
|
420
|
+
};
|
|
421
|
+
})
|
|
422
|
+
.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
423
|
+
if (files.length === 0)
|
|
424
|
+
return files;
|
|
425
|
+
const nameWidth = Math.min(MAX_PLAN_NAME_WIDTH, Math.max(...files.map((file) => file.name.length)));
|
|
426
|
+
const urlWidth = Math.min(MAX_PLAN_URL_WIDTH, Math.max(...files.map((file) => file.url.length)));
|
|
427
|
+
for (const file of files) {
|
|
428
|
+
const name = truncateMiddle(file.name, nameWidth).padEnd(nameWidth);
|
|
429
|
+
const url = truncateMiddle(file.url, urlWidth).padEnd(urlWidth);
|
|
430
|
+
file.label = `${name} ${url} ${file.testCount} ${pluralize(file.testCount, 'test')}`;
|
|
431
|
+
}
|
|
432
|
+
return files;
|
|
433
|
+
}
|
|
400
434
|
static resolveFile(file, plansDir) {
|
|
401
435
|
const names = [file];
|
|
402
436
|
if (!file.endsWith('.md'))
|
|
@@ -464,3 +498,21 @@ export class Plan {
|
|
|
464
498
|
return planToAiContext(this, options);
|
|
465
499
|
}
|
|
466
500
|
}
|
|
501
|
+
const debugLog = createDebug('explorbot:test-plan');
|
|
502
|
+
const MAX_PLAN_NAME_WIDTH = 38;
|
|
503
|
+
const MAX_PLAN_URL_WIDTH = 26;
|
|
504
|
+
const planFileCache = new Map();
|
|
505
|
+
function readPlanFile(filePath, modifiedAt) {
|
|
506
|
+
const cached = planFileCache.get(filePath);
|
|
507
|
+
if (cached?.modifiedAt === modifiedAt)
|
|
508
|
+
return cached.plan;
|
|
509
|
+
let plan = new Plan(path.basename(filePath));
|
|
510
|
+
try {
|
|
511
|
+
plan = parsePlanFromMarkdown(filePath);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
debugLog(`Failed to read plan file ${filePath}:`, error);
|
|
515
|
+
}
|
|
516
|
+
planFileCache.set(filePath, { modifiedAt, plan });
|
|
517
|
+
return plan;
|
|
518
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebElement } from "./web-element.js";
|
|
2
|
-
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
2
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=((?:f\d+)?e\d+)\]/;
|
|
3
3
|
const ARIA_REF_PATTERN = /^(f\d+)?e\d+$/i;
|
|
4
4
|
const REF_ROLES = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|
|
5
5
|
export function ariaRefSelector(ref) {
|
package/dist/src/utils/logger.js
CHANGED
|
@@ -75,8 +75,14 @@ class ConsoleDestination {
|
|
|
75
75
|
return;
|
|
76
76
|
if (entry.type === 'operation' && !this.verboseMode)
|
|
77
77
|
return;
|
|
78
|
-
if (entry.type === 'step' && !this.verboseMode
|
|
79
|
-
|
|
78
|
+
if (entry.type === 'step' && !this.verboseMode) {
|
|
79
|
+
const step = entry.originalArgs?.[0];
|
|
80
|
+
const error = entry.originalArgs?.[1];
|
|
81
|
+
if (error || step?.failed === true || step?.status === 'failed')
|
|
82
|
+
return;
|
|
83
|
+
if (this.recentSteps.shouldSuppress(entry.content))
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
80
86
|
let content = entry.content;
|
|
81
87
|
if (entry.type === 'multiline' || entry.type === 'details') {
|
|
82
88
|
const cleaned = stripAnsi(dedent(entry.content));
|
|
@@ -1,48 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export type SelectorType = 'section' | 'section1' | 'section2' | 'section3' | 'section4' | 'section5' | 'section6' | 'table' | 'heading' | 'paragraph' | 'list' | 'item' | 'code' | 'blockquote' | 'hr' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
3
|
-
export interface TextMatcher {
|
|
4
|
-
mode: 'exact' | 'contains' | 'regex';
|
|
5
|
-
value: string;
|
|
6
|
-
negated: boolean;
|
|
7
|
-
}
|
|
8
|
-
export interface QuerySegment {
|
|
9
|
-
selector: SelectorType;
|
|
10
|
-
textMatch?: TextMatcher;
|
|
11
|
-
index: number | null;
|
|
12
|
-
slice: {
|
|
13
|
-
from?: number;
|
|
14
|
-
to?: number;
|
|
15
|
-
} | null;
|
|
16
|
-
}
|
|
17
|
-
export interface MatchedRange {
|
|
18
|
-
token: Token;
|
|
19
|
-
start: number;
|
|
20
|
-
length: number;
|
|
21
|
-
innerTokens?: MatchedRange[];
|
|
22
|
-
}
|
|
23
|
-
export declare function parseQuery(input: string): QuerySegment[];
|
|
24
|
-
export declare class MarkdownQuery {
|
|
25
|
-
source: string;
|
|
26
|
-
matches: MatchedRange[];
|
|
27
|
-
constructor(source: string, matches?: MatchedRange[]);
|
|
28
|
-
query(selector: string): MarkdownQuery;
|
|
29
|
-
text(): string;
|
|
30
|
-
get(): string;
|
|
31
|
-
toJson(): Record<string, string>[];
|
|
32
|
-
keyValue(): Record<string, string>;
|
|
33
|
-
setKeyValue(key: string, value: string | null): string;
|
|
34
|
-
replace(content: string): string;
|
|
35
|
-
replaceEach(replacer: (match: MarkdownQuery, index: number) => string): string;
|
|
36
|
-
count(): number;
|
|
37
|
-
first(): MarkdownQuery;
|
|
38
|
-
last(): MarkdownQuery;
|
|
39
|
-
before(): MarkdownQuery;
|
|
40
|
-
after(): MarkdownQuery;
|
|
41
|
-
each(): MarkdownQuery[];
|
|
42
|
-
meta(): Array<{
|
|
43
|
-
type: string;
|
|
44
|
-
depth: number | null;
|
|
45
|
-
text: string;
|
|
46
|
-
}>;
|
|
47
|
-
}
|
|
48
|
-
export declare function mdq(source: string): MarkdownQuery;
|
|
1
|
+
export * from './mdq/query.js';
|