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,5 +1,5 @@
1
1
  import { ActionResult } from './action-result.js';
2
- interface Knowledge {
2
+ export interface Knowledge {
3
3
  filePath: string;
4
4
  url: string;
5
5
  content: string;
@@ -12,6 +12,7 @@ export declare class KnowledgeTracker {
12
12
  constructor();
13
13
  loadKnowledgeFiles(): void;
14
14
  getRelevantKnowledge(state: ActionResult): Knowledge[];
15
+ renderRelevantKnowledge(state: ActionResult): string;
15
16
  addKnowledge(urlPattern: string, description: string): {
16
17
  filename: string;
17
18
  filePath: string;
@@ -30,4 +31,3 @@ export declare class KnowledgeTracker {
30
31
  normalizeUrl(url: string): string;
31
32
  getStateParameters(state: ActionResult, keys: string[]): Record<string, any>;
32
33
  }
33
- export {};
@@ -1,11 +1,15 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } 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.js";
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
  const debugLog = createDebug('explorbot:knowledge-tracker');
10
14
  export class KnowledgeTracker {
11
15
  knowledgeDir;
@@ -14,14 +18,7 @@ export class KnowledgeTracker {
14
18
  constructor() {
15
19
  const configParser = ConfigParser.getInstance();
16
20
  const config = configParser.getConfig();
17
- const configPath = configParser.getConfigPath();
18
- if (configPath) {
19
- const projectRoot = dirname(configPath);
20
- this.knowledgeDir = join(projectRoot, config.dirs?.knowledge || 'knowledge');
21
- }
22
- else {
23
- this.knowledgeDir = config.dirs?.knowledge || 'knowledge';
24
- }
21
+ this.knowledgeDir = configParser.resolveProjectDir(config.dirs?.knowledge || 'knowledge');
25
22
  if (!existsSync(this.knowledgeDir)) {
26
23
  mkdirSync(this.knowledgeDir, { recursive: true });
27
24
  }
@@ -30,27 +27,13 @@ export class KnowledgeTracker {
30
27
  if (this.isLoaded)
31
28
  return;
32
29
  this.knowledgeFiles = [];
33
- if (!existsSync(this.knowledgeDir)) {
34
- return;
35
- }
36
- const files = readdirSync(this.knowledgeDir)
37
- .filter((file) => file.endsWith('.md'))
38
- .map((file) => join(this.knowledgeDir, file));
39
- for (const filePath of files) {
40
- try {
41
- const fileContent = readFileSync(filePath, 'utf8');
42
- const parsed = matter(fileContent);
43
- const urlPattern = parsed.data.url || parsed.data.path || '*';
44
- this.knowledgeFiles.push({
45
- filePath,
46
- url: urlPattern,
47
- content: this.interpolateVars(parsed.content),
48
- ...parsed.data,
49
- });
50
- }
51
- catch (error) {
52
- // Skip invalid files
53
- }
30
+ for (const entry of loadMarkdownFiles(this.knowledgeDir, { recursive: true })) {
31
+ this.knowledgeFiles.push({
32
+ filePath: entry.filePath,
33
+ url: entry.data.url || entry.data.path || '*',
34
+ content: this.interpolateVars(entry.content),
35
+ ...entry.data,
36
+ });
54
37
  }
55
38
  this.isLoaded = true;
56
39
  }
@@ -60,21 +43,35 @@ export class KnowledgeTracker {
60
43
  return state.isMatchedBy(knowledge);
61
44
  });
62
45
  }
46
+ renderRelevantKnowledge(state) {
47
+ const knowledgeFiles = this.getRelevantKnowledge(state);
48
+ if (knowledgeFiles.length === 0)
49
+ return '';
50
+ const knowledgeContent = knowledgeFiles
51
+ .map((k) => k.content)
52
+ .filter((k) => !!k)
53
+ .join('\n\n');
54
+ tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
55
+ return dedent `
56
+ <knowledge>
57
+ Here is relevant knowledge for this page:
58
+
59
+ ${knowledgeContent}
60
+ </knowledge>
61
+ `;
62
+ }
63
63
  addKnowledge(urlPattern, description) {
64
64
  const configParser = ConfigParser.getInstance();
65
- const config = configParser.getConfig();
66
65
  const configPath = configParser.getConfigPath();
67
66
  if (!configPath) {
68
67
  throw new Error(`No explorbot configuration found. Please run "${getCliName()} init" first.`);
69
68
  }
70
- const projectRoot = dirname(configPath);
71
- const knowledgeDir = join(projectRoot, config.dirs?.knowledge || 'knowledge');
72
- if (!existsSync(knowledgeDir)) {
73
- mkdirSync(knowledgeDir, { recursive: true });
69
+ if (!existsSync(this.knowledgeDir)) {
70
+ mkdirSync(this.knowledgeDir, { recursive: true });
74
71
  }
75
72
  const normalizedUrl = this.normalizeUrl(urlPattern);
76
73
  const filename = this.generateFilename(normalizedUrl);
77
- const filePath = join(knowledgeDir, filename);
74
+ const filePath = join(this.knowledgeDir, filename);
78
75
  const isNewFile = !existsSync(filePath);
79
76
  if (isNewFile) {
80
77
  const frontmatter = {
@@ -101,6 +98,7 @@ export class KnowledgeTracker {
101
98
  const fileContent = matter.stringify(newContent, frontmatter);
102
99
  writeFileSync(filePath, fileContent, 'utf8');
103
100
  }
101
+ this.isLoaded = false;
104
102
  return { filename, filePath, isNewFile };
105
103
  }
106
104
  interpolateVars(content) {
@@ -129,12 +127,7 @@ export class KnowledgeTracker {
129
127
  });
130
128
  }
131
129
  generateFilename(url) {
132
- let filename = url
133
- .replace(/https?:\/\//g, '')
134
- .replace(/[^a-zA-Z0-9_]/g, '_')
135
- .replace(/_+/g, '_')
136
- .replace(/^_|_$/g, '')
137
- .toLowerCase();
130
+ let filename = slugify(url.replace(/https?:\/\//g, ''));
138
131
  if (!filename || filename === '*') {
139
132
  filename = 'general';
140
133
  }
@@ -156,7 +149,7 @@ export class KnowledgeTracker {
156
149
  this.loadKnowledgeFiles();
157
150
  return this.knowledgeFiles.map((knowledge) => {
158
151
  const content = knowledge.content.trim();
159
- const firstLine = content.split('\n')[0]?.trim() || '';
152
+ const firstLine = mdq(content).meta()[0]?.text.split('\n')[0]?.trim() || '';
160
153
  return {
161
154
  url: knowledge.url,
162
155
  firstLine,
@@ -1,5 +1,6 @@
1
1
  import { ActionResult } from './action-result.js';
2
- import { ExperienceTracker } from './experience-tracker.js';
2
+ import type { ExperienceTracker } from './experience-tracker.js';
3
+ import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
3
4
  export interface Link {
4
5
  title: string;
5
6
  url: string;
@@ -54,24 +55,23 @@ export interface StateTransition {
54
55
  trigger: 'manual' | 'navigation' | 'automatic';
55
56
  }
56
57
  export type StateChangeListener = (event: StateTransition) => void;
57
- export interface Knowledge extends WebPageState {
58
- /** File path */
59
- filePath: string;
60
- /** Markdown content */
61
- content: string;
58
+ export interface TabInfo {
59
+ url: string;
60
+ title: string;
62
61
  }
62
+ export type { Knowledge };
63
63
  export declare class StateManager {
64
+ otherTabs: TabInfo[];
64
65
  currentState: WebPageState | null;
65
66
  stateHistory: StateTransition[];
66
67
  allVisitedUrls: Set<string>;
67
- knowledgeCache: Knowledge[];
68
- lastKnowledgeScan: Date | null;
69
68
  stateChangeListeners: StateChangeListener[];
70
69
  experienceTracker: ExperienceTracker;
71
- knowledgeDir: string;
70
+ knowledgeTracker: KnowledgeTracker;
72
71
  nextStateId: number;
73
- constructor();
72
+ constructor(experienceTracker: ExperienceTracker, knowledgeTracker: KnowledgeTracker);
74
73
  getExperienceTracker(): ExperienceTracker;
74
+ getKnowledgeTracker(): KnowledgeTracker;
75
75
  /**
76
76
  * Subscribe to state change events
77
77
  */
@@ -80,11 +80,6 @@ export declare class StateManager {
80
80
  * Emit state change event to all listeners
81
81
  */
82
82
  emitStateChange(event: StateTransition): void;
83
- /**
84
- * Extract state path from full URL
85
- * Removes domain, port, protocol
86
- * Keeps path, query, and hash: /path/to/page?tab=users#section
87
- */
88
83
  /**
89
84
  * Update current state from ActionResult and record transition if state changed
90
85
  */
@@ -119,10 +114,6 @@ export declare class StateManager {
119
114
  * Get the last N transitions
120
115
  */
121
116
  getRecentTransitions(count?: number): StateTransition[];
122
- /**
123
- * Scan knowledge directory for .md files and cache them
124
- */
125
- scanKnowledgeFiles(): void;
126
117
  /**
127
118
  * Get relevant knowledge files for current state
128
119
  */
@@ -134,12 +125,6 @@ export declare class StateManager {
134
125
  /**
135
126
  * Get all context for current state (knowledge + experience)
136
127
  */
137
- getCurrentContext(): {
138
- state: WebPageState;
139
- knowledge: Knowledge[];
140
- experience: string[];
141
- recentTransitions: StateTransition[];
142
- };
143
128
  /**
144
129
  * Check if we've been in this state before
145
130
  */
@@ -158,11 +143,6 @@ export declare class StateManager {
158
143
  * If the last transition changed URL, returns the fromState (for URL change detection).
159
144
  * Otherwise returns the most recent toState with content (for diffing).
160
145
  */
161
- getPreviousState(): WebPageState | null;
162
- /**
163
- * Load HTML content from file
164
- */
165
- loadHtmlFromFile(htmlFile: string): string | null;
166
146
  /**
167
147
  * Clear state history (useful for testing or reset)
168
148
  * Note: This preserves the current state, only clears navigation history
@@ -1,40 +1,28 @@
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';
7
2
  import { detectFocusArea } from './utils/aria.js';
8
3
  import { createDebug } from './utils/logger.js';
4
+ import { slugify } from './utils/strings.js';
9
5
  import { extractStatePath } from './utils/url-matcher.js';
10
6
  const debugLog = createDebug('explorbot:state');
11
7
  export class StateManager {
8
+ otherTabs = [];
12
9
  currentState = null;
13
10
  stateHistory = [];
14
11
  allVisitedUrls = new Set();
15
- knowledgeCache = [];
16
- lastKnowledgeScan = null;
17
12
  stateChangeListeners = [];
18
13
  experienceTracker;
19
- knowledgeDir;
14
+ knowledgeTracker;
20
15
  nextStateId = 1;
21
- constructor() {
22
- this.experienceTracker = new ExperienceTracker();
23
- const configParser = ConfigParser.getInstance();
24
- const config = configParser.getConfig();
25
- const configPath = configParser.getConfigPath();
26
- // Resolve knowledge directory relative to the config file location (project root)
27
- if (configPath) {
28
- const projectRoot = dirname(configPath);
29
- this.knowledgeDir = join(projectRoot, config.dirs?.knowledge || 'knowledge');
30
- }
31
- else {
32
- this.knowledgeDir = config.dirs?.knowledge || 'knowledge';
33
- }
16
+ constructor(experienceTracker, knowledgeTracker) {
17
+ this.experienceTracker = experienceTracker;
18
+ this.knowledgeTracker = knowledgeTracker;
34
19
  }
35
20
  getExperienceTracker() {
36
21
  return this.experienceTracker;
37
22
  }
23
+ getKnowledgeTracker() {
24
+ return this.knowledgeTracker;
25
+ }
38
26
  /**
39
27
  * Subscribe to state change events
40
28
  */
@@ -61,11 +49,6 @@ export class StateManager {
61
49
  }
62
50
  });
63
51
  }
64
- /**
65
- * Extract state path from full URL
66
- * Removes domain, port, protocol
67
- * Keeps path, query, and hash: /path/to/page?tab=users#section
68
- */
69
52
  /**
70
53
  * Update current state from ActionResult and record transition if state changed
71
54
  */
@@ -141,13 +124,7 @@ export class StateManager {
141
124
  if (title) {
142
125
  parts.push(`title_${title}`);
143
126
  }
144
- return parts
145
- .join('_')
146
- .replace(/[^a-zA-Z0-9_]/g, '_')
147
- .replace(/_+/g, '_')
148
- .replace(/^_|_$/g, '')
149
- .toLowerCase()
150
- .substring(0, 200);
127
+ return slugify(parts.join('_')).substring(0, 200);
151
128
  }
152
129
  /**
153
130
  * Get current state
@@ -226,57 +203,14 @@ export class StateManager {
226
203
  getRecentTransitions(count = 5) {
227
204
  return this.stateHistory.slice(-count);
228
205
  }
229
- /**
230
- * Scan knowledge directory for .md files and cache them
231
- */
232
- scanKnowledgeFiles() {
233
- const now = new Date();
234
- // Only rescan every 30 seconds to avoid excessive file I/O
235
- if (this.lastKnowledgeScan && now.getTime() - this.lastKnowledgeScan.getTime() < 30000) {
236
- return;
237
- }
238
- this.knowledgeCache = [];
239
- if (!existsSync(this.knowledgeDir)) {
240
- debugLog(`Knowledge directory not found: ${this.knowledgeDir}`);
241
- return;
242
- }
243
- try {
244
- const files = readdirSync(this.knowledgeDir, { recursive: true })
245
- .filter((file) => typeof file === 'string' && file.endsWith('.md'))
246
- .map((file) => join(this.knowledgeDir, file));
247
- for (const filePath of files) {
248
- try {
249
- const fileContent = readFileSync(filePath, 'utf8');
250
- const parsed = matter(fileContent);
251
- const urlPattern = parsed.data.url || parsed.data.path || '*';
252
- this.knowledgeCache.push({
253
- filePath,
254
- url: urlPattern,
255
- ...parsed.data,
256
- content: parsed.content,
257
- });
258
- debugLog(`Loaded knowledge file: ${filePath} (pattern: ${urlPattern})`);
259
- }
260
- catch (error) {
261
- debugLog(`Failed to load knowledge file ${filePath}:`, error);
262
- }
263
- }
264
- this.lastKnowledgeScan = now;
265
- debugLog(`Scanned ${this.knowledgeCache.length} knowledge files`);
266
- }
267
- catch (error) {
268
- debugLog('Failed to scan knowledge directory:', error);
269
- }
270
- }
271
206
  /**
272
207
  * Get relevant knowledge files for current state
273
208
  */
274
209
  getRelevantKnowledge() {
275
210
  if (!this.currentState)
276
211
  return [];
277
- this.scanKnowledgeFiles();
278
212
  const actionResult = ActionResult.fromState(this.currentState);
279
- return this.knowledgeCache.filter((knowledge) => actionResult.isMatchedBy(knowledge));
213
+ return this.knowledgeTracker.getRelevantKnowledge(actionResult);
280
214
  }
281
215
  /**
282
216
  * Get relevant experience files for current state
@@ -291,17 +225,6 @@ export class StateManager {
291
225
  /**
292
226
  * Get all context for current state (knowledge + experience)
293
227
  */
294
- getCurrentContext() {
295
- if (!this.currentState) {
296
- throw new Error('No current state available');
297
- }
298
- return {
299
- state: this.currentState,
300
- knowledge: this.getRelevantKnowledge(),
301
- experience: this.getRelevantExperience(),
302
- recentTransitions: this.getRecentTransitions(),
303
- };
304
- }
305
228
  /**
306
229
  * Check if we've been in this state before
307
230
  */
@@ -333,41 +256,6 @@ export class StateManager {
333
256
  * If the last transition changed URL, returns the fromState (for URL change detection).
334
257
  * Otherwise returns the most recent toState with content (for diffing).
335
258
  */
336
- getPreviousState() {
337
- if (this.stateHistory.length === 0)
338
- return null;
339
- const lastTransition = this.stateHistory[this.stateHistory.length - 1];
340
- if (lastTransition.fromState?.url !== lastTransition.toState?.url) {
341
- return lastTransition.fromState;
342
- }
343
- for (let i = this.stateHistory.length - 1; i >= 0; i--) {
344
- const toState = this.stateHistory[i].toState;
345
- if (!toState)
346
- continue;
347
- if (toState.id === this.currentState?.id)
348
- continue;
349
- if (toState.html || toState.ariaSnapshot) {
350
- return toState;
351
- }
352
- }
353
- return null;
354
- }
355
- /**
356
- * Load HTML content from file
357
- */
358
- loadHtmlFromFile(htmlFile) {
359
- try {
360
- const filePath = outputPath('states', htmlFile);
361
- if (existsSync(filePath)) {
362
- return readFileSync(filePath, 'utf8');
363
- }
364
- return null;
365
- }
366
- catch (error) {
367
- debugLog('Failed to load HTML from file:', error);
368
- return null;
369
- }
370
- }
371
259
  /**
372
260
  * Clear state history (useful for testing or reset)
373
261
  * Note: This preserves the current state, only clears navigation history
@@ -398,13 +286,7 @@ export class StateManager {
398
286
  this.stateHistory = [];
399
287
  this.allVisitedUrls.clear();
400
288
  this.stateChangeListeners = [];
401
- this.knowledgeCache = [];
402
- this.lastKnowledgeScan = null;
403
289
  this.nextStateId = 1;
404
- // Clean up experience tracker if it has cleanup method
405
- if (this.experienceTracker && typeof this.experienceTracker.cleanup === 'function') {
406
- this.experienceTracker.cleanup();
407
- }
408
290
  debugLog('StateManager cleanup completed');
409
291
  }
410
292
  }
@@ -4,7 +4,7 @@ export interface FocusAreaResult {
4
4
  name: string | null;
5
5
  }
6
6
  export declare const compactAriaSnapshot: (snapshot: string | null, keepNamed?: boolean) => string;
7
- export declare const diffAriaSnapshots: (previous: string | null, current: string | null) => string | null;
7
+ export declare const diffAriaSnapshots: (previous: string | null, current: string | null) => AriaDiff;
8
8
  export declare const detectFocusArea: (snapshot: string | null) => FocusAreaResult;
9
9
  export declare const collectInteractiveNodes: (snapshot: string | null) => Array<Record<string, unknown>>;
10
10
  export interface FocusedElementInfo {
@@ -18,3 +18,8 @@ export declare function parseAriaLocator(ariaStr: string): {
18
18
  role: string;
19
19
  text: string;
20
20
  } | null;
21
+ export declare const LARGE_ARIA_CHANGE_THRESHOLD = 50;
22
+ export interface AriaDiff {
23
+ text: string | null;
24
+ count: number;
25
+ }
@@ -490,7 +490,9 @@ export const diffAriaSnapshots = (previous, current) => {
490
490
  const currTotals = countBy(curr.map((e) => e.summary));
491
491
  const byCount = diffByCount(prevTotals, currTotals);
492
492
  const renames = detectRenames(prev, curr, prevTotals, currTotals);
493
- return formatDiff([...byCount.added, ...renames.added], [...byCount.removed, ...renames.removed], toggled);
493
+ const added = [...byCount.added, ...renames.added];
494
+ const removed = [...byCount.removed, ...renames.removed];
495
+ return { text: formatDiff(added, removed, toggled), count: added.length + removed.length + toggled.length };
494
496
  };
495
497
  export const detectFocusArea = (snapshot) => {
496
498
  let tree = parseSnapshot(snapshot);
@@ -543,3 +545,4 @@ export function parseAriaLocator(ariaStr) {
543
545
  return null;
544
546
  return { role: match[1], text: match[2] };
545
547
  }
548
+ export const LARGE_ARIA_CHANGE_THRESHOLD = 50;
@@ -0,0 +1,15 @@
1
+ export declare class TTLCache<T> {
2
+ store: Map<string, CacheEntry<T>>;
3
+ ttlMs: number;
4
+ constructor(ttlMs?: number);
5
+ get(key: string): T | undefined;
6
+ set(key: string, value: T): void;
7
+ fetch(key: string, compute: () => Promise<T>): Promise<T>;
8
+ delete(key: string): void;
9
+ clear(): void;
10
+ }
11
+ type CacheEntry<T> = {
12
+ value: T;
13
+ at: number;
14
+ };
15
+ export {};
@@ -0,0 +1,34 @@
1
+ export class TTLCache {
2
+ store = new Map();
3
+ ttlMs;
4
+ constructor(ttlMs = Number.POSITIVE_INFINITY) {
5
+ this.ttlMs = ttlMs;
6
+ }
7
+ get(key) {
8
+ const entry = this.store.get(key);
9
+ if (!entry)
10
+ return undefined;
11
+ if (Date.now() - entry.at > this.ttlMs) {
12
+ this.store.delete(key);
13
+ return undefined;
14
+ }
15
+ return entry.value;
16
+ }
17
+ set(key, value) {
18
+ this.store.set(key, { value, at: Date.now() });
19
+ }
20
+ async fetch(key, compute) {
21
+ const cached = this.get(key);
22
+ if (cached !== undefined)
23
+ return cached;
24
+ const value = await compute();
25
+ this.set(key, value);
26
+ return value;
27
+ }
28
+ delete(key) {
29
+ this.store.delete(key);
30
+ }
31
+ clear() {
32
+ this.store.clear();
33
+ }
34
+ }
@@ -21,5 +21,11 @@ export interface ContextData {
21
21
  combinedHtml?: string;
22
22
  research?: string;
23
23
  }
24
+ export declare function formatHeadings(headings: {
25
+ h1?: string;
26
+ h2?: string;
27
+ h3?: string;
28
+ h4?: string;
29
+ }): string[];
24
30
  export type ContextMode = 'attached' | 'compact' | 'full';
25
31
  export declare function formatContextSummary(data: ContextData, mode?: ContextMode): string;
@@ -10,6 +10,18 @@ function indent(text) {
10
10
  .map((line) => chalk.gray(` ${line}`))
11
11
  .join('\n');
12
12
  }
13
+ export function formatHeadings(headings) {
14
+ const lines = [];
15
+ if (headings.h1)
16
+ lines.push(`H1: ${headings.h1}`);
17
+ if (headings.h2)
18
+ lines.push(`H2: ${headings.h2}`);
19
+ if (headings.h3)
20
+ lines.push(`H3: ${headings.h3}`);
21
+ if (headings.h4)
22
+ lines.push(`H4: ${headings.h4}`);
23
+ return lines;
24
+ }
13
25
  export function formatContextSummary(data, mode = 'compact') {
14
26
  if (mode === 'full') {
15
27
  return formatContextFull(data);
@@ -27,15 +39,7 @@ function formatContextAttached(data) {
27
39
  lines.push(indent(data.title));
28
40
  }
29
41
  lines.push('');
30
- const headingLines = [];
31
- if (data.headings.h1)
32
- headingLines.push(`H1: ${data.headings.h1}`);
33
- if (data.headings.h2)
34
- headingLines.push(`H2: ${data.headings.h2}`);
35
- if (data.headings.h3)
36
- headingLines.push(`H3: ${data.headings.h3}`);
37
- if (data.headings.h4)
38
- headingLines.push(`H4: ${data.headings.h4}`);
42
+ const headingLines = formatHeadings(data.headings);
39
43
  if (headingLines.length > 0) {
40
44
  lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
41
45
  lines.push(indent(headingLines.join('\n')));
@@ -88,15 +92,7 @@ function formatContextCompact(data) {
88
92
  lines.push(indent(data.title));
89
93
  }
90
94
  lines.push('');
91
- const headingLines = [];
92
- if (data.headings.h1)
93
- headingLines.push(`H1: ${data.headings.h1}`);
94
- if (data.headings.h2)
95
- headingLines.push(`H2: ${data.headings.h2}`);
96
- if (data.headings.h3)
97
- headingLines.push(`H3: ${data.headings.h3}`);
98
- if (data.headings.h4)
99
- headingLines.push(`H4: ${data.headings.h4}`);
95
+ const headingLines = formatHeadings(data.headings);
100
96
  if (headingLines.length > 0) {
101
97
  lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
102
98
  lines.push(indent(headingLines.join('\n')));
@@ -144,15 +140,7 @@ function formatContextFull(data) {
144
140
  lines.push(indent(data.title));
145
141
  }
146
142
  lines.push('');
147
- const headingLines = [];
148
- if (data.headings.h1)
149
- headingLines.push(`H1: ${data.headings.h1}`);
150
- if (data.headings.h2)
151
- headingLines.push(`H2: ${data.headings.h2}`);
152
- if (data.headings.h3)
153
- headingLines.push(`H3: ${data.headings.h3}`);
154
- if (data.headings.h4)
155
- headingLines.push(`H4: ${data.headings.h4}`);
143
+ const headingLines = formatHeadings(data.headings);
156
144
  if (headingLines.length > 0) {
157
145
  lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
158
146
  lines.push(indent(headingLines.join('\n')));
@@ -49,12 +49,10 @@ export class HooksRunner {
49
49
  async executeHook(hook, url) {
50
50
  try {
51
51
  if (hook.type === 'playwright') {
52
- const page = this.explorer.playwrightHelper.page;
53
- await hook.hook({ page, url });
52
+ await this.explorer.withPage(async (page) => hook.hook({ page, url }));
54
53
  }
55
54
  else {
56
- const I = this.explorer.actor;
57
- await hook.hook({ I, url });
55
+ await hook.hook({ I: this.explorer.actor, url });
58
56
  }
59
57
  }
60
58
  catch (error) {
@@ -2,8 +2,10 @@ import type * as parse5TreeAdapter from 'parse5/lib/tree-adapters/default';
2
2
  import type { HtmlConfig } from '../config.js';
3
3
  export declare const EXPLORBOT_ATTRS: {
4
4
  readonly area: "data-explorbot-area";
5
+ readonly coveredBy: "data-explorbot-covered-by";
5
6
  readonly context: "data-explorbot-context";
6
7
  readonly eidx: "data-explorbot-eidx";
8
+ readonly hit: "data-explorbot-hit";
7
9
  readonly variant: "data-explorbot-variant";
8
10
  };
9
11
  export declare const HTML_SELECTORS: {
@@ -41,8 +43,10 @@ export declare function inferHtmlRole(data: {
41
43
  export declare const ELEMENT_EXTRACTION_CONFIG: {
42
44
  readonly attrs: {
43
45
  readonly area: "data-explorbot-area";
46
+ readonly coveredBy: "data-explorbot-covered-by";
44
47
  readonly context: "data-explorbot-context";
45
48
  readonly eidx: "data-explorbot-eidx";
49
+ readonly hit: "data-explorbot-hit";
46
50
  readonly variant: "data-explorbot-variant";
47
51
  };
48
52
  readonly codeEditorMarkers: readonly ["monaco", "codemirror", "ace", "ace_editor", "code"];
@@ -70,6 +74,7 @@ export type ComponentScopeExtractionConfig = {
70
74
  interactiveControlSelector: string;
71
75
  limits: typeof HTML_EXTRACTION_LIMITS;
72
76
  };
77
+ export declare function captureHtmlForSnapshot(): string;
73
78
  export declare function extractElementData(el: Element, config?: ElementExtractionConfig): {
74
79
  tag: string;
75
80
  text: string;