explorbot 0.0.5 → 0.1.1

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 (85) hide show
  1. package/bin/explorbot-cli.ts +97 -39
  2. package/dist/bin/explorbot-cli.js +75 -19
  3. package/dist/rules/rerunner/healing-approach.md +19 -0
  4. package/dist/src/action.js +8 -7
  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/subpages.js +42 -6
  9. package/dist/src/ai/planner.js +44 -13
  10. package/dist/src/ai/rerunner.js +472 -0
  11. package/dist/src/ai/researcher/cache.js +13 -8
  12. package/dist/src/ai/researcher/coordinates.js +4 -2
  13. package/dist/src/ai/researcher/deep-analysis.js +16 -19
  14. package/dist/src/ai/researcher/locators.js +1 -1
  15. package/dist/src/ai/researcher/parser.js +4 -3
  16. package/dist/src/ai/researcher/research-result.js +2 -0
  17. package/dist/src/ai/researcher.js +3 -3
  18. package/dist/src/ai/rules.js +2 -2
  19. package/dist/src/ai/tools.js +6 -2
  20. package/dist/src/commands/add-rule-command.js +1 -2
  21. package/dist/src/commands/base-command.js +12 -0
  22. package/dist/src/commands/context-command.js +10 -3
  23. package/dist/src/commands/drill-command.js +0 -1
  24. package/dist/src/commands/explore-command.js +21 -6
  25. package/dist/src/commands/freesail-command.js +8 -22
  26. package/dist/src/commands/index.js +4 -0
  27. package/dist/src/commands/init-command.js +7 -5
  28. package/dist/src/commands/path-command.js +2 -1
  29. package/dist/src/commands/plan-command.js +38 -11
  30. package/dist/src/commands/rerun-command.js +42 -0
  31. package/dist/src/commands/research-command.js +10 -4
  32. package/dist/src/commands/runs-command.js +22 -0
  33. package/dist/src/commands/start-command.js +0 -1
  34. package/dist/src/commands/test-command.js +3 -3
  35. package/dist/src/components/App.js +8 -0
  36. package/dist/src/config.js +3 -0
  37. package/dist/src/explorbot.js +20 -1
  38. package/dist/src/explorer.js +59 -16
  39. package/dist/src/suite.js +115 -0
  40. package/dist/src/utils/html.js +2 -5
  41. package/dist/src/utils/rules-loader.js +33 -17
  42. package/dist/src/utils/test-files.js +103 -0
  43. package/dist/src/utils/web-element.js +6 -4
  44. package/package.json +3 -2
  45. package/rules/rerunner/healing-approach.md +19 -0
  46. package/src/action.ts +8 -6
  47. package/src/ai/historian.ts +37 -3
  48. package/src/ai/navigator.ts +35 -28
  49. package/src/ai/pilot.ts +33 -9
  50. package/src/ai/planner/subpages.ts +37 -7
  51. package/src/ai/planner.ts +44 -12
  52. package/src/ai/rerunner.ts +532 -0
  53. package/src/ai/researcher/cache.ts +14 -8
  54. package/src/ai/researcher/coordinates.ts +8 -7
  55. package/src/ai/researcher/deep-analysis.ts +18 -21
  56. package/src/ai/researcher/locators.ts +3 -3
  57. package/src/ai/researcher/parser.ts +4 -4
  58. package/src/ai/researcher/research-result.ts +1 -0
  59. package/src/ai/researcher.ts +3 -3
  60. package/src/ai/rules.ts +2 -2
  61. package/src/ai/tools.ts +7 -2
  62. package/src/commands/add-rule-command.ts +1 -2
  63. package/src/commands/base-command.ts +13 -0
  64. package/src/commands/context-command.ts +10 -3
  65. package/src/commands/drill-command.ts +0 -1
  66. package/src/commands/explore-command.ts +22 -6
  67. package/src/commands/freesail-command.ts +6 -23
  68. package/src/commands/index.ts +4 -0
  69. package/src/commands/init-command.ts +8 -5
  70. package/src/commands/path-command.ts +2 -1
  71. package/src/commands/plan-command.ts +46 -12
  72. package/src/commands/rerun-command.ts +46 -0
  73. package/src/commands/research-command.ts +10 -4
  74. package/src/commands/runs-command.ts +27 -0
  75. package/src/commands/start-command.ts +0 -1
  76. package/src/commands/test-command.ts +3 -3
  77. package/src/components/App.tsx +8 -0
  78. package/src/config.ts +24 -0
  79. package/src/explorbot.ts +22 -1
  80. package/src/explorer.ts +68 -20
  81. package/src/suite.ts +135 -0
  82. package/src/utils/html.ts +1 -5
  83. package/src/utils/rules-loader.ts +35 -17
  84. package/src/utils/test-files.ts +122 -0
  85. package/src/utils/web-element.ts +12 -10
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import chalk from 'chalk';
4
5
  import { Command } from 'commander';
6
+ import figureSet from 'figures';
5
7
  import { render } from 'ink';
6
8
  import React from 'react';
7
9
  import { App } from '../src/components/App.js';
@@ -104,14 +106,14 @@ async function showStatsAndExit(code: number): Promise<never> {
104
106
  process.exit(code);
105
107
  }
106
108
 
107
- addCommonOptions(program.command('start [path]').alias('sail').description('Start web exploration')).action(async (startPath, options) => {
109
+ addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
108
110
  setPreserveConsoleLogs(false);
109
111
  const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
110
112
  await explorBot.start();
111
113
  await startTUI(explorBot);
112
114
  });
113
115
 
114
- addCommonOptions(program.command('explore <path>').description('Start web exploration (legacy command)').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
116
+ addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
115
117
  try {
116
118
  const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
117
119
  await explorBot.start();
@@ -128,10 +130,11 @@ addCommonOptions(program.command('explore <path>').description('Start web explor
128
130
  }
129
131
  });
130
132
 
131
- addCommonOptions(program.command('plan <path> [feature]').description('Generate test plan for a page and exit'))
133
+ addCommonOptions(program.command('plan <path>').description('Generate test plan for a page and exit'))
132
134
  .option('-a, --append', 'Add tests to existing plan file')
133
135
  .option('--style <style>', 'Planning style: normal, curious, psycho')
134
- .action(async (planPath, feature, options) => {
136
+ .option('--focus <feature>', 'Focus area for test planning')
137
+ .action(async (planPath, options) => {
135
138
  try {
136
139
  const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
137
140
  await explorBot.start();
@@ -146,7 +149,7 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
146
149
  }
147
150
  }
148
151
 
149
- await explorBot.plan(feature || undefined, {
152
+ await explorBot.plan(options.focus || undefined, {
150
153
  fresh: !options.append,
151
154
  style: options.style,
152
155
  });
@@ -158,6 +161,23 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
158
161
  await showStatsAndExit(1);
159
162
  }
160
163
 
164
+ const suite = explorBot.getSuite();
165
+ if (suite && suite.automatedTestCount > 0) {
166
+ const names = suite.getAutomatedTestNames();
167
+ console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
168
+ for (let i = 0; i < names.length; i++) {
169
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
170
+ }
171
+ }
172
+
173
+ if (plan?.tests.length) {
174
+ console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
175
+ for (let i = 0; i < plan.tests.length; i++) {
176
+ const t = plan.tests[i];
177
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
178
+ }
179
+ }
180
+
161
181
  const savedPath = explorBot.savePlan();
162
182
  const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
163
183
 
@@ -165,10 +185,14 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
165
185
  const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
166
186
 
167
187
  const lines: string[] = [];
168
- lines.push('Run tests:');
169
- lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
170
- lines.push(`\`${cli} test ${planFile} 1-3${cliSuffix}\` → run tests 1 to 3`);
171
- lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` run all tests`);
188
+ lines.push('Run commands:');
189
+ lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first new test`);
190
+ lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` → run all new tests`);
191
+ if (suite && suite.automatedTestCount > 0) {
192
+ for (const f of suite.getAutomatedTestFiles()) {
193
+ lines.push(`\`${cli} rerun ${path.relative(process.cwd(), f)}${cliSuffix}\` → re-run automated tests`);
194
+ }
195
+ }
172
196
 
173
197
  log(parseMarkdownToTerminal(lines.join('\n')));
174
198
 
@@ -280,6 +304,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
280
304
  }
281
305
  });
282
306
 
307
+ program
308
+ .command('runs [file]')
309
+ .description('List generated test files, or show steps for a specific file')
310
+ .option('-p, --path <path>', 'Working directory path')
311
+ .option('-c, --config <path>', 'Path to configuration file')
312
+ .action(async (file, options) => {
313
+ try {
314
+ await ConfigParser.getInstance().loadConfig({
315
+ config: options.config,
316
+ path: options.path || process.cwd(),
317
+ });
318
+ const explorBot = new ExplorBot({ path: options.path });
319
+ const { RunsCommand } = await import('../src/commands/runs-command.js');
320
+ await new RunsCommand(explorBot).execute(file || '');
321
+ } catch (error) {
322
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
323
+ process.exit(1);
324
+ }
325
+ });
326
+
327
+ addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
328
+ try {
329
+ const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
330
+ await explorBot.start();
331
+ const { RerunCommand } = await import('../src/commands/rerun-command.js');
332
+ const cmd = new RerunCommand(explorBot);
333
+ const args = index ? `${filename} ${index}` : filename;
334
+ await cmd.execute(args);
335
+ await explorBot.stop();
336
+ await showStatsAndExit(0);
337
+ } catch (error) {
338
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
339
+ await showStatsAndExit(1);
340
+ }
341
+ });
342
+
283
343
  addCommonOptions(
284
344
  program
285
345
  .command('freesail [startUrl]')
@@ -376,7 +436,6 @@ program
376
436
 
377
437
  program
378
438
  .command('learn [url] [description]')
379
- .alias('add-knowledge')
380
439
  .description('Add knowledge for URLs')
381
440
  .option('-p, --path <path>', 'Working directory path')
382
441
  .action(async (url, description, options) => {
@@ -447,32 +506,32 @@ addCommonOptions(program.command('research <url>').description('Research a page
447
506
  }
448
507
  );
449
508
 
450
- addCommonOptions(
451
- program.command('drill <url>').alias('bosun').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')
452
- ).action(async (url, options) => {
453
- try {
454
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
455
- await explorBot.start();
509
+ addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(
510
+ async (url, options) => {
511
+ try {
512
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
513
+ await explorBot.start();
456
514
 
457
- await explorBot.visit(url);
515
+ await explorBot.visit(url);
458
516
 
459
- const plan = await explorBot.agentBosun().drill({
460
- knowledgePath: options.knowledge,
461
- maxComponents: Number.parseInt(options.max, 10),
462
- interactive: false,
463
- });
517
+ const plan = await explorBot.agentBosun().drill({
518
+ knowledgePath: options.knowledge,
519
+ maxComponents: Number.parseInt(options.max, 10),
520
+ interactive: false,
521
+ });
464
522
 
465
- console.log(`\nDrill completed: ${plan.tests.length} components`);
466
- console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
467
- console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
523
+ console.log(`\nDrill completed: ${plan.tests.length} components`);
524
+ console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
525
+ console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
468
526
 
469
- await explorBot.stop();
470
- await showStatsAndExit(0);
471
- } catch (error) {
472
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
473
- await showStatsAndExit(1);
527
+ await explorBot.stop();
528
+ await showStatsAndExit(0);
529
+ } catch (error) {
530
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
531
+ await showStatsAndExit(1);
532
+ }
474
533
  }
475
- });
534
+ );
476
535
 
477
536
  program
478
537
  .command('context <url>')
@@ -624,18 +683,18 @@ browserCmd
624
683
  });
625
684
 
626
685
  program
627
- .command('extract-styles <agent>')
628
- .description('Extract built-in planning styles to a directory for customization')
629
- .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>/styles)')
686
+ .command('extract-rules <agent>')
687
+ .description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
688
+ .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
630
689
  .action(async (agent, options) => {
631
690
  try {
632
691
  const { RulesLoader } = await import('../src/utils/rules-loader.js');
633
- const targetDir = options.dir || path.resolve(`./rules/${agent}/styles`);
634
- const extracted = RulesLoader.extractStyles(agent, targetDir);
692
+ const targetDir = options.dir || path.resolve(`./rules/${agent}`);
693
+ const extracted = RulesLoader.extractRules(agent, targetDir);
635
694
  if (extracted.length === 0) {
636
- console.log('All style files already exist in target directory.');
695
+ console.log('All rule files already exist in target directory.');
637
696
  } else {
638
- console.log(`\nExtracted ${extracted.length} style files to ${targetDir}`);
697
+ console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
639
698
  }
640
699
  } catch (error) {
641
700
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
@@ -645,7 +704,6 @@ program
645
704
 
646
705
  program
647
706
  .command('add-rule [agent] [name]')
648
- .alias('rules:add')
649
707
  .description('Create a rule file for an agent')
650
708
  .option('--url <pattern>', 'URL pattern for this rule')
651
709
  .option('-p, --path <path>', 'Working directory path')
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import chalk from 'chalk';
4
5
  import { Command } from 'commander';
6
+ import figureSet from 'figures';
5
7
  import { render } from 'ink';
6
8
  import React from 'react';
7
9
  import { App } from '../src/components/App.js';
@@ -78,13 +80,13 @@ async function showStatsAndExit(code) {
78
80
  }
79
81
  process.exit(code);
80
82
  }
81
- addCommonOptions(program.command('start [path]').alias('sail').description('Start web exploration')).action(async (startPath, options) => {
83
+ addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
82
84
  setPreserveConsoleLogs(false);
83
85
  const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
84
86
  await explorBot.start();
85
87
  await startTUI(explorBot);
86
88
  });
87
- addCommonOptions(program.command('explore <path>').description('Start web exploration (legacy command)').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
89
+ addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
88
90
  try {
89
91
  const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
90
92
  await explorBot.start();
@@ -102,10 +104,11 @@ addCommonOptions(program.command('explore <path>').description('Start web explor
102
104
  await showStatsAndExit(1);
103
105
  }
104
106
  });
105
- addCommonOptions(program.command('plan <path> [feature]').description('Generate test plan for a page and exit'))
107
+ addCommonOptions(program.command('plan <path>').description('Generate test plan for a page and exit'))
106
108
  .option('-a, --append', 'Add tests to existing plan file')
107
109
  .option('--style <style>', 'Planning style: normal, curious, psycho')
108
- .action(async (planPath, feature, options) => {
110
+ .option('--focus <feature>', 'Focus area for test planning')
111
+ .action(async (planPath, options) => {
109
112
  try {
110
113
  const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
111
114
  await explorBot.start();
@@ -117,7 +120,7 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
117
120
  explorBot.loadPlan(existingPlanPath);
118
121
  }
119
122
  }
120
- await explorBot.plan(feature || undefined, {
123
+ await explorBot.plan(options.focus || undefined, {
121
124
  fresh: !options.append,
122
125
  style: options.style,
123
126
  });
@@ -127,15 +130,34 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
127
130
  await explorBot.stop();
128
131
  await showStatsAndExit(1);
129
132
  }
133
+ const suite = explorBot.getSuite();
134
+ if (suite && suite.automatedTestCount > 0) {
135
+ const names = suite.getAutomatedTestNames();
136
+ console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
137
+ for (let i = 0; i < names.length; i++) {
138
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
139
+ }
140
+ }
141
+ if (plan?.tests.length) {
142
+ console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
143
+ for (let i = 0; i < plan.tests.length; i++) {
144
+ const t = plan.tests[i];
145
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
146
+ }
147
+ }
130
148
  const savedPath = explorBot.savePlan();
131
149
  const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
132
150
  const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
133
151
  const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
134
152
  const lines = [];
135
- lines.push('Run tests:');
136
- lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
137
- lines.push(`\`${cli} test ${planFile} 1-3${cliSuffix}\` → run tests 1 to 3`);
138
- lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` run all tests`);
153
+ lines.push('Run commands:');
154
+ lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first new test`);
155
+ lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` → run all new tests`);
156
+ if (suite && suite.automatedTestCount > 0) {
157
+ for (const f of suite.getAutomatedTestFiles()) {
158
+ lines.push(`\`${cli} rerun ${path.relative(process.cwd(), f)}${cliSuffix}\` → re-run automated tests`);
159
+ }
160
+ }
139
161
  log(parseMarkdownToTerminal(lines.join('\n')));
140
162
  await explorBot.stop();
141
163
  await showStatsAndExit(0);
@@ -239,6 +261,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
239
261
  await showStatsAndExit(1);
240
262
  }
241
263
  });
264
+ program
265
+ .command('runs [file]')
266
+ .description('List generated test files, or show steps for a specific file')
267
+ .option('-p, --path <path>', 'Working directory path')
268
+ .option('-c, --config <path>', 'Path to configuration file')
269
+ .action(async (file, options) => {
270
+ try {
271
+ await ConfigParser.getInstance().loadConfig({
272
+ config: options.config,
273
+ path: options.path || process.cwd(),
274
+ });
275
+ const explorBot = new ExplorBot({ path: options.path });
276
+ const { RunsCommand } = await import('../src/commands/runs-command.js');
277
+ await new RunsCommand(explorBot).execute(file || '');
278
+ }
279
+ catch (error) {
280
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
281
+ process.exit(1);
282
+ }
283
+ });
284
+ addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
285
+ try {
286
+ const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
287
+ await explorBot.start();
288
+ const { RerunCommand } = await import('../src/commands/rerun-command.js');
289
+ const cmd = new RerunCommand(explorBot);
290
+ const args = index ? `${filename} ${index}` : filename;
291
+ await cmd.execute(args);
292
+ await explorBot.stop();
293
+ await showStatsAndExit(0);
294
+ }
295
+ catch (error) {
296
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
297
+ await showStatsAndExit(1);
298
+ }
299
+ });
242
300
  addCommonOptions(program
243
301
  .command('freesail [startUrl]')
244
302
  .description('Continuously explore and navigate to new pages autonomously')
@@ -327,7 +385,6 @@ program
327
385
  });
328
386
  program
329
387
  .command('learn [url] [description]')
330
- .alias('add-knowledge')
331
388
  .description('Add knowledge for URLs')
332
389
  .option('-p, --path <path>', 'Working directory path')
333
390
  .action(async (url, description, options) => {
@@ -394,7 +451,7 @@ addCommonOptions(program.command('research <url>').description('Research a page
394
451
  await showStatsAndExit(1);
395
452
  }
396
453
  });
397
- addCommonOptions(program.command('drill <url>').alias('bosun').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(async (url, options) => {
454
+ addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(async (url, options) => {
398
455
  try {
399
456
  const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
400
457
  await explorBot.start();
@@ -554,19 +611,19 @@ browserCmd
554
611
  }
555
612
  });
556
613
  program
557
- .command('extract-styles <agent>')
558
- .description('Extract built-in planning styles to a directory for customization')
559
- .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>/styles)')
614
+ .command('extract-rules <agent>')
615
+ .description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
616
+ .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
560
617
  .action(async (agent, options) => {
561
618
  try {
562
619
  const { RulesLoader } = await import('../src/utils/rules-loader.js');
563
- const targetDir = options.dir || path.resolve(`./rules/${agent}/styles`);
564
- const extracted = RulesLoader.extractStyles(agent, targetDir);
620
+ const targetDir = options.dir || path.resolve(`./rules/${agent}`);
621
+ const extracted = RulesLoader.extractRules(agent, targetDir);
565
622
  if (extracted.length === 0) {
566
- console.log('All style files already exist in target directory.');
623
+ console.log('All rule files already exist in target directory.');
567
624
  }
568
625
  else {
569
- console.log(`\nExtracted ${extracted.length} style files to ${targetDir}`);
626
+ console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
570
627
  }
571
628
  }
572
629
  catch (error) {
@@ -576,7 +633,6 @@ program
576
633
  });
577
634
  program
578
635
  .command('add-rule [agent] [name]')
579
- .alias('rules:add')
580
636
  .description('Create a rule file for an agent')
581
637
  .option('--url <pattern>', 'URL pattern for this rule')
582
638
  .option('-p, --path <path>', 'Working directory path')
@@ -0,0 +1,19 @@
1
+ <healing_approach>
2
+ The failed step was NOT performed. You MUST execute a replacement action.
3
+ Just waiting or diagnosing is NOT enough — you must perform the click/fill/press that was intended.
4
+
5
+ 1. FIRST: Check the page URL and ARIA — are you on the right page?
6
+ - If URL or ARIA shows login/error/404 page → call giveUp immediately
7
+ 2. If ARIA is empty/minimal → page may still be loading:
8
+ - Use xpathCheck() to detect spinners, loaders, or loading indicators on the page
9
+ - Use wait() to let the page load — it returns fresh ARIA automatically
10
+ - Then execute the replacement action with a working locator
11
+ 3. If the target element is visible in ARIA:
12
+ - Use click() with multiple fallback locators (ARIA, CSS, XPath)
13
+ 4. If element is NOT in ARIA but page is correct:
14
+ - Use xpathCheck() to search the full HTML
15
+ - Use research() to get a semantic UI map of the page if needed
16
+ - If found → click it
17
+ - If not → bash to check console logs → giveUp
18
+ 5. Call done() with the command that replaced the failed step
19
+ </healing_approach>
@@ -94,16 +94,17 @@ class Action {
94
94
  let ariaSnapshotFile = undefined;
95
95
  try {
96
96
  const page = this.playwrightHelper.page;
97
- const serializedSnapshot = await page.locator('body').ariaSnapshot();
98
- const ariaFileName = `${stateHash}_${timestamp}.aria.yaml`;
99
- const ariaPath = join(statesDir, ariaFileName);
100
- fs.writeFileSync(ariaPath, serializedSnapshot, 'utf8');
101
- ariaSnapshot = serializedSnapshot;
102
- ariaSnapshotFile = ariaFileName;
97
+ ariaSnapshot = await page.locator('body').ariaSnapshot();
103
98
  }
104
99
  catch (err) {
105
100
  debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
106
101
  }
102
+ if (ariaSnapshot) {
103
+ const ariaFileName = `${stateHash}_${timestamp}.aria.yaml`;
104
+ const ariaPath = join(statesDir, ariaFileName);
105
+ fs.writeFileSync(ariaPath, ariaSnapshot, 'utf8');
106
+ ariaSnapshotFile = ariaFileName;
107
+ }
107
108
  const result = new ActionResult({
108
109
  html,
109
110
  title,
@@ -115,7 +116,7 @@ class Action {
115
116
  iframeSnapshots,
116
117
  ariaSnapshot,
117
118
  ariaSnapshotFile,
118
- iframeURL: frame?.url?.() || undefined,
119
+ iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
119
120
  });
120
121
  this.stateManager.updateState(result);
121
122
  return result;
@@ -1,9 +1,10 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import dedent from 'dedent';
4
4
  import { z } from 'zod';
5
5
  import { ActionResult } from "../action-result.js";
6
6
  import { ConfigParser } from "../config.js";
7
+ import { KnowledgeTracker } from "../knowledge-tracker.js";
7
8
  import { ExperienceTracker } from "../experience-tracker.js";
8
9
  import { Test } from "../test-plan.js";
9
10
  import { createDebug, tag } from "../utils/logger.js";
@@ -329,6 +330,7 @@ export class Historian {
329
330
  if (startUrl) {
330
331
  lines.push('Before(({ I }) => {');
331
332
  lines.push(` I.amOnPage('${this.escapeString(startUrl)}');`);
333
+ lines.push(...this.getKnowledgeLines(startUrl));
332
334
  lines.push('});');
333
335
  lines.push('');
334
336
  }
@@ -356,8 +358,7 @@ export class Historian {
356
358
  lines.push('});');
357
359
  lines.push('');
358
360
  }
359
- const outputDir = ConfigParser.getInstance().getOutputDir();
360
- const testsDir = join(outputDir, 'tests');
361
+ const testsDir = ConfigParser.getInstance().getTestsDir();
361
362
  mkdirSync(testsDir, { recursive: true });
362
363
  const filename = plan.title.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
363
364
  const filePath = join(testsDir, `${filename}.js`);
@@ -365,12 +366,42 @@ export class Historian {
365
366
  tag('substep').log(`Saved plan tests to: ${filePath}`);
366
367
  return filePath;
367
368
  }
369
+ rewriteScenarioInFile(filePath, healedSteps) {
370
+ let content = readFileSync(filePath, 'utf-8');
371
+ for (const step of healedSteps) {
372
+ if (!content.includes(step.original))
373
+ continue;
374
+ content = content.replace(step.original, step.healed);
375
+ }
376
+ writeFileSync(filePath, content);
377
+ tag('substep').log(`Updated test file with healed steps: ${filePath}`);
378
+ }
368
379
  getExecutionLabel(exec, fallback) {
369
380
  return exec.input?.explanation || exec.input?.assertion || exec.input?.note || fallback || '';
370
381
  }
371
382
  escapeString(str) {
372
383
  return str.replace(/'/g, "\\'").replace(/\n/g, ' ');
373
384
  }
385
+ getKnowledgeLines(url, indent = ' ') {
386
+ const knowledgeTracker = new KnowledgeTracker();
387
+ const state = new ActionResult({ url });
388
+ const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
389
+ const lines = [];
390
+ if (wait !== undefined) {
391
+ lines.push(`${indent}I.wait(${wait});`);
392
+ }
393
+ if (waitForElement) {
394
+ lines.push(`${indent}I.waitForElement(${JSON.stringify(waitForElement)});`);
395
+ }
396
+ if (code) {
397
+ for (const codeLine of code.split('\n')) {
398
+ const trimmed = codeLine.trim();
399
+ if (trimmed)
400
+ lines.push(`${indent}${trimmed}`);
401
+ }
402
+ }
403
+ return lines;
404
+ }
374
405
  stripComments(code) {
375
406
  return code
376
407
  .split('\n')
@@ -31,6 +31,18 @@ class Navigator {
31
31
  You are given the web page and a message from user.
32
32
  You need to resolve the state of the page based on the message.
33
33
  </task>
34
+
35
+ ${locatorRule}
36
+
37
+ <constraints>
38
+ NEVER navigate away from the base URL domain. Stay on the same origin at all times.
39
+ NEVER attempt to rewrite, replace, mock, or spoof the URL via JavaScript, history API, location assignment, or any client-side trick.
40
+ NEVER use executeScript, executeAsyncScript, or any JS evaluation to change the URL, bypass redirects, or fake the page state.
41
+ If the target URL redirects to an authentication/login page, DO NOT try to force the original URL. Instead:
42
+ 1. Look for credentials in the provided knowledge/hint context and perform a real login through the form.
43
+ 2. If no credentials are available, ask the user for credentials or ask the user to log in manually.
44
+ A redirect to /login, /sign_in, /auth, or similar is a signal that authentication is required — treat it as such, never as an obstacle to bypass.
45
+ </constraints>
34
46
  `;
35
47
  freeSailSystemPrompt = dedent `
36
48
  <role>
@@ -145,6 +157,14 @@ class Navigator {
145
157
  ${message}
146
158
  </message>
147
159
 
160
+ <page>
161
+ ${actionResult.toAiContext()}
162
+
163
+ <page_html>
164
+ ${await actionResult.combinedHtml()}
165
+ </page_html>
166
+ </page>
167
+
148
168
  <task>
149
169
  Identify the actual request of the user.
150
170
  Identify what is expected by user.
@@ -155,25 +175,13 @@ class Navigator {
155
175
  Try various ways to achieve the result
156
176
  </task>
157
177
 
158
-
159
- <page>
160
- ${actionResult.toAiContext()}
161
-
162
- <page_html>
163
- ${await actionResult.simplifiedHtml()}
164
- </page_html>
165
- </page>
166
-
167
-
168
- ${knowledge}
169
-
170
178
  ${actionRule}
171
179
 
172
- ${experience}
180
+ ${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
173
181
 
174
- ${locatorRule}
182
+ ${experience}
175
183
 
176
- ${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
184
+ ${knowledge}
177
185
  `;
178
186
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
179
187
  conversation.addUserText(prompt);
@@ -206,7 +214,7 @@ class Navigator {
206
214
  Previous solutions did not work. Here is the full HTML context:
207
215
 
208
216
  <page_html>
209
- ${await actionResult.simplifiedHtml()}
217
+ ${await actionResult.combinedHtml()}
210
218
  </page_html>
211
219
 
212
220
  Please suggest new solutions based on this additional context.
@@ -234,6 +242,7 @@ class Navigator {
234
242
  }
235
243
  if (resolved) {
236
244
  tag('success').log('Navigation resolved successfully');
245
+ await this.experienceTracker.saveSuccessfulResolution(actionResult, message, codeBlock);
237
246
  stop();
238
247
  return;
239
248
  }
@@ -414,6 +423,14 @@ class Navigator {
414
423
  ${message}
415
424
  </message>
416
425
 
426
+ <page>
427
+ ${actionResult.toAiContext()}
428
+
429
+ <page_html>
430
+ ${await actionResult.combinedHtml()}
431
+ </page_html>
432
+ </page>
433
+
417
434
  <task>
418
435
  Identify what assertion the user wants to verify on the page.
419
436
  Propose different CodeceptJS assertion code blocks to verify the expected state.
@@ -427,21 +444,11 @@ class Navigator {
427
444
  Do not generate assertions that would pass even if the specific claim is false.
428
445
  </task>
429
446
 
430
- <page>
431
- ${actionResult.toAiContext()}
432
-
433
- <page_html>
434
- ${await actionResult.simplifiedHtml()}
435
- </page_html>
436
- </page>
437
-
438
- ${knowledge}
439
-
440
447
  ${RulesLoader.loadRules('navigator', ['verification-actions'], actionResult.url || '')}
441
448
 
442
- ${locatorRule}
443
-
444
449
  ${experience}
450
+
451
+ ${knowledge}
445
452
  `;
446
453
  debugLog('Sending verification prompt to AI provider');
447
454
  tag('debug').log('Prompt:', prompt);