explorbot 0.1.32-beta.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/bin/explorbot-cli.ts +4 -3
- package/boat/doc-collector/src/ai/documentarian.ts +22 -27
- package/boat/doc-collector/src/ai/tools.ts +67 -10
- package/boat/doc-collector/src/docbot.ts +28 -7
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +33 -1
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
- package/dist/boat/doc-collector/src/ai/tools.js +47 -10
- package/dist/boat/doc-collector/src/docbot.js +25 -6
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +29 -0
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/package.json +20 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +3 -10
- package/dist/src/action.js +18 -111
- package/dist/src/ai/captain/web-mode.js +0 -7
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +4 -19
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +0 -4
- package/dist/src/ai/driller.js +3 -12
- 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 +3 -2
- package/dist/src/ai/historian/playwright.d.ts +2 -0
- package/dist/src/ai/historian/playwright.js +3 -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 +5 -4
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +3 -5
- package/dist/src/ai/navigator.js +11 -46
- package/dist/src/ai/pilot.d.ts +1 -3
- package/dist/src/ai/pilot.js +6 -17
- package/dist/src/ai/planner.d.ts +3 -3
- package/dist/src/ai/planner.js +49 -37
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -136
- package/dist/src/ai/quartermaster.d.ts +21 -22
- package/dist/src/ai/quartermaster.js +4 -10
- package/dist/src/ai/rerunner.d.ts +1 -2
- package/dist/src/ai/rerunner.js +14 -22
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/deep-analysis.js +3 -6
- 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.js +5 -1
- package/dist/src/ai/researcher.js +12 -53
- 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 +1 -1
- package/dist/src/ai/task-agent.js +2 -27
- package/dist/src/ai/tester.d.ts +0 -2
- package/dist/src/ai/tester.js +10 -61
- package/dist/src/ai/tools.d.ts +18 -7
- package/dist/src/ai/tools.js +119 -101
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-command.js +1 -1
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +3 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +2 -2
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +81 -154
- package/dist/src/explorbot.d.ts +4 -5
- package/dist/src/explorbot.js +17 -51
- package/dist/src/explorer.d.ts +4 -15
- package/dist/src/explorer.js +18 -68
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +6 -31
- package/dist/src/state-manager.js +10 -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/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/web-element.d.ts +2 -1
- package/dist/src/utils/web-element.js +3 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/package.json +20 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +16 -130
- package/src/ai/captain/web-mode.ts +0 -7
- package/src/ai/captain.ts +5 -17
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +3 -16
- 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 +4 -2
- package/src/ai/historian/playwright.ts +4 -2
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +4 -7
- package/src/ai/navigator.ts +13 -49
- package/src/ai/pilot.ts +6 -16
- package/src/ai/planner.ts +56 -44
- package/src/ai/provider.ts +85 -145
- package/src/ai/quartermaster.ts +34 -41
- package/src/ai/rerunner.ts +17 -21
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/deep-analysis.ts +3 -6
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +5 -1
- package/src/ai/researcher.ts +11 -59
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +3 -30
- package/src/ai/tester.ts +12 -73
- package/src/ai/tools.ts +119 -106
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-command.ts +1 -1
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +3 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +2 -2
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +88 -151
- package/src/explorbot.ts +17 -51
- package/src/explorer.ts +20 -72
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +15 -156
- 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/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/web-element.ts +4 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npx explorbot start https://your-app.com
|
|
|
14
14
|
|
|
15
15
|
It runs with no babysitting and reports back what it finds. This is vibe-testing.
|
|
16
16
|
|
|
17
|
-
New here? Read the [Getting Started guide](docs/getting-started.md).
|
|
17
|
+
New here? Read the [Getting Started guide](docs/basics/getting-started.md).
|
|
18
18
|
|
|
19
19
|
## Use Cases
|
|
20
20
|
|
|
@@ -54,11 +54,11 @@ Cheap, fast workers do the clicking and reading; smart managers make the calls
|
|
|
54
54
|
|
|
55
55
|
| | | |
|
|
56
56
|
|---|---|---|
|
|
57
|
-
| [Researcher](docs/
|
|
58
|
-
| [Pilot](docs/
|
|
59
|
-
| [Analyst](docs/
|
|
57
|
+
| [Researcher](docs/web-testing/agents.md) | [Planner](docs/web-testing/agents.md) | [Tester](docs/web-testing/agents.md) |
|
|
58
|
+
| [Pilot](docs/web-testing/agents.md) | [Captain](docs/web-testing/agents.md) | [Navigator](docs/web-testing/agents.md) |
|
|
59
|
+
| [Analyst](docs/web-testing/agents.md) | [Historian](docs/web-testing/agents.md) | [Fisherman](docs/web-testing/agents.md) |
|
|
60
60
|
|
|
61
|
-
See [Agents](docs/
|
|
61
|
+
See [Agents](docs/web-testing/agents.md) for what each one does.
|
|
62
62
|
|
|
63
63
|
## Core Philosophy
|
|
64
64
|
|
|
@@ -74,7 +74,7 @@ See [Agents](docs/reference/agents.md) for what each one does.
|
|
|
74
74
|
|
|
75
75
|
When tuned, Explorbot **runs autonomously for hours**, trying new scenarios on its own. The more it runs, the more it learns.
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## Tests, reports, videos
|
|
78
78
|
|
|
79
79
|
Every run leaves behind:
|
|
80
80
|
|
|
@@ -83,7 +83,7 @@ Every run leaves behind:
|
|
|
83
83
|
- **Videos** — step-by-step screencasts of every run.
|
|
84
84
|
- **Experience** — what Explorbot learned, reused to test smarter next time.
|
|
85
85
|
|
|
86
|
-
See [Automated Tests](docs/
|
|
86
|
+
See [Automated Tests](docs/web-testing/automated-tests.md) for the test output and [Reporting](docs/workflow/reporting.md) for reports.
|
|
87
87
|
|
|
88
88
|
## It works with your suite
|
|
89
89
|
|
|
@@ -94,7 +94,7 @@ Explorbot won't replace your regression tests — it covers what they can't. You
|
|
|
94
94
|
- Node.js 24+ or **Bun**
|
|
95
95
|
- An **AI provider key** — OpenRouter recommended; Groq, Cerebras, OpenAI, Anthropic, and others via the [Vercel AI SDK](https://sdk.vercel.ai/providers)
|
|
96
96
|
- A **modern terminal** — iTerm2, WARP, Kitty, Ghostty, or Windows Terminal with WSL
|
|
97
|
-
- A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/
|
|
97
|
+
- A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/basics/prerequisites.md)
|
|
98
98
|
|
|
99
99
|
If your CI runs Playwright, it runs Explorbot. No GPUs, no special runners.
|
|
100
100
|
|
|
@@ -123,25 +123,25 @@ npx explorbot start /admin/users
|
|
|
123
123
|
|
|
124
124
|
Type `/explore`, and Explorbot runs its loop on its own — research, plan, test, repeat — learning from every run.
|
|
125
125
|
|
|
126
|
-
That's the gist. The [**Getting Started guide**](docs/getting-started.md) walks through the full setup — choosing models, teaching Explorbot to log in, and picking the right feature to start on.
|
|
126
|
+
That's the gist. The [**Getting Started guide**](docs/basics/getting-started.md) walks through the full setup — choosing models, teaching Explorbot to log in, and picking the right feature to start on.
|
|
127
127
|
|
|
128
128
|
## Teaching Explorbot
|
|
129
129
|
|
|
130
130
|
Explorbot gets better when you tell it about your app:
|
|
131
131
|
|
|
132
|
-
- **Knowledge** — credentials, form rules, navigation quirks. See [Knowledge](docs/
|
|
132
|
+
- **Knowledge** — credentials, form rules, navigation quirks. See [Knowledge](docs/workflow/knowledge.md).
|
|
133
133
|
- **Rules** — per-agent, per-page instructions. See [Configuration](docs/reference/configuration.md#rules).
|
|
134
134
|
- **Experience** — learned automatically from what works.
|
|
135
135
|
|
|
136
|
-
Handling logins, cookie banners, modals, and test data takes a few lines — see [Customization](docs/
|
|
136
|
+
Handling logins, cookie banners, modals, and test data takes a few lines — see [Customization](docs/web-testing/customization.md).
|
|
137
137
|
|
|
138
138
|
## It also tests REST APIs
|
|
139
139
|
|
|
140
|
-
Point Explorbot at an OpenAPI spec and it plans and runs API tests too. See [API Testing](docs/
|
|
140
|
+
Point Explorbot at an OpenAPI spec and it plans and runs API tests too. See [API Testing](docs/api-testing/basics.md).
|
|
141
141
|
|
|
142
142
|
## Keep going
|
|
143
143
|
|
|
144
|
-
When you're ready to go deeper, the [full documentation](docs/
|
|
144
|
+
When you're ready to go deeper, the [full documentation](docs/) covers everything, starting with the [Getting Started guide](docs/basics/getting-started.md).
|
|
145
145
|
|
|
146
146
|
## FAQ
|
|
147
147
|
|
|
@@ -163,7 +163,7 @@ No. With fast open models (e.g. `openai/gpt-oss-20b` on OpenRouter or Groq), exp
|
|
|
163
163
|
Not yet.
|
|
164
164
|
|
|
165
165
|
**Can I build my own agents with it?**
|
|
166
|
-
Yes, use the programmatic API. See [Scripting](docs/
|
|
166
|
+
Yes, use the programmatic API. See [Scripting](docs/reference/scripting.md).
|
|
167
167
|
|
|
168
168
|
**Can I do the same in Cursor with Playwright MCP?**
|
|
169
169
|
Good luck running it on CI!
|
package/bin/explorbot-cli.ts
CHANGED
|
@@ -509,9 +509,10 @@ program
|
|
|
509
509
|
path: options.path || process.cwd(),
|
|
510
510
|
});
|
|
511
511
|
|
|
512
|
+
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
513
|
+
const tracker = new KnowledgeTracker();
|
|
514
|
+
|
|
512
515
|
if (url && description) {
|
|
513
|
-
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
514
|
-
const tracker = new KnowledgeTracker();
|
|
515
516
|
const result = tracker.addKnowledge(url, description);
|
|
516
517
|
const action = result.isNewFile ? 'Created' : 'Updated';
|
|
517
518
|
console.log(`Knowledge ${action} in: ${result.filename}`);
|
|
@@ -519,7 +520,7 @@ program
|
|
|
519
520
|
}
|
|
520
521
|
|
|
521
522
|
const AddKnowledge = (await import('../src/components/AddKnowledge.js')).default;
|
|
522
|
-
render(React.createElement(AddKnowledge, { initialUrl: url || '' }), {
|
|
523
|
+
render(React.createElement(AddKnowledge, { initialUrl: url || '', knowledgeTracker: tracker }), {
|
|
523
524
|
exitOnCtrlC: false,
|
|
524
525
|
patchConsole: false,
|
|
525
526
|
});
|
|
@@ -5,7 +5,7 @@ import type Explorer from '../../../../src/explorer.ts';
|
|
|
5
5
|
import type { WebPageState } from '../../../../src/state-manager.ts';
|
|
6
6
|
import { tag } from '../../../../src/utils/logger.ts';
|
|
7
7
|
import type { DocbotConfig } from '../config.ts';
|
|
8
|
-
import { collectDocInteractions } from './tools.ts';
|
|
8
|
+
import { type CaptureInteractionState, type DocStateTransition, collectDocInteractions } from './tools.ts';
|
|
9
9
|
|
|
10
10
|
class Documentarian {
|
|
11
11
|
private provider: AIProvider;
|
|
@@ -18,7 +18,7 @@ class Documentarian {
|
|
|
18
18
|
this.explorer = explorer;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async document(state: WebPageState, research: string): Promise<PageDocumentation> {
|
|
21
|
+
async document(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
|
|
22
22
|
const interactiveEnabled = this.config.docs?.interactive === true && this.explorer;
|
|
23
23
|
if (!interactiveEnabled) {
|
|
24
24
|
tag('info').log('Documentarian: Using static mode (interactive disabled or no explorer)');
|
|
@@ -26,7 +26,7 @@ class Documentarian {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
tag('info').log('Documentarian: Using interactive mode with tools');
|
|
29
|
-
return this.documentWithInteraction(state, research);
|
|
29
|
+
return this.documentWithInteraction(state, research, captureState);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
private async documentStatic(state: WebPageState, research: string): Promise<PageDocumentation> {
|
|
@@ -41,12 +41,13 @@ class Documentarian {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
private async documentWithInteraction(state: WebPageState, research: string): Promise<PageDocumentation> {
|
|
44
|
+
private async documentWithInteraction(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
|
|
45
|
+
let meaningfulInteractions: StateTransition[] = [];
|
|
45
46
|
try {
|
|
46
47
|
tag('info').log('Starting interactive exploration...');
|
|
47
48
|
|
|
48
|
-
const deterministicInteractions = await collectDocInteractions(this.explorer!, state, research, this.config);
|
|
49
|
-
|
|
49
|
+
const deterministicInteractions = await collectDocInteractions(this.explorer!, state, research, this.config, captureState);
|
|
50
|
+
meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
|
|
50
51
|
if (meaningfulInteractions.length > 0) {
|
|
51
52
|
tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
|
|
52
53
|
return await this.generateDocumentationWithInteractions(state, research, meaningfulInteractions);
|
|
@@ -61,7 +62,20 @@ class Documentarian {
|
|
|
61
62
|
return this.documentStatic(state, research);
|
|
62
63
|
} catch (error) {
|
|
63
64
|
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
-
tag('warning').log(`Interactive documentation failed: ${message}
|
|
65
|
+
tag('warning').log(`Interactive documentation failed: ${message}.`);
|
|
66
|
+
if (meaningfulInteractions.length > 0) {
|
|
67
|
+
tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
|
|
68
|
+
return this.normalizeDocumentation(
|
|
69
|
+
{
|
|
70
|
+
summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
|
|
71
|
+
can: [],
|
|
72
|
+
might: [],
|
|
73
|
+
interactions: meaningfulInteractions,
|
|
74
|
+
},
|
|
75
|
+
state,
|
|
76
|
+
research
|
|
77
|
+
);
|
|
78
|
+
}
|
|
65
79
|
return this.documentStatic(state, research);
|
|
66
80
|
}
|
|
67
81
|
}
|
|
@@ -354,26 +368,7 @@ const pageDocumentationSchema = z.object({
|
|
|
354
368
|
interactions: z.array(stateTransitionSchema).nullable(),
|
|
355
369
|
});
|
|
356
370
|
|
|
357
|
-
type StateTransition =
|
|
358
|
-
action: string;
|
|
359
|
-
before: string;
|
|
360
|
-
after: string;
|
|
361
|
-
targetUrl?: string | null;
|
|
362
|
-
discoveredUrls?: string[] | null;
|
|
363
|
-
newCapabilities?: string[] | null;
|
|
364
|
-
element?: {
|
|
365
|
-
role: string;
|
|
366
|
-
name: string;
|
|
367
|
-
section: string;
|
|
368
|
-
container?: string | null;
|
|
369
|
-
locator?: string | null;
|
|
370
|
-
} | null;
|
|
371
|
-
changes?: {
|
|
372
|
-
urlChanged: boolean;
|
|
373
|
-
newElements: number;
|
|
374
|
-
removedElements: number;
|
|
375
|
-
} | null;
|
|
376
|
-
};
|
|
371
|
+
type StateTransition = DocStateTransition;
|
|
377
372
|
type PageDocumentation = Omit<z.infer<typeof pageDocumentationSchema>, 'interactions'> & {
|
|
378
373
|
interactions?: StateTransition[];
|
|
379
374
|
qualityNotes?: string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ResearchElement, parseResearchSections } from '../../../../src/ai/researcher/parser.ts';
|
|
2
2
|
import type Explorer from '../../../../src/explorer.ts';
|
|
3
3
|
import type { WebPageState } from '../../../../src/state-manager.ts';
|
|
4
|
+
import { detectFocusArea } from '../../../../src/utils/aria.ts';
|
|
4
5
|
import type { DocbotConfig } from '../config.ts';
|
|
5
6
|
|
|
6
7
|
export interface DocStateTransition {
|
|
@@ -12,6 +13,8 @@ export interface DocStateTransition {
|
|
|
12
13
|
newCapabilities?: string[];
|
|
13
14
|
element?: InteractionElement;
|
|
14
15
|
changes?: InteractionChanges;
|
|
16
|
+
targetState?: InteractionState;
|
|
17
|
+
screenshot?: InteractionScreenshot;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
interface InteractionCandidate {
|
|
@@ -35,6 +38,19 @@ interface InteractionChanges {
|
|
|
35
38
|
removedElements: number;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
export interface InteractionState {
|
|
42
|
+
kind: 'page' | 'dialog' | 'modal' | 'section';
|
|
43
|
+
label: string;
|
|
44
|
+
url: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InteractionScreenshot {
|
|
48
|
+
title: string;
|
|
49
|
+
relativePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type CaptureInteractionState = (state: WebPageState, transition: DocStateTransition) => Promise<InteractionScreenshot | null>;
|
|
53
|
+
|
|
38
54
|
const DEFAULT_MAX_PRIMARY_CANDIDATES = 3;
|
|
39
55
|
const DEFAULT_MAX_INTERACTIONS = 5;
|
|
40
56
|
const MAX_LINKS = 15;
|
|
@@ -42,14 +58,14 @@ const DEFAULT_WAIT_MS = 700;
|
|
|
42
58
|
const TAB_WAIT_MS = 500;
|
|
43
59
|
const DEFAULT_DENIED_ACTION_LABELS = ['delete', 'remove', 'destroy', 'archive', 'discard', 'logout', 'sign out', 'signout', 'sign_out', 'erase', 'drop'];
|
|
44
60
|
|
|
45
|
-
export async function collectDocInteractions(explorer: Explorer, state: WebPageState, research: string, config: DocbotConfig = {}): Promise<DocStateTransition[]> {
|
|
61
|
+
export async function collectDocInteractions(explorer: Explorer, state: WebPageState, research: string, config: DocbotConfig = {}, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
46
62
|
const sections = parseResearchSections(research);
|
|
47
63
|
const transitions: DocStateTransition[] = [];
|
|
48
64
|
const maxInteractions = getPositiveConfigNumber(config.docs?.maxInteractions, DEFAULT_MAX_INTERACTIONS);
|
|
49
65
|
const tabGroup = findTabGroup(sections);
|
|
50
66
|
|
|
51
67
|
if (tabGroup) {
|
|
52
|
-
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions)));
|
|
68
|
+
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions, captureState)));
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
for (const candidate of findActionCandidates(sections, config)) {
|
|
@@ -57,7 +73,7 @@ export async function collectDocInteractions(explorer: Explorer, state: WebPageS
|
|
|
57
73
|
break;
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS);
|
|
76
|
+
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
61
77
|
if (!transition) {
|
|
62
78
|
continue;
|
|
63
79
|
}
|
|
@@ -76,7 +92,7 @@ export function pickDocActionCandidates(research: string, config: DocbotConfig =
|
|
|
76
92
|
}));
|
|
77
93
|
}
|
|
78
94
|
|
|
79
|
-
async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number): Promise<DocStateTransition[]> {
|
|
95
|
+
async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
80
96
|
const transitions: DocStateTransition[] = [];
|
|
81
97
|
|
|
82
98
|
for (const element of tabGroup.elements) {
|
|
@@ -93,7 +109,8 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
93
109
|
sectionName: tabGroup.sectionName,
|
|
94
110
|
},
|
|
95
111
|
restoreUrl,
|
|
96
|
-
TAB_WAIT_MS
|
|
112
|
+
TAB_WAIT_MS,
|
|
113
|
+
captureState
|
|
97
114
|
);
|
|
98
115
|
if (!transition) {
|
|
99
116
|
continue;
|
|
@@ -106,7 +123,7 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
106
123
|
return transitions;
|
|
107
124
|
}
|
|
108
125
|
|
|
109
|
-
async function executeInteraction(explorer: Explorer, candidate: InteractionCandidate, restoreUrl: string, waitMs: number): Promise<DocStateTransition | null> {
|
|
126
|
+
async function executeInteraction(explorer: Explorer, candidate: InteractionCandidate, restoreUrl: string, waitMs: number, captureState?: CaptureInteractionState): Promise<DocStateTransition | null> {
|
|
110
127
|
const beforeState = explorer.getStateManager().getCurrentState();
|
|
111
128
|
if (!beforeState) {
|
|
112
129
|
return null;
|
|
@@ -132,7 +149,14 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
132
149
|
removedElements: ariaChanges.removedCount,
|
|
133
150
|
});
|
|
134
151
|
|
|
135
|
-
if (
|
|
152
|
+
if (captureState && isMeaningfulStateTransition(transition)) {
|
|
153
|
+
const screenshot = await captureState(afterState, transition);
|
|
154
|
+
if (screenshot) {
|
|
155
|
+
transition.screenshot = screenshot;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (urlChanged || ariaChanges.newCount > 0) {
|
|
136
160
|
await restoreInteractionState(explorer, restoreUrl);
|
|
137
161
|
}
|
|
138
162
|
|
|
@@ -143,7 +167,7 @@ async function attemptInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
143
167
|
const action = explorer.createAction();
|
|
144
168
|
|
|
145
169
|
for (const command of buildClickCommands(candidate.element, candidate.container)) {
|
|
146
|
-
const success = await action.attempt(command, buildPurpose(candidate)
|
|
170
|
+
const success = await action.attempt(command, buildPurpose(candidate));
|
|
147
171
|
if (success) {
|
|
148
172
|
return true;
|
|
149
173
|
}
|
|
@@ -155,7 +179,7 @@ async function attemptInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
155
179
|
async function restoreInteractionState(explorer: Explorer, restoreUrl: string, primaryCommand?: string | null): Promise<void> {
|
|
156
180
|
if (primaryCommand) {
|
|
157
181
|
const action = explorer.createAction();
|
|
158
|
-
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}
|
|
182
|
+
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`);
|
|
159
183
|
if (restored) {
|
|
160
184
|
await wait(TAB_WAIT_MS);
|
|
161
185
|
return;
|
|
@@ -163,7 +187,7 @@ async function restoreInteractionState(explorer: Explorer, restoreUrl: string, p
|
|
|
163
187
|
}
|
|
164
188
|
|
|
165
189
|
const action = explorer.createAction();
|
|
166
|
-
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}
|
|
190
|
+
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`);
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
function buildTransition(candidate: InteractionCandidate, beforeState: WebPageState, afterState: WebPageState, changes: InteractionChanges): DocStateTransition {
|
|
@@ -175,6 +199,7 @@ function buildTransition(candidate: InteractionCandidate, beforeState: WebPageSt
|
|
|
175
199
|
newCapabilities: collectDiscoveryNotes(afterState, changes),
|
|
176
200
|
element: buildInteractionElement(candidate),
|
|
177
201
|
changes,
|
|
202
|
+
targetState: describeTargetState(beforeState, afterState, candidate),
|
|
178
203
|
};
|
|
179
204
|
|
|
180
205
|
if (changes.urlChanged) {
|
|
@@ -184,6 +209,38 @@ function buildTransition(candidate: InteractionCandidate, beforeState: WebPageSt
|
|
|
184
209
|
return transition;
|
|
185
210
|
}
|
|
186
211
|
|
|
212
|
+
function describeTargetState(beforeState: WebPageState, afterState: WebPageState, candidate: InteractionCandidate): InteractionState {
|
|
213
|
+
const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
|
|
214
|
+
const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
|
|
215
|
+
if (afterFocus.detected && (!beforeFocus.detected || beforeFocus.name !== afterFocus.name)) {
|
|
216
|
+
return {
|
|
217
|
+
kind: afterFocus.type || 'dialog',
|
|
218
|
+
label: afterFocus.name || candidate.element.name.trim(),
|
|
219
|
+
url: afterState.url,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const beforePath = beforeState.url.split('?')[0].split('#')[0];
|
|
224
|
+
const afterPath = afterState.url.split('?')[0].split('#')[0];
|
|
225
|
+
const headings = collectHeadings(afterState);
|
|
226
|
+
let kind: InteractionState['kind'] = 'page';
|
|
227
|
+
if (beforePath === afterPath) {
|
|
228
|
+
kind = 'section';
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
kind,
|
|
232
|
+
label: headings[0] || afterState.title || candidate.element.name.trim(),
|
|
233
|
+
url: afterState.url,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isMeaningfulStateTransition(transition: DocStateTransition): boolean {
|
|
238
|
+
if (transition.targetUrl || transition.changes?.urlChanged) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
return (transition.changes?.newElements || 0) > 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
187
244
|
function buildInteractionElement(candidate: InteractionCandidate): InteractionElement {
|
|
188
245
|
const element: InteractionElement = {
|
|
189
246
|
role: candidate.role,
|
|
@@ -8,9 +8,10 @@ import { sanitizeFilename } from '../../../src/utils/strings.ts';
|
|
|
8
8
|
import { Documentarian, type PageDocumentation } from './ai/documentarian.ts';
|
|
9
9
|
import { type DocbotConfig, DocbotConfigParser } from './config.ts';
|
|
10
10
|
import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
|
|
11
|
+
import { renderMermaidBody } from './state-diagram.ts';
|
|
11
12
|
import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
|
|
12
13
|
import { extractResearchNavigationTargets } from './research-navigation.ts';
|
|
13
|
-
import { type DocumentationScreenshot, captureDocumentationScreenshots } from './screenshots.ts';
|
|
14
|
+
import { type DocumentationScreenshot, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
15
|
|
|
15
16
|
class DocBot {
|
|
16
17
|
private explorBot: ExplorBot;
|
|
@@ -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,7 +3,9 @@ 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;
|
|
@@ -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.playwrightHelper?.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
|
|