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,15 +1,19 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
- import { dirname, join, resolve } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import dedent from 'dedent';
3
4
  import matter from 'gray-matter';
4
5
  import { ActionResult } from './action-result.js';
5
6
  import { ConfigParser } from './config.js';
6
7
  import { getCliName } from './utils/cli-name.ts';
7
- import { createDebug } from './utils/logger.js';
8
+ import { createDebug, pluralize, tag } from './utils/logger.js';
9
+ import { loadMarkdownFiles } from './utils/markdown-files.js';
10
+ import { mdq } from './utils/markdown-query.js';
8
11
  import { isSecretName, registerSecret } from './utils/secrets.js';
12
+ import { slugify } from './utils/strings.js';
9
13
 
10
14
  const debugLog = createDebug('explorbot:knowledge-tracker');
11
15
 
12
- interface Knowledge {
16
+ export interface Knowledge {
13
17
  filePath: string;
14
18
  url: string;
15
19
  content: string;
@@ -24,14 +28,7 @@ export class KnowledgeTracker {
24
28
  constructor() {
25
29
  const configParser = ConfigParser.getInstance();
26
30
  const config = configParser.getConfig();
27
- const configPath = configParser.getConfigPath();
28
-
29
- if (configPath) {
30
- const projectRoot = dirname(configPath);
31
- this.knowledgeDir = join(projectRoot, config.dirs?.knowledge || 'knowledge');
32
- } else {
33
- this.knowledgeDir = config.dirs?.knowledge || 'knowledge';
34
- }
31
+ this.knowledgeDir = configParser.resolveProjectDir(config.dirs?.knowledge || 'knowledge');
35
32
 
36
33
  if (!existsSync(this.knowledgeDir)) {
37
34
  mkdirSync(this.knowledgeDir, { recursive: true });
@@ -43,29 +40,13 @@ export class KnowledgeTracker {
43
40
 
44
41
  this.knowledgeFiles = [];
45
42
 
46
- if (!existsSync(this.knowledgeDir)) {
47
- return;
48
- }
49
-
50
- const files = readdirSync(this.knowledgeDir)
51
- .filter((file) => file.endsWith('.md'))
52
- .map((file) => join(this.knowledgeDir, file));
53
-
54
- for (const filePath of files) {
55
- try {
56
- const fileContent = readFileSync(filePath, 'utf8');
57
- const parsed = matter(fileContent);
58
- const urlPattern = parsed.data.url || parsed.data.path || '*';
59
-
60
- this.knowledgeFiles.push({
61
- filePath,
62
- url: urlPattern,
63
- content: this.interpolateVars(parsed.content),
64
- ...parsed.data,
65
- });
66
- } catch (error) {
67
- // Skip invalid files
68
- }
43
+ for (const entry of loadMarkdownFiles(this.knowledgeDir, { recursive: true })) {
44
+ this.knowledgeFiles.push({
45
+ filePath: entry.filePath,
46
+ url: entry.data.url || entry.data.path || '*',
47
+ content: this.interpolateVars(entry.content),
48
+ ...entry.data,
49
+ });
69
50
  }
70
51
 
71
52
  this.isLoaded = true;
@@ -79,25 +60,40 @@ export class KnowledgeTracker {
79
60
  });
80
61
  }
81
62
 
63
+ renderRelevantKnowledge(state: ActionResult): string {
64
+ const knowledgeFiles = this.getRelevantKnowledge(state);
65
+ if (knowledgeFiles.length === 0) return '';
66
+
67
+ const knowledgeContent = knowledgeFiles
68
+ .map((k) => k.content)
69
+ .filter((k) => !!k)
70
+ .join('\n\n');
71
+
72
+ tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
73
+ return dedent`
74
+ <knowledge>
75
+ Here is relevant knowledge for this page:
76
+
77
+ ${knowledgeContent}
78
+ </knowledge>
79
+ `;
80
+ }
81
+
82
82
  addKnowledge(urlPattern: string, description: string): { filename: string; filePath: string; isNewFile: boolean } {
83
83
  const configParser = ConfigParser.getInstance();
84
- const config = configParser.getConfig();
85
84
  const configPath = configParser.getConfigPath();
86
85
 
87
86
  if (!configPath) {
88
87
  throw new Error(`No explorbot configuration found. Please run "${getCliName()} init" first.`);
89
88
  }
90
89
 
91
- const projectRoot = dirname(configPath);
92
- const knowledgeDir = join(projectRoot, config.dirs?.knowledge || 'knowledge');
93
-
94
- if (!existsSync(knowledgeDir)) {
95
- mkdirSync(knowledgeDir, { recursive: true });
90
+ if (!existsSync(this.knowledgeDir)) {
91
+ mkdirSync(this.knowledgeDir, { recursive: true });
96
92
  }
97
93
 
98
94
  const normalizedUrl = this.normalizeUrl(urlPattern);
99
95
  const filename = this.generateFilename(normalizedUrl);
100
- const filePath = join(knowledgeDir, filename);
96
+ const filePath = join(this.knowledgeDir, filename);
101
97
 
102
98
  const isNewFile = !existsSync(filePath);
103
99
 
@@ -128,6 +124,8 @@ export class KnowledgeTracker {
128
124
  writeFileSync(filePath, fileContent, 'utf8');
129
125
  }
130
126
 
127
+ this.isLoaded = false;
128
+
131
129
  return { filename, filePath, isNewFile };
132
130
  }
133
131
 
@@ -158,12 +156,7 @@ export class KnowledgeTracker {
158
156
  }
159
157
 
160
158
  private generateFilename(url: string): string {
161
- let filename = url
162
- .replace(/https?:\/\//g, '')
163
- .replace(/[^a-zA-Z0-9_]/g, '_')
164
- .replace(/_+/g, '_')
165
- .replace(/^_|_$/g, '')
166
- .toLowerCase();
159
+ let filename = slugify(url.replace(/https?:\/\//g, ''));
167
160
 
168
161
  if (!filename || filename === '*') {
169
162
  filename = 'general';
@@ -194,7 +187,7 @@ export class KnowledgeTracker {
194
187
 
195
188
  return this.knowledgeFiles.map((knowledge) => {
196
189
  const content = knowledge.content.trim();
197
- const firstLine = content.split('\n')[0]?.trim() || '';
190
+ const firstLine = mdq(content).meta()[0]?.text.split('\n')[0]?.trim() || '';
198
191
  return {
199
192
  url: knowledge.url,
200
193
  firstLine,
@@ -1,11 +1,9 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import matter from 'gray-matter';
4
1
  import { ActionResult } from './action-result.js';
5
- import { ConfigParser, outputPath } from './config.js';
6
- import { ExperienceTracker } from './experience-tracker.js';
2
+ import type { ExperienceTracker } from './experience-tracker.js';
3
+ import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
7
4
  import { detectFocusArea } from './utils/aria.js';
8
- import { createDebug, tag } from './utils/logger.js';
5
+ import { createDebug } from './utils/logger.js';
6
+ import { slugify } from './utils/strings.js';
9
7
  import { extractStatePath } from './utils/url-matcher.js';
10
8
 
11
9
  const debugLog = createDebug('explorbot:state');
@@ -69,43 +67,36 @@ export interface StateTransition {
69
67
 
70
68
  export type StateChangeListener = (event: StateTransition) => void;
71
69
 
72
- export interface Knowledge extends WebPageState {
73
- /** File path */
74
- filePath: string;
75
- /** Markdown content */
76
- content: string;
70
+ export interface TabInfo {
71
+ url: string;
72
+ title: string;
77
73
  }
78
74
 
75
+ export type { Knowledge };
76
+
79
77
  export class StateManager {
78
+ otherTabs: TabInfo[] = [];
80
79
  private currentState: WebPageState | null = null;
81
80
  private stateHistory: StateTransition[] = [];
82
81
  private allVisitedUrls: Set<string> = new Set();
83
- private knowledgeCache: Knowledge[] = [];
84
- private lastKnowledgeScan: Date | null = null;
85
82
  private stateChangeListeners: StateChangeListener[] = [];
86
83
  private experienceTracker!: ExperienceTracker;
87
- private knowledgeDir: string;
84
+ private knowledgeTracker: KnowledgeTracker;
88
85
  private nextStateId = 1;
89
86
 
90
- constructor() {
91
- this.experienceTracker = new ExperienceTracker();
92
- const configParser = ConfigParser.getInstance();
93
- const config = configParser.getConfig();
94
- const configPath = configParser.getConfigPath();
95
-
96
- // Resolve knowledge directory relative to the config file location (project root)
97
- if (configPath) {
98
- const projectRoot = dirname(configPath);
99
- this.knowledgeDir = join(projectRoot, config.dirs?.knowledge || 'knowledge');
100
- } else {
101
- this.knowledgeDir = config.dirs?.knowledge || 'knowledge';
102
- }
87
+ constructor(experienceTracker: ExperienceTracker, knowledgeTracker: KnowledgeTracker) {
88
+ this.experienceTracker = experienceTracker;
89
+ this.knowledgeTracker = knowledgeTracker;
103
90
  }
104
91
 
105
92
  getExperienceTracker(): ExperienceTracker {
106
93
  return this.experienceTracker;
107
94
  }
108
95
 
96
+ getKnowledgeTracker(): KnowledgeTracker {
97
+ return this.knowledgeTracker;
98
+ }
99
+
109
100
  /**
110
101
  * Subscribe to state change events
111
102
  */
@@ -134,11 +125,6 @@ export class StateManager {
134
125
  });
135
126
  }
136
127
 
137
- /**
138
- * Extract state path from full URL
139
- * Removes domain, port, protocol
140
- * Keeps path, query, and hash: /path/to/page?tab=users#section
141
- */
142
128
  /**
143
129
  * Update current state from ActionResult and record transition if state changed
144
130
  */
@@ -230,13 +216,7 @@ export class StateManager {
230
216
  parts.push(`title_${title}`);
231
217
  }
232
218
 
233
- return parts
234
- .join('_')
235
- .replace(/[^a-zA-Z0-9_]/g, '_')
236
- .replace(/_+/g, '_')
237
- .replace(/^_|_$/g, '')
238
- .toLowerCase()
239
- .substring(0, 200);
219
+ return slugify(parts.join('_')).substring(0, 200);
240
220
  }
241
221
 
242
222
  /**
@@ -327,65 +307,14 @@ export class StateManager {
327
307
  return this.stateHistory.slice(-count);
328
308
  }
329
309
 
330
- /**
331
- * Scan knowledge directory for .md files and cache them
332
- */
333
- private scanKnowledgeFiles(): void {
334
- const now = new Date();
335
-
336
- // Only rescan every 30 seconds to avoid excessive file I/O
337
- if (this.lastKnowledgeScan && now.getTime() - this.lastKnowledgeScan.getTime() < 30000) {
338
- return;
339
- }
340
-
341
- this.knowledgeCache = [];
342
-
343
- if (!existsSync(this.knowledgeDir)) {
344
- debugLog(`Knowledge directory not found: ${this.knowledgeDir}`);
345
- return;
346
- }
347
-
348
- try {
349
- const files = readdirSync(this.knowledgeDir, { recursive: true })
350
- .filter((file) => typeof file === 'string' && file.endsWith('.md'))
351
- .map((file) => join(this.knowledgeDir, file as string));
352
-
353
- for (const filePath of files) {
354
- try {
355
- const fileContent = readFileSync(filePath, 'utf8');
356
- const parsed = matter(fileContent);
357
-
358
- const urlPattern = parsed.data.url || parsed.data.path || '*';
359
-
360
- this.knowledgeCache.push({
361
- filePath,
362
- url: urlPattern,
363
- ...parsed.data,
364
- content: parsed.content,
365
- });
366
-
367
- debugLog(`Loaded knowledge file: ${filePath} (pattern: ${urlPattern})`);
368
- } catch (error) {
369
- debugLog(`Failed to load knowledge file ${filePath}:`, error);
370
- }
371
- }
372
-
373
- this.lastKnowledgeScan = now;
374
- debugLog(`Scanned ${this.knowledgeCache.length} knowledge files`);
375
- } catch (error) {
376
- debugLog('Failed to scan knowledge directory:', error);
377
- }
378
- }
379
310
  /**
380
311
  * Get relevant knowledge files for current state
381
312
  */
382
313
  getRelevantKnowledge(): Knowledge[] {
383
314
  if (!this.currentState) return [];
384
315
 
385
- this.scanKnowledgeFiles();
386
-
387
316
  const actionResult = ActionResult.fromState(this.currentState);
388
- return this.knowledgeCache.filter((knowledge) => actionResult.isMatchedBy(knowledge));
317
+ return this.knowledgeTracker.getRelevantKnowledge(actionResult);
389
318
  }
390
319
 
391
320
  /**
@@ -402,24 +331,6 @@ export class StateManager {
402
331
  /**
403
332
  * Get all context for current state (knowledge + experience)
404
333
  */
405
- getCurrentContext(): {
406
- state: WebPageState;
407
- knowledge: Knowledge[];
408
- experience: string[];
409
- recentTransitions: StateTransition[];
410
- } {
411
- if (!this.currentState) {
412
- throw new Error('No current state available');
413
- }
414
-
415
- return {
416
- state: this.currentState,
417
- knowledge: this.getRelevantKnowledge(),
418
- experience: this.getRelevantExperience(),
419
- recentTransitions: this.getRecentTransitions(),
420
- };
421
- }
422
-
423
334
  /**
424
335
  * Check if we've been in this state before
425
336
  */
@@ -455,45 +366,6 @@ export class StateManager {
455
366
  * If the last transition changed URL, returns the fromState (for URL change detection).
456
367
  * Otherwise returns the most recent toState with content (for diffing).
457
368
  */
458
- getPreviousState(): WebPageState | null {
459
- if (this.stateHistory.length === 0) return null;
460
-
461
- const lastTransition = this.stateHistory[this.stateHistory.length - 1];
462
-
463
- if (lastTransition.fromState?.url !== lastTransition.toState?.url) {
464
- return lastTransition.fromState;
465
- }
466
-
467
- for (let i = this.stateHistory.length - 1; i >= 0; i--) {
468
- const toState = this.stateHistory[i].toState;
469
-
470
- if (!toState) continue;
471
- if (toState.id === this.currentState?.id) continue;
472
-
473
- if (toState.html || toState.ariaSnapshot) {
474
- return toState;
475
- }
476
- }
477
-
478
- return null;
479
- }
480
-
481
- /**
482
- * Load HTML content from file
483
- */
484
- loadHtmlFromFile(htmlFile: string): string | null {
485
- try {
486
- const filePath = outputPath('states', htmlFile);
487
- if (existsSync(filePath)) {
488
- return readFileSync(filePath, 'utf8');
489
- }
490
- return null;
491
- } catch (error) {
492
- debugLog('Failed to load HTML from file:', error);
493
- return null;
494
- }
495
- }
496
-
497
369
  /**
498
370
  * Clear state history (useful for testing or reset)
499
371
  * Note: This preserves the current state, only clears navigation history
@@ -527,15 +399,8 @@ export class StateManager {
527
399
  this.stateHistory = [];
528
400
  this.allVisitedUrls.clear();
529
401
  this.stateChangeListeners = [];
530
- this.knowledgeCache = [];
531
- this.lastKnowledgeScan = null;
532
402
  this.nextStateId = 1;
533
403
 
534
- // Clean up experience tracker if it has cleanup method
535
- if (this.experienceTracker && typeof this.experienceTracker.cleanup === 'function') {
536
- this.experienceTracker.cleanup();
537
- }
538
-
539
404
  debugLog('StateManager cleanup completed');
540
405
  }
541
406
  }
package/src/utils/aria.ts CHANGED
@@ -483,7 +483,7 @@ export const compactAriaSnapshot = (snapshot: string | null, keepNamed = false):
483
483
  return renderTree(tree);
484
484
  };
485
485
 
486
- export const diffAriaSnapshots = (previous: string | null, current: string | null): string | null => {
486
+ export const diffAriaSnapshots = (previous: string | null, current: string | null): AriaDiff => {
487
487
  const flat = (snap: string | null): FlatEntry[] => {
488
488
  let tree = parseSnapshot(snap);
489
489
  tree = unwrapIgnored(tree);
@@ -500,7 +500,9 @@ export const diffAriaSnapshots = (previous: string | null, current: string | nul
500
500
  const currTotals = countBy(curr.map((e) => e.summary));
501
501
  const byCount = diffByCount(prevTotals, currTotals);
502
502
  const renames = detectRenames(prev, curr, prevTotals, currTotals);
503
- return formatDiff([...byCount.added, ...renames.added], [...byCount.removed, ...renames.removed], toggled);
503
+ const added = [...byCount.added, ...renames.added];
504
+ const removed = [...byCount.removed, ...renames.removed];
505
+ return { text: formatDiff(added, removed, toggled), count: added.length + removed.length + toggled.length };
504
506
  };
505
507
 
506
508
  export const detectFocusArea = (snapshot: string | null): FocusAreaResult => {
@@ -570,10 +572,17 @@ export function parseAriaLocator(ariaStr: string): { role: string; text: string
570
572
  return { role: match[1], text: match[2] };
571
573
  }
572
574
 
575
+ export const LARGE_ARIA_CHANGE_THRESHOLD = 50;
576
+
573
577
  // ─────────────────────────────────────────────────────────────────
574
578
  // Types
575
579
  // ─────────────────────────────────────────────────────────────────
576
580
 
581
+ export interface AriaDiff {
582
+ text: string | null;
583
+ count: number;
584
+ }
585
+
577
586
  type AriaNode = {
578
587
  role: string;
579
588
  name?: string;
@@ -0,0 +1,40 @@
1
+ export class TTLCache<T> {
2
+ private store = new Map<string, CacheEntry<T>>();
3
+ private ttlMs: number;
4
+
5
+ constructor(ttlMs = Number.POSITIVE_INFINITY) {
6
+ this.ttlMs = ttlMs;
7
+ }
8
+
9
+ get(key: string): T | undefined {
10
+ const entry = this.store.get(key);
11
+ if (!entry) return undefined;
12
+ if (Date.now() - entry.at > this.ttlMs) {
13
+ this.store.delete(key);
14
+ return undefined;
15
+ }
16
+ return entry.value;
17
+ }
18
+
19
+ set(key: string, value: T): void {
20
+ this.store.set(key, { value, at: Date.now() });
21
+ }
22
+
23
+ async fetch(key: string, compute: () => Promise<T>): Promise<T> {
24
+ const cached = this.get(key);
25
+ if (cached !== undefined) return cached;
26
+ const value = await compute();
27
+ this.set(key, value);
28
+ return value;
29
+ }
30
+
31
+ delete(key: string): void {
32
+ this.store.delete(key);
33
+ }
34
+
35
+ clear(): void {
36
+ this.store.clear();
37
+ }
38
+ }
39
+
40
+ type CacheEntry<T> = { value: T; at: number };
@@ -29,6 +29,15 @@ function indent(text: string): string {
29
29
  .join('\n');
30
30
  }
31
31
 
32
+ export function formatHeadings(headings: { h1?: string; h2?: string; h3?: string; h4?: string }): string[] {
33
+ const lines: string[] = [];
34
+ if (headings.h1) lines.push(`H1: ${headings.h1}`);
35
+ if (headings.h2) lines.push(`H2: ${headings.h2}`);
36
+ if (headings.h3) lines.push(`H3: ${headings.h3}`);
37
+ if (headings.h4) lines.push(`H4: ${headings.h4}`);
38
+ return lines;
39
+ }
40
+
32
41
  export type ContextMode = 'attached' | 'compact' | 'full';
33
42
 
34
43
  export function formatContextSummary(data: ContextData, mode: ContextMode = 'compact'): string {
@@ -51,11 +60,7 @@ function formatContextAttached(data: ContextData): string {
51
60
  }
52
61
  lines.push('');
53
62
 
54
- const headingLines: string[] = [];
55
- if (data.headings.h1) headingLines.push(`H1: ${data.headings.h1}`);
56
- if (data.headings.h2) headingLines.push(`H2: ${data.headings.h2}`);
57
- if (data.headings.h3) headingLines.push(`H3: ${data.headings.h3}`);
58
- if (data.headings.h4) headingLines.push(`H4: ${data.headings.h4}`);
63
+ const headingLines = formatHeadings(data.headings);
59
64
 
60
65
  if (headingLines.length > 0) {
61
66
  lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
@@ -116,11 +121,7 @@ function formatContextCompact(data: ContextData): string {
116
121
  }
117
122
  lines.push('');
118
123
 
119
- const headingLines: string[] = [];
120
- if (data.headings.h1) headingLines.push(`H1: ${data.headings.h1}`);
121
- if (data.headings.h2) headingLines.push(`H2: ${data.headings.h2}`);
122
- if (data.headings.h3) headingLines.push(`H3: ${data.headings.h3}`);
123
- if (data.headings.h4) headingLines.push(`H4: ${data.headings.h4}`);
124
+ const headingLines = formatHeadings(data.headings);
124
125
 
125
126
  if (headingLines.length > 0) {
126
127
  lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
@@ -177,11 +178,7 @@ function formatContextFull(data: ContextData): string {
177
178
  }
178
179
  lines.push('');
179
180
 
180
- const headingLines: string[] = [];
181
- if (data.headings.h1) headingLines.push(`H1: ${data.headings.h1}`);
182
- if (data.headings.h2) headingLines.push(`H2: ${data.headings.h2}`);
183
- if (data.headings.h3) headingLines.push(`H3: ${data.headings.h3}`);
184
- if (data.headings.h4) headingLines.push(`H4: ${data.headings.h4}`);
181
+ const headingLines = formatHeadings(data.headings);
185
182
 
186
183
  if (headingLines.length > 0) {
187
184
  lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
@@ -54,11 +54,9 @@ export class HooksRunner {
54
54
  private async executeHook(hook: Hook, url: string): Promise<void> {
55
55
  try {
56
56
  if (hook.type === 'playwright') {
57
- const page = this.explorer.playwrightHelper.page;
58
- await hook.hook({ page, url });
57
+ await this.explorer.withPage(async (page) => hook.hook({ page, url }));
59
58
  } else {
60
- const I = this.explorer.actor;
61
- await hook.hook({ I, url });
59
+ await hook.hook({ I: this.explorer.actor, url });
62
60
  }
63
61
  } catch (error) {
64
62
  debugLog(`Hook error: ${error}`);
package/src/utils/html.ts CHANGED
@@ -85,8 +85,10 @@ const HIDDEN_CLASSES = new Set(['hidden', 'invisible', 'd-none', 'hide', 'dn', '
85
85
 
86
86
  export const EXPLORBOT_ATTRS = {
87
87
  area: 'data-explorbot-area',
88
+ coveredBy: 'data-explorbot-covered-by',
88
89
  context: 'data-explorbot-context',
89
90
  eidx: 'data-explorbot-eidx',
91
+ hit: 'data-explorbot-hit',
90
92
  variant: 'data-explorbot-variant',
91
93
  } as const;
92
94
 
@@ -169,14 +171,49 @@ export type ComponentScopeExtractionConfig = {
169
171
  limits: typeof HTML_EXTRACTION_LIMITS;
170
172
  };
171
173
 
174
+ export function captureHtmlForSnapshot(): string {
175
+ const clone = document.documentElement.cloneNode(true) as HTMLElement;
176
+ const liveControls = Array.from(document.querySelectorAll('input, textarea, select')) as Array<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>;
177
+ const clonedControls = Array.from(clone.querySelectorAll('input, textarea, select')) as HTMLElement[];
178
+
179
+ for (let i = 0; i < liveControls.length; i++) {
180
+ const source = liveControls[i];
181
+ const target = clonedControls[i];
182
+ if (!target) continue;
183
+
184
+ if (source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement) {
185
+ target.setAttribute('data-explorbot-value', source.value);
186
+ }
187
+
188
+ if (source instanceof HTMLInputElement && (source.type === 'checkbox' || source.type === 'radio')) {
189
+ let checked = 'false';
190
+ if (source.checked) checked = 'true';
191
+ target.setAttribute('data-explorbot-checked', checked);
192
+ }
193
+
194
+ if (source instanceof HTMLSelectElement) {
195
+ target.setAttribute(
196
+ 'data-explorbot-value',
197
+ Array.from(source.selectedOptions)
198
+ .map((option) => option.value)
199
+ .join(',')
200
+ );
201
+ }
202
+ }
203
+
204
+ return clone.outerHTML;
205
+ }
206
+
172
207
  export function extractElementData(el: Element, config?: ElementExtractionConfig) {
173
208
  const cfg =
174
209
  config ||
175
210
  ({
176
211
  attrs: {
177
212
  area: 'data-explorbot-area',
213
+ coveredBy: 'data-explorbot-covered-by',
178
214
  context: 'data-explorbot-context',
179
215
  eidx: 'data-explorbot-eidx',
216
+ hit: 'data-explorbot-hit',
180
217
  variant: 'data-explorbot-variant',
181
218
  },
182
219
  codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'],
@@ -246,6 +283,45 @@ export function extractElementData(el: Element, config?: ElementExtractionConfig
246
283
  return Array.from(tokens).slice(0, 8);
247
284
  }
248
285
 
286
+ function describeOverlappingElement(target: Element): string {
287
+ const parts = [target.tagName.toLowerCase()];
288
+ const role = target.getAttribute('role');
289
+ const ariaLabel = target.getAttribute('aria-label');
290
+ const id = target.getAttribute('id');
291
+ const className = (target.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 3).join('.');
292
+
293
+ if (id) parts.push(`#${id.slice(0, 40)}`);
294
+ if (className) parts.push(`.${className.slice(0, 80)}`);
295
+ if (role) parts.push(`[role="${role.slice(0, 40)}"]`);
296
+ if (ariaLabel) parts.push(`[aria-label="${ariaLabel.slice(0, 80)}"]`);
297
+
298
+ return parts.join('');
299
+ }
300
+
301
+ function inspectHitTarget(target: Element, rect: DOMRect): { hit: string; coveredBy?: string } {
302
+ const viewportPoints = [
303
+ { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
304
+ { x: rect.left + Math.min(rect.width - 1, Math.max(1, rect.width * 0.25)), y: rect.top + Math.min(rect.height - 1, Math.max(1, rect.height * 0.25)) },
305
+ { x: rect.left + Math.min(rect.width - 1, Math.max(1, rect.width * 0.75)), y: rect.top + Math.min(rect.height - 1, Math.max(1, rect.height * 0.75)) },
306
+ ].filter((point) => point.x >= 0 && point.y >= 0 && point.x <= window.innerWidth && point.y <= window.innerHeight);
307
+
308
+ if (viewportPoints.length === 0) {
309
+ return { hit: 'offscreen' };
310
+ }
311
+
312
+ for (const point of viewportPoints) {
313
+ const top = document.elementFromPoint(point.x, point.y);
314
+ if (!top) continue;
315
+ if (top === target || target.contains(top)) {
316
+ return { hit: 'target' };
317
+ }
318
+ }
319
+
320
+ const centerPoint = viewportPoints[0];
321
+ const top = document.elementFromPoint(centerPoint.x, centerPoint.y);
322
+ return { hit: 'covered', coveredBy: top ? describeOverlappingElement(top) : undefined };
323
+ }
324
+
249
325
  function isEmbeddedCodeEditorFrame(target: Element): boolean {
250
326
  const src = (target.getAttribute('src') || '').toLowerCase();
251
327
  const markerSelector = cfg.codeEditorMarkers.map((marker) => `[class*="${marker}"]`).join(', ');
@@ -354,6 +430,9 @@ export function extractElementData(el: Element, config?: ElementExtractionConfig
354
430
  allAttrs[cfg.attrs.area] = areaHints.join('|');
355
431
  allAttrs[cfg.attrs.context] = findContextLabel(el);
356
432
  allAttrs[cfg.attrs.variant] = collectVariantHints(el).join('|');
433
+ const hitTarget = inspectHitTarget(el, rect);
434
+ allAttrs[cfg.attrs.hit] = hitTarget.hit;
435
+ if (hitTarget.coveredBy) allAttrs[cfg.attrs.coveredBy] = hitTarget.coveredBy;
357
436
 
358
437
  return {
359
438
  tag: el.tagName.toLowerCase(),