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
@@ -6,10 +6,22 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
10
- import path, { dirname, join, resolve } from 'node:path';
9
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import path, { basename, dirname, join, resolve } from 'node:path';
11
12
  import { parseEnv } from 'node:util';
13
+ import matter from 'gray-matter';
12
14
  import { log } from './utils/logger.js';
15
+ export const PROVIDERS = {
16
+ openai: async () => (await import('@ai-sdk/openai')).createOpenAI(),
17
+ anthropic: async () => (await import('@ai-sdk/anthropic')).createAnthropic(),
18
+ google: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI(),
19
+ groq: async () => (await import('@ai-sdk/groq')).createGroq(),
20
+ mistral: async () => (await import('@ai-sdk/mistral')).createMistral(),
21
+ openrouter: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter(),
22
+ sambanova: async () => (await import('sambanova-ai-provider')).createSambaNova(),
23
+ };
24
+ let cachedOutputRoot = null;
13
25
  const config = {
14
26
  playwright: {
15
27
  browser: 'chromium',
@@ -20,8 +32,21 @@ const config = {
20
32
  },
21
33
  };
22
34
  export const EXPLORBOT_CONFIG_PATHS = ['explorbot.config.js', 'explorbot.config.mjs', 'explorbot.config.ts'];
35
+ export const EXPLORBOT_ENV_VARS = [
36
+ { name: 'EXPLORBOT_AI_PROVIDER', required: true, description: 'Provider name; fills every model role from its recommended models. Turns on config-free mode' },
37
+ { name: 'EXPLORBOT_AI_MODEL', description: 'Pins the main model — a model id for the provider, or a standalone provider/model-id' },
38
+ { name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
39
+ { name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
40
+ { name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
41
+ { name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to a fresh temp directory' },
42
+ { name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
43
+ { name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
44
+ { name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
45
+ { name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
46
+ ];
23
47
  export class ConfigParser {
24
48
  static instance;
49
+ static recommended = null;
25
50
  config = null;
26
51
  configPath = null;
27
52
  runtimeBaseUrlOverride = null;
@@ -32,6 +57,10 @@ export class ConfigParser {
32
57
  return;
33
58
  Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
34
59
  }
60
+ static recommendedModels() {
61
+ ConfigParser.recommended ||= JSON.parse(readFileSync(new URL('../models.json', import.meta.url), 'utf8'));
62
+ return ConfigParser.recommended;
63
+ }
35
64
  static getInstance() {
36
65
  if (!ConfigParser.instance) {
37
66
  ConfigParser.instance = new ConfigParser();
@@ -54,18 +83,25 @@ export class ConfigParser {
54
83
  ConfigParser.loadEnv('.env');
55
84
  try {
56
85
  const resolvedPath = options?.config || this.findConfigFile();
57
- if (!resolvedPath) {
58
- throw new Error('No configuration file found. Please create explorbot.config.js or explorbot.config.ts');
86
+ let loadedConfig = null;
87
+ let sourcePath = resolvedPath;
88
+ if (resolvedPath) {
89
+ const configModule = await this.loadConfigModule(resolvedPath);
90
+ loadedConfig = configModule.default || configModule;
91
+ if (!loadedConfig) {
92
+ throw new Error('Configuration file is empty or invalid');
93
+ }
94
+ log(`Configuration loaded from: ${resolvedPath}`);
59
95
  }
60
- const configModule = await this.loadConfigModule(resolvedPath);
61
- const loadedConfig = configModule.default || configModule;
62
- if (!loadedConfig) {
63
- throw new Error('Configuration file is empty or invalid');
96
+ if (!resolvedPath) {
97
+ const outputRoot = resolveOutputRoot();
98
+ loadedConfig = await this.buildEnvConfig(options?.baseUrl, outputRoot);
99
+ sourcePath = join(outputRoot, 'explorbot.config.js');
100
+ log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
64
101
  }
65
102
  this.config = this.resolveConfig(loadedConfig, options);
66
103
  this.runtimeBaseUrlOverride = options?.baseUrl || null;
67
- this.configPath = resolvedPath;
68
- log(`Configuration loaded from: ${resolvedPath}`);
104
+ this.configPath = sourcePath;
69
105
  // Restore original directory after successful config load
70
106
  if (options?.path && originalCwd !== process.cwd()) {
71
107
  process.chdir(originalCwd);
@@ -96,6 +132,18 @@ export class ConfigParser {
96
132
  throw new Error('Config path not found');
97
133
  return path.join(path.dirname(configPath), config.dirs?.output || 'output');
98
134
  }
135
+ getProjectRoot() {
136
+ const configPath = this.getConfigPath();
137
+ if (configPath)
138
+ return path.dirname(configPath);
139
+ return process.cwd();
140
+ }
141
+ resolveProjectDir(relativeDir) {
142
+ const configPath = this.getConfigPath();
143
+ if (!configPath)
144
+ return relativeDir;
145
+ return path.join(path.dirname(configPath), relativeDir);
146
+ }
99
147
  getStatesDir() {
100
148
  return outputPath('states');
101
149
  }
@@ -107,6 +155,7 @@ export class ConfigParser {
107
155
  }
108
156
  // For testing purposes only
109
157
  static resetForTesting() {
158
+ cachedOutputRoot = null;
110
159
  if (ConfigParser.instance) {
111
160
  ConfigParser.instance.config = null;
112
161
  ConfigParser.instance.configPath = null;
@@ -157,6 +206,51 @@ export class ConfigParser {
157
206
  // Ignore cleanup errors
158
207
  }
159
208
  }
209
+ async buildEnvConfig(baseUrl, outputRoot) {
210
+ const provider = process.env.EXPLORBOT_AI_PROVIDER;
211
+ const modelSpec = process.env.EXPLORBOT_AI_MODEL;
212
+ if (!provider && !modelSpec) {
213
+ throw new Error('No configuration file found. Please create explorbot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
214
+ }
215
+ if (modelSpec && !provider && !modelSpec.includes('/')) {
216
+ throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
217
+ }
218
+ const url = process.env.EXPLORBOT_URL || baseUrl;
219
+ if (!url) {
220
+ throw new Error('No URL to explore. Set EXPLORBOT_URL or pass a URL to the command');
221
+ }
222
+ materializeKnowledge(outputRoot);
223
+ let model;
224
+ if (provider && modelSpec)
225
+ model = await createModel(provider, modelSpec);
226
+ if (provider && !modelSpec)
227
+ model = await resolveModel(provider, 'model');
228
+ if (!provider)
229
+ model = await resolveModel(modelSpec, 'model');
230
+ const ai = {
231
+ model,
232
+ agents: { historian: { enabled: false } },
233
+ };
234
+ let recommended = {};
235
+ if (provider)
236
+ recommended = ConfigParser.recommendedModels()[provider] || {};
237
+ const visionSpec = process.env.EXPLORBOT_VISION_MODEL;
238
+ if (visionSpec)
239
+ ai.visionModel = await resolveModel(visionSpec, 'visionModel');
240
+ if (!visionSpec && recommended.visionModel)
241
+ ai.visionModel = await resolveModel(provider, 'visionModel');
242
+ const agenticSpec = process.env.EXPLORBOT_AGENTIC_MODEL;
243
+ if (agenticSpec)
244
+ ai.agenticModel = await resolveModel(agenticSpec, 'agenticModel');
245
+ if (!agenticSpec && recommended.agenticModel)
246
+ ai.agenticModel = await resolveModel(provider, 'agenticModel');
247
+ return {
248
+ playwright: { browser: 'chromium', url, show: false },
249
+ ai,
250
+ dirs: { knowledge: 'knowledge', experience: 'experience', output: '.' },
251
+ experience: { disabled: true },
252
+ };
253
+ }
160
254
  findConfigFile() {
161
255
  const possiblePaths = [...EXPLORBOT_CONFIG_PATHS, 'config/explorbot.config.js', 'config/explorbot.config.mjs', 'config/explorbot.config.ts', 'src/config/explorbot.config.js', 'src/config/explorbot.config.mjs', 'src/config/explorbot.config.ts'];
162
256
  for (const path of possiblePaths) {
@@ -256,3 +350,55 @@ export class ConfigParser {
256
350
  export function outputPath(...segments) {
257
351
  return path.join(ConfigParser.getInstance().getOutputDir(), ...segments);
258
352
  }
353
+ export async function resolveModel(spec, role = 'model') {
354
+ const separator = spec.indexOf('/');
355
+ if (separator > 0) {
356
+ return createModel(spec.slice(0, separator), spec.slice(separator + 1));
357
+ }
358
+ const recommended = ConfigParser.recommendedModels()[spec];
359
+ if (!recommended) {
360
+ throw new Error(`No recommended models for "${spec}". Write it as "provider/model-id", or use a provider with recommendations: ${Object.keys(ConfigParser.recommendedModels()).join(', ')}`);
361
+ }
362
+ const modelId = recommended[role];
363
+ if (!modelId) {
364
+ throw new Error(`Provider "${spec}" has no recommended ${role}. Set it explicitly as "provider/model-id".`);
365
+ }
366
+ return createModel(spec, modelId);
367
+ }
368
+ export function resolveOutputRoot() {
369
+ if (cachedOutputRoot)
370
+ return cachedOutputRoot;
371
+ const configured = process.env.EXPLORBOT_OUTPUT;
372
+ if (!configured) {
373
+ cachedOutputRoot = mkdtempSync(join(tmpdir(), 'explorbot-'));
374
+ return cachedOutputRoot;
375
+ }
376
+ cachedOutputRoot = resolve(configured);
377
+ mkdirSync(cachedOutputRoot, { recursive: true });
378
+ return cachedOutputRoot;
379
+ }
380
+ export function materializeKnowledge(outputRoot) {
381
+ const inline = process.env.EXPLORBOT_KNOWLEDGE;
382
+ const knowledgeFile = process.env.EXPLORBOT_KNOWLEDGE_FILE;
383
+ if (!inline && !knowledgeFile)
384
+ return;
385
+ const knowledgeDir = join(outputRoot, 'knowledge');
386
+ mkdirSync(knowledgeDir, { recursive: true });
387
+ if (inline) {
388
+ writeFileSync(join(knowledgeDir, 'global.md'), matter.stringify(inline, { url: '*', endpoint: '*' }));
389
+ }
390
+ if (!knowledgeFile)
391
+ return;
392
+ const source = resolve(knowledgeFile);
393
+ if (!existsSync(source)) {
394
+ throw new Error(`Knowledge file from EXPLORBOT_KNOWLEDGE_FILE not found: ${source}`);
395
+ }
396
+ copyFileSync(source, join(knowledgeDir, basename(source)));
397
+ }
398
+ export async function createModel(provider, modelId) {
399
+ const factory = PROVIDERS[provider];
400
+ if (!factory) {
401
+ throw new Error(`Unknown AI provider "${provider}". Supported providers: ${Object.keys(PROVIDERS).join(', ')}`);
402
+ }
403
+ return (await factory())(modelId);
404
+ }
@@ -22,7 +22,7 @@ export declare class ExperienceTracker {
22
22
  experienceDir: string;
23
23
  disabled: boolean;
24
24
  knowledgeTracker: KnowledgeTracker;
25
- constructor(options?: {
25
+ constructor(knowledgeTracker: KnowledgeTracker, options?: {
26
26
  disabled?: boolean;
27
27
  });
28
28
  getExperienceDirectories(): string[];
@@ -31,8 +31,11 @@ export declare class ExperienceTracker {
31
31
  content: string;
32
32
  data: any;
33
33
  };
34
+ readExperienceFileAt(filePath: string): {
35
+ content: string;
36
+ data: any;
37
+ };
34
38
  writeExperienceFile(stateHash: string, content: string, frontmatter?: any): void;
35
- hasRecentExperience(stateHash: string, prefix?: string): boolean;
36
39
  getExperienceFilePath(stateHash: string): string;
37
40
  ensureExperienceFile(state: ActionResult): string;
38
41
  updateSummary(state: ActionResult, summary: string): void;
@@ -43,10 +46,6 @@ export declare class ExperienceTracker {
43
46
  getRelevantExperience(state: ActionResult, options?: {
44
47
  includeDescendantExperience?: boolean;
45
48
  }): ExperienceFile[];
46
- /**
47
- * Clean up experience tracker (for testing)
48
- */
49
- cleanup(): void;
50
49
  getSuccessfulExperience(state: ActionResult, options?: {
51
50
  includeDescendants?: boolean;
52
51
  stripCode?: boolean;
@@ -54,6 +53,7 @@ export declare class ExperienceTracker {
54
53
  getExperienceTableOfContents(state: ActionResult, options?: {
55
54
  includeDescendantExperience?: boolean;
56
55
  }): ExperienceTocEntry[];
56
+ renderExperienceTocFor(state: ActionResult): string;
57
57
  getExperienceSection(fileTag: string, sectionIndex: number, state: ActionResult, options?: {
58
58
  includeDescendantExperience?: boolean;
59
59
  }): {
@@ -61,6 +61,11 @@ export declare class ExperienceTracker {
61
61
  url: string;
62
62
  content: string;
63
63
  } | null;
64
+ buildToc(records: ExperienceFile[]): ExperienceTocEntry[];
65
+ readSectionByHash(fileHash: string, sectionIndex: number): {
66
+ title: string;
67
+ body: string;
68
+ } | null;
64
69
  findExperienceFileByHash(fileHash: string): string | null;
65
70
  listAllExperienceToc(filter?: string, options?: {
66
71
  recency?: 'recent' | 'old';
@@ -1,10 +1,9 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
- import { basename, dirname, join } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
3
  import matter from 'gray-matter';
4
- import { marked } from 'marked';
5
4
  import { ConfigParser } from './config.js';
6
- import { KnowledgeTracker } from './knowledge-tracker.js';
7
- import { createDebug, tag } from './utils/logger.js';
5
+ import { createDebug, pluralize, tag } from './utils/logger.js';
6
+ import { loadMarkdownFiles } from './utils/markdown-files.js';
8
7
  import { mdq } from './utils/markdown-query.js';
9
8
  import { redactSecrets } from './utils/secrets.js';
10
9
  import { isNonReusableCode } from "./utils/step-analyzer.js";
@@ -33,20 +32,13 @@ export class ExperienceTracker {
33
32
  experienceDir;
34
33
  disabled;
35
34
  knowledgeTracker;
36
- constructor(options = {}) {
35
+ constructor(knowledgeTracker, options = {}) {
37
36
  const configParser = ConfigParser.getInstance();
38
37
  const config = configParser.getConfig();
39
- const configPath = configParser.getConfigPath();
40
- this.disabled = options.disabled ?? false;
41
- this.knowledgeTracker = new KnowledgeTracker();
38
+ this.disabled = options.disabled ?? config.experience?.disabled ?? false;
39
+ this.knowledgeTracker = knowledgeTracker;
42
40
  // Resolve experience directory relative to the config file location (project root)
43
- if (configPath) {
44
- const projectRoot = dirname(configPath);
45
- this.experienceDir = join(projectRoot, config.dirs?.experience || 'experience');
46
- }
47
- else {
48
- this.experienceDir = config.dirs?.experience || 'experience';
49
- }
41
+ this.experienceDir = configParser.resolveProjectDir(config.dirs?.experience || 'experience');
50
42
  if (!this.disabled) {
51
43
  this.ensureDirectory(this.experienceDir);
52
44
  }
@@ -84,7 +76,9 @@ export class ExperienceTracker {
84
76
  }
85
77
  }
86
78
  readExperienceFile(stateHash) {
87
- const filePath = this.getExperienceFilePath(stateHash);
79
+ return this.readExperienceFileAt(this.getExperienceFilePath(stateHash));
80
+ }
81
+ readExperienceFileAt(filePath) {
88
82
  const fileContent = readFileSync(filePath, 'utf8');
89
83
  const { content, data } = matter(fileContent);
90
84
  return { content, data };
@@ -97,20 +91,6 @@ export class ExperienceTracker {
97
91
  const fileContent = matter.stringify(redactSecrets(content), frontmatter || {});
98
92
  writeFileSync(filePath, fileContent, 'utf8');
99
93
  }
100
- hasRecentExperience(stateHash, prefix = '') {
101
- if (this.disabled) {
102
- return false;
103
- }
104
- if (prefix) {
105
- stateHash = `${prefix}_${stateHash}`;
106
- }
107
- const filePath = this.getExperienceFilePath(stateHash);
108
- if (!existsSync(filePath)) {
109
- return false;
110
- }
111
- const stats = statSync(filePath);
112
- return stats.mtime.getTime() > Date.now() - 1000 * 60 * 60 * 24;
113
- }
114
94
  getExperienceFilePath(stateHash) {
115
95
  return join(this.experienceDir, `${stateHash}.md`);
116
96
  }
@@ -154,7 +134,10 @@ export class ExperienceTracker {
154
134
  this.ensureExperienceFile(state);
155
135
  const stateHash = state.getStateHash();
156
136
  const { content, data } = this.readExperienceFile(stateHash);
157
- if (content.includes(action.code)) {
137
+ if (mdq(content)
138
+ .query('code')
139
+ .meta()
140
+ .some((m) => m.text.trim() === action.code.trim())) {
158
141
  debugLog('Skipping duplicate action', action.code);
159
142
  return;
160
143
  }
@@ -179,7 +162,10 @@ export class ExperienceTracker {
179
162
  this.ensureExperienceFile(state);
180
163
  const stateHash = state.getStateHash();
181
164
  const { content, data } = this.readExperienceFile(stateHash);
182
- if (content.includes(body)) {
165
+ if (mdq(content)
166
+ .query('section2')
167
+ .each()
168
+ .some((s) => s.text().trim() === body.trim())) {
183
169
  debugLog('Skipping duplicate flow body');
184
170
  return;
185
171
  }
@@ -200,25 +186,9 @@ export class ExperienceTracker {
200
186
  continue;
201
187
  }
202
188
  try {
203
- const files = readdirSync(experienceDir)
204
- .filter((file) => file.endsWith('.md'))
205
- .map((file) => join(experienceDir, file));
206
- for (const file of files) {
207
- try {
208
- const content = readFileSync(file, 'utf8');
209
- const parsed = matter(content);
210
- const mtime = statSync(file).mtime;
211
- allFiles.push({
212
- filePath: file,
213
- data: parsed.data,
214
- content: parsed.content,
215
- mtime,
216
- });
217
- }
218
- catch (error) {
219
- debugLog(`Failed to read experience file ${file}:`, error);
220
- }
221
- }
189
+ allFiles.push(...loadMarkdownFiles(experienceDir, {
190
+ onError: (filePath, error) => debugLog(`Failed to read experience file ${filePath}:`, error),
191
+ }));
222
192
  }
223
193
  catch (error) {
224
194
  debugLog(`Failed to read experience directory ${experienceDir}:`, error);
@@ -242,19 +212,18 @@ export class ExperienceTracker {
242
212
  });
243
213
  })
244
214
  .map((experience) => {
245
- const lines = experience.content.split('\n');
246
- if (lines.length <= maxLines)
215
+ if (experience.content.split('\n').length <= maxLines)
247
216
  return experience;
248
- return { ...experience, content: lines.slice(0, maxLines).join('\n') };
217
+ let content = experience.content;
218
+ while (content.split('\n').length > maxLines) {
219
+ const sections = mdq(content).query('section2').each();
220
+ if (sections.length <= 1)
221
+ break;
222
+ content = sections[sections.length - 1].replace('');
223
+ }
224
+ return { ...experience, content };
249
225
  });
250
226
  }
251
- /**
252
- * Clean up experience tracker (for testing)
253
- */
254
- cleanup() {
255
- // Clear any in-memory state if needed
256
- // The actual files will be cleaned up by test cleanup
257
- }
258
227
  getSuccessfulExperience(state, options) {
259
228
  const records = this.getRelevantExperience(state, {
260
229
  includeDescendantExperience: options?.includeDescendants,
@@ -286,36 +255,48 @@ export class ExperienceTracker {
286
255
  const bHash = basename(b.filePath, '.md');
287
256
  return aHash.localeCompare(bHash);
288
257
  });
258
+ return this.buildToc(sorted);
259
+ }
260
+ renderExperienceTocFor(state) {
261
+ const toc = this.getExperienceTableOfContents(state);
262
+ if (toc.length === 0)
263
+ return '';
264
+ const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
265
+ debugLog(`injecting experience TOC (${toc.length} files, ${totalSections} sections)`);
266
+ tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections)`);
267
+ return renderExperienceToc(toc);
268
+ }
269
+ getExperienceSection(fileTag, sectionIndex, state, options) {
270
+ const toc = this.getExperienceTableOfContents(state, options);
271
+ const entry = toc.find((e) => e.fileTag === fileTag);
272
+ if (!entry)
273
+ return null;
274
+ const extracted = this.readSectionByHash(entry.fileHash, sectionIndex);
275
+ if (!extracted)
276
+ return null;
277
+ return { title: extracted.title, url: entry.url, content: extracted.body };
278
+ }
279
+ buildToc(records) {
289
280
  const toc = [];
290
- for (let i = 0; i < sorted.length; i++) {
291
- const record = sorted[i];
292
- const fileHash = basename(record.filePath, '.md');
293
- const url = record.data?.url || '';
281
+ for (const record of records) {
294
282
  const sections = listTocHeadings(record.content);
295
283
  if (sections.length === 0)
296
284
  continue;
297
285
  toc.push({
298
- fileTag: indexToLetters(i),
299
- fileHash,
300
- url,
286
+ fileTag: indexToLetters(toc.length),
287
+ fileHash: basename(record.filePath, '.md'),
288
+ url: record.data?.url || '',
301
289
  sections,
302
290
  });
303
291
  }
304
292
  return toc;
305
293
  }
306
- getExperienceSection(fileTag, sectionIndex, state, options) {
307
- const toc = this.getExperienceTableOfContents(state, options);
308
- const entry = toc.find((e) => e.fileTag === fileTag);
309
- if (!entry)
310
- return null;
311
- const filePath = this.findExperienceFileByHash(entry.fileHash);
294
+ readSectionByHash(fileHash, sectionIndex) {
295
+ const filePath = this.findExperienceFileByHash(fileHash);
312
296
  if (!filePath)
313
297
  return null;
314
- const { content } = this.readExperienceFile(entry.fileHash);
315
- const extracted = extractHeadingSection(content, sectionIndex);
316
- if (!extracted)
317
- return null;
318
- return { title: extracted.title, url: entry.url, content: extracted.body };
298
+ const { content } = this.readExperienceFileAt(filePath);
299
+ return extractHeadingSection(content, sectionIndex);
319
300
  }
320
301
  findExperienceFileByHash(fileHash) {
321
302
  for (const dir of this.getExperienceDirectories()) {
@@ -359,81 +340,34 @@ export class ExperienceTracker {
359
340
  const bUrl = b.data?.url || '';
360
341
  return aUrl.localeCompare(bUrl);
361
342
  });
362
- const toc = [];
363
- for (let i = 0; i < sorted.length; i++) {
364
- const record = sorted[i];
365
- const sections = listTocHeadings(record.content);
366
- if (sections.length === 0)
367
- continue;
368
- toc.push({
369
- fileTag: indexToLetters(toc.length),
370
- fileHash: basename(record.filePath, '.md'),
371
- url: record.data?.url || '',
372
- sections,
373
- });
374
- }
375
- return toc;
343
+ return this.buildToc(sorted);
376
344
  }
377
345
  getExperienceSectionByTag(fileTag, sectionIndex, filter) {
378
346
  const toc = this.listAllExperienceToc(filter);
379
347
  const entry = toc.find((e) => e.fileTag === fileTag);
380
348
  if (!entry)
381
349
  return null;
382
- const filePath = this.findExperienceFileByHash(entry.fileHash);
383
- if (!filePath)
384
- return null;
385
- const { content } = this.readExperienceFile(entry.fileHash);
386
- const extracted = extractHeadingSection(content, sectionIndex);
350
+ const extracted = this.readSectionByHash(entry.fileHash, sectionIndex);
387
351
  if (!extracted)
388
352
  return null;
389
353
  return { title: extracted.title, url: entry.url, content: extracted.body, fileHash: entry.fileHash };
390
354
  }
391
355
  }
392
356
  function listTocHeadings(content) {
393
- const tokens = marked.lexer(content);
394
- const result = [];
395
- let index = 0;
396
- for (const token of tokens) {
397
- if (token.type !== 'heading')
398
- continue;
399
- const heading = token;
400
- if (heading.depth !== 2 && heading.depth !== 3)
401
- continue;
402
- index++;
403
- result.push({ index, level: heading.depth, title: heading.text });
404
- }
405
- return result;
357
+ return mdq(content)
358
+ .query('heading')
359
+ .meta()
360
+ .filter((m) => m.depth === 2 || m.depth === 3)
361
+ .map((m, i) => ({ index: i + 1, level: m.depth, title: m.text }));
406
362
  }
407
363
  function extractHeadingSection(content, sectionIndex) {
408
- const tokens = marked.lexer(content);
409
- const matching = [];
410
- for (let i = 0; i < tokens.length; i++) {
411
- const token = tokens[i];
412
- if (token.type !== 'heading')
413
- continue;
414
- const heading = token;
415
- if (heading.depth !== 2 && heading.depth !== 3)
416
- continue;
417
- matching.push({ tokenIdx: i, depth: heading.depth, text: heading.text });
418
- }
364
+ const sections = mdq(content).query('section').each();
365
+ const metas = mdq(content).query('section').meta();
366
+ const matching = metas.map((meta, i) => ({ meta, section: sections[i] })).filter((entry) => entry.meta.depth === 2 || entry.meta.depth === 3);
419
367
  if (sectionIndex < 1 || sectionIndex > matching.length)
420
368
  return null;
421
369
  const target = matching[sectionIndex - 1];
422
- let endTokenIdx = tokens.length;
423
- for (let j = target.tokenIdx + 1; j < tokens.length; j++) {
424
- const token = tokens[j];
425
- if (token.type !== 'heading')
426
- continue;
427
- if (token.depth <= target.depth) {
428
- endTokenIdx = j;
429
- break;
430
- }
431
- }
432
- const body = tokens
433
- .slice(target.tokenIdx, endTokenIdx)
434
- .map((t) => t.raw || '')
435
- .join('');
436
- return { title: target.text, body };
370
+ return { title: target.meta.text, body: target.section.text() };
437
371
  }
438
372
  function indexToLetters(index) {
439
373
  let n = index;
@@ -499,21 +433,14 @@ function generateActionContent(title, code, explanation) {
499
433
  return lines.join('\n');
500
434
  }
501
435
  function renderAsHowTo(content) {
502
- const tokens = marked.lexer(content);
503
- let result = '';
504
- for (const token of tokens) {
505
- if (token.type === 'heading' && token.depth === 2) {
506
- const text = token.text.trim();
507
- if (text.startsWith('FLOW:')) {
508
- result += `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
509
- continue;
510
- }
511
- if (text.startsWith('ACTION:')) {
512
- result += `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
513
- continue;
514
- }
515
- }
516
- result += token.raw || '';
517
- }
518
- return result;
436
+ return mdq(content)
437
+ .query('h2')
438
+ .replaceEach((heading) => {
439
+ const text = heading.meta()[0]?.text.trim() || '';
440
+ if (text.startsWith('FLOW:'))
441
+ return `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
442
+ if (text.startsWith('ACTION:'))
443
+ return `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
444
+ return heading.text();
445
+ });
519
446
  }