explorbot 0.4.1 → 0.4.3
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/bin/explorbot-cli.ts +14 -4
- package/boat/api-tester/src/ai/curler-tools.ts +1 -1
- package/boat/api-tester/src/cli.ts +3 -0
- package/boat/doc-collector/src/ai/documentarian.ts +57 -5
- package/boat/doc-collector/src/ai/tools.ts +1 -1
- package/boat/doc-collector/src/cli.ts +18 -2
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +56 -11
- package/boat/doc-collector/src/docs-renderer.ts +79 -55
- package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
- package/boat/doc-collector/src/screenshots.ts +106 -3
- package/boat/doc-collector/src/template-dedup.ts +31 -0
- package/boat/prima/src/cli.ts +3 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +14 -4
- package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
- package/dist/boat/api-tester/src/cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
- package/dist/boat/doc-collector/src/ai/tools.js +1 -1
- package/dist/boat/doc-collector/src/cli.js +15 -2
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +52 -10
- package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
- package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
- package/dist/boat/doc-collector/src/screenshots.js +104 -1
- package/dist/boat/doc-collector/src/template-dedup.js +27 -0
- package/dist/boat/prima/src/cli.js +2 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +2 -2
- package/dist/src/action-result.js +15 -17
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +15 -15
- package/dist/src/ai/fisherman/request-haul.d.ts +12 -0
- package/dist/src/ai/fisherman/request-haul.js +31 -0
- package/dist/src/ai/fisherman/tools.d.ts +50 -0
- package/dist/src/ai/{fisherman-tools.js → fisherman/tools.js} +109 -40
- package/dist/src/ai/fisherman.d.ts +14 -4
- package/dist/src/ai/fisherman.js +96 -18
- package/dist/src/ai/pilot.d.ts +13 -1
- package/dist/src/ai/pilot.js +40 -22
- package/dist/src/ai/provider.js +8 -14
- package/dist/src/ai/researcher/coordinates.d.ts +2 -0
- package/dist/src/ai/researcher/coordinates.js +13 -1
- package/dist/src/ai/researcher/deep-analysis.js +9 -9
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +10 -0
- package/dist/src/ai/tester.js +20 -20
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +18 -74
- package/dist/src/api/request-result.js +3 -1
- package/dist/src/api/request-store.d.ts +7 -6
- package/dist/src/api/request-store.js +65 -37
- package/dist/src/api/xhr-capture.d.ts +2 -0
- package/dist/src/api/xhr-capture.js +35 -10
- package/dist/src/commands/config-command.js +2 -9
- package/dist/src/commands/help-json-command.d.ts +31 -0
- package/dist/src/commands/help-json-command.js +58 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- package/dist/src/commands/plans-command.d.ts +4 -1
- package/dist/src/commands/plans-command.js +4 -4
- package/dist/src/commands/recommended-models-command.d.ts +9 -0
- package/dist/src/commands/recommended-models-command.js +85 -0
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +7 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +3 -3
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- package/dist/src/state-manager.d.ts +3 -3
- package/dist/src/state-manager.js +8 -8
- package/dist/src/test-plan.d.ts +1 -0
- package/dist/src/test-plan.js +13 -9
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +23 -2
- package/dist/src/utils/cli-section.d.ts +1 -0
- package/dist/src/utils/cli-section.js +12 -0
- package/dist/src/utils/html-diff.d.ts +1 -0
- package/dist/src/utils/html-diff.js +5 -5
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/html.js +5 -4
- package/dist/src/utils/overlay.d.ts +19 -55
- package/dist/src/utils/overlay.js +94 -148
- package/dist/src/utils/region.d.ts +41 -0
- package/dist/src/utils/region.js +99 -0
- package/dist/src/utils/similarity.d.ts +1 -0
- package/dist/src/utils/similarity.js +11 -0
- package/dist/src/utils/url-matcher.d.ts +1 -1
- package/dist/src/utils/url-matcher.js +4 -4
- package/docs/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +4 -0
- package/docs/reference/configuration.md +4 -0
- package/docs/superpowers/plans/2026-09-03-fisherman-query-api.md +1361 -0
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/agentic-usage.md +12 -0
- package/docs/workflow/application-spec.md +4 -0
- package/package.json +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- package/src/ai/fisherman/request-haul.ts +36 -0
- package/src/ai/{fisherman-tools.ts → fisherman/tools.ts} +125 -46
- package/src/ai/fisherman.ts +110 -20
- package/src/ai/pilot.ts +38 -20
- package/src/ai/provider.ts +8 -12
- package/src/ai/researcher/coordinates.ts +13 -1
- package/src/ai/researcher/deep-analysis.ts +9 -9
- package/src/ai/researcher/fingerprint-worker.ts +1 -10
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +10 -0
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-result.ts +2 -1
- package/src/api/request-store.ts +68 -42
- package/src/api/xhr-capture.ts +39 -11
- package/src/commands/config-command.ts +2 -7
- package/src/commands/help-json-command.ts +74 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- package/src/commands/plans-command.ts +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +9 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +3 -3
- package/src/global-config.ts +8 -0
- package/src/state-manager.ts +10 -9
- package/src/test-plan.ts +12 -10
- package/src/utils/aria.ts +26 -3
- package/src/utils/cli-section.ts +12 -0
- package/src/utils/html-diff.ts +6 -6
- package/src/utils/html.ts +6 -4
- package/src/utils/overlay.ts +105 -192
- package/src/utils/region.ts +132 -0
- package/src/utils/similarity.ts +9 -0
- package/src/utils/url-matcher.ts +3 -3
- package/dist/src/ai/fisherman-tools.d.ts +0 -138
- package/dist/src/utils/request-map.d.ts +0 -7
- package/dist/src/utils/request-map.js +0 -16
- package/src/utils/request-map.ts +0 -19
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { PNG } from 'pngjs';
|
|
4
|
+
import { removeVisualAnnotations } from '../../../src/ai/researcher/coordinates.ts';
|
|
3
5
|
import { parseResearchSections } from '../../../src/ai/researcher/parser.ts';
|
|
6
|
+
import { parseAriaLocator } from '../../../src/utils/aria.ts';
|
|
4
7
|
import type Explorer from '../../../src/explorer.ts';
|
|
5
|
-
import type
|
|
8
|
+
import { type WebPageState, normalizeUrl } from '../../../src/state-manager.ts';
|
|
6
9
|
import { safeFilename, sanitizeFilename } from '../../../src/utils/strings.ts';
|
|
10
|
+
import type { PageDocumentation } from './ai/documentarian.ts';
|
|
7
11
|
import type { DocStateTransition } from './ai/tools.ts';
|
|
8
12
|
import type { DocbotConfig } from './config.ts';
|
|
9
|
-
import { captureInteractionAfter, captureInteractionBefore } from './interaction-screenshots.ts';
|
|
13
|
+
import { addPadding, captureInteractionAfter, captureInteractionBefore, regionAround, saveRegion } from './interaction-screenshots.ts';
|
|
10
14
|
|
|
11
15
|
const DEFAULT_MAX_SECTION_SCREENSHOTS = 8;
|
|
16
|
+
const EVIDENCE_PADDING = 250;
|
|
12
17
|
|
|
13
18
|
export async function captureDocumentationScreenshots(explorer: Explorer, state: WebPageState, research: string, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot[]> {
|
|
14
19
|
const page = explorer.page;
|
|
@@ -16,6 +21,8 @@ export async function captureDocumentationScreenshots(explorer: Explorer, state:
|
|
|
16
21
|
return [];
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
await dismissTransientOverlay(page);
|
|
25
|
+
await removeVisualAnnotations(page);
|
|
19
26
|
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
20
27
|
|
|
21
28
|
const screenshots: DocumentationScreenshot[] = [];
|
|
@@ -37,6 +44,101 @@ export async function captureDocumentationScreenshots(explorer: Explorer, state:
|
|
|
37
44
|
return screenshots;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
export async function captureEvidenceScreenshots(explorer: Explorer, state: WebPageState, documentation: PageDocumentation, options: DocumentationScreenshotOptions): Promise<Array<DocumentationScreenshot | null>> {
|
|
48
|
+
const page = explorer.page;
|
|
49
|
+
if (!page) {
|
|
50
|
+
return documentation.can.map(() => null);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await dismissTransientOverlay(page);
|
|
54
|
+
await removeVisualAnnotations(page);
|
|
55
|
+
if (!isOnDocumentedPage(page, state)) {
|
|
56
|
+
return documentation.can.map(() => null);
|
|
57
|
+
}
|
|
58
|
+
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
59
|
+
const pageName = sanitizeFilename(state.url || 'page') || 'page';
|
|
60
|
+
const shots: Array<DocumentationScreenshot | null> = [];
|
|
61
|
+
for (const [index, item] of documentation.can.entries()) {
|
|
62
|
+
shots.push(await captureEvidenceScreenshot(page, pageName, index, item, options));
|
|
63
|
+
}
|
|
64
|
+
return shots;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function captureEvidenceScreenshot(page: any, pageName: string, index: number, item: { action: string; element?: string | null }, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot | null> {
|
|
68
|
+
if (!item.element) return null;
|
|
69
|
+
const element = resolveResearchLocator(item.element, options.research);
|
|
70
|
+
if (!element) return null;
|
|
71
|
+
const locatorInfo = parseAriaLocator(element);
|
|
72
|
+
try {
|
|
73
|
+
const locator = resolveEvidenceLocator(page, element, locatorInfo);
|
|
74
|
+
if ((await locator.count()) !== 1) return null;
|
|
75
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 2000 });
|
|
76
|
+
const box = await locator.boundingBox();
|
|
77
|
+
if (!box || box.width <= 0 || box.height <= 0) return null;
|
|
78
|
+
const image = await page.screenshot();
|
|
79
|
+
const png = PNG.sync.read(image);
|
|
80
|
+
const region = regionAround(png, box, await page.viewportSize(), EVIDENCE_PADDING);
|
|
81
|
+
const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_can_${index + 1}`, '.png'));
|
|
82
|
+
saveRegion(png, region, filePath);
|
|
83
|
+
return {
|
|
84
|
+
title: item.action,
|
|
85
|
+
path: filePath,
|
|
86
|
+
relativePath: toMarkdownPath(options.pageFilePath, filePath),
|
|
87
|
+
kind: 'evidence',
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveResearchLocator(requested: string, research?: string): string | null {
|
|
95
|
+
if (!research) return requested;
|
|
96
|
+
const requestedAria = parseAriaLocator(requested);
|
|
97
|
+
const requestedCss = unwrapLocator(requested);
|
|
98
|
+
for (const section of parseResearchSections(research)) {
|
|
99
|
+
for (const element of section.elements) {
|
|
100
|
+
if (requestedAria && element.aria?.role === requestedAria.role && element.aria.text === requestedAria.text) {
|
|
101
|
+
return `{ role: '${element.aria.role}', text: '${element.aria.text}' }`;
|
|
102
|
+
}
|
|
103
|
+
if (element.css && requestedCss === unwrapLocator(element.css)) return element.css;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function unwrapLocator(locator: string): string {
|
|
110
|
+
const trimmed = locator.trim();
|
|
111
|
+
const first = trimmed.at(0);
|
|
112
|
+
const last = trimmed.at(-1);
|
|
113
|
+
if (first === last && (first === '`' || first === '"' || first === "'")) {
|
|
114
|
+
return trimmed.slice(1, -1).trim();
|
|
115
|
+
}
|
|
116
|
+
return trimmed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isOnDocumentedPage(page: any, state: WebPageState): boolean {
|
|
120
|
+
try {
|
|
121
|
+
const current = new URL(page.url());
|
|
122
|
+
const expected = new URL(state.url || '', current.origin);
|
|
123
|
+
return current.origin === expected.origin && normalizeUrl(current.href) === normalizeUrl(expected.href);
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveEvidenceLocator(page: any, element: string, locatorInfo: { role: string; text: string } | null): any {
|
|
130
|
+
if (locatorInfo?.text) {
|
|
131
|
+
return page.getByRole(locatorInfo.role, { name: locatorInfo.text, exact: true });
|
|
132
|
+
}
|
|
133
|
+
return page.locator(element);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function dismissTransientOverlay(page: any): Promise<void> {
|
|
137
|
+
try {
|
|
138
|
+
await page.keyboard.press('Escape');
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
40
142
|
export function getScreenshotSections(research: string): ScreenshotSection[] {
|
|
41
143
|
const sections: ScreenshotSection[] = [];
|
|
42
144
|
const seen = new Set<string>();
|
|
@@ -150,7 +252,7 @@ export interface DocumentationScreenshot {
|
|
|
150
252
|
title: string;
|
|
151
253
|
path: string;
|
|
152
254
|
relativePath: string;
|
|
153
|
-
kind: 'page' | 'section' | 'state';
|
|
255
|
+
kind: 'page' | 'section' | 'state' | 'evidence';
|
|
154
256
|
selector?: string;
|
|
155
257
|
}
|
|
156
258
|
|
|
@@ -158,6 +260,7 @@ interface DocumentationScreenshotOptions {
|
|
|
158
260
|
pageFilePath: string;
|
|
159
261
|
screenshotsDir: string;
|
|
160
262
|
config: DocbotConfig;
|
|
263
|
+
research?: string;
|
|
161
264
|
}
|
|
162
265
|
|
|
163
266
|
interface ScreenshotSection {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ariaTemplateSignature } from '../../../src/utils/aria.ts';
|
|
2
|
+
import { diceSimilarity } from '../../../src/utils/similarity.ts';
|
|
3
|
+
|
|
4
|
+
const TEMPLATE_MIN_SIGNATURE_SIZE = 8;
|
|
5
|
+
const TEMPLATE_SIMILARITY_THRESHOLD = 90;
|
|
6
|
+
|
|
7
|
+
export function buildTemplateRecord(url: string, snapshot: string | null): TemplateRecord | null {
|
|
8
|
+
const signature = ariaTemplateSignature(snapshot);
|
|
9
|
+
if (signature.size < TEMPLATE_MIN_SIGNATURE_SIZE) return null;
|
|
10
|
+
return { url, signature };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function findTemplateMatch(snapshot: string | null, known: TemplateRecord[], threshold: number = TEMPLATE_SIMILARITY_THRESHOLD): string | null {
|
|
14
|
+
const signature = ariaTemplateSignature(snapshot);
|
|
15
|
+
if (signature.size < TEMPLATE_MIN_SIGNATURE_SIZE) return null;
|
|
16
|
+
let bestUrl: string | null = null;
|
|
17
|
+
let bestScore = 0;
|
|
18
|
+
for (const record of known) {
|
|
19
|
+
const score = diceSimilarity(signature, record.signature);
|
|
20
|
+
if (score < bestScore) continue;
|
|
21
|
+
bestScore = score;
|
|
22
|
+
bestUrl = record.url;
|
|
23
|
+
}
|
|
24
|
+
if (bestScore < threshold) return null;
|
|
25
|
+
return bestUrl;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TemplateRecord {
|
|
29
|
+
url: string;
|
|
30
|
+
signature: Set<string>;
|
|
31
|
+
}
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { keepServerRunning } from '../../../src/browser-server.ts';
|
|
4
|
+
import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
|
|
4
5
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
5
6
|
import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
|
|
6
7
|
import { clearActivityLine, trackActivityLine } from './activity-line.ts';
|
|
@@ -220,6 +221,8 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
220
221
|
process.exit(0);
|
|
221
222
|
});
|
|
222
223
|
|
|
224
|
+
RecommendedModelsCommand.register(cmd);
|
|
225
|
+
|
|
223
226
|
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
224
227
|
.addHelpText('after', `\n${statusHelp}`)
|
|
225
228
|
.action(async (hash, options) => {
|
package/boat/prima/src/prima.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { getAliveEndpoint, launchServer, listInstances, stopServer } from '../..
|
|
|
15
15
|
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
16
16
|
import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, type ExplorbotConfig, outputPath } from '../../../src/config.ts';
|
|
17
17
|
import { ExplorBot } from '../../../src/explorbot.ts';
|
|
18
|
-
import { listSites } from '../../../src/global-config.ts';
|
|
18
|
+
import { findSiteWith, listSites } from '../../../src/global-config.ts';
|
|
19
19
|
import { Reporter } from '../../../src/reporter.ts';
|
|
20
20
|
import type { WebPageState } from '../../../src/state-manager.ts';
|
|
21
21
|
import { Stats } from '../../../src/stats.ts';
|
|
@@ -1067,8 +1067,7 @@ export class Prima {
|
|
|
1067
1067
|
|
|
1068
1068
|
async status(hash: string): Promise<EnvelopeData> {
|
|
1069
1069
|
if (!this.artifactsDir) {
|
|
1070
|
-
const
|
|
1071
|
-
const site = sites.find((candidate) => existsSync(path.join(candidate.dir, 'output', 'prima', hash))) || sites[0];
|
|
1070
|
+
const site = findSiteWith(path.join('output', 'prima', hash)) || listSites()[0];
|
|
1072
1071
|
if (site && !this.configBaseUrl()) this.sessionUrl = site.url;
|
|
1073
1072
|
await this.loadConfig();
|
|
1074
1073
|
}
|
|
@@ -1113,7 +1112,7 @@ export class Prima {
|
|
|
1113
1112
|
aria: result.ariaSnapshot,
|
|
1114
1113
|
html: await result.combinedHtml(),
|
|
1115
1114
|
screenshot: result.screenshot,
|
|
1116
|
-
requests: this.bot.requestStore().
|
|
1115
|
+
requests: this.bot.requestStore().getMadeRequests(),
|
|
1117
1116
|
});
|
|
1118
1117
|
}
|
|
1119
1118
|
|
|
@@ -8,6 +8,8 @@ import figureSet from 'figures';
|
|
|
8
8
|
import { render } from 'ink';
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { flushTelemetry } from '../src/ai/provider.js';
|
|
11
|
+
import { HelpJsonCommand } from '../src/commands/help-json-command.js';
|
|
12
|
+
import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
|
|
11
13
|
import { App } from '../src/components/App.js';
|
|
12
14
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
13
15
|
import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
@@ -36,7 +38,8 @@ process.on('uncaughtException', async (error) => {
|
|
|
36
38
|
process.on('unhandledRejection', (reason) => {
|
|
37
39
|
tag('error').log(`Unhandled rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason)}`);
|
|
38
40
|
});
|
|
39
|
-
|
|
41
|
+
const printsJson = process.argv.includes('--json') || process.argv.includes('help-json');
|
|
42
|
+
if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima') && !printsJson) {
|
|
40
43
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
41
44
|
}
|
|
42
45
|
function buildExplorBotOptions(from, options) {
|
|
@@ -296,10 +299,14 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
296
299
|
planfileArg = options.fromPlan;
|
|
297
300
|
indexArg = planfile;
|
|
298
301
|
}
|
|
299
|
-
const
|
|
300
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(
|
|
302
|
+
const peeked = Plan.loadFromFile(planfileArg);
|
|
303
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
301
304
|
await explorBot.start();
|
|
302
|
-
|
|
305
|
+
let plan = peeked;
|
|
306
|
+
if (plan)
|
|
307
|
+
explorBot.setCurrentPlan(plan);
|
|
308
|
+
if (!plan)
|
|
309
|
+
plan = explorBot.loadPlan(planfileArg);
|
|
303
310
|
const pending = plan.getPendingTests();
|
|
304
311
|
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
305
312
|
const startUrl = plan.startUrl;
|
|
@@ -420,6 +427,7 @@ program
|
|
|
420
427
|
process.exit(1);
|
|
421
428
|
}
|
|
422
429
|
});
|
|
430
|
+
RecommendedModelsCommand.register(program);
|
|
423
431
|
program
|
|
424
432
|
.command('init')
|
|
425
433
|
.description('Initialize configuration for a project or for this machine')
|
|
@@ -861,5 +869,7 @@ ${rows}
|
|
|
861
869
|
${cli} explore /login --max-tests 3
|
|
862
870
|
`;
|
|
863
871
|
};
|
|
872
|
+
program.addHelpText('after', `\nFor agents and tools:\n ${cli} help-json [command...] the same definitions as JSON — commands, arguments, options, defaults\n`);
|
|
864
873
|
program.addHelpText('after', envHelp);
|
|
874
|
+
HelpJsonCommand.register(program);
|
|
865
875
|
program.parse();
|
|
@@ -61,7 +61,7 @@ export function createCurlerTools(apiClient, requestState, test, searchSpec) {
|
|
|
61
61
|
body: input.body,
|
|
62
62
|
queryParams: input.queryParams,
|
|
63
63
|
});
|
|
64
|
-
requestState.
|
|
64
|
+
requestState.addMadeRequest(result);
|
|
65
65
|
if (result.error) {
|
|
66
66
|
tag('error').log(`${input.method} ${input.path} > Network error: ${result.error}`);
|
|
67
67
|
activeNote.commit(TestResult.FAILED);
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
5
|
+
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
5
6
|
import { listSites } from "../../../src/global-config.js";
|
|
6
7
|
import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
|
|
7
8
|
import { getStyles } from "./ai/chief/styles.js";
|
|
@@ -95,6 +96,7 @@ export function createApiCommands(name = 'api') {
|
|
|
95
96
|
process.exit(1);
|
|
96
97
|
}
|
|
97
98
|
});
|
|
99
|
+
RecommendedModelsCommand.register(cmd);
|
|
98
100
|
addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
|
|
99
101
|
setPreserveConsoleLogs(true);
|
|
100
102
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { parseAriaLocator } from "../../../../src/utils/aria.js";
|
|
3
4
|
import { tag } from "../../../../src/utils/logger.js";
|
|
5
|
+
import { parseResearchSections } from "../../../../src/ai/researcher/parser.js";
|
|
4
6
|
import { collectDocInteractions } from "./tools.js";
|
|
5
7
|
class Documentarian {
|
|
6
8
|
provider;
|
|
@@ -147,6 +149,9 @@ class Documentarian {
|
|
|
147
149
|
Only list capabilities that are grounded in the provided page research.
|
|
148
150
|
Put actions into "can" only when there is direct evidence in the page context.
|
|
149
151
|
Put actions into "might" only when the UI strongly suggests a capability but proof is incomplete.
|
|
152
|
+
Going to another page is navigation, not a capability. "can" and "might" list only actions the user performs on this page; links and menus are documented in a separate navigation section, never here.
|
|
153
|
+
Raw interaction observations that change the URL are navigation evidence only. Never turn them into "can" or "might" actions.
|
|
154
|
+
Exclude capabilities supplied by unrelated embedded support, marketing, consent, or feedback widgets. Include an embedded interface only when it is part of the current page's documented purpose.
|
|
150
155
|
Describe each action from the end-user perspective.
|
|
151
156
|
Be explicit about scope:
|
|
152
157
|
- one item
|
|
@@ -154,7 +159,7 @@ class Documentarian {
|
|
|
154
159
|
- bulk operations
|
|
155
160
|
- all items
|
|
156
161
|
- page-level
|
|
157
|
-
Avoid implementation details, selectors, and QA wording.
|
|
162
|
+
Avoid implementation details, selectors, and QA wording — except the machine-consumed element field, which is a locator, not prose.
|
|
158
163
|
Avoid duplicate actions with different phrasing.
|
|
159
164
|
</rules>
|
|
160
165
|
|
|
@@ -203,6 +208,8 @@ class Documentarian {
|
|
|
203
208
|
- action: concise user-facing capability phrased as "user can ..."
|
|
204
209
|
- scope: one of one item, list of items, bulk operations, all items, page-level
|
|
205
210
|
- evidence: short reason based on visible UI or research
|
|
211
|
+
- element: required for every "can" and "might" action. Return the proving or suggesting control's locator copied verbatim from the page research table — the value from the ARIA column, or from the CSS column when the ARIA column is empty. Return null only when no single element supports the action. It is machine metadata used for validation and never shown as prose.
|
|
212
|
+
Never copy the human-readable Element description from interaction_observations into element. It is not a locator.
|
|
206
213
|
</output_requirements>
|
|
207
214
|
`;
|
|
208
215
|
}
|
|
@@ -233,8 +240,37 @@ class Documentarian {
|
|
|
233
240
|
const message = error instanceof Error ? error.message : String(error);
|
|
234
241
|
return message.includes('Failed to generate JSON') || message.includes('Failed to validate JSON') || message.includes('failed_generation') || message.includes('No object generated') || message.includes('response did not match schema');
|
|
235
242
|
}
|
|
236
|
-
normalizeDocumentation(documentation, _state,
|
|
243
|
+
normalizeDocumentation(documentation, _state, research) {
|
|
237
244
|
const normalized = { ...documentation };
|
|
245
|
+
const navigationLocators = new Set();
|
|
246
|
+
const researchLocators = new Set();
|
|
247
|
+
for (const section of parseResearchSections(research)) {
|
|
248
|
+
for (const element of section.elements) {
|
|
249
|
+
const aria = element.aria ? `{ role: '${element.aria.role}', text: '${element.aria.text}' }` : null;
|
|
250
|
+
if (aria)
|
|
251
|
+
researchLocators.add(this.locatorKey(aria));
|
|
252
|
+
if (element.css)
|
|
253
|
+
researchLocators.add(this.locatorKey(element.css));
|
|
254
|
+
if (element.aria?.role !== 'link')
|
|
255
|
+
continue;
|
|
256
|
+
if (aria)
|
|
257
|
+
navigationLocators.add(this.locatorKey(aria));
|
|
258
|
+
if (element.css)
|
|
259
|
+
navigationLocators.add(this.locatorKey(element.css));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
normalized.can = normalized.can.filter((capability) => {
|
|
263
|
+
if (!capability.element)
|
|
264
|
+
return true;
|
|
265
|
+
const key = this.locatorKey(capability.element);
|
|
266
|
+
return researchLocators.has(key) && !navigationLocators.has(key);
|
|
267
|
+
});
|
|
268
|
+
normalized.might = normalized.might.filter((capability) => {
|
|
269
|
+
if (!capability.element)
|
|
270
|
+
return false;
|
|
271
|
+
const key = this.locatorKey(capability.element);
|
|
272
|
+
return researchLocators.has(key) && !navigationLocators.has(key);
|
|
273
|
+
});
|
|
238
274
|
if (!normalized.interactions) {
|
|
239
275
|
normalized.interactions = undefined;
|
|
240
276
|
}
|
|
@@ -244,6 +280,18 @@ class Documentarian {
|
|
|
244
280
|
qualityNotes,
|
|
245
281
|
});
|
|
246
282
|
}
|
|
283
|
+
locatorKey(locator) {
|
|
284
|
+
const aria = parseAriaLocator(locator);
|
|
285
|
+
if (aria)
|
|
286
|
+
return `aria:${aria.role}:${aria.text}`;
|
|
287
|
+
const trimmed = locator.trim();
|
|
288
|
+
const first = trimmed.at(0);
|
|
289
|
+
const last = trimmed.at(-1);
|
|
290
|
+
if (first === last && (first === '`' || first === '"' || first === "'")) {
|
|
291
|
+
return `css:${trimmed.slice(1, -1).trim()}`;
|
|
292
|
+
}
|
|
293
|
+
return `css:${trimmed}`;
|
|
294
|
+
}
|
|
247
295
|
evaluateDocumentationQuality(documentation) {
|
|
248
296
|
const notes = [];
|
|
249
297
|
if ((documentation.interactions || []).length === 0 && this.config.docs?.interactive) {
|
|
@@ -300,6 +348,13 @@ const capabilitySchema = z.object({
|
|
|
300
348
|
action: z.string(),
|
|
301
349
|
scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
|
|
302
350
|
evidence: z.string(),
|
|
351
|
+
element: z.string().nullable().optional(),
|
|
352
|
+
});
|
|
353
|
+
const generatedCapabilitySchema = z.object({
|
|
354
|
+
action: z.string(),
|
|
355
|
+
scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
|
|
356
|
+
evidence: z.string(),
|
|
357
|
+
element: z.string().nullable(),
|
|
303
358
|
});
|
|
304
359
|
const stateTransitionSchema = z.object({
|
|
305
360
|
action: z.string(),
|
|
@@ -339,11 +394,14 @@ const stateTransitionSchema = z.object({
|
|
|
339
394
|
.optional(),
|
|
340
395
|
});
|
|
341
396
|
const generatedPageDocumentationSchema = z.object({
|
|
397
|
+
summary: z.string(),
|
|
398
|
+
can: z.array(generatedCapabilitySchema),
|
|
399
|
+
might: z.array(generatedCapabilitySchema),
|
|
400
|
+
});
|
|
401
|
+
const pageDocumentationSchema = z.object({
|
|
342
402
|
summary: z.string(),
|
|
343
403
|
can: z.array(capabilitySchema),
|
|
344
404
|
might: z.array(capabilitySchema),
|
|
345
|
-
});
|
|
346
|
-
const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
|
|
347
405
|
interactions: z.array(stateTransitionSchema).optional(),
|
|
348
406
|
qualityNotes: z.array(z.string()).optional(),
|
|
349
407
|
});
|
|
@@ -132,7 +132,7 @@ function buildTransition(candidate, beforeState, afterState, changes) {
|
|
|
132
132
|
function describeTargetState(beforeState, afterState, candidate) {
|
|
133
133
|
const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
|
|
134
134
|
const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
|
|
135
|
-
if (afterFocus.
|
|
135
|
+
if (afterFocus.type && (!beforeFocus.type || beforeFocus.name !== afterFocus.name)) {
|
|
136
136
|
return {
|
|
137
137
|
kind: afterFocus.type || 'dialog',
|
|
138
138
|
label: afterFocus.name || candidate.element.name.trim(),
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
5
|
+
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
5
6
|
import { remote } from "../../../src/remote.js";
|
|
6
7
|
import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from "../../../src/utils/logger.js";
|
|
7
8
|
import { DocBot } from "./docbot.js";
|
|
@@ -34,7 +35,12 @@ function addCommonOptions(cmd) {
|
|
|
34
35
|
export function createDocsCommands(name = 'docs') {
|
|
35
36
|
const cmd = new Command(name);
|
|
36
37
|
cmd.description('AI-powered website documentation collector');
|
|
37
|
-
addCommonOptions(cmd
|
|
38
|
+
addCommonOptions(cmd
|
|
39
|
+
.command('collect <path>')
|
|
40
|
+
.description('Crawl pages and generate documentation spec')
|
|
41
|
+
.option('--max-pages <count>', 'Maximum number of pages to document')
|
|
42
|
+
.option('--no-collapse-template-pages', 'Visit every page even when its layout matches a documented page')
|
|
43
|
+
.option('--template-similarity <percent>', 'Structural similarity percent that counts pages as the same layout (default 90)')).action(async (startPath, options) => {
|
|
38
44
|
setPreserveConsoleLogs(true);
|
|
39
45
|
try {
|
|
40
46
|
const bot = new DocBot({
|
|
@@ -46,7 +52,11 @@ export function createDocsCommands(name = 'docs') {
|
|
|
46
52
|
if (options.maxPages) {
|
|
47
53
|
maxPages = Number.parseInt(options.maxPages, 10);
|
|
48
54
|
}
|
|
49
|
-
|
|
55
|
+
let templateSimilarity;
|
|
56
|
+
if (options.templateSimilarity) {
|
|
57
|
+
templateSimilarity = Number.parseInt(options.templateSimilarity, 10);
|
|
58
|
+
}
|
|
59
|
+
const result = await bot.collect(startPath, { maxPages, collapseTemplatePages: options.collapseTemplatePages, templateSimilarity });
|
|
50
60
|
console.log(`\nDocumented ${result.pages.length} page(s)`);
|
|
51
61
|
console.log(`Skipped ${result.skipped.length} page(s)`);
|
|
52
62
|
console.log(`Spec index: ${result.indexPath}`);
|
|
@@ -74,6 +84,7 @@ export function createDocsCommands(name = 'docs') {
|
|
|
74
84
|
process.exit(1);
|
|
75
85
|
}
|
|
76
86
|
});
|
|
87
|
+
RecommendedModelsCommand.register(cmd);
|
|
77
88
|
cmd
|
|
78
89
|
.command('init')
|
|
79
90
|
.description('Initialize doc collector configuration')
|
|
@@ -101,6 +112,8 @@ export function createDocsCommands(name = 'docs') {
|
|
|
101
112
|
interactive: false,
|
|
102
113
|
ignoreErrors: true,
|
|
103
114
|
collapseDynamicPages: true,
|
|
115
|
+
collapseTemplatePages: true,
|
|
116
|
+
templateSimilarity: 90,
|
|
104
117
|
scope: 'site',
|
|
105
118
|
includePaths: [],
|
|
106
119
|
excludePaths: [],
|
|
@@ -9,8 +9,9 @@ import { DocbotConfigParser } from "./config.js";
|
|
|
9
9
|
import { renderPageDocumentation, renderSpecIndex } from "./docs-renderer.js";
|
|
10
10
|
import { getDocPageKey, shouldCrawlDocPath } from "./path-filter.js";
|
|
11
11
|
import { extractResearchNavigationTargets } from "./research-navigation.js";
|
|
12
|
-
import { captureBeforeInteraction, captureDocumentationScreenshots, captureInteractionScreenshot } from "./screenshots.js";
|
|
12
|
+
import { captureBeforeInteraction, captureDocumentationScreenshots, captureEvidenceScreenshots, captureInteractionScreenshot } from "./screenshots.js";
|
|
13
13
|
import { renderMermaidBody } from "./state-diagram.js";
|
|
14
|
+
import { buildTemplateRecord, findTemplateMatch } from "./template-dedup.js";
|
|
14
15
|
class DocBot {
|
|
15
16
|
explorBot;
|
|
16
17
|
configParser;
|
|
@@ -53,6 +54,7 @@ class DocBot {
|
|
|
53
54
|
const queue = [];
|
|
54
55
|
const queued = new Set();
|
|
55
56
|
const documented = new Set();
|
|
57
|
+
const templates = [];
|
|
56
58
|
const pages = [];
|
|
57
59
|
const skipped = [];
|
|
58
60
|
const baseUrl = this.explorBot.getConfig().playwright.url;
|
|
@@ -73,14 +75,6 @@ class DocBot {
|
|
|
73
75
|
try {
|
|
74
76
|
tag('info').log(`Collecting docs for ${this.toDisplayUrl(target, baseUrl)}`);
|
|
75
77
|
await this.explorBot.visit(target);
|
|
76
|
-
if (stateManager.isInDeadLoop()) {
|
|
77
|
-
tag('warning').log('Dead loop detected during docs crawl, stopping collection');
|
|
78
|
-
skipped.push({
|
|
79
|
-
url: target,
|
|
80
|
-
reason: 'dead loop detected during crawl',
|
|
81
|
-
});
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
78
|
const state = this.explorBot.getCurrentState();
|
|
85
79
|
if (!state) {
|
|
86
80
|
skipped.push({
|
|
@@ -93,6 +87,22 @@ class DocBot {
|
|
|
93
87
|
if (documented.has(pageKey)) {
|
|
94
88
|
continue;
|
|
95
89
|
}
|
|
90
|
+
if (this.shouldCollapseTemplates(opts.collapseTemplatePages)) {
|
|
91
|
+
const templateUrl = findTemplateMatch(state.ariaSnapshot ?? null, templates, this.getTemplateSimilarity(opts.templateSimilarity));
|
|
92
|
+
if (templateUrl) {
|
|
93
|
+
skipped.push({
|
|
94
|
+
url: state.url,
|
|
95
|
+
reason: `same layout as ${templateUrl} (only content differs)`,
|
|
96
|
+
});
|
|
97
|
+
documented.add(pageKey);
|
|
98
|
+
for (const nextPath of this.extractNextPaths(state, baseUrl, '')) {
|
|
99
|
+
if (documented.has(this.getPageKey(nextPath)) || stateManager.hasVisitedState(nextPath))
|
|
100
|
+
continue;
|
|
101
|
+
this.enqueuePath(nextPath, queue, queued);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
96
106
|
const research = await this.explorBot.agentResearcher().research(state, {
|
|
97
107
|
screenshot: this.shouldUseScreenshots(),
|
|
98
108
|
force: true,
|
|
@@ -134,6 +144,9 @@ class DocBot {
|
|
|
134
144
|
filePath,
|
|
135
145
|
});
|
|
136
146
|
documented.add(pageKey);
|
|
147
|
+
const templateRecord = buildTemplateRecord(state.url, state.ariaSnapshot ?? null);
|
|
148
|
+
if (templateRecord)
|
|
149
|
+
templates.push(templateRecord);
|
|
137
150
|
const nextPaths = this.extractNextPaths(state, baseUrl, research, documentation);
|
|
138
151
|
const interactionPriorityPaths = new Set(this.extractInteractionPaths(baseUrl, documentation));
|
|
139
152
|
for (const nextPath of nextPaths) {
|
|
@@ -184,6 +197,23 @@ class DocBot {
|
|
|
184
197
|
}
|
|
185
198
|
return true;
|
|
186
199
|
}
|
|
200
|
+
shouldCollapseTemplates(override) {
|
|
201
|
+
if (override === false) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (this.config.docs?.collapseTemplatePages === false) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
getTemplateSimilarity(override) {
|
|
210
|
+
const value = override ?? this.config.docs?.templateSimilarity;
|
|
211
|
+
if (value == null)
|
|
212
|
+
return undefined;
|
|
213
|
+
if (!Number.isFinite(value) || value <= 0 || value > 100)
|
|
214
|
+
return undefined;
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
187
217
|
extractNextPaths(state, baseUrl, research, documentation) {
|
|
188
218
|
const paths = [];
|
|
189
219
|
const seen = new Set();
|
|
@@ -393,7 +423,8 @@ class DocBot {
|
|
|
393
423
|
async savePageDocumentation(state, documentation, research) {
|
|
394
424
|
const pagePath = this.getPageFilePath(state.url);
|
|
395
425
|
const screenshots = await this.captureScreenshots(state, research, pagePath);
|
|
396
|
-
|
|
426
|
+
const evidence = await this.captureEvidence(state, documentation, research, pagePath);
|
|
427
|
+
writeFileSync(pagePath, renderPageDocumentation(state, documentation, screenshots, evidence), 'utf8');
|
|
397
428
|
return pagePath;
|
|
398
429
|
}
|
|
399
430
|
async captureScreenshots(state, research, pagePath) {
|
|
@@ -406,6 +437,17 @@ class DocBot {
|
|
|
406
437
|
config: this.config,
|
|
407
438
|
});
|
|
408
439
|
}
|
|
440
|
+
async captureEvidence(state, documentation, research, pagePath) {
|
|
441
|
+
if (!this.shouldUseScreenshots()) {
|
|
442
|
+
return documentation.can.map(() => null);
|
|
443
|
+
}
|
|
444
|
+
return captureEvidenceScreenshots(this.explorBot.getExplorer(), state, documentation, {
|
|
445
|
+
pageFilePath: pagePath,
|
|
446
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
447
|
+
config: this.config,
|
|
448
|
+
research,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
409
451
|
saveIndex(startPath, pages, skipped, maxPages) {
|
|
410
452
|
const outputDir = this.configParser.getOutputDir();
|
|
411
453
|
const indexPath = path.join(outputDir, 'index.md');
|