explorbot 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/bin/explorbot-cli.ts +70 -8
  2. package/boat/api-tester/src/ai/curler-tools.ts +3 -3
  3. package/boat/api-tester/src/ai/curler.ts +1 -1
  4. package/boat/api-tester/src/apibot.ts +2 -2
  5. package/boat/api-tester/src/config.ts +1 -1
  6. package/dist/bin/explorbot-cli.js +70 -7
  7. package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
  8. package/dist/boat/api-tester/src/apibot.js +2 -2
  9. package/dist/package.json +1 -1
  10. package/dist/src/ai/bosun.js +5 -1
  11. package/dist/src/ai/experience-compactor.js +235 -50
  12. package/dist/src/ai/historian.js +13 -6
  13. package/dist/src/ai/navigator.js +62 -62
  14. package/dist/src/ai/pilot.js +22 -0
  15. package/dist/src/ai/planner/subpages.js +1 -30
  16. package/dist/src/ai/planner.js +4 -4
  17. package/dist/src/ai/provider.js +1 -1
  18. package/dist/src/ai/rerunner.js +3 -3
  19. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  20. package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
  21. package/dist/src/ai/researcher/locators.js +1 -1
  22. package/dist/src/ai/researcher/sections.js +8 -1
  23. package/dist/src/ai/researcher.js +4 -11
  24. package/dist/src/ai/tools.js +5 -3
  25. package/dist/src/api/request-store.js +20 -0
  26. package/dist/src/api/xhr-capture.js +19 -3
  27. package/dist/src/command-handler.js +1 -1
  28. package/dist/src/commands/add-rule-command.js +1 -1
  29. package/dist/src/commands/base-command.js +20 -0
  30. package/dist/src/commands/clean-command.js +1 -1
  31. package/dist/src/commands/compact-command.js +138 -0
  32. package/dist/src/commands/context-command.js +7 -1
  33. package/dist/src/commands/drill-command.js +4 -1
  34. package/dist/src/commands/experience-command.js +104 -0
  35. package/dist/src/commands/explore-command.js +33 -7
  36. package/dist/src/commands/freesail-command.js +2 -0
  37. package/dist/src/commands/index.js +7 -3
  38. package/dist/src/commands/init-command.js +2 -2
  39. package/dist/src/commands/learn-command.js +1 -1
  40. package/dist/src/commands/navigate-command.js +4 -1
  41. package/dist/src/commands/plan-clear-command.js +4 -1
  42. package/dist/src/commands/plan-command.js +11 -4
  43. package/dist/src/commands/plan-edit-command.js +1 -1
  44. package/dist/src/commands/plan-load-command.js +4 -1
  45. package/dist/src/commands/plan-reload-command.js +4 -1
  46. package/dist/src/commands/plan-save-command.js +1 -1
  47. package/dist/src/commands/research-command.js +5 -2
  48. package/dist/src/commands/start-command.js +5 -1
  49. package/dist/src/commands/test-command.js +7 -1
  50. package/dist/src/experience-tracker.js +191 -56
  51. package/dist/src/explorbot.js +26 -14
  52. package/dist/src/explorer.js +3 -3
  53. package/dist/src/reporter.js +17 -2
  54. package/dist/src/stats.js +2 -0
  55. package/dist/src/suite.js +1 -1
  56. package/dist/src/utils/error-page.js +10 -0
  57. package/dist/src/utils/logger.js +1 -1
  58. package/dist/src/utils/rules-loader.js +1 -1
  59. package/dist/src/utils/test-files.js +1 -1
  60. package/dist/src/utils/url-matcher.js +50 -0
  61. package/package.json +1 -1
  62. package/src/ai/bosun.ts +5 -1
  63. package/src/ai/experience-compactor.ts +270 -63
  64. package/src/ai/historian.ts +12 -7
  65. package/src/ai/navigator.ts +68 -66
  66. package/src/ai/pilot.ts +22 -0
  67. package/src/ai/planner/subpages.ts +1 -24
  68. package/src/ai/planner.ts +5 -5
  69. package/src/ai/provider.ts +1 -1
  70. package/src/ai/rerunner.ts +3 -3
  71. package/src/ai/researcher/deep-analysis.ts +1 -1
  72. package/src/ai/researcher/fingerprint-worker.ts +1 -1
  73. package/src/ai/researcher/locators.ts +2 -2
  74. package/src/ai/researcher/sections.ts +7 -1
  75. package/src/ai/researcher.ts +4 -11
  76. package/src/ai/task-agent.ts +1 -1
  77. package/src/ai/tools.ts +6 -4
  78. package/src/api/request-store.ts +22 -0
  79. package/src/api/xhr-capture.ts +21 -3
  80. package/src/command-handler.ts +1 -1
  81. package/src/commands/add-rule-command.ts +2 -2
  82. package/src/commands/base-command.ts +26 -1
  83. package/src/commands/clean-command.ts +2 -2
  84. package/src/commands/compact-command.ts +156 -0
  85. package/src/commands/context-command.ts +8 -2
  86. package/src/commands/drill-command.ts +5 -2
  87. package/src/commands/experience-command.ts +125 -0
  88. package/src/commands/explore-command.ts +35 -9
  89. package/src/commands/freesail-command.ts +2 -0
  90. package/src/commands/index.ts +7 -3
  91. package/src/commands/init-command.ts +2 -2
  92. package/src/commands/learn-command.ts +2 -2
  93. package/src/commands/navigate-command.ts +5 -2
  94. package/src/commands/plan-clear-command.ts +5 -2
  95. package/src/commands/plan-command.ts +12 -5
  96. package/src/commands/plan-edit-command.ts +2 -2
  97. package/src/commands/plan-load-command.ts +5 -2
  98. package/src/commands/plan-reload-command.ts +5 -2
  99. package/src/commands/plan-save-command.ts +2 -2
  100. package/src/commands/research-command.ts +6 -3
  101. package/src/commands/start-command.ts +6 -2
  102. package/src/commands/test-command.ts +8 -2
  103. package/src/experience-tracker.ts +220 -71
  104. package/src/explorbot.ts +28 -15
  105. package/src/explorer.ts +3 -3
  106. package/src/reporter.ts +17 -3
  107. package/src/stats.ts +4 -0
  108. package/src/suite.ts +1 -1
  109. package/src/utils/error-page.ts +10 -0
  110. package/src/utils/logger.ts +1 -1
  111. package/src/utils/rules-loader.ts +1 -1
  112. package/src/utils/test-files.ts +1 -1
  113. package/src/utils/url-matcher.ts +43 -0
@@ -39,7 +39,7 @@ function cleanDirectoryContents(dirPath) {
39
39
  export class CleanCommand extends BaseCommand {
40
40
  name = 'clean';
41
41
  description = 'Clean files: clean [states|research|plans|experiences|output]';
42
- suggestions = Object.keys(CLEAN_TARGETS).map((t) => `/clean ${t}`);
42
+ suggestions = Object.entries(CLEAN_TARGETS).map(([name, target]) => ({ command: `clean ${name}`, hint: `clean ${target.description}` }));
43
43
  async execute(args) {
44
44
  const target = args.trim().toLowerCase();
45
45
  if (!target) {
@@ -0,0 +1,138 @@
1
+ import { basename } from 'node:path';
2
+ import chalk from 'chalk';
3
+ import { tag } from '../utils/logger.js';
4
+ import { BaseCommand } from './base-command.js';
5
+ const COMPACT_THRESHOLD = 5000;
6
+ export class CompactCommand extends BaseCommand {
7
+ name = 'compact';
8
+ description = 'Compact stored experience files; optionally filtered by filename or URL substring';
9
+ options = [
10
+ { flags: '--dry-run', description: 'Preview without running AI or writing files' },
11
+ { flags: '--no-merge', description: 'Skip the cross-URL merge step when compacting all' },
12
+ ];
13
+ suggestions = [];
14
+ async execute(args) {
15
+ const { opts, args: remaining } = this.parseArgs(args);
16
+ const target = remaining[0];
17
+ const dryRun = !!opts.dryRun;
18
+ const merge = opts.merge !== false;
19
+ const tracker = this.explorBot.getExperienceTracker();
20
+ const files = this.resolveTarget(tracker, target);
21
+ if (files === null) {
22
+ tag('info').log('No experience files found.');
23
+ return;
24
+ }
25
+ if (files.length === 0) {
26
+ tag('info').log(`No experience files match target "${target}"`);
27
+ return;
28
+ }
29
+ const compactor = this.explorBot.agentExperienceCompactor();
30
+ if (dryRun) {
31
+ this.printDryRun(compactor, files, target, !target && merge);
32
+ this.suggestions = this.buildDryRunSuggestions(compactor, files, target);
33
+ return;
34
+ }
35
+ if (!target) {
36
+ const { merged, compacted } = await this.runFullSweep(compactor, merge);
37
+ tag('success').log(`Done. ${merged} merged, ${compacted} compacted.`);
38
+ this.suggestions = [{ command: 'experience', hint: 'list stored experiences' }];
39
+ return;
40
+ }
41
+ tag('info').log(`Compacting ${files.length} experience file${files.length === 1 ? '' : 's'}…`);
42
+ const compacted = await compactor.compactFiles(files);
43
+ if (compacted === 0) {
44
+ tag('info').log('No experience changes — nothing to strip and all content under size limit.');
45
+ }
46
+ else {
47
+ tag('success').log(`Compacted ${compacted}/${files.length} experience file${files.length === 1 ? '' : 's'}.`);
48
+ }
49
+ this.suggestions = [{ command: `experience ${target}`, hint: 'view updated file' }];
50
+ }
51
+ buildDryRunSuggestions(compactor, files, target) {
52
+ const wouldTouch = files.filter((f) => {
53
+ const stripped = compactor.stripNonUsefulEntries(f.content);
54
+ return stripped !== f.content || stripped.length >= COMPACT_THRESHOLD || compactor.isRecent(f);
55
+ });
56
+ if (wouldTouch.length === 0)
57
+ return [];
58
+ const argsPart = target ? ` ${target}` : '';
59
+ const count = wouldTouch.length;
60
+ return [{ command: `compact${argsPart}`, hint: `run compaction for real (${count} file${count === 1 ? '' : 's'} will be touched)` }];
61
+ }
62
+ async runFullSweep(compactor, merge) {
63
+ if (!merge) {
64
+ tag('info').log('Compacting all experience files (merge skipped)…');
65
+ const compacted = await compactor.compactFiles(this.explorBot.getExperienceTracker().getAllExperience());
66
+ return { merged: 0, compacted };
67
+ }
68
+ tag('info').log('Merging experience files with similar URLs, then compacting all…');
69
+ return compactor.compactAllExperiences();
70
+ }
71
+ resolveTarget(tracker, target) {
72
+ const all = tracker.getAllExperience();
73
+ if (all.length === 0)
74
+ return null;
75
+ if (!target)
76
+ return all;
77
+ if (target.endsWith('.md')) {
78
+ const bare = target.slice(0, -3);
79
+ const byFilename = all.find((f) => basename(f.filePath, '.md') === bare);
80
+ return byFilename ? [byFilename] : [];
81
+ }
82
+ const filter = target.toLowerCase();
83
+ return all.filter((f) => (f.data.url || '').toLowerCase().includes(filter));
84
+ }
85
+ printDryRun(compactor, files, target, willMerge) {
86
+ const lines = [];
87
+ const title = target ? `Dry run — target: "${target}"` : 'Dry run — full sweep';
88
+ lines.push(chalk.bold.underline.cyan(title));
89
+ if (willMerge) {
90
+ lines.push(chalk.dim('Merge step would run across all files before compaction.'));
91
+ }
92
+ let wouldStrip = 0;
93
+ let wouldAiReview = 0;
94
+ let wouldAiCompact = 0;
95
+ let totalChars = 0;
96
+ for (const file of files) {
97
+ const chars = file.content.length;
98
+ totalChars += chars;
99
+ const stripped = compactor.stripNonUsefulEntries(file.content);
100
+ const strippedChars = stripped.length;
101
+ const charsRemoved = chars - strippedChars;
102
+ const willStrip = stripped !== file.content;
103
+ const willReview = compactor.isRecent(file);
104
+ const willAi = strippedChars >= COMPACT_THRESHOLD;
105
+ if (willStrip)
106
+ wouldStrip++;
107
+ if (willReview)
108
+ wouldAiReview++;
109
+ if (willAi)
110
+ wouldAiCompact++;
111
+ const name = basename(file.filePath);
112
+ const url = file.data.url || chalk.dim('(no url)');
113
+ const markers = [];
114
+ if (willStrip) {
115
+ const label = charsRemoved > 0 ? `✂ strip -${charsRemoved} chars` : '✂ strip (rewrite)';
116
+ markers.push(chalk.yellow(label));
117
+ }
118
+ if (willReview)
119
+ markers.push(chalk.magenta('✓ ai-review'));
120
+ if (willAi)
121
+ markers.push(chalk.yellow('✎ ai-compact'));
122
+ if (markers.length === 0)
123
+ markers.push(chalk.dim('· skip'));
124
+ lines.push('');
125
+ lines.push(chalk.bold.green(name));
126
+ lines.push(` ${chalk.cyan(url)}`);
127
+ lines.push(` ${chalk.dim('size:')} ${chars} chars ${markers.join(' ')}`);
128
+ }
129
+ lines.push('');
130
+ lines.push(chalk.bold('Summary'));
131
+ lines.push(` ${chalk.dim('Files matched:')} ${chalk.bold(String(files.length))}`);
132
+ lines.push(` ${chalk.dim('Would strip:')} ${chalk.bold(String(wouldStrip))} ${chalk.dim('(remove legacy / FAILED / Visual click, rename to FLOW/ACTION)')}`);
133
+ lines.push(` ${chalk.dim('Would ai-review:')} ${chalk.bold(String(wouldAiReview))} ${chalk.dim('(recent files ≤30d — quality/reusability check)')}`);
134
+ lines.push(` ${chalk.dim('Would ai-compact:')} ${chalk.bold(String(wouldAiCompact))} ${chalk.dim(`(over ${COMPACT_THRESHOLD} chars after strip)`)}`);
135
+ lines.push(` ${chalk.dim('Total chars:')} ${chalk.bold(String(totalChars))}`);
136
+ tag('info').log(lines.join('\n'));
137
+ }
138
+ }
@@ -7,7 +7,13 @@ import { BaseCommand } from './base-command.js';
7
7
  export class ContextCommand extends BaseCommand {
8
8
  name = 'context';
9
9
  description = 'Show page context summary (URL, headings, experience, knowledge, ARIA, HTML, research)';
10
- suggestions = ['context:aria', 'context:html', 'context:knowledge', 'context:experience', 'context:data'];
10
+ suggestions = [
11
+ { command: 'context:aria', hint: 'show page ARIA snapshot' },
12
+ { command: 'context:html', hint: 'show page HTML' },
13
+ { command: 'context:knowledge', hint: 'show relevant knowledge' },
14
+ { command: 'context:experience', hint: 'show relevant experience' },
15
+ { command: 'context:data', hint: 'show captured page data' },
16
+ ];
11
17
  options = [
12
18
  { flags: '--visual', description: 'Include annotated screenshot' },
13
19
  { flags: '--screenshot', description: 'Include annotated screenshot' },
@@ -2,7 +2,10 @@ import { BaseCommand } from './base-command.js';
2
2
  export class DrillCommand extends BaseCommand {
3
3
  name = 'drill';
4
4
  description = 'Drill all components on current page to learn interactions';
5
- suggestions = ['/research - to see UI map first', '/navigate <page> - to go to another page'];
5
+ suggestions = [
6
+ { command: 'research', hint: 'see UI map first' },
7
+ { command: 'navigate <page>', hint: 'go to another page' },
8
+ ];
6
9
  async execute(args) {
7
10
  const knowledgePath = this.parseKnowledgeArg(args);
8
11
  const maxComponents = this.parseMaxArg(args);
@@ -0,0 +1,104 @@
1
+ import chalk from 'chalk';
2
+ import { tag } from '../utils/logger.js';
3
+ import { BaseCommand } from './base-command.js';
4
+ export class ExperienceCommand extends BaseCommand {
5
+ name = 'experience';
6
+ description = 'List stored experiences; filter by filename or URL substring; expand a section by ref (e.g. LW1)';
7
+ options = [
8
+ { flags: '--recent', description: 'Only files modified within the last 30 days' },
9
+ { flags: '--old', description: 'Only files modified more than 30 days ago' },
10
+ ];
11
+ suggestions = [];
12
+ async execute(args) {
13
+ const { opts, args: remaining } = this.parseArgs(args);
14
+ if (opts.recent && opts.old) {
15
+ tag('info').log('Flags --recent and --old are mutually exclusive.');
16
+ return;
17
+ }
18
+ const recency = opts.recent ? 'recent' : opts.old ? 'old' : undefined;
19
+ const tracker = this.explorBot.getExperienceTracker();
20
+ const [first, second] = remaining;
21
+ const combinedRef = first?.match(/^([A-Z]+)[.\-]?(\d+)$/i);
22
+ if (combinedRef) {
23
+ this.expand(tracker, combinedRef[1].toUpperCase(), Number(combinedRef[2]));
24
+ return;
25
+ }
26
+ if (first && /^[A-Z]+$/i.test(first) && second && /^\d+$/.test(second)) {
27
+ this.expand(tracker, first.toUpperCase(), Number(second));
28
+ return;
29
+ }
30
+ const secondRef = second?.match(/^([A-Z]+)[.\-]?(\d+)$/i);
31
+ if (first && secondRef) {
32
+ this.expand(tracker, secondRef[1].toUpperCase(), Number(secondRef[2]), first);
33
+ return;
34
+ }
35
+ if (first && second && /^\d+$/.test(second)) {
36
+ const toc = tracker.listAllExperienceToc(first, { recency });
37
+ if (toc.length === 0) {
38
+ tag('info').log(`No experience found matching: ${first}`);
39
+ return;
40
+ }
41
+ this.expand(tracker, toc[0].fileTag, Number(second), first);
42
+ return;
43
+ }
44
+ const toc = tracker.listAllExperienceToc(first, { recency });
45
+ if (toc.length === 0) {
46
+ const scope = recency === 'recent' ? ' (recent only)' : recency === 'old' ? ' (old only)' : '';
47
+ tag('info').log(first ? `No experience found matching: ${first}${scope}` : `No experience files found${scope}. Experience is recorded automatically during test sessions.`);
48
+ return;
49
+ }
50
+ tag('info').log(this.formatToc(toc, first, recency));
51
+ const hints = [];
52
+ const exampleRef = `${toc[0].fileTag}1`;
53
+ hints.push({ command: `experience ${exampleRef}`, hint: 'read a section' });
54
+ const overloaded = toc.filter((entry) => entry.sections.length >= 10);
55
+ for (const entry of overloaded) {
56
+ hints.push({ command: `compact ${entry.fileHash}.md`, hint: `compact this file (${entry.sections.length} sections)` });
57
+ }
58
+ this.suggestions = hints;
59
+ }
60
+ expand(tracker, fileTag, sectionIndex, urlFilter) {
61
+ const section = tracker.getExperienceSectionByTag(fileTag, sectionIndex, urlFilter);
62
+ if (!section) {
63
+ tag('info').log(`No section ${fileTag}${sectionIndex} found${urlFilter ? ` for URL matching: ${urlFilter}` : ''}`);
64
+ return;
65
+ }
66
+ tag('info').log(`${chalk.dim('File:')} ${chalk.bold.green(`${section.fileHash}.md`)}`);
67
+ tag('info').log(`${chalk.dim('URL:')} ${chalk.bold.cyan(section.url)}`);
68
+ tag('info').log(`${chalk.green(`${fileTag}${sectionIndex}`)} ${chalk.bold(section.title)}`);
69
+ tag('multiline').log(this.stripLeadingHeading(section.content.trim()));
70
+ }
71
+ formatToc(toc, urlFilter, recency) {
72
+ const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
73
+ const lines = [];
74
+ const scope = recency === 'recent' ? ' (recent, ≤30d)' : recency === 'old' ? ' (old, >30d)' : '';
75
+ const title = urlFilter ? `Experience matching "${urlFilter}"${scope}` : `Stored experience${scope}`;
76
+ lines.push(chalk.bold.underline.cyan(title));
77
+ for (const entry of toc) {
78
+ lines.push('');
79
+ lines.push(chalk.bold.green(`${entry.fileHash}.md`));
80
+ lines.push(` ${chalk.cyan(entry.url)}`);
81
+ for (let i = 0; i < entry.sections.length; i++) {
82
+ const section = entry.sections[i];
83
+ const isLast = i === entry.sections.length - 1;
84
+ const branch = isLast ? '└─' : '├─';
85
+ const ref = chalk.yellow(`${entry.fileTag}${section.index}`);
86
+ lines.push(` ${chalk.dim(branch)} ${ref} ${chalk.dim(':')} ${section.title}`);
87
+ }
88
+ }
89
+ const urls = new Set(toc.map((e) => e.url)).size;
90
+ lines.push('');
91
+ lines.push(chalk.bold('Summary'));
92
+ lines.push(` ${chalk.dim('URLs:')} ${chalk.bold(String(urls))}`);
93
+ lines.push(` ${chalk.dim('Files:')} ${chalk.bold(String(toc.length))}`);
94
+ lines.push(` ${chalk.dim('Entries:')} ${chalk.bold(String(totalSections))}`);
95
+ return lines.join('\n');
96
+ }
97
+ stripLeadingHeading(body) {
98
+ const lines = body.split('\n');
99
+ if (lines[0]?.match(/^#{1,6}\s/)) {
100
+ return lines.slice(1).join('\n').trimStart();
101
+ }
102
+ return body;
103
+ }
104
+ }
@@ -1,15 +1,24 @@
1
- import figureSet from 'figures';
2
1
  import path from 'node:path';
2
+ import figureSet from 'figures';
3
3
  import { getStyles } from '../ai/planner/styles.js';
4
+ import { Stats } from '../stats.js';
4
5
  import { getCliName } from "../utils/cli-name.js";
5
- import { jsonToTable } from '../utils/markdown-parser.js';
6
+ import { ErrorPageError } from "../utils/error-page.js";
6
7
  import { tag } from '../utils/logger.js';
8
+ import { jsonToTable } from '../utils/markdown-parser.js';
7
9
  import { BaseCommand } from './base-command.js';
8
10
  export class ExploreCommand extends BaseCommand {
9
11
  name = 'explore';
10
12
  description = 'Start web exploration';
11
- options = [{ flags: '--max-tests <number>', description: 'Maximum number of tests to run' }];
12
- suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
13
+ options = [
14
+ { flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
15
+ { flags: '--focus <feature>', description: 'Focus area for exploration' },
16
+ ];
17
+ suggestions = [
18
+ { command: 'navigate <page>', hint: 'go to another page' },
19
+ { command: 'research', hint: 'analyze current page' },
20
+ { command: 'plan <feature>', hint: 'plan testing' },
21
+ ];
13
22
  maxTests;
14
23
  testsRun = 0;
15
24
  completedPlans = [];
@@ -18,14 +27,16 @@ export class ExploreCommand extends BaseCommand {
18
27
  if (opts.maxTests) {
19
28
  this.maxTests = Number.parseInt(opts.maxTests, 10);
20
29
  }
21
- const feature = remaining.join(' ') || undefined;
30
+ const feature = opts.focus || remaining.join(' ') || undefined;
31
+ Stats.mode ??= 'explore';
32
+ Stats.focus ??= feature;
22
33
  const mainUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
23
34
  await this.runAllStyles(mainUrl, feature);
24
35
  const mainPlan = this.explorBot.getCurrentPlan();
25
36
  if (!mainPlan)
26
37
  return;
27
38
  this.completedPlans.push(mainPlan);
28
- if (!this.isLimitReached()) {
39
+ if (!feature && !this.isLimitReached()) {
29
40
  const planner = this.explorBot.agentPlanner();
30
41
  while (true) {
31
42
  if (this.isLimitReached())
@@ -66,11 +77,26 @@ export class ExploreCommand extends BaseCommand {
66
77
  const opts = { fresh, style, completedPlans };
67
78
  if (fresh && parentPlan)
68
79
  opts.extend = parentPlan;
69
- await this.explorBot.plan(feature, opts);
80
+ await this.planWithRetry(feature, opts, pageUrl);
70
81
  await this.runPendingTests();
71
82
  fresh = false;
72
83
  }
73
84
  }
85
+ async planWithRetry(feature, opts, pageUrl) {
86
+ await this.explorBot.plan(feature, opts);
87
+ if (!this.explorBot.lastPlanError)
88
+ return;
89
+ if (this.explorBot.lastPlanError instanceof ErrorPageError) {
90
+ throw this.explorBot.lastPlanError;
91
+ }
92
+ tag('info').log(`Retrying planning style '${opts.style}'...`);
93
+ if (pageUrl)
94
+ await this.explorBot.visit(pageUrl);
95
+ await this.explorBot.plan(feature, opts);
96
+ if (this.explorBot.lastPlanError) {
97
+ tag('warning').log(`Planning style '${opts.style}' failed after retry, skipping`);
98
+ }
99
+ }
74
100
  printResults(savedPath) {
75
101
  const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title })));
76
102
  if (allTests.length === 0)
@@ -1,5 +1,6 @@
1
1
  import { Planner } from '../ai/planner.js';
2
2
  import { Researcher } from '../ai/researcher.js';
3
+ import { Stats } from '../stats.js';
3
4
  import { tag } from '../utils/logger.js';
4
5
  import { loop } from '../utils/loop.js';
5
6
  import { BaseCommand } from './base-command.js';
@@ -16,6 +17,7 @@ export class FreesailCommand extends BaseCommand {
16
17
  { flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
17
18
  ];
18
19
  async execute(args) {
20
+ Stats.mode = 'freesail';
19
21
  const { opts } = this.parseArgs(args);
20
22
  let strategy;
21
23
  if (opts.deep)
@@ -1,19 +1,21 @@
1
+ import { AddRuleCommand } from './add-rule-command.js';
1
2
  import { CleanCommand } from './clean-command.js';
2
- import { DebugCommand } from './debug-command.js';
3
+ import { CompactCommand } from './compact-command.js';
3
4
  import { ContextAriaCommand } from './context-aria-command.js';
4
5
  import { ContextCommand } from './context-command.js';
5
6
  import { ContextDataCommand } from './context-data-command.js';
6
7
  import { ContextExperienceCommand } from './context-experience-command.js';
7
8
  import { ContextHtmlCommand } from './context-html-command.js';
8
9
  import { ContextKnowledgeCommand } from './context-knowledge-command.js';
10
+ import { DebugCommand } from './debug-command.js';
9
11
  import { DrillCommand } from './drill-command.js';
10
12
  import { ExitCommand } from './exit-command.js';
13
+ import { ExperienceCommand } from './experience-command.js';
11
14
  import { ExploreCommand } from './explore-command.js';
12
15
  import { FreesailCommand } from './freesail-command.js';
13
16
  import { HelpCommand } from './help-command.js';
14
- import { AddRuleCommand } from './add-rule-command.js';
15
- import { LearnCommand } from './learn-command.js';
16
17
  import { KnowsCommand } from './knows-command.js';
18
+ import { LearnCommand } from './learn-command.js';
17
19
  import { NavigateCommand } from './navigate-command.js';
18
20
  import { PathCommand } from './path-command.js';
19
21
  import { PlanClearCommand } from './plan-clear-command.js';
@@ -47,6 +49,8 @@ const commandClasses = [
47
49
  PathCommand,
48
50
  LearnCommand,
49
51
  KnowsCommand,
52
+ ExperienceCommand,
53
+ CompactCommand,
50
54
  AddRuleCommand,
51
55
  ContextCommand,
52
56
  ContextAriaCommand,
@@ -1,9 +1,9 @@
1
1
  import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, extname, join, resolve } from 'node:path';
3
- import { log, tag } from '../utils/logger.js';
4
- import dedent from 'dedent';
5
3
  import chalk from 'chalk';
4
+ import dedent from 'dedent';
6
5
  import { getCliName } from "../utils/cli-name.js";
6
+ import { log, tag } from '../utils/logger.js';
7
7
  const DEFAULT_CONFIG_TEMPLATE = `import { createOpenRouter } from '@openrouter/ai-sdk-provider';
8
8
  // import { '<your provider here>' } from '<your provider package here>';
9
9
 
@@ -5,7 +5,7 @@ import { BaseCommand } from './base-command.js';
5
5
  export class LearnCommand extends BaseCommand {
6
6
  name = 'learn';
7
7
  description = 'Store knowledge for current page';
8
- suggestions = ['/knows - to view all knowledge'];
8
+ suggestions = [{ command: 'knows', hint: 'view all knowledge' }];
9
9
  async execute(args) {
10
10
  const note = args.trim();
11
11
  if (!note) {
@@ -3,7 +3,10 @@ import { BaseCommand } from './base-command.js';
3
3
  export class NavigateCommand extends BaseCommand {
4
4
  name = 'navigate';
5
5
  description = 'Navigate to URI or state using AI';
6
- suggestions = ['/research - to analyze current page', '/plan <feature> - to plan testing'];
6
+ suggestions = [
7
+ { command: 'research', hint: 'analyze current page' },
8
+ { command: 'plan <feature>', hint: 'plan testing' },
9
+ ];
7
10
  async execute(args) {
8
11
  const destination = args.trim();
9
12
  if (!destination) {
@@ -3,7 +3,10 @@ import { BaseCommand } from './base-command.js';
3
3
  export class PlanClearCommand extends BaseCommand {
4
4
  name = 'plan:clear';
5
5
  description = 'Clear current plan and create a new one';
6
- suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
6
+ suggestions = [
7
+ { command: 'test', hint: 'launch first test' },
8
+ { command: 'test *', hint: 'launch all tests' },
9
+ ];
7
10
  async execute(args) {
8
11
  this.explorBot.clearPlan();
9
12
  tag('success').log('Plan cleared');
@@ -6,7 +6,11 @@ import { BaseCommand } from './base-command.js';
6
6
  export class PlanCommand extends BaseCommand {
7
7
  name = 'plan';
8
8
  description = 'Plan testing for a feature';
9
- suggestions = ['/test - to launch first test', '/test * - to launch all tests', 'Edit the plan in file and call /plan:reload to update it'];
9
+ suggestions = [
10
+ { command: 'test', hint: 'launch first test' },
11
+ { command: 'test *', hint: 'launch all tests' },
12
+ { command: 'plan:reload', hint: 'after editing the plan file, reload it' },
13
+ ];
10
14
  options = [
11
15
  { flags: '--fresh', description: 'Regenerate plan from scratch' },
12
16
  { flags: '--clear', description: 'Clear plan before regenerating' },
@@ -50,13 +54,16 @@ export class PlanCommand extends BaseCommand {
50
54
  }
51
55
  }
52
56
  updateSuggestions() {
53
- this.suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
57
+ this.suggestions = [
58
+ { command: 'test', hint: 'launch first test' },
59
+ { command: 'test *', hint: 'launch all tests' },
60
+ ];
54
61
  const suite = this.explorBot.getSuite();
55
62
  if (suite && suite.automatedTestCount > 0) {
56
63
  for (const f of suite.getAutomatedTestFiles()) {
57
- this.suggestions.push(`/rerun ${path.relative(process.cwd(), f)} - re-run automated tests`);
64
+ this.suggestions.push({ command: `rerun ${path.relative(process.cwd(), f)}`, hint: 're-run automated tests' });
58
65
  }
59
66
  }
60
- this.suggestions.push('Edit the plan in file and call /plan:reload to update it');
67
+ this.suggestions.push({ command: 'plan:reload', hint: 'after editing the plan file, reload it' });
61
68
  }
62
69
  }
@@ -2,6 +2,6 @@ import { BaseCommand } from './base-command.js';
2
2
  export class PlanEditCommand extends BaseCommand {
3
3
  name = 'plan:edit';
4
4
  description = 'Open test plan editor';
5
- suggestions = ['/plan:edit - toggle tests on/off'];
5
+ suggestions = [{ command: 'plan:edit', hint: 'toggle tests on/off' }];
6
6
  async execute(_args) { }
7
7
  }
@@ -3,7 +3,10 @@ import { BaseCommand } from './base-command.js';
3
3
  export class PlanLoadCommand extends BaseCommand {
4
4
  name = 'plan:load';
5
5
  description = 'Load plan from file';
6
- suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
6
+ suggestions = [
7
+ { command: 'test', hint: 'launch first test' },
8
+ { command: 'test *', hint: 'launch all tests' },
9
+ ];
7
10
  async execute(args) {
8
11
  const filename = args.trim();
9
12
  if (!filename) {
@@ -3,7 +3,10 @@ import { BaseCommand } from './base-command.js';
3
3
  export class PlanReloadCommand extends BaseCommand {
4
4
  name = 'plan:reload';
5
5
  description = 'Clear current plan and regenerate';
6
- suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
6
+ suggestions = [
7
+ { command: 'test', hint: 'launch first test' },
8
+ { command: 'test *', hint: 'launch all tests' },
9
+ ];
7
10
  async execute(args) {
8
11
  const currentPlan = this.explorBot.getCurrentPlan();
9
12
  if (!currentPlan) {
@@ -4,7 +4,7 @@ import { BaseCommand } from './base-command.js';
4
4
  export class PlanSaveCommand extends BaseCommand {
5
5
  name = 'plan:save';
6
6
  description = 'Save current plan to file';
7
- suggestions = ['/test - to launch first test'];
7
+ suggestions = [{ command: 'test', hint: 'launch first test' }];
8
8
  async execute(args) {
9
9
  const plan = this.explorBot.getCurrentPlan();
10
10
  if (!plan) {
@@ -5,7 +5,10 @@ import { BaseCommand } from './base-command.js';
5
5
  export class ResearchCommand extends BaseCommand {
6
6
  name = 'research';
7
7
  description = 'Research current page or navigate to URI and research. Use --deep to explore interactive elements by clicking them. Use --data to include page data.';
8
- suggestions = ['/navigate <page> - to go to another page', '/plan <feature> - to plan testing'];
8
+ suggestions = [
9
+ { command: 'navigate <page>', hint: 'go to another page' },
10
+ { command: 'plan <feature>', hint: 'plan testing' },
11
+ ];
9
12
  options = [
10
13
  { flags: '--data', description: 'Include page data' },
11
14
  { flags: '--deep', description: 'Explore interactive elements by clicking them' },
@@ -37,7 +40,7 @@ export class ResearchCommand extends BaseCommand {
37
40
  tag('info').log(`Research file: ${join(outputDir, 'research', `${state.hash}.md`)}`);
38
41
  }
39
42
  if (!enableDeep) {
40
- this.suggestions = ['/research <page> --deep - analyze page for all expandable elements and interactions'];
43
+ this.suggestions = [{ command: 'research <page> --deep', hint: 'analyze page for all expandable elements and interactions' }];
41
44
  }
42
45
  }
43
46
  }
@@ -3,7 +3,11 @@ import { ExploreCommand } from './explore-command.js';
3
3
  export class StartCommand extends BaseCommand {
4
4
  name = 'start';
5
5
  description = 'Start web exploration';
6
- suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
6
+ suggestions = [
7
+ { command: 'navigate <page>', hint: 'go to another page' },
8
+ { command: 'research', hint: 'analyze current page' },
9
+ { command: 'plan <feature>', hint: 'plan testing' },
10
+ ];
7
11
  async execute(args) {
8
12
  await new ExploreCommand(this.explorBot).execute(args);
9
13
  }
@@ -1,12 +1,18 @@
1
+ import { Stats } from '../stats.js';
1
2
  import { Test } from '../test-plan.js';
2
3
  import { tag } from '../utils/logger.js';
3
4
  import { BaseCommand } from './base-command.js';
4
5
  export class TestCommand extends BaseCommand {
5
6
  name = 'test';
6
7
  description = 'Launch tester agent to execute test scenarios';
7
- suggestions = ['/test - to run next test', '/plan - to create new plan'];
8
+ suggestions = [
9
+ { command: 'test', hint: 'run next test' },
10
+ { command: 'plan', hint: 'create new plan' },
11
+ ];
8
12
  async execute(args) {
9
13
  const plan = this.explorBot.getCurrentPlan();
14
+ Stats.mode = 'test';
15
+ Stats.focus = plan?.title;
10
16
  const toExecute = [];
11
17
  const requirePlan = () => {
12
18
  if (!plan)