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
@@ -155,7 +155,7 @@ export class CommandHandler implements InputManager {
155
155
  this.runningCommands.add(command.name);
156
156
  try {
157
157
  await command.execute(argsString);
158
- command.suggestions.forEach((s) => tag('step').log(s));
158
+ command.printSuggestions();
159
159
  } catch (error: any) {
160
160
  if (error?.name === 'AbortError') throw error;
161
161
  tag('error').log(`/${command.name} failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -3,12 +3,12 @@ import { join } from 'node:path';
3
3
  import { render } from 'ink';
4
4
  import React from 'react';
5
5
  import { tag } from '../utils/logger.js';
6
- import { BaseCommand } from './base-command.js';
6
+ import { BaseCommand, type Suggestion } from './base-command.js';
7
7
 
8
8
  export class AddRuleCommand extends BaseCommand {
9
9
  name = 'add-rule';
10
10
  description = 'Create a rule file for an agent';
11
- suggestions = ['/add-rule researcher check-tooltips'];
11
+ suggestions: Suggestion[] = [{ command: 'add-rule researcher check-tooltips', hint: 'example — add a rule for the researcher agent' }];
12
12
 
13
13
  async execute(args: string): Promise<void> {
14
14
  const parts = args.trim().split(/\s+/);
@@ -1,18 +1,27 @@
1
+ import chalk from 'chalk';
1
2
  import { Command } from 'commander';
3
+ import { isInteractive } from '../ai/task-agent.js';
2
4
  import type { ExplorBot } from '../explorbot.js';
5
+ import { getCliName } from '../utils/cli-name.js';
6
+ import { tag } from '../utils/logger.js';
3
7
 
4
8
  export interface CommandOption {
5
9
  flags: string;
6
10
  description: string;
7
11
  }
8
12
 
13
+ export interface Suggestion {
14
+ command?: string;
15
+ hint: string;
16
+ }
17
+
9
18
  export abstract class BaseCommand {
10
19
  abstract name: string;
11
20
  abstract description: string;
12
21
  aliases: string[] = [];
13
22
  options: CommandOption[] = [];
14
23
  tuiEnabled = true;
15
- suggestions: string[] = [];
24
+ suggestions: Suggestion[] = [];
16
25
 
17
26
  protected explorBot: ExplorBot;
18
27
 
@@ -26,6 +35,22 @@ export abstract class BaseCommand {
26
35
  return this.name === commandName || this.aliases.includes(commandName);
27
36
  }
28
37
 
38
+ printSuggestions(): void {
39
+ if (this.suggestions.length === 0) return;
40
+ const prefix = isInteractive() ? '/' : `${getCliName()} `;
41
+ tag('info').log('');
42
+ tag('info').log(chalk.bold('Suggested:'));
43
+ for (const { command, hint } of this.suggestions) {
44
+ tag('info').log('');
45
+ if (!command) {
46
+ tag('info').log(chalk.dim(hint));
47
+ continue;
48
+ }
49
+ tag('info').log(chalk.dim(`${hint}:`));
50
+ tag('info').log(` ${chalk.yellow(`${prefix}${command}`)}`);
51
+ }
52
+ }
53
+
29
54
  protected parseArgs(args: string): { opts: Record<string, string | boolean>; args: string[] } {
30
55
  const cmd = new Command();
31
56
  cmd.exitOverride();
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { ConfigParser, outputPath } from '../config.js';
4
4
  import { tag } from '../utils/logger.js';
5
- import { BaseCommand } from './base-command.js';
5
+ import { BaseCommand, type Suggestion } from './base-command.js';
6
6
 
7
7
  export const CLEAN_TARGETS: Record<string, { description: string; getDir: () => string }> = {
8
8
  states: { description: 'page states', getDir: () => outputPath('states') },
@@ -41,7 +41,7 @@ function cleanDirectoryContents(dirPath: string): number {
41
41
  export class CleanCommand extends BaseCommand {
42
42
  name = 'clean';
43
43
  description = 'Clean files: clean [states|research|plans|experiences|output]';
44
- suggestions = Object.keys(CLEAN_TARGETS).map((t) => `/clean ${t}`);
44
+ suggestions: Suggestion[] = Object.entries(CLEAN_TARGETS).map(([name, target]) => ({ command: `clean ${name}`, hint: `clean ${target.description}` }));
45
45
 
46
46
  async execute(args: string): Promise<void> {
47
47
  const target = args.trim().toLowerCase();
@@ -0,0 +1,156 @@
1
+ import { basename } from 'node:path';
2
+ import chalk from 'chalk';
3
+ import type { ExperienceFile } from '../ai/experience-compactor.js';
4
+ import type { ExperienceTracker } from '../experience-tracker.js';
5
+ import { tag } from '../utils/logger.js';
6
+ import { BaseCommand, type Suggestion } from './base-command.js';
7
+
8
+ const COMPACT_THRESHOLD = 5000;
9
+
10
+ export class CompactCommand extends BaseCommand {
11
+ name = 'compact';
12
+ description = 'Compact stored experience files; optionally filtered by filename or URL substring';
13
+ options = [
14
+ { flags: '--dry-run', description: 'Preview without running AI or writing files' },
15
+ { flags: '--no-merge', description: 'Skip the cross-URL merge step when compacting all' },
16
+ ];
17
+ suggestions: Suggestion[] = [];
18
+
19
+ async execute(args: string): Promise<void> {
20
+ const { opts, args: remaining } = this.parseArgs(args);
21
+ const target = remaining[0];
22
+ const dryRun = !!opts.dryRun;
23
+ const merge = opts.merge !== false;
24
+
25
+ const tracker = this.explorBot.getExperienceTracker();
26
+ const files = this.resolveTarget(tracker, target);
27
+
28
+ if (files === null) {
29
+ tag('info').log('No experience files found.');
30
+ return;
31
+ }
32
+ if (files.length === 0) {
33
+ tag('info').log(`No experience files match target "${target}"`);
34
+ return;
35
+ }
36
+
37
+ const compactor = this.explorBot.agentExperienceCompactor();
38
+
39
+ if (dryRun) {
40
+ this.printDryRun(compactor, files, target, !target && merge);
41
+ this.suggestions = this.buildDryRunSuggestions(compactor, files, target);
42
+ return;
43
+ }
44
+
45
+ if (!target) {
46
+ const { merged, compacted } = await this.runFullSweep(compactor, merge);
47
+ tag('success').log(`Done. ${merged} merged, ${compacted} compacted.`);
48
+ this.suggestions = [{ command: 'experience', hint: 'list stored experiences' }];
49
+ return;
50
+ }
51
+
52
+ tag('info').log(`Compacting ${files.length} experience file${files.length === 1 ? '' : 's'}…`);
53
+ const compacted = await compactor.compactFiles(files);
54
+ if (compacted === 0) {
55
+ tag('info').log('No experience changes — nothing to strip and all content under size limit.');
56
+ } else {
57
+ tag('success').log(`Compacted ${compacted}/${files.length} experience file${files.length === 1 ? '' : 's'}.`);
58
+ }
59
+ this.suggestions = [{ command: `experience ${target}`, hint: 'view updated file' }];
60
+ }
61
+
62
+ private buildDryRunSuggestions(compactor: ReturnType<typeof this.explorBot.agentExperienceCompactor>, files: ExperienceFile[], target?: string): Suggestion[] {
63
+ const wouldTouch = files.filter((f) => {
64
+ const stripped = compactor.stripNonUsefulEntries(f.content);
65
+ return stripped !== f.content || stripped.length >= COMPACT_THRESHOLD || compactor.isRecent(f);
66
+ });
67
+ if (wouldTouch.length === 0) return [];
68
+ const argsPart = target ? ` ${target}` : '';
69
+ const count = wouldTouch.length;
70
+ return [{ command: `compact${argsPart}`, hint: `run compaction for real (${count} file${count === 1 ? '' : 's'} will be touched)` }];
71
+ }
72
+
73
+ private async runFullSweep(compactor: ReturnType<typeof this.explorBot.agentExperienceCompactor>, merge: boolean): Promise<{ merged: number; compacted: number }> {
74
+ if (!merge) {
75
+ tag('info').log('Compacting all experience files (merge skipped)…');
76
+ const compacted = await compactor.compactFiles(this.explorBot.getExperienceTracker().getAllExperience());
77
+ return { merged: 0, compacted };
78
+ }
79
+
80
+ tag('info').log('Merging experience files with similar URLs, then compacting all…');
81
+ return compactor.compactAllExperiences();
82
+ }
83
+
84
+ private resolveTarget(tracker: ExperienceTracker, target?: string): ExperienceFile[] | null {
85
+ const all = tracker.getAllExperience();
86
+ if (all.length === 0) return null;
87
+ if (!target) return all;
88
+
89
+ if (target.endsWith('.md')) {
90
+ const bare = target.slice(0, -3);
91
+ const byFilename = all.find((f) => basename(f.filePath, '.md') === bare);
92
+ return byFilename ? [byFilename] : [];
93
+ }
94
+
95
+ const filter = target.toLowerCase();
96
+ return all.filter((f) => (f.data.url || '').toLowerCase().includes(filter));
97
+ }
98
+
99
+ private printDryRun(compactor: ReturnType<typeof this.explorBot.agentExperienceCompactor>, files: ExperienceFile[], target: string | undefined, willMerge: boolean): void {
100
+ const lines: string[] = [];
101
+ const title = target ? `Dry run — target: "${target}"` : 'Dry run — full sweep';
102
+ lines.push(chalk.bold.underline.cyan(title));
103
+
104
+ if (willMerge) {
105
+ lines.push(chalk.dim('Merge step would run across all files before compaction.'));
106
+ }
107
+
108
+ let wouldStrip = 0;
109
+ let wouldAiReview = 0;
110
+ let wouldAiCompact = 0;
111
+ let totalChars = 0;
112
+
113
+ for (const file of files) {
114
+ const chars = file.content.length;
115
+ totalChars += chars;
116
+
117
+ const stripped = compactor.stripNonUsefulEntries(file.content);
118
+ const strippedChars = stripped.length;
119
+ const charsRemoved = chars - strippedChars;
120
+ const willStrip = stripped !== file.content;
121
+ const willReview = compactor.isRecent(file);
122
+ const willAi = strippedChars >= COMPACT_THRESHOLD;
123
+
124
+ if (willStrip) wouldStrip++;
125
+ if (willReview) wouldAiReview++;
126
+ if (willAi) wouldAiCompact++;
127
+
128
+ const name = basename(file.filePath);
129
+ const url = file.data.url || chalk.dim('(no url)');
130
+
131
+ const markers: string[] = [];
132
+ if (willStrip) {
133
+ const label = charsRemoved > 0 ? `✂ strip -${charsRemoved} chars` : '✂ strip (rewrite)';
134
+ markers.push(chalk.yellow(label));
135
+ }
136
+ if (willReview) markers.push(chalk.magenta('✓ ai-review'));
137
+ if (willAi) markers.push(chalk.yellow('✎ ai-compact'));
138
+ if (markers.length === 0) markers.push(chalk.dim('· skip'));
139
+
140
+ lines.push('');
141
+ lines.push(chalk.bold.green(name));
142
+ lines.push(` ${chalk.cyan(url)}`);
143
+ lines.push(` ${chalk.dim('size:')} ${chars} chars ${markers.join(' ')}`);
144
+ }
145
+
146
+ lines.push('');
147
+ lines.push(chalk.bold('Summary'));
148
+ lines.push(` ${chalk.dim('Files matched:')} ${chalk.bold(String(files.length))}`);
149
+ lines.push(` ${chalk.dim('Would strip:')} ${chalk.bold(String(wouldStrip))} ${chalk.dim('(remove legacy / FAILED / Visual click, rename to FLOW/ACTION)')}`);
150
+ lines.push(` ${chalk.dim('Would ai-review:')} ${chalk.bold(String(wouldAiReview))} ${chalk.dim('(recent files ≤30d — quality/reusability check)')}`);
151
+ lines.push(` ${chalk.dim('Would ai-compact:')} ${chalk.bold(String(wouldAiCompact))} ${chalk.dim(`(over ${COMPACT_THRESHOLD} chars after strip)`)}`);
152
+ lines.push(` ${chalk.dim('Total chars:')} ${chalk.bold(String(totalChars))}`);
153
+
154
+ tag('info').log(lines.join('\n'));
155
+ }
156
+ }
@@ -4,12 +4,18 @@ import { outputPath } from '../config.js';
4
4
  import { type ContextData, type ContextMode, formatContextSummary } from '../utils/context-formatter.js';
5
5
  import { tag } from '../utils/logger.js';
6
6
  import { extractValidContainers } from '../utils/research-parser.js';
7
- import { BaseCommand } from './base-command.js';
7
+ import { BaseCommand, type Suggestion } from './base-command.js';
8
8
 
9
9
  export class ContextCommand extends BaseCommand {
10
10
  name = 'context';
11
11
  description = 'Show page context summary (URL, headings, experience, knowledge, ARIA, HTML, research)';
12
- suggestions = ['context:aria', 'context:html', 'context:knowledge', 'context:experience', 'context:data'];
12
+ suggestions: Suggestion[] = [
13
+ { command: 'context:aria', hint: 'show page ARIA snapshot' },
14
+ { command: 'context:html', hint: 'show page HTML' },
15
+ { command: 'context:knowledge', hint: 'show relevant knowledge' },
16
+ { command: 'context:experience', hint: 'show relevant experience' },
17
+ { command: 'context:data', hint: 'show captured page data' },
18
+ ];
13
19
  options = [
14
20
  { flags: '--visual', description: 'Include annotated screenshot' },
15
21
  { flags: '--screenshot', description: 'Include annotated screenshot' },
@@ -1,9 +1,12 @@
1
- import { BaseCommand } from './base-command.js';
1
+ import { BaseCommand, type Suggestion } from './base-command.js';
2
2
 
3
3
  export class DrillCommand extends BaseCommand {
4
4
  name = 'drill';
5
5
  description = 'Drill all components on current page to learn interactions';
6
- suggestions = ['/research - to see UI map first', '/navigate <page> - to go to another page'];
6
+ suggestions: Suggestion[] = [
7
+ { command: 'research', hint: 'see UI map first' },
8
+ { command: 'navigate <page>', hint: 'go to another page' },
9
+ ];
7
10
 
8
11
  async execute(args: string): Promise<void> {
9
12
  const knowledgePath = this.parseKnowledgeArg(args);
@@ -0,0 +1,125 @@
1
+ import chalk from 'chalk';
2
+ import type { ExperienceTocEntry, ExperienceTracker } from '../experience-tracker.js';
3
+ import { tag } from '../utils/logger.js';
4
+ import { BaseCommand, type Suggestion } from './base-command.js';
5
+
6
+ export class ExperienceCommand extends BaseCommand {
7
+ name = 'experience';
8
+ description = 'List stored experiences; filter by filename or URL substring; expand a section by ref (e.g. LW1)';
9
+ options = [
10
+ { flags: '--recent', description: 'Only files modified within the last 30 days' },
11
+ { flags: '--old', description: 'Only files modified more than 30 days ago' },
12
+ ];
13
+ suggestions: Suggestion[] = [];
14
+
15
+ async execute(args: string): Promise<void> {
16
+ const { opts, args: remaining } = this.parseArgs(args);
17
+ if (opts.recent && opts.old) {
18
+ tag('info').log('Flags --recent and --old are mutually exclusive.');
19
+ return;
20
+ }
21
+ const recency = opts.recent ? 'recent' : opts.old ? 'old' : undefined;
22
+
23
+ const tracker = this.explorBot.getExperienceTracker();
24
+ const [first, second] = remaining;
25
+
26
+ const combinedRef = first?.match(/^([A-Z]+)[.\-]?(\d+)$/i);
27
+ if (combinedRef) {
28
+ this.expand(tracker, combinedRef[1].toUpperCase(), Number(combinedRef[2]));
29
+ return;
30
+ }
31
+
32
+ if (first && /^[A-Z]+$/i.test(first) && second && /^\d+$/.test(second)) {
33
+ this.expand(tracker, first.toUpperCase(), Number(second));
34
+ return;
35
+ }
36
+
37
+ const secondRef = second?.match(/^([A-Z]+)[.\-]?(\d+)$/i);
38
+ if (first && secondRef) {
39
+ this.expand(tracker, secondRef[1].toUpperCase(), Number(secondRef[2]), first);
40
+ return;
41
+ }
42
+
43
+ if (first && second && /^\d+$/.test(second)) {
44
+ const toc = tracker.listAllExperienceToc(first, { recency });
45
+ if (toc.length === 0) {
46
+ tag('info').log(`No experience found matching: ${first}`);
47
+ return;
48
+ }
49
+ this.expand(tracker, toc[0].fileTag, Number(second), first);
50
+ return;
51
+ }
52
+
53
+ const toc = tracker.listAllExperienceToc(first, { recency });
54
+ if (toc.length === 0) {
55
+ const scope = recency === 'recent' ? ' (recent only)' : recency === 'old' ? ' (old only)' : '';
56
+ tag('info').log(first ? `No experience found matching: ${first}${scope}` : `No experience files found${scope}. Experience is recorded automatically during test sessions.`);
57
+ return;
58
+ }
59
+
60
+ tag('info').log(this.formatToc(toc, first, recency));
61
+
62
+ const hints: Suggestion[] = [];
63
+ const exampleRef = `${toc[0].fileTag}1`;
64
+ hints.push({ command: `experience ${exampleRef}`, hint: 'read a section' });
65
+
66
+ const overloaded = toc.filter((entry) => entry.sections.length >= 10);
67
+ for (const entry of overloaded) {
68
+ hints.push({ command: `compact ${entry.fileHash}.md`, hint: `compact this file (${entry.sections.length} sections)` });
69
+ }
70
+
71
+ this.suggestions = hints;
72
+ }
73
+
74
+ private expand(tracker: ExperienceTracker, fileTag: string, sectionIndex: number, urlFilter?: string): void {
75
+ const section = tracker.getExperienceSectionByTag(fileTag, sectionIndex, urlFilter);
76
+ if (!section) {
77
+ tag('info').log(`No section ${fileTag}${sectionIndex} found${urlFilter ? ` for URL matching: ${urlFilter}` : ''}`);
78
+ return;
79
+ }
80
+
81
+ tag('info').log(`${chalk.dim('File:')} ${chalk.bold.green(`${section.fileHash}.md`)}`);
82
+ tag('info').log(`${chalk.dim('URL:')} ${chalk.bold.cyan(section.url)}`);
83
+ tag('info').log(`${chalk.green(`${fileTag}${sectionIndex}`)} ${chalk.bold(section.title)}`);
84
+ tag('multiline').log(this.stripLeadingHeading(section.content.trim()));
85
+ }
86
+
87
+ private formatToc(toc: ExperienceTocEntry[], urlFilter?: string, recency?: 'recent' | 'old'): string {
88
+ const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
89
+ const lines: string[] = [];
90
+
91
+ const scope = recency === 'recent' ? ' (recent, ≤30d)' : recency === 'old' ? ' (old, >30d)' : '';
92
+ const title = urlFilter ? `Experience matching "${urlFilter}"${scope}` : `Stored experience${scope}`;
93
+ lines.push(chalk.bold.underline.cyan(title));
94
+
95
+ for (const entry of toc) {
96
+ lines.push('');
97
+ lines.push(chalk.bold.green(`${entry.fileHash}.md`));
98
+ lines.push(` ${chalk.cyan(entry.url)}`);
99
+ for (let i = 0; i < entry.sections.length; i++) {
100
+ const section = entry.sections[i];
101
+ const isLast = i === entry.sections.length - 1;
102
+ const branch = isLast ? '└─' : '├─';
103
+ const ref = chalk.yellow(`${entry.fileTag}${section.index}`);
104
+ lines.push(` ${chalk.dim(branch)} ${ref} ${chalk.dim(':')} ${section.title}`);
105
+ }
106
+ }
107
+
108
+ const urls = new Set(toc.map((e) => e.url)).size;
109
+ lines.push('');
110
+ lines.push(chalk.bold('Summary'));
111
+ lines.push(` ${chalk.dim('URLs:')} ${chalk.bold(String(urls))}`);
112
+ lines.push(` ${chalk.dim('Files:')} ${chalk.bold(String(toc.length))}`);
113
+ lines.push(` ${chalk.dim('Entries:')} ${chalk.bold(String(totalSections))}`);
114
+
115
+ return lines.join('\n');
116
+ }
117
+
118
+ private stripLeadingHeading(body: string): string {
119
+ const lines = body.split('\n');
120
+ if (lines[0]?.match(/^#{1,6}\s/)) {
121
+ return lines.slice(1).join('\n').trimStart();
122
+ }
123
+ return body;
124
+ }
125
+ }
@@ -1,17 +1,26 @@
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 { getCliName } from '../utils/cli-name.ts';
4
+ import { Stats } from '../stats.js';
5
5
  import type { Plan } from '../test-plan.js';
6
- import { jsonToTable } from '../utils/markdown-parser.js';
6
+ import { getCliName } from '../utils/cli-name.ts';
7
+ import { ErrorPageError } from '../utils/error-page.ts';
7
8
  import { tag } from '../utils/logger.js';
8
- import { BaseCommand } from './base-command.js';
9
+ import { jsonToTable } from '../utils/markdown-parser.js';
10
+ import { BaseCommand, type Suggestion } from './base-command.js';
9
11
 
10
12
  export class ExploreCommand extends BaseCommand {
11
13
  name = 'explore';
12
14
  description = 'Start web exploration';
13
- options = [{ flags: '--max-tests <number>', description: 'Maximum number of tests to run' }];
14
- suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
15
+ options = [
16
+ { flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
17
+ { flags: '--focus <feature>', description: 'Focus area for exploration' },
18
+ ];
19
+ suggestions: Suggestion[] = [
20
+ { command: 'navigate <page>', hint: 'go to another page' },
21
+ { command: 'research', hint: 'analyze current page' },
22
+ { command: 'plan <feature>', hint: 'plan testing' },
23
+ ];
15
24
 
16
25
  maxTests?: number;
17
26
  private testsRun = 0;
@@ -23,7 +32,9 @@ export class ExploreCommand extends BaseCommand {
23
32
  this.maxTests = Number.parseInt(opts.maxTests as string, 10);
24
33
  }
25
34
 
26
- const feature = remaining.join(' ') || undefined;
35
+ const feature = (opts.focus as string) || remaining.join(' ') || undefined;
36
+ Stats.mode ??= 'explore';
37
+ Stats.focus ??= feature;
27
38
  const mainUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
28
39
 
29
40
  await this.runAllStyles(mainUrl, feature);
@@ -31,7 +42,7 @@ export class ExploreCommand extends BaseCommand {
31
42
  if (!mainPlan) return;
32
43
  this.completedPlans.push(mainPlan);
33
44
 
34
- if (!this.isLimitReached()) {
45
+ if (!feature && !this.isLimitReached()) {
35
46
  const planner = this.explorBot.agentPlanner();
36
47
  while (true) {
37
48
  if (this.isLimitReached()) break;
@@ -71,12 +82,27 @@ export class ExploreCommand extends BaseCommand {
71
82
  }
72
83
  const opts: { fresh: boolean; style: string; extend?: Plan; completedPlans?: Plan[] } = { fresh, style, completedPlans };
73
84
  if (fresh && parentPlan) opts.extend = parentPlan;
74
- await this.explorBot.plan(feature, opts);
85
+ await this.planWithRetry(feature, opts, pageUrl);
75
86
  await this.runPendingTests();
76
87
  fresh = false;
77
88
  }
78
89
  }
79
90
 
91
+ private async planWithRetry(feature: string | undefined, opts: { fresh: boolean; style: string; extend?: Plan; completedPlans?: Plan[] }, pageUrl?: string): Promise<void> {
92
+ await this.explorBot.plan(feature, opts);
93
+ if (!this.explorBot.lastPlanError) return;
94
+ if (this.explorBot.lastPlanError instanceof ErrorPageError) {
95
+ throw this.explorBot.lastPlanError;
96
+ }
97
+
98
+ tag('info').log(`Retrying planning style '${opts.style}'...`);
99
+ if (pageUrl) await this.explorBot.visit(pageUrl);
100
+ await this.explorBot.plan(feature, opts);
101
+ if (this.explorBot.lastPlanError) {
102
+ tag('warning').log(`Planning style '${opts.style}' failed after retry, skipping`);
103
+ }
104
+ }
105
+
80
106
  private printResults(savedPath?: string | null): void {
81
107
  const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title })));
82
108
 
@@ -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';
@@ -18,6 +19,7 @@ export class FreesailCommand extends BaseCommand {
18
19
  ];
19
20
 
20
21
  async execute(args: string): Promise<void> {
22
+ Stats.mode = 'freesail';
21
23
  const { opts } = this.parseArgs(args);
22
24
  let strategy: 'deep' | 'shallow' | undefined;
23
25
  if (opts.deep) strategy = 'deep';
@@ -1,21 +1,23 @@
1
1
  import type { ExplorBot } from '../explorbot.js';
2
+ import { AddRuleCommand } from './add-rule-command.js';
2
3
  import type { BaseCommand } from './base-command.js';
3
4
  import { CleanCommand } from './clean-command.js';
4
- import { DebugCommand } from './debug-command.js';
5
+ import { CompactCommand } from './compact-command.js';
5
6
  import { ContextAriaCommand } from './context-aria-command.js';
6
7
  import { ContextCommand } from './context-command.js';
7
8
  import { ContextDataCommand } from './context-data-command.js';
8
9
  import { ContextExperienceCommand } from './context-experience-command.js';
9
10
  import { ContextHtmlCommand } from './context-html-command.js';
10
11
  import { ContextKnowledgeCommand } from './context-knowledge-command.js';
12
+ import { DebugCommand } from './debug-command.js';
11
13
  import { DrillCommand } from './drill-command.js';
12
14
  import { ExitCommand } from './exit-command.js';
15
+ import { ExperienceCommand } from './experience-command.js';
13
16
  import { ExploreCommand } from './explore-command.js';
14
17
  import { FreesailCommand } from './freesail-command.js';
15
18
  import { HelpCommand } from './help-command.js';
16
- import { AddRuleCommand } from './add-rule-command.js';
17
- import { LearnCommand } from './learn-command.js';
18
19
  import { KnowsCommand } from './knows-command.js';
20
+ import { LearnCommand } from './learn-command.js';
19
21
  import { NavigateCommand } from './navigate-command.js';
20
22
  import { PathCommand } from './path-command.js';
21
23
  import { PlanClearCommand } from './plan-clear-command.js';
@@ -53,6 +55,8 @@ const commandClasses: CommandClass[] = [
53
55
  PathCommand,
54
56
  LearnCommand,
55
57
  KnowsCommand,
58
+ ExperienceCommand,
59
+ CompactCommand,
56
60
  AddRuleCommand,
57
61
  ContextCommand,
58
62
  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.ts';
6
+ import { log, tag } from '../utils/logger.js';
7
7
 
8
8
  const DEFAULT_CONFIG_TEMPLATE = `import { createOpenRouter } from '@openrouter/ai-sdk-provider';
9
9
  // import { '<your provider here>' } from '<your provider package here>';
@@ -1,12 +1,12 @@
1
1
  import { render } from 'ink';
2
2
  import React from 'react';
3
3
  import { tag } from '../utils/logger.js';
4
- import { BaseCommand } from './base-command.js';
4
+ import { BaseCommand, type Suggestion } from './base-command.js';
5
5
 
6
6
  export class LearnCommand extends BaseCommand {
7
7
  name = 'learn';
8
8
  description = 'Store knowledge for current page';
9
- suggestions = ['/knows - to view all knowledge'];
9
+ suggestions: Suggestion[] = [{ command: 'knows', hint: 'view all knowledge' }];
10
10
 
11
11
  async execute(args: string): Promise<void> {
12
12
  const note = args.trim();
@@ -1,10 +1,13 @@
1
1
  import { tag } from '../utils/logger.js';
2
- import { BaseCommand } from './base-command.js';
2
+ import { BaseCommand, type Suggestion } from './base-command.js';
3
3
 
4
4
  export class NavigateCommand extends BaseCommand {
5
5
  name = 'navigate';
6
6
  description = 'Navigate to URI or state using AI';
7
- suggestions = ['/research - to analyze current page', '/plan <feature> - to plan testing'];
7
+ suggestions: Suggestion[] = [
8
+ { command: 'research', hint: 'analyze current page' },
9
+ { command: 'plan <feature>', hint: 'plan testing' },
10
+ ];
8
11
 
9
12
  async execute(args: string): Promise<void> {
10
13
  const destination = args.trim();
@@ -1,10 +1,13 @@
1
1
  import { tag } from '../utils/logger.js';
2
- import { BaseCommand } from './base-command.js';
2
+ import { BaseCommand, type Suggestion } from './base-command.js';
3
3
 
4
4
  export class PlanClearCommand extends BaseCommand {
5
5
  name = 'plan:clear';
6
6
  description = 'Clear current plan and create a new one';
7
- suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
7
+ suggestions: Suggestion[] = [
8
+ { command: 'test', hint: 'launch first test' },
9
+ { command: 'test *', hint: 'launch all tests' },
10
+ ];
8
11
 
9
12
  async execute(args: string): Promise<void> {
10
13
  this.explorBot.clearPlan();
@@ -2,12 +2,16 @@ import path from 'node:path';
2
2
  import chalk from 'chalk';
3
3
  import figureSet from 'figures';
4
4
  import { tag } from '../utils/logger.js';
5
- import { BaseCommand } from './base-command.js';
5
+ import { BaseCommand, type Suggestion } from './base-command.js';
6
6
 
7
7
  export class PlanCommand extends BaseCommand {
8
8
  name = 'plan';
9
9
  description = 'Plan testing for a feature';
10
- suggestions = ['/test - to launch first test', '/test * - to launch all tests', 'Edit the plan in file and call /plan:reload to update it'];
10
+ suggestions: Suggestion[] = [
11
+ { command: 'test', hint: 'launch first test' },
12
+ { command: 'test *', hint: 'launch all tests' },
13
+ { command: 'plan:reload', hint: 'after editing the plan file, reload it' },
14
+ ];
11
15
  options = [
12
16
  { flags: '--fresh', description: 'Regenerate plan from scratch' },
13
17
  { flags: '--clear', description: 'Clear plan before regenerating' },
@@ -61,15 +65,18 @@ export class PlanCommand extends BaseCommand {
61
65
  }
62
66
 
63
67
  private updateSuggestions(): void {
64
- this.suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
68
+ this.suggestions = [
69
+ { command: 'test', hint: 'launch first test' },
70
+ { command: 'test *', hint: 'launch all tests' },
71
+ ];
65
72
 
66
73
  const suite = this.explorBot.getSuite();
67
74
  if (suite && suite.automatedTestCount > 0) {
68
75
  for (const f of suite.getAutomatedTestFiles()) {
69
- this.suggestions.push(`/rerun ${path.relative(process.cwd(), f)} - re-run automated tests`);
76
+ this.suggestions.push({ command: `rerun ${path.relative(process.cwd(), f)}`, hint: 're-run automated tests' });
70
77
  }
71
78
  }
72
79
 
73
- this.suggestions.push('Edit the plan in file and call /plan:reload to update it');
80
+ this.suggestions.push({ command: 'plan:reload', hint: 'after editing the plan file, reload it' });
74
81
  }
75
82
  }