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
@@ -17,7 +17,7 @@ export const DEADLINE_RESERVE_MS = 3 * 60_000;
17
17
  export const DEADLINE_TEST_ALLOWANCE_MS = 5 * 60_000;
18
18
  export class ExploreCommand extends BaseCommand {
19
19
  name = 'explore';
20
- description = 'Start web exploration';
20
+ description = 'Start web exploration on the current page, or on the URL given as the first argument';
21
21
  options = [
22
22
  { flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
23
23
  { flags: '--max-duration <minutes>', description: 'Wall-clock budget in minutes; wraps up the session before the limit is hit' },
@@ -55,6 +55,11 @@ export class ExploreCommand extends BaseCommand {
55
55
  if (this.hardDeadlineAt == null && this.maxDurationMinutes != null) {
56
56
  this.hardDeadlineAt = Date.now() + this.maxDurationMinutes * 60_000 - DEADLINE_RESERVE_MS;
57
57
  }
58
+ const [target] = remaining;
59
+ if (target?.startsWith('/') || target?.startsWith('http://') || target?.startsWith('https://')) {
60
+ remaining.shift();
61
+ await this.explorBot.visit(target);
62
+ }
58
63
  const feature = opts.focus || remaining.join(' ') || undefined;
59
64
  const cfg = this.parseConfigure(opts.configure);
60
65
  if (cfg.priorities)
@@ -96,6 +101,12 @@ export class ExploreCommand extends BaseCommand {
96
101
  await this.explorBot.printSessionAnalysis();
97
102
  }
98
103
  }
104
+ completeArguments() {
105
+ return this.explorBot
106
+ .stateManager()
107
+ .getKnownUrls()
108
+ .map((url) => ({ value: url }));
109
+ }
99
110
  originLabel(test) {
100
111
  return this.oldTestRefs.has(test) ? 'OLD' : 'NEW';
101
112
  }
@@ -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';
@@ -65,11 +66,16 @@ export class FreesailCommand extends BaseCommand {
65
66
  const suggestion = await navigator.freeSail({ strategy, scope, visitedUrls });
66
67
  if (!suggestion) {
67
68
  tag('info').log('No navigation suggestion available');
68
- return;
69
+ ctx.stop();
69
70
  }
70
71
  if (scope && !suggestion.target.startsWith(scope)) {
71
72
  tag('warning').log(`Suggestion ${suggestion.target} is outside scope ${scope}, skipping`);
72
- return;
73
+ ctx.stop();
74
+ }
75
+ const currentUrl = stateManager.getCurrentState()?.url;
76
+ if (currentUrl && normalizeUrl(suggestion.target) === normalizeUrl(currentUrl)) {
77
+ tag('info').log(`No new navigation target available after ${currentUrl}; stopping exploration`);
78
+ ctx.stop();
73
79
  }
74
80
  tag('info').log(`Navigating to: ${suggestion.target} - ${suggestion.reason}`);
75
81
  await this.explorBot.openTab();
@@ -145,7 +145,7 @@ export function runInitCommand(options) {
145
145
  log('2. Set AI models config file');
146
146
  log('3. Set web application URL in the config file');
147
147
  log('4. Add initial knowledge (how to authorize to the application, etc.)');
148
- tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to authorize use these credentials: admin@example.com / secret123'`));
148
+ tag('substep').log(chalk.yellow(`${getCliName()} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
149
149
  tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
150
150
  log('5. Launch application on a relative URL');
151
151
  tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
@@ -1,7 +1,8 @@
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 NavigateCommand extends BaseCommand {
3
3
  name: string;
4
4
  description: string;
5
5
  suggestions: Suggestion[];
6
6
  execute(args: string): Promise<void>;
7
+ completeArguments(): ArgumentCompletion[];
7
8
  }
@@ -15,4 +15,10 @@ export class NavigateCommand extends BaseCommand {
15
15
  await this.explorBot.visit(destination);
16
16
  tag('success').log(`Navigation requested: ${destination}`);
17
17
  }
18
+ completeArguments() {
19
+ return this.explorBot
20
+ .stateManager()
21
+ .getKnownUrls()
22
+ .map((url) => ({ value: url }));
23
+ }
18
24
  }
@@ -1,7 +1,8 @@
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 PlanLoadCommand extends BaseCommand {
3
3
  name: string;
4
4
  description: string;
5
5
  suggestions: Suggestion[];
6
6
  execute(args: string): Promise<void>;
7
+ completeArguments(): ArgumentCompletion[];
7
8
  }
@@ -1,3 +1,4 @@
1
+ import { Plan } from '../test-plan.js';
1
2
  import { tag } from '../utils/logger.js';
2
3
  import { BaseCommand } from './base-command.js';
3
4
  export class PlanLoadCommand extends BaseCommand {
@@ -15,4 +16,7 @@ export class PlanLoadCommand extends BaseCommand {
15
16
  const plan = this.explorBot.loadPlan(filename);
16
17
  tag('success').log(`Plan loaded: ${plan.title} with ${plan.tests.length} tests`);
17
18
  }
19
+ completeArguments() {
20
+ return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
21
+ }
18
22
  }
@@ -1,5 +1,5 @@
1
- import { Plan } from '../test-plan.js';
2
- import { BaseCommand } from './base-command.js';
1
+ import { Plan, type PlanFile } from '../test-plan.js';
2
+ import { type ArgumentCompletion, BaseCommand } from './base-command.js';
3
3
  export declare class PlansCommand extends BaseCommand {
4
4
  name: string;
5
5
  description: string;
@@ -8,7 +8,7 @@ export declare class PlansCommand extends BaseCommand {
8
8
  description: string;
9
9
  }[];
10
10
  execute(args: string): Promise<void>;
11
- getPlanFiles(): PlanFile[];
11
+ completeArguments(): ArgumentCompletion[];
12
12
  printPlans(files: PlanFile[]): void;
13
13
  printPlanDetails(plan: Plan, file: PlanFile): void;
14
14
  resolvePlanFile(target: string, files: PlanFile[]): {
@@ -16,9 +16,3 @@ export declare class PlansCommand extends BaseCommand {
16
16
  file: PlanFile;
17
17
  };
18
18
  }
19
- interface PlanFile {
20
- name: string;
21
- path: string;
22
- modifiedAt: number;
23
- }
24
- export {};
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
1
+ import { statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Plan } from '../test-plan.js';
4
4
  import { getCliName } from '../utils/cli-name.js';
@@ -11,7 +11,7 @@ export class PlansCommand extends BaseCommand {
11
11
  options = [{ flags: '--from-plan <file>', description: 'Plan file to show' }];
12
12
  async execute(args) {
13
13
  const { opts, args: remaining } = this.parseArgs(args);
14
- const files = this.getPlanFiles();
14
+ const files = Plan.listFiles(this.explorBot.getPlansDir());
15
15
  const target = String(opts.fromPlan || remaining[0] || '').trim();
16
16
  if (!target) {
17
17
  this.printPlans(files);
@@ -20,22 +20,8 @@ export class PlansCommand extends BaseCommand {
20
20
  const { plan, file } = this.resolvePlanFile(target, files);
21
21
  this.printPlanDetails(plan, file);
22
22
  }
23
- getPlanFiles() {
24
- const plansDir = this.explorBot.getPlansDir();
25
- if (!existsSync(plansDir))
26
- return [];
27
- return readdirSync(plansDir)
28
- .filter((file) => file.endsWith('.md'))
29
- .map((file) => {
30
- const filePath = path.join(plansDir, file);
31
- const stat = statSync(filePath);
32
- return {
33
- name: file,
34
- path: filePath,
35
- modifiedAt: stat.mtimeMs,
36
- };
37
- })
38
- .sort((left, right) => right.modifiedAt - left.modifiedAt);
23
+ completeArguments() {
24
+ return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
39
25
  }
40
26
  printPlans(files) {
41
27
  if (files.length === 0) {
@@ -45,8 +31,7 @@ export class PlansCommand extends BaseCommand {
45
31
  tag('info').log('Saved plans:');
46
32
  for (let i = 0; i < files.length; i++) {
47
33
  const file = files[i];
48
- const plan = Plan.fromMarkdown(file.path);
49
- tag('info').log(`${i + 1}. ${plan.title} (${plan.tests.length} tests) - ${file.name}`);
34
+ tag('info').log(`${i + 1}. ${file.title} (${file.testCount} tests) - ${file.name}`);
50
35
  }
51
36
  tag('info').log('');
52
37
  tag('info').log(`View plan tests: ${getCliName()} plans <number>`);
@@ -73,10 +58,15 @@ export class PlansCommand extends BaseCommand {
73
58
  if (!plan?.filePath) {
74
59
  throw new Error(`Plan file not found: ${target}`);
75
60
  }
61
+ const name = path.basename(plan.filePath);
76
62
  const file = {
77
- name: path.basename(plan.filePath),
63
+ name,
78
64
  path: plan.filePath,
79
65
  modifiedAt: statSync(plan.filePath).mtimeMs,
66
+ title: plan.title,
67
+ url: plan.startUrl || '',
68
+ testCount: plan.tests.length,
69
+ label: name,
80
70
  };
81
71
  return { plan, file };
82
72
  }
@@ -1,7 +1,8 @@
1
- import { BaseCommand } from './base-command.js';
1
+ import { type ArgumentCompletion, BaseCommand } from './base-command.js';
2
2
  export declare class RerunCommand extends BaseCommand {
3
3
  name: string;
4
4
  description: string;
5
5
  tuiEnabled: boolean;
6
6
  execute(args: string): Promise<void>;
7
+ completeArguments(): ArgumentCompletion[];
7
8
  }
@@ -1,7 +1,8 @@
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.js";
4
4
  import { tag } from "../utils/logger.js";
5
+ import { listTestFiles } from "../utils/test-files.js";
5
6
  import { BaseCommand } from './base-command.js';
6
7
  export class RerunCommand extends BaseCommand {
7
8
  name = 'rerun';
@@ -26,6 +27,9 @@ export class RerunCommand extends BaseCommand {
26
27
  const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
27
28
  await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
28
29
  }
30
+ completeArguments() {
31
+ return listTestFiles(ConfigParser.getInstance().getTestsDir()).map((file) => ({ value: basename(file) }));
32
+ }
29
33
  }
30
34
  function parseTestIndices(input) {
31
35
  if (input === '*' || input === 'all')
@@ -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 ResearchCommand extends BaseCommand {
3
3
  name: string;
4
4
  description: string;
@@ -8,4 +8,5 @@ export declare class ResearchCommand extends BaseCommand {
8
8
  description: string;
9
9
  }[];
10
10
  execute(args: string): Promise<void>;
11
+ completeArguments(): ArgumentCompletion[];
11
12
  }
@@ -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 maxHeight = 7;
17
- const rowsPerColumn = Math.min(Math.max(1, maxHeight - 2), suggestions.length);
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: maxHeight, 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) => {
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;
@@ -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
  }
@@ -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;
@@ -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`);
@@ -13,6 +13,7 @@ import { ConfigParser } from './config.js';
13
13
  import { BrowserRecoveryError, browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
14
14
  import { createDebug, log, tag } from './utils/logger.js';
15
15
  import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
16
+ import { isInternalStep } from "./utils/step-analyzer.js";
16
17
  const debugLog = createDebug('explorbot:explorer');
17
18
  const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
18
19
  const RECOVERY_NAVIGATION = { waitUntil: 'domcontentloaded', timeout: 10000 };
@@ -161,9 +162,7 @@ class Explorer {
161
162
  const stepHandler = (step, status, error, log) => {
162
163
  if (!step.toCode)
163
164
  return;
164
- if (step?.name?.startsWith('grab'))
165
- return;
166
- if (step?.name?.startsWith('save'))
165
+ if (isInternalStep(step))
167
166
  return;
168
167
  test.addStep(step.toCode(), step.duration, status, error, log);
169
168
  if (!this.stateManager.getCurrentState())
@@ -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).meta()[0]?.text.split('\n')[0]?.trim() || '';
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,
@@ -111,6 +111,7 @@ export class Reporter {
111
111
  }
112
112
  combineStepsAndNotes(test, lastScreenshotFile) {
113
113
  const noteEntries = Object.entries(test.notes)
114
+ .filter(([, note]) => !note.observation)
114
115
  .map(([timestampKey, note]) => ({
115
116
  startTime: note.startTime,
116
117
  endTime: note.endTime,
@@ -233,9 +234,12 @@ export class Reporter {
233
234
  description: test.description,
234
235
  code: test.generatedCode || '',
235
236
  steps,
236
- logs: Object.values(test.steps)
237
- .map((stepData) => stepData.text)
238
- .join('\n'),
237
+ logs: [
238
+ ...Object.values(test.steps).map((stepData) => stepData.text),
239
+ ...Object.values(test.notes)
240
+ .filter((note) => note.observation)
241
+ .map((note) => note.message),
242
+ ].join('\n'),
239
243
  files: Object.values(test.artifacts) || [],
240
244
  message: test.summary || this.extractLastNoteMessage(test) || '',
241
245
  meta,
@@ -300,7 +304,7 @@ export class Reporter {
300
304
  }
301
305
  }
302
306
  extractLastNoteMessage(test) {
303
- const notes = Object.values(test.notes);
307
+ const notes = Object.values(test.notes).filter((note) => !note.observation);
304
308
  if (notes.length === 0)
305
309
  return '';
306
310
  return notes[notes.length - 1].message;
@@ -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
  */