explorbot 0.1.0 → 0.1.2
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 +93 -36
- package/dist/bin/explorbot-cli.js +71 -16
- package/dist/rules/rerunner/healing-approach.md +19 -0
- package/dist/src/action.js +8 -10
- 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/session-dedup.js +3 -0
- package/dist/src/ai/planner/styles.js +3 -0
- package/dist/src/ai/planner.js +29 -10
- package/dist/src/ai/rerunner.js +472 -0
- package/dist/src/ai/researcher/cache.js +4 -3
- package/dist/src/ai/researcher/fingerprint-worker.js +7 -6
- package/dist/src/ai/researcher.js +3 -4
- package/dist/src/ai/rules.js +2 -2
- package/dist/src/ai/tools.js +2 -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 +12 -5
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/explore-command.js +20 -5
- package/dist/src/commands/freesail-command.js +8 -22
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/init-command.js +3 -3
- package/dist/src/commands/path-command.js +2 -1
- package/dist/src/commands/plan-command.js +37 -15
- 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 +19 -0
- package/dist/src/explorer.js +2 -1
- 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/package.json +3 -1
- package/rules/rerunner/healing-approach.md +19 -0
- package/src/action.ts +7 -9
- 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/session-dedup.ts +4 -0
- package/src/ai/planner/styles.ts +4 -0
- package/src/ai/planner.ts +28 -9
- package/src/ai/rerunner.ts +532 -0
- package/src/ai/researcher/cache.ts +4 -3
- package/src/ai/researcher/fingerprint-worker.ts +7 -13
- package/src/ai/researcher.ts +3 -4
- package/src/ai/rules.ts +2 -2
- package/src/ai/tools.ts +2 -2
- package/src/commands/add-rule-command.ts +1 -2
- package/src/commands/base-command.ts +13 -0
- package/src/commands/context-command.ts +12 -5
- package/src/commands/drill-command.ts +0 -1
- package/src/commands/explore-command.ts +21 -5
- package/src/commands/freesail-command.ts +6 -23
- package/src/commands/index.ts +4 -0
- package/src/commands/init-command.ts +3 -3
- package/src/commands/path-command.ts +2 -1
- package/src/commands/plan-command.ts +45 -16
- 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 +23 -0
- package/src/explorbot.ts +21 -0
- package/src/explorer.ts +3 -2
- 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/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();
|
|
@@ -159,6 +161,23 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
159
161
|
await showStatsAndExit(1);
|
|
160
162
|
}
|
|
161
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
|
+
|
|
162
181
|
const savedPath = explorBot.savePlan();
|
|
163
182
|
const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
|
|
164
183
|
|
|
@@ -166,10 +185,14 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
166
185
|
const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
|
|
167
186
|
|
|
168
187
|
const lines: string[] = [];
|
|
169
|
-
lines.push('Run
|
|
170
|
-
lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
|
|
171
|
-
lines.push(`\`${cli} test ${planFile}
|
|
172
|
-
|
|
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
|
+
}
|
|
173
196
|
|
|
174
197
|
log(parseMarkdownToTerminal(lines.join('\n')));
|
|
175
198
|
|
|
@@ -281,6 +304,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
281
304
|
}
|
|
282
305
|
});
|
|
283
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
|
+
|
|
284
343
|
addCommonOptions(
|
|
285
344
|
program
|
|
286
345
|
.command('freesail [startUrl]')
|
|
@@ -377,7 +436,6 @@ program
|
|
|
377
436
|
|
|
378
437
|
program
|
|
379
438
|
.command('learn [url] [description]')
|
|
380
|
-
.alias('add-knowledge')
|
|
381
439
|
.description('Add knowledge for URLs')
|
|
382
440
|
.option('-p, --path <path>', 'Working directory path')
|
|
383
441
|
.action(async (url, description, options) => {
|
|
@@ -448,32 +506,32 @@ addCommonOptions(program.command('research <url>').description('Research a page
|
|
|
448
506
|
}
|
|
449
507
|
);
|
|
450
508
|
|
|
451
|
-
addCommonOptions(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
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();
|
|
457
514
|
|
|
458
|
-
|
|
515
|
+
await explorBot.visit(url);
|
|
459
516
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
517
|
+
const plan = await explorBot.agentBosun().drill({
|
|
518
|
+
knowledgePath: options.knowledge,
|
|
519
|
+
maxComponents: Number.parseInt(options.max, 10),
|
|
520
|
+
interactive: false,
|
|
521
|
+
});
|
|
465
522
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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}`);
|
|
469
526
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
+
}
|
|
475
533
|
}
|
|
476
|
-
|
|
534
|
+
);
|
|
477
535
|
|
|
478
536
|
program
|
|
479
537
|
.command('context <url>')
|
|
@@ -625,18 +683,18 @@ browserCmd
|
|
|
625
683
|
});
|
|
626
684
|
|
|
627
685
|
program
|
|
628
|
-
.command('extract-
|
|
629
|
-
.description('Extract built-in planning styles to a directory for customization')
|
|
630
|
-
.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>)')
|
|
631
689
|
.action(async (agent, options) => {
|
|
632
690
|
try {
|
|
633
691
|
const { RulesLoader } = await import('../src/utils/rules-loader.js');
|
|
634
|
-
const targetDir = options.dir || path.resolve(`./rules/${agent}
|
|
635
|
-
const extracted = RulesLoader.
|
|
692
|
+
const targetDir = options.dir || path.resolve(`./rules/${agent}`);
|
|
693
|
+
const extracted = RulesLoader.extractRules(agent, targetDir);
|
|
636
694
|
if (extracted.length === 0) {
|
|
637
|
-
console.log('All
|
|
695
|
+
console.log('All rule files already exist in target directory.');
|
|
638
696
|
} else {
|
|
639
|
-
console.log(`\nExtracted ${extracted.length}
|
|
697
|
+
console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
|
|
640
698
|
}
|
|
641
699
|
} catch (error) {
|
|
642
700
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -646,7 +704,6 @@ program
|
|
|
646
704
|
|
|
647
705
|
program
|
|
648
706
|
.command('add-rule [agent] [name]')
|
|
649
|
-
.alias('rules:add')
|
|
650
707
|
.description('Create a rule file for an agent')
|
|
651
708
|
.option('--url <pattern>', 'URL pattern for this rule')
|
|
652
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();
|
|
@@ -128,15 +130,34 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
128
130
|
await explorBot.stop();
|
|
129
131
|
await showStatsAndExit(1);
|
|
130
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
|
+
}
|
|
131
148
|
const savedPath = explorBot.savePlan();
|
|
132
149
|
const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
|
|
133
150
|
const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
|
|
134
151
|
const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
|
|
135
152
|
const lines = [];
|
|
136
|
-
lines.push('Run
|
|
137
|
-
lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
|
|
138
|
-
lines.push(`\`${cli} test ${planFile}
|
|
139
|
-
|
|
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
|
+
}
|
|
140
161
|
log(parseMarkdownToTerminal(lines.join('\n')));
|
|
141
162
|
await explorBot.stop();
|
|
142
163
|
await showStatsAndExit(0);
|
|
@@ -240,6 +261,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
240
261
|
await showStatsAndExit(1);
|
|
241
262
|
}
|
|
242
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
|
+
});
|
|
243
300
|
addCommonOptions(program
|
|
244
301
|
.command('freesail [startUrl]')
|
|
245
302
|
.description('Continuously explore and navigate to new pages autonomously')
|
|
@@ -328,7 +385,6 @@ program
|
|
|
328
385
|
});
|
|
329
386
|
program
|
|
330
387
|
.command('learn [url] [description]')
|
|
331
|
-
.alias('add-knowledge')
|
|
332
388
|
.description('Add knowledge for URLs')
|
|
333
389
|
.option('-p, --path <path>', 'Working directory path')
|
|
334
390
|
.action(async (url, description, options) => {
|
|
@@ -395,7 +451,7 @@ addCommonOptions(program.command('research <url>').description('Research a page
|
|
|
395
451
|
await showStatsAndExit(1);
|
|
396
452
|
}
|
|
397
453
|
});
|
|
398
|
-
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) => {
|
|
399
455
|
try {
|
|
400
456
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
401
457
|
await explorBot.start();
|
|
@@ -555,19 +611,19 @@ browserCmd
|
|
|
555
611
|
}
|
|
556
612
|
});
|
|
557
613
|
program
|
|
558
|
-
.command('extract-
|
|
559
|
-
.description('Extract built-in planning styles to a directory for customization')
|
|
560
|
-
.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>)')
|
|
561
617
|
.action(async (agent, options) => {
|
|
562
618
|
try {
|
|
563
619
|
const { RulesLoader } = await import('../src/utils/rules-loader.js');
|
|
564
|
-
const targetDir = options.dir || path.resolve(`./rules/${agent}
|
|
565
|
-
const extracted = RulesLoader.
|
|
620
|
+
const targetDir = options.dir || path.resolve(`./rules/${agent}`);
|
|
621
|
+
const extracted = RulesLoader.extractRules(agent, targetDir);
|
|
566
622
|
if (extracted.length === 0) {
|
|
567
|
-
console.log('All
|
|
623
|
+
console.log('All rule files already exist in target directory.');
|
|
568
624
|
}
|
|
569
625
|
else {
|
|
570
|
-
console.log(`\nExtracted ${extracted.length}
|
|
626
|
+
console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
|
|
571
627
|
}
|
|
572
628
|
}
|
|
573
629
|
catch (error) {
|
|
@@ -577,7 +633,6 @@ program
|
|
|
577
633
|
});
|
|
578
634
|
program
|
|
579
635
|
.command('add-rule [agent] [name]')
|
|
580
|
-
.alias('rules:add')
|
|
581
636
|
.description('Create a rule file for an agent')
|
|
582
637
|
.option('--url <pattern>', 'URL pattern for this rule')
|
|
583
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
|
@@ -50,7 +50,7 @@ class Action {
|
|
|
50
50
|
return undefined;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
async capturePageState({ includeScreenshot = false
|
|
53
|
+
async capturePageState({ includeScreenshot = false } = {}) {
|
|
54
54
|
try {
|
|
55
55
|
const currentState = this.stateManager.getCurrentState();
|
|
56
56
|
const stateHash = currentState?.hash || 'screenshot';
|
|
@@ -90,16 +90,14 @@ class Action {
|
|
|
90
90
|
debugLog('Page:', { url, title, size: html.length, html: html.substring(0, 100) });
|
|
91
91
|
// Capture iframe HTML snapshots
|
|
92
92
|
const iframeSnapshots = await this.captureIframeSnapshots(html);
|
|
93
|
-
let ariaSnapshot =
|
|
93
|
+
let ariaSnapshot = null;
|
|
94
94
|
let ariaSnapshotFile = undefined;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
102
|
-
}
|
|
95
|
+
try {
|
|
96
|
+
const page = this.playwrightHelper.page;
|
|
97
|
+
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
103
101
|
}
|
|
104
102
|
if (ariaSnapshot) {
|
|
105
103
|
const ariaFileName = `${stateHash}_${timestamp}.aria.yaml`;
|
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);
|