explorbot 0.1.32-beta.1 → 0.2.0

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 (168) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +20 -3
  17. package/dist/src/action-result.d.ts +8 -10
  18. package/dist/src/action-result.js +72 -164
  19. package/dist/src/action.d.ts +3 -10
  20. package/dist/src/action.js +18 -111
  21. package/dist/src/ai/captain/web-mode.js +0 -7
  22. package/dist/src/ai/captain.d.ts +1 -3
  23. package/dist/src/ai/captain.js +4 -19
  24. package/dist/src/ai/conversation.d.ts +2 -5
  25. package/dist/src/ai/conversation.js +9 -28
  26. package/dist/src/ai/driller.d.ts +0 -4
  27. package/dist/src/ai/driller.js +3 -12
  28. package/dist/src/ai/experience-compactor.d.ts +2 -2
  29. package/dist/src/ai/experience-compactor.js +16 -33
  30. package/dist/src/ai/fisherman-tools.js +1 -1
  31. package/dist/src/ai/fisherman.d.ts +0 -1
  32. package/dist/src/ai/fisherman.js +0 -3
  33. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  34. package/dist/src/ai/historian/codeceptjs.js +3 -2
  35. package/dist/src/ai/historian/playwright.d.ts +2 -0
  36. package/dist/src/ai/historian/playwright.js +3 -2
  37. package/dist/src/ai/historian/utils.d.ts +0 -1
  38. package/dist/src/ai/historian/utils.js +0 -1
  39. package/dist/src/ai/historian.d.ts +5 -4
  40. package/dist/src/ai/historian.js +1 -5
  41. package/dist/src/ai/navigator.d.ts +3 -5
  42. package/dist/src/ai/navigator.js +11 -46
  43. package/dist/src/ai/pilot.d.ts +1 -3
  44. package/dist/src/ai/pilot.js +6 -17
  45. package/dist/src/ai/planner.d.ts +3 -3
  46. package/dist/src/ai/planner.js +49 -37
  47. package/dist/src/ai/provider.d.ts +4 -1
  48. package/dist/src/ai/provider.js +77 -136
  49. package/dist/src/ai/quartermaster.d.ts +21 -22
  50. package/dist/src/ai/quartermaster.js +4 -10
  51. package/dist/src/ai/rerunner.d.ts +1 -2
  52. package/dist/src/ai/rerunner.js +14 -22
  53. package/dist/src/ai/researcher/cache.js +11 -18
  54. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  55. package/dist/src/ai/researcher/parser.js +1 -1
  56. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  57. package/dist/src/ai/researcher/research-result.js +0 -19
  58. package/dist/src/ai/researcher/sections.js +5 -1
  59. package/dist/src/ai/researcher.js +12 -53
  60. package/dist/src/ai/rules.d.ts +1 -0
  61. package/dist/src/ai/rules.js +22 -15
  62. package/dist/src/ai/session-analyst.js +8 -5
  63. package/dist/src/ai/task-agent.d.ts +1 -1
  64. package/dist/src/ai/task-agent.js +2 -27
  65. package/dist/src/ai/tester.d.ts +0 -2
  66. package/dist/src/ai/tester.js +10 -61
  67. package/dist/src/ai/tools.d.ts +18 -7
  68. package/dist/src/ai/tools.js +119 -101
  69. package/dist/src/commands/compact-command.d.ts +1 -1
  70. package/dist/src/commands/compact-command.js +6 -6
  71. package/dist/src/commands/context-command.js +1 -1
  72. package/dist/src/commands/context-knowledge-command.js +1 -1
  73. package/dist/src/commands/experience-command.js +1 -1
  74. package/dist/src/commands/knows-command.js +1 -1
  75. package/dist/src/commands/learn-command.js +3 -1
  76. package/dist/src/components/AddKnowledge.d.ts +2 -0
  77. package/dist/src/components/AddKnowledge.js +3 -7
  78. package/dist/src/components/App.js +2 -2
  79. package/dist/src/config.d.ts +2 -0
  80. package/dist/src/config.js +12 -0
  81. package/dist/src/experience-tracker.d.ts +11 -6
  82. package/dist/src/experience-tracker.js +81 -154
  83. package/dist/src/explorbot.d.ts +4 -5
  84. package/dist/src/explorbot.js +17 -51
  85. package/dist/src/explorer.d.ts +4 -15
  86. package/dist/src/explorer.js +18 -68
  87. package/dist/src/knowledge-tracker.d.ts +2 -2
  88. package/dist/src/knowledge-tracker.js +38 -45
  89. package/dist/src/state-manager.d.ts +6 -31
  90. package/dist/src/state-manager.js +10 -129
  91. package/dist/src/utils/aria.d.ts +6 -1
  92. package/dist/src/utils/aria.js +4 -1
  93. package/dist/src/utils/cache.d.ts +15 -0
  94. package/dist/src/utils/cache.js +34 -0
  95. package/dist/src/utils/context-formatter.d.ts +6 -0
  96. package/dist/src/utils/context-formatter.js +15 -27
  97. package/dist/src/utils/html.d.ts +5 -0
  98. package/dist/src/utils/html.js +71 -0
  99. package/dist/src/utils/markdown-files.d.ts +10 -0
  100. package/dist/src/utils/markdown-files.js +21 -0
  101. package/dist/src/utils/markdown-query.d.ts +6 -0
  102. package/dist/src/utils/markdown-query.js +14 -1
  103. package/dist/src/utils/page-readiness.d.ts +1 -0
  104. package/dist/src/utils/page-readiness.js +1 -1
  105. package/dist/src/utils/secrets.js +2 -2
  106. package/dist/src/utils/strings.d.ts +2 -0
  107. package/dist/src/utils/strings.js +10 -0
  108. package/dist/src/utils/web-element.d.ts +2 -1
  109. package/dist/src/utils/web-element.js +3 -0
  110. package/dist/src/utils/web-sandbox.d.ts +4 -0
  111. package/dist/src/utils/web-sandbox.js +37 -0
  112. package/package.json +20 -3
  113. package/src/action-result.ts +70 -173
  114. package/src/action.ts +16 -130
  115. package/src/ai/captain/web-mode.ts +0 -7
  116. package/src/ai/captain.ts +5 -17
  117. package/src/ai/conversation.ts +9 -32
  118. package/src/ai/driller.ts +3 -16
  119. package/src/ai/experience-compactor.ts +16 -35
  120. package/src/ai/fisherman-tools.ts +1 -1
  121. package/src/ai/fisherman.ts +0 -4
  122. package/src/ai/historian/codeceptjs.ts +4 -2
  123. package/src/ai/historian/playwright.ts +4 -2
  124. package/src/ai/historian/utils.ts +0 -1
  125. package/src/ai/historian.ts +4 -7
  126. package/src/ai/navigator.ts +13 -49
  127. package/src/ai/pilot.ts +6 -16
  128. package/src/ai/planner.ts +56 -44
  129. package/src/ai/provider.ts +85 -145
  130. package/src/ai/quartermaster.ts +34 -41
  131. package/src/ai/rerunner.ts +17 -21
  132. package/src/ai/researcher/cache.ts +10 -16
  133. package/src/ai/researcher/deep-analysis.ts +3 -6
  134. package/src/ai/researcher/parser.ts +1 -1
  135. package/src/ai/researcher/research-result.ts +0 -18
  136. package/src/ai/researcher/sections.ts +5 -1
  137. package/src/ai/researcher.ts +11 -59
  138. package/src/ai/rules.ts +24 -13
  139. package/src/ai/session-analyst.ts +8 -5
  140. package/src/ai/task-agent.ts +3 -30
  141. package/src/ai/tester.ts +12 -73
  142. package/src/ai/tools.ts +119 -106
  143. package/src/commands/compact-command.ts +7 -8
  144. package/src/commands/context-command.ts +1 -1
  145. package/src/commands/context-knowledge-command.ts +1 -1
  146. package/src/commands/experience-command.ts +1 -1
  147. package/src/commands/knows-command.ts +1 -1
  148. package/src/commands/learn-command.ts +3 -1
  149. package/src/components/AddKnowledge.tsx +5 -7
  150. package/src/components/App.tsx +2 -2
  151. package/src/config.ts +12 -0
  152. package/src/experience-tracker.ts +88 -151
  153. package/src/explorbot.ts +17 -51
  154. package/src/explorer.ts +20 -72
  155. package/src/knowledge-tracker.ts +42 -49
  156. package/src/state-manager.ts +15 -156
  157. package/src/utils/aria.ts +11 -2
  158. package/src/utils/cache.ts +40 -0
  159. package/src/utils/context-formatter.ts +12 -15
  160. package/src/utils/html.ts +79 -0
  161. package/src/utils/markdown-files.ts +30 -0
  162. package/src/utils/markdown-query.ts +15 -1
  163. package/src/utils/page-readiness.ts +1 -1
  164. package/src/utils/secrets.ts +2 -3
  165. package/src/utils/strings.ts +12 -0
  166. package/src/utils/web-element.ts +4 -0
  167. package/src/utils/web-sandbox.ts +43 -0
  168. package/dist/boat/api-tester/example/apibot.config.js +0 -30
@@ -1,4 +1,4 @@
1
- import { type RawElementData } from './html.js';
1
+ import { EXPLORBOT_ATTRS, type RawElementData } from './html.js';
2
2
  import { type XPathMatch } from './xpath.js';
3
3
  export { extractElementData } from './html.js';
4
4
  export declare class WebElement {
@@ -26,6 +26,7 @@ export declare class WebElement {
26
26
  get keyAttrs(): string;
27
27
  get coordinates(): string;
28
28
  get eidx(): string | null;
29
+ ourAttr(name: keyof typeof EXPLORBOT_ATTRS): string | undefined;
29
30
  get isNavigationLink(): boolean;
30
31
  get filteredClasses(): string[];
31
32
  get areaHints(): string[];
@@ -39,6 +39,9 @@ export class WebElement {
39
39
  get eidx() {
40
40
  return this.attrs[EXPLORBOT_ATTRS.eidx] || this.attrs.eidx || null;
41
41
  }
42
+ ourAttr(name) {
43
+ return this.attrs[EXPLORBOT_ATTRS[name]];
44
+ }
42
45
  get isNavigationLink() {
43
46
  if (this.tag !== 'a')
44
47
  return false;
@@ -0,0 +1,4 @@
1
+ export declare function sanitizeCodeBlock(code: string): string;
2
+ export declare function hasPlaywrightCommands(code: string): boolean;
3
+ export declare function playwrightSandbox(page: any, code: string): Promise<any>;
4
+ export declare function codeceptJSSandbox(actor: any, codeOrFn: string | ((...args: any[]) => void)): void;
@@ -0,0 +1,37 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
3
+ import step from 'codeceptjs/steps';
4
+ const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
5
+ const ALLOWED_COMMAND_HEADS = ['I.', 'page.', 'await page.', 'await I.'];
6
+ const PLAYWRIGHT_COMMAND_HEADS = ['page.', 'await page.'];
7
+ const PLAYWRIGHT_ARG_NAMES = ['page'];
8
+ const CODECEPT_ARG_NAMES = ['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker'];
9
+ export function sanitizeCodeBlock(code) {
10
+ return code
11
+ .split('\n')
12
+ .map((line) => line.trim())
13
+ .filter((line) => ALLOWED_COMMAND_HEADS.some((head) => line.startsWith(head)))
14
+ .join('\n');
15
+ }
16
+ export function hasPlaywrightCommands(code) {
17
+ return code.split('\n').some((line) => {
18
+ const trimmed = line.trim();
19
+ return PLAYWRIGHT_COMMAND_HEADS.some((head) => trimmed.startsWith(head));
20
+ });
21
+ }
22
+ export function playwrightSandbox(page, code) {
23
+ const run = createSandbox(PLAYWRIGHT_ARG_NAMES, `return (async () => { ${code} })()`);
24
+ return run(page);
25
+ }
26
+ export function codeceptJSSandbox(actor, codeOrFn) {
27
+ if (typeof codeOrFn === 'function') {
28
+ codeOrFn(actor, tryTo, retryTo, within, hopeThat, step, faker);
29
+ return;
30
+ }
31
+ const run = createSandbox(CODECEPT_ARG_NAMES, codeOrFn);
32
+ run(actor, tryTo, retryTo, within, hopeThat, step, faker);
33
+ }
34
+ function createSandbox(argNames, body) {
35
+ const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
36
+ return (...args) => fn(...args);
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.32-beta.1",
3
+ "version": "0.2.0",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -16,7 +16,18 @@
16
16
  "bin": {
17
17
  "explorbot": "./dist/bin/explorbot-cli.js"
18
18
  },
19
- "files": ["dist/", "src/**/*.ts", "src/**/*.tsx", "bin/**/*.ts", "boat/api-tester/src/**/*.ts", "boat/doc-collector/src/**/*.ts", "boat/doc-collector/bin/**/*.ts", "boat/doc-collector/package.json", "rules/", "assets/sample-files/"],
19
+ "files": [
20
+ "dist/",
21
+ "src/**/*.ts",
22
+ "src/**/*.tsx",
23
+ "bin/**/*.ts",
24
+ "boat/api-tester/src/**/*.ts",
25
+ "boat/doc-collector/src/**/*.ts",
26
+ "boat/doc-collector/bin/**/*.ts",
27
+ "boat/doc-collector/package.json",
28
+ "rules/",
29
+ "assets/sample-files/"
30
+ ],
20
31
  "scripts": {
21
32
  "build": "bun run build:bin",
22
33
  "build:bin": "bun build bin/explorbot-cli.ts --outdir bin --target node --external commander --format esm",
@@ -43,7 +54,13 @@
43
54
  "check:fix": "biome check --write .",
44
55
  "langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts"
45
56
  },
46
- "keywords": ["cli", "react", "ink", "codeceptjs", "playwright"],
57
+ "keywords": [
58
+ "cli",
59
+ "react",
60
+ "ink",
61
+ "codeceptjs",
62
+ "playwright"
63
+ ],
47
64
  "repository": {
48
65
  "type": "git",
49
66
  "url": "https://github.com/testomatio/explorbot"
@@ -2,10 +2,12 @@ import fs from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ConfigParser, type HtmlConfig, outputPath } from './config.ts';
4
4
  import type { Link, WebPageState } from './state-manager.ts';
5
- import { compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
5
+ import { LARGE_ARIA_CHANGE_THRESHOLD, compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
6
+ import { TTLCache } from './utils/cache.ts';
6
7
  import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff } from './utils/html-diff.ts';
7
8
  import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from './utils/html.ts';
8
9
  import { createDebug } from './utils/logger.ts';
10
+ import { slugify } from './utils/strings.ts';
9
11
  import { extractStatePath, matchesUrl } from './utils/url-matcher.ts';
10
12
 
11
13
  const debugLog = createDebug('explorbot:state');
@@ -37,6 +39,7 @@ export interface PageDiff {
37
39
  previousUrl?: string;
38
40
  currentUrl: string;
39
41
  ariaChanges?: string | null;
42
+ ariaChangeCount?: number;
40
43
  htmlParts?: HtmlDiffPart[];
41
44
  iframes?: string;
42
45
  }
@@ -68,13 +71,12 @@ export class ActionResult implements ActionResultData {
68
71
  private _screenshot: Buffer | undefined = undefined;
69
72
  readonly htmlFile: string | undefined = undefined;
70
73
  private _html: string | undefined = undefined;
71
- private snapshotCache = new Map<string, string>();
74
+ private snapshotCache = new TTLCache<string>();
72
75
  readonly logFile: string | undefined = undefined;
73
- private _browserLogs: any[] | undefined = undefined;
74
76
  readonly ariaSnapshotFile: string | undefined = undefined;
75
77
  private _ariaSnapshot: string | null | undefined = undefined;
76
78
  private _lastExtractedHtml: string | undefined = undefined;
77
- notes: any;
79
+ notes: string[] = [];
78
80
  public links: Link[] = [];
79
81
  public verifications?: Record<string, boolean>;
80
82
 
@@ -106,14 +108,11 @@ export class ActionResult implements ActionResultData {
106
108
  this.ariaSnapshotFile = data.ariaSnapshotFile;
107
109
  }
108
110
 
109
- // Store HTML and browser logs in private properties if provided
111
+ // Store HTML in a private property if provided
110
112
  if (data.html !== undefined) {
111
113
  this._html = data.html;
112
114
  }
113
115
 
114
- if (data.browserLogs !== undefined) {
115
- this._browserLogs = data.browserLogs;
116
- }
117
116
  if (data.screenshot !== undefined) {
118
117
  this._screenshot = data.screenshot;
119
118
  }
@@ -121,7 +120,7 @@ export class ActionResult implements ActionResultData {
121
120
  this._ariaSnapshot = data.ariaSnapshot;
122
121
  }
123
122
 
124
- if (!this.fullUrl && this.url && this.url !== '') {
123
+ if (!this.fullUrl && this.url) {
125
124
  this.fullUrl = this.url;
126
125
  }
127
126
 
@@ -133,7 +132,7 @@ export class ActionResult implements ActionResultData {
133
132
  this.links = extractLinks(this._html);
134
133
  }
135
134
 
136
- if (this.url && this.url !== '') {
135
+ if (this.url) {
137
136
  this.url = extractStatePath(this.url);
138
137
  }
139
138
  }
@@ -165,16 +164,6 @@ export class ActionResult implements ActionResultData {
165
164
  return undefined;
166
165
  }
167
166
 
168
- get browserLogsContent(): any[] {
169
- if (this._browserLogs !== undefined) {
170
- return this._browserLogs;
171
- }
172
- if (this.logFile) {
173
- return ActionResult.loadBrowserLogsFromFile(this.logFile);
174
- }
175
- return [];
176
- }
177
-
178
167
  get ariaSnapshot(): string | null {
179
168
  if (this._ariaSnapshot !== undefined) {
180
169
  return this._ariaSnapshot;
@@ -223,14 +212,14 @@ export class ActionResult implements ActionResultData {
223
212
  }
224
213
 
225
214
  isSameUrl(state: WebPageState): boolean {
226
- if (!this.url || this.url === '') {
215
+ if (!this.url) {
227
216
  return false;
228
217
  }
229
218
  return extractStatePath(state.url) === extractStatePath(this.url);
230
219
  }
231
220
 
232
221
  isMatchedBy(state: WebPageState): boolean {
233
- if (!this.url || this.url === '') {
222
+ if (!this.url) {
234
223
  return false;
235
224
  }
236
225
 
@@ -266,13 +255,13 @@ export class ActionResult implements ActionResultData {
266
255
  async simplifiedHtml(htmlConfig?: HtmlConfig): Promise<string> {
267
256
  const normalizedConfig = this.normalizeHtmlConfig(htmlConfig);
268
257
  const cacheKey = `simplified:${JSON.stringify(normalizedConfig?.minimal ?? null)}`;
269
- return this.cachedSnapshot(cacheKey, () => minifyHtml(htmlMinimalUISnapshot(this.html ?? '', normalizedConfig?.minimal)));
258
+ return this.snapshotCache.fetch(cacheKey, () => minifyHtml(htmlMinimalUISnapshot(this.html ?? '', normalizedConfig?.minimal)));
270
259
  }
271
260
 
272
261
  async combinedHtml(htmlConfig?: HtmlConfig & { keepPositions?: boolean }): Promise<string> {
273
262
  const normalizedConfig = this.normalizeHtmlConfig(htmlConfig);
274
263
  const cacheKey = `combined:${!!htmlConfig?.keepPositions}:${JSON.stringify(normalizedConfig?.combined ?? null)}`;
275
- return this.cachedSnapshot(cacheKey, async () => {
264
+ return this.snapshotCache.fetch(cacheKey, async () => {
276
265
  const combinedHtml = await minifyHtml(htmlCombinedSnapshot(this.html ?? '', normalizedConfig?.combined, { keepPositions: htmlConfig?.keepPositions }));
277
266
  debugLog(`----${this.url}----`);
278
267
  debugLog(`Combined HTML: \n${combinedHtml}`);
@@ -284,7 +273,7 @@ export class ActionResult implements ActionResultData {
284
273
  async textHtml(htmlConfig?: HtmlConfig): Promise<string> {
285
274
  const normalizedConfig = this.normalizeHtmlConfig(htmlConfig);
286
275
  const cacheKey = `text:${JSON.stringify(normalizedConfig?.text ?? null)}`;
287
- return this.cachedSnapshot(cacheKey, () => minifyHtml(htmlTextSnapshot(this.html ?? '', normalizedConfig?.text)));
276
+ return this.snapshotCache.fetch(cacheKey, () => minifyHtml(htmlTextSnapshot(this.html ?? '', normalizedConfig?.text)));
288
277
  }
289
278
 
290
279
  getInteractiveARIA(): string {
@@ -303,14 +292,6 @@ export class ActionResult implements ActionResultData {
303
292
  return parser.getConfig().html;
304
293
  }
305
294
 
306
- private async cachedSnapshot(key: string, compute: () => Promise<string>): Promise<string> {
307
- const cached = this.snapshotCache.get(key);
308
- if (cached !== undefined) return cached;
309
- const value = await compute();
310
- this.snapshotCache.set(key, value);
311
- return value;
312
- }
313
-
314
295
  static fromState(state: WebPageState): ActionResult {
315
296
  let html: string | undefined = undefined;
316
297
  let screenshot: Buffer | undefined = undefined;
@@ -356,26 +337,26 @@ export class ActionResult implements ActionResultData {
356
337
  return new ActionResult(actionResultData);
357
338
  }
358
339
 
359
- private static loadHtmlFromFile(htmlFile: string): string | null {
340
+ private static loadStateFile(file: string): string | null {
360
341
  try {
361
- const filePath = outputPath('states', htmlFile);
362
- if (fs.existsSync(filePath)) {
363
- return fs.readFileSync(filePath, 'utf8');
364
- }
365
- return null;
342
+ const filePath = outputPath('states', file);
343
+ if (!fs.existsSync(filePath)) return null;
344
+ return fs.readFileSync(filePath, 'utf8');
366
345
  } catch (error) {
367
- console.error('Failed to load HTML from file:', error);
346
+ console.error('Failed to load state file:', error);
368
347
  return null;
369
348
  }
370
349
  }
371
350
 
351
+ private static loadHtmlFromFile(htmlFile: string): string | null {
352
+ return ActionResult.loadStateFile(htmlFile);
353
+ }
354
+
372
355
  private static loadScreenshotFromFile(screenshotFile: string): Buffer | undefined {
373
356
  try {
374
357
  const filePath = outputPath('states', screenshotFile);
375
- if (fs.existsSync(filePath)) {
376
- return fs.readFileSync(filePath);
377
- }
378
- return undefined;
358
+ if (!fs.existsSync(filePath)) return undefined;
359
+ return fs.readFileSync(filePath);
379
360
  } catch (error) {
380
361
  console.error('Failed to load screenshot from file:', error);
381
362
  return undefined;
@@ -383,58 +364,38 @@ export class ActionResult implements ActionResultData {
383
364
  }
384
365
 
385
366
  private static loadBrowserLogsFromFile(logFile: string): any[] {
386
- try {
387
- const filePath = outputPath('states', logFile);
388
- if (fs.existsSync(filePath)) {
389
- const logContent = fs.readFileSync(filePath, 'utf8');
390
- return logContent
391
- .split('\n')
392
- .filter((line) => line.trim())
393
- .map((line) => {
394
- const match = line.match(/\[([^\]]+)\] (\w+): (.+)/);
395
- if (match) {
396
- return {
397
- timestamp: match[1],
398
- type: match[2].toLowerCase(),
399
- text: match[3],
400
- level: match[2].toLowerCase(),
401
- message: match[3],
402
- };
403
- }
404
- return { text: line, type: 'log', level: 'log', message: line };
405
- });
406
- }
407
- return [];
408
- } catch (error) {
409
- console.error('Failed to load browser logs from file:', error);
410
- return [];
411
- }
367
+ const logContent = ActionResult.loadStateFile(logFile);
368
+ if (!logContent) return [];
369
+ return logContent
370
+ .split('\n')
371
+ .filter((line) => line.trim())
372
+ .map((line) => {
373
+ const match = line.match(/\[([^\]]+)\] (\w+): (.+)/);
374
+ if (match) {
375
+ return {
376
+ timestamp: match[1],
377
+ type: match[2].toLowerCase(),
378
+ text: match[3],
379
+ level: match[2].toLowerCase(),
380
+ message: match[3],
381
+ };
382
+ }
383
+ return { text: line, type: 'log', level: 'log', message: line };
384
+ });
412
385
  }
413
386
 
414
387
  private static loadAriaSnapshotFromFile(ariaSnapshotFile: string): string | null {
415
- try {
416
- const filePath = outputPath('states', ariaSnapshotFile);
417
- if (!fs.existsSync(filePath)) {
418
- return null;
419
- }
420
-
421
- const content = fs.readFileSync(filePath, 'utf8');
422
- const trimmed = content.trim();
423
- if (!trimmed) {
424
- return null;
425
- }
426
-
427
- return trimmed.endsWith('\n') ? trimmed : `${trimmed}\n`;
428
- } catch (error) {
429
- console.error('Failed to load aria snapshot from file:', error);
430
- return null;
431
- }
388
+ const content = ActionResult.loadStateFile(ariaSnapshotFile);
389
+ if (!content) return null;
390
+ const trimmed = content.trim();
391
+ if (!trimmed) return null;
392
+ return trimmed.endsWith('\n') ? trimmed : `${trimmed}\n`;
432
393
  }
433
394
 
434
395
  toAiContext(): string {
435
396
  const parts: string[] = [];
436
397
 
437
- if (this.url && this.url !== '') {
398
+ if (this.url) {
438
399
  parts.push(`<url>${this.url}</url>`);
439
400
  }
440
401
 
@@ -480,7 +441,7 @@ export class ActionResult implements ActionResultData {
480
441
  }
481
442
 
482
443
  get relativeUrl(): string | null {
483
- if (!this.url || this.url === '') return null;
444
+ if (!this.url) return null;
484
445
 
485
446
  try {
486
447
  const urlObj = new URL(this.url);
@@ -505,22 +466,10 @@ export class ActionResult implements ActionResultData {
505
466
 
506
467
  this.extractHeadings(this.html);
507
468
 
508
- const headings = ['h1', 'h2'];
469
+ if (this.h1) parts.push(`h1_${this.h1}`);
470
+ if (this.h2) parts.push(`h2_${this.h2}`);
509
471
 
510
- for (const heading of headings) {
511
- const value = this[heading as keyof this] as string;
512
- if (value) {
513
- parts.push(`${heading}_${value}`);
514
- }
515
- }
516
-
517
- let stateString = parts
518
- .map((part) => part.substring(0, 100))
519
- .join('_')
520
- .replace(/[^a-zA-Z0-9_]/g, '_')
521
- .replace(/_+/g, '_')
522
- .replace(/^_|_$/g, '')
523
- .toLowerCase();
472
+ let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
524
473
 
525
474
  if (stateString.length > 200) {
526
475
  stateString = stateString.substring(0, 200);
@@ -532,60 +481,8 @@ export class ActionResult implements ActionResultData {
532
481
  return stateString;
533
482
  }
534
483
 
535
- private saveBrowserLogs(): void {
536
- const logs = this.browserLogsContent;
537
- if (!logs || logs.length === 0) return;
538
-
539
- try {
540
- const outputDir = 'output';
541
- const stateHash = this.getStateHash();
542
- const filename = `${stateHash}.log`;
543
- const filePath = join(outputDir, filename);
544
-
545
- // Ensure output directory exists
546
- if (!fs.existsSync(outputDir)) {
547
- fs.mkdirSync(outputDir, { recursive: true });
548
- }
549
-
550
- // Format logs for saving
551
- const formattedLogs = logs.map((log: any) => {
552
- const timestamp = new Date().toISOString();
553
- const level = (log.type || log.level || 'LOG').toUpperCase();
554
- const message = log.text || log.message || String(log);
555
- return `[${timestamp}] ${level}: ${message}`;
556
- });
557
-
558
- // Save log content to file
559
- const logContent = `${formattedLogs.join('\n')}\n`;
560
- fs.writeFileSync(filePath, logContent, 'utf8');
561
- } catch (error) {
562
- // Silently fail to avoid breaking the main flow
563
- console.error('Failed to save browser logs:', error);
564
- }
565
- }
566
-
567
- private saveHtmlOutput(): void {
568
- try {
569
- const outputDir = 'output';
570
- const stateHash = this.getStateHash();
571
- const filename = `${stateHash}.html`;
572
- const filePath = join(outputDir, filename);
573
-
574
- // Ensure output directory exists
575
- if (!fs.existsSync(outputDir)) {
576
- fs.mkdirSync(outputDir, { recursive: true });
577
- }
578
-
579
- // Save HTML content to file
580
- fs.writeFileSync(filePath, this.html, 'utf8');
581
- } catch (error) {
582
- // Silently fail to avoid breaking the main flow
583
- console.error('Failed to save HTML output:', error);
584
- }
585
- }
586
-
587
484
  async diff(previousState: ActionResult | null): Promise<Diff> {
588
- return new Diff(this, previousState);
485
+ return Diff.create(this, previousState);
589
486
  }
590
487
 
591
488
  async toToolResult(previousState: ActionResult | null, locator: string): Promise<ToolResultMetadata> {
@@ -617,7 +514,6 @@ export class ActionResult implements ActionResultData {
617
514
  }
618
515
 
619
516
  const diff = await this.diff(previousState);
620
- await diff.calculate();
621
517
 
622
518
  const pageDiff: PageDiff = {
623
519
  urlChanged,
@@ -627,6 +523,7 @@ export class ActionResult implements ActionResultData {
627
523
 
628
524
  if (diff.ariaChanged) {
629
525
  pageDiff.ariaChanges = diff.ariaChanged;
526
+ pageDiff.ariaChangeCount = diff.ariaChangeCount;
630
527
  }
631
528
 
632
529
  if (diff.htmlParts.length > 0) {
@@ -645,10 +542,7 @@ export class ActionResult implements ActionResultData {
645
542
  }
646
543
 
647
544
  if (pageDiff.ariaChanges && this.iframeSnapshots.length > 0) {
648
- const addedCount = (pageDiff.ariaChanges.match(/\n {4}- /g) || []).length;
649
- const removedMatch = pageDiff.ariaChanges.match(/removed: (\d+) interactive/);
650
- const removedCount = removedMatch ? Number.parseInt(removedMatch[1]) : 0;
651
- if (addedCount + removedCount >= 50) {
545
+ if (diff.ariaChangeCount >= LARGE_ARIA_CHANGE_THRESHOLD) {
652
546
  pageDiff.iframes = this.iframeSnapshots.map((snap) => `iframe src="${snap.src}":\n${snap.html}`).join('\n\n');
653
547
  }
654
548
  }
@@ -686,20 +580,25 @@ function collapseHtmlParts(parts: HtmlDiffPart[]): HtmlDiffPart[] {
686
580
  export class Diff {
687
581
  private _htmlDiffResult: HtmlDiffResult | null = null;
688
582
  private _ariaDiffResult: string | null = null;
583
+ private _ariaChangeCount = 0;
689
584
  private _isSameUrl: boolean;
690
- private _urlChanged: boolean;
691
585
 
692
586
  constructor(
693
587
  private current: ActionResult,
694
588
  private previous: ActionResult | null
695
589
  ) {
696
590
  this._isSameUrl = previous ? current.isSameUrl({ url: previous.url }) : false;
697
- this._urlChanged = !this._isSameUrl;
591
+ }
592
+
593
+ static async create(current: ActionResult, previous: ActionResult | null): Promise<Diff> {
594
+ const diff = new Diff(current, previous);
595
+ await diff.calculate();
596
+ return diff;
698
597
  }
699
598
 
700
599
  hasChanges(): boolean {
701
600
  if (!this.previous) return false;
702
- if (this._urlChanged) return true;
601
+ if (!this._isSameUrl) return true;
703
602
 
704
603
  const hasHtmlChanges = this._htmlDiffResult && (this._htmlDiffResult.parts.length > 0 || this._htmlDiffResult.added.length > 0 || this._htmlDiffResult.removed.length > 0);
705
604
  const hasAriaChanges = this._ariaDiffResult !== null;
@@ -712,7 +611,7 @@ export class Diff {
712
611
  }
713
612
 
714
613
  urlHasChanged(): boolean {
715
- return this._urlChanged;
614
+ return !this._isSameUrl;
716
615
  }
717
616
 
718
617
  get htmlParts(): HtmlDiffPart[] {
@@ -724,18 +623,14 @@ export class Diff {
724
623
  return this._ariaDiffResult;
725
624
  }
726
625
 
727
- get ariaRemoved(): string | null {
728
- return this._ariaDiffResult;
626
+ get ariaChangeCount(): number {
627
+ return this._ariaChangeCount;
729
628
  }
730
629
 
731
630
  get htmlDiff(): HtmlDiffResult | null {
732
631
  return this._htmlDiffResult;
733
632
  }
734
633
 
735
- get ariaDiff(): string | null {
736
- return this._ariaDiffResult;
737
- }
738
-
739
634
  async calculate(): Promise<void> {
740
635
  if (!this.previous) return;
741
636
 
@@ -743,6 +638,8 @@ export class Diff {
743
638
  this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
744
639
  }
745
640
 
746
- this._ariaDiffResult = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
641
+ const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
642
+ this._ariaDiffResult = ariaDiff.text;
643
+ this._ariaChangeCount = ariaDiff.count;
747
644
  }
748
645
  }