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