explorbot 0.1.27 → 0.1.29

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 (68) hide show
  1. package/README.md +83 -245
  2. package/bin/explorbot-cli.ts +1 -0
  3. package/dist/bin/explorbot-cli.js +1 -0
  4. package/dist/package.json +8 -6
  5. package/dist/rules/navigator/verification-actions.md +2 -0
  6. package/dist/src/action-result.js +2 -0
  7. package/dist/src/action.js +88 -7
  8. package/dist/src/ai/captain/file-tools.js +100 -0
  9. package/dist/src/ai/captain/idle-mode.js +70 -6
  10. package/dist/src/ai/captain/web-mode.js +36 -6
  11. package/dist/src/ai/captain.js +87 -19
  12. package/dist/src/ai/fisherman.js +14 -3
  13. package/dist/src/ai/historian/screencast.js +11 -2
  14. package/dist/src/ai/navigator.js +5 -2
  15. package/dist/src/ai/pilot.js +52 -9
  16. package/dist/src/ai/planner.js +16 -5
  17. package/dist/src/ai/provider.js +53 -18
  18. package/dist/src/ai/researcher/coordinates.js +2 -3
  19. package/dist/src/ai/researcher/deep-analysis.js +3 -4
  20. package/dist/src/ai/researcher/locators.js +1 -2
  21. package/dist/src/ai/researcher.js +24 -19
  22. package/dist/src/ai/rules.js +44 -0
  23. package/dist/src/ai/task-agent.js +1 -0
  24. package/dist/src/ai/tester.js +161 -46
  25. package/dist/src/ai/tools.js +84 -8
  26. package/dist/src/commands/explore-command.js +6 -1
  27. package/dist/src/components/LogPane.js +4 -3
  28. package/dist/src/explorbot.js +7 -2
  29. package/dist/src/explorer.js +270 -35
  30. package/dist/src/stats.js +16 -0
  31. package/dist/src/utils/aria.js +66 -6
  32. package/dist/src/utils/browser-errors.js +23 -0
  33. package/dist/src/utils/error-page.js +17 -2
  34. package/dist/src/utils/logger.js +2 -2
  35. package/package.json +8 -6
  36. package/rules/navigator/verification-actions.md +2 -0
  37. package/src/action-result.ts +2 -0
  38. package/src/action.ts +83 -7
  39. package/src/ai/captain/file-tools.ts +126 -0
  40. package/src/ai/captain/idle-mode.ts +72 -6
  41. package/src/ai/captain/mixin.ts +1 -1
  42. package/src/ai/captain/web-mode.ts +40 -5
  43. package/src/ai/captain.ts +94 -20
  44. package/src/ai/fisherman.ts +14 -3
  45. package/src/ai/historian/screencast.ts +11 -2
  46. package/src/ai/navigator.ts +6 -2
  47. package/src/ai/pilot.ts +53 -9
  48. package/src/ai/planner.ts +16 -5
  49. package/src/ai/provider.ts +51 -19
  50. package/src/ai/researcher/coordinates.ts +2 -3
  51. package/src/ai/researcher/deep-analysis.ts +3 -4
  52. package/src/ai/researcher/locators.ts +1 -2
  53. package/src/ai/researcher.ts +25 -19
  54. package/src/ai/rules.ts +46 -0
  55. package/src/ai/task-agent.ts +1 -1
  56. package/src/ai/tester.ts +175 -48
  57. package/src/ai/tools.ts +97 -8
  58. package/src/commands/explore-command.ts +6 -1
  59. package/src/components/LogPane.tsx +4 -3
  60. package/src/config.ts +1 -0
  61. package/src/explorbot.ts +6 -2
  62. package/src/explorer.ts +295 -38
  63. package/src/state-manager.ts +2 -0
  64. package/src/stats.ts +18 -0
  65. package/src/utils/aria.ts +63 -6
  66. package/src/utils/browser-errors.ts +25 -0
  67. package/src/utils/error-page.ts +16 -3
  68. package/src/utils/logger.ts +3 -3
@@ -4,13 +4,14 @@ import { z } from 'zod';
4
4
  import { ActionResult } from "../action-result.js";
5
5
  import { TestResult } from '../test-plan.js';
6
6
  import { extractFocusedElement } from "../utils/aria.js";
7
+ import { isFatalBrowserError } from "../utils/browser-errors.js";
7
8
  import { createDebug, tag } from '../utils/logger.js';
8
9
  import { pause } from '../utils/loop.js';
9
10
  import { WebElement } from "../utils/web-element.js";
10
11
  import { sectionContextRule } from "./rules.js";
11
12
  import { isInteractive } from "./task-agent.js";
12
13
  const debugLog = createDebug('explorbot:tools');
13
- export const CODECEPT_TOOLS = ['click', 'pressKey', 'form'];
14
+ export const CODECEPT_TOOLS = ['click', 'hover', 'pressKey', 'form'];
14
15
  export const ASSERTION_TOOLS = ['verify'];
15
16
  export function createCodeceptJSTools(explorer, task) {
16
17
  const stateManager = explorer.getStateManager();
@@ -132,6 +133,71 @@ export function createCodeceptJSTools(explorer, task) {
132
133
  }, action.lastError);
133
134
  },
134
135
  }),
136
+ hover: tool({
137
+ description: dedent `
138
+ Move the mouse cursor to an element to reveal hover-only controls.
139
+
140
+ Use this before clicking row actions, icon buttons, menus, or toolbars that appear only
141
+ when the user hovers a list item, table row, card, or tree node.
142
+
143
+ This tool ONLY accepts I.moveCursorTo(locator) commands. It does not click.
144
+ After hovering, use context(), see(), or click() the revealed control.
145
+ `,
146
+ inputSchema: z.object({
147
+ commands: z.array(z.string()).describe(dedent `
148
+ FALLBACK LOCATORS for ONE element to hover.
149
+ Order by reliability:
150
+ 1. I.moveCursorTo(text, container)
151
+ 2. I.moveCursorTo(ARIA, container)
152
+ 3. I.moveCursorTo(CSS, container)
153
+ 4. I.moveCursorTo(CSS) or I.moveCursorTo(XPath)
154
+ `),
155
+ explanation: z.string().describe('Why you are hovering this element'),
156
+ }),
157
+ execute: async ({ commands: rawCommands, explanation }) => {
158
+ const activeNote = task.startNote(explanation);
159
+ if (rawCommands.length === 0) {
160
+ activeNote.commit(TestResult.FAILED);
161
+ return failedToolResult('hover', 'No commands provided');
162
+ }
163
+ const invalidCommands = rawCommands.map((cmd) => cmd.trim()).filter((cmd) => cmd.startsWith('I.') && !cmd.startsWith('I.moveCursorTo'));
164
+ if (invalidCommands.length > 0) {
165
+ activeNote.commit(TestResult.FAILED);
166
+ return failedToolResult('hover', `Invalid commands: ${invalidCommands.join(', ')}. Hover tool only accepts I.moveCursorTo() commands.`, {
167
+ suggestion: 'Use click() to click elements, or form() for typing/selecting.',
168
+ });
169
+ }
170
+ const commands = rawCommands.map((cmd) => {
171
+ const trimmed = cmd.trim();
172
+ if (trimmed.startsWith('I.moveCursorTo'))
173
+ return trimmed;
174
+ return `I.moveCursorTo(${JSON.stringify(trimmed)})`;
175
+ });
176
+ const previousState = ActionResult.fromState(stateManager.getCurrentState());
177
+ const action = explorer.createAction();
178
+ const attempts = [];
179
+ for (const command of commands) {
180
+ const success = await action.attempt(command, explanation, true);
181
+ attempts.push({
182
+ command,
183
+ success,
184
+ ...(action.lastError && { error: action.lastError.toString() }),
185
+ });
186
+ if (!success)
187
+ continue;
188
+ const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, command);
189
+ activeNote.commit(TestResult.PASSED);
190
+ return successToolResult('hover', { ...toolResult, attempts, code: command }, action);
191
+ }
192
+ const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, commands[0]);
193
+ activeNote.commit(TestResult.FAILED);
194
+ return failedToolResult('hover', 'All hover commands failed', {
195
+ ...toolResult,
196
+ attempts,
197
+ suggestion: 'Use xpathCheck() to locate the row/card/tree node, or visualClick() if the hover target is only visually identifiable.',
198
+ }, action.lastError);
199
+ },
200
+ }),
135
201
  pressKey: tool({
136
202
  description: dedent `
137
203
  Press a keyboard key or key combination. Use this for special keys like Enter, Escape, Tab, Arrow keys, or key combinations with modifiers.
@@ -243,6 +309,7 @@ export function createCodeceptJSTools(explorer, task) {
243
309
  });
244
310
  }
245
311
  catch (error) {
312
+ throwIfFatalBrowserError(error);
246
313
  activeNote.commit(TestResult.FAILED);
247
314
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
248
315
  return failedToolResult('pressKey', `PressKey tool failed: ${errorMessage}`);
@@ -343,6 +410,7 @@ export function createCodeceptJSTools(explorer, task) {
343
410
  }, action);
344
411
  }
345
412
  catch (error) {
413
+ throwIfFatalBrowserError(error);
346
414
  activeNote.commit(TestResult.FAILED);
347
415
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
348
416
  return failedToolResult('form', `Form tool failed: ${errorMessage}`);
@@ -376,8 +444,7 @@ export function createSpecialContextTools(explorer, context) {
376
444
  });
377
445
  }
378
446
  await explorer.switchToMainFrame();
379
- const action = explorer.createAction();
380
- const nextState = await action.capturePageState();
447
+ const nextState = await explorer.capturePageState();
381
448
  const toolResult = await nextState.toToolResult(previousState, 'I.switchTo()');
382
449
  return successToolResult('exitIframe', {
383
450
  ...toolResult,
@@ -386,6 +453,7 @@ export function createSpecialContextTools(explorer, context) {
386
453
  });
387
454
  }
388
455
  catch (error) {
456
+ throwIfFatalBrowserError(error);
389
457
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
390
458
  return failedToolResult('exitIframe', `Failed to exit iframe: ${errorMessage}`);
391
459
  }
@@ -401,6 +469,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
401
469
  Check the page contents based on current page state and screenshot.
402
470
  This tool will trigger visual research to check the page contents on request.
403
471
  Use it to verify the actions were performed correctly and the page is in the expected state.
472
+ Input schema has exactly one field: request. Do not pass text, reason, assertion, or other fields.
404
473
 
405
474
  <example>
406
475
  request: "Check current state of the Login form"
@@ -415,8 +484,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
415
484
  return failedToolResult('see', 'Vision tools are disabled for this session. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.');
416
485
  }
417
486
  try {
418
- const action = explorer.createAction();
419
- const actionResult = await action.caputrePageWithScreenshot();
487
+ const actionResult = await explorer.capturePageWithScreenshot();
420
488
  if (!actionResult.screenshot) {
421
489
  return failedToolResult('see', 'Failed to capture screenshot for analysis');
422
490
  }
@@ -431,6 +499,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
431
499
  });
432
500
  }
433
501
  catch (error) {
502
+ throwIfFatalBrowserError(error);
434
503
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
435
504
  visionDisabled = true;
436
505
  tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
@@ -506,8 +575,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
506
575
  suggestion: verifications[assertion] ? 'This verification already passed. Call finish() to complete the test.' : 'This verification already failed. Perform actions to change the page state, then try again.',
507
576
  });
508
577
  }
509
- const action = explorer.createAction();
510
- const actionResult = await action.capturePageState();
578
+ const actionResult = await explorer.capturePageState();
511
579
  const result = await navigator.verifyState(assertion, actionResult);
512
580
  if (result.verified) {
513
581
  return successToolResult('verify', {
@@ -520,6 +588,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
520
588
  });
521
589
  }
522
590
  catch (error) {
591
+ throwIfFatalBrowserError(error);
523
592
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
524
593
  return failedToolResult('verify', `Verify tool failed: ${errorMessage}`, {
525
594
  error: errorMessage,
@@ -572,6 +641,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
572
641
  });
573
642
  }
574
643
  catch (error) {
644
+ throwIfFatalBrowserError(error);
575
645
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
576
646
  return failedToolResult('research', `Research tool failed: ${errorMessage}`, {
577
647
  error: errorMessage,
@@ -611,6 +681,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
611
681
  });
612
682
  }
613
683
  catch (error) {
684
+ throwIfFatalBrowserError(error);
614
685
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
615
686
  return failedToolResult('interact', `Interact tool failed: ${errorMessage}`, {
616
687
  error: errorMessage,
@@ -645,7 +716,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
645
716
  }
646
717
  const previousState = ActionResult.fromState(currentState);
647
718
  const action = explorer.createAction();
648
- const actionResult = await action.caputrePageWithScreenshot();
719
+ const actionResult = await explorer.capturePageWithScreenshot();
649
720
  if (!actionResult.screenshot) {
650
721
  return failedToolResult('visualClick', 'Failed to capture screenshot for visual analysis');
651
722
  }
@@ -678,6 +749,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
678
749
  });
679
750
  }
680
751
  catch (error) {
752
+ throwIfFatalBrowserError(error);
681
753
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
682
754
  visionDisabled = true;
683
755
  tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
@@ -874,6 +946,10 @@ function cap(text, max) {
874
946
  return text;
875
947
  return `${text.slice(0, max)}\n[...truncated; ${text.length - max} chars omitted...]`;
876
948
  }
949
+ function throwIfFatalBrowserError(error) {
950
+ if (isFatalBrowserError(error))
951
+ throw error;
952
+ }
877
953
  function transformContainsCommand(command) {
878
954
  if (!command.includes(':contains('))
879
955
  return command;
@@ -5,7 +5,7 @@ import { normalizeUrl } from '../state-manager.js';
5
5
  import { Stats } from '../stats.js';
6
6
  import { TestResult } from '../test-plan.js';
7
7
  import { getCliName } from "../utils/cli-name.js";
8
- import { ErrorPageError } from "../utils/error-page.js";
8
+ import { ErrorPageError, getStateErrorPageError } from "../utils/error-page.js";
9
9
  import { tag } from '../utils/logger.js';
10
10
  import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
11
11
  import { safeFilename } from "../utils/strings.js";
@@ -53,6 +53,11 @@ export class ExploreCommand extends BaseCommand {
53
53
  Stats.mode ??= 'explore';
54
54
  Stats.focus ??= feature;
55
55
  const mainUrl = this.getCurrentPageUrl();
56
+ const error = getStateErrorPageError(this.explorBot.getExplorer().getStateManager().getCurrentState());
57
+ if (error) {
58
+ tag('warning').log(error.message);
59
+ return;
60
+ }
56
61
  if (cfg.enabled) {
57
62
  await this.runReuseMode(mainUrl, feature, cfg);
58
63
  }
@@ -94,6 +94,7 @@ const LogPane = React.memo(({ verboseMode }) => {
94
94
  case 'step':
95
95
  return { color: 'cyan', dimColor: true };
96
96
  case 'multiline':
97
+ case 'details':
97
98
  return { color: 'gray', dimColor: true };
98
99
  case 'html':
99
100
  return { color: 'gray' };
@@ -113,14 +114,14 @@ const LogPane = React.memo(({ verboseMode }) => {
113
114
  return null;
114
115
  }
115
116
  const styles = getLogStyles(log.type);
116
- if (log.type === 'multiline') {
117
+ if (log.type === 'multiline' || log.type === 'details') {
117
118
  const cleaned = stripAnsi(dedent(log.content));
118
119
  const parsed = parseMarkdownToTerminal(cleaned);
119
120
  const lines = parsed.split('\n');
120
121
  const maxLines = log.maxLines || 16;
121
- const truncated = lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
122
+ const content = log.type === 'details' ? parsed : lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
122
123
  return (React.createElement(Box, { key: index, borderStyle: "classic", borderLeft: false, borderRight: false, marginY: 1, padding: 1, borderColor: "dim", overflow: "hidden" },
123
- React.createElement(Text, { color: "gray", dimColor: true }, truncated)));
124
+ React.createElement(Text, { color: "gray", dimColor: true }, content)));
124
125
  }
125
126
  if (log.type === 'html') {
126
127
  // Convert HTML to markdown, then render as multiline
@@ -23,11 +23,12 @@ import { ConfigParser } from "./config.js";
23
23
  import { ExperienceTracker } from "./experience-tracker.js";
24
24
  import Explorer from "./explorer.js";
25
25
  import { KnowledgeTracker } from "./knowledge-tracker.js";
26
+ import { Stats } from "./stats.js";
26
27
  import { Plan } from "./test-plan.js";
27
- import { parsePlansFromMarkdown } from "./utils/test-plan-markdown.js";
28
28
  import { setVerboseMode, tag } from "./utils/logger.js";
29
29
  import { relativeToCwd } from "./utils/next-steps.js";
30
30
  import { sanitizeFilename } from "./utils/strings.js";
31
+ import { parsePlansFromMarkdown } from "./utils/test-plan-markdown.js";
31
32
  export class ExplorBot {
32
33
  configParser;
33
34
  explorer;
@@ -427,7 +428,11 @@ export class ExplorBot {
427
428
  tag('info').log(`Session report saved: ${relativeToCwd(filePath)}`);
428
429
  const reporter = this.explorer?.getReporter();
429
430
  if (reporter?.isEnabled()) {
430
- await reporter.setRunDescription(markdown);
431
+ let description = markdown;
432
+ const modelsTable = Stats.modelsTable(this.provider.getConfiguredModels());
433
+ if (modelsTable)
434
+ description = `${markdown}\n\n${modelsTable}`;
435
+ await reporter.setRunDescription(description);
431
436
  }
432
437
  this.lastReportedTestCount = tests.length;
433
438
  }