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
@@ -5,7 +5,7 @@ import { AIProvider } from './ai/provider.js';
5
5
  import { RequestStore } from './api/request-store.js';
6
6
  import { XhrCapture } from './api/xhr-capture.js';
7
7
  import type { ExplorbotConfig } from './config.js';
8
- import type { UserResolveFunction } from './explorbot.js';
8
+ import type { ExperienceTracker } from './experience-tracker.js';
9
9
  import { KnowledgeTracker } from './knowledge-tracker.js';
10
10
  import { PlaywrightRecorder } from './playwright-recorder.js';
11
11
  import { Reporter } from './reporter.js';
@@ -42,7 +42,6 @@ declare class Explorer {
42
42
  stateManager: StateManager;
43
43
  knowledgeTracker: KnowledgeTracker;
44
44
  config: ExplorbotConfig;
45
- userResolveFn: UserResolveFunction | null;
46
45
  options?: {
47
46
  show?: boolean;
48
47
  headless?: boolean;
@@ -59,19 +58,17 @@ declare class Explorer {
59
58
  testPageErrorHandler: ((error: Error) => void) | null;
60
59
  testConsoleHandler: ((message: any) => void) | null;
61
60
  testDialogHandler: ((dialog: any) => void) | null;
62
- constructor(config: ExplorbotConfig, aiProvider: AIProvider, options?: {
61
+ constructor(config: ExplorbotConfig, aiProvider: AIProvider, options: {
63
62
  show?: boolean;
64
63
  headless?: boolean;
65
64
  incognito?: boolean;
66
65
  session?: string;
67
- });
66
+ } | undefined, experienceTracker: ExperienceTracker, knowledgeTracker: KnowledgeTracker);
68
67
  initializeContainer(): void;
69
68
  convertToCodeceptConfig(config: ExplorbotConfig): any;
70
69
  getConfig(): ExplorbotConfig;
71
- getConfigPath(): string | null;
72
70
  getAIProvider(): AIProvider;
73
71
  getStateManager(): StateManager;
74
- getCurrentUrl(): string;
75
72
  getKnowledgeTracker(): KnowledgeTracker;
76
73
  getReporter(): Reporter;
77
74
  getRequestStore(): RequestStore | null;
@@ -87,7 +84,7 @@ declare class Explorer {
87
84
  }): Promise<ActionResult>;
88
85
  capturePageWithScreenshot(): Promise<ActionResult>;
89
86
  executeAction(code: string): Promise<Action>;
90
- attemptAction(code: string, originalMessage?: string, experience?: boolean): Promise<boolean>;
87
+ attemptAction(code: string, originalMessage?: string): Promise<boolean>;
91
88
  getPlaywrightRecorder(): PlaywrightRecorder;
92
89
  visit(url: string): Promise<Action>;
93
90
  visitOnce(url: string): Promise<Action>;
@@ -100,7 +97,6 @@ declare class Explorer {
100
97
  }): Promise<number>;
101
98
  getEidxInContainer(containerCss: string | null): Promise<string[]>;
102
99
  getEidxByLocator(locator: string, container?: string | null): Promise<string | null>;
103
- reload(): Promise<void>;
104
100
  resolveBrowserUrl(url?: string): string | null;
105
101
  isFatalBrowserError(error: unknown): boolean;
106
102
  recoverFromBrowserError(): Promise<boolean>;
@@ -112,22 +108,15 @@ declare class Explorer {
112
108
  hasOtherTabs(): boolean;
113
109
  getOtherTabsInfo(): TabInfo[];
114
110
  clearOtherTabsInfo(): void;
115
- setUserResolve(userResolveFn: UserResolveFunction): void;
116
111
  listenToStateChanged(): void;
117
112
  stop(): Promise<void>;
118
113
  get activeTest(): Test | null;
119
114
  startTest(test: Test): Promise<boolean>;
120
115
  ensurePageAvailable(): Promise<boolean>;
121
- ensureActiveTestPageAvailable(): Promise<boolean>;
122
116
  handleExecutionError(error: unknown): Promise<BrowserExecutionErrorResult>;
123
117
  watchActiveTestPage(page?: any): void;
124
118
  stopTest(test: Test, meta?: Record<string, string>): Promise<void>;
125
119
  unwatchActiveTestPages(): void;
126
- hasPlaywrightLocator(locatorFn: (page: any) => any, opts?: {
127
- multiple?: boolean;
128
- contents?: boolean;
129
- success?: (locator: any) => Promise<void> | void;
130
- }): Promise<boolean>;
131
120
  playwrightLocatorCount(locatorFn: (page: any) => any): Promise<number>;
132
121
  bindFrameNavigated(page: any): void;
133
122
  openFreshTab(): Promise<void>;
@@ -12,15 +12,14 @@ import { visuallyAnnotateContainers } from "./ai/researcher/coordinates.js";
12
12
  import { RequestStore } from "./api/request-store.js";
13
13
  import { XhrCapture } from "./api/xhr-capture.js";
14
14
  import { ConfigParser } from './config.js';
15
- import { KnowledgeTracker } from './knowledge-tracker.js';
16
15
  import { PlaywrightRecorder } from "./playwright-recorder.js";
17
16
  import { Reporter } from "./reporter.js";
18
17
  import { StateManager } from './state-manager.js';
19
18
  import { TestResult } from "./test-plan.js";
20
- import { BrowserRecoveryError, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
19
+ import { BrowserRecoveryError, browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
21
20
  import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from "./utils/html.js";
22
21
  import { createDebug, log, tag } from './utils/logger.js';
23
- import { waitForPageReadiness } from "./utils/page-readiness.js";
22
+ import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
24
23
  import { WebElement } from "./utils/web-element.js";
25
24
  const debugLog = createDebug('explorbot:explorer');
26
25
  const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
@@ -33,7 +32,6 @@ class Explorer {
33
32
  stateManager;
34
33
  knowledgeTracker;
35
34
  config;
36
- userResolveFn = null;
37
35
  options;
38
36
  reporter;
39
37
  otherTabs = [];
@@ -45,21 +43,20 @@ class Explorer {
45
43
  testPageErrorHandler = null;
46
44
  testConsoleHandler = null;
47
45
  testDialogHandler = null;
48
- constructor(config, aiProvider, options) {
46
+ constructor(config, aiProvider, options, experienceTracker, knowledgeTracker) {
49
47
  this.config = config;
50
48
  this.aiProvider = aiProvider;
51
49
  this.options = options;
52
50
  this.initializeContainer();
53
- this.stateManager = new StateManager({ incognito: this.options?.incognito });
54
- this.knowledgeTracker = new KnowledgeTracker();
51
+ this.knowledgeTracker = knowledgeTracker;
52
+ this.stateManager = new StateManager(experienceTracker, knowledgeTracker);
55
53
  this.reporter = new Reporter(config.reporter, this.stateManager);
56
54
  }
57
55
  initializeContainer() {
58
56
  try {
59
57
  // Use project root for output directory, not current working directory
60
58
  const configParser = ConfigParser.getInstance();
61
- const configPath = configParser.getConfigPath();
62
- const projectRoot = configPath ? path.dirname(configPath) : process.cwd();
59
+ const projectRoot = configParser.getProjectRoot();
63
60
  global.output_dir = path.join(projectRoot, 'output', 'states');
64
61
  global.codecept_dir = projectRoot;
65
62
  configParser.validateConfig(this.config);
@@ -113,31 +110,20 @@ class Explorer {
113
110
  },
114
111
  };
115
112
  if (this.config.stepsFile) {
116
- const configPath = ConfigParser.getInstance().getConfigPath();
117
- const projectRoot = configPath ? path.dirname(configPath) : process.cwd();
113
+ const projectRoot = ConfigParser.getInstance().getProjectRoot();
118
114
  codeceptConfig.include = { I: path.resolve(projectRoot, this.config.stepsFile) };
119
115
  }
120
116
  return codeceptConfig;
121
117
  }
122
118
  getConfig() {
123
- if (!this.config) {
124
- throw new Error('Configuration not loaded. Call run() first.');
125
- }
126
119
  return this.config;
127
120
  }
128
- getConfigPath() {
129
- const configParser = ConfigParser.getInstance();
130
- return configParser.getConfigPath();
131
- }
132
121
  getAIProvider() {
133
122
  return this.aiProvider;
134
123
  }
135
124
  getStateManager() {
136
125
  return this.stateManager;
137
126
  }
138
- getCurrentUrl() {
139
- return this.stateManager.getCurrentState().url || '?';
140
- }
141
127
  getKnowledgeTracker() {
142
128
  return this.knowledgeTracker;
143
129
  }
@@ -175,9 +161,6 @@ class Explorer {
175
161
  if (this.isStarted) {
176
162
  return;
177
163
  }
178
- if (!this.config) {
179
- await this.initializeContainer();
180
- }
181
164
  await codeceptjs.recorder.start();
182
165
  await codeceptjs.container.started(null);
183
166
  storeListener();
@@ -294,8 +277,8 @@ class Explorer {
294
277
  async executeAction(code) {
295
278
  return this.runWithBrowserRecovery('executeAction', () => this.createAction().execute(code));
296
279
  }
297
- async attemptAction(code, originalMessage, experience = true) {
298
- return this.runWithBrowserRecovery('attemptAction', () => this.createAction().attempt(code, originalMessage, experience));
280
+ async attemptAction(code, originalMessage) {
281
+ return this.runWithBrowserRecovery('attemptAction', () => this.createAction().attempt(code, originalMessage));
299
282
  }
300
283
  getPlaywrightRecorder() {
301
284
  return this.playwrightRecorder;
@@ -318,7 +301,7 @@ class Explorer {
318
301
  await action.execute(`I.amOnPage(${serializedUrl})`);
319
302
  }
320
303
  catch (err) {
321
- const msg = err instanceof Error ? err.message : String(err);
304
+ const msg = browserErrorMessage(err);
322
305
  if (!RECOVERABLE_NAVIGATION_ERRORS.test(msg))
323
306
  throw err;
324
307
  tag('warning').log(`Navigation warning (continuing after load): ${msg.split('\n')[0]}`);
@@ -363,7 +346,7 @@ class Explorer {
363
346
  }
364
347
  catch (error) {
365
348
  if (this.isFatalBrowserError(error)) {
366
- tag('warning').log(`getEidxInContainer: ${error instanceof Error ? error.message : error}`);
349
+ tag('warning').log(`getEidxInContainer: ${browserErrorMessage(error)}`);
367
350
  await this.recoverFromBrowserError();
368
351
  }
369
352
  return [];
@@ -378,16 +361,12 @@ class Explorer {
378
361
  }
379
362
  catch (error) {
380
363
  if (this.isFatalBrowserError(error)) {
381
- tag('warning').log(`getEidxByLocator: ${error instanceof Error ? error.message : error}`);
364
+ tag('warning').log(`getEidxByLocator: ${browserErrorMessage(error)}`);
382
365
  await this.recoverFromBrowserError();
383
366
  }
384
367
  return null;
385
368
  }
386
369
  }
387
- async reload() {
388
- await this.closeOtherTabs();
389
- await this.playwrightHelper.page.reload();
390
- }
391
370
  resolveBrowserUrl(url) {
392
371
  if (!url)
393
372
  return null;
@@ -433,7 +412,7 @@ class Explorer {
433
412
  return this.waitForPageReadiness();
434
413
  }
435
414
  catch (err) {
436
- tag('error').log(`Browser recovery failed: ${err instanceof Error ? err.message : err}`);
415
+ tag('error').log(`Browser recovery failed: ${browserErrorMessage(err)}`);
437
416
  return false;
438
417
  }
439
418
  }
@@ -471,7 +450,7 @@ class Explorer {
471
450
  return true;
472
451
  }
473
452
  catch (err) {
474
- tag('error').log(`Browser restart failed: ${err instanceof Error ? err.message : err}`);
453
+ tag('error').log(`Browser restart failed: ${browserErrorMessage(err)}`);
475
454
  return false;
476
455
  }
477
456
  }
@@ -502,7 +481,7 @@ class Explorer {
502
481
  }
503
482
  catch (error) {
504
483
  if (this.isFatalBrowserError(error)) {
505
- tag('warning').log(`isInsideIframe: ${error instanceof Error ? error.message : error}`);
484
+ tag('warning').log(`isInsideIframe: ${browserErrorMessage(error)}`);
506
485
  await this.recoverFromBrowserError();
507
486
  }
508
487
  return false;
@@ -522,9 +501,6 @@ class Explorer {
522
501
  clearOtherTabsInfo() {
523
502
  this.otherTabs = [];
524
503
  }
525
- setUserResolve(userResolveFn) {
526
- this.userResolveFn = userResolveFn;
527
- }
528
504
  listenToStateChanged() {
529
505
  if (!this.playwrightHelper) {
530
506
  debugLog('Playwright helper not available for state monitoring');
@@ -658,11 +634,8 @@ class Explorer {
658
634
  this.watchActiveTestPage();
659
635
  return true;
660
636
  }
661
- async ensureActiveTestPageAvailable() {
662
- return this.ensurePageAvailable();
663
- }
664
637
  async handleExecutionError(error) {
665
- const message = error instanceof Error ? error.message : String(error);
638
+ const message = browserErrorMessage(error);
666
639
  tag('error').log(`Browser execution error: ${message}`);
667
640
  if (error instanceof Error && error.name === 'AbortError') {
668
641
  return {
@@ -766,29 +739,6 @@ class Explorer {
766
739
  }
767
740
  this.observedTestPages.clear();
768
741
  }
769
- async hasPlaywrightLocator(locatorFn, opts = {}) {
770
- try {
771
- const pwLocator = locatorFn(this.playwrightHelper.page);
772
- const count = await pwLocator.count();
773
- if (opts.multiple ? count === 0 : count !== 1)
774
- return false;
775
- if (opts.contents) {
776
- const html = await pwLocator.first().innerHTML();
777
- if (!html?.trim())
778
- return false;
779
- }
780
- if (opts.success)
781
- await opts.success(pwLocator);
782
- return true;
783
- }
784
- catch (error) {
785
- if (this.isFatalBrowserError(error)) {
786
- tag('warning').log(`hasPlaywrightLocator: ${error instanceof Error ? error.message : error}`);
787
- await this.recoverFromBrowserError();
788
- }
789
- return false;
790
- }
791
- }
792
742
  async playwrightLocatorCount(locatorFn) {
793
743
  try {
794
744
  const pwLocator = locatorFn(this.playwrightHelper.page);
@@ -796,7 +746,7 @@ class Explorer {
796
746
  }
797
747
  catch (error) {
798
748
  if (this.isFatalBrowserError(error)) {
799
- tag('warning').log(`playwrightLocatorCount: ${error instanceof Error ? error.message : error}`);
749
+ tag('warning').log(`playwrightLocatorCount: ${browserErrorMessage(error)}`);
800
750
  await this.recoverFromBrowserError();
801
751
  }
802
752
  throw error;
@@ -815,7 +765,7 @@ class Explorer {
815
765
  debugLog('Failed to get page title:', error);
816
766
  }
817
767
  this.stateManager.updateStateFromBasic(newUrl, newTitle, 'navigation');
818
- await new Promise((resolve) => setTimeout(resolve, 500));
768
+ await sleep(500);
819
769
  });
820
770
  }
821
771
  async openFreshTab() {
@@ -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 { KnowledgeTracker, Knowledge } from './knowledge-tracker.js';
3
4
  export interface Link {
4
5
  title: string;
5
6
  url: string;
@@ -54,24 +55,18 @@ 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;
62
- }
58
+ export type { Knowledge };
63
59
  export declare class StateManager {
64
60
  currentState: WebPageState | null;
65
61
  stateHistory: StateTransition[];
66
62
  allVisitedUrls: Set<string>;
67
- knowledgeCache: Knowledge[];
68
- lastKnowledgeScan: Date | null;
69
63
  stateChangeListeners: StateChangeListener[];
70
64
  experienceTracker: ExperienceTracker;
71
- knowledgeDir: string;
65
+ knowledgeTracker: KnowledgeTracker;
72
66
  nextStateId: number;
73
- constructor();
67
+ constructor(experienceTracker: ExperienceTracker, knowledgeTracker: KnowledgeTracker);
74
68
  getExperienceTracker(): ExperienceTracker;
69
+ getKnowledgeTracker(): KnowledgeTracker;
75
70
  /**
76
71
  * Subscribe to state change events
77
72
  */
@@ -80,11 +75,6 @@ export declare class StateManager {
80
75
  * Emit state change event to all listeners
81
76
  */
82
77
  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
78
  /**
89
79
  * Update current state from ActionResult and record transition if state changed
90
80
  */
@@ -119,10 +109,6 @@ export declare class StateManager {
119
109
  * Get the last N transitions
120
110
  */
121
111
  getRecentTransitions(count?: number): StateTransition[];
122
- /**
123
- * Scan knowledge directory for .md files and cache them
124
- */
125
- scanKnowledgeFiles(): void;
126
112
  /**
127
113
  * Get relevant knowledge files for current state
128
114
  */
@@ -134,12 +120,6 @@ export declare class StateManager {
134
120
  /**
135
121
  * Get all context for current state (knowledge + experience)
136
122
  */
137
- getCurrentContext(): {
138
- state: WebPageState;
139
- knowledge: Knowledge[];
140
- experience: string[];
141
- recentTransitions: StateTransition[];
142
- };
143
123
  /**
144
124
  * Check if we've been in this state before
145
125
  */
@@ -158,11 +138,6 @@ export declare class StateManager {
158
138
  * If the last transition changed URL, returns the fromState (for URL change detection).
159
139
  * Otherwise returns the most recent toState with content (for diffing).
160
140
  */
161
- getPreviousState(): WebPageState | null;
162
- /**
163
- * Load HTML content from file
164
- */
165
- loadHtmlFromFile(htmlFile: string): string | null;
166
141
  /**
167
142
  * Clear state history (useful for testing or reset)
168
143
  * Note: This preserves the current state, only clears navigation history