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
@@ -142,7 +142,7 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
142
142
  if (!mounted) return;
143
143
  setChecklistData({
144
144
  config: explorBot.getConfig(),
145
- knowledgeTracker: explorBot.getKnowledgeTracker(),
145
+ knowledgeTracker: explorBot.knowledgeTracker(),
146
146
  });
147
147
  setShowWelcome(true);
148
148
  setShowInput(true);
@@ -334,7 +334,7 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
334
334
  {showSessionTimer && <SessionTimer startedAt={sessionStartedAtRef.current} />}
335
335
  </Box>
336
336
 
337
- {showWelcome && <WelcomeCommands hasKnowledge={explorBot.getKnowledgeTracker().listAllKnowledge().length > 0} />}
337
+ {showWelcome && <WelcomeCommands hasKnowledge={explorBot.knowledgeTracker().listAllKnowledge().length > 0} />}
338
338
  {showInput && <Box height={1} />}
339
339
  {interruptPrompt && showInput && (
340
340
  <Box paddingX={1}>
package/src/config.ts CHANGED
@@ -362,6 +362,18 @@ export class ConfigParser {
362
362
  return path.join(path.dirname(configPath), config.dirs?.output || 'output');
363
363
  }
364
364
 
365
+ public getProjectRoot(): string {
366
+ const configPath = this.getConfigPath();
367
+ if (configPath) return path.dirname(configPath);
368
+ return process.cwd();
369
+ }
370
+
371
+ public resolveProjectDir(relativeDir: string): string {
372
+ const configPath = this.getConfigPath();
373
+ if (!configPath) return relativeDir;
374
+ return path.join(path.dirname(configPath), relativeDir);
375
+ }
376
+
365
377
  public getStatesDir(): string {
366
378
  return outputPath('states');
367
379
  }
@@ -1,12 +1,12 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
- import { basename, dirname, join } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
3
  import matter from 'gray-matter';
4
- import { type Tokens, marked } from 'marked';
5
4
  import type { ActionResult } from './action-result.js';
6
5
  import { ConfigParser } from './config.js';
7
6
  import { KnowledgeTracker } from './knowledge-tracker.js';
8
7
  import type { WebPageState } from './state-manager.js';
9
- import { createDebug, tag } from './utils/logger.js';
8
+ import { createDebug, pluralize, tag } from './utils/logger.js';
9
+ import { loadMarkdownFiles } from './utils/markdown-files.js';
10
10
  import { mdq } from './utils/markdown-query.js';
11
11
  import { redactSecrets } from './utils/secrets.js';
12
12
  import { isNonReusableCode } from './utils/step-analyzer.ts';
@@ -39,20 +39,14 @@ export class ExperienceTracker {
39
39
  private disabled: boolean;
40
40
  private knowledgeTracker: KnowledgeTracker;
41
41
 
42
- constructor(options: { disabled?: boolean } = {}) {
42
+ constructor(knowledgeTracker: KnowledgeTracker, options: { disabled?: boolean } = {}) {
43
43
  const configParser = ConfigParser.getInstance();
44
44
  const config = configParser.getConfig();
45
- const configPath = configParser.getConfigPath();
46
45
  this.disabled = options.disabled ?? false;
47
- this.knowledgeTracker = new KnowledgeTracker();
46
+ this.knowledgeTracker = knowledgeTracker;
48
47
 
49
48
  // Resolve experience directory relative to the config file location (project root)
50
- if (configPath) {
51
- const projectRoot = dirname(configPath);
52
- this.experienceDir = join(projectRoot, config.dirs?.experience || 'experience');
53
- } else {
54
- this.experienceDir = config.dirs?.experience || 'experience';
55
- }
49
+ this.experienceDir = configParser.resolveProjectDir(config.dirs?.experience || 'experience');
56
50
 
57
51
  if (!this.disabled) {
58
52
  this.ensureDirectory(this.experienceDir);
@@ -99,7 +93,10 @@ export class ExperienceTracker {
99
93
  }
100
94
 
101
95
  readExperienceFile(stateHash: string): { content: string; data: any } {
102
- const filePath = this.getExperienceFilePath(stateHash);
96
+ return this.readExperienceFileAt(this.getExperienceFilePath(stateHash));
97
+ }
98
+
99
+ private readExperienceFileAt(filePath: string): { content: string; data: any } {
103
100
  const fileContent = readFileSync(filePath, 'utf8');
104
101
  const { content, data } = matter(fileContent);
105
102
  return { content, data };
@@ -114,21 +111,6 @@ export class ExperienceTracker {
114
111
  writeFileSync(filePath, fileContent, 'utf8');
115
112
  }
116
113
 
117
- hasRecentExperience(stateHash: string, prefix = ''): boolean {
118
- if (this.disabled) {
119
- return false;
120
- }
121
- if (prefix) {
122
- stateHash = `${prefix}_${stateHash}`;
123
- }
124
- const filePath = this.getExperienceFilePath(stateHash);
125
- if (!existsSync(filePath)) {
126
- return false;
127
- }
128
- const stats = statSync(filePath);
129
- return stats.mtime.getTime() > Date.now() - 1000 * 60 * 60 * 24;
130
- }
131
-
132
114
  private getExperienceFilePath(stateHash: string): string {
133
115
  return join(this.experienceDir, `${stateHash}.md`);
134
116
  }
@@ -176,7 +158,12 @@ export class ExperienceTracker {
176
158
  this.ensureExperienceFile(state);
177
159
  const stateHash = state.getStateHash();
178
160
  const { content, data } = this.readExperienceFile(stateHash);
179
- if (content.includes(action.code)) {
161
+ if (
162
+ mdq(content)
163
+ .query('code')
164
+ .meta()
165
+ .some((m) => m.text.trim() === action.code.trim())
166
+ ) {
180
167
  debugLog('Skipping duplicate action', action.code);
181
168
  return;
182
169
  }
@@ -204,7 +191,12 @@ export class ExperienceTracker {
204
191
  const stateHash = state.getStateHash();
205
192
  const { content, data } = this.readExperienceFile(stateHash);
206
193
 
207
- if (content.includes(body)) {
194
+ if (
195
+ mdq(content)
196
+ .query('section2')
197
+ .each()
198
+ .some((s) => s.text().trim() === body.trim())
199
+ ) {
208
200
  debugLog('Skipping duplicate flow body');
209
201
  return;
210
202
  }
@@ -231,25 +223,11 @@ export class ExperienceTracker {
231
223
  }
232
224
 
233
225
  try {
234
- const files = readdirSync(experienceDir)
235
- .filter((file: string) => file.endsWith('.md'))
236
- .map((file: string) => join(experienceDir, file));
237
-
238
- for (const file of files) {
239
- try {
240
- const content = readFileSync(file, 'utf8');
241
- const parsed = matter(content);
242
- const mtime = statSync(file).mtime;
243
- allFiles.push({
244
- filePath: file,
245
- data: parsed.data,
246
- content: parsed.content,
247
- mtime,
248
- });
249
- } catch (error) {
250
- debugLog(`Failed to read experience file ${file}:`, error);
251
- }
252
- }
226
+ allFiles.push(
227
+ ...loadMarkdownFiles(experienceDir, {
228
+ onError: (filePath, error) => debugLog(`Failed to read experience file ${filePath}:`, error),
229
+ })
230
+ );
253
231
  } catch (error) {
254
232
  debugLog(`Failed to read experience directory ${experienceDir}:`, error);
255
233
  }
@@ -275,20 +253,17 @@ export class ExperienceTracker {
275
253
  });
276
254
  })
277
255
  .map((experience) => {
278
- const lines = experience.content.split('\n');
279
- if (lines.length <= maxLines) return experience;
280
- return { ...experience, content: lines.slice(0, maxLines).join('\n') };
256
+ if (experience.content.split('\n').length <= maxLines) return experience;
257
+ let content = experience.content;
258
+ while (content.split('\n').length > maxLines) {
259
+ const sections = mdq(content).query('section2').each();
260
+ if (sections.length <= 1) break;
261
+ content = sections[sections.length - 1].replace('');
262
+ }
263
+ return { ...experience, content };
281
264
  });
282
265
  }
283
266
 
284
- /**
285
- * Clean up experience tracker (for testing)
286
- */
287
- cleanup(): void {
288
- // Clear any in-memory state if needed
289
- // The actual files will be cleaned up by test cleanup
290
- }
291
-
292
267
  getSuccessfulExperience(state: ActionResult, options?: { includeDescendants?: boolean; stripCode?: boolean }): string[] {
293
268
  const records = this.getRelevantExperience(state, {
294
269
  includeDescendantExperience: options?.includeDescendants,
@@ -326,36 +301,50 @@ export class ExperienceTracker {
326
301
  return aHash.localeCompare(bHash);
327
302
  });
328
303
 
304
+ return this.buildToc(sorted);
305
+ }
306
+
307
+ renderExperienceTocFor(state: ActionResult): string {
308
+ const toc = this.getExperienceTableOfContents(state);
309
+ if (toc.length === 0) return '';
310
+
311
+ const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
312
+ debugLog(`injecting experience TOC (${toc.length} files, ${totalSections} sections)`);
313
+ tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections)`);
314
+ return renderExperienceToc(toc);
315
+ }
316
+
317
+ getExperienceSection(fileTag: string, sectionIndex: number, state: ActionResult, options?: { includeDescendantExperience?: boolean }): { title: string; url: string; content: string } | null {
318
+ const toc = this.getExperienceTableOfContents(state, options);
319
+ const entry = toc.find((e) => e.fileTag === fileTag);
320
+ if (!entry) return null;
321
+
322
+ const extracted = this.readSectionByHash(entry.fileHash, sectionIndex);
323
+ if (!extracted) return null;
324
+
325
+ return { title: extracted.title, url: entry.url, content: extracted.body };
326
+ }
327
+
328
+ private buildToc(records: ExperienceFile[]): ExperienceTocEntry[] {
329
329
  const toc: ExperienceTocEntry[] = [];
330
- for (let i = 0; i < sorted.length; i++) {
331
- const record = sorted[i];
332
- const fileHash = basename(record.filePath, '.md');
333
- const url = (record.data as WebPageState)?.url || '';
330
+ for (const record of records) {
334
331
  const sections = listTocHeadings(record.content);
335
332
  if (sections.length === 0) continue;
336
333
  toc.push({
337
- fileTag: indexToLetters(i),
338
- fileHash,
339
- url,
334
+ fileTag: indexToLetters(toc.length),
335
+ fileHash: basename(record.filePath, '.md'),
336
+ url: (record.data as WebPageState)?.url || '',
340
337
  sections,
341
338
  });
342
339
  }
343
340
  return toc;
344
341
  }
345
342
 
346
- getExperienceSection(fileTag: string, sectionIndex: number, state: ActionResult, options?: { includeDescendantExperience?: boolean }): { title: string; url: string; content: string } | null {
347
- const toc = this.getExperienceTableOfContents(state, options);
348
- const entry = toc.find((e) => e.fileTag === fileTag);
349
- if (!entry) return null;
350
-
351
- const filePath = this.findExperienceFileByHash(entry.fileHash);
343
+ private readSectionByHash(fileHash: string, sectionIndex: number): { title: string; body: string } | null {
344
+ const filePath = this.findExperienceFileByHash(fileHash);
352
345
  if (!filePath) return null;
353
-
354
- const { content } = this.readExperienceFile(entry.fileHash);
355
- const extracted = extractHeadingSection(content, sectionIndex);
356
- if (!extracted) return null;
357
-
358
- return { title: extracted.title, url: entry.url, content: extracted.body };
346
+ const { content } = this.readExperienceFileAt(filePath);
347
+ return extractHeadingSection(content, sectionIndex);
359
348
  }
360
349
 
361
350
  private findExperienceFileByHash(fileHash: string): string | null {
@@ -402,19 +391,7 @@ export class ExperienceTracker {
402
391
  return aUrl.localeCompare(bUrl);
403
392
  });
404
393
 
405
- const toc: ExperienceTocEntry[] = [];
406
- for (let i = 0; i < sorted.length; i++) {
407
- const record = sorted[i];
408
- const sections = listTocHeadings(record.content);
409
- if (sections.length === 0) continue;
410
- toc.push({
411
- fileTag: indexToLetters(toc.length),
412
- fileHash: basename(record.filePath, '.md'),
413
- url: (record.data as WebPageState)?.url || '',
414
- sections,
415
- });
416
- }
417
- return toc;
394
+ return this.buildToc(sorted);
418
395
  }
419
396
 
420
397
  getExperienceSectionByTag(fileTag: string, sectionIndex: number, filter?: string): { title: string; url: string; content: string; fileHash: string } | null {
@@ -422,11 +399,7 @@ export class ExperienceTracker {
422
399
  const entry = toc.find((e) => e.fileTag === fileTag);
423
400
  if (!entry) return null;
424
401
 
425
- const filePath = this.findExperienceFileByHash(entry.fileHash);
426
- if (!filePath) return null;
427
-
428
- const { content } = this.readExperienceFile(entry.fileHash);
429
- const extracted = extractHeadingSection(content, sectionIndex);
402
+ const extracted = this.readSectionByHash(entry.fileHash, sectionIndex);
430
403
  if (!extracted) return null;
431
404
 
432
405
  return { title: extracted.title, url: entry.url, content: extracted.body, fileHash: entry.fileHash };
@@ -434,49 +407,22 @@ export class ExperienceTracker {
434
407
  }
435
408
 
436
409
  function listTocHeadings(content: string): { index: number; level: 2 | 3; title: string }[] {
437
- const tokens = marked.lexer(content);
438
- const result: { index: number; level: 2 | 3; title: string }[] = [];
439
- let index = 0;
440
- for (const token of tokens) {
441
- if (token.type !== 'heading') continue;
442
- const heading = token as Tokens.Heading;
443
- if (heading.depth !== 2 && heading.depth !== 3) continue;
444
- index++;
445
- result.push({ index, level: heading.depth as 2 | 3, title: heading.text });
446
- }
447
- return result;
410
+ return mdq(content)
411
+ .query('heading')
412
+ .meta()
413
+ .filter((m) => m.depth === 2 || m.depth === 3)
414
+ .map((m, i) => ({ index: i + 1, level: m.depth as 2 | 3, title: m.text }));
448
415
  }
449
416
 
450
417
  function extractHeadingSection(content: string, sectionIndex: number): { title: string; body: string } | null {
451
- const tokens = marked.lexer(content);
452
- const matching: { tokenIdx: number; depth: number; text: string }[] = [];
453
-
454
- for (let i = 0; i < tokens.length; i++) {
455
- const token = tokens[i];
456
- if (token.type !== 'heading') continue;
457
- const heading = token as Tokens.Heading;
458
- if (heading.depth !== 2 && heading.depth !== 3) continue;
459
- matching.push({ tokenIdx: i, depth: heading.depth, text: heading.text });
460
- }
418
+ const sections = mdq(content).query('section').each();
419
+ const metas = mdq(content).query('section').meta();
420
+ const matching = metas.map((meta, i) => ({ meta, section: sections[i] })).filter((entry) => entry.meta.depth === 2 || entry.meta.depth === 3);
461
421
 
462
422
  if (sectionIndex < 1 || sectionIndex > matching.length) return null;
463
423
 
464
424
  const target = matching[sectionIndex - 1];
465
- let endTokenIdx = tokens.length;
466
- for (let j = target.tokenIdx + 1; j < tokens.length; j++) {
467
- const token = tokens[j];
468
- if (token.type !== 'heading') continue;
469
- if ((token as Tokens.Heading).depth <= target.depth) {
470
- endTokenIdx = j;
471
- break;
472
- }
473
- }
474
-
475
- const body = tokens
476
- .slice(target.tokenIdx, endTokenIdx)
477
- .map((t) => (t as any).raw || '')
478
- .join('');
479
- return { title: target.text, body };
425
+ return { title: target.meta.text, body: target.section.text() };
480
426
  }
481
427
 
482
428
  function indexToLetters(index: number): string {
@@ -545,23 +491,14 @@ function generateActionContent(title: string, code: string, explanation?: string
545
491
  }
546
492
 
547
493
  function renderAsHowTo(content: string): string {
548
- const tokens = marked.lexer(content);
549
- let result = '';
550
- for (const token of tokens) {
551
- if (token.type === 'heading' && (token as Tokens.Heading).depth === 2) {
552
- const text = (token as Tokens.Heading).text.trim();
553
- if (text.startsWith('FLOW:')) {
554
- result += `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
555
- continue;
556
- }
557
- if (text.startsWith('ACTION:')) {
558
- result += `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
559
- continue;
560
- }
561
- }
562
- result += (token as any).raw || '';
563
- }
564
- return result;
494
+ return mdq(content)
495
+ .query('h2')
496
+ .replaceEach((heading) => {
497
+ const text = heading.meta()[0]?.text.trim() || '';
498
+ if (text.startsWith('FLOW:')) return `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
499
+ if (text.startsWith('ACTION:')) return `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
500
+ return heading.text();
501
+ });
565
502
  }
566
503
 
567
504
  export interface ExperienceFile {
package/src/explorbot.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { existsSync, mkdirSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { ActionResult } from './action-result.ts';
4
3
  import { Captain } from './ai/captain.ts';
5
4
  import { Driller } from './ai/driller.ts';
6
5
  import { ExperienceCompactor } from './ai/experience-compactor.ts';
@@ -28,6 +27,7 @@ import { WebPageState } from './state-manager.ts';
28
27
  import { Stats } from './stats.ts';
29
28
  import type { Suite } from './suite.ts';
30
29
  import { Plan, type Test } from './test-plan.ts';
30
+ import { browserErrorMessage } from './utils/browser-errors.ts';
31
31
  import { setVerboseMode, tag } from './utils/logger.ts';
32
32
  import { relativeToCwd } from './utils/next-steps.ts';
33
33
  import { sanitizeFilename } from './utils/strings.ts';
@@ -54,7 +54,6 @@ export class ExplorBot {
54
54
  private config!: ExplorbotConfig;
55
55
  private options: ExplorBotOptions;
56
56
  private userResolveFn: UserResolveFunction | null = null;
57
- public needsInput = false;
58
57
  private currentPlan?: Plan;
59
58
  private planFeature?: string;
60
59
  lastPlanError: Error | null = null;
@@ -62,6 +61,8 @@ export class ExplorBot {
62
61
  private agents: Record<string, any> = {};
63
62
  private sessionPlans: Plan[] = [];
64
63
  private lastReportedTestCount = 0;
64
+ private _experienceTracker?: ExperienceTracker;
65
+ private _knowledgeTracker?: KnowledgeTracker;
65
66
 
66
67
  constructor(options: ExplorBotOptions = {}) {
67
68
  this.options = options;
@@ -87,14 +88,13 @@ export class ExplorBot {
87
88
 
88
89
  try {
89
90
  await this.startProviderOnly();
90
- this.explorer = new Explorer(this.config, this.provider, this.options);
91
+ this.explorer = new Explorer(this.config, this.provider, this.options, this.experienceTracker(), this.knowledgeTracker());
91
92
  await this.explorer.start();
92
93
  if (!this.options.incognito) {
93
94
  await this.agentExperienceCompactor().autocompact();
94
95
  }
95
- if (this.userResolveFn) this.explorer.setUserResolve(this.userResolveFn);
96
96
  } catch (error) {
97
- const message = error instanceof Error ? error.message : String(error);
97
+ const message = browserErrorMessage(error);
98
98
  console.error('\nFailed to start:', message);
99
99
  process.exit(1);
100
100
  }
@@ -133,18 +133,12 @@ export class ExplorBot {
133
133
  return this.explorer;
134
134
  }
135
135
 
136
- getKnowledgeTracker(): KnowledgeTracker {
137
- if (this.explorer) {
138
- return this.explorer.getKnowledgeTracker();
139
- }
140
- return new KnowledgeTracker();
136
+ knowledgeTracker(): KnowledgeTracker {
137
+ return (this._knowledgeTracker ||= new KnowledgeTracker());
141
138
  }
142
139
 
143
- getExperienceTracker(): ExperienceTracker {
144
- if (this.explorer) {
145
- return this.explorer.getStateManager().getExperienceTracker();
146
- }
147
- return new ExperienceTracker();
140
+ experienceTracker(): ExperienceTracker {
141
+ return (this._experienceTracker ||= new ExperienceTracker(this.knowledgeTracker()));
148
142
  }
149
143
 
150
144
  getConfig(): ExplorbotConfig {
@@ -154,13 +148,6 @@ export class ExplorBot {
154
148
  getOptions(): ExplorBotOptions {
155
149
  return this.options;
156
150
  }
157
- isReady(): boolean {
158
- return this.explorer?.isStarted;
159
- }
160
-
161
- getConfigParser(): ConfigParser {
162
- return this.configParser;
163
- }
164
151
 
165
152
  getProvider(): AIProvider {
166
153
  return this.provider;
@@ -173,12 +160,9 @@ export class ExplorBot {
173
160
  config: this.config,
174
161
  });
175
162
 
176
- const agentEmoji = (agent as any).emoji || '';
177
163
  const agentName = (agent as any).constructor.name.toLowerCase();
178
164
  tag('debug').log(`Created ${agentName} agent`);
179
165
 
180
- // Agent is stored by the calling method using a string key
181
-
182
166
  return agent;
183
167
  }
184
168
 
@@ -188,13 +172,13 @@ export class ExplorBot {
188
172
 
189
173
  agentNavigator(): Navigator {
190
174
  return (this.agents.navigator ||= this.createAgent(({ ai, explorer }) => {
191
- return new Navigator(explorer, ai, this.agentExperienceCompactor(), explorer.getStateManager().getExperienceTracker());
175
+ return new Navigator(explorer, ai);
192
176
  }));
193
177
  }
194
178
 
195
179
  agentPlanner(): Planner {
196
180
  if (!this.agents.planner) {
197
- this.agents.planner = this.createAgent(({ ai, explorer }) => new Planner(explorer, ai));
181
+ this.agents.planner = this.createAgent(({ ai, explorer }) => new Planner(explorer, ai, this.agentResearcher()));
198
182
  const fisherman = this.agentFisherman();
199
183
  if (fisherman) this.agents.planner.setFisherman(fisherman);
200
184
  }
@@ -205,14 +189,8 @@ export class ExplorBot {
205
189
  return (this.agents.pilot ||= this.createAgent(({ ai, explorer }) => {
206
190
  const researcher = this.agentResearcher();
207
191
  const navigator = this.agentNavigator();
208
- const stateManager = explorer.getStateManager();
209
- const experienceTracker = stateManager.getExperienceTracker();
210
- const getState = () => {
211
- const state = stateManager.getCurrentState();
212
- return state ? ActionResult.fromState(state) : null;
213
- };
214
- const tools = createAgentTools({ explorer, researcher, navigator, experienceTracker, getState, supervisor: true });
215
- return new Pilot(ai, tools, researcher, explorer, experienceTracker);
192
+ const tools = createAgentTools({ explorer, researcher, navigator, supervisor: true });
193
+ return new Pilot(ai, tools, researcher, explorer);
216
194
  }));
217
195
  }
218
196
 
@@ -221,13 +199,7 @@ export class ExplorBot {
221
199
  this.agents.tester = this.createAgent(({ ai, explorer }) => {
222
200
  const researcher = this.agentResearcher();
223
201
  const navigator = this.agentNavigator();
224
- const stateManager = explorer.getStateManager();
225
- const experienceTracker = stateManager.getExperienceTracker();
226
- const getState = () => {
227
- const state = stateManager.getCurrentState();
228
- return state ? ActionResult.fromState(state) : null;
229
- };
230
- const tools = createAgentTools({ explorer, researcher, navigator, experienceTracker, getState });
202
+ const tools = createAgentTools({ explorer, researcher, navigator });
231
203
  return new Tester(explorer, ai, researcher, navigator, tools);
232
204
  });
233
205
 
@@ -246,16 +218,12 @@ export class ExplorBot {
246
218
  agentCaptain(): Captain {
247
219
  if (!this.agents.captain) {
248
220
  this.agents.captain = new Captain(this);
249
-
250
- const qm = this.agentQuartermaster();
251
- if (qm) this.agents.captain.setQuartermaster(qm);
252
- this.agents.captain.setHistorian(this.agentHistorian());
253
221
  }
254
222
  return this.agents.captain;
255
223
  }
256
224
 
257
225
  agentExperienceCompactor(): ExperienceCompactor {
258
- return (this.agents.experienceCompactor ||= new ExperienceCompactor(this.provider, this.getExperienceTracker()));
226
+ return (this.agents.experienceCompactor ||= new ExperienceCompactor(this.provider, this.experienceTracker()));
259
227
  }
260
228
 
261
229
  agentQuartermaster(): Quartermaster | null {
@@ -287,11 +255,9 @@ export class ExplorBot {
287
255
  this.agents.rerunner = this.createAgent(({ ai, explorer }) => {
288
256
  const researcher = this.agentResearcher();
289
257
  const navigator = this.agentNavigator();
290
- const tools = createAgentTools({ explorer, researcher, navigator });
258
+ const tools = createAgentTools({ explorer, researcher, navigator, withExperience: false });
291
259
  return new Rerunner(explorer, ai, tools);
292
260
  });
293
- const qm = this.agentQuartermaster();
294
- if (qm) this.agents.rerunner.setQuartermaster(qm);
295
261
  this.agents.rerunner.setHistorian(this.agentHistorian());
296
262
  }
297
263
  return this.agents.rerunner;
@@ -509,7 +475,7 @@ export class ExplorBot {
509
475
 
510
476
  this.lastReportedTestCount = tests.length;
511
477
  } catch (error) {
512
- const message = error instanceof Error ? error.message : String(error);
478
+ const message = browserErrorMessage(error);
513
479
  tag('warning').log(`Session analysis failed: ${message}`);
514
480
  }
515
481
  }