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.
Files changed (128) 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/ai/agent.d.ts +3 -1
  12. package/dist/src/ai/judge-provider.d.ts +17 -0
  13. package/dist/src/ai/judge-provider.js +56 -0
  14. package/dist/src/ai/judge-tool.d.ts +2 -0
  15. package/dist/src/ai/judge-tool.js +33 -0
  16. package/dist/src/ai/judge.d.ts +28 -0
  17. package/dist/src/ai/judge.js +71 -0
  18. package/dist/src/ai/navigator.d.ts +3 -0
  19. package/dist/src/ai/navigator.js +13 -4
  20. package/dist/src/ai/pilot.d.ts +4 -0
  21. package/dist/src/ai/pilot.js +57 -5
  22. package/dist/src/ai/planner.js +9 -6
  23. package/dist/src/ai/provider.d.ts +4 -1
  24. package/dist/src/ai/provider.js +52 -7
  25. package/dist/src/ai/researcher/deep-analysis.js +2 -2
  26. package/dist/src/ai/researcher/locators.js +2 -2
  27. package/dist/src/ai/researcher/pagination.js +1 -1
  28. package/dist/src/ai/researcher/research-result.js +2 -2
  29. package/dist/src/ai/researcher.js +1 -1
  30. package/dist/src/ai/task-agent.d.ts +2 -0
  31. package/dist/src/ai/task-agent.js +3 -1
  32. package/dist/src/ai/tester.js +19 -15
  33. package/dist/src/ai/tools.d.ts +4 -3
  34. package/dist/src/ai/tools.js +35 -7
  35. package/dist/src/api/request-result.js +2 -1
  36. package/dist/src/command-handler.d.ts +1 -0
  37. package/dist/src/command-handler.js +24 -3
  38. package/dist/src/commands/base-command.d.ts +5 -0
  39. package/dist/src/commands/base-command.js +3 -0
  40. package/dist/src/commands/explore-command.d.ts +2 -1
  41. package/dist/src/commands/explore-command.js +12 -1
  42. package/dist/src/commands/freesail-command.js +8 -2
  43. package/dist/src/commands/init-command.js +1 -1
  44. package/dist/src/commands/navigate-command.d.ts +2 -1
  45. package/dist/src/commands/navigate-command.js +6 -0
  46. package/dist/src/commands/plan-load-command.d.ts +2 -1
  47. package/dist/src/commands/plan-load-command.js +4 -0
  48. package/dist/src/commands/plans-command.d.ts +3 -9
  49. package/dist/src/commands/plans-command.js +11 -21
  50. package/dist/src/commands/rerun-command.d.ts +2 -1
  51. package/dist/src/commands/rerun-command.js +5 -1
  52. package/dist/src/commands/research-command.d.ts +2 -1
  53. package/dist/src/commands/research-command.js +6 -0
  54. package/dist/src/commands/test-command.d.ts +2 -1
  55. package/dist/src/commands/test-command.js +4 -1
  56. package/dist/src/components/Autocomplete.js +26 -12
  57. package/dist/src/components/InputReadline.js +10 -1
  58. package/dist/src/config.d.ts +6 -0
  59. package/dist/src/experience-tracker.js +4 -3
  60. package/dist/src/explorbot.d.ts +3 -0
  61. package/dist/src/explorbot.js +8 -0
  62. package/dist/src/knowledge-tracker.js +1 -1
  63. package/dist/src/state-manager.d.ts +2 -0
  64. package/dist/src/state-manager.js +16 -0
  65. package/dist/src/test-plan.d.ts +11 -0
  66. package/dist/src/test-plan.js +54 -2
  67. package/dist/src/utils/aria-ref.js +1 -1
  68. package/dist/src/utils/logger.js +8 -2
  69. package/dist/src/utils/markdown-query.d.ts +1 -48
  70. package/dist/src/utils/markdown-query.js +1 -444
  71. package/dist/src/utils/mdq/cli.d.ts +6 -0
  72. package/dist/src/utils/mdq/cli.js +122 -0
  73. package/dist/src/utils/mdq/edit.d.ts +24 -0
  74. package/dist/src/utils/mdq/edit.js +147 -0
  75. package/dist/src/utils/mdq/query.d.ts +118 -0
  76. package/dist/src/utils/mdq/query.js +451 -0
  77. package/dist/src/utils/strings.d.ts +1 -0
  78. package/dist/src/utils/strings.js +7 -0
  79. package/dist/src/utils/test-files.d.ts +1 -0
  80. package/dist/src/utils/test-files.js +5 -2
  81. package/docs/api-testing/planning.md +1 -1
  82. package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
  83. package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
  84. package/package.json +3 -2
  85. package/src/ai/agent.ts +3 -1
  86. package/src/ai/judge-provider.ts +62 -0
  87. package/src/ai/judge-tool.ts +35 -0
  88. package/src/ai/judge.ts +75 -0
  89. package/src/ai/navigator.ts +15 -4
  90. package/src/ai/pilot.ts +58 -5
  91. package/src/ai/planner.ts +9 -6
  92. package/src/ai/provider.ts +51 -7
  93. package/src/ai/researcher/deep-analysis.ts +2 -2
  94. package/src/ai/researcher/locators.ts +2 -2
  95. package/src/ai/researcher/pagination.ts +1 -1
  96. package/src/ai/researcher/research-result.ts +2 -2
  97. package/src/ai/researcher.ts +1 -1
  98. package/src/ai/task-agent.ts +4 -1
  99. package/src/ai/tester.ts +19 -16
  100. package/src/ai/tools.ts +42 -7
  101. package/src/api/request-result.ts +2 -1
  102. package/src/command-handler.ts +28 -3
  103. package/src/commands/base-command.ts +9 -0
  104. package/src/commands/explore-command.ts +15 -2
  105. package/src/commands/freesail-command.ts +9 -2
  106. package/src/commands/init-command.ts +1 -1
  107. package/src/commands/navigate-command.ts +8 -1
  108. package/src/commands/plan-load-command.ts +6 -1
  109. package/src/commands/plans-command.ts +13 -29
  110. package/src/commands/rerun-command.ts +7 -2
  111. package/src/commands/research-command.ts +8 -1
  112. package/src/commands/test-command.ts +6 -2
  113. package/src/components/Autocomplete.tsx +39 -10
  114. package/src/components/InputReadline.tsx +10 -1
  115. package/src/config.ts +1 -0
  116. package/src/experience-tracker.ts +4 -3
  117. package/src/explorbot.ts +8 -0
  118. package/src/knowledge-tracker.ts +1 -1
  119. package/src/state-manager.ts +16 -0
  120. package/src/test-plan.ts +67 -2
  121. package/src/utils/aria-ref.ts +1 -1
  122. package/src/utils/logger.ts +6 -1
  123. package/src/utils/markdown-query.ts +1 -519
  124. package/src/utils/mdq/cli.ts +118 -0
  125. package/src/utils/mdq/edit.ts +158 -0
  126. package/src/utils/mdq/query.ts +556 -0
  127. package/src/utils/strings.ts +7 -0
  128. 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 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`);
@@ -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,
@@ -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
  */
@@ -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 {};
@@ -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) {
@@ -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 && this.recentSteps.shouldSuppress(entry.content))
79
- return;
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
- import { type Token } from 'marked';
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';