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.
Files changed (144) hide show
  1. package/README.md +4 -1
  2. package/bin/mdq.ts +18 -0
  3. package/boat/api-tester/src/ai/chief.ts +72 -0
  4. package/boat/api-tester/src/api-client.ts +37 -0
  5. package/boat/prima/src/prima.ts +41 -2
  6. package/dist/bin/mdq.js +19 -0
  7. package/dist/boat/api-tester/src/ai/chief.js +69 -0
  8. package/dist/boat/api-tester/src/api-client.js +26 -0
  9. package/dist/boat/prima/src/prima.js +42 -2
  10. package/dist/package.json +3 -2
  11. package/dist/src/action.js +4 -2
  12. package/dist/src/ai/agent.d.ts +3 -1
  13. package/dist/src/ai/judge-provider.d.ts +17 -0
  14. package/dist/src/ai/judge-provider.js +56 -0
  15. package/dist/src/ai/judge-tool.d.ts +2 -0
  16. package/dist/src/ai/judge-tool.js +33 -0
  17. package/dist/src/ai/judge.d.ts +28 -0
  18. package/dist/src/ai/judge.js +71 -0
  19. package/dist/src/ai/navigator.d.ts +3 -1
  20. package/dist/src/ai/navigator.js +24 -28
  21. package/dist/src/ai/pilot.d.ts +4 -0
  22. package/dist/src/ai/pilot.js +67 -9
  23. package/dist/src/ai/planner.js +9 -6
  24. package/dist/src/ai/provider.d.ts +4 -1
  25. package/dist/src/ai/provider.js +52 -7
  26. package/dist/src/ai/rerunner.js +7 -0
  27. package/dist/src/ai/researcher/deep-analysis.js +2 -2
  28. package/dist/src/ai/researcher/locators.js +2 -2
  29. package/dist/src/ai/researcher/pagination.js +1 -1
  30. package/dist/src/ai/researcher/research-result.js +2 -2
  31. package/dist/src/ai/researcher.js +1 -1
  32. package/dist/src/ai/task-agent.d.ts +2 -0
  33. package/dist/src/ai/task-agent.js +3 -1
  34. package/dist/src/ai/tester.js +19 -15
  35. package/dist/src/ai/tools.d.ts +4 -3
  36. package/dist/src/ai/tools.js +35 -7
  37. package/dist/src/api/request-result.js +2 -1
  38. package/dist/src/api/xhr-capture.js +2 -1
  39. package/dist/src/command-handler.d.ts +1 -0
  40. package/dist/src/command-handler.js +24 -3
  41. package/dist/src/commands/base-command.d.ts +5 -0
  42. package/dist/src/commands/base-command.js +3 -0
  43. package/dist/src/commands/explore-command.d.ts +2 -1
  44. package/dist/src/commands/explore-command.js +12 -1
  45. package/dist/src/commands/freesail-command.js +8 -2
  46. package/dist/src/commands/init-command.js +1 -1
  47. package/dist/src/commands/navigate-command.d.ts +2 -1
  48. package/dist/src/commands/navigate-command.js +6 -0
  49. package/dist/src/commands/plan-load-command.d.ts +2 -1
  50. package/dist/src/commands/plan-load-command.js +4 -0
  51. package/dist/src/commands/plans-command.d.ts +3 -9
  52. package/dist/src/commands/plans-command.js +11 -21
  53. package/dist/src/commands/rerun-command.d.ts +2 -1
  54. package/dist/src/commands/rerun-command.js +5 -1
  55. package/dist/src/commands/research-command.d.ts +2 -1
  56. package/dist/src/commands/research-command.js +6 -0
  57. package/dist/src/commands/test-command.d.ts +2 -1
  58. package/dist/src/commands/test-command.js +4 -1
  59. package/dist/src/components/Autocomplete.js +26 -12
  60. package/dist/src/components/InputReadline.js +10 -1
  61. package/dist/src/config.d.ts +6 -0
  62. package/dist/src/experience-tracker.js +4 -3
  63. package/dist/src/explorbot.d.ts +3 -0
  64. package/dist/src/explorbot.js +8 -0
  65. package/dist/src/explorer.js +2 -3
  66. package/dist/src/knowledge-tracker.js +1 -1
  67. package/dist/src/reporter.js +8 -4
  68. package/dist/src/state-manager.d.ts +2 -0
  69. package/dist/src/state-manager.js +16 -0
  70. package/dist/src/test-plan.d.ts +11 -0
  71. package/dist/src/test-plan.js +54 -2
  72. package/dist/src/utils/aria-ref.js +1 -1
  73. package/dist/src/utils/logger.js +9 -3
  74. package/dist/src/utils/markdown-query.d.ts +1 -48
  75. package/dist/src/utils/markdown-query.js +1 -444
  76. package/dist/src/utils/mdq/cli.d.ts +6 -0
  77. package/dist/src/utils/mdq/cli.js +122 -0
  78. package/dist/src/utils/mdq/edit.d.ts +24 -0
  79. package/dist/src/utils/mdq/edit.js +147 -0
  80. package/dist/src/utils/mdq/query.d.ts +118 -0
  81. package/dist/src/utils/mdq/query.js +451 -0
  82. package/dist/src/utils/step-analyzer.d.ts +3 -0
  83. package/dist/src/utils/step-analyzer.js +7 -0
  84. package/dist/src/utils/strings.d.ts +1 -0
  85. package/dist/src/utils/strings.js +7 -0
  86. package/dist/src/utils/test-files.d.ts +1 -0
  87. package/dist/src/utils/test-files.js +5 -2
  88. package/dist/src/utils/url-matcher.d.ts +1 -0
  89. package/dist/src/utils/url-matcher.js +7 -0
  90. package/docs/api-testing/planning.md +1 -1
  91. package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
  92. package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
  93. package/package.json +3 -2
  94. package/src/action.ts +4 -2
  95. package/src/ai/agent.ts +3 -1
  96. package/src/ai/judge-provider.ts +62 -0
  97. package/src/ai/judge-tool.ts +35 -0
  98. package/src/ai/judge.ts +75 -0
  99. package/src/ai/navigator.ts +24 -27
  100. package/src/ai/pilot.ts +68 -9
  101. package/src/ai/planner.ts +9 -6
  102. package/src/ai/provider.ts +51 -7
  103. package/src/ai/rerunner.ts +4 -0
  104. package/src/ai/researcher/deep-analysis.ts +2 -2
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/pagination.ts +1 -1
  107. package/src/ai/researcher/research-result.ts +2 -2
  108. package/src/ai/researcher.ts +1 -1
  109. package/src/ai/task-agent.ts +4 -1
  110. package/src/ai/tester.ts +19 -16
  111. package/src/ai/tools.ts +42 -7
  112. package/src/api/request-result.ts +2 -1
  113. package/src/api/xhr-capture.ts +2 -1
  114. package/src/command-handler.ts +28 -3
  115. package/src/commands/base-command.ts +9 -0
  116. package/src/commands/explore-command.ts +15 -2
  117. package/src/commands/freesail-command.ts +9 -2
  118. package/src/commands/init-command.ts +1 -1
  119. package/src/commands/navigate-command.ts +8 -1
  120. package/src/commands/plan-load-command.ts +6 -1
  121. package/src/commands/plans-command.ts +13 -29
  122. package/src/commands/rerun-command.ts +7 -2
  123. package/src/commands/research-command.ts +8 -1
  124. package/src/commands/test-command.ts +6 -2
  125. package/src/components/Autocomplete.tsx +39 -10
  126. package/src/components/InputReadline.tsx +10 -1
  127. package/src/config.ts +1 -0
  128. package/src/experience-tracker.ts +4 -3
  129. package/src/explorbot.ts +8 -0
  130. package/src/explorer.ts +2 -2
  131. package/src/knowledge-tracker.ts +1 -1
  132. package/src/reporter.ts +8 -4
  133. package/src/state-manager.ts +16 -0
  134. package/src/test-plan.ts +67 -2
  135. package/src/utils/aria-ref.ts +1 -1
  136. package/src/utils/logger.ts +7 -2
  137. package/src/utils/markdown-query.ts +1 -519
  138. package/src/utils/mdq/cli.ts +118 -0
  139. package/src/utils/mdq/edit.ts +158 -0
  140. package/src/utils/mdq/query.ts +556 -0
  141. package/src/utils/step-analyzer.ts +8 -0
  142. package/src/utils/strings.ts +7 -0
  143. package/src/utils/test-files.ts +5 -2
  144. 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
- return;
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
- return;
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 { existsSync, readdirSync, statSync } from 'node:fs';
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.getPlanFiles();
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
- private getPlanFiles(): PlanFile[] {
29
- const plansDir = this.explorBot.getPlansDir();
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
- const plan = Plan.fromMarkdown(file.path);
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: path.basename(plan.filePath),
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 { BaseCommand } from './base-command.js';
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 maxHeight = 7;
24
- const rowsPerColumn = Math.min(Math.max(1, maxHeight - 2), suggestions.length);
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={maxHeight} overflow="hidden" paddingX={1} paddingY={1} backgroundColor="#2a2a2a" flexDirection="row" columnGap={2}>
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?.name?.startsWith('grab')) return;
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
 
@@ -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).meta()[0]?.text.split('\n')[0]?.trim() || '';
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: Object.values(test.steps)
276
- .map((stepData) => stepData.text)
277
- .join('\n'),
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
  }
@@ -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
  */