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