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
package/bin/explorbot-cli.ts
CHANGED
|
@@ -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';
|
|
@@ -42,7 +44,9 @@ process.on('unhandledRejection', (reason) => {
|
|
|
42
44
|
tag('error').log(`Unhandled rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason)}`);
|
|
43
45
|
});
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
const printsJson = process.argv.includes('--json') || process.argv.includes('help-json');
|
|
48
|
+
|
|
49
|
+
if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima') && !printsJson) {
|
|
46
50
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -336,12 +340,14 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
336
340
|
indexArg = planfile;
|
|
337
341
|
}
|
|
338
342
|
|
|
339
|
-
const
|
|
343
|
+
const peeked = Plan.loadFromFile(planfileArg);
|
|
340
344
|
|
|
341
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(
|
|
345
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
342
346
|
await explorBot.start();
|
|
343
347
|
|
|
344
|
-
|
|
348
|
+
let plan = peeked;
|
|
349
|
+
if (plan) explorBot.setCurrentPlan(plan);
|
|
350
|
+
if (!plan) plan = explorBot.loadPlan(planfileArg);
|
|
345
351
|
const pending = plan.getPendingTests();
|
|
346
352
|
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
347
353
|
|
|
@@ -470,6 +476,8 @@ program
|
|
|
470
476
|
}
|
|
471
477
|
});
|
|
472
478
|
|
|
479
|
+
RecommendedModelsCommand.register(program);
|
|
480
|
+
|
|
473
481
|
program
|
|
474
482
|
.command('init')
|
|
475
483
|
.description('Initialize configuration for a project or for this machine')
|
|
@@ -947,6 +955,8 @@ ${rows}
|
|
|
947
955
|
`;
|
|
948
956
|
};
|
|
949
957
|
|
|
958
|
+
program.addHelpText('after', `\nFor agents and tools:\n ${cli} help-json [command...] the same definitions as JSON — commands, arguments, options, defaults\n`);
|
|
950
959
|
program.addHelpText('after', envHelp);
|
|
960
|
+
HelpJsonCommand.register(program);
|
|
951
961
|
|
|
952
962
|
program.parse();
|
|
@@ -66,7 +66,7 @@ export function createCurlerTools(apiClient: ApiClient, requestState: RequestSto
|
|
|
66
66
|
queryParams: input.queryParams,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
requestState.
|
|
69
|
+
requestState.addMadeRequest(result);
|
|
70
70
|
|
|
71
71
|
if (result.error) {
|
|
72
72
|
tag('error').log(`${input.method} ${input.path} > Network error: ${result.error}`);
|
|
@@ -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.ts';
|
|
5
|
+
import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
|
|
5
6
|
import { listSites } from '../../../src/global-config.ts';
|
|
6
7
|
import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
|
|
7
8
|
import { getStyles } from './ai/chief/styles.ts';
|
|
@@ -109,6 +110,8 @@ export function createApiCommands(name = 'api'): Command {
|
|
|
109
110
|
}
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
RecommendedModelsCommand.register(cmd);
|
|
114
|
+
|
|
112
115
|
addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
|
|
113
116
|
setPreserveConsoleLogs(true);
|
|
114
117
|
try {
|
|
@@ -4,7 +4,9 @@ import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
|
4
4
|
import type Explorer from '../../../../src/explorer.ts';
|
|
5
5
|
import type { StateManager } from '../../../../src/state-manager.ts';
|
|
6
6
|
import type { WebPageState } from '../../../../src/state-manager.ts';
|
|
7
|
+
import { parseAriaLocator } from '../../../../src/utils/aria.ts';
|
|
7
8
|
import { tag } from '../../../../src/utils/logger.ts';
|
|
9
|
+
import { parseResearchSections } from '../../../../src/ai/researcher/parser.ts';
|
|
8
10
|
import type { DocbotConfig } from '../config.ts';
|
|
9
11
|
import { type CaptureInteractionState, type DocStateTransition, collectDocInteractions } from './tools.ts';
|
|
10
12
|
|
|
@@ -182,6 +184,9 @@ class Documentarian {
|
|
|
182
184
|
Only list capabilities that are grounded in the provided page research.
|
|
183
185
|
Put actions into "can" only when there is direct evidence in the page context.
|
|
184
186
|
Put actions into "might" only when the UI strongly suggests a capability but proof is incomplete.
|
|
187
|
+
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.
|
|
188
|
+
Raw interaction observations that change the URL are navigation evidence only. Never turn them into "can" or "might" actions.
|
|
189
|
+
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.
|
|
185
190
|
Describe each action from the end-user perspective.
|
|
186
191
|
Be explicit about scope:
|
|
187
192
|
- one item
|
|
@@ -189,7 +194,7 @@ class Documentarian {
|
|
|
189
194
|
- bulk operations
|
|
190
195
|
- all items
|
|
191
196
|
- page-level
|
|
192
|
-
Avoid implementation details, selectors, and QA wording.
|
|
197
|
+
Avoid implementation details, selectors, and QA wording — except the machine-consumed element field, which is a locator, not prose.
|
|
193
198
|
Avoid duplicate actions with different phrasing.
|
|
194
199
|
</rules>
|
|
195
200
|
|
|
@@ -241,6 +246,8 @@ class Documentarian {
|
|
|
241
246
|
- action: concise user-facing capability phrased as "user can ..."
|
|
242
247
|
- scope: one of one item, list of items, bulk operations, all items, page-level
|
|
243
248
|
- evidence: short reason based on visible UI or research
|
|
249
|
+
- 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.
|
|
250
|
+
Never copy the human-readable Element description from interaction_observations into element. It is not a locator.
|
|
244
251
|
</output_requirements>
|
|
245
252
|
`;
|
|
246
253
|
}
|
|
@@ -274,8 +281,30 @@ class Documentarian {
|
|
|
274
281
|
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');
|
|
275
282
|
}
|
|
276
283
|
|
|
277
|
-
private normalizeDocumentation(documentation: GeneratedPageDocumentation & Partial<Pick<PageDocumentation, 'interactions'>>, _state: WebPageState,
|
|
284
|
+
private normalizeDocumentation(documentation: GeneratedPageDocumentation & Partial<Pick<PageDocumentation, 'interactions'>>, _state: WebPageState, research: string): PageDocumentation {
|
|
278
285
|
const normalized = { ...documentation };
|
|
286
|
+
const navigationLocators = new Set<string>();
|
|
287
|
+
const researchLocators = new Set<string>();
|
|
288
|
+
for (const section of parseResearchSections(research)) {
|
|
289
|
+
for (const element of section.elements) {
|
|
290
|
+
const aria = element.aria ? `{ role: '${element.aria.role}', text: '${element.aria.text}' }` : null;
|
|
291
|
+
if (aria) researchLocators.add(this.locatorKey(aria));
|
|
292
|
+
if (element.css) researchLocators.add(this.locatorKey(element.css));
|
|
293
|
+
if (element.aria?.role !== 'link') continue;
|
|
294
|
+
if (aria) navigationLocators.add(this.locatorKey(aria));
|
|
295
|
+
if (element.css) navigationLocators.add(this.locatorKey(element.css));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
normalized.can = normalized.can.filter((capability) => {
|
|
299
|
+
if (!capability.element) return true;
|
|
300
|
+
const key = this.locatorKey(capability.element);
|
|
301
|
+
return researchLocators.has(key) && !navigationLocators.has(key);
|
|
302
|
+
});
|
|
303
|
+
normalized.might = normalized.might.filter((capability) => {
|
|
304
|
+
if (!capability.element) return false;
|
|
305
|
+
const key = this.locatorKey(capability.element);
|
|
306
|
+
return researchLocators.has(key) && !navigationLocators.has(key);
|
|
307
|
+
});
|
|
279
308
|
if (!normalized.interactions) {
|
|
280
309
|
normalized.interactions = undefined;
|
|
281
310
|
}
|
|
@@ -288,6 +317,18 @@ class Documentarian {
|
|
|
288
317
|
});
|
|
289
318
|
}
|
|
290
319
|
|
|
320
|
+
private locatorKey(locator: string): string {
|
|
321
|
+
const aria = parseAriaLocator(locator);
|
|
322
|
+
if (aria) return `aria:${aria.role}:${aria.text}`;
|
|
323
|
+
const trimmed = locator.trim();
|
|
324
|
+
const first = trimmed.at(0);
|
|
325
|
+
const last = trimmed.at(-1);
|
|
326
|
+
if (first === last && (first === '`' || first === '"' || first === "'")) {
|
|
327
|
+
return `css:${trimmed.slice(1, -1).trim()}`;
|
|
328
|
+
}
|
|
329
|
+
return `css:${trimmed}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
291
332
|
private evaluateDocumentationQuality(documentation: PageDocumentation): string[] {
|
|
292
333
|
const notes: string[] = [];
|
|
293
334
|
|
|
@@ -355,6 +396,14 @@ const capabilitySchema = z.object({
|
|
|
355
396
|
action: z.string(),
|
|
356
397
|
scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
|
|
357
398
|
evidence: z.string(),
|
|
399
|
+
element: z.string().nullable().optional(),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const generatedCapabilitySchema = z.object({
|
|
403
|
+
action: z.string(),
|
|
404
|
+
scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
|
|
405
|
+
evidence: z.string(),
|
|
406
|
+
element: z.string().nullable(),
|
|
358
407
|
});
|
|
359
408
|
|
|
360
409
|
const stateTransitionSchema = z.object({
|
|
@@ -397,11 +446,14 @@ const stateTransitionSchema = z.object({
|
|
|
397
446
|
|
|
398
447
|
const generatedPageDocumentationSchema = z.object({
|
|
399
448
|
summary: z.string(),
|
|
400
|
-
can: z.array(
|
|
401
|
-
might: z.array(
|
|
449
|
+
can: z.array(generatedCapabilitySchema),
|
|
450
|
+
might: z.array(generatedCapabilitySchema),
|
|
402
451
|
});
|
|
403
452
|
|
|
404
|
-
const pageDocumentationSchema =
|
|
453
|
+
const pageDocumentationSchema = z.object({
|
|
454
|
+
summary: z.string(),
|
|
455
|
+
can: z.array(capabilitySchema),
|
|
456
|
+
might: z.array(capabilitySchema),
|
|
405
457
|
interactions: z.array(stateTransitionSchema).optional(),
|
|
406
458
|
qualityNotes: z.array(z.string()).optional(),
|
|
407
459
|
});
|
|
@@ -225,7 +225,7 @@ function buildTransition(candidate: InteractionCandidate, beforeState: WebPageSt
|
|
|
225
225
|
function describeTargetState(beforeState: WebPageState, afterState: WebPageState, candidate: InteractionCandidate): InteractionState {
|
|
226
226
|
const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
|
|
227
227
|
const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
|
|
228
|
-
if (afterFocus.
|
|
228
|
+
if (afterFocus.type && (!beforeFocus.type || beforeFocus.name !== afterFocus.name)) {
|
|
229
229
|
return {
|
|
230
230
|
kind: afterFocus.type || 'dialog',
|
|
231
231
|
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.ts';
|
|
5
|
+
import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
|
|
5
6
|
import { remote } from '../../../src/remote.ts';
|
|
6
7
|
import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from '../../../src/utils/logger.ts';
|
|
7
8
|
import { DocBot, type DocbotOptions } from './docbot.ts';
|
|
@@ -38,7 +39,14 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
38
39
|
const cmd = new Command(name);
|
|
39
40
|
cmd.description('AI-powered website documentation collector');
|
|
40
41
|
|
|
41
|
-
addCommonOptions(
|
|
42
|
+
addCommonOptions(
|
|
43
|
+
cmd
|
|
44
|
+
.command('collect <path>')
|
|
45
|
+
.description('Crawl pages and generate documentation spec')
|
|
46
|
+
.option('--max-pages <count>', 'Maximum number of pages to document')
|
|
47
|
+
.option('--no-collapse-template-pages', 'Visit every page even when its layout matches a documented page')
|
|
48
|
+
.option('--template-similarity <percent>', 'Structural similarity percent that counts pages as the same layout (default 90)')
|
|
49
|
+
).action(async (startPath, options) => {
|
|
42
50
|
setPreserveConsoleLogs(true);
|
|
43
51
|
|
|
44
52
|
try {
|
|
@@ -52,8 +60,12 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
52
60
|
if (options.maxPages) {
|
|
53
61
|
maxPages = Number.parseInt(options.maxPages, 10);
|
|
54
62
|
}
|
|
63
|
+
let templateSimilarity: number | undefined;
|
|
64
|
+
if (options.templateSimilarity) {
|
|
65
|
+
templateSimilarity = Number.parseInt(options.templateSimilarity, 10);
|
|
66
|
+
}
|
|
55
67
|
|
|
56
|
-
const result = await bot.collect(startPath, { maxPages });
|
|
68
|
+
const result = await bot.collect(startPath, { maxPages, collapseTemplatePages: options.collapseTemplatePages, templateSimilarity });
|
|
57
69
|
|
|
58
70
|
console.log(`\nDocumented ${result.pages.length} page(s)`);
|
|
59
71
|
console.log(`Skipped ${result.skipped.length} page(s)`);
|
|
@@ -83,6 +95,8 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
83
95
|
}
|
|
84
96
|
});
|
|
85
97
|
|
|
98
|
+
RecommendedModelsCommand.register(cmd);
|
|
99
|
+
|
|
86
100
|
cmd
|
|
87
101
|
.command('init')
|
|
88
102
|
.description('Initialize doc collector configuration')
|
|
@@ -112,6 +126,8 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
112
126
|
interactive: false,
|
|
113
127
|
ignoreErrors: true,
|
|
114
128
|
collapseDynamicPages: true,
|
|
129
|
+
collapseTemplatePages: true,
|
|
130
|
+
templateSimilarity: 90,
|
|
115
131
|
scope: 'site',
|
|
116
132
|
includePaths: [],
|
|
117
133
|
excludePaths: [],
|
|
@@ -119,6 +119,7 @@ class DocbotConfigParser {
|
|
|
119
119
|
interactive: false,
|
|
120
120
|
ignoreErrors: true,
|
|
121
121
|
collapseDynamicPages: true,
|
|
122
|
+
collapseTemplatePages: true,
|
|
122
123
|
scope: 'site',
|
|
123
124
|
includePaths: [],
|
|
124
125
|
excludePaths: [],
|
|
@@ -157,6 +158,8 @@ interface DocbotConfig {
|
|
|
157
158
|
ignoreErrors?: boolean | string[];
|
|
158
159
|
prompt?: string;
|
|
159
160
|
collapseDynamicPages?: boolean;
|
|
161
|
+
collapseTemplatePages?: boolean;
|
|
162
|
+
templateSimilarity?: number;
|
|
160
163
|
scope?: 'site' | 'section' | 'subtree';
|
|
161
164
|
includePaths?: string[];
|
|
162
165
|
excludePaths?: string[];
|
|
@@ -11,8 +11,9 @@ import { type DocbotConfig, DocbotConfigParser } from './config.ts';
|
|
|
11
11
|
import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
|
|
12
12
|
import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
|
|
13
13
|
import { extractResearchNavigationTargets } from './research-navigation.ts';
|
|
14
|
-
import { type DocumentationScreenshot, captureBeforeInteraction, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
|
+
import { type DocumentationScreenshot, captureBeforeInteraction, captureDocumentationScreenshots, captureEvidenceScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
15
15
|
import { renderMermaidBody } from './state-diagram.ts';
|
|
16
|
+
import { type TemplateRecord, buildTemplateRecord, findTemplateMatch } from './template-dedup.ts';
|
|
16
17
|
|
|
17
18
|
class DocBot {
|
|
18
19
|
private explorBot: ExplorBot;
|
|
@@ -60,6 +61,7 @@ class DocBot {
|
|
|
60
61
|
const queue: string[] = [];
|
|
61
62
|
const queued = new Set<string>();
|
|
62
63
|
const documented = new Set<string>();
|
|
64
|
+
const templates: TemplateRecord[] = [];
|
|
63
65
|
const pages: DocumentedPage[] = [];
|
|
64
66
|
const skipped: SkippedPage[] = [];
|
|
65
67
|
const baseUrl = this.explorBot.getConfig().playwright.url;
|
|
@@ -86,15 +88,6 @@ class DocBot {
|
|
|
86
88
|
tag('info').log(`Collecting docs for ${this.toDisplayUrl(target, baseUrl)}`);
|
|
87
89
|
await this.explorBot.visit(target);
|
|
88
90
|
|
|
89
|
-
if (stateManager.isInDeadLoop()) {
|
|
90
|
-
tag('warning').log('Dead loop detected during docs crawl, stopping collection');
|
|
91
|
-
skipped.push({
|
|
92
|
-
url: target,
|
|
93
|
-
reason: 'dead loop detected during crawl',
|
|
94
|
-
});
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
91
|
const state = this.explorBot.getCurrentState();
|
|
99
92
|
if (!state) {
|
|
100
93
|
skipped.push({
|
|
@@ -109,6 +102,22 @@ class DocBot {
|
|
|
109
102
|
continue;
|
|
110
103
|
}
|
|
111
104
|
|
|
105
|
+
if (this.shouldCollapseTemplates(opts.collapseTemplatePages)) {
|
|
106
|
+
const templateUrl = findTemplateMatch(state.ariaSnapshot ?? null, templates, this.getTemplateSimilarity(opts.templateSimilarity));
|
|
107
|
+
if (templateUrl) {
|
|
108
|
+
skipped.push({
|
|
109
|
+
url: state.url,
|
|
110
|
+
reason: `same layout as ${templateUrl} (only content differs)`,
|
|
111
|
+
});
|
|
112
|
+
documented.add(pageKey);
|
|
113
|
+
for (const nextPath of this.extractNextPaths(state, baseUrl, '')) {
|
|
114
|
+
if (documented.has(this.getPageKey(nextPath)) || stateManager.hasVisitedState(nextPath)) continue;
|
|
115
|
+
this.enqueuePath(nextPath, queue, queued);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
const research = await this.explorBot.agentResearcher().research(state, {
|
|
113
122
|
screenshot: this.shouldUseScreenshots(),
|
|
114
123
|
force: true,
|
|
@@ -153,6 +162,9 @@ class DocBot {
|
|
|
153
162
|
});
|
|
154
163
|
documented.add(pageKey);
|
|
155
164
|
|
|
165
|
+
const templateRecord = buildTemplateRecord(state.url, state.ariaSnapshot ?? null);
|
|
166
|
+
if (templateRecord) templates.push(templateRecord);
|
|
167
|
+
|
|
156
168
|
const nextPaths = this.extractNextPaths(state, baseUrl, research, documentation);
|
|
157
169
|
const interactionPriorityPaths = new Set(this.extractInteractionPaths(baseUrl, documentation));
|
|
158
170
|
for (const nextPath of nextPaths) {
|
|
@@ -209,6 +221,23 @@ class DocBot {
|
|
|
209
221
|
return true;
|
|
210
222
|
}
|
|
211
223
|
|
|
224
|
+
private shouldCollapseTemplates(override?: boolean): boolean {
|
|
225
|
+
if (override === false) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (this.config.docs?.collapseTemplatePages === false) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private getTemplateSimilarity(override?: number): number | undefined {
|
|
235
|
+
const value = override ?? this.config.docs?.templateSimilarity;
|
|
236
|
+
if (value == null) return undefined;
|
|
237
|
+
if (!Number.isFinite(value) || value <= 0 || value > 100) return undefined;
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
|
|
212
241
|
private extractNextPaths(state: WebPageState, baseUrl: string, research: string, documentation?: PageDocumentation): string[] {
|
|
213
242
|
const paths: string[] = [];
|
|
214
243
|
const seen = new Set<string>();
|
|
@@ -446,7 +475,8 @@ class DocBot {
|
|
|
446
475
|
private async savePageDocumentation(state: WebPageState, documentation: PageDocumentation, research: string): Promise<string> {
|
|
447
476
|
const pagePath = this.getPageFilePath(state.url);
|
|
448
477
|
const screenshots = await this.captureScreenshots(state, research, pagePath);
|
|
449
|
-
|
|
478
|
+
const evidence = await this.captureEvidence(state, documentation, research, pagePath);
|
|
479
|
+
writeFileSync(pagePath, renderPageDocumentation(state, documentation, screenshots, evidence), 'utf8');
|
|
450
480
|
return pagePath;
|
|
451
481
|
}
|
|
452
482
|
|
|
@@ -462,6 +492,19 @@ class DocBot {
|
|
|
462
492
|
});
|
|
463
493
|
}
|
|
464
494
|
|
|
495
|
+
private async captureEvidence(state: WebPageState, documentation: PageDocumentation, research: string, pagePath: string): Promise<Array<DocumentationScreenshot | null>> {
|
|
496
|
+
if (!this.shouldUseScreenshots()) {
|
|
497
|
+
return documentation.can.map(() => null);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return captureEvidenceScreenshots(this.explorBot.getExplorer(), state, documentation, {
|
|
501
|
+
pageFilePath: pagePath,
|
|
502
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
503
|
+
config: this.config,
|
|
504
|
+
research,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
465
508
|
private saveIndex(startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): { indexPath: string; diagramPath: string } {
|
|
466
509
|
const outputDir = this.configParser.getOutputDir();
|
|
467
510
|
const indexPath = path.join(outputDir, 'index.md');
|
|
@@ -505,6 +548,8 @@ interface DocbotOptions extends ExplorBotOptions {
|
|
|
505
548
|
|
|
506
549
|
interface CollectOptions {
|
|
507
550
|
maxPages?: number;
|
|
551
|
+
collapseTemplatePages?: boolean;
|
|
552
|
+
templateSimilarity?: number;
|
|
508
553
|
}
|
|
509
554
|
|
|
510
555
|
interface CollectionResult {
|
|
@@ -7,7 +7,7 @@ import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
|
|
|
7
7
|
import type { DocumentationScreenshot } from './screenshots.ts';
|
|
8
8
|
import { type DocumentedPage, type SkippedPage, buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from './state-diagram.ts';
|
|
9
9
|
|
|
10
|
-
function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = []): string {
|
|
10
|
+
function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = [], evidence: Array<DocumentationScreenshot | null> = []): string {
|
|
11
11
|
const lines: string[] = [];
|
|
12
12
|
lines.push(`# ${state.url}`);
|
|
13
13
|
lines.push('');
|
|
@@ -22,6 +22,42 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
22
22
|
lines.push(ensureSentence(documentation.summary));
|
|
23
23
|
lines.push('');
|
|
24
24
|
|
|
25
|
+
lines.push('## User Can');
|
|
26
|
+
lines.push('');
|
|
27
|
+
|
|
28
|
+
if (documentation.can.length === 0) {
|
|
29
|
+
lines.push('- No proven actions were identified from the collected research.');
|
|
30
|
+
lines.push('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [index, item] of documentation.can.entries()) {
|
|
34
|
+
lines.push(`- ${normalizeAction(item.action)} -> ${item.scope}`);
|
|
35
|
+
lines.push(` Proof: ${ensureSentence(item.evidence)}`);
|
|
36
|
+
const shot = evidence[index];
|
|
37
|
+
if (shot) {
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(` `);
|
|
40
|
+
}
|
|
41
|
+
lines.push('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
lines.push('## User Might');
|
|
45
|
+
lines.push('');
|
|
46
|
+
|
|
47
|
+
if (documentation.might.length === 0) {
|
|
48
|
+
lines.push('- No assumption-based actions were identified.');
|
|
49
|
+
lines.push('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const item of documentation.might) {
|
|
53
|
+
lines.push(`- ${normalizeAction(item.action, 'might')} -> ${item.scope}`);
|
|
54
|
+
lines.push(` Signal: ${ensureSentence(item.evidence)}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (documentation.might.length > 0) {
|
|
58
|
+
lines.push('');
|
|
59
|
+
}
|
|
60
|
+
|
|
25
61
|
if (screenshots.length > 0) {
|
|
26
62
|
lines.push('## Screenshots');
|
|
27
63
|
lines.push('');
|
|
@@ -34,6 +70,26 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
34
70
|
}
|
|
35
71
|
}
|
|
36
72
|
|
|
73
|
+
const navigationLinks = renderNavigationLinks(state);
|
|
74
|
+
if (navigationLinks.length > 0) {
|
|
75
|
+
lines.push('## Navigation');
|
|
76
|
+
lines.push('');
|
|
77
|
+
for (const link of navigationLinks) {
|
|
78
|
+
lines.push(`- ${link.title}: ${link.url}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const qualityNotes = documentation.qualityNotes;
|
|
84
|
+
if (qualityNotes && qualityNotes.length > 0) {
|
|
85
|
+
lines.push('## Coverage Notes');
|
|
86
|
+
lines.push('');
|
|
87
|
+
for (const note of qualityNotes) {
|
|
88
|
+
lines.push(`- ${ensureSentence(note)}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
|
|
37
93
|
const interactions = documentation.interactions;
|
|
38
94
|
const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
|
|
39
95
|
if (pageStateDiagram) {
|
|
@@ -66,50 +122,6 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
66
122
|
}
|
|
67
123
|
}
|
|
68
124
|
|
|
69
|
-
lines.push('## User Can');
|
|
70
|
-
lines.push('');
|
|
71
|
-
|
|
72
|
-
if (documentation.can.length === 0) {
|
|
73
|
-
lines.push('- No proven actions were identified from the collected research.');
|
|
74
|
-
lines.push('');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
for (const item of documentation.can) {
|
|
78
|
-
lines.push(`- ${normalizeAction(item.action)} -> ${item.scope}`);
|
|
79
|
-
lines.push(` Proof: ${ensureSentence(item.evidence)}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (documentation.can.length > 0) {
|
|
83
|
-
lines.push('');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
lines.push('## User Might');
|
|
87
|
-
lines.push('');
|
|
88
|
-
|
|
89
|
-
if (documentation.might.length === 0) {
|
|
90
|
-
lines.push('- No assumption-based actions were identified.');
|
|
91
|
-
lines.push('');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for (const item of documentation.might) {
|
|
95
|
-
lines.push(`- ${normalizeAction(item.action, 'might')} -> ${item.scope}`);
|
|
96
|
-
lines.push(` Signal: ${ensureSentence(item.evidence)}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (documentation.might.length > 0) {
|
|
100
|
-
lines.push('');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const qualityNotes = documentation.qualityNotes;
|
|
104
|
-
if (qualityNotes && qualityNotes.length > 0) {
|
|
105
|
-
lines.push('## Coverage Notes');
|
|
106
|
-
lines.push('');
|
|
107
|
-
for (const note of qualityNotes) {
|
|
108
|
-
lines.push(`- ${ensureSentence(note)}`);
|
|
109
|
-
}
|
|
110
|
-
lines.push('');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
125
|
return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
|
|
114
126
|
url: state.url,
|
|
115
127
|
format: APPLICATION_SPEC_FORMAT,
|
|
@@ -130,17 +142,7 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
|
|
|
130
142
|
lines.push('');
|
|
131
143
|
const graph = buildStateGraph(outputDir, pages);
|
|
132
144
|
const mermaid = renderMermaidFromGraph(graph, true);
|
|
133
|
-
lines.push('## State Transitions');
|
|
134
|
-
lines.push('');
|
|
135
|
-
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
136
|
-
lines.push('');
|
|
137
145
|
const stateMap = renderStateMapFromGraph(graph);
|
|
138
|
-
if (stateMap) {
|
|
139
|
-
lines.push('### State Index');
|
|
140
|
-
lines.push('');
|
|
141
|
-
lines.push(stateMap);
|
|
142
|
-
lines.push('');
|
|
143
|
-
}
|
|
144
146
|
lines.push('## Pages');
|
|
145
147
|
lines.push('');
|
|
146
148
|
|
|
@@ -208,9 +210,31 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
|
|
|
208
210
|
lines.push('');
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
lines.push('## State Transitions');
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
216
|
+
lines.push('');
|
|
217
|
+
if (stateMap) {
|
|
218
|
+
lines.push('### State Index');
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push(stateMap);
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
212
225
|
}
|
|
213
226
|
|
|
227
|
+
function renderNavigationLinks(state: WebPageState): Array<{ title: string; url: string }> {
|
|
228
|
+
const links: Array<{ title: string; url: string }> = [];
|
|
229
|
+
const seen = new Set<string>();
|
|
230
|
+
for (const link of state.links || []) {
|
|
231
|
+
if (!link.url || seen.has(link.url)) continue;
|
|
232
|
+
seen.add(link.url);
|
|
233
|
+
links.push({ title: normalizeInlineText(link.title) || link.url, url: link.url });
|
|
234
|
+
}
|
|
235
|
+
return links;
|
|
236
|
+
}
|
|
237
|
+
|
|
214
238
|
function normalizeAction(action: string, kind: 'can' | 'might' = 'can'): string {
|
|
215
239
|
const trimmed = normalizeInlineText(action);
|
|
216
240
|
if (!trimmed) {
|
|
@@ -2,6 +2,7 @@ import { writeFileSync } from 'node:fs';
|
|
|
2
2
|
import pixelmatch from 'pixelmatch';
|
|
3
3
|
import type { Page } from 'playwright';
|
|
4
4
|
import { PNG } from 'pngjs';
|
|
5
|
+
import { removeVisualAnnotations } from '../../../src/ai/researcher/coordinates.ts';
|
|
5
6
|
|
|
6
7
|
const REGION_PADDING = 30;
|
|
7
8
|
const SCREENSHOT_OPTIONS = { animations: 'disabled', caret: 'hide' } as const;
|
|
@@ -44,7 +45,7 @@ export function findChangedRegion(beforeScreenshot: Buffer, afterScreenshot: Buf
|
|
|
44
45
|
return changedPixels ? addPadding(changedPixels, before.width, before.height, padding) : null;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function saveRegion(after: PNG, region: ScreenshotRegion, filePath: string): void {
|
|
48
|
+
export function saveRegion(after: PNG, region: ScreenshotRegion, filePath: string): void {
|
|
48
49
|
const cropped = new PNG({ width: region.width, height: region.height });
|
|
49
50
|
PNG.bitblt(after, cropped, region.x, region.y, region.width, region.height, 0, 0);
|
|
50
51
|
writeFileSync(filePath, PNG.sync.write(cropped));
|
|
@@ -73,7 +74,23 @@ function findChangedPixelBounds(before: PNG, after: PNG): ScreenshotRegion | nul
|
|
|
73
74
|
return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
function
|
|
77
|
+
export function regionAround(png: PNG, box: { x: number; y: number; width: number; height: number }, viewport: { width: number; height: number } | null, padding: number): ScreenshotRegion {
|
|
78
|
+
let scaleX = 1;
|
|
79
|
+
let scaleY = 1;
|
|
80
|
+
if (viewport) {
|
|
81
|
+
scaleX = png.width / viewport.width;
|
|
82
|
+
scaleY = png.height / viewport.height;
|
|
83
|
+
}
|
|
84
|
+
const scaled: ScreenshotRegion = {
|
|
85
|
+
x: Math.round(box.x * scaleX),
|
|
86
|
+
y: Math.round(box.y * scaleY),
|
|
87
|
+
width: Math.round(box.width * scaleX),
|
|
88
|
+
height: Math.round(box.height * scaleY),
|
|
89
|
+
};
|
|
90
|
+
return addPadding(scaled, png.width, png.height, Math.round(padding * scaleX));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function addPadding(region: ScreenshotRegion, imageWidth: number, imageHeight: number, padding = REGION_PADDING): ScreenshotRegion {
|
|
77
94
|
const x = Math.max(0, region.x - padding);
|
|
78
95
|
const y = Math.max(0, region.y - padding);
|
|
79
96
|
const maxX = Math.min(imageWidth, region.x + region.width + padding);
|
|
@@ -81,14 +98,6 @@ function addPadding(region: ScreenshotRegion, imageWidth: number, imageHeight: n
|
|
|
81
98
|
return { x, y, width: maxX - x, height: maxY - y };
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
async function removeVisualAnnotations(page: Page): Promise<void> {
|
|
85
|
-
try {
|
|
86
|
-
await page.locator('[data-explorbot-annotation]').evaluateAll((elements) => {
|
|
87
|
-
for (const element of elements) element.remove();
|
|
88
|
-
});
|
|
89
|
-
} catch {}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
101
|
async function findOverlayRegion(page: Page, image: PNG, detectUnmarkedOverlay: boolean): Promise<ScreenshotRegion | null> {
|
|
93
102
|
let box: { x: number; y: number; width: number; height: number } | null = null;
|
|
94
103
|
try {
|