explorbot 0.1.32-beta.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +35 -19
  2. package/bin/explorbot-cli.ts +33 -10
  3. package/boat/api-tester/src/config.ts +45 -3
  4. package/boat/doc-collector/src/ai/documentarian.ts +26 -28
  5. package/boat/doc-collector/src/ai/tools.ts +74 -16
  6. package/boat/doc-collector/src/cli.ts +1 -6
  7. package/boat/doc-collector/src/docbot.ts +30 -9
  8. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  9. package/boat/doc-collector/src/screenshots.ts +34 -2
  10. package/boat/doc-collector/src/state-diagram.ts +281 -0
  11. package/dist/bin/explorbot-cli.js +29 -9
  12. package/dist/boat/api-tester/src/config.js +43 -4
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
  14. package/dist/boat/doc-collector/src/ai/tools.js +53 -16
  15. package/dist/boat/doc-collector/src/cli.js +1 -5
  16. package/dist/boat/doc-collector/src/docbot.js +27 -8
  17. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  18. package/dist/boat/doc-collector/src/screenshots.js +30 -1
  19. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  20. package/dist/models.json +30 -0
  21. package/dist/package.json +23 -3
  22. package/dist/src/action-result.d.ts +8 -10
  23. package/dist/src/action-result.js +72 -164
  24. package/dist/src/action.d.ts +13 -12
  25. package/dist/src/action.js +37 -113
  26. package/dist/src/ai/agent.d.ts +17 -0
  27. package/dist/src/ai/captain/idle-mode.js +1 -1
  28. package/dist/src/ai/captain/test-mode.js +1 -1
  29. package/dist/src/ai/captain/web-mode.js +18 -29
  30. package/dist/src/ai/captain.d.ts +1 -3
  31. package/dist/src/ai/captain.js +17 -45
  32. package/dist/src/ai/conversation.d.ts +2 -5
  33. package/dist/src/ai/conversation.js +9 -28
  34. package/dist/src/ai/driller.d.ts +2 -17
  35. package/dist/src/ai/driller.js +26 -50
  36. package/dist/src/ai/experience-compactor.d.ts +2 -2
  37. package/dist/src/ai/experience-compactor.js +16 -33
  38. package/dist/src/ai/fisherman-tools.js +1 -1
  39. package/dist/src/ai/fisherman.d.ts +0 -1
  40. package/dist/src/ai/fisherman.js +0 -3
  41. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  42. package/dist/src/ai/historian/codeceptjs.js +4 -3
  43. package/dist/src/ai/historian/playwright.d.ts +4 -1
  44. package/dist/src/ai/historian/playwright.js +4 -3
  45. package/dist/src/ai/historian/screencast.d.ts +2 -1
  46. package/dist/src/ai/historian/screencast.js +2 -2
  47. package/dist/src/ai/historian/utils.d.ts +0 -1
  48. package/dist/src/ai/historian/utils.js +0 -1
  49. package/dist/src/ai/historian.d.ts +10 -8
  50. package/dist/src/ai/historian.js +1 -5
  51. package/dist/src/ai/navigator.d.ts +8 -6
  52. package/dist/src/ai/navigator.js +34 -65
  53. package/dist/src/ai/pilot.d.ts +13 -8
  54. package/dist/src/ai/pilot.js +53 -59
  55. package/dist/src/ai/planner.d.ts +5 -4
  56. package/dist/src/ai/planner.js +66 -52
  57. package/dist/src/ai/provider.d.ts +4 -1
  58. package/dist/src/ai/provider.js +77 -137
  59. package/dist/src/ai/quartermaster.d.ts +22 -22
  60. package/dist/src/ai/quartermaster.js +8 -14
  61. package/dist/src/ai/rerunner.d.ts +3 -13
  62. package/dist/src/ai/rerunner.js +23 -44
  63. package/dist/src/ai/researcher/cache.js +11 -18
  64. package/dist/src/ai/researcher/coordinates.js +4 -3
  65. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
  66. package/dist/src/ai/researcher/deep-analysis.js +8 -11
  67. package/dist/src/ai/researcher/locators.d.ts +9 -2
  68. package/dist/src/ai/researcher/locators.js +65 -8
  69. package/dist/src/ai/researcher/parser.js +1 -1
  70. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  71. package/dist/src/ai/researcher/research-result.js +0 -19
  72. package/dist/src/ai/researcher/sections.d.ts +2 -0
  73. package/dist/src/ai/researcher/sections.js +7 -3
  74. package/dist/src/ai/researcher.js +40 -106
  75. package/dist/src/ai/rules.d.ts +1 -0
  76. package/dist/src/ai/rules.js +22 -15
  77. package/dist/src/ai/session-analyst.js +8 -5
  78. package/dist/src/ai/task-agent.d.ts +19 -6
  79. package/dist/src/ai/task-agent.js +37 -33
  80. package/dist/src/ai/tester.d.ts +6 -14
  81. package/dist/src/ai/tester.js +46 -111
  82. package/dist/src/ai/tools.d.ts +26 -14
  83. package/dist/src/ai/tools.js +138 -132
  84. package/dist/src/command-handler.js +1 -1
  85. package/dist/src/commands/compact-command.d.ts +1 -1
  86. package/dist/src/commands/compact-command.js +6 -6
  87. package/dist/src/commands/context-aria-command.js +1 -1
  88. package/dist/src/commands/context-command.js +8 -6
  89. package/dist/src/commands/context-data-command.js +2 -2
  90. package/dist/src/commands/context-experience-command.js +2 -2
  91. package/dist/src/commands/context-html-command.js +2 -2
  92. package/dist/src/commands/context-knowledge-command.js +2 -2
  93. package/dist/src/commands/drill-command.js +1 -1
  94. package/dist/src/commands/experience-command.js +1 -1
  95. package/dist/src/commands/explore-command.js +4 -3
  96. package/dist/src/commands/freesail-command.js +2 -2
  97. package/dist/src/commands/knows-command.js +1 -1
  98. package/dist/src/commands/learn-command.js +5 -3
  99. package/dist/src/commands/path-command.js +1 -1
  100. package/dist/src/commands/research-command.js +1 -1
  101. package/dist/src/commands/test-command.js +1 -1
  102. package/dist/src/components/AddKnowledge.d.ts +2 -0
  103. package/dist/src/components/AddKnowledge.js +3 -7
  104. package/dist/src/components/App.js +3 -3
  105. package/dist/src/config.d.ts +19 -0
  106. package/dist/src/config.js +156 -10
  107. package/dist/src/experience-tracker.d.ts +11 -6
  108. package/dist/src/experience-tracker.js +82 -155
  109. package/dist/src/explorbot.d.ts +21 -13
  110. package/dist/src/explorbot.js +84 -80
  111. package/dist/src/explorer.d.ts +66 -102
  112. package/dist/src/explorer.js +332 -623
  113. package/dist/src/knowledge-tracker.d.ts +2 -2
  114. package/dist/src/knowledge-tracker.js +38 -45
  115. package/dist/src/state-manager.d.ts +10 -30
  116. package/dist/src/state-manager.js +11 -129
  117. package/dist/src/utils/aria.d.ts +6 -1
  118. package/dist/src/utils/aria.js +4 -1
  119. package/dist/src/utils/cache.d.ts +15 -0
  120. package/dist/src/utils/cache.js +34 -0
  121. package/dist/src/utils/context-formatter.d.ts +6 -0
  122. package/dist/src/utils/context-formatter.js +15 -27
  123. package/dist/src/utils/hooks-runner.js +2 -4
  124. package/dist/src/utils/html.d.ts +5 -0
  125. package/dist/src/utils/html.js +71 -0
  126. package/dist/src/utils/markdown-files.d.ts +10 -0
  127. package/dist/src/utils/markdown-files.js +21 -0
  128. package/dist/src/utils/markdown-query.d.ts +6 -0
  129. package/dist/src/utils/markdown-query.js +14 -1
  130. package/dist/src/utils/page-readiness.d.ts +1 -0
  131. package/dist/src/utils/page-readiness.js +1 -1
  132. package/dist/src/utils/secrets.js +2 -2
  133. package/dist/src/utils/strings.d.ts +2 -0
  134. package/dist/src/utils/strings.js +10 -0
  135. package/dist/src/utils/test-files.js +1 -1
  136. package/dist/src/utils/web-annotate.d.ts +5 -0
  137. package/dist/src/utils/web-annotate.js +58 -0
  138. package/dist/src/utils/web-eidx.d.ts +2 -0
  139. package/dist/src/utils/web-eidx.js +20 -0
  140. package/dist/src/utils/web-element.d.ts +3 -1
  141. package/dist/src/utils/web-element.js +32 -0
  142. package/dist/src/utils/web-sandbox.d.ts +4 -0
  143. package/dist/src/utils/web-sandbox.js +37 -0
  144. package/models.json +30 -0
  145. package/package.json +23 -3
  146. package/src/action-result.ts +70 -173
  147. package/src/action.ts +38 -132
  148. package/src/ai/agent.ts +20 -0
  149. package/src/ai/captain/idle-mode.ts +1 -1
  150. package/src/ai/captain/test-mode.ts +1 -1
  151. package/src/ai/captain/web-mode.ts +18 -30
  152. package/src/ai/captain.ts +17 -42
  153. package/src/ai/conversation.ts +9 -32
  154. package/src/ai/driller.ts +53 -83
  155. package/src/ai/experience-compactor.ts +16 -35
  156. package/src/ai/fisherman-tools.ts +1 -1
  157. package/src/ai/fisherman.ts +0 -4
  158. package/src/ai/historian/codeceptjs.ts +5 -3
  159. package/src/ai/historian/playwright.ts +7 -4
  160. package/src/ai/historian/screencast.ts +4 -3
  161. package/src/ai/historian/utils.ts +0 -1
  162. package/src/ai/historian.ts +6 -8
  163. package/src/ai/navigator.ts +39 -70
  164. package/src/ai/pilot.ts +59 -61
  165. package/src/ai/planner.ts +76 -62
  166. package/src/ai/provider.ts +85 -146
  167. package/src/ai/quartermaster.ts +39 -45
  168. package/src/ai/rerunner.ts +27 -50
  169. package/src/ai/researcher/cache.ts +10 -16
  170. package/src/ai/researcher/coordinates.ts +4 -3
  171. package/src/ai/researcher/deep-analysis.ts +10 -11
  172. package/src/ai/researcher/locators.ts +66 -9
  173. package/src/ai/researcher/parser.ts +1 -1
  174. package/src/ai/researcher/research-result.ts +0 -18
  175. package/src/ai/researcher/sections.ts +9 -3
  176. package/src/ai/researcher.ts +42 -119
  177. package/src/ai/rules.ts +24 -13
  178. package/src/ai/session-analyst.ts +8 -5
  179. package/src/ai/task-agent.ts +45 -38
  180. package/src/ai/tester.ts +51 -129
  181. package/src/ai/tools.ts +145 -153
  182. package/src/command-handler.ts +1 -1
  183. package/src/commands/compact-command.ts +7 -8
  184. package/src/commands/context-aria-command.ts +1 -1
  185. package/src/commands/context-command.ts +8 -6
  186. package/src/commands/context-data-command.ts +2 -2
  187. package/src/commands/context-experience-command.ts +2 -2
  188. package/src/commands/context-html-command.ts +2 -2
  189. package/src/commands/context-knowledge-command.ts +2 -2
  190. package/src/commands/drill-command.ts +1 -1
  191. package/src/commands/experience-command.ts +1 -1
  192. package/src/commands/explore-command.ts +4 -3
  193. package/src/commands/freesail-command.ts +2 -2
  194. package/src/commands/knows-command.ts +1 -1
  195. package/src/commands/learn-command.ts +5 -3
  196. package/src/commands/path-command.ts +1 -1
  197. package/src/commands/research-command.ts +1 -1
  198. package/src/commands/test-command.ts +1 -1
  199. package/src/components/AddKnowledge.tsx +5 -7
  200. package/src/components/App.tsx +3 -3
  201. package/src/config.ts +185 -11
  202. package/src/experience-tracker.ts +89 -152
  203. package/src/explorbot.ts +88 -83
  204. package/src/explorer.ts +402 -693
  205. package/src/knowledge-tracker.ts +42 -49
  206. package/src/state-manager.ts +20 -155
  207. package/src/utils/aria.ts +11 -2
  208. package/src/utils/cache.ts +40 -0
  209. package/src/utils/context-formatter.ts +12 -15
  210. package/src/utils/hooks-runner.ts +2 -4
  211. package/src/utils/html.ts +79 -0
  212. package/src/utils/markdown-files.ts +30 -0
  213. package/src/utils/markdown-query.ts +15 -1
  214. package/src/utils/page-readiness.ts +1 -1
  215. package/src/utils/secrets.ts +2 -3
  216. package/src/utils/strings.ts +12 -0
  217. package/src/utils/test-files.ts +1 -1
  218. package/src/utils/web-annotate.ts +64 -0
  219. package/src/utils/web-eidx.ts +21 -0
  220. package/src/utils/web-element.ts +34 -0
  221. package/src/utils/web-sandbox.ts +43 -0
  222. package/dist/boat/api-tester/example/apibot.config.js +0 -30
@@ -1,8 +1,9 @@
1
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import type { ActionResult } from '../action-result.ts';
5
- import { ConfigParser } from '../config.ts';
5
+ import { outputPath } from '../config.ts';
6
+ import type Explorer from '../explorer.ts';
6
7
  import type { StateManager, StateTransition, WebPageState } from '../state-manager.ts';
7
8
  import type { Task } from '../test-plan.ts';
8
9
  import { createDebug, tag } from '../utils/logger.ts';
@@ -12,56 +13,25 @@ import type { Provider } from './provider.ts';
12
13
 
13
14
  const debugLog = createDebug('explorbot:quartermaster');
14
15
 
15
- interface AxeViolation {
16
- id: string;
17
- impact: 'critical' | 'serious' | 'moderate' | 'minor';
18
- description: string;
19
- helpUrl: string;
20
- nodes: Array<{ html: string; target: string[] }>;
21
- }
22
-
23
- interface PageAnalysis {
24
- url: string;
25
- stateHash: string;
26
- timestamp: string;
27
- axeViolations: AxeViolation[];
28
- }
29
-
30
- interface SemanticIssue {
31
- type: 'unclear_intention' | 'confusing_naming' | 'hard_to_interact';
32
- element: string;
33
- issue: string;
34
- suggestion: string;
35
- }
36
-
37
- interface AnalysisReport {
38
- scenario: string;
39
- timestamp: string;
40
- url: string;
41
- axeViolations: AxeViolation[];
42
- semanticIssues: SemanticIssue[];
43
- }
44
-
45
16
  export class Quartermaster {
46
17
  private provider: Provider;
47
18
  private model?: any;
48
19
  private outputDir: string;
49
20
  private pageAnalyses: Map<string, PageAnalysis> = new Map();
50
21
  private unsubscribe: (() => void) | null = null;
51
- private playwrightHelper: any = null;
22
+ private explorer: Explorer | null = null;
52
23
  private pendingAnalyses: Promise<void>[] = [];
53
24
 
54
25
  constructor(provider: Provider, options?: { model?: any }) {
55
26
  this.provider = provider;
56
27
  this.model = options?.model;
57
28
 
58
- const configParser = ConfigParser.getInstance();
59
- this.outputDir = join(configParser.getOutputDir(), 'a11y');
29
+ this.outputDir = outputPath('a11y');
60
30
  }
61
31
 
62
- start(playwrightHelper: any, stateManager: StateManager): void {
63
- this.playwrightHelper = playwrightHelper;
64
- this.ensureDirectory();
32
+ start(explorer: Explorer, stateManager: StateManager): void {
33
+ this.explorer = explorer;
34
+ mkdirSync(this.outputDir, { recursive: true });
65
35
 
66
36
  this.unsubscribe = stateManager.onStateChange((event) => {
67
37
  this.onPageChange(event);
@@ -78,12 +48,6 @@ export class Quartermaster {
78
48
  debugLog('Quartermaster stopped');
79
49
  }
80
50
 
81
- private ensureDirectory(): void {
82
- if (!existsSync(this.outputDir)) {
83
- mkdirSync(this.outputDir, { recursive: true });
84
- }
85
- }
86
-
87
51
  private onPageChange(event: StateTransition): void {
88
52
  const state = event.toState;
89
53
  if (!state?.hash) return;
@@ -97,7 +61,7 @@ export class Quartermaster {
97
61
  }
98
62
 
99
63
  private async runAxeAnalysis(state: WebPageState): Promise<void> {
100
- const page = this.playwrightHelper?.page;
64
+ const page = this.explorer?.page;
101
65
  if (!page || !state.hash) {
102
66
  debugLog('No page available for axe analysis');
103
67
  return;
@@ -284,3 +248,33 @@ Focus on what would confuse a real user or caused the agent to make mistakes.`;
284
248
  return content;
285
249
  }
286
250
  }
251
+
252
+ interface AxeViolation {
253
+ id: string;
254
+ impact: 'critical' | 'serious' | 'moderate' | 'minor';
255
+ description: string;
256
+ helpUrl: string;
257
+ nodes: Array<{ html: string; target: string[] }>;
258
+ }
259
+
260
+ interface PageAnalysis {
261
+ url: string;
262
+ stateHash: string;
263
+ timestamp: string;
264
+ axeViolations: AxeViolation[];
265
+ }
266
+
267
+ interface SemanticIssue {
268
+ type: 'unclear_intention' | 'confusing_naming' | 'hard_to_interact';
269
+ element: string;
270
+ issue: string;
271
+ suggestion: string;
272
+ }
273
+
274
+ interface AnalysisReport {
275
+ scenario: string;
276
+ timestamp: string;
277
+ url: string;
278
+ axeViolations: AxeViolation[];
279
+ semanticIssues: SemanticIssue[];
280
+ }
@@ -12,16 +12,13 @@ import figureSet from 'figures';
12
12
  import { z } from 'zod';
13
13
  import { ActionResult } from '../action-result.ts';
14
14
  import { setActivity } from '../activity.ts';
15
- import type { ExperienceTracker } from '../experience-tracker.ts';
16
- import type Explorer from '../explorer.ts';
17
- import type { KnowledgeTracker } from '../knowledge-tracker.ts';
18
15
  import { Stats } from '../stats.ts';
19
16
  import { Task, Test, TestResult } from '../test-plan.ts';
17
+ import { formatHeadings } from '../utils/context-formatter.ts';
20
18
  import { createDebug, tag } from '../utils/logger.ts';
21
19
  import { loop } from '../utils/loop.ts';
22
20
  import { RulesLoader } from '../utils/rules-loader.ts';
23
- import { loadTestSuites, printTestList } from '../utils/test-files.ts';
24
- import type { Agent } from './agent.ts';
21
+ import type { Agent, AgentDeps } from './agent.ts';
25
22
  import { toolExecutionLabel } from './conversation.ts';
26
23
  import type { Navigator } from './navigator.ts';
27
24
  import { Provider } from './provider.ts';
@@ -35,16 +32,13 @@ export class Rerunner extends TaskAgent implements Agent {
35
32
  protected readonly ACTION_TOOLS = ['click', 'pressKey', 'form'];
36
33
  emoji = '🔄';
37
34
 
38
- private explorer: Explorer;
39
- private provider: Provider;
40
35
  private agentTools: any;
41
- private healedSteps: Array<{ test: string; original: string; healed: string }> = [];
36
+ private healedSteps: Array<{ original: string; healed: string }> = [];
42
37
  private traceDir = '';
38
+ private static pluginsWired = false;
43
39
 
44
- constructor(explorer: Explorer, provider: Provider, agentTools?: any) {
45
- super();
46
- this.explorer = explorer;
47
- this.provider = provider;
40
+ constructor(deps: AgentDeps, agentTools?: any) {
41
+ super(deps);
48
42
  this.agentTools = agentTools;
49
43
  }
50
44
 
@@ -52,20 +46,8 @@ export class Rerunner extends TaskAgent implements Agent {
52
46
  throw new Error('Rerunner does not use Navigator');
53
47
  }
54
48
 
55
- protected getExperienceTracker(): ExperienceTracker {
56
- return this.explorer.getStateManager().getExperienceTracker();
57
- }
58
-
59
- protected getKnowledgeTracker(): KnowledgeTracker {
60
- return this.explorer.getKnowledgeTracker();
61
- }
62
-
63
- protected getProvider(): Provider {
64
- return this.provider;
65
- }
66
-
67
49
  private get rerunnerConfig(): Record<string, any> {
68
- return (this.explorer.getConfig().ai?.agents?.rerunner as any) || {};
50
+ return (this.config.ai?.agents?.rerunner as any) || {};
69
51
  }
70
52
 
71
53
  private get healLimit(): number {
@@ -76,10 +58,6 @@ export class Rerunner extends TaskAgent implements Agent {
76
58
  return this.rerunnerConfig.healMaxIterations ?? 3;
77
59
  }
78
60
 
79
- listTests(testsDir: string): void {
80
- printTestList(loadTestSuites(testsDir));
81
- }
82
-
83
61
  async rerun(filePath: string, options?: { testIndices?: number[] }): Promise<RerunResult> {
84
62
  const absPath = resolve(filePath);
85
63
  if (!existsSync(absPath)) {
@@ -219,7 +197,6 @@ export class Rerunner extends TaskAgent implements Agent {
219
197
 
220
198
  private setupPlugins(): void {
221
199
  const healMod = heal.default || heal;
222
- healMod.connectToEvents();
223
200
 
224
201
  healMod.addRecipe('explorbot-ai-healer', {
225
202
  priority: 10,
@@ -233,21 +210,29 @@ export class Rerunner extends TaskAgent implements Agent {
233
210
  healMod.addRecipe(name, recipe);
234
211
  }
235
212
 
213
+ const outputDir = (global as any).output_dir || 'output';
214
+ this.traceDir = `${outputDir}/rerun-traces`;
215
+
216
+ if (Rerunner.pluginsWired) return;
217
+ Rerunner.pluginsWired = true;
218
+
219
+ healMod.connectToEvents();
220
+
236
221
  let currentTest: any = null;
237
222
  let healTries = 0;
238
223
  let isHealing = false;
239
224
  let caughtError: any = null;
240
- const healLimit = this.healLimit;
241
225
 
242
226
  codeceptjs.event.dispatcher.on('test.before', (test: any) => {
243
227
  currentTest = test;
244
228
  healTries = 0;
245
229
  caughtError = null;
230
+ isHealing = false;
246
231
  });
247
232
 
248
233
  codeceptjs.event.dispatcher.on('step.after', (step: any) => {
249
234
  if (isHealing) return;
250
- if (healTries >= healLimit) return;
235
+ if (healTries >= this.healLimit) return;
251
236
  if (!healMod.hasCorrespondingRecipes(step)) return;
252
237
 
253
238
  codeceptjs.recorder.catchWithoutStop(async (err: any) => {
@@ -286,10 +271,6 @@ export class Rerunner extends TaskAgent implements Agent {
286
271
  factor: 1.5,
287
272
  });
288
273
 
289
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
290
- const outputDir = (global as any).output_dir || 'output';
291
- this.traceDir = `${outputDir}/rerun_${timestamp}`;
292
-
293
274
  const aiTrace = aiTracePlugin.default || aiTracePlugin;
294
275
  aiTrace(this.rerunnerConfig.aiTrace || { output: this.traceDir });
295
276
 
@@ -303,9 +284,9 @@ export class Rerunner extends TaskAgent implements Agent {
303
284
 
304
285
  private teardownHealing(): void {
305
286
  const healMod = heal.default || heal;
306
- healMod.recipes['explorbot-ai-healer'] = undefined;
287
+ Reflect.deleteProperty(healMod.recipes, 'explorbot-ai-healer');
307
288
  for (const name of Object.keys(this.rerunnerConfig.recipes || {})) {
308
- healMod.recipes[name] = undefined;
289
+ Reflect.deleteProperty(healMod.recipes, name);
309
290
  }
310
291
  }
311
292
 
@@ -325,7 +306,7 @@ export class Rerunner extends TaskAgent implements Agent {
325
306
  });
326
307
 
327
308
  const healTask = new Task(`Heal: ${failedCode}`);
328
- const codeceptTools = createCodeceptJSTools(this.explorer, healTask);
309
+ const codeceptTools = createCodeceptJSTools(this.toolDeps, healTask);
329
310
 
330
311
  let healed = false;
331
312
  let healedCommand = '';
@@ -345,9 +326,9 @@ export class Rerunner extends TaskAgent implements Agent {
345
326
  healTask.addNote(note);
346
327
  tag('substep').log(note);
347
328
  }
348
- const action = this.explorer.createAction();
329
+ const action = this.explorer.action();
349
330
  await action.execute(`I.wait(${seconds})`);
350
- const state = this.explorer.getStateManager().getCurrentState();
331
+ const state = this.stateManager.getCurrentState();
351
332
  const ar = state ? ActionResult.fromState(state) : null;
352
333
  return {
353
334
  success: true,
@@ -432,14 +413,14 @@ export class Rerunner extends TaskAgent implements Agent {
432
413
  throw new Error(`Could not heal: ${failedCode}`);
433
414
  }
434
415
 
435
- this.healedSteps.push({ test: '', original: failedCode, healed: healedCommand });
416
+ this.healedSteps.push({ original: failedCode, healed: healedCommand });
436
417
  console.log(chalk.green(` ${figureSet.tick} Healed: ${healedCommand}`));
437
418
  };
438
419
  }
439
420
 
440
421
  private getHealSystemPrompt(): string {
441
- const customRules = this.provider.getSystemPromptForAgent('rerunner', this.explorer.getStateManager().getCurrentState()?.url) || '';
442
- const currentUrl = this.explorer.getStateManager().getCurrentState()?.url || '';
422
+ const customRules = this.provider.getSystemPromptForAgent('rerunner', this.stateManager.getCurrentState()?.url) || '';
423
+ const currentUrl = this.stateManager.getCurrentState()?.url || '';
443
424
  const approach = RulesLoader.loadRules('rerunner', ['healing-approach'], currentUrl);
444
425
 
445
426
  return dedent`
@@ -474,14 +455,10 @@ export class Rerunner extends TaskAgent implements Agent {
474
455
  }
475
456
 
476
457
  private getHealUserPrompt(failedCode: string, error: Error): string {
477
- const state = this.explorer.getStateManager().getCurrentState();
458
+ const state = this.stateManager.getCurrentState();
478
459
  const actionResult = state ? ActionResult.fromState(state) : null;
479
460
 
480
- const headings: string[] = [];
481
- if (state?.h1) headings.push(`H1: ${state.h1}`);
482
- if (state?.h2) headings.push(`H2: ${state.h2}`);
483
- if (state?.h3) headings.push(`H3: ${state.h3}`);
484
- if (state?.h4) headings.push(`H4: ${state.h4}`);
461
+ const headings = formatHeadings(state || {});
485
462
 
486
463
  return dedent`
487
464
  A test step failed and needs healing.
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
2
2
  import { join } from 'node:path';
3
3
  import { Worker } from 'node:worker_threads';
4
4
  import { outputPath } from '../../config.ts';
5
+ import { TTLCache } from '../../utils/cache.ts';
5
6
  import { computeHtmlFingerprint } from '../../utils/html-diff.ts';
6
7
  import { debugLog } from './mixin.ts';
7
8
 
@@ -10,8 +11,7 @@ const FINGERPRINT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
10
11
  const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
11
12
  const SIMILARITY_THRESHOLD = 90;
12
13
 
13
- const memoryCache: Record<string, string> = {};
14
- const memoryCacheTimestamps: Record<string, number> = {};
14
+ const memoryCache = new TTLCache<string>(CACHE_TTL_MS);
15
15
 
16
16
  let fingerprintWorker: Worker | null = null;
17
17
 
@@ -28,25 +28,20 @@ function getFingerprintWorker(): Worker {
28
28
  }
29
29
 
30
30
  export function clearResearchCache(): void {
31
- for (const key of Object.keys(memoryCache)) delete memoryCache[key];
32
- for (const key of Object.keys(memoryCacheTimestamps)) delete memoryCacheTimestamps[key];
31
+ memoryCache.clear();
33
32
  }
34
33
 
35
34
  export function getCachedResearch(hash: string): string {
36
35
  if (!hash) return '';
37
- const now = Date.now();
38
- const timestamp = memoryCacheTimestamps[hash];
39
- if (timestamp && now - timestamp <= CACHE_TTL_MS) {
40
- return memoryCache[hash] || '';
41
- }
36
+ const cached = memoryCache.get(hash);
37
+ if (cached !== undefined) return cached;
42
38
  const researchFile = outputPath('research', `${hash}.md`);
43
39
  if (!existsSync(researchFile)) return '';
44
40
  const stats = statSync(researchFile);
45
- if (now - stats.mtimeMs > CACHE_TTL_MS) return '';
46
- const cached = readFileSync(researchFile, 'utf8');
47
- memoryCache[hash] = cached;
48
- memoryCacheTimestamps[hash] = now;
49
- return cached;
41
+ if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) return '';
42
+ const fromDisk = readFileSync(researchFile, 'utf8');
43
+ memoryCache.set(hash, fromDisk);
44
+ return fromDisk;
50
45
  }
51
46
 
52
47
  export function getPreviousResearch(hash: string): string {
@@ -61,8 +56,7 @@ export function saveResearch(hash: string, text: string, combinedHtml?: string):
61
56
  const researchFile = join(researchDir, `${hash}.md`);
62
57
  if (!existsSync(researchDir)) mkdirSync(researchDir, { recursive: true });
63
58
  writeFileSync(researchFile, text);
64
- memoryCache[hash] = text;
65
- memoryCacheTimestamps[hash] = Date.now();
59
+ memoryCache.set(hash, text);
66
60
  debugLog(`Research saved to ${researchFile}`);
67
61
 
68
62
  if (combinedHtml) {
@@ -4,6 +4,7 @@ import type { ActionResult } from '../../action-result.js';
4
4
  import type Explorer from '../../explorer.ts';
5
5
  import { tag } from '../../utils/logger.js';
6
6
  import { mdq } from '../../utils/markdown-query.ts';
7
+ import { eidxByLocator } from '../../utils/web-eidx.ts';
7
8
  import { WebElement } from '../../utils/web-element.ts';
8
9
  import type { Provider } from '../provider.js';
9
10
  import { type Constructor, debugLog } from './mixin.ts';
@@ -81,7 +82,7 @@ export function WithCoordinates<T extends Constructor>(Base: T) {
81
82
  }
82
83
 
83
84
  async visuallyAnnotateElements(opts?: { containers?: Array<{ css: string; label: string }> }): Promise<number> {
84
- return this.explorer.visuallyAnnotateElements(opts);
85
+ return this.explorer.withPage((page) => visuallyAnnotateContainers(page, opts?.containers || []));
85
86
  }
86
87
 
87
88
  private async _analyzeScreenshotForVisualProps(): Promise<VisualAnalysisResult> {
@@ -177,7 +178,7 @@ export function WithCoordinates<T extends Constructor>(Base: T) {
177
178
  let eidx = el.eidx || null;
178
179
  if (!eidx) {
179
180
  const locator = el.css || el.xpath || (el.aria ? `role=${el.aria.role}[name="${el.aria.text}"]` : null);
180
- if (locator) eidx = await this.explorer.getEidxByLocator(locator, section.containerCss);
181
+ if (locator) eidx = await this.explorer.withPage((page) => eidxByLocator(page, locator, section.containerCss));
181
182
  }
182
183
  if (!eidx) continue;
183
184
 
@@ -202,7 +203,7 @@ export function WithCoordinates<T extends Constructor>(Base: T) {
202
203
  }
203
204
  if (eidxWithoutCoords.length === 0) return;
204
205
 
205
- const webElements = await this.explorer.runWithBrowserRecovery('backfillCoordinates', () => WebElement.fromEidxList(this.explorer.playwrightHelper.page, eidxWithoutCoords));
206
+ const webElements = await this.explorer.withPage((page) => WebElement.fromEidxList(page, eidxWithoutCoords));
206
207
  if (webElements.length === 0) return;
207
208
 
208
209
  const rectMap = new Map(webElements.map((w) => [w.eidx!, w]));
@@ -1,5 +1,6 @@
1
1
  import dedent from 'dedent';
2
2
  import { ActionResult, type Diff } from '../../action-result.js';
3
+ import type { ExplorbotConfig } from '../../config.ts';
3
4
  import { executionController } from '../../execution-controller.ts';
4
5
  import type Explorer from '../../explorer.ts';
5
6
  import type { StateManager } from '../../state-manager.js';
@@ -21,13 +22,14 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
21
22
  declare explorer: Explorer;
22
23
  declare provider: Provider;
23
24
  declare stateManager: StateManager;
25
+ declare config: ExplorbotConfig;
24
26
  declare actionResult: ActionResult | undefined;
25
27
 
26
28
  async performDeepAnalysis(state: WebPageState, result: ResearchResult): Promise<void> {
27
29
  tag('info').log('Starting deep analysis of expandable elements');
28
30
  await (this as any).navigateTo(state.fullUrl || state.url);
29
31
 
30
- const maxClicks = (this.explorer.getConfig().ai?.agents?.researcher as any)?.maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
32
+ const maxClicks = (this.config.ai?.agents?.researcher as any)?.maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
31
33
 
32
34
  const expandedSections: string[] = [];
33
35
  const navigationLinks: Array<{ code: string; url: string }> = [];
@@ -98,7 +100,6 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
98
100
  }
99
101
 
100
102
  const diff = await current.diff(previous);
101
- await diff.calculate();
102
103
 
103
104
  if (!diff.ariaChanged && diff.htmlParts.length === 0) {
104
105
  debugLog(`No diff between current and previous state for overlay "${focusArea.name}"`);
@@ -359,13 +360,12 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
359
360
  const isCoordinateClick = el.commands[0].startsWith('I.clickXY(');
360
361
  if (!isCoordinateClick) {
361
362
  const hoverCmd = el.commands[0].replace('I.click(', 'I.moveCursorTo(');
362
- await this.explorer.attemptAction(hoverCmd, undefined, false);
363
+ await this.explorer.action().attempt(hoverCmd);
363
364
  await new Promise((r) => setTimeout(r, 500));
364
365
 
365
- await this.explorer.capturePageState();
366
+ await this.explorer.capture();
366
367
  const hoverAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
367
368
  const hoverDiff = await hoverAR.diff(previousState);
368
- await hoverDiff.calculate();
369
369
  const hoverHtmlSize = hoverDiff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
370
370
  const hoverRevealed = hoverDiff.ariaChanged && hoverHtmlSize > 500;
371
371
 
@@ -403,9 +403,9 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
403
403
  const previousState = ActionResult.fromState(this.stateManager.getCurrentState()!);
404
404
 
405
405
  let clickCode: string | null = null;
406
- const action = this.explorer.createAction();
406
+ const action = this.explorer.action();
407
407
  for (const cmd of commands) {
408
- if (await action.attempt(cmd, undefined, false)) {
408
+ if (await action.attempt(cmd, undefined)) {
409
409
  clickCode = cmd;
410
410
  break;
411
411
  }
@@ -419,10 +419,9 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
419
419
 
420
420
  let diff: Diff;
421
421
  try {
422
- await this.explorer.createAction().capturePageState();
422
+ await this.explorer.capture();
423
423
  const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
424
424
  diff = await currAR.diff(previousState);
425
- await diff.calculate();
426
425
  } catch (err) {
427
426
  tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
428
427
  await this._restorePageState(state.url, originalAria);
@@ -451,9 +450,9 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
451
450
  private async _restorePageState(url: string, originalAria: string): Promise<void> {
452
451
  try {
453
452
  await (this as any).cancelInUi();
454
- await this.explorer.capturePageState();
453
+ await this.explorer.capture();
455
454
  const currentAria = this.stateManager.getCurrentState()?.ariaSnapshot || '';
456
- if (!diffAriaSnapshots(originalAria, currentAria)) return;
455
+ if (!diffAriaSnapshots(originalAria, currentAria).text) return;
457
456
  } catch (err) {
458
457
  tag('warning').log(`State capture failed after cancelInUi: ${err instanceof Error ? err.message : err}`);
459
458
  }
@@ -6,6 +6,7 @@ import { parseAriaLocator } from '../../utils/aria.ts';
6
6
  import { tag } from '../../utils/logger.js';
7
7
  import { mdq } from '../../utils/markdown-query.ts';
8
8
  import { WebElement } from '../../utils/web-element.ts';
9
+ import { isDynamicId } from '../../utils/xpath.ts';
9
10
  import type { Conversation } from '../conversation.ts';
10
11
  import type { Provider } from '../provider.js';
11
12
  import { locatorRule as generalLocatorRuleText } from '../rules.js';
@@ -38,7 +39,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
38
39
  declare provider: Provider;
39
40
  declare actionResult: ActionResult | undefined;
40
41
 
41
- async testLocators(locators: Locator[]): Promise<void> {
42
+ async testLocators(locators: Locator[], opts: { scope?: boolean } = {}): Promise<void> {
42
43
  let broken = 0;
43
44
  for (const loc of locators) {
44
45
  if (executionController.isInterrupted()) break;
@@ -51,20 +52,21 @@ export function WithLocators<T extends Constructor>(Base: T) {
51
52
  continue;
52
53
  }
53
54
  try {
54
- const count = await this.explorer.playwrightLocatorCount((page) => {
55
+ const count = await this.explorer.withPage((page) => {
55
56
  const base = loc.container ? page.locator(loc.container) : page;
56
57
  if (loc.type === 'aria') {
57
58
  const parsed = parseAriaLocator(loc.locator);
58
- if (!parsed) return page.locator('__invalid__');
59
- return base.getByRole(parsed.role as any, { name: parsed.text });
59
+ if (!parsed) return page.locator('__invalid__').count();
60
+ return base.getByRole(parsed.role as any, { name: parsed.text }).count();
60
61
  }
61
62
  const converted = loc.locator.replace(/:contains\(/g, ':has-text(');
62
63
  if (converted !== loc.locator) {
63
64
  loc.locator = converted;
64
65
  }
65
- return base.locator(loc.locator);
66
+ return base.locator(loc.locator).count();
66
67
  });
67
68
  loc.valid = count === 1;
69
+ if (opts.scope && count > 1) loc.valid = true;
68
70
  loc.pwLocator = buildPwLocatorString(loc);
69
71
  if (!loc.valid) {
70
72
  loc.error = count === 0 ? '0 elements' : `${count} elements`;
@@ -194,7 +196,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
194
196
  }
195
197
 
196
198
  if (needsXpath.length > 0) {
197
- const webElements = await this.explorer.runWithBrowserRecovery('backfillBrokenLocators', () => WebElement.fromEidxList(this.explorer.playwrightHelper.page, needsXpath));
199
+ const webElements = await this.explorer.withPage((page) => WebElement.fromEidxList(page, needsXpath));
198
200
  const changedSections = new Set<(typeof sections)[0]>();
199
201
  for (const w of webElements) {
200
202
  const entry = needsXpathEls.get(w.eidx!);
@@ -209,6 +211,60 @@ export function WithLocators<T extends Constructor>(Base: T) {
209
211
  await this.validateContainers(result);
210
212
  }
211
213
 
214
+ async resolveContainers(result: ResearchResult): Promise<Locator[]> {
215
+ const containerLocs = result.containerLocators;
216
+ await this.testLocators(containerLocs, { scope: true });
217
+ for (const loc of containerLocs.filter((l) => l.valid === false)) {
218
+ if (await this.recoverContainerFromChildren(result, loc.locator)) loc.valid = true;
219
+ }
220
+ return containerLocs.filter((l) => l.valid === false);
221
+ }
222
+
223
+ private async recoverContainerFromChildren(result: ResearchResult, css: string): Promise<boolean> {
224
+ const sections = parseResearchSections(result.text).filter((s) => s.containerCss === css);
225
+ let recovered = 0;
226
+
227
+ for (const section of sections) {
228
+ const eidxList = section.elements.map((el) => el.eidx).filter(Boolean) as string[];
229
+ if (eidxList.length < 2) continue;
230
+
231
+ const ancestor = await this.explorer.runWithBrowserRecovery('recoverContainerFromChildren', () => WebElement.commonAncestor(this.explorer.playwrightHelper.page, eidxList));
232
+ if (!ancestor) continue;
233
+
234
+ const candidates: string[] = [];
235
+ const id = ancestor.attrs.id;
236
+ if (id && !isDynamicId(id)) candidates.push(`#${id}`);
237
+ for (const cls of ancestor.filteredClasses) {
238
+ if (!/^[\w-]+$/.test(cls)) continue;
239
+ candidates.push(`${ancestor.tag}.${cls}`);
240
+ }
241
+
242
+ let unique: string | null = null;
243
+ let multiple: string | null = null;
244
+ for (const candidate of candidates) {
245
+ let count = 0;
246
+ try {
247
+ count = await this.explorer.playwrightLocatorCount((page) => page.locator(candidate));
248
+ } catch {}
249
+ if (count === 1) {
250
+ unique = candidate;
251
+ break;
252
+ }
253
+ if (count > 1 && !multiple) multiple = candidate;
254
+ }
255
+
256
+ const newCss = unique || multiple;
257
+ if (!newCss) continue;
258
+
259
+ debugLog(`Recovered container: '${css}' → '${newCss}' in "${section.name}"`);
260
+ this.updateSectionContainer(result, section, newCss);
261
+ recovered++;
262
+ }
263
+
264
+ if (recovered > 0 && recovered < sections.length) debugLog(`Container '${css}' recovered in ${recovered}/${sections.length} sections, still broken for the rest`);
265
+ return recovered > 0 && recovered === sections.length;
266
+ }
267
+
212
268
  private async validateContainers(result: ResearchResult): Promise<void> {
213
269
  const sections = parseResearchSections(result.text);
214
270
 
@@ -217,7 +273,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
217
273
 
218
274
  let count = 0;
219
275
  try {
220
- count = await this.explorer.playwrightLocatorCount((page) => page.locator(section.containerCss!));
276
+ count = await this.explorer.withPage((page) => page.locator(section.containerCss!).count());
221
277
  } catch {}
222
278
 
223
279
  if (count >= 1) continue;
@@ -226,7 +282,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
226
282
  if (simplified) {
227
283
  let simplifiedCount = 0;
228
284
  try {
229
- simplifiedCount = await this.explorer.playwrightLocatorCount((page) => page.locator(simplified));
285
+ simplifiedCount = await this.explorer.withPage((page) => page.locator(simplified).count());
230
286
  } catch {}
231
287
 
232
288
  if (simplifiedCount >= 1) {
@@ -275,7 +331,8 @@ export interface Locator {
275
331
  }
276
332
 
277
333
  export interface LocatorMethods {
278
- testLocators(locators: Locator[]): Promise<void>;
334
+ testLocators(locators: Locator[], opts?: { scope?: boolean }): Promise<void>;
279
335
  fixBrokenSections(result: ResearchResult, conversation: Conversation): Promise<void>;
280
336
  backfillBrokenLocators(result: ResearchResult): Promise<void>;
337
+ resolveContainers(result: ResearchResult): Promise<Locator[]>;
281
338
  }
@@ -94,7 +94,7 @@ export function extractContainerFromBlockquote(sectionMarkdown: string): string
94
94
  }
95
95
 
96
96
  export function parseResearchSections(markdown: string): ResearchSection[] {
97
- const hasExtendedResearch = markdown.includes('\n# Extended Research') || markdown.startsWith('# Extended Research');
97
+ const hasExtendedResearch = mdq(markdown).query('section1(~"Extended Research")').count() > 0;
98
98
 
99
99
  return parseSections(markdown)
100
100
  .filter((s) => !SKIP_SECTIONS.has(s.name.toLowerCase()) && !s.name.toLowerCase().includes('data:'))
@@ -46,24 +46,6 @@ export class ResearchResult {
46
46
  }));
47
47
  }
48
48
 
49
- updateSection(sectionName: string, locators: Locator[]): void {
50
- const sections = parseResearchSections(this.text);
51
- const section = sections.find((s) => s.name === sectionName);
52
- if (!section) return;
53
-
54
- for (const el of section.elements) {
55
- const elLocators = locators.filter((l) => l.element === el.name);
56
- for (const loc of elLocators) {
57
- const value = loc.valid === false ? null : loc.locator || null;
58
- if (loc.type === 'css') el.css = value;
59
- if (loc.type === 'xpath') el.xpath = value;
60
- if (loc.type === 'aria') el.aria = value ? parseAriaLocator(value) : null;
61
- }
62
- }
63
-
64
- this.rebuildSectionInText(section);
65
- }
66
-
67
49
  rebuildSectionInText(section: ResearchSection): void {
68
50
  if (section.elements.length === 0) return;
69
51
  const newTable = rebuildSectionMarkdown(section);