explorbot 0.1.0 → 0.1.2

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 (77) hide show
  1. package/bin/explorbot-cli.ts +93 -36
  2. package/dist/bin/explorbot-cli.js +71 -16
  3. package/dist/rules/rerunner/healing-approach.md +19 -0
  4. package/dist/src/action.js +8 -10
  5. package/dist/src/ai/historian.js +34 -3
  6. package/dist/src/ai/navigator.js +35 -28
  7. package/dist/src/ai/pilot.js +33 -9
  8. package/dist/src/ai/planner/session-dedup.js +3 -0
  9. package/dist/src/ai/planner/styles.js +3 -0
  10. package/dist/src/ai/planner.js +29 -10
  11. package/dist/src/ai/rerunner.js +472 -0
  12. package/dist/src/ai/researcher/cache.js +4 -3
  13. package/dist/src/ai/researcher/fingerprint-worker.js +7 -6
  14. package/dist/src/ai/researcher.js +3 -4
  15. package/dist/src/ai/rules.js +2 -2
  16. package/dist/src/ai/tools.js +2 -2
  17. package/dist/src/commands/add-rule-command.js +1 -2
  18. package/dist/src/commands/base-command.js +12 -0
  19. package/dist/src/commands/context-command.js +12 -5
  20. package/dist/src/commands/drill-command.js +0 -1
  21. package/dist/src/commands/explore-command.js +20 -5
  22. package/dist/src/commands/freesail-command.js +8 -22
  23. package/dist/src/commands/index.js +4 -0
  24. package/dist/src/commands/init-command.js +3 -3
  25. package/dist/src/commands/path-command.js +2 -1
  26. package/dist/src/commands/plan-command.js +37 -15
  27. package/dist/src/commands/rerun-command.js +42 -0
  28. package/dist/src/commands/research-command.js +10 -4
  29. package/dist/src/commands/runs-command.js +22 -0
  30. package/dist/src/commands/start-command.js +0 -1
  31. package/dist/src/commands/test-command.js +3 -3
  32. package/dist/src/components/App.js +8 -0
  33. package/dist/src/config.js +3 -0
  34. package/dist/src/explorbot.js +19 -0
  35. package/dist/src/explorer.js +2 -1
  36. package/dist/src/suite.js +115 -0
  37. package/dist/src/utils/html.js +2 -5
  38. package/dist/src/utils/rules-loader.js +33 -17
  39. package/dist/src/utils/test-files.js +103 -0
  40. package/package.json +3 -1
  41. package/rules/rerunner/healing-approach.md +19 -0
  42. package/src/action.ts +7 -9
  43. package/src/ai/historian.ts +37 -3
  44. package/src/ai/navigator.ts +35 -28
  45. package/src/ai/pilot.ts +33 -9
  46. package/src/ai/planner/session-dedup.ts +4 -0
  47. package/src/ai/planner/styles.ts +4 -0
  48. package/src/ai/planner.ts +28 -9
  49. package/src/ai/rerunner.ts +532 -0
  50. package/src/ai/researcher/cache.ts +4 -3
  51. package/src/ai/researcher/fingerprint-worker.ts +7 -13
  52. package/src/ai/researcher.ts +3 -4
  53. package/src/ai/rules.ts +2 -2
  54. package/src/ai/tools.ts +2 -2
  55. package/src/commands/add-rule-command.ts +1 -2
  56. package/src/commands/base-command.ts +13 -0
  57. package/src/commands/context-command.ts +12 -5
  58. package/src/commands/drill-command.ts +0 -1
  59. package/src/commands/explore-command.ts +21 -5
  60. package/src/commands/freesail-command.ts +6 -23
  61. package/src/commands/index.ts +4 -0
  62. package/src/commands/init-command.ts +3 -3
  63. package/src/commands/path-command.ts +2 -1
  64. package/src/commands/plan-command.ts +45 -16
  65. package/src/commands/rerun-command.ts +46 -0
  66. package/src/commands/research-command.ts +10 -4
  67. package/src/commands/runs-command.ts +27 -0
  68. package/src/commands/start-command.ts +0 -1
  69. package/src/commands/test-command.ts +3 -3
  70. package/src/components/App.tsx +8 -0
  71. package/src/config.ts +23 -0
  72. package/src/explorbot.ts +21 -0
  73. package/src/explorer.ts +3 -2
  74. package/src/suite.ts +135 -0
  75. package/src/utils/html.ts +1 -5
  76. package/src/utils/rules-loader.ts +35 -17
  77. package/src/utils/test-files.ts +122 -0
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { parentPort } from 'node:worker_threads';
3
4
  import { computeHtmlFingerprint } from "../../utils/html-diff.js";
4
5
  function diceSimilarity(a, b) {
5
6
  let intersection = 0;
@@ -12,15 +13,15 @@ function diceSimilarity(a, b) {
12
13
  return 100;
13
14
  return Math.round(((2 * intersection) / total) * 100);
14
15
  }
15
- self.onmessage = (event) => {
16
- const { html, statesDir, maxAgeMs, threshold } = event.data;
16
+ parentPort.on('message', (data) => {
17
+ const { html, statesDir, maxAgeMs, threshold } = data;
17
18
  if (!existsSync(statesDir)) {
18
- self.postMessage({ matchHash: null, similarity: 0 });
19
+ parentPort.postMessage({ matchHash: null, similarity: 0 });
19
20
  return;
20
21
  }
21
22
  const currentFingerprint = new Set(computeHtmlFingerprint(html));
22
23
  if (currentFingerprint.size === 0) {
23
- self.postMessage({ matchHash: null, similarity: 0 });
24
+ parentPort.postMessage({ matchHash: null, similarity: 0 });
24
25
  return;
25
26
  }
26
27
  const now = Date.now();
@@ -41,5 +42,5 @@ self.onmessage = (event) => {
41
42
  }
42
43
  }
43
44
  const matched = bestSimilarity >= threshold;
44
- self.postMessage({ matchHash: matched ? bestHash : null, similarity: bestSimilarity });
45
- };
45
+ parentPort.postMessage({ matchHash: matched ? bestHash : null, similarity: bestSimilarity });
46
+ });
@@ -98,9 +98,9 @@ export class Researcher extends ResearcherBase {
98
98
  setActivity(`${this.emoji} Researching...`, 'action');
99
99
  await this.ensureNavigated(state.url, screenshot && this.provider.hasVision());
100
100
  await this.hooksRunner.runBeforeHook('researcher', state.url);
101
- const { ariaSnapshot, elements: annotatedElements } = await this.explorer.annotateElements();
101
+ const annotatedElements = await this.explorer.annotateElements();
102
102
  debugLog(`Annotated ${annotatedElements.length} interactive elements with eidx`);
103
- this.actionResult = await this.explorer.createAction().capturePageState({ includeScreenshot: screenshot && this.provider.hasVision(), ariaSnapshot });
103
+ this.actionResult = await this.explorer.createAction().capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
104
104
  if (isErrorPage(this.actionResult)) {
105
105
  const recovered = await this.waitForPageLoad(screenshot);
106
106
  if (!recovered) {
@@ -325,10 +325,9 @@ export class Researcher extends ResearcherBase {
325
325
  return false;
326
326
  try {
327
327
  await withRetry(async () => {
328
- const { ariaSnapshot } = await this.explorer.annotateElements();
328
+ await this.explorer.annotateElements();
329
329
  this.actionResult = await this.explorer.createAction().capturePageState({
330
330
  includeScreenshot: screenshot && this.provider.hasVision(),
331
- ariaSnapshot,
332
331
  });
333
332
  if (isErrorPage(this.actionResult))
334
333
  throw new Error('Error page detected');
@@ -257,7 +257,7 @@ export const actionRule = dedent `
257
257
  I.fillField('Username', 'John', '.login-form'); // fills Username inside .login-form
258
258
  I.fillField('Username', 'John'); // fills the field located by name or placeholder or label "Username" with the text "John"
259
259
  I.fillField('//user/input', 'John'); // fills the field located by XPath "//user/input" with the text "John"
260
- </example>
260
+ </example>
261
261
 
262
262
  ### I.type
263
263
 
@@ -294,7 +294,7 @@ export const actionRule = dedent `
294
294
  </example>
295
295
 
296
296
  IMPORTANT: Requires an active/focused element for most keys.
297
- Commonly used after I.type() to submit forms or navigate dropdowns.
297
+ Commonly used after I.type() or I.fillField() to submit forms or navigate dropdowns.
298
298
 
299
299
  ### I.switchTo
300
300
 
@@ -274,7 +274,7 @@ export function createCodeceptJSTools(explorer, task) {
274
274
  I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
275
275
 
276
276
  Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
277
- Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
277
+ Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
278
278
  `,
279
279
  inputSchema: z.object({
280
280
  codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
@@ -335,7 +335,7 @@ export function createCodeceptJSTools(explorer, task) {
335
335
  message: `Form completed successfully with ${lines.length} commands.`,
336
336
  commandsExecuted: lines.length,
337
337
  code: codeBlock,
338
- suggestion: 'Verify the form was filled in correctly using see() tool. Submit if needed by using click() tool.',
338
+ suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
339
339
  });
340
340
  }
341
341
  catch (error) {
@@ -5,8 +5,7 @@ import React from 'react';
5
5
  import { tag } from '../utils/logger.js';
6
6
  import { BaseCommand } from './base-command.js';
7
7
  export class AddRuleCommand extends BaseCommand {
8
- name = 'rules:add';
9
- aliases = ['add-rule'];
8
+ name = 'add-rule';
10
9
  description = 'Create a rule file for an agent';
11
10
  suggestions = ['/add-rule researcher check-tooltips'];
12
11
  async execute(args) {
@@ -1,3 +1,4 @@
1
+ import { Command } from 'commander';
1
2
  export class BaseCommand {
2
3
  aliases = [];
3
4
  options = [];
@@ -10,4 +11,15 @@ export class BaseCommand {
10
11
  matches(commandName) {
11
12
  return this.name === commandName || this.aliases.includes(commandName);
12
13
  }
14
+ parseArgs(args) {
15
+ const cmd = new Command();
16
+ cmd.exitOverride();
17
+ for (const opt of this.options) {
18
+ cmd.option(opt.flags, opt.description);
19
+ }
20
+ cmd.argument('[args...]');
21
+ const argv = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).map((s) => s.replace(/^["']|["']$/g, ''));
22
+ cmd.parse(argv, { from: 'user' });
23
+ return { opts: cmd.opts(), args: cmd.args };
24
+ }
13
25
  }
@@ -8,27 +8,34 @@ export class ContextCommand extends BaseCommand {
8
8
  name = 'context';
9
9
  description = 'Show page context summary (URL, headings, experience, knowledge, ARIA, HTML, research)';
10
10
  suggestions = ['context:aria', 'context:html', 'context:knowledge', 'context:experience', 'context:data'];
11
+ options = [
12
+ { flags: '--visual', description: 'Include annotated screenshot' },
13
+ { flags: '--screenshot', description: 'Include annotated screenshot' },
14
+ { flags: '--full', description: 'Show full context with HTML' },
15
+ { flags: '--attached', description: 'Show attached context mode' },
16
+ ];
11
17
  async execute(args) {
12
18
  const explorer = this.explorBot.getExplorer();
13
19
  const state = explorer.getStateManager().getCurrentState();
14
20
  if (!state) {
15
21
  throw new Error('No active page to show context for');
16
22
  }
17
- const isVisual = args.includes('--visual') || args.includes('--screenshot');
18
- const { ariaSnapshot } = await explorer.annotateElements();
23
+ const { opts } = this.parseArgs(args);
24
+ const isVisual = !!(opts.visual || opts.screenshot);
25
+ await explorer.annotateElements();
19
26
  if (isVisual) {
20
27
  const cachedResearch = Researcher.getCachedResearch(state);
21
28
  const containers = cachedResearch ? extractValidContainers(cachedResearch) : [];
22
29
  await explorer.visuallyAnnotateElements({ containers });
23
30
  }
24
- const actionResult = await explorer.createAction().capturePageState({ includeScreenshot: isVisual, ariaSnapshot });
31
+ const actionResult = await explorer.createAction().capturePageState({ includeScreenshot: isVisual });
25
32
  const experienceTracker = explorer.getStateManager().getExperienceTracker();
26
33
  const knowledgeTracker = this.explorBot.getKnowledgeTracker();
27
34
  let mode = 'compact';
28
- if (args.includes('--full')) {
35
+ if (opts.full) {
29
36
  mode = 'full';
30
37
  }
31
- else if (args.includes('--attached')) {
38
+ else if (opts.attached) {
32
39
  mode = 'attached';
33
40
  }
34
41
  const contextData = {
@@ -2,7 +2,6 @@ import { BaseCommand } from './base-command.js';
2
2
  export class DrillCommand extends BaseCommand {
3
3
  name = 'drill';
4
4
  description = 'Drill all components on current page to learn interactions';
5
- aliases = ['bosun'];
6
5
  suggestions = ['/research - to see UI map first', '/navigate <page> - to go to another page'];
7
6
  async execute(args) {
8
7
  const knowledgePath = this.parseKnowledgeArg(args);
@@ -1,6 +1,8 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
1
2
  import figureSet from 'figures';
2
3
  import path from 'node:path';
3
4
  import { getStyles } from '../ai/planner/styles.js';
5
+ import { ConfigParser } from "../config.js";
4
6
  import { getCliName } from "../utils/cli-name.js";
5
7
  import { jsonToTable } from '../utils/markdown-parser.js';
6
8
  import { tag } from '../utils/logger.js';
@@ -14,12 +16,11 @@ export class ExploreCommand extends BaseCommand {
14
16
  testsRun = 0;
15
17
  completedPlans = [];
16
18
  async execute(args) {
17
- const maxTestsMatch = args.match(/--max-tests\s+(\d+)/);
18
- if (maxTestsMatch) {
19
- this.maxTests = Number.parseInt(maxTestsMatch[1], 10);
20
- args = args.replace(/--max-tests\s+\d+/, '').trim();
19
+ const { opts, args: remaining } = this.parseArgs(args);
20
+ if (opts.maxTests) {
21
+ this.maxTests = Number.parseInt(opts.maxTests, 10);
21
22
  }
22
- const feature = args.trim() || undefined;
23
+ const feature = remaining.join(' ') || undefined;
23
24
  const mainUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
24
25
  await this.runAllStyles(mainUrl, feature);
25
26
  const mainPlan = this.explorBot.getCurrentPlan();
@@ -56,6 +57,7 @@ export class ExploreCommand extends BaseCommand {
56
57
  await this.explorBot.visit(mainUrl);
57
58
  const savedPath = this.explorBot.savePlans(this.completedPlans);
58
59
  this.printResults(savedPath);
60
+ this.printRerunSuggestions();
59
61
  }
60
62
  async runAllStyles(pageUrl, feature, parentPlan, completedPlans) {
61
63
  let fresh = true;
@@ -107,6 +109,19 @@ export class ExploreCommand extends BaseCommand {
107
109
  tag('info').log(`Re-run tests: ${getCliName()} test ${relativePath} <index>`);
108
110
  }
109
111
  }
112
+ printRerunSuggestions() {
113
+ const testsDir = ConfigParser.getInstance().getTestsDir();
114
+ if (!existsSync(testsDir))
115
+ return;
116
+ const testFiles = readdirSync(testsDir).filter((f) => f.endsWith('.js'));
117
+ if (testFiles.length === 0)
118
+ return;
119
+ for (const file of testFiles) {
120
+ tag('info').log(`Generated: ${file}`);
121
+ }
122
+ tag('info').log(`List tests: ${getCliName()} runs`);
123
+ tag('info').log(`Re-run with healing: ${getCliName()} rerun <filename> [index]`);
124
+ }
110
125
  isLimitReached() {
111
126
  return this.maxTests != null && this.testsRun >= this.maxTests;
112
127
  }
@@ -16,7 +16,14 @@ export class FreesailCommand extends BaseCommand {
16
16
  { flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
17
17
  ];
18
18
  async execute(args) {
19
- const { strategy, scope, maxTests } = parseArgs(args);
19
+ const { opts } = this.parseArgs(args);
20
+ let strategy;
21
+ if (opts.deep)
22
+ strategy = 'deep';
23
+ if (opts.shallow)
24
+ strategy = 'shallow';
25
+ const scope = opts.scope;
26
+ const maxTests = opts.maxTests ? Number.parseInt(opts.maxTests, 10) : undefined;
20
27
  await this.explorBot.visitInitialState();
21
28
  let testsRun = 0;
22
29
  await loop(async (ctx) => {
@@ -60,24 +67,3 @@ export class FreesailCommand extends BaseCommand {
60
67
  }, { maxAttempts: Number.POSITIVE_INFINITY });
61
68
  }
62
69
  }
63
- function parseArgs(args) {
64
- const parts = args.trim().split(/\s+/);
65
- let strategy;
66
- let scope;
67
- let maxTests;
68
- for (let i = 0; i < parts.length; i++) {
69
- if (parts[i] === '--deep')
70
- strategy = 'deep';
71
- if (parts[i] === '--shallow')
72
- strategy = 'shallow';
73
- if (parts[i] === '--scope' && parts[i + 1]) {
74
- scope = parts[i + 1];
75
- i++;
76
- }
77
- if (parts[i] === '--max-tests' && parts[i + 1]) {
78
- maxTests = Number.parseInt(parts[i + 1], 10);
79
- i++;
80
- }
81
- }
82
- return { strategy, scope, maxTests };
83
- }
@@ -22,7 +22,9 @@ import { PlanEditCommand } from './plan-edit-command.js';
22
22
  import { PlanLoadCommand } from './plan-load-command.js';
23
23
  import { PlanReloadCommand } from './plan-reload-command.js';
24
24
  import { PlanSaveCommand } from './plan-save-command.js';
25
+ import { RerunCommand } from './rerun-command.js';
25
26
  import { ResearchCommand } from './research-command.js';
27
+ import { RunsCommand } from './runs-command.js';
26
28
  import { StartCommand } from './start-command.js';
27
29
  import { StatusCommand } from "./status-command.js";
28
30
  import { TestCommand } from './test-command.js';
@@ -53,6 +55,8 @@ const commandClasses = [
53
55
  ContextExperienceCommand,
54
56
  ContextDataCommand,
55
57
  TestCommand,
58
+ RunsCommand,
59
+ RerunCommand,
56
60
  StatusCommand,
57
61
  DebugCommand,
58
62
  ExitCommand,
@@ -22,11 +22,11 @@ const config = {
22
22
 
23
23
  ai: {
24
24
  // fast model with tool calling capabilities
25
- model: openrouter('<your base model here>'),
25
+ model: openrouter('openai/gpt-oss-20b:nitro'),
26
26
  // vision model for screenshot analysis
27
- visionModel: openrouter('<your vision model here>'),
27
+ visionModel: openrouter('meta-llama/llama-4-scout-17b-16e-instruct'),
28
28
  // agentic model for decision making
29
- agenticModel: openrouter('<your agentic model here>'),
29
+ agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
30
30
  },
31
31
  };
32
32
 
@@ -5,7 +5,8 @@ export class PathCommand extends BaseCommand {
5
5
  description = 'Display ASCII graph of navigation paths during the session';
6
6
  options = [{ flags: '--links', description: 'Show outgoing links from each page' }];
7
7
  async execute(args) {
8
- const showLinks = args.includes('--links');
8
+ const { opts } = this.parseArgs(args);
9
+ const showLinks = !!opts.links;
9
10
  const stateManager = this.explorBot.getExplorer().getStateManager();
10
11
  const history = stateManager.getStateHistory();
11
12
  if (history.length === 0) {
@@ -1,3 +1,6 @@
1
+ import path from 'node:path';
2
+ import chalk from 'chalk';
3
+ import figureSet from 'figures';
1
4
  import { tag } from '../utils/logger.js';
2
5
  import { BaseCommand } from './base-command.js';
3
6
  export class PlanCommand extends BaseCommand {
@@ -11,30 +14,49 @@ export class PlanCommand extends BaseCommand {
11
14
  { flags: '--focus <feature>', description: 'Focus area for test planning' },
12
15
  ];
13
16
  async execute(args) {
14
- const clear = args.includes('--clear');
15
- const fresh = args.includes('--fresh') || clear;
16
- const styleMatch = args.match(/--style\s+(\S+)/);
17
- const style = styleMatch?.[1];
18
- const focusMatch = args.match(/--focus\s+("[^"]+"|'[^']+'|\S+)/);
19
- const focusFromFlag = focusMatch?.[1]?.replace(/^["']|["']$/g, '');
20
- const focusFromText = args
21
- .replace('--clear', '')
22
- .replace('--fresh', '')
23
- .replace(/--style\s+\S+/, '')
24
- .replace(/--focus\s+("[^"]+"|'[^']+'|\S+)/, '')
25
- .trim();
26
- const focus = focusFromFlag || focusFromText;
27
- if (clear) {
17
+ const { opts, args: remaining } = this.parseArgs(args);
18
+ const focus = opts.focus || remaining.join(' ') || undefined;
19
+ if (opts.clear) {
28
20
  this.explorBot.clearPlan();
29
21
  tag('success').log('Plan cleared');
30
22
  }
31
23
  if (focus) {
32
24
  tag('info').log(`Planning focus: ${focus}`);
33
25
  }
34
- await this.explorBot.plan(focus || undefined, { fresh, style });
26
+ await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style });
35
27
  const plan = this.explorBot.getCurrentPlan();
36
28
  if (!plan?.tests.length) {
37
29
  throw new Error('No test scenarios in the current plan.');
38
30
  }
31
+ this.printPlanSummary();
32
+ this.updateSuggestions();
33
+ }
34
+ printPlanSummary() {
35
+ const suite = this.explorBot.getSuite();
36
+ const plan = this.explorBot.getCurrentPlan();
37
+ if (suite && suite.automatedTestCount > 0) {
38
+ const names = suite.getAutomatedTestNames();
39
+ console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
40
+ for (let i = 0; i < names.length; i++) {
41
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
42
+ }
43
+ }
44
+ if (plan?.tests.length) {
45
+ console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
46
+ for (let i = 0; i < plan.tests.length; i++) {
47
+ const t = plan.tests[i];
48
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
49
+ }
50
+ }
51
+ }
52
+ updateSuggestions() {
53
+ this.suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
54
+ const suite = this.explorBot.getSuite();
55
+ if (suite && suite.automatedTestCount > 0) {
56
+ for (const f of suite.getAutomatedTestFiles()) {
57
+ this.suggestions.push(`/rerun ${path.relative(process.cwd(), f)} - re-run automated tests`);
58
+ }
59
+ }
60
+ this.suggestions.push('Edit the plan in file and call /plan:reload to update it');
39
61
  }
40
62
  }
@@ -0,0 +1,42 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { ConfigParser } from "../config.js";
4
+ import { tag } from "../utils/logger.js";
5
+ import { BaseCommand } from './base-command.js';
6
+ export class RerunCommand extends BaseCommand {
7
+ name = 'rerun';
8
+ description = 'Re-run generated tests with AI auto-healing';
9
+ tuiEnabled = true;
10
+ async execute(args) {
11
+ const { args: remaining } = this.parseArgs(args);
12
+ const filename = remaining[0];
13
+ const indexArg = remaining[1];
14
+ if (!filename) {
15
+ tag('error').log('Usage: /rerun <filename> [index]');
16
+ return;
17
+ }
18
+ let filePath = resolve(filename);
19
+ if (!existsSync(filePath)) {
20
+ filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
21
+ }
22
+ const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
23
+ await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
24
+ }
25
+ }
26
+ function parseTestIndices(input) {
27
+ if (input === '*' || input === 'all')
28
+ return [];
29
+ const indices = new Set();
30
+ for (const part of input.split(',')) {
31
+ const trimmed = part.trim();
32
+ const range = trimmed.match(/^(\d+)-(\d+)$/);
33
+ if (range) {
34
+ for (let i = Number.parseInt(range[1]); i <= Number.parseInt(range[2]); i++)
35
+ indices.add(i - 1);
36
+ }
37
+ else {
38
+ indices.add(Number.parseInt(trimmed) - 1);
39
+ }
40
+ }
41
+ return [...indices].sort((a, b) => a - b);
42
+ }
@@ -6,11 +6,17 @@ export class ResearchCommand extends BaseCommand {
6
6
  name = 'research';
7
7
  description = 'Research current page or navigate to URI and research. Use --deep to explore interactive elements by clicking them. Use --data to include page data.';
8
8
  suggestions = ['/navigate <page> - to go to another page', '/plan <feature> - to plan testing'];
9
+ options = [
10
+ { flags: '--data', description: 'Include page data' },
11
+ { flags: '--deep', description: 'Explore interactive elements by clicking them' },
12
+ { flags: '--no-fix', description: 'Skip fixing research issues' },
13
+ ];
9
14
  async execute(args) {
10
- const includeData = args.includes('--data');
11
- const enableDeep = args.includes('--deep');
12
- const noFix = args.includes('--no-fix');
13
- const target = args.replace('--data', '').replace('--deep', '').replace('--no-fix', '').trim();
15
+ const { opts, args: remaining } = this.parseArgs(args);
16
+ const includeData = !!opts.data;
17
+ const enableDeep = !!opts.deep;
18
+ const noFix = !!opts.noFix;
19
+ const target = remaining.join(' ');
14
20
  if (target) {
15
21
  await this.explorBot.agentNavigator().visit(target);
16
22
  }
@@ -0,0 +1,22 @@
1
+ import * as codeceptjs from 'codeceptjs';
2
+ import { ConfigParser } from "../config.js";
3
+ import { dryRunTestFile, loadTestSuites, printTestList } from "../utils/test-files.js";
4
+ import { BaseCommand } from './base-command.js';
5
+ export class RunsCommand extends BaseCommand {
6
+ name = 'runs';
7
+ description = 'List generated test files and their scenarios';
8
+ tuiEnabled = true;
9
+ async execute(args) {
10
+ if (!this.explorBot.isExploring) {
11
+ codeceptjs.container.create({ helpers: {} }, {});
12
+ }
13
+ const { args: remaining } = this.parseArgs(args);
14
+ const filePath = remaining[0];
15
+ if (filePath) {
16
+ await dryRunTestFile(filePath);
17
+ return;
18
+ }
19
+ const suites = loadTestSuites(ConfigParser.getInstance().getTestsDir());
20
+ printTestList(suites);
21
+ }
22
+ }
@@ -2,7 +2,6 @@ import { BaseCommand } from './base-command.js';
2
2
  import { ExploreCommand } from './explore-command.js';
3
3
  export class StartCommand extends BaseCommand {
4
4
  name = 'start';
5
- aliases = ['sail'];
6
5
  description = 'Start web exploration';
7
6
  suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
8
7
  async execute(args) {
@@ -24,10 +24,10 @@ export class TestCommand extends BaseCommand {
24
24
  toExecute.push(...requirePlan().getPendingTests());
25
25
  }
26
26
  else if (args.match(/^[\d,\-\s]+$/)) {
27
- const pending = requirePlan().getPendingTests();
28
- const indices = parseTestIndices(args, pending.length);
27
+ const visible = requirePlan().tests.filter((t) => t.enabled);
28
+ const indices = parseTestIndices(args, visible.length);
29
29
  for (const idx of indices) {
30
- toExecute.push(pending[idx]);
30
+ toExecute.push(visible[idx]);
31
31
  }
32
32
  }
33
33
  else {
@@ -206,6 +206,14 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
206
206
  setShowPlanEditor(true);
207
207
  return;
208
208
  }
209
+ if (key.upArrow) {
210
+ setTaskScrollOffset((prev) => Math.max(0, prev - 1));
211
+ return;
212
+ }
213
+ if (key.downArrow) {
214
+ setTaskScrollOffset((prev) => prev + 1);
215
+ return;
216
+ }
209
217
  }
210
218
  if (!showInput && !showPlanEditor) {
211
219
  if (key.upArrow) {
@@ -100,6 +100,9 @@ export class ConfigParser {
100
100
  getPlansDir() {
101
101
  return outputPath('plans');
102
102
  }
103
+ getTestsDir() {
104
+ return outputPath('tests');
105
+ }
103
106
  // For testing purposes only
104
107
  static resetForTesting() {
105
108
  if (ConfigParser.instance) {
@@ -14,6 +14,7 @@ import { Planner } from "./ai/planner.js";
14
14
  import { AIProvider } from "./ai/provider.js";
15
15
  import { Quartermaster } from "./ai/quartermaster.js";
16
16
  import { Researcher } from "./ai/researcher.js";
17
+ import { Rerunner } from "./ai/rerunner.js";
17
18
  import { Tester } from "./ai/tester.js";
18
19
  import { createAgentTools } from "./ai/tools.js";
19
20
  import { ConfigParser } from "./config.js";
@@ -202,6 +203,21 @@ export class ExplorBot {
202
203
  return new Historian(ai, experienceTracker, reporter, explorer.getStateManager());
203
204
  }));
204
205
  }
206
+ agentRerunner() {
207
+ if (!this.agents.rerunner) {
208
+ this.agents.rerunner = this.createAgent(({ ai, explorer }) => {
209
+ const researcher = this.agentResearcher();
210
+ const navigator = this.agentNavigator();
211
+ const tools = createAgentTools({ explorer, researcher, navigator });
212
+ return new Rerunner(explorer, ai, tools);
213
+ });
214
+ const qm = this.agentQuartermaster();
215
+ if (qm)
216
+ this.agents.rerunner.setQuartermaster(qm);
217
+ this.agents.rerunner.setHistorian(this.agentHistorian());
218
+ }
219
+ return this.agents.rerunner;
220
+ }
205
221
  agentBosun() {
206
222
  return (this.agents.bosun ||= this.createAgent(({ ai, explorer }) => {
207
223
  const researcher = this.agentResearcher();
@@ -243,6 +259,9 @@ export class ExplorBot {
243
259
  getCurrentPlan() {
244
260
  return this.currentPlan;
245
261
  }
262
+ getSuite() {
263
+ return this.agentPlanner().getSuite();
264
+ }
246
265
  getPlanFeature() {
247
266
  return this.planFeature;
248
267
  }
@@ -244,7 +244,8 @@ class Explorer {
244
244
  return action;
245
245
  }
246
246
  async annotateElements() {
247
- return annotatePageElements(this.playwrightHelper.page);
247
+ const { elements } = await annotatePageElements(this.playwrightHelper.page);
248
+ return elements;
248
249
  }
249
250
  async visuallyAnnotateElements(opts) {
250
251
  return visuallyAnnotateContainers(this.playwrightHelper.page, opts?.containers || []);