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.js";
10
+ import { createDocsCommands } from "../boat/doc-collector/src/cli.js";
11
+ import { createPrimaCommands } from "../boat/prima/src/cli.js";
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,18 +25,46 @@ import { getCliName } from "../src/utils/cli-name.js";
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 { printNextSteps, relativeToCwd } from "../src/utils/next-steps.js";
26
28
  import { readCliSitemap } from "../src/utils/sitemap.js";
27
29
  import { UrlFilter } from "../src/utils/url-filter.js";
28
30
  const program = new Command();
29
31
  const cli = getCliName();
30
32
  const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
31
33
  const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
32
- program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
33
- wsOption.register(program);
34
- knowledgeOption.register(program);
35
- authOption.register(program);
36
- decisionModelOption.register(program);
34
+ const GROUPS = {
35
+ explore: 'Explore:',
36
+ steps: 'Steps:',
37
+ results: 'Results:',
38
+ knowledge: 'Knowledge & rules:',
39
+ setup: 'Setup:',
40
+ tools: 'More tools:',
41
+ help: 'Help:',
42
+ };
43
+ program
44
+ .name(cli)
45
+ .description('AI-powered exploratory testing for web apps')
46
+ .version(pkgVersion, '-V, --version')
47
+ .configureHelp({
48
+ styleTitle: (str) => chalk.yellow.bold(str),
49
+ styleCommandText: (str) => chalk.bold(str),
50
+ styleSubcommandText: (str) => chalk.cyan(str),
51
+ styleOptionText: (str) => chalk.green(str),
52
+ styleArgumentText: (str) => chalk.magenta(str),
53
+ showGlobalOptions: true,
54
+ visibleOptions(cmd) {
55
+ const options = Help.prototype.visibleOptions.call(this, cmd);
56
+ if (cmd.parent)
57
+ return options;
58
+ return options.filter((option) => !isGlobalOption(option.flags));
59
+ },
60
+ visibleGlobalOptions(cmd) {
61
+ return Help.prototype.visibleGlobalOptions.call(this, cmd).filter((option) => isGlobalOption(option.flags));
62
+ },
63
+ });
64
+ const globalOptions = [knowledgeOption, authOption, decisionModelOption, wsOption];
65
+ for (const option of globalOptions)
66
+ option.register(program);
67
+ const isGlobalOption = (flags) => globalOptions.some((option) => option.flags === flags);
37
68
  process.on('uncaughtException', async (error) => {
38
69
  tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
39
70
  await flushTelemetry();
@@ -47,6 +78,7 @@ if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima') && !prin
47
78
  console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
48
79
  }
49
80
  function buildExplorBotOptions(from, options) {
81
+ const spec = Array.isArray(options.spec) ? (options.spec.length > 0 ? options.spec : undefined) : options.spec;
50
82
  return {
51
83
  from,
52
84
  baseUrl: options.baseUrl,
@@ -57,7 +89,7 @@ function buildExplorBotOptions(from, options) {
57
89
  headless: options.headless,
58
90
  incognito: options.incognito,
59
91
  session: options.session,
60
- applicationSpec: options.spec,
92
+ applicationSpec: spec,
61
93
  };
62
94
  }
63
95
  function addCommonOptions(cmd) {
@@ -70,7 +102,7 @@ function addCommonOptions(cmd) {
70
102
  .option('-s, --show', 'Show browser window')
71
103
  .option('--headless', 'Run browser in headless mode')
72
104
  .option('--incognito', 'Run without recording experiences')
73
- .option('--spec <path>', 'Use a Docbot application spec directory or index.md')
105
+ .option('--spec <path>', 'Use a Docbot application spec directory or index.md (repeatable)', (val, acc) => [...(acc || []), val])
74
106
  .option('--session [file]', 'Save/restore browser session from file');
75
107
  }
76
108
  async function startTUI(explorBot) {
@@ -132,7 +164,8 @@ async function showStatsAndExit(code) {
132
164
  await flushTelemetry();
133
165
  process.exit(code);
134
166
  }
135
- addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
167
+ program.commandsGroup(GROUPS.explore);
168
+ addCommonOptions(program.command('start [path]').description('Open the interactive TUI and explore with commands')).action(async (startPath, options) => {
136
169
  setPreserveConsoleLogs(false);
137
170
  const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
138
171
  await explorBot.start();
@@ -140,7 +173,7 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
140
173
  });
141
174
  addCommonOptions(program
142
175
  .command('explore [path]')
143
- .description('Explore a page autonomously and run invented scenarios')
176
+ .description('Invent test scenarios for a page and run them')
144
177
  .option('--sitemap <source>', 'Explore every page listed in a sitemap: file path, URL, or - for stdin (a piped stdin is read automatically)')
145
178
  .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"')
146
179
  .option('--max-tests <count>', 'Maximum number of tests to run')
@@ -185,72 +218,227 @@ addCommonOptions(program
185
218
  await showStatsAndExit(1);
186
219
  }
187
220
  });
188
- addCommonOptions(program.command('plan <path>').description('Generate test plan for a page and exit'))
221
+ addCommonOptions(program
222
+ .command('freesail [startUrl]')
223
+ .description('Roam the app on its own, moving on to new pages')
224
+ .option('--deep', 'Depth-first: prioritize newly discovered pages')
225
+ .option('--shallow', 'Breadth-first: pick globally least-visited page')
226
+ .option('--scope <prefix>', 'Restrict navigation to URL prefix')
227
+ .option('--max-tests <count>', 'Maximum number of tests to run')
228
+ .option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
229
+ .option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')).action(async (startUrl, options) => {
230
+ const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
231
+ await explorBot.start();
232
+ 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}"`]
233
+ .filter(Boolean)
234
+ .join(' ');
235
+ const { FreesailCommand } = await import('../src/commands/freesail-command.js');
236
+ const cmd = new FreesailCommand(explorBot);
237
+ try {
238
+ await cmd.execute(args);
239
+ }
240
+ catch (error) {
241
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
242
+ printPageContext(explorBot);
243
+ await showStatsAndExit(1);
244
+ }
245
+ });
246
+ program.commandsGroup(GROUPS.steps);
247
+ 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(async (url, options) => {
248
+ try {
249
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
250
+ await explorBot.start();
251
+ const argParts = [url];
252
+ if (options.data)
253
+ argParts.push('--data');
254
+ if (options.deep)
255
+ argParts.push('--deep');
256
+ if (options.fix === false)
257
+ argParts.push('--no-fix');
258
+ const { ResearchCommand } = await import('../src/commands/research-command.js');
259
+ await new ResearchCommand(explorBot).execute(argParts.join(' '));
260
+ await explorBot.stop();
261
+ await showStatsAndExit(0);
262
+ }
263
+ catch (error) {
264
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
265
+ await showStatsAndExit(1);
266
+ }
267
+ });
268
+ addCommonOptions(program.command('plan <path>').description('Write a test plan for a page'))
189
269
  .option('-a, --append', 'Add tests to existing plan file')
190
270
  .option('--style <style>', 'Planning style: normal, curious, psycho')
191
271
  .option('--focus <feature>', 'Focus area for test planning')
272
+ .option('--tests <count>', 'Exact number of tests to generate')
273
+ .option('--min-tests <count>', 'Minimum number of tests to generate')
274
+ .option('--max-tests <count>', 'Maximum number of tests to generate')
192
275
  .action(async (planPath, options) => {
193
276
  try {
194
277
  const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
195
278
  await explorBot.start();
196
279
  await explorBot.visit(planPath);
197
- if (options.append) {
198
- const planFilename = explorBot.generatePlanFilename();
199
- const existingPlanPath = path.join(explorBot.getPlansDir(), planFilename);
200
- if (fs.existsSync(existingPlanPath)) {
201
- explorBot.loadPlan(existingPlanPath);
202
- }
203
- }
204
- await explorBot.plan(options.focus || undefined, {
205
- fresh: !options.append,
206
- style: options.style,
207
- });
208
- const plan = explorBot.getCurrentPlan();
209
- if (!plan?.tests.length) {
210
- console.error('No test scenarios generated.');
211
- await explorBot.stop();
212
- await showStatsAndExit(1);
213
- }
214
- const suite = explorBot.getSuite();
215
- if (suite && suite.automatedTestCount > 0) {
216
- const names = suite.getAutomatedTestNames();
217
- console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
218
- for (let i = 0; i < names.length; i++) {
219
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
220
- }
221
- }
222
- if (plan?.tests.length) {
223
- console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
224
- for (let i = 0; i < plan.tests.length; i++) {
225
- const t = plan.tests[i];
226
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
227
- }
228
- }
229
- const savedPath = explorBot.savePlan();
230
- const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
231
- const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
232
- const sections = [];
233
- if (savedPath) {
234
- const relPlan = relativeToCwd(savedPath);
235
- sections.push({
236
- label: 'Plan',
237
- path: savedPath,
238
- commands: [
239
- { label: 'Re-run', command: `${cli} test ${relPlan} 1${cliSuffix}` },
240
- { label: 'Run all', command: `${cli} test ${relPlan} *${cliSuffix}` },
241
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3${cliSuffix}` },
242
- ],
243
- });
280
+ const args = ['--fresh'];
281
+ if (options.append)
282
+ args[0] = '--append';
283
+ if (options.style)
284
+ args.push(`--style ${options.style}`);
285
+ if (options.focus)
286
+ args.push(`--focus "${options.focus}"`);
287
+ if (options.tests)
288
+ args.push(`--tests ${options.tests}`);
289
+ if (options.minTests)
290
+ args.push(`--min-tests ${options.minTests}`);
291
+ if (options.maxTests)
292
+ args.push(`--max-tests ${options.maxTests}`);
293
+ await new PlanCommand(explorBot).execute(args.join(' '));
294
+ await explorBot.stop();
295
+ await showStatsAndExit(0);
296
+ }
297
+ catch (error) {
298
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
299
+ await showStatsAndExit(1);
300
+ }
301
+ });
302
+ 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(async (planfile, index, options) => {
303
+ try {
304
+ let planfileArg = planfile;
305
+ let indexArg = index;
306
+ if (options.fromPlan) {
307
+ planfileArg = options.fromPlan;
308
+ indexArg = planfile;
244
309
  }
245
- const automatedFiles = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
246
- if (automatedFiles.length > 0) {
247
- const commands = automatedFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
248
- sections.push({
249
- label: `Automated tests (${automatedFiles.length})`,
250
- commands,
251
- });
310
+ const peeked = Plan.loadFromFile(planfileArg);
311
+ const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
312
+ await explorBot.start();
313
+ const plan = explorBot.loadPlan(planfileArg);
314
+ const pending = plan.getPendingTests();
315
+ log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
316
+ const startUrl = plan.startUrl;
317
+ if (!startUrl) {
318
+ throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
252
319
  }
253
- printNextSteps(sections);
320
+ log(`Navigating to ${startUrl}`);
321
+ await explorBot.visit(startUrl);
322
+ let args = '*';
323
+ if (indexArg)
324
+ args = indexArg;
325
+ else if (options.grep)
326
+ args = options.grep;
327
+ const { TestCommand } = await import('../src/commands/test-command.js');
328
+ const cmd = new TestCommand(explorBot);
329
+ await cmd.execute(args);
330
+ await explorBot.printSessionAnalysis();
331
+ await explorBot.stop();
332
+ await showStatsAndExit(0);
333
+ }
334
+ catch (error) {
335
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
336
+ await showStatsAndExit(1);
337
+ }
338
+ });
339
+ addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests, healing failures with AI')).action(async (filename, index, options) => {
340
+ try {
341
+ const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
342
+ await explorBot.start();
343
+ const { RerunCommand } = await import('../src/commands/rerun-command.js');
344
+ const cmd = new RerunCommand(explorBot);
345
+ const args = index ? `${filename} ${index}` : filename;
346
+ await cmd.execute(args);
347
+ await explorBot.stop();
348
+ await showStatsAndExit(0);
349
+ }
350
+ catch (error) {
351
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
352
+ await showStatsAndExit(1);
353
+ }
354
+ });
355
+ addCommonOptions(program.command('navigate <url>').description('Reach a URL with the AI Navigator; exits 1 if unreachable')).action(async (url, options) => {
356
+ try {
357
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
358
+ await explorBot.start();
359
+ const { NavigateCommand } = await import('../src/commands/navigate-command.js');
360
+ await new NavigateCommand(explorBot).execute(url);
361
+ await explorBot.stop();
362
+ await showStatsAndExit(0);
363
+ }
364
+ catch (error) {
365
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
366
+ await showStatsAndExit(1);
367
+ }
368
+ });
369
+ addCommonOptions(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')).action(async (url, options) => {
370
+ try {
371
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
372
+ await explorBot.start();
373
+ await explorBot.visit(url);
374
+ const plan = await explorBot.agentDriller().drill({
375
+ knowledgePath: options.saveKnowledge,
376
+ maxComponents: Number.parseInt(options.maxComponents || '30', 10),
377
+ interactive: false,
378
+ });
379
+ console.log(`\nDrill completed: ${plan.tests.length} components`);
380
+ console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
381
+ console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
382
+ await explorBot.stop();
383
+ await showStatsAndExit(0);
384
+ }
385
+ catch (error) {
386
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
387
+ await showStatsAndExit(1);
388
+ }
389
+ });
390
+ program
391
+ .command('context <url>')
392
+ .description('Print what agents see on a page: headings, knowledge, experience, elements')
393
+ .option('-p, --path <path>', 'Working directory path')
394
+ .option('-c, --config <path>', 'Path to configuration file')
395
+ .option('--session [file]', 'Save/restore browser session from file')
396
+ .option('--full', 'Include HTML and all data')
397
+ .option('--compact', 'Compact view with summaries')
398
+ .option('--attached', 'Only auto-attached sections (default)')
399
+ .option('--visual', 'Annotate elements on screenshot and print screenshot path')
400
+ .option('--screenshot', 'Alias for --visual')
401
+ .action(async (url, options) => {
402
+ try {
403
+ const mainOptions = {
404
+ path: options.path,
405
+ config: options.config,
406
+ headless: true,
407
+ session: options.session,
408
+ };
409
+ const explorBot = new ExplorBot(mainOptions);
410
+ await explorBot.start();
411
+ await explorBot.visit(url);
412
+ const { ContextCommand } = await import('../src/commands/context-command.js');
413
+ const argParts = [];
414
+ if (options.full)
415
+ argParts.push('--full');
416
+ else if (options.compact)
417
+ argParts.push('--compact');
418
+ else
419
+ argParts.push('--attached');
420
+ if (options.visual || options.screenshot)
421
+ argParts.push('--visual');
422
+ await new ContextCommand(explorBot).execute(argParts.join(' '));
423
+ await explorBot.stop();
424
+ await showStatsAndExit(0);
425
+ }
426
+ catch (error) {
427
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
428
+ await showStatsAndExit(1);
429
+ }
430
+ });
431
+ addCommonOptions(program.command('shell <url> <command>').description('Run one CodeceptJS command on a page')).action(async (url, command, options) => {
432
+ try {
433
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
434
+ await explorBot.start();
435
+ await explorBot.visit(url);
436
+ const action = explorBot.getExplorer().action();
437
+ await action.execute(command);
438
+ log('Command executed successfully');
439
+ const state = explorBot.stateManager().getCurrentState();
440
+ if (state)
441
+ log(`URL: ${state.url}`);
254
442
  await explorBot.stop();
255
443
  await showStatsAndExit(0);
256
444
  }
@@ -259,7 +447,28 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
259
447
  await showStatsAndExit(1);
260
448
  }
261
449
  });
262
- 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, index) => {
450
+ program.commandsGroup(GROUPS.results);
451
+ program
452
+ .command('plans [plan]')
453
+ .description('List saved plans, or the tests of one plan')
454
+ .option('-p, --path <path>', 'Working directory path')
455
+ .option('-c, --config <path>', 'Path to configuration file')
456
+ .action(async (plan, options) => {
457
+ try {
458
+ await ConfigParser.getInstance().loadConfig({
459
+ config: options.config,
460
+ path: options.path || process.cwd(),
461
+ });
462
+ const explorBot = new ExplorBot({ path: options.path });
463
+ const { PlansCommand } = await import('../src/commands/plans-command.js');
464
+ await new PlansCommand(explorBot).execute(plan || '');
465
+ }
466
+ catch (error) {
467
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
468
+ process.exit(1);
469
+ }
470
+ });
471
+ addCommonOptions(program.command('plan:load <planfile> [index]').description('Show the tests of a plan file, or one test in detail')).action(async (planfile, index) => {
263
472
  try {
264
473
  const plan = Plan.loadFromFile(planfile);
265
474
  if (!plan?.filePath) {
@@ -323,50 +532,9 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
323
532
  process.exit(1);
324
533
  }
325
534
  });
326
- 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(async (planfile, index, options) => {
327
- try {
328
- let planfileArg = planfile;
329
- let indexArg = index;
330
- if (options.fromPlan) {
331
- planfileArg = options.fromPlan;
332
- indexArg = planfile;
333
- }
334
- const peeked = Plan.loadFromFile(planfileArg);
335
- const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
336
- await explorBot.start();
337
- let plan = peeked;
338
- if (plan)
339
- explorBot.setCurrentPlan(plan);
340
- if (!plan)
341
- plan = explorBot.loadPlan(planfileArg);
342
- const pending = plan.getPendingTests();
343
- log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
344
- const startUrl = plan.startUrl;
345
- if (!startUrl) {
346
- throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
347
- }
348
- log(`Navigating to ${startUrl}`);
349
- await explorBot.visit(startUrl);
350
- let args = '*';
351
- if (indexArg)
352
- args = indexArg;
353
- else if (options.grep)
354
- args = options.grep;
355
- const { TestCommand } = await import('../src/commands/test-command.js');
356
- const cmd = new TestCommand(explorBot);
357
- await cmd.execute(args);
358
- await explorBot.printSessionAnalysis();
359
- await explorBot.stop();
360
- await showStatsAndExit(0);
361
- }
362
- catch (error) {
363
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
364
- await showStatsAndExit(1);
365
- }
366
- });
367
535
  program
368
536
  .command('runs [file]')
369
- .description('List generated test files, or show steps for a specific file')
537
+ .description('List generated test files, or the steps of one file')
370
538
  .option('-p, --path <path>', 'Working directory path')
371
539
  .option('-c, --config <path>', 'Path to configuration file')
372
540
  .action(async (file, options) => {
@@ -385,19 +553,29 @@ program
385
553
  }
386
554
  });
387
555
  program
388
- .command('plans [plan]')
389
- .description('List saved plans, or show tests for a specific plan')
556
+ .command('experience [filter] [index]')
557
+ .description('List stored experience by URL; pass a tag (A.1) or index to expand one')
390
558
  .option('-p, --path <path>', 'Working directory path')
391
559
  .option('-c, --config <path>', 'Path to configuration file')
392
- .action(async (plan, options) => {
560
+ .option('--recent', 'Only files modified within the last 30 days')
561
+ .option('--old', 'Only files modified more than 30 days ago')
562
+ .action(async (filter, index, options) => {
393
563
  try {
394
564
  await ConfigParser.getInstance().loadConfig({
395
565
  config: options.config,
396
566
  path: options.path || process.cwd(),
397
567
  });
568
+ const { ExperienceCommand } = await import('../src/commands/experience-command.js');
398
569
  const explorBot = new ExplorBot({ path: options.path });
399
- const { PlansCommand } = await import('../src/commands/plans-command.js');
400
- await new PlansCommand(explorBot).execute(plan || '');
570
+ const command = new ExperienceCommand(explorBot);
571
+ const flags = [];
572
+ if (options.recent)
573
+ flags.push('--recent');
574
+ if (options.old)
575
+ flags.push('--old');
576
+ const args = [...flags, filter, index].filter(Boolean).join(' ');
577
+ await command.execute(args);
578
+ command.printSuggestions();
401
579
  }
402
580
  catch (error) {
403
581
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
@@ -405,161 +583,28 @@ program
405
583
  }
406
584
  });
407
585
  program
408
- .command('sites')
409
- .description('List sites registered in the global installation')
410
- .action(async () => {
411
- const { SitesCommand } = await import('../src/commands/sites-command.js');
412
- await new SitesCommand(new ExplorBot()).execute('');
413
- });
414
- addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
415
- try {
416
- const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
417
- await explorBot.start();
418
- const { RerunCommand } = await import('../src/commands/rerun-command.js');
419
- const cmd = new RerunCommand(explorBot);
420
- const args = index ? `${filename} ${index}` : filename;
421
- await cmd.execute(args);
422
- await explorBot.stop();
423
- await showStatsAndExit(0);
424
- }
425
- catch (error) {
426
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
427
- await showStatsAndExit(1);
428
- }
429
- });
430
- addCommonOptions(program
431
- .command('freesail [startUrl]')
432
- .description('Continuously explore and navigate to new pages autonomously')
433
- .option('--deep', 'Depth-first: prioritize newly discovered pages')
434
- .option('--shallow', 'Breadth-first: pick globally least-visited page')
435
- .option('--scope <prefix>', 'Restrict navigation to URL prefix')
436
- .option('--max-tests <count>', 'Maximum number of tests to run')
437
- .option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
438
- .option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')).action(async (startUrl, options) => {
439
- const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
440
- await explorBot.start();
441
- 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}"`]
442
- .filter(Boolean)
443
- .join(' ');
444
- const { FreesailCommand } = await import('../src/commands/freesail-command.js');
445
- const cmd = new FreesailCommand(explorBot);
446
- try {
447
- await cmd.execute(args);
448
- }
449
- catch (error) {
450
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
451
- printPageContext(explorBot);
452
- await showStatsAndExit(1);
453
- }
454
- });
455
- program
456
- .command('config [url]')
457
- .description('Show models, config file and paths used by this run')
458
- .option('-c, --config <path>', 'Path to configuration file')
586
+ .command('knows [url]')
587
+ .description('List knowledge URLs, or the knowledge matching a URL')
459
588
  .option('-p, --path <path>', 'Working directory path')
460
- .option('--json', 'Print the resolved config as JSON')
461
589
  .action(async (url, options) => {
462
- setQuietMode(!isVerboseMode());
463
- const { ConfigCommand } = await import('../src/commands/config-command.js');
464
- try {
465
- console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
466
- }
467
- catch (error) {
468
- console.error(error instanceof Error ? error.message : 'Unknown error');
469
- process.exit(1);
470
- }
471
- });
472
- RecommendedModelsCommand.register(program);
473
- program
474
- .command('init')
475
- .description('Initialize configuration for a project or for this machine')
476
- .option('-c, --config-path <path>', 'Path for the config file')
477
- .option('-f, --force', 'Overwrite existing config file')
478
- .option('-p, --path <path>', 'Working directory for initialization')
479
- .option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
480
- .option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
481
- .option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
482
- .option('--url <url>', 'Web application URL written to web.url in the local config')
483
- .action(async (options) => {
484
590
  try {
485
- const { runInit } = await import('../src/commands/init-command.js');
486
- await runInit({
487
- configPath: options.configPath,
488
- force: options.force,
489
- path: options.path,
490
- global: options.global,
491
- provider: options.provider,
492
- apiKey: options.apiKey,
493
- url: options.url,
591
+ await ConfigParser.getInstance().loadConfig({
592
+ path: options.path || process.cwd(),
494
593
  });
594
+ const { KnowsCommand } = await import('../src/commands/knows-command.js');
595
+ const explorBot = new ExplorBot({ path: options.path });
596
+ const command = new KnowsCommand(explorBot);
597
+ await command.execute(url || '');
495
598
  }
496
599
  catch (error) {
497
600
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
498
601
  process.exit(1);
499
602
  }
500
603
  });
501
- program
502
- .command('clean [target]')
503
- .description('Clean files: states, research, plans, tests, experiences, output (default: output + experiences)')
504
- .option('-p, --path <path>', 'Custom path to clean')
505
- .action(async (target, options) => {
506
- const customPath = options.path;
507
- const originalCwd = process.cwd();
508
- const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
509
- const targets = {
510
- states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
511
- research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
512
- plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
513
- tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
514
- experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
515
- output: { description: 'all output files', dir: path.join(basePath, 'output') },
516
- };
517
- function cleanDirectoryContents(dirPath) {
518
- let count = 0;
519
- for (const item of fs.readdirSync(dirPath)) {
520
- const itemPath = path.join(dirPath, item);
521
- if (fs.statSync(itemPath).isDirectory()) {
522
- count += cleanDirectoryContents(itemPath);
523
- fs.rmSync(itemPath, { recursive: true });
524
- }
525
- else {
526
- fs.unlinkSync(itemPath);
527
- count++;
528
- }
529
- }
530
- return count;
531
- }
532
- function cleanTarget(name) {
533
- const t = targets[name];
534
- if (!fs.existsSync(t.dir)) {
535
- console.log(`${name}: nothing to clean (${t.dir} not found)`);
536
- return;
537
- }
538
- const count = cleanDirectoryContents(t.dir);
539
- console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
540
- }
541
- try {
542
- if (target && !targets[target]) {
543
- console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
544
- process.exit(1);
545
- }
546
- if (!target) {
547
- cleanTarget('output');
548
- cleanTarget('experiences');
549
- }
550
- else {
551
- cleanTarget(target);
552
- }
553
- console.log('Cleanup completed successfully!');
554
- }
555
- catch (error) {
556
- console.error('Failed to clean:', error);
557
- process.exit(1);
558
- }
559
- });
604
+ program.commandsGroup(GROUPS.knowledge);
560
605
  program
561
606
  .command('learn [url] [description]')
562
- .description('Add knowledge for URLs')
607
+ .description('Add knowledge for a URL')
563
608
  .option('-p, --path <path>', 'Working directory path')
564
609
  .option('--replace', 'Replace existing knowledge for this URL instead of appending')
565
610
  .action(async (url, description, options) => {
@@ -586,28 +631,9 @@ program
586
631
  process.exit(1);
587
632
  }
588
633
  });
589
- program
590
- .command('knows [url]')
591
- .description('List all knowledge URLs or show matching knowledge for a URL')
592
- .option('-p, --path <path>', 'Working directory path')
593
- .action(async (url, options) => {
594
- try {
595
- await ConfigParser.getInstance().loadConfig({
596
- path: options.path || process.cwd(),
597
- });
598
- const { KnowsCommand } = await import('../src/commands/knows-command.js');
599
- const explorBot = new ExplorBot({ path: options.path });
600
- const command = new KnowsCommand(explorBot);
601
- await command.execute(url || '');
602
- }
603
- catch (error) {
604
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
605
- process.exit(1);
606
- }
607
- });
608
634
  program
609
635
  .command('compact [target]')
610
- .description('Compact stored experience files; pass filename or URL substring to limit scope')
636
+ .description('Compact experience files; pass a filename or URL substring to narrow')
611
637
  .option('--dry-run', 'Preview without running AI or writing files')
612
638
  .option('--no-merge', 'Skip the cross-URL merge step when compacting all')
613
639
  .option('-p, --path <path>', 'Working directory path')
@@ -634,152 +660,105 @@ program
634
660
  }
635
661
  });
636
662
  program
637
- .command('experience [filter] [index]')
638
- .description('List stored experiences grouped by URL; pass URL substring to filter; pass tag (A.1) or index to expand a section')
663
+ .command('add-rule [agent] [name]')
664
+ .description('Create a rule file for an agent')
665
+ .option('--url <pattern>', 'URL pattern for this rule')
639
666
  .option('-p, --path <path>', 'Working directory path')
640
- .option('-c, --config <path>', 'Path to configuration file')
641
- .option('--recent', 'Only files modified within the last 30 days')
642
- .option('--old', 'Only files modified more than 30 days ago')
643
- .action(async (filter, index, options) => {
644
- try {
645
- await ConfigParser.getInstance().loadConfig({
646
- config: options.config,
647
- path: options.path || process.cwd(),
667
+ .action(async (agent, name, options) => {
668
+ if (options.path)
669
+ process.chdir(path.resolve(options.path));
670
+ if (agent && name) {
671
+ const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
672
+ const result = AddRuleCommand.createRuleFile(agent, name, {
673
+ urlPattern: options.url,
648
674
  });
649
- const { ExperienceCommand } = await import('../src/commands/experience-command.js');
650
- const explorBot = new ExplorBot({ path: options.path });
651
- const command = new ExperienceCommand(explorBot);
652
- const flags = [];
653
- if (options.recent)
654
- flags.push('--recent');
655
- if (options.old)
656
- flags.push('--old');
657
- const args = [...flags, filter, index].filter(Boolean).join(' ');
658
- await command.execute(args);
659
- command.printSuggestions();
660
- }
661
- catch (error) {
662
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
663
- process.exit(1);
664
- }
665
- });
666
- 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(async (url, options) => {
667
- try {
668
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
669
- await explorBot.start();
670
- const argParts = [url];
671
- if (options.data)
672
- argParts.push('--data');
673
- if (options.deep)
674
- argParts.push('--deep');
675
- if (options.fix === false)
676
- argParts.push('--no-fix');
677
- const { ResearchCommand } = await import('../src/commands/research-command.js');
678
- await new ResearchCommand(explorBot).execute(argParts.join(' '));
679
- await explorBot.stop();
680
- await showStatsAndExit(0);
681
- }
682
- catch (error) {
683
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
684
- await showStatsAndExit(1);
675
+ process.exit(result ? 0 : 1);
685
676
  }
677
+ const AddRule = (await import('../src/components/AddRule.js')).default;
678
+ render(React.createElement(AddRule, {
679
+ initialAgent: agent || '',
680
+ initialName: name || '',
681
+ }), {
682
+ exitOnCtrlC: false,
683
+ patchConsole: false,
684
+ });
686
685
  });
687
- addCommonOptions(program.command('navigate <url>').description('Navigate to a URL using the AI Navigator. Exits 0 if reachable, 1 otherwise.')).action(async (url, options) => {
686
+ program
687
+ .command('extract-rules <agent>')
688
+ .description("Copy an agent's built-in rules and planning styles out to customize")
689
+ .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
690
+ .action(async (agent, options) => {
688
691
  try {
689
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
690
- await explorBot.start();
691
- const { NavigateCommand } = await import('../src/commands/navigate-command.js');
692
- await new NavigateCommand(explorBot).execute(url);
693
- await explorBot.stop();
694
- await showStatsAndExit(0);
692
+ const { RulesLoader } = await import('../src/utils/rules-loader.js');
693
+ const targetDir = options.dir || path.resolve(`./rules/${agent}`);
694
+ const extracted = RulesLoader.extractRules(agent, targetDir);
695
+ if (extracted.length === 0) {
696
+ console.log('All rule files already exist in target directory.');
697
+ }
698
+ else {
699
+ console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
700
+ }
695
701
  }
696
702
  catch (error) {
697
703
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
698
- await showStatsAndExit(1);
704
+ process.exit(1);
699
705
  }
700
706
  });
701
- addCommonOptions(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')).action(async (url, options) => {
707
+ program.commandsGroup(GROUPS.setup);
708
+ program
709
+ .command('init')
710
+ .description('Initialize configuration for a project or for this machine')
711
+ .option('-c, --config-path <path>', 'Path for the config file')
712
+ .option('-f, --force', 'Overwrite existing config file')
713
+ .option('-p, --path <path>', 'Working directory for initialization')
714
+ .option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
715
+ .option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
716
+ .option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
717
+ .option('--url <url>', 'Web application URL written to web.url in the local config')
718
+ .action(async (options) => {
702
719
  try {
703
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
704
- await explorBot.start();
705
- await explorBot.visit(url);
706
- const plan = await explorBot.agentDriller().drill({
707
- knowledgePath: options.saveKnowledge,
708
- maxComponents: Number.parseInt(options.maxComponents || '30', 10),
709
- interactive: false,
720
+ const { runInit } = await import('../src/commands/init-command.js');
721
+ await runInit({
722
+ configPath: options.configPath,
723
+ force: options.force,
724
+ path: options.path,
725
+ global: options.global,
726
+ provider: options.provider,
727
+ apiKey: options.apiKey,
728
+ url: options.url,
710
729
  });
711
- console.log(`\nDrill completed: ${plan.tests.length} components`);
712
- console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
713
- console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
714
- await explorBot.stop();
715
- await showStatsAndExit(0);
716
730
  }
717
731
  catch (error) {
718
732
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
719
- await showStatsAndExit(1);
733
+ process.exit(1);
720
734
  }
721
735
  });
722
736
  program
723
- .command('context <url>')
724
- .description('Print page context (URL, headings, knowledge, experience, interactive elements)')
725
- .option('-p, --path <path>', 'Working directory path')
737
+ .command('config [url]')
738
+ .description('Show models, config file and paths used by this run')
726
739
  .option('-c, --config <path>', 'Path to configuration file')
727
- .option('--session [file]', 'Save/restore browser session from file')
728
- .option('--full', 'Include HTML and all data')
729
- .option('--compact', 'Compact view with summaries')
730
- .option('--attached', 'Only auto-attached sections (default)')
731
- .option('--visual', 'Annotate elements on screenshot and print screenshot path')
732
- .option('--screenshot', 'Alias for --visual')
740
+ .option('-p, --path <path>', 'Working directory path')
741
+ .option('--json', 'Print the resolved config as JSON')
733
742
  .action(async (url, options) => {
743
+ setQuietMode(!isVerboseMode());
744
+ const { ConfigCommand } = await import('../src/commands/config-command.js');
734
745
  try {
735
- const mainOptions = {
736
- path: options.path,
737
- config: options.config,
738
- headless: true,
739
- session: options.session,
740
- };
741
- const explorBot = new ExplorBot(mainOptions);
742
- await explorBot.start();
743
- await explorBot.visit(url);
744
- const { ContextCommand } = await import('../src/commands/context-command.js');
745
- const argParts = [];
746
- if (options.full)
747
- argParts.push('--full');
748
- else if (options.compact)
749
- argParts.push('--compact');
750
- else
751
- argParts.push('--attached');
752
- if (options.visual || options.screenshot)
753
- argParts.push('--visual');
754
- await new ContextCommand(explorBot).execute(argParts.join(' '));
755
- await explorBot.stop();
756
- await showStatsAndExit(0);
746
+ console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
757
747
  }
758
748
  catch (error) {
759
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
760
- await showStatsAndExit(1);
749
+ console.error(error instanceof Error ? error.message : 'Unknown error');
750
+ process.exit(1);
761
751
  }
762
752
  });
763
- addCommonOptions(program.command('shell <url> <command>').description('Execute a CodeceptJS command on a page and exit')).action(async (url, command, options) => {
764
- try {
765
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
766
- await explorBot.start();
767
- await explorBot.visit(url);
768
- const action = explorBot.getExplorer().action();
769
- await action.execute(command);
770
- log('Command executed successfully');
771
- const state = explorBot.stateManager().getCurrentState();
772
- if (state)
773
- log(`URL: ${state.url}`);
774
- await explorBot.stop();
775
- await showStatsAndExit(0);
776
- }
777
- catch (error) {
778
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
779
- await showStatsAndExit(1);
780
- }
753
+ RecommendedModelsCommand.register(program);
754
+ program
755
+ .command('sites')
756
+ .description('List sites registered in the global installation')
757
+ .action(async () => {
758
+ const { SitesCommand } = await import('../src/commands/sites-command.js');
759
+ await new SitesCommand(new ExplorBot()).execute('');
781
760
  });
782
- const browserCmd = program.command('browser').description('Manage persistent browser server');
761
+ const browserCmd = program.command('browser').description('Start, stop or check a persistent browser server');
783
762
  browserCmd
784
763
  .command('start')
785
764
  .description('Launch a persistent browser server')
@@ -848,72 +827,104 @@ browserCmd
848
827
  }
849
828
  });
850
829
  program
851
- .command('extract-rules <agent>')
852
- .description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
853
- .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
854
- .action(async (agent, options) => {
830
+ .command('clean [target]')
831
+ .description('Delete states, research, plans, tests, experiences or output (default: output + experiences)')
832
+ .option('-p, --path <path>', 'Custom path to clean')
833
+ .action(async (target, options) => {
834
+ const customPath = options.path;
835
+ const originalCwd = process.cwd();
836
+ const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
837
+ const targets = {
838
+ states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
839
+ research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
840
+ plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
841
+ tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
842
+ experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
843
+ output: { description: 'all output files', dir: path.join(basePath, 'output') },
844
+ };
845
+ function cleanDirectoryContents(dirPath) {
846
+ let count = 0;
847
+ for (const item of fs.readdirSync(dirPath)) {
848
+ const itemPath = path.join(dirPath, item);
849
+ if (fs.statSync(itemPath).isDirectory()) {
850
+ count += cleanDirectoryContents(itemPath);
851
+ fs.rmSync(itemPath, { recursive: true });
852
+ }
853
+ else {
854
+ fs.unlinkSync(itemPath);
855
+ count++;
856
+ }
857
+ }
858
+ return count;
859
+ }
860
+ function cleanTarget(name) {
861
+ const t = targets[name];
862
+ if (!fs.existsSync(t.dir)) {
863
+ console.log(`${name}: nothing to clean (${t.dir} not found)`);
864
+ return;
865
+ }
866
+ const count = cleanDirectoryContents(t.dir);
867
+ console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
868
+ }
855
869
  try {
856
- const { RulesLoader } = await import('../src/utils/rules-loader.js');
857
- const targetDir = options.dir || path.resolve(`./rules/${agent}`);
858
- const extracted = RulesLoader.extractRules(agent, targetDir);
859
- if (extracted.length === 0) {
860
- console.log('All rule files already exist in target directory.');
870
+ if (target && !targets[target]) {
871
+ console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
872
+ process.exit(1);
873
+ }
874
+ if (!target) {
875
+ cleanTarget('output');
876
+ cleanTarget('experiences');
861
877
  }
862
878
  else {
863
- console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
879
+ cleanTarget(target);
864
880
  }
881
+ console.log('Cleanup completed successfully!');
865
882
  }
866
883
  catch (error) {
867
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
884
+ console.error('Failed to clean:', error);
868
885
  process.exit(1);
869
886
  }
870
887
  });
871
- program
872
- .command('add-rule [agent] [name]')
873
- .description('Create a rule file for an agent')
874
- .option('--url <pattern>', 'URL pattern for this rule')
875
- .option('-p, --path <path>', 'Working directory path')
876
- .action(async (agent, name, options) => {
877
- if (options.path)
878
- process.chdir(path.resolve(options.path));
879
- if (agent && name) {
880
- const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
881
- const result = AddRuleCommand.createRuleFile(agent, name, {
882
- urlPattern: options.url,
883
- });
884
- process.exit(result ? 0 : 1);
885
- }
886
- const AddRule = (await import('../src/components/AddRule.js')).default;
887
- render(React.createElement(AddRule, {
888
- initialAgent: agent || '',
889
- initialName: name || '',
890
- }), {
891
- exitOnCtrlC: false,
892
- patchConsole: false,
893
- });
894
- });
895
- import { createApiCommands } from "../boat/api-tester/src/cli.js";
896
- import { createDocsCommands } from "../boat/doc-collector/src/cli.js";
897
- import { createPrimaCommands } from "../boat/prima/src/cli.js";
888
+ program.commandsGroup(GROUPS.tools);
898
889
  program.addCommand(createApiCommands('api'));
899
890
  program.addCommand(createDocsCommands('docs'));
900
891
  program.addCommand(createPrimaCommands('prima'));
901
892
  const envHelp = () => {
902
893
  const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
903
- const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
894
+ const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${chalk.green(v.name.padEnd(width))} ${v.description}`).join('\n');
904
895
  return `
905
- Environment variables (config-free one-liner mode):
896
+ ${chalk.yellow.bold('Environment variables:')}
906
897
  Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
907
898
 
908
899
  ${rows}
909
900
 
910
901
  Providers: ${Object.keys(PROVIDERS).join(', ')}
911
- Example:
912
- EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
913
- ${cli} explore /login --max-tests 3
902
+
903
+ ${chalk.yellow.bold('Example:')}
904
+ EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
905
+ ${cli} explore /login --max-tests 3
914
906
  `;
915
907
  };
916
- program.addHelpText('after', `\nFor agents and tools:\n ${cli} help-json [command...] the same definitions as JSON — commands, arguments, options, defaults\n`);
917
- program.addHelpText('after', envHelp);
908
+ program
909
+ .command('env')
910
+ .description('Show environment variables for running without a config file')
911
+ .helpGroup(GROUPS.setup)
912
+ .addHelpText('after', envHelp)
913
+ .action(() => console.log(envHelp()));
914
+ const examples = [
915
+ [`${cli} start`, 'interactive TUI, drive it with /commands'],
916
+ [`${cli} explore /login`, 'invent and run tests for one page'],
917
+ [`${cli} freesail`, 'roam the whole app on its own'],
918
+ [`${cli} plan /users`, 'write a test plan, then run it with test'],
919
+ ];
920
+ const exampleWidth = Math.max(...examples.map(([command]) => command.length));
921
+ program.addHelpText('after', `
922
+ ${chalk.yellow.bold('Examples:')}
923
+ ${examples.map(([command, note]) => ` ${chalk.cyan(command.padEnd(exampleWidth))} ${chalk.dim(note)}`).join('\n')}
924
+
925
+ ${chalk.dim(`Environment variables: ${cli} help env · JSON definitions for agents: ${cli} help-json [command...]`)}
926
+ `);
927
+ program.commandsGroup(GROUPS.help);
928
+ program.helpCommand(true);
918
929
  HelpJsonCommand.register(program);
919
930
  program.parse();