explorbot 0.5.10 → 0.6.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 (151) hide show
  1. package/bin/explorbot-cli.ts +494 -482
  2. package/boat/api-tester/src/ai/curler.ts +1 -1
  3. package/boat/api-tester/src/cli.ts +4 -4
  4. package/boat/doc-collector/src/screenshots.ts +1 -1
  5. package/dist/bin/explorbot-cli.js +455 -444
  6. package/dist/boat/api-tester/src/ai/curler.js +1 -1
  7. package/dist/boat/api-tester/src/cli.js +4 -4
  8. package/dist/models.json +1 -1
  9. package/dist/package.json +2 -1
  10. package/dist/rules/navigator/output.md +2 -8
  11. package/dist/rules/navigator/verification-actions.md +6 -2
  12. package/dist/rules/planner/ignored-pages.md +1 -0
  13. package/dist/src/action-result.js +1 -1
  14. package/dist/src/action.d.ts +0 -1
  15. package/dist/src/action.js +0 -4
  16. package/dist/src/ai/captain/idle-mode.js +42 -26
  17. package/dist/src/ai/captain/mixin.d.ts +2 -0
  18. package/dist/src/ai/captain/web-mode.js +11 -0
  19. package/dist/src/ai/captain.d.ts +6 -4
  20. package/dist/src/ai/captain.js +25 -1
  21. package/dist/src/ai/driller.js +1 -1
  22. package/dist/src/ai/experience-compactor.js +2 -2
  23. package/dist/src/ai/judge.d.ts +7 -0
  24. package/dist/src/ai/judge.js +8 -1
  25. package/dist/src/ai/navigator.d.ts +1 -0
  26. package/dist/src/ai/navigator.js +11 -7
  27. package/dist/src/ai/planner/subpages.js +1 -1
  28. package/dist/src/ai/planner.d.ts +12 -5
  29. package/dist/src/ai/planner.js +138 -86
  30. package/dist/src/ai/provider.d.ts +1 -1
  31. package/dist/src/ai/provider.js +16 -5
  32. package/dist/src/ai/rerunner.js +1 -1
  33. package/dist/src/ai/researcher/cache.js +14 -6
  34. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
  35. package/dist/src/ai/researcher/deep-analysis.js +7 -2
  36. package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
  37. package/dist/src/ai/researcher.js +44 -24
  38. package/dist/src/ai/rules.js +6 -7
  39. package/dist/src/ai/scout.d.ts +12 -2
  40. package/dist/src/ai/scout.js +103 -8
  41. package/dist/src/ai/tester.d.ts +9 -5
  42. package/dist/src/ai/tester.js +22 -42
  43. package/dist/src/ai/tools.js +10 -8
  44. package/dist/src/application-spec.d.ts +2 -0
  45. package/dist/src/application-spec.js +17 -12
  46. package/dist/src/commands/context-experience-command.js +0 -1
  47. package/dist/src/commands/context-knowledge-command.js +0 -1
  48. package/dist/src/commands/explore-command.d.ts +3 -2
  49. package/dist/src/commands/explore-command.js +35 -23
  50. package/dist/src/commands/help-json-command.js +3 -1
  51. package/dist/src/commands/init-command.js +4 -4
  52. package/dist/src/commands/learn-command.js +0 -2
  53. package/dist/src/commands/plan-command.d.ts +3 -0
  54. package/dist/src/commands/plan-command.js +43 -17
  55. package/dist/src/commands/plan-save-command.js +6 -17
  56. package/dist/src/commands/plans-command.js +1 -1
  57. package/dist/src/commands/test-command.js +12 -2
  58. package/dist/src/components/App.d.ts +1 -2
  59. package/dist/src/components/App.js +1 -3
  60. package/dist/src/components/InputPane.js +0 -5
  61. package/dist/src/config.d.ts +3 -2
  62. package/dist/src/config.js +2 -12
  63. package/dist/src/explorbot.d.ts +3 -2
  64. package/dist/src/explorbot.js +32 -10
  65. package/dist/src/explorer.js +6 -7
  66. package/dist/src/knowledge-tracker.d.ts +2 -2
  67. package/dist/src/knowledge-tracker.js +18 -6
  68. package/dist/src/playwright-recorder.d.ts +0 -1
  69. package/dist/src/playwright-recorder.js +0 -2
  70. package/dist/src/reporter.d.ts +1 -1
  71. package/dist/src/reporter.js +3 -3
  72. package/dist/src/test-plan.js +1 -1
  73. package/dist/src/utils/error-page.d.ts +17 -0
  74. package/dist/src/utils/error-page.js +53 -2
  75. package/dist/src/utils/html-diff.d.ts +1 -2
  76. package/dist/src/utils/html-diff.js +4 -44
  77. package/dist/src/utils/logger.js +27 -10
  78. package/dist/src/utils/markdown-terminal.js +1 -2
  79. package/dist/src/utils/next-steps.js +20 -16
  80. package/dist/src/utils/page-readiness.d.ts +2 -1
  81. package/dist/src/utils/page-readiness.js +24 -5
  82. package/dist/src/utils/test-plan-markdown.d.ts +3 -0
  83. package/dist/src/utils/test-plan-markdown.js +35 -4
  84. package/dist/src/utils/url-matcher.d.ts +1 -0
  85. package/dist/src/utils/url-matcher.js +5 -0
  86. package/docs/basics/providers.md +1 -1
  87. package/docs/contributing/regression-tests.md +19 -4
  88. package/docs/reference/commands.md +6 -2
  89. package/docs/reference/configuration.md +1 -1
  90. package/docs/web-testing/agents.md +8 -0
  91. package/docs/web-testing/customization.md +2 -0
  92. package/docs/web-testing/researcher.md +1 -1
  93. package/models.json +1 -1
  94. package/package.json +2 -1
  95. package/rules/navigator/output.md +2 -8
  96. package/rules/navigator/verification-actions.md +6 -2
  97. package/rules/planner/ignored-pages.md +1 -0
  98. package/src/action-result.ts +1 -2
  99. package/src/action.ts +0 -5
  100. package/src/ai/captain/idle-mode.ts +38 -26
  101. package/src/ai/captain/mixin.ts +2 -1
  102. package/src/ai/captain/web-mode.ts +12 -0
  103. package/src/ai/captain.ts +25 -7
  104. package/src/ai/driller.ts +1 -2
  105. package/src/ai/experience-compactor.ts +2 -2
  106. package/src/ai/judge.ts +10 -1
  107. package/src/ai/navigator.ts +12 -7
  108. package/src/ai/planner/subpages.ts +1 -2
  109. package/src/ai/planner.ts +140 -92
  110. package/src/ai/provider.ts +15 -6
  111. package/src/ai/rerunner.ts +1 -2
  112. package/src/ai/researcher/cache.ts +16 -7
  113. package/src/ai/researcher/deep-analysis.ts +7 -2
  114. package/src/ai/researcher/fingerprint-worker.ts +5 -5
  115. package/src/ai/researcher.ts +44 -28
  116. package/src/ai/rules.ts +6 -7
  117. package/src/ai/scout.ts +98 -10
  118. package/src/ai/tester.ts +29 -47
  119. package/src/ai/tools.ts +11 -9
  120. package/src/application-spec.ts +19 -13
  121. package/src/commands/clean-command.ts +1 -1
  122. package/src/commands/context-command.ts +0 -1
  123. package/src/commands/context-experience-command.ts +0 -1
  124. package/src/commands/context-knowledge-command.ts +0 -1
  125. package/src/commands/explore-command.ts +36 -23
  126. package/src/commands/help-json-command.ts +8 -6
  127. package/src/commands/init-command.ts +5 -3
  128. package/src/commands/learn-command.ts +0 -2
  129. package/src/commands/plan-command.ts +44 -17
  130. package/src/commands/plan-save-command.ts +6 -17
  131. package/src/commands/plans-command.ts +1 -1
  132. package/src/commands/test-command.ts +11 -2
  133. package/src/components/App.tsx +2 -6
  134. package/src/components/InputPane.tsx +0 -5
  135. package/src/config.ts +4 -16
  136. package/src/experience-tracker.ts +1 -1
  137. package/src/explorbot.ts +31 -11
  138. package/src/explorer.ts +6 -7
  139. package/src/knowledge-tracker.ts +18 -7
  140. package/src/playwright-recorder.ts +0 -2
  141. package/src/reporter.ts +3 -4
  142. package/src/test-plan.ts +1 -2
  143. package/src/utils/error-page.ts +64 -2
  144. package/src/utils/html-diff.ts +4 -56
  145. package/src/utils/html.ts +0 -1
  146. package/src/utils/logger.ts +30 -12
  147. package/src/utils/markdown-terminal.ts +1 -2
  148. package/src/utils/next-steps.ts +19 -17
  149. package/src/utils/page-readiness.ts +33 -7
  150. package/src/utils/test-plan-markdown.ts +36 -4
  151. package/src/utils/url-matcher.ts +6 -0
@@ -3,13 +3,16 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import chalk from 'chalk';
6
- import { Command } from 'commander';
7
- import figureSet from 'figures';
6
+ import { Command, Help } from 'commander';
8
7
  import { render } from 'ink';
9
8
  import React from 'react';
9
+ import { createApiCommands } from '../boat/api-tester/src/cli.ts';
10
+ import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
11
+ import { createPrimaCommands } from '../boat/prima/src/cli.ts';
10
12
  import { flushTelemetry } from '../src/ai/provider.js';
11
13
  import { HelpJsonCommand } from '../src/commands/help-json-command.js';
12
14
  import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
15
+ import { PlanCommand } from '../src/commands/plan-command.js';
13
16
  import { App } from '../src/components/App.js';
14
17
  import { StatusPane } from '../src/components/StatusPane.js';
15
18
  import { authOption, decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
@@ -22,7 +25,6 @@ import { getCliName } from '../src/utils/cli-name.ts';
22
25
  import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode, tag } from '../src/utils/logger.js';
23
26
  import { jsonToTable } from '../src/utils/markdown-parser.js';
24
27
  import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
25
- import { type NextStepSection, printNextSteps, relativeToCwd } from '../src/utils/next-steps.ts';
26
28
  import { readCliSitemap } from '../src/utils/sitemap.ts';
27
29
  import { UrlFilter } from '../src/utils/url-filter.ts';
28
30
 
@@ -32,11 +34,40 @@ const cli = getCliName();
32
34
  const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
33
35
  const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
34
36
 
35
- program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
36
- wsOption.register(program);
37
- knowledgeOption.register(program);
38
- authOption.register(program);
39
- decisionModelOption.register(program);
37
+ const GROUPS = {
38
+ explore: 'Explore:',
39
+ steps: 'Steps:',
40
+ results: 'Results:',
41
+ knowledge: 'Knowledge & rules:',
42
+ setup: 'Setup:',
43
+ tools: 'More tools:',
44
+ help: 'Help:',
45
+ };
46
+
47
+ program
48
+ .name(cli)
49
+ .description('AI-powered exploratory testing for web apps')
50
+ .version(pkgVersion, '-V, --version')
51
+ .configureHelp({
52
+ styleTitle: (str) => chalk.yellow.bold(str),
53
+ styleCommandText: (str) => chalk.bold(str),
54
+ styleSubcommandText: (str) => chalk.cyan(str),
55
+ styleOptionText: (str) => chalk.green(str),
56
+ styleArgumentText: (str) => chalk.magenta(str),
57
+ showGlobalOptions: true,
58
+ visibleOptions(cmd) {
59
+ const options = Help.prototype.visibleOptions.call(this, cmd);
60
+ if (cmd.parent) return options;
61
+ return options.filter((option) => !isGlobalOption(option.flags));
62
+ },
63
+ visibleGlobalOptions(cmd) {
64
+ return Help.prototype.visibleGlobalOptions.call(this, cmd).filter((option) => isGlobalOption(option.flags));
65
+ },
66
+ });
67
+
68
+ const globalOptions = [knowledgeOption, authOption, decisionModelOption, wsOption];
69
+ for (const option of globalOptions) option.register(program);
70
+ const isGlobalOption = (flags: string) => globalOptions.some((option) => option.flags === flags);
40
71
 
41
72
  process.on('uncaughtException', async (error) => {
42
73
  tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
@@ -64,10 +95,11 @@ interface CLIOptions {
64
95
  headless?: boolean;
65
96
  incognito?: boolean;
66
97
  session?: string | boolean;
67
- spec?: string;
98
+ spec?: string | string[];
68
99
  }
69
100
 
70
101
  function buildExplorBotOptions(from: string | undefined, options: CLIOptions): ExplorBotOptions {
102
+ const spec = Array.isArray(options.spec) ? (options.spec.length > 0 ? options.spec : undefined) : options.spec;
71
103
  return {
72
104
  from,
73
105
  baseUrl: options.baseUrl,
@@ -78,7 +110,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
78
110
  headless: options.headless,
79
111
  incognito: options.incognito,
80
112
  session: options.session,
81
- applicationSpec: options.spec,
113
+ applicationSpec: spec,
82
114
  } as ExplorBotOptions;
83
115
  }
84
116
 
@@ -92,7 +124,7 @@ function addCommonOptions(cmd: Command): Command {
92
124
  .option('-s, --show', 'Show browser window')
93
125
  .option('--headless', 'Run browser in headless mode')
94
126
  .option('--incognito', 'Run without recording experiences')
95
- .option('--spec <path>', 'Use a Docbot application spec directory or index.md')
127
+ .option('--spec <path>', 'Use a Docbot application spec directory or index.md (repeatable)', (val: string, acc: string[]) => [...(acc || []), val])
96
128
  .option('--session [file]', 'Save/restore browser session from file');
97
129
  }
98
130
 
@@ -159,7 +191,9 @@ async function showStatsAndExit(code: number): Promise<never> {
159
191
  process.exit(code);
160
192
  }
161
193
 
162
- addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
194
+ program.commandsGroup(GROUPS.explore);
195
+
196
+ addCommonOptions(program.command('start [path]').description('Open the interactive TUI and explore with commands')).action(async (startPath, options) => {
163
197
  setPreserveConsoleLogs(false);
164
198
  const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
165
199
  await explorBot.start();
@@ -169,7 +203,7 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
169
203
  addCommonOptions(
170
204
  program
171
205
  .command('explore [path]')
172
- .description('Explore a page autonomously and run invented scenarios')
206
+ .description('Invent test scenarios for a page and run them')
173
207
  .option('--sitemap <source>', 'Explore every page listed in a sitemap: file path, URL, or - for stdin (a piped stdin is read automatically)')
174
208
  .option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, plus sort:priority|lastmod for a sitemap, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')
175
209
  .option('--max-tests <count>', 'Maximum number of tests to run')
@@ -207,10 +241,65 @@ addCommonOptions(
207
241
  }
208
242
  });
209
243
 
210
- addCommonOptions(program.command('plan <path>').description('Generate test plan for a page and exit'))
244
+ addCommonOptions(
245
+ program
246
+ .command('freesail [startUrl]')
247
+ .description('Roam the app on its own, moving on to new pages')
248
+ .option('--deep', 'Depth-first: prioritize newly discovered pages')
249
+ .option('--shallow', 'Breadth-first: pick globally least-visited page')
250
+ .option('--scope <prefix>', 'Restrict navigation to URL prefix')
251
+ .option('--max-tests <count>', 'Maximum number of tests to run')
252
+ .option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
253
+ .option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')
254
+ ).action(async (startUrl, options) => {
255
+ const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
256
+ await explorBot.start();
257
+ const args = [options.deep && '--deep', options.shallow && '--shallow', options.scope && `--scope ${options.scope}`, options.maxTests && `--max-tests ${options.maxTests}`, options.maxDuration && `--max-duration ${options.maxDuration}`, options.urlFilter && `--url-filter "${options.urlFilter}"`]
258
+ .filter(Boolean)
259
+ .join(' ');
260
+ const { FreesailCommand } = await import('../src/commands/freesail-command.js');
261
+ const cmd = new FreesailCommand(explorBot);
262
+ try {
263
+ await cmd.execute(args);
264
+ } catch (error) {
265
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
266
+ printPageContext(explorBot);
267
+ await showStatsAndExit(1);
268
+ }
269
+ });
270
+
271
+ program.commandsGroup(GROUPS.steps);
272
+
273
+ addCommonOptions(program.command('research <url>').description('Research a page and print its UI map').option('--data', 'Include data extraction in research').option('--deep', 'Enable deep analysis (expand hidden elements)').option('--no-fix', 'Skip locator fix cycle (for debugging)')).action(
274
+ async (url, options) => {
275
+ try {
276
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
277
+ await explorBot.start();
278
+
279
+ const argParts: string[] = [url];
280
+ if (options.data) argParts.push('--data');
281
+ if (options.deep) argParts.push('--deep');
282
+ if (options.fix === false) argParts.push('--no-fix');
283
+
284
+ const { ResearchCommand } = await import('../src/commands/research-command.js');
285
+ await new ResearchCommand(explorBot).execute(argParts.join(' '));
286
+
287
+ await explorBot.stop();
288
+ await showStatsAndExit(0);
289
+ } catch (error) {
290
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
291
+ await showStatsAndExit(1);
292
+ }
293
+ }
294
+ );
295
+
296
+ addCommonOptions(program.command('plan <path>').description('Write a test plan for a page'))
211
297
  .option('-a, --append', 'Add tests to existing plan file')
212
298
  .option('--style <style>', 'Planning style: normal, curious, psycho')
213
299
  .option('--focus <feature>', 'Focus area for test planning')
300
+ .option('--tests <count>', 'Exact number of tests to generate')
301
+ .option('--min-tests <count>', 'Minimum number of tests to generate')
302
+ .option('--max-tests <count>', 'Maximum number of tests to generate')
214
303
  .action(async (planPath, options) => {
215
304
  try {
216
305
  const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
@@ -218,70 +307,159 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
218
307
 
219
308
  await explorBot.visit(planPath);
220
309
 
221
- if (options.append) {
222
- const planFilename = explorBot.generatePlanFilename();
223
- const existingPlanPath = path.join(explorBot.getPlansDir(), planFilename);
224
- if (fs.existsSync(existingPlanPath)) {
225
- explorBot.loadPlan(existingPlanPath);
226
- }
227
- }
310
+ const args = ['--fresh'];
311
+ if (options.append) args[0] = '--append';
312
+ if (options.style) args.push(`--style ${options.style}`);
313
+ if (options.focus) args.push(`--focus "${options.focus}"`);
314
+ if (options.tests) args.push(`--tests ${options.tests}`);
315
+ if (options.minTests) args.push(`--min-tests ${options.minTests}`);
316
+ if (options.maxTests) args.push(`--max-tests ${options.maxTests}`);
317
+ await new PlanCommand(explorBot).execute(args.join(' '));
228
318
 
229
- await explorBot.plan(options.focus || undefined, {
230
- fresh: !options.append,
231
- style: options.style,
232
- });
319
+ await explorBot.stop();
320
+ await showStatsAndExit(0);
321
+ } catch (error) {
322
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
323
+ await showStatsAndExit(1);
324
+ }
325
+ });
233
326
 
234
- const plan = explorBot.getCurrentPlan();
235
- if (!plan?.tests.length) {
236
- console.error('No test scenarios generated.');
237
- await explorBot.stop();
238
- await showStatsAndExit(1);
327
+ addCommonOptions(program.command('test <planfile> [index]').description('Run tests from a plan file; index: 1, 1,3, 1-5, *').option('--grep <pattern>', 'Run tests matching pattern').option('--from-plan <file>', 'Load plan file when the first argument is a test index')).action(
328
+ async (planfile, index, options) => {
329
+ try {
330
+ let planfileArg = planfile;
331
+ let indexArg = index;
332
+ if (options.fromPlan) {
333
+ planfileArg = options.fromPlan;
334
+ indexArg = planfile;
239
335
  }
240
336
 
241
- const suite = explorBot.getSuite();
242
- if (suite && suite.automatedTestCount > 0) {
243
- const names = suite.getAutomatedTestNames();
244
- console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
245
- for (let i = 0; i < names.length; i++) {
246
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
247
- }
248
- }
337
+ const peeked = Plan.loadFromFile(planfileArg);
249
338
 
250
- if (plan?.tests.length) {
251
- console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
252
- for (let i = 0; i < plan.tests.length; i++) {
253
- const t = plan.tests[i];
254
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
255
- }
256
- }
339
+ const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
340
+ await explorBot.start();
257
341
 
258
- const savedPath = explorBot.savePlan();
259
-
260
- const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
261
- const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
262
-
263
- const sections: NextStepSection[] = [];
264
- if (savedPath) {
265
- const relPlan = relativeToCwd(savedPath);
266
- sections.push({
267
- label: 'Plan',
268
- path: savedPath,
269
- commands: [
270
- { label: 'Re-run', command: `${cli} test ${relPlan} 1${cliSuffix}` },
271
- { label: 'Run all', command: `${cli} test ${relPlan} *${cliSuffix}` },
272
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3${cliSuffix}` },
273
- ],
274
- });
275
- }
276
- const automatedFiles = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
277
- if (automatedFiles.length > 0) {
278
- const commands = automatedFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
279
- sections.push({
280
- label: `Automated tests (${automatedFiles.length})`,
281
- commands,
282
- });
342
+ const plan = explorBot.loadPlan(planfileArg);
343
+ const pending = plan.getPendingTests();
344
+ log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
345
+
346
+ const startUrl = plan.startUrl;
347
+ if (!startUrl) {
348
+ throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
283
349
  }
284
- printNextSteps(sections);
350
+
351
+ log(`Navigating to ${startUrl}`);
352
+ await explorBot.visit(startUrl);
353
+
354
+ let args = '*';
355
+ if (indexArg) args = indexArg;
356
+ else if (options.grep) args = options.grep;
357
+
358
+ const { TestCommand } = await import('../src/commands/test-command.js');
359
+ const cmd = new TestCommand(explorBot);
360
+ await cmd.execute(args);
361
+
362
+ await explorBot.printSessionAnalysis();
363
+ await explorBot.stop();
364
+ await showStatsAndExit(0);
365
+ } catch (error) {
366
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
367
+ await showStatsAndExit(1);
368
+ }
369
+ }
370
+ );
371
+
372
+ addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests, healing failures with AI')).action(async (filename, index, options) => {
373
+ try {
374
+ const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
375
+ await explorBot.start();
376
+ const { RerunCommand } = await import('../src/commands/rerun-command.js');
377
+ const cmd = new RerunCommand(explorBot);
378
+ const args = index ? `${filename} ${index}` : filename;
379
+ await cmd.execute(args);
380
+ await explorBot.stop();
381
+ await showStatsAndExit(0);
382
+ } catch (error) {
383
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
384
+ await showStatsAndExit(1);
385
+ }
386
+ });
387
+
388
+ addCommonOptions(program.command('navigate <url>').description('Reach a URL with the AI Navigator; exits 1 if unreachable')).action(async (url, options) => {
389
+ try {
390
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
391
+ await explorBot.start();
392
+
393
+ const { NavigateCommand } = await import('../src/commands/navigate-command.js');
394
+ await new NavigateCommand(explorBot).execute(url);
395
+
396
+ await explorBot.stop();
397
+ await showStatsAndExit(0);
398
+ } catch (error) {
399
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
400
+ await showStatsAndExit(1);
401
+ }
402
+ });
403
+
404
+ addCommonOptions(
405
+ program.command('drill <url>').alias('driller').description('Try every component on a page to learn how it works').option('--save-knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')
406
+ ).action(async (url, options) => {
407
+ try {
408
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
409
+ await explorBot.start();
410
+
411
+ await explorBot.visit(url);
412
+
413
+ const plan = await explorBot.agentDriller().drill({
414
+ knowledgePath: options.saveKnowledge,
415
+ maxComponents: Number.parseInt(options.maxComponents || '30', 10),
416
+ interactive: false,
417
+ });
418
+
419
+ console.log(`\nDrill completed: ${plan.tests.length} components`);
420
+ console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
421
+ console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
422
+
423
+ await explorBot.stop();
424
+ await showStatsAndExit(0);
425
+ } catch (error) {
426
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
427
+ await showStatsAndExit(1);
428
+ }
429
+ });
430
+
431
+ program
432
+ .command('context <url>')
433
+ .description('Print what agents see on a page: headings, knowledge, experience, elements')
434
+ .option('-p, --path <path>', 'Working directory path')
435
+ .option('-c, --config <path>', 'Path to configuration file')
436
+ .option('--session [file]', 'Save/restore browser session from file')
437
+ .option('--full', 'Include HTML and all data')
438
+ .option('--compact', 'Compact view with summaries')
439
+ .option('--attached', 'Only auto-attached sections (default)')
440
+ .option('--visual', 'Annotate elements on screenshot and print screenshot path')
441
+ .option('--screenshot', 'Alias for --visual')
442
+ .action(async (url, options) => {
443
+ try {
444
+ const mainOptions: ExplorBotOptions = {
445
+ path: options.path,
446
+ config: options.config,
447
+ headless: true,
448
+ session: options.session,
449
+ };
450
+
451
+ const explorBot = new ExplorBot(mainOptions);
452
+ await explorBot.start();
453
+
454
+ await explorBot.visit(url);
455
+
456
+ const { ContextCommand } = await import('../src/commands/context-command.js');
457
+ const argParts: string[] = [];
458
+ if (options.full) argParts.push('--full');
459
+ else if (options.compact) argParts.push('--compact');
460
+ else argParts.push('--attached');
461
+ if (options.visual || options.screenshot) argParts.push('--visual');
462
+ await new ContextCommand(explorBot).execute(argParts.join(' '));
285
463
 
286
464
  await explorBot.stop();
287
465
  await showStatsAndExit(0);
@@ -291,7 +469,50 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
291
469
  }
292
470
  });
293
471
 
294
- addCommonOptions(program.command('plan:load <planfile> [index]').description('Load a plan file and display its tests. Pass index to see test details.')).action(async (planfile: string, index: string | undefined) => {
472
+ addCommonOptions(program.command('shell <url> <command>').description('Run one CodeceptJS command on a page')).action(async (url, command, options) => {
473
+ try {
474
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
475
+ await explorBot.start();
476
+ await explorBot.visit(url);
477
+
478
+ const action = explorBot.getExplorer().action();
479
+ await action.execute(command);
480
+
481
+ log('Command executed successfully');
482
+ const state = explorBot.stateManager().getCurrentState();
483
+ if (state) log(`URL: ${state.url}`);
484
+
485
+ await explorBot.stop();
486
+ await showStatsAndExit(0);
487
+ } catch (error) {
488
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
489
+ await showStatsAndExit(1);
490
+ }
491
+ });
492
+
493
+ program.commandsGroup(GROUPS.results);
494
+
495
+ program
496
+ .command('plans [plan]')
497
+ .description('List saved plans, or the tests of one plan')
498
+ .option('-p, --path <path>', 'Working directory path')
499
+ .option('-c, --config <path>', 'Path to configuration file')
500
+ .action(async (plan, options) => {
501
+ try {
502
+ await ConfigParser.getInstance().loadConfig({
503
+ config: options.config,
504
+ path: options.path || process.cwd(),
505
+ });
506
+ const explorBot = new ExplorBot({ path: options.path });
507
+ const { PlansCommand } = await import('../src/commands/plans-command.js');
508
+ await new PlansCommand(explorBot).execute(plan || '');
509
+ } catch (error) {
510
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
511
+ process.exit(1);
512
+ }
513
+ });
514
+
515
+ addCommonOptions(program.command('plan:load <planfile> [index]').description('Show the tests of a plan file, or one test in detail')).action(async (planfile: string, index: string | undefined) => {
295
516
  try {
296
517
  const plan = Plan.loadFromFile(planfile);
297
518
  if (!plan?.filePath) {
@@ -357,59 +578,12 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
357
578
  }
358
579
  });
359
580
 
360
- addCommonOptions(program.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1,3, 1-5, *, all').option('--grep <pattern>', 'Run tests matching pattern').option('--from-plan <file>', 'Load plan file when the first argument is a test index')).action(
361
- async (planfile, index, options) => {
362
- try {
363
- let planfileArg = planfile;
364
- let indexArg = index;
365
- if (options.fromPlan) {
366
- planfileArg = options.fromPlan;
367
- indexArg = planfile;
368
- }
369
-
370
- const peeked = Plan.loadFromFile(planfileArg);
371
-
372
- const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
373
- await explorBot.start();
374
-
375
- let plan = peeked;
376
- if (plan) explorBot.setCurrentPlan(plan);
377
- if (!plan) plan = explorBot.loadPlan(planfileArg);
378
- const pending = plan.getPendingTests();
379
- log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
380
-
381
- const startUrl = plan.startUrl;
382
- if (!startUrl) {
383
- throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
384
- }
385
-
386
- log(`Navigating to ${startUrl}`);
387
- await explorBot.visit(startUrl);
388
-
389
- let args = '*';
390
- if (indexArg) args = indexArg;
391
- else if (options.grep) args = options.grep;
392
-
393
- const { TestCommand } = await import('../src/commands/test-command.js');
394
- const cmd = new TestCommand(explorBot);
395
- await cmd.execute(args);
396
-
397
- await explorBot.printSessionAnalysis();
398
- await explorBot.stop();
399
- await showStatsAndExit(0);
400
- } catch (error) {
401
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
402
- await showStatsAndExit(1);
403
- }
404
- }
405
- );
406
-
407
- program
408
- .command('runs [file]')
409
- .description('List generated test files, or show steps for a specific file')
410
- .option('-p, --path <path>', 'Working directory path')
411
- .option('-c, --config <path>', 'Path to configuration file')
412
- .action(async (file, options) => {
581
+ program
582
+ .command('runs [file]')
583
+ .description('List generated test files, or the steps of one file')
584
+ .option('-p, --path <path>', 'Working directory path')
585
+ .option('-c, --config <path>', 'Path to configuration file')
586
+ .action(async (file, options) => {
413
587
  try {
414
588
  await ConfigParser.getInstance().loadConfig({
415
589
  config: options.config,
@@ -425,19 +599,27 @@ program
425
599
  });
426
600
 
427
601
  program
428
- .command('plans [plan]')
429
- .description('List saved plans, or show tests for a specific plan')
602
+ .command('experience [filter] [index]')
603
+ .description('List stored experience by URL; pass a tag (A.1) or index to expand one')
430
604
  .option('-p, --path <path>', 'Working directory path')
431
605
  .option('-c, --config <path>', 'Path to configuration file')
432
- .action(async (plan, options) => {
606
+ .option('--recent', 'Only files modified within the last 30 days')
607
+ .option('--old', 'Only files modified more than 30 days ago')
608
+ .action(async (filter, index, options) => {
433
609
  try {
434
610
  await ConfigParser.getInstance().loadConfig({
435
611
  config: options.config,
436
612
  path: options.path || process.cwd(),
437
613
  });
614
+ const { ExperienceCommand } = await import('../src/commands/experience-command.js');
438
615
  const explorBot = new ExplorBot({ path: options.path });
439
- const { PlansCommand } = await import('../src/commands/plans-command.js');
440
- await new PlansCommand(explorBot).execute(plan || '');
616
+ const command = new ExperienceCommand(explorBot);
617
+ const flags: string[] = [];
618
+ if (options.recent) flags.push('--recent');
619
+ if (options.old) flags.push('--old');
620
+ const args = [...flags, filter, index].filter(Boolean).join(' ');
621
+ await command.execute(args);
622
+ command.printSuggestions();
441
623
  } catch (error) {
442
624
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
443
625
  process.exit(1);
@@ -445,169 +627,29 @@ program
445
627
  });
446
628
 
447
629
  program
448
- .command('sites')
449
- .description('List sites registered in the global installation')
450
- .action(async () => {
451
- const { SitesCommand } = await import('../src/commands/sites-command.js');
452
- await new SitesCommand(new ExplorBot()).execute('');
453
- });
454
-
455
- addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
456
- try {
457
- const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
458
- await explorBot.start();
459
- const { RerunCommand } = await import('../src/commands/rerun-command.js');
460
- const cmd = new RerunCommand(explorBot);
461
- const args = index ? `${filename} ${index}` : filename;
462
- await cmd.execute(args);
463
- await explorBot.stop();
464
- await showStatsAndExit(0);
465
- } catch (error) {
466
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
467
- await showStatsAndExit(1);
468
- }
469
- });
470
-
471
- addCommonOptions(
472
- program
473
- .command('freesail [startUrl]')
474
- .description('Continuously explore and navigate to new pages autonomously')
475
- .option('--deep', 'Depth-first: prioritize newly discovered pages')
476
- .option('--shallow', 'Breadth-first: pick globally least-visited page')
477
- .option('--scope <prefix>', 'Restrict navigation to URL prefix')
478
- .option('--max-tests <count>', 'Maximum number of tests to run')
479
- .option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
480
- .option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')
481
- ).action(async (startUrl, options) => {
482
- const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
483
- await explorBot.start();
484
- const args = [options.deep && '--deep', options.shallow && '--shallow', options.scope && `--scope ${options.scope}`, options.maxTests && `--max-tests ${options.maxTests}`, options.maxDuration && `--max-duration ${options.maxDuration}`, options.urlFilter && `--url-filter "${options.urlFilter}"`]
485
- .filter(Boolean)
486
- .join(' ');
487
- const { FreesailCommand } = await import('../src/commands/freesail-command.js');
488
- const cmd = new FreesailCommand(explorBot);
489
- try {
490
- await cmd.execute(args);
491
- } catch (error) {
492
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
493
- printPageContext(explorBot);
494
- await showStatsAndExit(1);
495
- }
496
- });
497
-
498
- program
499
- .command('config [url]')
500
- .description('Show models, config file and paths used by this run')
501
- .option('-c, --config <path>', 'Path to configuration file')
630
+ .command('knows [url]')
631
+ .description('List knowledge URLs, or the knowledge matching a URL')
502
632
  .option('-p, --path <path>', 'Working directory path')
503
- .option('--json', 'Print the resolved config as JSON')
504
633
  .action(async (url, options) => {
505
- setQuietMode(!isVerboseMode());
506
- const { ConfigCommand } = await import('../src/commands/config-command.js');
507
634
  try {
508
- console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
509
- } catch (error) {
510
- console.error(error instanceof Error ? error.message : 'Unknown error');
511
- process.exit(1);
512
- }
513
- });
514
-
515
- RecommendedModelsCommand.register(program);
516
-
517
- program
518
- .command('init')
519
- .description('Initialize configuration for a project or for this machine')
520
- .option('-c, --config-path <path>', 'Path for the config file')
521
- .option('-f, --force', 'Overwrite existing config file')
522
- .option('-p, --path <path>', 'Working directory for initialization')
523
- .option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
524
- .option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
525
- .option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
526
- .option('--url <url>', 'Web application URL written to web.url in the local config')
527
- .action(async (options) => {
528
- try {
529
- const { runInit } = await import('../src/commands/init-command.js');
530
- await runInit({
531
- configPath: options.configPath,
532
- force: options.force,
533
- path: options.path,
534
- global: options.global,
535
- provider: options.provider,
536
- apiKey: options.apiKey,
537
- url: options.url,
635
+ await ConfigParser.getInstance().loadConfig({
636
+ path: options.path || process.cwd(),
538
637
  });
638
+ const { KnowsCommand } = await import('../src/commands/knows-command.js');
639
+ const explorBot = new ExplorBot({ path: options.path });
640
+ const command = new KnowsCommand(explorBot);
641
+ await command.execute(url || '');
539
642
  } catch (error) {
540
643
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
541
644
  process.exit(1);
542
645
  }
543
646
  });
544
647
 
545
- program
546
- .command('clean [target]')
547
- .description('Clean files: states, research, plans, tests, experiences, output (default: output + experiences)')
548
- .option('-p, --path <path>', 'Custom path to clean')
549
- .action(async (target, options) => {
550
- const customPath = options.path;
551
- const originalCwd = process.cwd();
552
- const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
553
-
554
- const targets: Record<string, { description: string; dir: string }> = {
555
- states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
556
- research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
557
- plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
558
- tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
559
- experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
560
- output: { description: 'all output files', dir: path.join(basePath, 'output') },
561
- };
562
-
563
- function cleanDirectoryContents(dirPath: string): number {
564
- let count = 0;
565
- for (const item of fs.readdirSync(dirPath)) {
566
- const itemPath = path.join(dirPath, item);
567
- if (fs.statSync(itemPath).isDirectory()) {
568
- count += cleanDirectoryContents(itemPath);
569
- fs.rmSync(itemPath, { recursive: true });
570
- } else {
571
- fs.unlinkSync(itemPath);
572
- count++;
573
- }
574
- }
575
- return count;
576
- }
577
-
578
- function cleanTarget(name: string): void {
579
- const t = targets[name];
580
- if (!fs.existsSync(t.dir)) {
581
- console.log(`${name}: nothing to clean (${t.dir} not found)`);
582
- return;
583
- }
584
- const count = cleanDirectoryContents(t.dir);
585
- console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
586
- }
587
-
588
- try {
589
- if (target && !targets[target]) {
590
- console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
591
- process.exit(1);
592
- }
593
-
594
- if (!target) {
595
- cleanTarget('output');
596
- cleanTarget('experiences');
597
- } else {
598
- cleanTarget(target);
599
- }
600
-
601
- console.log('Cleanup completed successfully!');
602
- } catch (error) {
603
- console.error('Failed to clean:', error);
604
- process.exit(1);
605
- }
606
- });
648
+ program.commandsGroup(GROUPS.knowledge);
607
649
 
608
650
  program
609
651
  .command('learn [url] [description]')
610
- .description('Add knowledge for URLs')
652
+ .description('Add knowledge for a URL')
611
653
  .option('-p, --path <path>', 'Working directory path')
612
654
  .option('--replace', 'Replace existing knowledge for this URL instead of appending')
613
655
  .action(async (url, description, options) => {
@@ -637,28 +679,9 @@ program
637
679
  }
638
680
  });
639
681
 
640
- program
641
- .command('knows [url]')
642
- .description('List all knowledge URLs or show matching knowledge for a URL')
643
- .option('-p, --path <path>', 'Working directory path')
644
- .action(async (url, options) => {
645
- try {
646
- await ConfigParser.getInstance().loadConfig({
647
- path: options.path || process.cwd(),
648
- });
649
- const { KnowsCommand } = await import('../src/commands/knows-command.js');
650
- const explorBot = new ExplorBot({ path: options.path });
651
- const command = new KnowsCommand(explorBot);
652
- await command.execute(url || '');
653
- } catch (error) {
654
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
655
- process.exit(1);
656
- }
657
- });
658
-
659
682
  program
660
683
  .command('compact [target]')
661
- .description('Compact stored experience files; pass filename or URL substring to limit scope')
684
+ .description('Compact experience files; pass a filename or URL substring to narrow')
662
685
  .option('--dry-run', 'Preview without running AI or writing files')
663
686
  .option('--no-merge', 'Skip the cross-URL merge step when compacting all')
664
687
  .option('-p, --path <path>', 'Working directory path')
@@ -687,162 +710,112 @@ program
687
710
  });
688
711
 
689
712
  program
690
- .command('experience [filter] [index]')
691
- .description('List stored experiences grouped by URL; pass URL substring to filter; pass tag (A.1) or index to expand a section')
713
+ .command('add-rule [agent] [name]')
714
+ .description('Create a rule file for an agent')
715
+ .option('--url <pattern>', 'URL pattern for this rule')
692
716
  .option('-p, --path <path>', 'Working directory path')
693
- .option('-c, --config <path>', 'Path to configuration file')
694
- .option('--recent', 'Only files modified within the last 30 days')
695
- .option('--old', 'Only files modified more than 30 days ago')
696
- .action(async (filter, index, options) => {
697
- try {
698
- await ConfigParser.getInstance().loadConfig({
699
- config: options.config,
700
- path: options.path || process.cwd(),
717
+ .action(async (agent, name, options) => {
718
+ if (options.path) process.chdir(path.resolve(options.path));
719
+
720
+ if (agent && name) {
721
+ const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
722
+ const result = AddRuleCommand.createRuleFile(agent, name, {
723
+ urlPattern: options.url,
701
724
  });
702
- const { ExperienceCommand } = await import('../src/commands/experience-command.js');
703
- const explorBot = new ExplorBot({ path: options.path });
704
- const command = new ExperienceCommand(explorBot);
705
- const flags: string[] = [];
706
- if (options.recent) flags.push('--recent');
707
- if (options.old) flags.push('--old');
708
- const args = [...flags, filter, index].filter(Boolean).join(' ');
709
- await command.execute(args);
710
- command.printSuggestions();
725
+ process.exit(result ? 0 : 1);
726
+ }
727
+
728
+ const AddRule = (await import('../src/components/AddRule.js')).default;
729
+ render(
730
+ React.createElement(AddRule, {
731
+ initialAgent: agent || '',
732
+ initialName: name || '',
733
+ }),
734
+ {
735
+ exitOnCtrlC: false,
736
+ patchConsole: false,
737
+ }
738
+ );
739
+ });
740
+
741
+ program
742
+ .command('extract-rules <agent>')
743
+ .description("Copy an agent's built-in rules and planning styles out to customize")
744
+ .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
745
+ .action(async (agent, options) => {
746
+ try {
747
+ const { RulesLoader } = await import('../src/utils/rules-loader.js');
748
+ const targetDir = options.dir || path.resolve(`./rules/${agent}`);
749
+ const extracted = RulesLoader.extractRules(agent, targetDir);
750
+ if (extracted.length === 0) {
751
+ console.log('All rule files already exist in target directory.');
752
+ } else {
753
+ console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
754
+ }
711
755
  } catch (error) {
712
756
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
713
757
  process.exit(1);
714
758
  }
715
759
  });
716
760
 
717
- addCommonOptions(program.command('research <url>').description('Research a page and print UI analysis').option('--data', 'Include data extraction in research').option('--deep', 'Enable deep analysis (expand hidden elements)').option('--no-fix', 'Skip locator fix cycle (for debugging)')).action(
718
- async (url, options) => {
719
- try {
720
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
721
- await explorBot.start();
722
-
723
- const argParts: string[] = [url];
724
- if (options.data) argParts.push('--data');
725
- if (options.deep) argParts.push('--deep');
726
- if (options.fix === false) argParts.push('--no-fix');
727
-
728
- const { ResearchCommand } = await import('../src/commands/research-command.js');
729
- await new ResearchCommand(explorBot).execute(argParts.join(' '));
761
+ program.commandsGroup(GROUPS.setup);
730
762
 
731
- await explorBot.stop();
732
- await showStatsAndExit(0);
763
+ program
764
+ .command('init')
765
+ .description('Initialize configuration for a project or for this machine')
766
+ .option('-c, --config-path <path>', 'Path for the config file')
767
+ .option('-f, --force', 'Overwrite existing config file')
768
+ .option('-p, --path <path>', 'Working directory for initialization')
769
+ .option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
770
+ .option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
771
+ .option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
772
+ .option('--url <url>', 'Web application URL written to web.url in the local config')
773
+ .action(async (options) => {
774
+ try {
775
+ const { runInit } = await import('../src/commands/init-command.js');
776
+ await runInit({
777
+ configPath: options.configPath,
778
+ force: options.force,
779
+ path: options.path,
780
+ global: options.global,
781
+ provider: options.provider,
782
+ apiKey: options.apiKey,
783
+ url: options.url,
784
+ });
733
785
  } catch (error) {
734
786
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
735
- await showStatsAndExit(1);
787
+ process.exit(1);
736
788
  }
737
- }
738
- );
739
-
740
- addCommonOptions(program.command('navigate <url>').description('Navigate to a URL using the AI Navigator. Exits 0 if reachable, 1 otherwise.')).action(async (url, options) => {
741
- try {
742
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
743
- await explorBot.start();
744
-
745
- const { NavigateCommand } = await import('../src/commands/navigate-command.js');
746
- await new NavigateCommand(explorBot).execute(url);
747
-
748
- await explorBot.stop();
749
- await showStatsAndExit(0);
750
- } catch (error) {
751
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
752
- await showStatsAndExit(1);
753
- }
754
- });
755
-
756
- addCommonOptions(
757
- program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--save-knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')
758
- ).action(async (url, options) => {
759
- try {
760
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
761
- await explorBot.start();
762
-
763
- await explorBot.visit(url);
764
-
765
- const plan = await explorBot.agentDriller().drill({
766
- knowledgePath: options.saveKnowledge,
767
- maxComponents: Number.parseInt(options.maxComponents || '30', 10),
768
- interactive: false,
769
- });
770
-
771
- console.log(`\nDrill completed: ${plan.tests.length} components`);
772
- console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
773
- console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
774
-
775
- await explorBot.stop();
776
- await showStatsAndExit(0);
777
- } catch (error) {
778
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
779
- await showStatsAndExit(1);
780
- }
781
- });
789
+ });
782
790
 
783
791
  program
784
- .command('context <url>')
785
- .description('Print page context (URL, headings, knowledge, experience, interactive elements)')
786
- .option('-p, --path <path>', 'Working directory path')
792
+ .command('config [url]')
793
+ .description('Show models, config file and paths used by this run')
787
794
  .option('-c, --config <path>', 'Path to configuration file')
788
- .option('--session [file]', 'Save/restore browser session from file')
789
- .option('--full', 'Include HTML and all data')
790
- .option('--compact', 'Compact view with summaries')
791
- .option('--attached', 'Only auto-attached sections (default)')
792
- .option('--visual', 'Annotate elements on screenshot and print screenshot path')
793
- .option('--screenshot', 'Alias for --visual')
795
+ .option('-p, --path <path>', 'Working directory path')
796
+ .option('--json', 'Print the resolved config as JSON')
794
797
  .action(async (url, options) => {
798
+ setQuietMode(!isVerboseMode());
799
+ const { ConfigCommand } = await import('../src/commands/config-command.js');
795
800
  try {
796
- const mainOptions: ExplorBotOptions = {
797
- path: options.path,
798
- config: options.config,
799
- headless: true,
800
- session: options.session,
801
- };
802
-
803
- const explorBot = new ExplorBot(mainOptions);
804
- await explorBot.start();
805
-
806
- await explorBot.visit(url);
807
-
808
- const { ContextCommand } = await import('../src/commands/context-command.js');
809
- const argParts: string[] = [];
810
- if (options.full) argParts.push('--full');
811
- else if (options.compact) argParts.push('--compact');
812
- else argParts.push('--attached');
813
- if (options.visual || options.screenshot) argParts.push('--visual');
814
- await new ContextCommand(explorBot).execute(argParts.join(' '));
815
-
816
- await explorBot.stop();
817
- await showStatsAndExit(0);
801
+ console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
818
802
  } catch (error) {
819
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
820
- await showStatsAndExit(1);
803
+ console.error(error instanceof Error ? error.message : 'Unknown error');
804
+ process.exit(1);
821
805
  }
822
806
  });
823
807
 
824
- addCommonOptions(program.command('shell <url> <command>').description('Execute a CodeceptJS command on a page and exit')).action(async (url, command, options) => {
825
- try {
826
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
827
- await explorBot.start();
828
- await explorBot.visit(url);
829
-
830
- const action = explorBot.getExplorer().action();
831
- await action.execute(command);
832
-
833
- log('Command executed successfully');
834
- const state = explorBot.stateManager().getCurrentState();
835
- if (state) log(`URL: ${state.url}`);
808
+ RecommendedModelsCommand.register(program);
836
809
 
837
- await explorBot.stop();
838
- await showStatsAndExit(0);
839
- } catch (error) {
840
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
841
- await showStatsAndExit(1);
842
- }
843
- });
810
+ program
811
+ .command('sites')
812
+ .description('List sites registered in the global installation')
813
+ .action(async () => {
814
+ const { SitesCommand } = await import('../src/commands/sites-command.js');
815
+ await new SitesCommand(new ExplorBot()).execute('');
816
+ });
844
817
 
845
- const browserCmd = program.command('browser').description('Manage persistent browser server');
818
+ const browserCmd = program.command('browser').description('Start, stop or check a persistent browser server');
846
819
 
847
820
  browserCmd
848
821
  .command('start')
@@ -921,80 +894,119 @@ browserCmd
921
894
  });
922
895
 
923
896
  program
924
- .command('extract-rules <agent>')
925
- .description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
926
- .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
927
- .action(async (agent, options) => {
897
+ .command('clean [target]')
898
+ .description('Delete states, research, plans, tests, experiences or output (default: output + experiences)')
899
+ .option('-p, --path <path>', 'Custom path to clean')
900
+ .action(async (target, options) => {
901
+ const customPath = options.path;
902
+ const originalCwd = process.cwd();
903
+ const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
904
+
905
+ const targets: Record<string, { description: string; dir: string }> = {
906
+ states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
907
+ research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
908
+ plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
909
+ tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
910
+ experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
911
+ output: { description: 'all output files', dir: path.join(basePath, 'output') },
912
+ };
913
+
914
+ function cleanDirectoryContents(dirPath: string): number {
915
+ let count = 0;
916
+ for (const item of fs.readdirSync(dirPath)) {
917
+ const itemPath = path.join(dirPath, item);
918
+ if (fs.statSync(itemPath).isDirectory()) {
919
+ count += cleanDirectoryContents(itemPath);
920
+ fs.rmSync(itemPath, { recursive: true });
921
+ } else {
922
+ fs.unlinkSync(itemPath);
923
+ count++;
924
+ }
925
+ }
926
+ return count;
927
+ }
928
+
929
+ function cleanTarget(name: string): void {
930
+ const t = targets[name];
931
+ if (!fs.existsSync(t.dir)) {
932
+ console.log(`${name}: nothing to clean (${t.dir} not found)`);
933
+ return;
934
+ }
935
+ const count = cleanDirectoryContents(t.dir);
936
+ console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
937
+ }
938
+
928
939
  try {
929
- const { RulesLoader } = await import('../src/utils/rules-loader.js');
930
- const targetDir = options.dir || path.resolve(`./rules/${agent}`);
931
- const extracted = RulesLoader.extractRules(agent, targetDir);
932
- if (extracted.length === 0) {
933
- console.log('All rule files already exist in target directory.');
940
+ if (target && !targets[target]) {
941
+ console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
942
+ process.exit(1);
943
+ }
944
+
945
+ if (!target) {
946
+ cleanTarget('output');
947
+ cleanTarget('experiences');
934
948
  } else {
935
- console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
949
+ cleanTarget(target);
936
950
  }
951
+
952
+ console.log('Cleanup completed successfully!');
937
953
  } catch (error) {
938
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
954
+ console.error('Failed to clean:', error);
939
955
  process.exit(1);
940
956
  }
941
957
  });
942
958
 
943
- program
944
- .command('add-rule [agent] [name]')
945
- .description('Create a rule file for an agent')
946
- .option('--url <pattern>', 'URL pattern for this rule')
947
- .option('-p, --path <path>', 'Working directory path')
948
- .action(async (agent, name, options) => {
949
- if (options.path) process.chdir(path.resolve(options.path));
950
-
951
- if (agent && name) {
952
- const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
953
- const result = AddRuleCommand.createRuleFile(agent, name, {
954
- urlPattern: options.url,
955
- });
956
- process.exit(result ? 0 : 1);
957
- }
958
-
959
- const AddRule = (await import('../src/components/AddRule.js')).default;
960
- render(
961
- React.createElement(AddRule, {
962
- initialAgent: agent || '',
963
- initialName: name || '',
964
- }),
965
- {
966
- exitOnCtrlC: false,
967
- patchConsole: false,
968
- }
969
- );
970
- });
959
+ program.commandsGroup(GROUPS.tools);
971
960
 
972
- import { createApiCommands } from '../boat/api-tester/src/cli.ts';
973
- import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
974
- import { createPrimaCommands } from '../boat/prima/src/cli.ts';
975
961
  program.addCommand(createApiCommands('api'));
976
962
  program.addCommand(createDocsCommands('docs'));
977
963
  program.addCommand(createPrimaCommands('prima'));
978
964
 
979
965
  const envHelp = () => {
980
966
  const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
981
- const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
967
+ const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${chalk.green(v.name.padEnd(width))} ${v.description}`).join('\n');
982
968
 
983
969
  return `
984
- Environment variables (config-free one-liner mode):
970
+ ${chalk.yellow.bold('Environment variables:')}
985
971
  Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
986
972
 
987
973
  ${rows}
988
974
 
989
975
  Providers: ${Object.keys(PROVIDERS).join(', ')}
990
- Example:
991
- EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
992
- ${cli} explore /login --max-tests 3
976
+
977
+ ${chalk.yellow.bold('Example:')}
978
+ EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
979
+ ${cli} explore /login --max-tests 3
993
980
  `;
994
981
  };
995
982
 
996
- program.addHelpText('after', `\nFor agents and tools:\n ${cli} help-json [command...] the same definitions as JSON — commands, arguments, options, defaults\n`);
997
- program.addHelpText('after', envHelp);
983
+ program
984
+ .command('env')
985
+ .description('Show environment variables for running without a config file')
986
+ .helpGroup(GROUPS.setup)
987
+ .addHelpText('after', envHelp)
988
+ .action(() => console.log(envHelp()));
989
+
990
+ const examples = [
991
+ [`${cli} start`, 'interactive TUI, drive it with /commands'],
992
+ [`${cli} explore /login`, 'invent and run tests for one page'],
993
+ [`${cli} freesail`, 'roam the whole app on its own'],
994
+ [`${cli} plan /users`, 'write a test plan, then run it with test'],
995
+ ];
996
+ const exampleWidth = Math.max(...examples.map(([command]) => command.length));
997
+
998
+ program.addHelpText(
999
+ 'after',
1000
+ `
1001
+ ${chalk.yellow.bold('Examples:')}
1002
+ ${examples.map(([command, note]) => ` ${chalk.cyan(command.padEnd(exampleWidth))} ${chalk.dim(note)}`).join('\n')}
1003
+
1004
+ ${chalk.dim(`Environment variables: ${cli} help env · JSON definitions for agents: ${cli} help-json [command...]`)}
1005
+ `
1006
+ );
1007
+
1008
+ program.commandsGroup(GROUPS.help);
1009
+ program.helpCommand(true);
998
1010
  HelpJsonCommand.register(program);
999
1011
 
1000
1012
  program.parse();