explorbot 0.1.9 → 0.1.11

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 (157) hide show
  1. package/README.md +27 -1
  2. package/bin/explorbot-cli.ts +86 -15
  3. package/boat/api-tester/src/ai/curler-tools.ts +3 -3
  4. package/boat/api-tester/src/ai/curler.ts +1 -1
  5. package/boat/api-tester/src/apibot.ts +2 -2
  6. package/boat/api-tester/src/config.ts +1 -1
  7. package/dist/bin/explorbot-cli.js +85 -14
  8. package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
  9. package/dist/boat/api-tester/src/apibot.js +2 -2
  10. package/dist/package.json +2 -2
  11. package/dist/rules/navigator/output.md +9 -0
  12. package/dist/rules/navigator/verification-actions.md +2 -0
  13. package/dist/src/action-result.js +23 -1
  14. package/dist/src/action.js +46 -38
  15. package/dist/src/ai/bosun.js +16 -2
  16. package/dist/src/ai/conversation.js +39 -0
  17. package/dist/src/ai/experience-compactor.js +235 -50
  18. package/dist/src/ai/historian/codeceptjs.js +109 -0
  19. package/dist/src/ai/historian/experience.js +320 -0
  20. package/dist/src/ai/historian/mixin.js +2 -0
  21. package/dist/src/ai/historian/playwright.js +145 -0
  22. package/dist/src/ai/historian/utils.js +18 -0
  23. package/dist/src/ai/historian.js +19 -398
  24. package/dist/src/ai/navigator.js +133 -80
  25. package/dist/src/ai/pilot.js +254 -13
  26. package/dist/src/ai/planner/subpages.js +1 -30
  27. package/dist/src/ai/planner.js +33 -13
  28. package/dist/src/ai/provider.js +55 -18
  29. package/dist/src/ai/rerunner.js +3 -3
  30. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  31. package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
  32. package/dist/src/ai/researcher/locators.js +1 -1
  33. package/dist/src/ai/researcher/sections.js +8 -1
  34. package/dist/src/ai/researcher.js +43 -41
  35. package/dist/src/ai/rules.js +26 -14
  36. package/dist/src/ai/tester.js +90 -26
  37. package/dist/src/ai/tools.js +18 -10
  38. package/dist/src/api/request-store.js +20 -0
  39. package/dist/src/api/xhr-capture.js +19 -3
  40. package/dist/src/browser-server.js +16 -3
  41. package/dist/src/command-handler.js +1 -1
  42. package/dist/src/commands/add-rule-command.js +12 -9
  43. package/dist/src/commands/base-command.js +20 -0
  44. package/dist/src/commands/clean-command.js +3 -2
  45. package/dist/src/commands/compact-command.js +138 -0
  46. package/dist/src/commands/context-command.js +7 -1
  47. package/dist/src/commands/drill-command.js +4 -1
  48. package/dist/src/commands/experience-command.js +104 -0
  49. package/dist/src/commands/explore-command.js +54 -19
  50. package/dist/src/commands/freesail-command.js +2 -0
  51. package/dist/src/commands/index.js +7 -3
  52. package/dist/src/commands/init-command.js +11 -10
  53. package/dist/src/commands/learn-command.js +1 -1
  54. package/dist/src/commands/navigate-command.js +4 -1
  55. package/dist/src/commands/plan-clear-command.js +4 -1
  56. package/dist/src/commands/plan-command.js +43 -4
  57. package/dist/src/commands/plan-edit-command.js +1 -1
  58. package/dist/src/commands/plan-load-command.js +4 -1
  59. package/dist/src/commands/plan-reload-command.js +4 -1
  60. package/dist/src/commands/plan-save-command.js +20 -8
  61. package/dist/src/commands/rerun-command.js +4 -0
  62. package/dist/src/commands/research-command.js +5 -2
  63. package/dist/src/commands/start-command.js +5 -1
  64. package/dist/src/commands/test-command.js +7 -1
  65. package/dist/src/components/App.js +15 -5
  66. package/dist/src/execution-controller.js +13 -2
  67. package/dist/src/experience-tracker.js +174 -83
  68. package/dist/src/explorbot.js +31 -22
  69. package/dist/src/explorer.js +12 -5
  70. package/dist/src/observability.js +50 -99
  71. package/dist/src/playwright-recorder.js +309 -0
  72. package/dist/src/reporter.js +17 -2
  73. package/dist/src/stats.js +2 -0
  74. package/dist/src/suite.js +1 -1
  75. package/dist/src/test-plan.js +12 -0
  76. package/dist/src/utils/aria.js +37 -1
  77. package/dist/src/utils/error-page.js +30 -7
  78. package/dist/src/utils/logger.js +1 -1
  79. package/dist/src/utils/next-steps.js +37 -0
  80. package/dist/src/utils/rules-loader.js +1 -1
  81. package/dist/src/utils/test-files.js +1 -1
  82. package/dist/src/utils/url-matcher.js +50 -0
  83. package/package.json +2 -2
  84. package/rules/navigator/output.md +9 -0
  85. package/rules/navigator/verification-actions.md +2 -0
  86. package/src/action-result.ts +26 -1
  87. package/src/action.ts +44 -37
  88. package/src/ai/bosun.ts +16 -2
  89. package/src/ai/conversation.ts +37 -0
  90. package/src/ai/experience-compactor.ts +270 -63
  91. package/src/ai/historian/codeceptjs.ts +130 -0
  92. package/src/ai/historian/experience.ts +383 -0
  93. package/src/ai/historian/mixin.ts +4 -0
  94. package/src/ai/historian/playwright.ts +169 -0
  95. package/src/ai/historian/utils.ts +23 -0
  96. package/src/ai/historian.ts +35 -468
  97. package/src/ai/navigator.ts +140 -85
  98. package/src/ai/pilot.ts +259 -14
  99. package/src/ai/planner/subpages.ts +1 -24
  100. package/src/ai/planner.ts +34 -14
  101. package/src/ai/provider.ts +52 -18
  102. package/src/ai/rerunner.ts +3 -3
  103. package/src/ai/researcher/deep-analysis.ts +1 -1
  104. package/src/ai/researcher/fingerprint-worker.ts +1 -1
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/sections.ts +7 -1
  107. package/src/ai/researcher.ts +47 -42
  108. package/src/ai/rules.ts +27 -14
  109. package/src/ai/task-agent.ts +1 -1
  110. package/src/ai/tester.ts +94 -26
  111. package/src/ai/tools.ts +53 -29
  112. package/src/api/request-store.ts +22 -0
  113. package/src/api/xhr-capture.ts +21 -3
  114. package/src/browser-server.ts +17 -3
  115. package/src/command-handler.ts +1 -1
  116. package/src/commands/add-rule-command.ts +13 -9
  117. package/src/commands/base-command.ts +26 -1
  118. package/src/commands/clean-command.ts +4 -3
  119. package/src/commands/compact-command.ts +156 -0
  120. package/src/commands/context-command.ts +8 -2
  121. package/src/commands/drill-command.ts +5 -2
  122. package/src/commands/experience-command.ts +125 -0
  123. package/src/commands/explore-command.ts +58 -21
  124. package/src/commands/freesail-command.ts +2 -0
  125. package/src/commands/index.ts +7 -3
  126. package/src/commands/init-command.ts +11 -10
  127. package/src/commands/learn-command.ts +2 -2
  128. package/src/commands/navigate-command.ts +5 -2
  129. package/src/commands/plan-clear-command.ts +5 -2
  130. package/src/commands/plan-command.ts +47 -5
  131. package/src/commands/plan-edit-command.ts +2 -2
  132. package/src/commands/plan-load-command.ts +5 -2
  133. package/src/commands/plan-reload-command.ts +5 -2
  134. package/src/commands/plan-save-command.ts +20 -9
  135. package/src/commands/rerun-command.ts +5 -0
  136. package/src/commands/research-command.ts +6 -3
  137. package/src/commands/start-command.ts +6 -2
  138. package/src/commands/test-command.ts +8 -2
  139. package/src/components/App.tsx +16 -5
  140. package/src/config.ts +6 -1
  141. package/src/execution-controller.ts +14 -3
  142. package/src/experience-tracker.ts +198 -100
  143. package/src/explorbot.ts +33 -23
  144. package/src/explorer.ts +14 -5
  145. package/src/observability.ts +50 -109
  146. package/src/playwright-recorder.ts +305 -0
  147. package/src/reporter.ts +17 -3
  148. package/src/stats.ts +4 -0
  149. package/src/suite.ts +1 -1
  150. package/src/test-plan.ts +12 -0
  151. package/src/utils/aria.ts +38 -1
  152. package/src/utils/error-page.ts +32 -7
  153. package/src/utils/logger.ts +1 -1
  154. package/src/utils/next-steps.ts +51 -0
  155. package/src/utils/rules-loader.ts +1 -1
  156. package/src/utils/test-files.ts +1 -1
  157. package/src/utils/url-matcher.ts +43 -0
@@ -1,11 +1,13 @@
1
1
  import figureSet from 'figures';
2
- import path from 'node:path';
3
2
  import { getStyles } from '../ai/planner/styles.js';
4
- import { getCliName } from '../utils/cli-name.ts';
3
+ import { Stats } from '../stats.js';
5
4
  import type { Plan } from '../test-plan.js';
6
- import { jsonToTable } from '../utils/markdown-parser.js';
5
+ import { getCliName } from '../utils/cli-name.ts';
6
+ import { ErrorPageError } from '../utils/error-page.ts';
7
7
  import { tag } from '../utils/logger.js';
8
- import { BaseCommand } from './base-command.js';
8
+ import { jsonToTable } from '../utils/markdown-parser.js';
9
+ import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
10
+ import { BaseCommand, type Suggestion } from './base-command.js';
9
11
 
10
12
  export class ExploreCommand extends BaseCommand {
11
13
  name = 'explore';
@@ -14,7 +16,11 @@ export class ExploreCommand extends BaseCommand {
14
16
  { flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
15
17
  { flags: '--focus <feature>', description: 'Focus area for exploration' },
16
18
  ];
17
- suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
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
+ ];
18
24
 
19
25
  maxTests?: number;
20
26
  private testsRun = 0;
@@ -27,6 +33,8 @@ export class ExploreCommand extends BaseCommand {
27
33
  }
28
34
 
29
35
  const feature = (opts.focus as string) || remaining.join(' ') || undefined;
36
+ Stats.mode ??= 'explore';
37
+ Stats.focus ??= feature;
30
38
  const mainUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
31
39
 
32
40
  await this.runAllStyles(mainUrl, feature);
@@ -34,7 +42,7 @@ export class ExploreCommand extends BaseCommand {
34
42
  if (!mainPlan) return;
35
43
  this.completedPlans.push(mainPlan);
36
44
 
37
- if (!this.isLimitReached()) {
45
+ if (!feature && !this.isLimitReached()) {
38
46
  const planner = this.explorBot.agentPlanner();
39
47
  while (true) {
40
48
  if (this.isLimitReached()) break;
@@ -62,8 +70,8 @@ export class ExploreCommand extends BaseCommand {
62
70
  this.explorBot.setCurrentPlan(mainPlan);
63
71
  if (mainUrl) await this.explorBot.visit(mainUrl);
64
72
  const savedPath = this.explorBot.savePlans(this.completedPlans);
65
- this.printResults(savedPath);
66
- this.printRerunSuggestions();
73
+ this.printResults();
74
+ this.printNextSteps(savedPath);
67
75
  }
68
76
 
69
77
  private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, completedPlans?: Plan[]): Promise<void> {
@@ -74,13 +82,28 @@ export class ExploreCommand extends BaseCommand {
74
82
  }
75
83
  const opts: { fresh: boolean; style: string; extend?: Plan; completedPlans?: Plan[] } = { fresh, style, completedPlans };
76
84
  if (fresh && parentPlan) opts.extend = parentPlan;
77
- await this.explorBot.plan(feature, opts);
85
+ await this.planWithRetry(feature, opts, pageUrl);
78
86
  await this.runPendingTests();
79
87
  fresh = false;
80
88
  }
81
89
  }
82
90
 
83
- private printResults(savedPath?: string | null): void {
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
+
106
+ private printResults(): void {
84
107
  const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title })));
85
108
 
86
109
  if (allTests.length === 0) return;
@@ -109,22 +132,36 @@ export class ExploreCommand extends BaseCommand {
109
132
  if (hasSubPages) columns.push('Plan');
110
133
  tag('multiline').log(jsonToTable(rows, columns));
111
134
  tag('info').log(`${figureSet.tick} ${allTests.length} tests completed`);
135
+ }
112
136
 
113
- if (savedPath) {
114
- const relativePath = path.relative(process.cwd(), savedPath);
115
- tag('info').log(`Re-run tests: ${getCliName()} test ${relativePath} <index>`);
137
+ private printNextSteps(savedPlanPath?: string | null): void {
138
+ const cli = getCliName();
139
+ const sections: NextStepSection[] = [];
140
+
141
+ if (savedPlanPath) {
142
+ const relPlan = relativeToCwd(savedPlanPath);
143
+ sections.push({
144
+ label: 'Plan',
145
+ path: savedPlanPath,
146
+ commands: [
147
+ { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
148
+ { label: 'Run all', command: `${cli} test ${relPlan} *` },
149
+ { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
150
+ ],
151
+ });
116
152
  }
117
- }
118
153
 
119
- private printRerunSuggestions(): void {
120
154
  const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
121
- if (savedFiles.length === 0) return;
122
-
123
- for (const filePath of savedFiles) {
124
- tag('info').log(`Generated: ${path.basename(filePath)}`);
155
+ if (savedFiles.length > 0) {
156
+ const commands = savedFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
157
+ commands.push({ label: 'List tests', command: `${cli} runs` });
158
+ sections.push({
159
+ label: `Generated tests (${savedFiles.length})`,
160
+ commands,
161
+ });
125
162
  }
126
- tag('info').log(`List tests: ${getCliName()} runs`);
127
- tag('info').log(`Re-run with healing: ${getCliName()} rerun <filename> [index]`);
163
+
164
+ printNextSteps(sections);
128
165
  }
129
166
 
130
167
  private isLimitReached(): boolean {
@@ -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,10 @@
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
+ import { relativeToCwd } from '../utils/next-steps.ts';
7
8
 
8
9
  const DEFAULT_CONFIG_TEMPLATE = `import { createOpenRouter } from '@openrouter/ai-sdk-provider';
9
10
  // import { '<your provider here>' } from '<your provider package here>';
@@ -61,10 +62,10 @@ export function runInitCommand(options: InitCommandOptions): void {
61
62
  const dir = resolve(customPath);
62
63
  if (!existsSync(dir)) {
63
64
  mkdirSync(dir, { recursive: true });
64
- log(`Created directory: ${dir}`);
65
+ log(`Created directory: ${relativeToCwd(dir)}`);
65
66
  }
66
67
  process.chdir(dir);
67
- log(`Working in directory: ${dir}`);
68
+ log(`Working in directory: ${relativeToCwd(dir)}`);
68
69
  }
69
70
 
70
71
  try {
@@ -78,24 +79,24 @@ export function runInitCommand(options: InitCommandOptions): void {
78
79
  const dir = dirname(outPath);
79
80
  if (!existsSync(dir)) {
80
81
  mkdirSync(dir, { recursive: true });
81
- log(`Created directory: ${dir}`);
82
+ log(`Created directory: ${relativeToCwd(dir)}`);
82
83
  }
83
84
 
84
85
  if (existsSync(outPath) && !force) {
85
- log(`Config file already exists: ${outPath}`);
86
+ log(`Config file already exists: ${relativeToCwd(outPath)}`);
86
87
  log('Use --force to overwrite existing file');
87
88
  process.exit(1);
88
89
  }
89
90
 
90
91
  writeFileSync(outPath, DEFAULT_CONFIG_TEMPLATE, 'utf8');
91
- log(`Created config file: ${outPath}`);
92
+ log(`Created config file: ${relativeToCwd(outPath)}`);
92
93
 
93
94
  const envPath = resolve(process.cwd(), '.env');
94
95
  if (!existsSync(envPath)) {
95
96
  writeFileSync(envPath, `${DEFAULT_ENV_TEMPLATE}\n`, 'utf8');
96
- log(`Created env file: ${envPath}`);
97
+ log(`Created env file: ${relativeToCwd(envPath)}`);
97
98
  } else {
98
- log(`Env file already exists: ${envPath}`);
99
+ log(`Env file already exists: ${relativeToCwd(envPath)}`);
99
100
  }
100
101
 
101
102
  log('');
@@ -112,7 +113,7 @@ export function runInitCommand(options: InitCommandOptions): void {
112
113
 
113
114
  if (!existsSync('./output')) {
114
115
  mkdirSync('./output', { recursive: true });
115
- log('Created directory: ./output');
116
+ log('Created directory: output');
116
117
  }
117
118
  } catch (error) {
118
119
  log('Failed to create config file:', error);
@@ -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();
@@ -1,13 +1,19 @@
1
1
  import path from 'node:path';
2
2
  import chalk from 'chalk';
3
3
  import figureSet from 'figures';
4
+ import { getCliName } from '../utils/cli-name.ts';
4
5
  import { tag } from '../utils/logger.js';
5
- import { BaseCommand } from './base-command.js';
6
+ import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
7
+ import { BaseCommand, type Suggestion } from './base-command.js';
6
8
 
7
9
  export class PlanCommand extends BaseCommand {
8
10
  name = 'plan';
9
11
  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'];
12
+ suggestions: Suggestion[] = [
13
+ { command: 'test', hint: 'launch first test' },
14
+ { command: 'test *', hint: 'launch all tests' },
15
+ { command: 'plan:reload', hint: 'after editing the plan file, reload it' },
16
+ ];
11
17
  options = [
12
18
  { flags: '--fresh', description: 'Regenerate plan from scratch' },
13
19
  { flags: '--clear', description: 'Clear plan before regenerating' },
@@ -37,6 +43,39 @@ export class PlanCommand extends BaseCommand {
37
43
 
38
44
  this.printPlanSummary();
39
45
  this.updateSuggestions();
46
+ this.printNextSteps();
47
+ }
48
+
49
+ private printNextSteps(): void {
50
+ const savedPath = this.explorBot.lastSavedPlanPath;
51
+ if (!savedPath) return;
52
+
53
+ const cli = getCliName();
54
+ const relPlan = relativeToCwd(savedPath);
55
+ const sections: NextStepSection[] = [
56
+ {
57
+ label: 'Plan',
58
+ path: savedPath,
59
+ commands: [
60
+ { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
61
+ { label: 'Run all', command: `${cli} test ${relPlan} *` },
62
+ { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
63
+ { label: 'Reload', command: `/plan:load ${relPlan}` },
64
+ ],
65
+ },
66
+ ];
67
+
68
+ const suite = this.explorBot.getSuite();
69
+ const files = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
70
+ if (files.length > 0) {
71
+ const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
72
+ sections.push({
73
+ label: `Automated tests (${files.length})`,
74
+ commands,
75
+ });
76
+ }
77
+
78
+ printNextSteps(sections);
40
79
  }
41
80
 
42
81
  private printPlanSummary(): void {
@@ -61,15 +100,18 @@ export class PlanCommand extends BaseCommand {
61
100
  }
62
101
 
63
102
  private updateSuggestions(): void {
64
- this.suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
103
+ this.suggestions = [
104
+ { command: 'test', hint: 'launch first test' },
105
+ { command: 'test *', hint: 'launch all tests' },
106
+ ];
65
107
 
66
108
  const suite = this.explorBot.getSuite();
67
109
  if (suite && suite.automatedTestCount > 0) {
68
110
  for (const f of suite.getAutomatedTestFiles()) {
69
- this.suggestions.push(`/rerun ${path.relative(process.cwd(), f)} - re-run automated tests`);
111
+ this.suggestions.push({ command: `rerun ${path.relative(process.cwd(), f)}`, hint: 're-run automated tests' });
70
112
  }
71
113
  }
72
114
 
73
- this.suggestions.push('Edit the plan in file and call /plan:reload to update it');
115
+ this.suggestions.push({ command: 'plan:reload', hint: 'after editing the plan file, reload it' });
74
116
  }
75
117
  }
@@ -1,9 +1,9 @@
1
- import { BaseCommand } from './base-command.js';
1
+ import { BaseCommand, type Suggestion } from './base-command.js';
2
2
 
3
3
  export class PlanEditCommand extends BaseCommand {
4
4
  name = 'plan:edit';
5
5
  description = 'Open test plan editor';
6
- suggestions = ['/plan:edit - toggle tests on/off'];
6
+ suggestions: Suggestion[] = [{ command: 'plan:edit', hint: 'toggle tests on/off' }];
7
7
 
8
8
  async execute(_args: string): Promise<void> {}
9
9
  }
@@ -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 PlanLoadCommand extends BaseCommand {
5
5
  name = 'plan:load';
6
6
  description = 'Load plan from file';
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
  const filename = 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 PlanReloadCommand extends BaseCommand {
5
5
  name = 'plan:reload';
6
6
  description = 'Clear current plan and regenerate';
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
  const currentPlan = this.explorBot.getCurrentPlan();
@@ -1,11 +1,11 @@
1
- import path from 'node:path';
2
- import { tag } from '../utils/logger.js';
3
- import { BaseCommand } from './base-command.js';
1
+ import { getCliName } from '../utils/cli-name.ts';
2
+ import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
3
+ import { BaseCommand, type Suggestion } from './base-command.js';
4
4
 
5
5
  export class PlanSaveCommand extends BaseCommand {
6
6
  name = 'plan:save';
7
7
  description = 'Save current plan to file';
8
- suggestions = ['/test - to launch first test'];
8
+ suggestions: Suggestion[] = [{ command: 'test', hint: 'launch first test' }];
9
9
 
10
10
  async execute(args: string): Promise<void> {
11
11
  const plan = this.explorBot.getCurrentPlan();
@@ -15,11 +15,22 @@ export class PlanSaveCommand extends BaseCommand {
15
15
 
16
16
  const filename = args.trim() || undefined;
17
17
  const savedPath = this.explorBot.savePlan(filename);
18
+ if (!savedPath) return;
18
19
 
19
- if (savedPath) {
20
- const relativePath = path.relative(process.cwd(), savedPath);
21
- tag('success').log(`Plan saved to: ${relativePath}`);
22
- tag('info').log(`Run /plan:load ${relativePath} to reload it`);
23
- }
20
+ const cli = getCliName();
21
+ const relPlan = relativeToCwd(savedPath);
22
+ const sections: NextStepSection[] = [
23
+ {
24
+ label: 'Plan',
25
+ path: savedPath,
26
+ commands: [
27
+ { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
28
+ { label: 'Run all', command: `${cli} test ${relPlan} *` },
29
+ { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
30
+ { label: 'Reload', command: `/plan:load ${relPlan}` },
31
+ ],
32
+ },
33
+ ];
34
+ printNextSteps(sections);
24
35
  }
25
36
  }
@@ -24,6 +24,11 @@ export class RerunCommand extends BaseCommand {
24
24
  filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
25
25
  }
26
26
 
27
+ if (filePath.endsWith('.spec.ts') || filePath.endsWith('.spec.js')) {
28
+ tag('error').log(`Rerun does not support Playwright tests. Run them with: npx playwright test ${filePath}`);
29
+ return;
30
+ }
31
+
27
32
  const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
28
33
  await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
29
34
  }
@@ -1,12 +1,15 @@
1
1
  import { join } from 'node:path';
2
2
  import { ConfigParser } from '../config.ts';
3
3
  import { tag } from '../utils/logger.ts';
4
- import { BaseCommand } from './base-command.js';
4
+ import { BaseCommand, type Suggestion } from './base-command.js';
5
5
 
6
6
  export class ResearchCommand extends BaseCommand {
7
7
  name = 'research';
8
8
  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.';
9
- suggestions = ['/navigate <page> - to go to another page', '/plan <feature> - to plan testing'];
9
+ suggestions: Suggestion[] = [
10
+ { command: 'navigate <page>', hint: 'go to another page' },
11
+ { command: 'plan <feature>', hint: 'plan testing' },
12
+ ];
10
13
  options = [
11
14
  { flags: '--data', description: 'Include page data' },
12
15
  { flags: '--deep', description: 'Explore interactive elements by clicking them' },
@@ -45,7 +48,7 @@ export class ResearchCommand extends BaseCommand {
45
48
  }
46
49
 
47
50
  if (!enableDeep) {
48
- this.suggestions = ['/research <page> --deep - analyze page for all expandable elements and interactions'];
51
+ this.suggestions = [{ command: 'research <page> --deep', hint: 'analyze page for all expandable elements and interactions' }];
49
52
  }
50
53
  }
51
54
  }
@@ -1,10 +1,14 @@
1
- import { BaseCommand } from './base-command.js';
1
+ import { BaseCommand, type Suggestion } from './base-command.js';
2
2
  import { ExploreCommand } from './explore-command.js';
3
3
 
4
4
  export class StartCommand extends BaseCommand {
5
5
  name = 'start';
6
6
  description = 'Start web exploration';
7
- suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
7
+ suggestions: Suggestion[] = [
8
+ { command: 'navigate <page>', hint: 'go to another page' },
9
+ { command: 'research', hint: 'analyze current page' },
10
+ { command: 'plan <feature>', hint: 'plan testing' },
11
+ ];
8
12
 
9
13
  async execute(args: string): Promise<void> {
10
14
  await new ExploreCommand(this.explorBot).execute(args);
@@ -1,14 +1,20 @@
1
+ import { Stats } from '../stats.js';
1
2
  import { Test } from '../test-plan.js';
2
3
  import { tag } from '../utils/logger.js';
3
- import { BaseCommand } from './base-command.js';
4
+ import { BaseCommand, type Suggestion } from './base-command.js';
4
5
 
5
6
  export class TestCommand extends BaseCommand {
6
7
  name = 'test';
7
8
  description = 'Launch tester agent to execute test scenarios';
8
- suggestions = ['/test - to run next test', '/plan - to create new plan'];
9
+ suggestions: Suggestion[] = [
10
+ { command: 'test', hint: 'run next test' },
11
+ { command: 'plan', hint: 'create new plan' },
12
+ ];
9
13
 
10
14
  async execute(args: string): Promise<void> {
11
15
  const plan = this.explorBot.getCurrentPlan();
16
+ Stats.mode = 'test';
17
+ Stats.focus = plan?.title;
12
18
  const toExecute: Test[] = [];
13
19
 
14
20
  const requirePlan = () => {
@@ -99,7 +99,11 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
99
99
  setShowInput(true);
100
100
 
101
101
  return new Promise<string | null>((resolve) => {
102
- interruptResolveRef.current = resolve;
102
+ interruptResolveRef.current = (value) => {
103
+ interruptResolveRef.current = null;
104
+ setInterruptPrompt(null);
105
+ resolve(value);
106
+ };
103
107
  });
104
108
  });
105
109
 
@@ -107,11 +111,19 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
107
111
  setShowInput(true);
108
112
  };
109
113
 
114
+ const handleInterrupt = () => {
115
+ if (interruptResolveRef.current) {
116
+ interruptResolveRef.current(null);
117
+ }
118
+ };
119
+
110
120
  executionController.on('idle', handleIdle);
121
+ executionController.on('interrupt', handleInterrupt);
111
122
  setInputCallbackReady(true);
112
123
 
113
124
  return () => {
114
125
  executionController.off('idle', handleIdle);
126
+ executionController.off('interrupt', handleInterrupt);
115
127
  executionController.reset();
116
128
  };
117
129
  }, []);
@@ -284,9 +296,10 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
284
296
  }
285
297
 
286
298
  if (isCommand) {
287
- setInterruptPrompt(null);
299
+ if (interruptResolveRef.current) {
300
+ interruptResolveRef.current(null);
301
+ }
288
302
  setShowInput(false);
289
- interruptResolveRef.current = null;
290
303
  executionController.startExecution();
291
304
  try {
292
305
  await commandHandler.executeCommand(trimmed);
@@ -303,8 +316,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
303
316
 
304
317
  if (interruptResolveRef.current) {
305
318
  interruptResolveRef.current(input);
306
- interruptResolveRef.current = null;
307
- setInterruptPrompt(null);
308
319
  setShowInput(false);
309
320
  return;
310
321
  }
package/src/config.ts CHANGED
@@ -107,6 +107,10 @@ interface PlannerAgentConfig extends AgentConfig {
107
107
  stylesDir?: string;
108
108
  }
109
109
 
110
+ interface HistorianAgentConfig extends AgentConfig {
111
+ framework?: 'codeceptjs' | 'playwright';
112
+ }
113
+
110
114
  interface AgentsConfig {
111
115
  tester?: TesterAgentConfig;
112
116
  navigator?: NavigatorAgentConfig;
@@ -116,7 +120,7 @@ interface AgentsConfig {
116
120
  'experience-compactor'?: AgentConfig;
117
121
  captain?: AgentConfig;
118
122
  quartermaster?: AgentConfig;
119
- historian?: AgentConfig;
123
+ historian?: HistorianAgentConfig;
120
124
  fisherman?: AgentConfig;
121
125
  chief?: AgentConfig;
122
126
  curler?: AgentConfig;
@@ -229,6 +233,7 @@ export type {
229
233
  ActionConfig,
230
234
  AgentConfig,
231
235
  AgentsConfig,
236
+ HistorianAgentConfig,
232
237
  ResearcherAgentConfig,
233
238
  NavigatorAgentConfig,
234
239
  PlannerAgentConfig,