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.
- package/README.md +35 -19
- package/bin/explorbot-cli.ts +33 -10
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +26 -28
- package/boat/doc-collector/src/ai/tools.ts +74 -16
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +30 -9
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +34 -2
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +29 -9
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
- package/dist/boat/doc-collector/src/ai/tools.js +53 -16
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +27 -8
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +30 -1
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/models.json +30 -0
- package/dist/package.json +23 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +13 -12
- package/dist/src/action.js +37 -113
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -29
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +17 -45
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +2 -17
- package/dist/src/ai/driller.js +26 -50
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +4 -3
- package/dist/src/ai/historian/playwright.d.ts +4 -1
- package/dist/src/ai/historian/playwright.js +4 -3
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +10 -8
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +8 -6
- package/dist/src/ai/navigator.js +34 -65
- package/dist/src/ai/pilot.d.ts +13 -8
- package/dist/src/ai/pilot.js +53 -59
- package/dist/src/ai/planner.d.ts +5 -4
- package/dist/src/ai/planner.js +66 -52
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -137
- package/dist/src/ai/quartermaster.d.ts +22 -22
- package/dist/src/ai/quartermaster.js +8 -14
- package/dist/src/ai/rerunner.d.ts +3 -13
- package/dist/src/ai/rerunner.js +23 -44
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +8 -11
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +7 -3
- package/dist/src/ai/researcher.js +40 -106
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +19 -6
- package/dist/src/ai/task-agent.js +37 -33
- package/dist/src/ai/tester.d.ts +6 -14
- package/dist/src/ai/tester.js +46 -111
- package/dist/src/ai/tools.d.ts +26 -14
- package/dist/src/ai/tools.js +138 -132
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +8 -6
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +2 -2
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +5 -3
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +3 -3
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +156 -10
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +82 -155
- package/dist/src/explorbot.d.ts +21 -13
- package/dist/src/explorbot.js +84 -80
- package/dist/src/explorer.d.ts +66 -102
- package/dist/src/explorer.js +332 -623
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +10 -30
- package/dist/src/state-manager.js +11 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +3 -1
- package/dist/src/utils/web-element.js +32 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/models.json +30 -0
- package/package.json +23 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +38 -132
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -30
- package/src/ai/captain.ts +17 -42
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +53 -83
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +5 -3
- package/src/ai/historian/playwright.ts +7 -4
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +6 -8
- package/src/ai/navigator.ts +39 -70
- package/src/ai/pilot.ts +59 -61
- package/src/ai/planner.ts +76 -62
- package/src/ai/provider.ts +85 -146
- package/src/ai/quartermaster.ts +39 -45
- package/src/ai/rerunner.ts +27 -50
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +10 -11
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +9 -3
- package/src/ai/researcher.ts +42 -119
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +45 -38
- package/src/ai/tester.ts +51 -129
- package/src/ai/tools.ts +145 -153
- package/src/command-handler.ts +1 -1
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +8 -6
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +2 -2
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +5 -3
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +3 -3
- package/src/config.ts +185 -11
- package/src/experience-tracker.ts +89 -152
- package/src/explorbot.ts +88 -83
- package/src/explorer.ts +402 -693
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +20 -155
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- package/src/utils/web-element.ts +34 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
|
@@ -10,7 +10,8 @@ import { type DocbotConfig, DocbotConfigParser } from './config.ts';
|
|
|
10
10
|
import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
|
|
11
11
|
import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
|
|
12
12
|
import { extractResearchNavigationTargets } from './research-navigation.ts';
|
|
13
|
-
import { type DocumentationScreenshot, captureDocumentationScreenshots } from './screenshots.ts';
|
|
13
|
+
import { type DocumentationScreenshot, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
|
+
import { renderMermaidBody } from './state-diagram.ts';
|
|
14
15
|
|
|
15
16
|
class DocBot {
|
|
16
17
|
private explorBot: ExplorBot;
|
|
@@ -42,7 +43,7 @@ class DocBot {
|
|
|
42
43
|
config: this.options.docsConfig,
|
|
43
44
|
path: this.options.path,
|
|
44
45
|
});
|
|
45
|
-
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer());
|
|
46
|
+
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer(), this.explorBot.stateManager());
|
|
46
47
|
this.ensureDirectory(this.configParser.getOutputDir());
|
|
47
48
|
this.ensureDirectory(this.getPagesDir());
|
|
48
49
|
}
|
|
@@ -75,7 +76,7 @@ class DocBot {
|
|
|
75
76
|
continue;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
const stateManager = this.explorBot.
|
|
79
|
+
const stateManager = this.explorBot.stateManager();
|
|
79
80
|
if (stateManager.hasVisitedState(target)) {
|
|
80
81
|
continue;
|
|
81
82
|
}
|
|
@@ -111,7 +112,17 @@ class DocBot {
|
|
|
111
112
|
screenshot: this.shouldUseScreenshots(),
|
|
112
113
|
force: true,
|
|
113
114
|
});
|
|
114
|
-
const
|
|
115
|
+
const pagePath = this.getPageFilePath(state.url);
|
|
116
|
+
const documentation = await this.documentarian.document(state, research, async (interactionState, transition) => {
|
|
117
|
+
if (!this.shouldUseScreenshots()) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return captureInteractionScreenshot(this.explorBot.getExplorer(), interactionState, transition, {
|
|
121
|
+
pageFilePath: pagePath,
|
|
122
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
123
|
+
config: this.config,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
115
126
|
const lowSignalReason = this.getLowSignalReason(documentation, research);
|
|
116
127
|
if (lowSignalReason) {
|
|
117
128
|
skipped.push({
|
|
@@ -134,6 +145,7 @@ class DocBot {
|
|
|
134
145
|
mightActions: documentation.might.map((item) => item.action),
|
|
135
146
|
interactionActions: (documentation.interactions || []).map((item) => item.action),
|
|
136
147
|
qualityNotes: documentation.qualityNotes || [],
|
|
148
|
+
interactions: documentation.interactions || [],
|
|
137
149
|
filePath,
|
|
138
150
|
});
|
|
139
151
|
documented.add(pageKey);
|
|
@@ -159,12 +171,13 @@ class DocBot {
|
|
|
159
171
|
}
|
|
160
172
|
}
|
|
161
173
|
|
|
162
|
-
const indexPath = this.saveIndex(effectiveStartPath, pages, skipped, effectiveMaxPages);
|
|
174
|
+
const { indexPath, diagramPath } = this.saveIndex(effectiveStartPath, pages, skipped, effectiveMaxPages);
|
|
163
175
|
|
|
164
176
|
return {
|
|
165
177
|
pages,
|
|
166
178
|
skipped,
|
|
167
179
|
indexPath,
|
|
180
|
+
diagramPath,
|
|
168
181
|
outputDir: this.configParser.getOutputDir(),
|
|
169
182
|
};
|
|
170
183
|
}
|
|
@@ -385,6 +398,10 @@ class DocBot {
|
|
|
385
398
|
return null;
|
|
386
399
|
}
|
|
387
400
|
|
|
401
|
+
if ((documentation.interactions || []).length > 0) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
388
405
|
const interactiveCount = this.countInteractiveElements(research);
|
|
389
406
|
if (interactiveCount >= minInteractiveElements) {
|
|
390
407
|
return null;
|
|
@@ -417,10 +434,13 @@ class DocBot {
|
|
|
417
434
|
});
|
|
418
435
|
}
|
|
419
436
|
|
|
420
|
-
private saveIndex(startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): string {
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
437
|
+
private saveIndex(startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): { indexPath: string; diagramPath: string } {
|
|
438
|
+
const outputDir = this.configParser.getOutputDir();
|
|
439
|
+
const indexPath = path.join(outputDir, 'index.md');
|
|
440
|
+
writeFileSync(indexPath, renderSpecIndex(outputDir, startPath, pages, skipped, maxPages), 'utf8');
|
|
441
|
+
const diagramPath = path.join(outputDir, 'state-diagram.mmd');
|
|
442
|
+
writeFileSync(diagramPath, renderMermaidBody(outputDir, pages), 'utf8');
|
|
443
|
+
return { indexPath, diagramPath };
|
|
424
444
|
}
|
|
425
445
|
|
|
426
446
|
private getPagesDir(): string {
|
|
@@ -461,6 +481,7 @@ interface CollectionResult {
|
|
|
461
481
|
pages: DocumentedPage[];
|
|
462
482
|
skipped: SkippedPage[];
|
|
463
483
|
indexPath: string;
|
|
484
|
+
diagramPath: string;
|
|
464
485
|
outputDir: string;
|
|
465
486
|
}
|
|
466
487
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import type
|
|
2
|
+
import { type WebPageState } from '../../../src/state-manager.ts';
|
|
3
3
|
import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
|
|
4
4
|
import type { DocumentationScreenshot } from './screenshots.ts';
|
|
5
|
+
import { buildStateGraph, renderMermaidFromGraph, renderStateMapFromGraph, type DocumentedPage, type SkippedPage } from './state-diagram.ts';
|
|
6
|
+
import { normalizeInlineText } from '../../../src/utils/strings.ts';
|
|
5
7
|
|
|
6
8
|
function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = []): string {
|
|
7
9
|
const lines: string[] = [];
|
|
@@ -48,6 +50,10 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
48
50
|
}
|
|
49
51
|
lines.push('');
|
|
50
52
|
}
|
|
53
|
+
if (transition.screenshot) {
|
|
54
|
+
lines.push(``);
|
|
55
|
+
lines.push('');
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -109,6 +115,18 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
|
|
|
109
115
|
lines.push(`Pages skipped: ${skipped.length}`);
|
|
110
116
|
lines.push(`Max pages: ${maxPages}`);
|
|
111
117
|
lines.push('');
|
|
118
|
+
const graph = buildStateGraph(outputDir, pages);
|
|
119
|
+
lines.push('## State Transitions');
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push(`\`\`\`mermaid\n${renderMermaidFromGraph(graph)}\n\`\`\``);
|
|
122
|
+
lines.push('');
|
|
123
|
+
const stateMap = renderStateMapFromGraph(graph);
|
|
124
|
+
if (stateMap) {
|
|
125
|
+
lines.push('### State Index');
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push(stateMap);
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
112
130
|
lines.push('## Pages');
|
|
113
131
|
lines.push('');
|
|
114
132
|
|
|
@@ -227,28 +245,5 @@ function ensureSentence(text: string): string {
|
|
|
227
245
|
return `${trimmed}.`;
|
|
228
246
|
}
|
|
229
247
|
|
|
230
|
-
function normalizeInlineText(text: string): string {
|
|
231
|
-
return text.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
interface DocumentedPage {
|
|
235
|
-
url: string;
|
|
236
|
-
title: string;
|
|
237
|
-
summary: string;
|
|
238
|
-
canCount: number;
|
|
239
|
-
mightCount: number;
|
|
240
|
-
interactionCount: number;
|
|
241
|
-
canActions: string[];
|
|
242
|
-
mightActions: string[];
|
|
243
|
-
interactionActions: string[];
|
|
244
|
-
qualityNotes: string[];
|
|
245
|
-
filePath: string;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
interface SkippedPage {
|
|
249
|
-
url: string;
|
|
250
|
-
reason: string;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
248
|
export { renderPageDocumentation, renderSpecIndex, ensureSentence, normalizeAction };
|
|
254
249
|
export type { DocumentedPage, SkippedPage, StateTransition };
|
|
@@ -3,13 +3,15 @@ import path from 'node:path';
|
|
|
3
3
|
import { parseResearchSections } from '../../../src/ai/researcher/parser.ts';
|
|
4
4
|
import type Explorer from '../../../src/explorer.ts';
|
|
5
5
|
import type { WebPageState } from '../../../src/state-manager.ts';
|
|
6
|
+
import { detectFocusArea } from '../../../src/utils/aria.ts';
|
|
6
7
|
import { safeFilename, sanitizeFilename } from '../../../src/utils/strings.ts';
|
|
8
|
+
import type { DocStateTransition } from './ai/tools.ts';
|
|
7
9
|
import type { DocbotConfig } from './config.ts';
|
|
8
10
|
|
|
9
11
|
const DEFAULT_MAX_SECTION_SCREENSHOTS = 8;
|
|
10
12
|
|
|
11
13
|
export async function captureDocumentationScreenshots(explorer: Explorer, state: WebPageState, research: string, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot[]> {
|
|
12
|
-
const page = explorer.
|
|
14
|
+
const page = explorer.page;
|
|
13
15
|
if (!page) {
|
|
14
16
|
return [];
|
|
15
17
|
}
|
|
@@ -59,6 +61,36 @@ export function getScreenshotSections(research: string): ScreenshotSection[] {
|
|
|
59
61
|
return sections;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
export async function captureInteractionScreenshot(explorer: Explorer, state: WebPageState, transition: DocStateTransition, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot | null> {
|
|
65
|
+
const page = explorer.page;
|
|
66
|
+
if (!page) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
71
|
+
const pageName = sanitizeFilename(state.url || 'page') || 'page';
|
|
72
|
+
const stateName = sanitizeFilename(transition.targetState?.label || transition.action) || 'state';
|
|
73
|
+
const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_${stateName}`, '.png'));
|
|
74
|
+
const focus = detectFocusArea(state.ariaSnapshot || null);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (focus.detected) {
|
|
78
|
+
await page.locator('[role="dialog"], [role="alertdialog"], [aria-modal="true"]').last().screenshot({ path: filePath });
|
|
79
|
+
} else {
|
|
80
|
+
await page.screenshot({ path: filePath });
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
title: transition.targetState?.label || transition.action,
|
|
88
|
+
path: filePath,
|
|
89
|
+
relativePath: toMarkdownPath(options.pageFilePath, filePath),
|
|
90
|
+
kind: 'state',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
async function captureFullPageScreenshot(page: any, pageName: string, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot | null> {
|
|
63
95
|
const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_page`, '.png'));
|
|
64
96
|
try {
|
|
@@ -110,7 +142,7 @@ export interface DocumentationScreenshot {
|
|
|
110
142
|
title: string;
|
|
111
143
|
path: string;
|
|
112
144
|
relativePath: string;
|
|
113
|
-
kind: 'page' | 'section';
|
|
145
|
+
kind: 'page' | 'section' | 'state';
|
|
114
146
|
selector?: string;
|
|
115
147
|
}
|
|
116
148
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { normalizeUrl } from '../../../src/state-manager.ts';
|
|
3
|
+
import { normalizeInlineText } from '../../../src/utils/strings.ts';
|
|
4
|
+
import type { StateTransition } from './ai/documentarian.ts';
|
|
5
|
+
|
|
6
|
+
function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph {
|
|
7
|
+
const pageIds = new Map<string, string>();
|
|
8
|
+
const pageNodes: StateNode[] = [];
|
|
9
|
+
const adjacency = new Map<string, Set<string>>();
|
|
10
|
+
const classAssignment = new Map<StateClass, string[]>([
|
|
11
|
+
['page', []],
|
|
12
|
+
['dialog', []],
|
|
13
|
+
['section', []],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
for (const [index, page] of pages.entries()) {
|
|
17
|
+
const id = `page${index}`;
|
|
18
|
+
pageIds.set(normalizeUrl(page.url), id);
|
|
19
|
+
adjacency.set(id, new Set());
|
|
20
|
+
classAssignment.get('page')?.push(id);
|
|
21
|
+
pageNodes.push({ id, kind: 'page', label: page.title || page.url, subLabel: page.url, filePath: page.filePath });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const stateKeys = new Map<string, string>();
|
|
25
|
+
const transientByPage = new Map<string, StateNode[]>();
|
|
26
|
+
const clicks: StateClick[] = [];
|
|
27
|
+
const edges: StateEdge[] = [];
|
|
28
|
+
const drawnBack = new Set<string>();
|
|
29
|
+
let stateIndex = 0;
|
|
30
|
+
|
|
31
|
+
for (const [pageIndex, page] of pages.entries()) {
|
|
32
|
+
const sourceId = `page${pageIndex}`;
|
|
33
|
+
for (const transition of page.interactions || []) {
|
|
34
|
+
const targetState = transition.targetState;
|
|
35
|
+
if (!targetState) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalizedTarget = normalizeUrl(targetState.url);
|
|
40
|
+
const isPageTarget = targetState.kind === 'page';
|
|
41
|
+
let pageTargetId: string | undefined;
|
|
42
|
+
if (isPageTarget) {
|
|
43
|
+
pageTargetId = pageIds.get(normalizedTarget);
|
|
44
|
+
}
|
|
45
|
+
let targetId: string | undefined;
|
|
46
|
+
if (pageTargetId && pageTargetId !== sourceId) {
|
|
47
|
+
targetId = pageTargetId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!targetId) {
|
|
51
|
+
const stateKey = `${sourceId}:${targetState.kind}:${normalizeInlineText(targetState.label)}:${normalizedTarget}`;
|
|
52
|
+
targetId = stateKeys.get(stateKey);
|
|
53
|
+
if (!targetId) {
|
|
54
|
+
targetId = `state${stateIndex++}`;
|
|
55
|
+
stateKeys.set(stateKey, targetId);
|
|
56
|
+
adjacency.set(targetId, new Set());
|
|
57
|
+
classAssignment.get(classForKind(targetState.kind))?.push(targetId);
|
|
58
|
+
const list = transientByPage.get(sourceId) ?? [];
|
|
59
|
+
list.push({ id: targetId, kind: targetState.kind, label: targetState.label, subLabel: targetState.kind, parentPageId: sourceId });
|
|
60
|
+
transientByPage.set(sourceId, list);
|
|
61
|
+
if (transition.screenshot) {
|
|
62
|
+
const screenshotPath = path.resolve(path.dirname(page.filePath), transition.screenshot.relativePath);
|
|
63
|
+
clicks.push({ node: targetId, target: path.relative(outputDir, screenshotPath).replaceAll('\\', '/'), tooltip: 'Open state screenshot' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!targetId) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pairKey = `${sourceId}>${targetId}`;
|
|
73
|
+
if (adjacency.get(targetId)?.has(sourceId)) {
|
|
74
|
+
if (drawnBack.has(pairKey)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
drawnBack.add(pairKey);
|
|
78
|
+
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: true });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (adjacency.get(sourceId)?.has(targetId) || createsCycle(sourceId, targetId, adjacency)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
adjacency.get(sourceId)?.add(targetId);
|
|
86
|
+
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: false });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const pageClicks: StateClick[] = [];
|
|
91
|
+
for (const pageNode of pageNodes) {
|
|
92
|
+
const filePath = pageNode.filePath;
|
|
93
|
+
if (!filePath) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const relativeFile = path.relative(outputDir, filePath).replaceAll('\\', '/');
|
|
97
|
+
pageClicks.push({ node: pageNode.id, target: relativeFile, tooltip: `Open ${pageNode.label}` });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { pages: pageNodes, transientByPage, edges, clicks: [...pageClicks, ...clicks], classAssignment };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderMermaidBody(outputDir: string, pages: DocumentedPage[]): string {
|
|
104
|
+
return renderMermaidFromGraph(buildStateGraph(outputDir, pages));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderMermaidFromGraph(graph: StateGraph): string {
|
|
108
|
+
const lines: string[] = ['flowchart TD'];
|
|
109
|
+
if (graph.pages.length === 0) {
|
|
110
|
+
lines.push(' empty["No documented states"]');
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const page of graph.pages) {
|
|
115
|
+
lines.push(` ${renderNodeLine(page)}`);
|
|
116
|
+
const children = graph.transientByPage.get(page.id);
|
|
117
|
+
if (!children || children.length === 0) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
lines.push(` subgraph sg_${page.id} ["${escapeMermaidLabel(page.label)} — transient states"]`);
|
|
121
|
+
for (const child of children) {
|
|
122
|
+
lines.push(` ${renderNodeLine(child)}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push(' end');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const edge of graph.edges) {
|
|
128
|
+
let arrow = '-->';
|
|
129
|
+
if (edge.isBack) {
|
|
130
|
+
arrow = '-.->';
|
|
131
|
+
}
|
|
132
|
+
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push(' classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a;');
|
|
136
|
+
lines.push(' classDef dialog fill:#ffedd5,stroke:#ea580c,color:#0f172a;');
|
|
137
|
+
lines.push(' classDef section fill:#f3e8ff,stroke:#9333ea,color:#0f172a;');
|
|
138
|
+
for (const [className, ids] of graph.classAssignment) {
|
|
139
|
+
if (ids.length === 0) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
lines.push(` class ${ids.join(',')} ${className};`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const click of graph.clicks) {
|
|
146
|
+
lines.push(` click ${click.node} "${click.target}" "${escapeMermaidLabel(click.tooltip)}"`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderStateMapFromGraph(graph: StateGraph): string {
|
|
153
|
+
if (graph.pages.length === 0) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
const clickByNode = new Map(graph.clicks.map((click) => [click.node, click]));
|
|
157
|
+
const rows = ['| State | Type | Open |', '| --- | --- | --- |'];
|
|
158
|
+
for (const pageNode of graph.pages) {
|
|
159
|
+
const pageClick = clickByNode.get(pageNode.id);
|
|
160
|
+
let openCell = '—';
|
|
161
|
+
if (pageClick) {
|
|
162
|
+
openCell = `[open page](${pageClick.target})`;
|
|
163
|
+
}
|
|
164
|
+
rows.push(`| ${escapeTable(pageNode.label)} | page | ${openCell} |`);
|
|
165
|
+
for (const child of graph.transientByPage.get(pageNode.id) ?? []) {
|
|
166
|
+
const childClick = clickByNode.get(child.id);
|
|
167
|
+
let childCell = '—';
|
|
168
|
+
if (childClick) {
|
|
169
|
+
childCell = `[view screenshot](${childClick.target})`;
|
|
170
|
+
}
|
|
171
|
+
rows.push(`| ${escapeTable(child.label)} | ${child.kind} | ${childCell} |`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return rows.join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderNodeLine(node: StateNode): string {
|
|
178
|
+
const label = `${escapeMermaidLabel(node.label)}<br/>${escapeMermaidLabel(node.subLabel)}`;
|
|
179
|
+
if (node.kind === 'dialog' || node.kind === 'modal') {
|
|
180
|
+
return `${node.id}{{"${label}"}}`;
|
|
181
|
+
}
|
|
182
|
+
if (node.kind === 'section') {
|
|
183
|
+
return `${node.id}("${label}")`;
|
|
184
|
+
}
|
|
185
|
+
return `${node.id}["${label}"]`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createsCycle(sourceId: string, targetId: string, adjacency: Map<string, Set<string>>): boolean {
|
|
189
|
+
if (sourceId === targetId) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const pending = [targetId];
|
|
194
|
+
const visited = new Set<string>();
|
|
195
|
+
while (pending.length > 0) {
|
|
196
|
+
const nodeId = pending.pop();
|
|
197
|
+
if (!nodeId || visited.has(nodeId)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (nodeId === sourceId) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
visited.add(nodeId);
|
|
204
|
+
pending.push(...(adjacency.get(nodeId) || []));
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function escapeMermaidLabel(value: string): string {
|
|
210
|
+
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('|', '|');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function escapeTable(value: string): string {
|
|
214
|
+
return normalizeInlineText(value).replaceAll('|', '\\|');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function classForKind(kind: StateKind): StateClass {
|
|
218
|
+
if (kind === 'section') {
|
|
219
|
+
return 'section';
|
|
220
|
+
}
|
|
221
|
+
if (kind === 'page') {
|
|
222
|
+
return 'page';
|
|
223
|
+
}
|
|
224
|
+
return 'dialog';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface DocumentedPage {
|
|
228
|
+
url: string;
|
|
229
|
+
title: string;
|
|
230
|
+
summary: string;
|
|
231
|
+
canCount: number;
|
|
232
|
+
mightCount: number;
|
|
233
|
+
interactionCount: number;
|
|
234
|
+
canActions: string[];
|
|
235
|
+
mightActions: string[];
|
|
236
|
+
interactionActions: string[];
|
|
237
|
+
qualityNotes: string[];
|
|
238
|
+
interactions?: StateTransition[];
|
|
239
|
+
filePath: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface SkippedPage {
|
|
243
|
+
url: string;
|
|
244
|
+
reason: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type StateKind = 'page' | 'dialog' | 'modal' | 'section';
|
|
248
|
+
type StateClass = 'page' | 'dialog' | 'section';
|
|
249
|
+
|
|
250
|
+
interface StateNode {
|
|
251
|
+
id: string;
|
|
252
|
+
kind: StateKind;
|
|
253
|
+
label: string;
|
|
254
|
+
subLabel: string;
|
|
255
|
+
filePath?: string;
|
|
256
|
+
parentPageId?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface StateEdge {
|
|
260
|
+
source: string;
|
|
261
|
+
target: string;
|
|
262
|
+
action: string;
|
|
263
|
+
isBack: boolean;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface StateClick {
|
|
267
|
+
node: string;
|
|
268
|
+
target: string;
|
|
269
|
+
tooltip: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface StateGraph {
|
|
273
|
+
pages: StateNode[];
|
|
274
|
+
transientByPage: Map<string, StateNode[]>;
|
|
275
|
+
edges: StateEdge[];
|
|
276
|
+
clicks: StateClick[];
|
|
277
|
+
classAssignment: Map<StateClass, string[]>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderStateMapFromGraph };
|
|
281
|
+
export type { DocumentedPage, SkippedPage, StateGraph, StateNode, StateEdge, StateClick };
|
|
@@ -9,7 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
-
import { ConfigParser } from '../src/config.js';
|
|
12
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
13
|
import { ExplorBot } from '../src/explorbot.js';
|
|
14
14
|
import { Stats } from '../src/stats.js';
|
|
15
15
|
import { Plan } from '../src/test-plan.js';
|
|
@@ -27,7 +27,6 @@ if (!process.env.EXPLORBOT_NO_BANNER) {
|
|
|
27
27
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
28
28
|
}
|
|
29
29
|
function buildExplorBotOptions(from, options) {
|
|
30
|
-
const sessionFile = options.session === true ? path.join(path.resolve(options.path || process.cwd()), 'output', 'session.json') : options.session;
|
|
31
30
|
return {
|
|
32
31
|
from,
|
|
33
32
|
verbose: options.verbose || options.debug,
|
|
@@ -36,7 +35,7 @@ function buildExplorBotOptions(from, options) {
|
|
|
36
35
|
show: options.show,
|
|
37
36
|
headless: options.headless,
|
|
38
37
|
incognito: options.incognito,
|
|
39
|
-
session:
|
|
38
|
+
session: options.session,
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
41
|
function addCommonOptions(cmd) {
|
|
@@ -454,16 +453,16 @@ program
|
|
|
454
453
|
await ConfigParser.getInstance().loadConfig({
|
|
455
454
|
path: options.path || process.cwd(),
|
|
456
455
|
});
|
|
456
|
+
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
457
|
+
const tracker = new KnowledgeTracker();
|
|
457
458
|
if (url && description) {
|
|
458
|
-
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
459
|
-
const tracker = new KnowledgeTracker();
|
|
460
459
|
const result = tracker.addKnowledge(url, description);
|
|
461
460
|
const action = result.isNewFile ? 'Created' : 'Updated';
|
|
462
461
|
console.log(`Knowledge ${action} in: ${result.filename}`);
|
|
463
462
|
return;
|
|
464
463
|
}
|
|
465
464
|
const AddKnowledge = (await import('../src/components/AddKnowledge.js')).default;
|
|
466
|
-
render(React.createElement(AddKnowledge, { initialUrl: url || '' }), {
|
|
465
|
+
render(React.createElement(AddKnowledge, { initialUrl: url || '', knowledgeTracker: tracker }), {
|
|
467
466
|
exitOnCtrlC: false,
|
|
468
467
|
patchConsole: false,
|
|
469
468
|
});
|
|
@@ -623,7 +622,7 @@ program
|
|
|
623
622
|
path: options.path,
|
|
624
623
|
config: options.config,
|
|
625
624
|
headless: true,
|
|
626
|
-
session: options.session
|
|
625
|
+
session: options.session,
|
|
627
626
|
};
|
|
628
627
|
const explorBot = new ExplorBot(mainOptions);
|
|
629
628
|
await explorBot.start();
|
|
@@ -652,10 +651,10 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
652
651
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
653
652
|
await explorBot.start();
|
|
654
653
|
await explorBot.agentNavigator().visit(url);
|
|
655
|
-
const action = explorBot.getExplorer().
|
|
654
|
+
const action = explorBot.getExplorer().action();
|
|
656
655
|
await action.execute(command);
|
|
657
656
|
log('Command executed successfully');
|
|
658
|
-
const state = explorBot.
|
|
657
|
+
const state = explorBot.stateManager().getCurrentState();
|
|
659
658
|
if (state)
|
|
660
659
|
log(`URL: ${state.url}`);
|
|
661
660
|
await explorBot.stop();
|
|
@@ -793,4 +792,25 @@ import { createApiCommands } from "../boat/api-tester/src/cli.js";
|
|
|
793
792
|
import { createDocsCommands } from "../boat/doc-collector/src/cli.js";
|
|
794
793
|
program.addCommand(createApiCommands('api'));
|
|
795
794
|
program.addCommand(createDocsCommands('docs'));
|
|
795
|
+
const envHelp = () => {
|
|
796
|
+
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
797
|
+
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
|
|
798
|
+
return `
|
|
799
|
+
Environment variables (config-free one-liner mode):
|
|
800
|
+
Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
|
|
801
|
+
|
|
802
|
+
${rows}
|
|
803
|
+
|
|
804
|
+
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
805
|
+
Example:
|
|
806
|
+
EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
|
|
807
|
+
${cli} explore /login --max-tests 3
|
|
808
|
+
`;
|
|
809
|
+
};
|
|
810
|
+
const addEnvHelp = (cmd) => {
|
|
811
|
+
cmd.addHelpText('after', envHelp);
|
|
812
|
+
for (const sub of cmd.commands)
|
|
813
|
+
addEnvHelp(sub);
|
|
814
|
+
};
|
|
815
|
+
addEnvHelp(program);
|
|
796
816
|
program.parse();
|
|
@@ -9,7 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
10
|
import path, { resolve } from 'node:path';
|
|
11
11
|
import { parseEnv } from 'node:util';
|
|
12
|
-
import { EXPLORBOT_CONFIG_PATHS } from "../../../src/config.js";
|
|
12
|
+
import { EXPLORBOT_CONFIG_PATHS, createModel, materializeKnowledge, resolveModel, resolveOutputRoot } from "../../../src/config.js";
|
|
13
13
|
export class ApibotConfigParser {
|
|
14
14
|
static instance;
|
|
15
15
|
config = null;
|
|
@@ -37,9 +37,13 @@ export class ApibotConfigParser {
|
|
|
37
37
|
ApibotConfigParser.loadEnv('.env');
|
|
38
38
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
39
39
|
if (!resolvedPath) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
try {
|
|
41
|
+
return await this.loadEnvConfig();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (options?.path && originalCwd !== process.cwd())
|
|
45
|
+
process.chdir(originalCwd);
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
48
|
try {
|
|
45
49
|
const configModule = await this.loadConfigModule(resolvedPath);
|
|
@@ -98,6 +102,41 @@ export class ApibotConfigParser {
|
|
|
98
102
|
mkdirSync(dirPath, { recursive: true });
|
|
99
103
|
}
|
|
100
104
|
}
|
|
105
|
+
async loadEnvConfig() {
|
|
106
|
+
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
107
|
+
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
108
|
+
if (!provider && !modelSpec) {
|
|
109
|
+
throw new Error('No configuration file found. Create apibot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
|
|
110
|
+
}
|
|
111
|
+
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
112
|
+
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
113
|
+
}
|
|
114
|
+
const baseEndpoint = process.env.EXPLORBOT_URL;
|
|
115
|
+
if (!baseEndpoint) {
|
|
116
|
+
throw new Error('No API endpoint to test. Set EXPLORBOT_URL to the API base endpoint');
|
|
117
|
+
}
|
|
118
|
+
const outputRoot = resolveOutputRoot();
|
|
119
|
+
materializeKnowledge(outputRoot);
|
|
120
|
+
const api = { baseEndpoint };
|
|
121
|
+
if (process.env.EXPLORBOT_API_SPEC) {
|
|
122
|
+
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
123
|
+
}
|
|
124
|
+
let model;
|
|
125
|
+
if (provider && modelSpec)
|
|
126
|
+
model = await createModel(provider, modelSpec);
|
|
127
|
+
if (provider && !modelSpec)
|
|
128
|
+
model = await resolveModel(provider, 'model');
|
|
129
|
+
if (!provider)
|
|
130
|
+
model = await resolveModel(modelSpec, 'model');
|
|
131
|
+
this.config = {
|
|
132
|
+
ai: { model },
|
|
133
|
+
api,
|
|
134
|
+
dirs: { output: '.', knowledge: 'knowledge' },
|
|
135
|
+
};
|
|
136
|
+
this.configPath = path.join(outputRoot, 'apibot.config.js');
|
|
137
|
+
this.validateConfig(this.config);
|
|
138
|
+
return this.config;
|
|
139
|
+
}
|
|
101
140
|
findConfigFile() {
|
|
102
141
|
const apibotPaths = ['apibot.config.js', 'apibot.config.mjs', 'apibot.config.ts'];
|
|
103
142
|
for (const p of apibotPaths) {
|