explorbot 0.0.5 → 0.1.1
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 +97 -39
- package/dist/bin/explorbot-cli.js +75 -19
- package/dist/rules/rerunner/healing-approach.md +19 -0
- package/dist/src/action.js +8 -7
- package/dist/src/ai/historian.js +34 -3
- package/dist/src/ai/navigator.js +35 -28
- package/dist/src/ai/pilot.js +33 -9
- package/dist/src/ai/planner/subpages.js +42 -6
- package/dist/src/ai/planner.js +44 -13
- package/dist/src/ai/rerunner.js +472 -0
- package/dist/src/ai/researcher/cache.js +13 -8
- package/dist/src/ai/researcher/coordinates.js +4 -2
- package/dist/src/ai/researcher/deep-analysis.js +16 -19
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/parser.js +4 -3
- package/dist/src/ai/researcher/research-result.js +2 -0
- package/dist/src/ai/researcher.js +3 -3
- package/dist/src/ai/rules.js +2 -2
- package/dist/src/ai/tools.js +6 -2
- package/dist/src/commands/add-rule-command.js +1 -2
- package/dist/src/commands/base-command.js +12 -0
- package/dist/src/commands/context-command.js +10 -3
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/explore-command.js +21 -6
- package/dist/src/commands/freesail-command.js +8 -22
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/init-command.js +7 -5
- package/dist/src/commands/path-command.js +2 -1
- package/dist/src/commands/plan-command.js +38 -11
- package/dist/src/commands/rerun-command.js +42 -0
- package/dist/src/commands/research-command.js +10 -4
- package/dist/src/commands/runs-command.js +22 -0
- package/dist/src/commands/start-command.js +0 -1
- package/dist/src/commands/test-command.js +3 -3
- package/dist/src/components/App.js +8 -0
- package/dist/src/config.js +3 -0
- package/dist/src/explorbot.js +20 -1
- package/dist/src/explorer.js +59 -16
- package/dist/src/suite.js +115 -0
- package/dist/src/utils/html.js +2 -5
- package/dist/src/utils/rules-loader.js +33 -17
- package/dist/src/utils/test-files.js +103 -0
- package/dist/src/utils/web-element.js +6 -4
- package/package.json +3 -2
- package/rules/rerunner/healing-approach.md +19 -0
- package/src/action.ts +8 -6
- package/src/ai/historian.ts +37 -3
- package/src/ai/navigator.ts +35 -28
- package/src/ai/pilot.ts +33 -9
- package/src/ai/planner/subpages.ts +37 -7
- package/src/ai/planner.ts +44 -12
- package/src/ai/rerunner.ts +532 -0
- package/src/ai/researcher/cache.ts +14 -8
- package/src/ai/researcher/coordinates.ts +8 -7
- package/src/ai/researcher/deep-analysis.ts +18 -21
- package/src/ai/researcher/locators.ts +3 -3
- package/src/ai/researcher/parser.ts +4 -4
- package/src/ai/researcher/research-result.ts +1 -0
- package/src/ai/researcher.ts +3 -3
- package/src/ai/rules.ts +2 -2
- package/src/ai/tools.ts +7 -2
- package/src/commands/add-rule-command.ts +1 -2
- package/src/commands/base-command.ts +13 -0
- package/src/commands/context-command.ts +10 -3
- package/src/commands/drill-command.ts +0 -1
- package/src/commands/explore-command.ts +22 -6
- package/src/commands/freesail-command.ts +6 -23
- package/src/commands/index.ts +4 -0
- package/src/commands/init-command.ts +8 -5
- package/src/commands/path-command.ts +2 -1
- package/src/commands/plan-command.ts +46 -12
- package/src/commands/rerun-command.ts +46 -0
- package/src/commands/research-command.ts +10 -4
- package/src/commands/runs-command.ts +27 -0
- package/src/commands/start-command.ts +0 -1
- package/src/commands/test-command.ts +3 -3
- package/src/components/App.tsx +8 -0
- package/src/config.ts +24 -0
- package/src/explorbot.ts +22 -1
- package/src/explorer.ts +68 -20
- package/src/suite.ts +135 -0
- package/src/utils/html.ts +1 -5
- package/src/utils/rules-loader.ts +35 -17
- package/src/utils/test-files.ts +122 -0
- package/src/utils/web-element.ts +12 -10
package/bin/explorbot-cli.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
import { Command } from 'commander';
|
|
6
|
+
import figureSet from 'figures';
|
|
5
7
|
import { render } from 'ink';
|
|
6
8
|
import React from 'react';
|
|
7
9
|
import { App } from '../src/components/App.js';
|
|
@@ -104,14 +106,14 @@ async function showStatsAndExit(code: number): Promise<never> {
|
|
|
104
106
|
process.exit(code);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
addCommonOptions(program.command('start [path]').
|
|
109
|
+
addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
|
|
108
110
|
setPreserveConsoleLogs(false);
|
|
109
111
|
const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
|
|
110
112
|
await explorBot.start();
|
|
111
113
|
await startTUI(explorBot);
|
|
112
114
|
});
|
|
113
115
|
|
|
114
|
-
addCommonOptions(program.command('explore <path>').description('
|
|
116
|
+
addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
|
|
115
117
|
try {
|
|
116
118
|
const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
|
|
117
119
|
await explorBot.start();
|
|
@@ -128,10 +130,11 @@ addCommonOptions(program.command('explore <path>').description('Start web explor
|
|
|
128
130
|
}
|
|
129
131
|
});
|
|
130
132
|
|
|
131
|
-
addCommonOptions(program.command('plan <path>
|
|
133
|
+
addCommonOptions(program.command('plan <path>').description('Generate test plan for a page and exit'))
|
|
132
134
|
.option('-a, --append', 'Add tests to existing plan file')
|
|
133
135
|
.option('--style <style>', 'Planning style: normal, curious, psycho')
|
|
134
|
-
.
|
|
136
|
+
.option('--focus <feature>', 'Focus area for test planning')
|
|
137
|
+
.action(async (planPath, options) => {
|
|
135
138
|
try {
|
|
136
139
|
const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
|
|
137
140
|
await explorBot.start();
|
|
@@ -146,7 +149,7 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
await explorBot.plan(
|
|
152
|
+
await explorBot.plan(options.focus || undefined, {
|
|
150
153
|
fresh: !options.append,
|
|
151
154
|
style: options.style,
|
|
152
155
|
});
|
|
@@ -158,6 +161,23 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
|
|
|
158
161
|
await showStatsAndExit(1);
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
const suite = explorBot.getSuite();
|
|
165
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
166
|
+
const names = suite.getAutomatedTestNames();
|
|
167
|
+
console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
|
|
168
|
+
for (let i = 0; i < names.length; i++) {
|
|
169
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (plan?.tests.length) {
|
|
174
|
+
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
|
|
175
|
+
for (let i = 0; i < plan.tests.length; i++) {
|
|
176
|
+
const t = plan.tests[i];
|
|
177
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
161
181
|
const savedPath = explorBot.savePlan();
|
|
162
182
|
const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
|
|
163
183
|
|
|
@@ -165,10 +185,14 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
|
|
|
165
185
|
const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
|
|
166
186
|
|
|
167
187
|
const lines: string[] = [];
|
|
168
|
-
lines.push('Run
|
|
169
|
-
lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
|
|
170
|
-
lines.push(`\`${cli} test ${planFile}
|
|
171
|
-
|
|
188
|
+
lines.push('Run commands:');
|
|
189
|
+
lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first new test`);
|
|
190
|
+
lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` → run all new tests`);
|
|
191
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
192
|
+
for (const f of suite.getAutomatedTestFiles()) {
|
|
193
|
+
lines.push(`\`${cli} rerun ${path.relative(process.cwd(), f)}${cliSuffix}\` → re-run automated tests`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
172
196
|
|
|
173
197
|
log(parseMarkdownToTerminal(lines.join('\n')));
|
|
174
198
|
|
|
@@ -280,6 +304,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
280
304
|
}
|
|
281
305
|
});
|
|
282
306
|
|
|
307
|
+
program
|
|
308
|
+
.command('runs [file]')
|
|
309
|
+
.description('List generated test files, or show steps for a specific file')
|
|
310
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
311
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
312
|
+
.action(async (file, options) => {
|
|
313
|
+
try {
|
|
314
|
+
await ConfigParser.getInstance().loadConfig({
|
|
315
|
+
config: options.config,
|
|
316
|
+
path: options.path || process.cwd(),
|
|
317
|
+
});
|
|
318
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
319
|
+
const { RunsCommand } = await import('../src/commands/runs-command.js');
|
|
320
|
+
await new RunsCommand(explorBot).execute(file || '');
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
328
|
+
try {
|
|
329
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
330
|
+
await explorBot.start();
|
|
331
|
+
const { RerunCommand } = await import('../src/commands/rerun-command.js');
|
|
332
|
+
const cmd = new RerunCommand(explorBot);
|
|
333
|
+
const args = index ? `${filename} ${index}` : filename;
|
|
334
|
+
await cmd.execute(args);
|
|
335
|
+
await explorBot.stop();
|
|
336
|
+
await showStatsAndExit(0);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
339
|
+
await showStatsAndExit(1);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
283
343
|
addCommonOptions(
|
|
284
344
|
program
|
|
285
345
|
.command('freesail [startUrl]')
|
|
@@ -376,7 +436,6 @@ program
|
|
|
376
436
|
|
|
377
437
|
program
|
|
378
438
|
.command('learn [url] [description]')
|
|
379
|
-
.alias('add-knowledge')
|
|
380
439
|
.description('Add knowledge for URLs')
|
|
381
440
|
.option('-p, --path <path>', 'Working directory path')
|
|
382
441
|
.action(async (url, description, options) => {
|
|
@@ -447,32 +506,32 @@ addCommonOptions(program.command('research <url>').description('Research a page
|
|
|
447
506
|
}
|
|
448
507
|
);
|
|
449
508
|
|
|
450
|
-
addCommonOptions(
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
await explorBot.start();
|
|
509
|
+
addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(
|
|
510
|
+
async (url, options) => {
|
|
511
|
+
try {
|
|
512
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
513
|
+
await explorBot.start();
|
|
456
514
|
|
|
457
|
-
|
|
515
|
+
await explorBot.visit(url);
|
|
458
516
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
517
|
+
const plan = await explorBot.agentBosun().drill({
|
|
518
|
+
knowledgePath: options.knowledge,
|
|
519
|
+
maxComponents: Number.parseInt(options.max, 10),
|
|
520
|
+
interactive: false,
|
|
521
|
+
});
|
|
464
522
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
523
|
+
console.log(`\nDrill completed: ${plan.tests.length} components`);
|
|
524
|
+
console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
|
|
525
|
+
console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
|
|
468
526
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
527
|
+
await explorBot.stop();
|
|
528
|
+
await showStatsAndExit(0);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
531
|
+
await showStatsAndExit(1);
|
|
532
|
+
}
|
|
474
533
|
}
|
|
475
|
-
|
|
534
|
+
);
|
|
476
535
|
|
|
477
536
|
program
|
|
478
537
|
.command('context <url>')
|
|
@@ -624,18 +683,18 @@ browserCmd
|
|
|
624
683
|
});
|
|
625
684
|
|
|
626
685
|
program
|
|
627
|
-
.command('extract-
|
|
628
|
-
.description('Extract built-in planning styles to a directory for customization')
|
|
629
|
-
.option('-d, --dir <path>', 'Target directory (default: ./rules/<agent
|
|
686
|
+
.command('extract-rules <agent>')
|
|
687
|
+
.description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
|
|
688
|
+
.option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
|
|
630
689
|
.action(async (agent, options) => {
|
|
631
690
|
try {
|
|
632
691
|
const { RulesLoader } = await import('../src/utils/rules-loader.js');
|
|
633
|
-
const targetDir = options.dir || path.resolve(`./rules/${agent}
|
|
634
|
-
const extracted = RulesLoader.
|
|
692
|
+
const targetDir = options.dir || path.resolve(`./rules/${agent}`);
|
|
693
|
+
const extracted = RulesLoader.extractRules(agent, targetDir);
|
|
635
694
|
if (extracted.length === 0) {
|
|
636
|
-
console.log('All
|
|
695
|
+
console.log('All rule files already exist in target directory.');
|
|
637
696
|
} else {
|
|
638
|
-
console.log(`\nExtracted ${extracted.length}
|
|
697
|
+
console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
|
|
639
698
|
}
|
|
640
699
|
} catch (error) {
|
|
641
700
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -645,7 +704,6 @@ program
|
|
|
645
704
|
|
|
646
705
|
program
|
|
647
706
|
.command('add-rule [agent] [name]')
|
|
648
|
-
.alias('rules:add')
|
|
649
707
|
.description('Create a rule file for an agent')
|
|
650
708
|
.option('--url <pattern>', 'URL pattern for this rule')
|
|
651
709
|
.option('-p, --path <path>', 'Working directory path')
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
import { Command } from 'commander';
|
|
6
|
+
import figureSet from 'figures';
|
|
5
7
|
import { render } from 'ink';
|
|
6
8
|
import React from 'react';
|
|
7
9
|
import { App } from '../src/components/App.js';
|
|
@@ -78,13 +80,13 @@ async function showStatsAndExit(code) {
|
|
|
78
80
|
}
|
|
79
81
|
process.exit(code);
|
|
80
82
|
}
|
|
81
|
-
addCommonOptions(program.command('start [path]').
|
|
83
|
+
addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
|
|
82
84
|
setPreserveConsoleLogs(false);
|
|
83
85
|
const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
|
|
84
86
|
await explorBot.start();
|
|
85
87
|
await startTUI(explorBot);
|
|
86
88
|
});
|
|
87
|
-
addCommonOptions(program.command('explore <path>').description('
|
|
89
|
+
addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
|
|
88
90
|
try {
|
|
89
91
|
const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
|
|
90
92
|
await explorBot.start();
|
|
@@ -102,10 +104,11 @@ addCommonOptions(program.command('explore <path>').description('Start web explor
|
|
|
102
104
|
await showStatsAndExit(1);
|
|
103
105
|
}
|
|
104
106
|
});
|
|
105
|
-
addCommonOptions(program.command('plan <path>
|
|
107
|
+
addCommonOptions(program.command('plan <path>').description('Generate test plan for a page and exit'))
|
|
106
108
|
.option('-a, --append', 'Add tests to existing plan file')
|
|
107
109
|
.option('--style <style>', 'Planning style: normal, curious, psycho')
|
|
108
|
-
.
|
|
110
|
+
.option('--focus <feature>', 'Focus area for test planning')
|
|
111
|
+
.action(async (planPath, options) => {
|
|
109
112
|
try {
|
|
110
113
|
const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
|
|
111
114
|
await explorBot.start();
|
|
@@ -117,7 +120,7 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
|
|
|
117
120
|
explorBot.loadPlan(existingPlanPath);
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
|
-
await explorBot.plan(
|
|
123
|
+
await explorBot.plan(options.focus || undefined, {
|
|
121
124
|
fresh: !options.append,
|
|
122
125
|
style: options.style,
|
|
123
126
|
});
|
|
@@ -127,15 +130,34 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
|
|
|
127
130
|
await explorBot.stop();
|
|
128
131
|
await showStatsAndExit(1);
|
|
129
132
|
}
|
|
133
|
+
const suite = explorBot.getSuite();
|
|
134
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
135
|
+
const names = suite.getAutomatedTestNames();
|
|
136
|
+
console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
|
|
137
|
+
for (let i = 0; i < names.length; i++) {
|
|
138
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (plan?.tests.length) {
|
|
142
|
+
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
|
|
143
|
+
for (let i = 0; i < plan.tests.length; i++) {
|
|
144
|
+
const t = plan.tests[i];
|
|
145
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
130
148
|
const savedPath = explorBot.savePlan();
|
|
131
149
|
const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
|
|
132
150
|
const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
|
|
133
151
|
const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
|
|
134
152
|
const lines = [];
|
|
135
|
-
lines.push('Run
|
|
136
|
-
lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
|
|
137
|
-
lines.push(`\`${cli} test ${planFile}
|
|
138
|
-
|
|
153
|
+
lines.push('Run commands:');
|
|
154
|
+
lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first new test`);
|
|
155
|
+
lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` → run all new tests`);
|
|
156
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
157
|
+
for (const f of suite.getAutomatedTestFiles()) {
|
|
158
|
+
lines.push(`\`${cli} rerun ${path.relative(process.cwd(), f)}${cliSuffix}\` → re-run automated tests`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
139
161
|
log(parseMarkdownToTerminal(lines.join('\n')));
|
|
140
162
|
await explorBot.stop();
|
|
141
163
|
await showStatsAndExit(0);
|
|
@@ -239,6 +261,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
239
261
|
await showStatsAndExit(1);
|
|
240
262
|
}
|
|
241
263
|
});
|
|
264
|
+
program
|
|
265
|
+
.command('runs [file]')
|
|
266
|
+
.description('List generated test files, or show steps for a specific file')
|
|
267
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
268
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
269
|
+
.action(async (file, options) => {
|
|
270
|
+
try {
|
|
271
|
+
await ConfigParser.getInstance().loadConfig({
|
|
272
|
+
config: options.config,
|
|
273
|
+
path: options.path || process.cwd(),
|
|
274
|
+
});
|
|
275
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
276
|
+
const { RunsCommand } = await import('../src/commands/runs-command.js');
|
|
277
|
+
await new RunsCommand(explorBot).execute(file || '');
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
285
|
+
try {
|
|
286
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
287
|
+
await explorBot.start();
|
|
288
|
+
const { RerunCommand } = await import('../src/commands/rerun-command.js');
|
|
289
|
+
const cmd = new RerunCommand(explorBot);
|
|
290
|
+
const args = index ? `${filename} ${index}` : filename;
|
|
291
|
+
await cmd.execute(args);
|
|
292
|
+
await explorBot.stop();
|
|
293
|
+
await showStatsAndExit(0);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
297
|
+
await showStatsAndExit(1);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
242
300
|
addCommonOptions(program
|
|
243
301
|
.command('freesail [startUrl]')
|
|
244
302
|
.description('Continuously explore and navigate to new pages autonomously')
|
|
@@ -327,7 +385,6 @@ program
|
|
|
327
385
|
});
|
|
328
386
|
program
|
|
329
387
|
.command('learn [url] [description]')
|
|
330
|
-
.alias('add-knowledge')
|
|
331
388
|
.description('Add knowledge for URLs')
|
|
332
389
|
.option('-p, --path <path>', 'Working directory path')
|
|
333
390
|
.action(async (url, description, options) => {
|
|
@@ -394,7 +451,7 @@ addCommonOptions(program.command('research <url>').description('Research a page
|
|
|
394
451
|
await showStatsAndExit(1);
|
|
395
452
|
}
|
|
396
453
|
});
|
|
397
|
-
addCommonOptions(program.command('drill <url>').
|
|
454
|
+
addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(async (url, options) => {
|
|
398
455
|
try {
|
|
399
456
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
400
457
|
await explorBot.start();
|
|
@@ -554,19 +611,19 @@ browserCmd
|
|
|
554
611
|
}
|
|
555
612
|
});
|
|
556
613
|
program
|
|
557
|
-
.command('extract-
|
|
558
|
-
.description('Extract built-in planning styles to a directory for customization')
|
|
559
|
-
.option('-d, --dir <path>', 'Target directory (default: ./rules/<agent
|
|
614
|
+
.command('extract-rules <agent>')
|
|
615
|
+
.description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
|
|
616
|
+
.option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
|
|
560
617
|
.action(async (agent, options) => {
|
|
561
618
|
try {
|
|
562
619
|
const { RulesLoader } = await import('../src/utils/rules-loader.js');
|
|
563
|
-
const targetDir = options.dir || path.resolve(`./rules/${agent}
|
|
564
|
-
const extracted = RulesLoader.
|
|
620
|
+
const targetDir = options.dir || path.resolve(`./rules/${agent}`);
|
|
621
|
+
const extracted = RulesLoader.extractRules(agent, targetDir);
|
|
565
622
|
if (extracted.length === 0) {
|
|
566
|
-
console.log('All
|
|
623
|
+
console.log('All rule files already exist in target directory.');
|
|
567
624
|
}
|
|
568
625
|
else {
|
|
569
|
-
console.log(`\nExtracted ${extracted.length}
|
|
626
|
+
console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
|
|
570
627
|
}
|
|
571
628
|
}
|
|
572
629
|
catch (error) {
|
|
@@ -576,7 +633,6 @@ program
|
|
|
576
633
|
});
|
|
577
634
|
program
|
|
578
635
|
.command('add-rule [agent] [name]')
|
|
579
|
-
.alias('rules:add')
|
|
580
636
|
.description('Create a rule file for an agent')
|
|
581
637
|
.option('--url <pattern>', 'URL pattern for this rule')
|
|
582
638
|
.option('-p, --path <path>', 'Working directory path')
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<healing_approach>
|
|
2
|
+
The failed step was NOT performed. You MUST execute a replacement action.
|
|
3
|
+
Just waiting or diagnosing is NOT enough — you must perform the click/fill/press that was intended.
|
|
4
|
+
|
|
5
|
+
1. FIRST: Check the page URL and ARIA — are you on the right page?
|
|
6
|
+
- If URL or ARIA shows login/error/404 page → call giveUp immediately
|
|
7
|
+
2. If ARIA is empty/minimal → page may still be loading:
|
|
8
|
+
- Use xpathCheck() to detect spinners, loaders, or loading indicators on the page
|
|
9
|
+
- Use wait() to let the page load — it returns fresh ARIA automatically
|
|
10
|
+
- Then execute the replacement action with a working locator
|
|
11
|
+
3. If the target element is visible in ARIA:
|
|
12
|
+
- Use click() with multiple fallback locators (ARIA, CSS, XPath)
|
|
13
|
+
4. If element is NOT in ARIA but page is correct:
|
|
14
|
+
- Use xpathCheck() to search the full HTML
|
|
15
|
+
- Use research() to get a semantic UI map of the page if needed
|
|
16
|
+
- If found → click it
|
|
17
|
+
- If not → bash to check console logs → giveUp
|
|
18
|
+
5. Call done() with the command that replaced the failed step
|
|
19
|
+
</healing_approach>
|
package/dist/src/action.js
CHANGED
|
@@ -94,16 +94,17 @@ class Action {
|
|
|
94
94
|
let ariaSnapshotFile = undefined;
|
|
95
95
|
try {
|
|
96
96
|
const page = this.playwrightHelper.page;
|
|
97
|
-
|
|
98
|
-
const ariaFileName = `${stateHash}_${timestamp}.aria.yaml`;
|
|
99
|
-
const ariaPath = join(statesDir, ariaFileName);
|
|
100
|
-
fs.writeFileSync(ariaPath, serializedSnapshot, 'utf8');
|
|
101
|
-
ariaSnapshot = serializedSnapshot;
|
|
102
|
-
ariaSnapshotFile = ariaFileName;
|
|
97
|
+
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
103
98
|
}
|
|
104
99
|
catch (err) {
|
|
105
100
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
106
101
|
}
|
|
102
|
+
if (ariaSnapshot) {
|
|
103
|
+
const ariaFileName = `${stateHash}_${timestamp}.aria.yaml`;
|
|
104
|
+
const ariaPath = join(statesDir, ariaFileName);
|
|
105
|
+
fs.writeFileSync(ariaPath, ariaSnapshot, 'utf8');
|
|
106
|
+
ariaSnapshotFile = ariaFileName;
|
|
107
|
+
}
|
|
107
108
|
const result = new ActionResult({
|
|
108
109
|
html,
|
|
109
110
|
title,
|
|
@@ -115,7 +116,7 @@ class Action {
|
|
|
115
116
|
iframeSnapshots,
|
|
116
117
|
ariaSnapshot,
|
|
117
118
|
ariaSnapshotFile,
|
|
118
|
-
iframeURL: frame
|
|
119
|
+
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
119
120
|
});
|
|
120
121
|
this.stateManager.updateState(result);
|
|
121
122
|
return result;
|
package/dist/src/ai/historian.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ActionResult } from "../action-result.js";
|
|
6
6
|
import { ConfigParser } from "../config.js";
|
|
7
|
+
import { KnowledgeTracker } from "../knowledge-tracker.js";
|
|
7
8
|
import { ExperienceTracker } from "../experience-tracker.js";
|
|
8
9
|
import { Test } from "../test-plan.js";
|
|
9
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
@@ -329,6 +330,7 @@ export class Historian {
|
|
|
329
330
|
if (startUrl) {
|
|
330
331
|
lines.push('Before(({ I }) => {');
|
|
331
332
|
lines.push(` I.amOnPage('${this.escapeString(startUrl)}');`);
|
|
333
|
+
lines.push(...this.getKnowledgeLines(startUrl));
|
|
332
334
|
lines.push('});');
|
|
333
335
|
lines.push('');
|
|
334
336
|
}
|
|
@@ -356,8 +358,7 @@ export class Historian {
|
|
|
356
358
|
lines.push('});');
|
|
357
359
|
lines.push('');
|
|
358
360
|
}
|
|
359
|
-
const
|
|
360
|
-
const testsDir = join(outputDir, 'tests');
|
|
361
|
+
const testsDir = ConfigParser.getInstance().getTestsDir();
|
|
361
362
|
mkdirSync(testsDir, { recursive: true });
|
|
362
363
|
const filename = plan.title.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
|
363
364
|
const filePath = join(testsDir, `${filename}.js`);
|
|
@@ -365,12 +366,42 @@ export class Historian {
|
|
|
365
366
|
tag('substep').log(`Saved plan tests to: ${filePath}`);
|
|
366
367
|
return filePath;
|
|
367
368
|
}
|
|
369
|
+
rewriteScenarioInFile(filePath, healedSteps) {
|
|
370
|
+
let content = readFileSync(filePath, 'utf-8');
|
|
371
|
+
for (const step of healedSteps) {
|
|
372
|
+
if (!content.includes(step.original))
|
|
373
|
+
continue;
|
|
374
|
+
content = content.replace(step.original, step.healed);
|
|
375
|
+
}
|
|
376
|
+
writeFileSync(filePath, content);
|
|
377
|
+
tag('substep').log(`Updated test file with healed steps: ${filePath}`);
|
|
378
|
+
}
|
|
368
379
|
getExecutionLabel(exec, fallback) {
|
|
369
380
|
return exec.input?.explanation || exec.input?.assertion || exec.input?.note || fallback || '';
|
|
370
381
|
}
|
|
371
382
|
escapeString(str) {
|
|
372
383
|
return str.replace(/'/g, "\\'").replace(/\n/g, ' ');
|
|
373
384
|
}
|
|
385
|
+
getKnowledgeLines(url, indent = ' ') {
|
|
386
|
+
const knowledgeTracker = new KnowledgeTracker();
|
|
387
|
+
const state = new ActionResult({ url });
|
|
388
|
+
const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
|
|
389
|
+
const lines = [];
|
|
390
|
+
if (wait !== undefined) {
|
|
391
|
+
lines.push(`${indent}I.wait(${wait});`);
|
|
392
|
+
}
|
|
393
|
+
if (waitForElement) {
|
|
394
|
+
lines.push(`${indent}I.waitForElement(${JSON.stringify(waitForElement)});`);
|
|
395
|
+
}
|
|
396
|
+
if (code) {
|
|
397
|
+
for (const codeLine of code.split('\n')) {
|
|
398
|
+
const trimmed = codeLine.trim();
|
|
399
|
+
if (trimmed)
|
|
400
|
+
lines.push(`${indent}${trimmed}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return lines;
|
|
404
|
+
}
|
|
374
405
|
stripComments(code) {
|
|
375
406
|
return code
|
|
376
407
|
.split('\n')
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -31,6 +31,18 @@ class Navigator {
|
|
|
31
31
|
You are given the web page and a message from user.
|
|
32
32
|
You need to resolve the state of the page based on the message.
|
|
33
33
|
</task>
|
|
34
|
+
|
|
35
|
+
${locatorRule}
|
|
36
|
+
|
|
37
|
+
<constraints>
|
|
38
|
+
NEVER navigate away from the base URL domain. Stay on the same origin at all times.
|
|
39
|
+
NEVER attempt to rewrite, replace, mock, or spoof the URL via JavaScript, history API, location assignment, or any client-side trick.
|
|
40
|
+
NEVER use executeScript, executeAsyncScript, or any JS evaluation to change the URL, bypass redirects, or fake the page state.
|
|
41
|
+
If the target URL redirects to an authentication/login page, DO NOT try to force the original URL. Instead:
|
|
42
|
+
1. Look for credentials in the provided knowledge/hint context and perform a real login through the form.
|
|
43
|
+
2. If no credentials are available, ask the user for credentials or ask the user to log in manually.
|
|
44
|
+
A redirect to /login, /sign_in, /auth, or similar is a signal that authentication is required — treat it as such, never as an obstacle to bypass.
|
|
45
|
+
</constraints>
|
|
34
46
|
`;
|
|
35
47
|
freeSailSystemPrompt = dedent `
|
|
36
48
|
<role>
|
|
@@ -145,6 +157,14 @@ class Navigator {
|
|
|
145
157
|
${message}
|
|
146
158
|
</message>
|
|
147
159
|
|
|
160
|
+
<page>
|
|
161
|
+
${actionResult.toAiContext()}
|
|
162
|
+
|
|
163
|
+
<page_html>
|
|
164
|
+
${await actionResult.combinedHtml()}
|
|
165
|
+
</page_html>
|
|
166
|
+
</page>
|
|
167
|
+
|
|
148
168
|
<task>
|
|
149
169
|
Identify the actual request of the user.
|
|
150
170
|
Identify what is expected by user.
|
|
@@ -155,25 +175,13 @@ class Navigator {
|
|
|
155
175
|
Try various ways to achieve the result
|
|
156
176
|
</task>
|
|
157
177
|
|
|
158
|
-
|
|
159
|
-
<page>
|
|
160
|
-
${actionResult.toAiContext()}
|
|
161
|
-
|
|
162
|
-
<page_html>
|
|
163
|
-
${await actionResult.simplifiedHtml()}
|
|
164
|
-
</page_html>
|
|
165
|
-
</page>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
${knowledge}
|
|
169
|
-
|
|
170
178
|
${actionRule}
|
|
171
179
|
|
|
172
|
-
${
|
|
180
|
+
${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
|
|
173
181
|
|
|
174
|
-
${
|
|
182
|
+
${experience}
|
|
175
183
|
|
|
176
|
-
${
|
|
184
|
+
${knowledge}
|
|
177
185
|
`;
|
|
178
186
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
179
187
|
conversation.addUserText(prompt);
|
|
@@ -206,7 +214,7 @@ class Navigator {
|
|
|
206
214
|
Previous solutions did not work. Here is the full HTML context:
|
|
207
215
|
|
|
208
216
|
<page_html>
|
|
209
|
-
${await actionResult.
|
|
217
|
+
${await actionResult.combinedHtml()}
|
|
210
218
|
</page_html>
|
|
211
219
|
|
|
212
220
|
Please suggest new solutions based on this additional context.
|
|
@@ -234,6 +242,7 @@ class Navigator {
|
|
|
234
242
|
}
|
|
235
243
|
if (resolved) {
|
|
236
244
|
tag('success').log('Navigation resolved successfully');
|
|
245
|
+
await this.experienceTracker.saveSuccessfulResolution(actionResult, message, codeBlock);
|
|
237
246
|
stop();
|
|
238
247
|
return;
|
|
239
248
|
}
|
|
@@ -414,6 +423,14 @@ class Navigator {
|
|
|
414
423
|
${message}
|
|
415
424
|
</message>
|
|
416
425
|
|
|
426
|
+
<page>
|
|
427
|
+
${actionResult.toAiContext()}
|
|
428
|
+
|
|
429
|
+
<page_html>
|
|
430
|
+
${await actionResult.combinedHtml()}
|
|
431
|
+
</page_html>
|
|
432
|
+
</page>
|
|
433
|
+
|
|
417
434
|
<task>
|
|
418
435
|
Identify what assertion the user wants to verify on the page.
|
|
419
436
|
Propose different CodeceptJS assertion code blocks to verify the expected state.
|
|
@@ -427,21 +444,11 @@ class Navigator {
|
|
|
427
444
|
Do not generate assertions that would pass even if the specific claim is false.
|
|
428
445
|
</task>
|
|
429
446
|
|
|
430
|
-
<page>
|
|
431
|
-
${actionResult.toAiContext()}
|
|
432
|
-
|
|
433
|
-
<page_html>
|
|
434
|
-
${await actionResult.simplifiedHtml()}
|
|
435
|
-
</page_html>
|
|
436
|
-
</page>
|
|
437
|
-
|
|
438
|
-
${knowledge}
|
|
439
|
-
|
|
440
447
|
${RulesLoader.loadRules('navigator', ['verification-actions'], actionResult.url || '')}
|
|
441
448
|
|
|
442
|
-
${locatorRule}
|
|
443
|
-
|
|
444
449
|
${experience}
|
|
450
|
+
|
|
451
|
+
${knowledge}
|
|
445
452
|
`;
|
|
446
453
|
debugLog('Sending verification prompt to AI provider');
|
|
447
454
|
tag('debug').log('Prompt:', prompt);
|