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.
- package/bin/explorbot-cli.ts +494 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/dist/bin/explorbot-cli.js +455 -444
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +4 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +6 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +5 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +4 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
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
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
389
|
-
.description('List
|
|
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
|
-
.
|
|
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
|
|
400
|
-
|
|
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('
|
|
409
|
-
.description('List
|
|
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
|
-
|
|
486
|
-
|
|
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
|
|
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
|
|
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('
|
|
638
|
-
.description('
|
|
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
|
-
.
|
|
641
|
-
.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
704
|
+
process.exit(1);
|
|
699
705
|
}
|
|
700
706
|
});
|
|
701
|
-
|
|
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
|
|
704
|
-
await
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
733
|
+
process.exit(1);
|
|
720
734
|
}
|
|
721
735
|
});
|
|
722
736
|
program
|
|
723
|
-
.command('
|
|
724
|
-
.description('
|
|
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('--
|
|
728
|
-
.option('--
|
|
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
|
-
|
|
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(
|
|
760
|
-
|
|
749
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
750
|
+
process.exit(1);
|
|
761
751
|
}
|
|
762
752
|
});
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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('
|
|
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('
|
|
852
|
-
.description('
|
|
853
|
-
.option('-
|
|
854
|
-
.action(async (
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
879
|
+
cleanTarget(target);
|
|
864
880
|
}
|
|
881
|
+
console.log('Cleanup completed successfully!');
|
|
865
882
|
}
|
|
866
883
|
catch (error) {
|
|
867
|
-
console.error('Failed
|
|
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
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
|
917
|
-
|
|
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();
|