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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { normalizeUrl } from '../../../src/state-manager.ts';
|
|
3
|
+
import { normalizeInlineText } from '../../../src/utils/strings.ts';
|
|
4
|
+
import type { StateTransition } from './ai/documentarian.ts';
|
|
5
|
+
|
|
6
|
+
function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph {
|
|
7
|
+
const pageIds = new Map<string, string>();
|
|
8
|
+
const pageNodes: StateNode[] = [];
|
|
9
|
+
const adjacency = new Map<string, Set<string>>();
|
|
10
|
+
const classAssignment = new Map<StateClass, string[]>([
|
|
11
|
+
['page', []],
|
|
12
|
+
['dialog', []],
|
|
13
|
+
['section', []],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
for (const [index, page] of pages.entries()) {
|
|
17
|
+
const id = `page${index}`;
|
|
18
|
+
pageIds.set(normalizeUrl(page.url), id);
|
|
19
|
+
adjacency.set(id, new Set());
|
|
20
|
+
classAssignment.get('page')?.push(id);
|
|
21
|
+
pageNodes.push({ id, kind: 'page', label: page.title || page.url, subLabel: page.url, filePath: page.filePath });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const stateKeys = new Map<string, string>();
|
|
25
|
+
const transientByPage = new Map<string, StateNode[]>();
|
|
26
|
+
const clicks: StateClick[] = [];
|
|
27
|
+
const edges: StateEdge[] = [];
|
|
28
|
+
const drawnBack = new Set<string>();
|
|
29
|
+
let stateIndex = 0;
|
|
30
|
+
|
|
31
|
+
for (const [pageIndex, page] of pages.entries()) {
|
|
32
|
+
const sourceId = `page${pageIndex}`;
|
|
33
|
+
for (const transition of page.interactions || []) {
|
|
34
|
+
const targetState = transition.targetState;
|
|
35
|
+
if (!targetState) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalizedTarget = normalizeUrl(targetState.url);
|
|
40
|
+
const isPageTarget = targetState.kind === 'page';
|
|
41
|
+
let pageTargetId: string | undefined;
|
|
42
|
+
if (isPageTarget) {
|
|
43
|
+
pageTargetId = pageIds.get(normalizedTarget);
|
|
44
|
+
}
|
|
45
|
+
let targetId: string | undefined;
|
|
46
|
+
if (pageTargetId && pageTargetId !== sourceId) {
|
|
47
|
+
targetId = pageTargetId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!targetId) {
|
|
51
|
+
const stateKey = `${sourceId}:${targetState.kind}:${normalizeInlineText(targetState.label)}:${normalizedTarget}`;
|
|
52
|
+
targetId = stateKeys.get(stateKey);
|
|
53
|
+
if (!targetId) {
|
|
54
|
+
targetId = `state${stateIndex++}`;
|
|
55
|
+
stateKeys.set(stateKey, targetId);
|
|
56
|
+
adjacency.set(targetId, new Set());
|
|
57
|
+
classAssignment.get(classForKind(targetState.kind))?.push(targetId);
|
|
58
|
+
const list = transientByPage.get(sourceId) ?? [];
|
|
59
|
+
list.push({ id: targetId, kind: targetState.kind, label: targetState.label, subLabel: targetState.kind, parentPageId: sourceId });
|
|
60
|
+
transientByPage.set(sourceId, list);
|
|
61
|
+
if (transition.screenshot) {
|
|
62
|
+
const screenshotPath = path.resolve(path.dirname(page.filePath), transition.screenshot.relativePath);
|
|
63
|
+
clicks.push({ node: targetId, target: path.relative(outputDir, screenshotPath).replaceAll('\\', '/'), tooltip: 'Open state screenshot' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!targetId) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pairKey = `${sourceId}>${targetId}`;
|
|
73
|
+
if (adjacency.get(targetId)?.has(sourceId)) {
|
|
74
|
+
if (drawnBack.has(pairKey)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
drawnBack.add(pairKey);
|
|
78
|
+
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: true });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (adjacency.get(sourceId)?.has(targetId) || createsCycle(sourceId, targetId, adjacency)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
adjacency.get(sourceId)?.add(targetId);
|
|
86
|
+
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: false });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const pageClicks: StateClick[] = [];
|
|
91
|
+
for (const pageNode of pageNodes) {
|
|
92
|
+
const filePath = pageNode.filePath;
|
|
93
|
+
if (!filePath) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const relativeFile = path.relative(outputDir, filePath).replaceAll('\\', '/');
|
|
97
|
+
pageClicks.push({ node: pageNode.id, target: relativeFile, tooltip: `Open ${pageNode.label}` });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { pages: pageNodes, transientByPage, edges, clicks: [...pageClicks, ...clicks], classAssignment };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderMermaidBody(outputDir: string, pages: DocumentedPage[]): string {
|
|
104
|
+
return renderMermaidFromGraph(buildStateGraph(outputDir, pages));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderMermaidFromGraph(graph: StateGraph): string {
|
|
108
|
+
const lines: string[] = ['flowchart TD'];
|
|
109
|
+
if (graph.pages.length === 0) {
|
|
110
|
+
lines.push(' empty["No documented states"]');
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const page of graph.pages) {
|
|
115
|
+
lines.push(` ${renderNodeLine(page)}`);
|
|
116
|
+
const children = graph.transientByPage.get(page.id);
|
|
117
|
+
if (!children || children.length === 0) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
lines.push(` subgraph sg_${page.id} ["${escapeMermaidLabel(page.label)} — transient states"]`);
|
|
121
|
+
for (const child of children) {
|
|
122
|
+
lines.push(` ${renderNodeLine(child)}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push(' end');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const edge of graph.edges) {
|
|
128
|
+
let arrow = '-->';
|
|
129
|
+
if (edge.isBack) {
|
|
130
|
+
arrow = '-.->';
|
|
131
|
+
}
|
|
132
|
+
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push(' classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a;');
|
|
136
|
+
lines.push(' classDef dialog fill:#ffedd5,stroke:#ea580c,color:#0f172a;');
|
|
137
|
+
lines.push(' classDef section fill:#f3e8ff,stroke:#9333ea,color:#0f172a;');
|
|
138
|
+
for (const [className, ids] of graph.classAssignment) {
|
|
139
|
+
if (ids.length === 0) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
lines.push(` class ${ids.join(',')} ${className};`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const click of graph.clicks) {
|
|
146
|
+
lines.push(` click ${click.node} "${click.target}" "${escapeMermaidLabel(click.tooltip)}"`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderStateMapFromGraph(graph: StateGraph): string {
|
|
153
|
+
if (graph.pages.length === 0) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
const clickByNode = new Map(graph.clicks.map((click) => [click.node, click]));
|
|
157
|
+
const rows = ['| State | Type | Open |', '| --- | --- | --- |'];
|
|
158
|
+
for (const pageNode of graph.pages) {
|
|
159
|
+
const pageClick = clickByNode.get(pageNode.id);
|
|
160
|
+
let openCell = '—';
|
|
161
|
+
if (pageClick) {
|
|
162
|
+
openCell = `[open page](${pageClick.target})`;
|
|
163
|
+
}
|
|
164
|
+
rows.push(`| ${escapeTable(pageNode.label)} | page | ${openCell} |`);
|
|
165
|
+
for (const child of graph.transientByPage.get(pageNode.id) ?? []) {
|
|
166
|
+
const childClick = clickByNode.get(child.id);
|
|
167
|
+
let childCell = '—';
|
|
168
|
+
if (childClick) {
|
|
169
|
+
childCell = `[view screenshot](${childClick.target})`;
|
|
170
|
+
}
|
|
171
|
+
rows.push(`| ${escapeTable(child.label)} | ${child.kind} | ${childCell} |`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return rows.join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderNodeLine(node: StateNode): string {
|
|
178
|
+
const label = `${escapeMermaidLabel(node.label)}<br/>${escapeMermaidLabel(node.subLabel)}`;
|
|
179
|
+
if (node.kind === 'dialog' || node.kind === 'modal') {
|
|
180
|
+
return `${node.id}{{"${label}"}}`;
|
|
181
|
+
}
|
|
182
|
+
if (node.kind === 'section') {
|
|
183
|
+
return `${node.id}("${label}")`;
|
|
184
|
+
}
|
|
185
|
+
return `${node.id}["${label}"]`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createsCycle(sourceId: string, targetId: string, adjacency: Map<string, Set<string>>): boolean {
|
|
189
|
+
if (sourceId === targetId) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const pending = [targetId];
|
|
194
|
+
const visited = new Set<string>();
|
|
195
|
+
while (pending.length > 0) {
|
|
196
|
+
const nodeId = pending.pop();
|
|
197
|
+
if (!nodeId || visited.has(nodeId)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (nodeId === sourceId) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
visited.add(nodeId);
|
|
204
|
+
pending.push(...(adjacency.get(nodeId) || []));
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function escapeMermaidLabel(value: string): string {
|
|
210
|
+
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('|', '|');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function escapeTable(value: string): string {
|
|
214
|
+
return normalizeInlineText(value).replaceAll('|', '\\|');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function classForKind(kind: StateKind): StateClass {
|
|
218
|
+
if (kind === 'section') {
|
|
219
|
+
return 'section';
|
|
220
|
+
}
|
|
221
|
+
if (kind === 'page') {
|
|
222
|
+
return 'page';
|
|
223
|
+
}
|
|
224
|
+
return 'dialog';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface DocumentedPage {
|
|
228
|
+
url: string;
|
|
229
|
+
title: string;
|
|
230
|
+
summary: string;
|
|
231
|
+
canCount: number;
|
|
232
|
+
mightCount: number;
|
|
233
|
+
interactionCount: number;
|
|
234
|
+
canActions: string[];
|
|
235
|
+
mightActions: string[];
|
|
236
|
+
interactionActions: string[];
|
|
237
|
+
qualityNotes: string[];
|
|
238
|
+
interactions?: StateTransition[];
|
|
239
|
+
filePath: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface SkippedPage {
|
|
243
|
+
url: string;
|
|
244
|
+
reason: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type StateKind = 'page' | 'dialog' | 'modal' | 'section';
|
|
248
|
+
type StateClass = 'page' | 'dialog' | 'section';
|
|
249
|
+
|
|
250
|
+
interface StateNode {
|
|
251
|
+
id: string;
|
|
252
|
+
kind: StateKind;
|
|
253
|
+
label: string;
|
|
254
|
+
subLabel: string;
|
|
255
|
+
filePath?: string;
|
|
256
|
+
parentPageId?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface StateEdge {
|
|
260
|
+
source: string;
|
|
261
|
+
target: string;
|
|
262
|
+
action: string;
|
|
263
|
+
isBack: boolean;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface StateClick {
|
|
267
|
+
node: string;
|
|
268
|
+
target: string;
|
|
269
|
+
tooltip: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface StateGraph {
|
|
273
|
+
pages: StateNode[];
|
|
274
|
+
transientByPage: Map<string, StateNode[]>;
|
|
275
|
+
edges: StateEdge[];
|
|
276
|
+
clicks: StateClick[];
|
|
277
|
+
classAssignment: Map<StateClass, string[]>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderStateMapFromGraph };
|
|
281
|
+
export type { DocumentedPage, SkippedPage, StateGraph, StateNode, StateEdge, StateClick };
|
|
@@ -454,16 +454,16 @@ program
|
|
|
454
454
|
await ConfigParser.getInstance().loadConfig({
|
|
455
455
|
path: options.path || process.cwd(),
|
|
456
456
|
});
|
|
457
|
+
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
458
|
+
const tracker = new KnowledgeTracker();
|
|
457
459
|
if (url && description) {
|
|
458
|
-
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
459
|
-
const tracker = new KnowledgeTracker();
|
|
460
460
|
const result = tracker.addKnowledge(url, description);
|
|
461
461
|
const action = result.isNewFile ? 'Created' : 'Updated';
|
|
462
462
|
console.log(`Knowledge ${action} in: ${result.filename}`);
|
|
463
463
|
return;
|
|
464
464
|
}
|
|
465
465
|
const AddKnowledge = (await import('../src/components/AddKnowledge.js')).default;
|
|
466
|
-
render(React.createElement(AddKnowledge, { initialUrl: url || '' }), {
|
|
466
|
+
render(React.createElement(AddKnowledge, { initialUrl: url || '', knowledgeTracker: tracker }), {
|
|
467
467
|
exitOnCtrlC: false,
|
|
468
468
|
patchConsole: false,
|
|
469
469
|
});
|
|
@@ -11,14 +11,14 @@ class Documentarian {
|
|
|
11
11
|
this.config = config;
|
|
12
12
|
this.explorer = explorer;
|
|
13
13
|
}
|
|
14
|
-
async document(state, research) {
|
|
14
|
+
async document(state, research, captureState) {
|
|
15
15
|
const interactiveEnabled = this.config.docs?.interactive === true && this.explorer;
|
|
16
16
|
if (!interactiveEnabled) {
|
|
17
17
|
tag('info').log('Documentarian: Using static mode (interactive disabled or no explorer)');
|
|
18
18
|
return this.documentStatic(state, research);
|
|
19
19
|
}
|
|
20
20
|
tag('info').log('Documentarian: Using interactive mode with tools');
|
|
21
|
-
return this.documentWithInteraction(state, research);
|
|
21
|
+
return this.documentWithInteraction(state, research, captureState);
|
|
22
22
|
}
|
|
23
23
|
async documentStatic(state, research) {
|
|
24
24
|
try {
|
|
@@ -31,11 +31,12 @@ class Documentarian {
|
|
|
31
31
|
return this.generateDocumentation(state, this.sanitizeResearch(research), true);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
async documentWithInteraction(state, research) {
|
|
34
|
+
async documentWithInteraction(state, research, captureState) {
|
|
35
|
+
let meaningfulInteractions = [];
|
|
35
36
|
try {
|
|
36
37
|
tag('info').log('Starting interactive exploration...');
|
|
37
|
-
const deterministicInteractions = await collectDocInteractions(this.explorer, state, research, this.config);
|
|
38
|
-
|
|
38
|
+
const deterministicInteractions = await collectDocInteractions(this.explorer, state, research, this.config, captureState);
|
|
39
|
+
meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
|
|
39
40
|
if (meaningfulInteractions.length > 0) {
|
|
40
41
|
tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
|
|
41
42
|
return await this.generateDocumentationWithInteractions(state, research, meaningfulInteractions);
|
|
@@ -50,7 +51,16 @@ class Documentarian {
|
|
|
50
51
|
}
|
|
51
52
|
catch (error) {
|
|
52
53
|
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
-
tag('warning').log(`Interactive documentation failed: ${message}
|
|
54
|
+
tag('warning').log(`Interactive documentation failed: ${message}.`);
|
|
55
|
+
if (meaningfulInteractions.length > 0) {
|
|
56
|
+
tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
|
|
57
|
+
return this.normalizeDocumentation({
|
|
58
|
+
summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
|
|
59
|
+
can: [],
|
|
60
|
+
might: [],
|
|
61
|
+
interactions: meaningfulInteractions,
|
|
62
|
+
}, state, research);
|
|
63
|
+
}
|
|
54
64
|
return this.documentStatic(state, research);
|
|
55
65
|
}
|
|
56
66
|
}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { parseResearchSections } from "../../../../src/ai/researcher/parser.js";
|
|
2
|
+
import { detectFocusArea } from "../../../../src/utils/aria.js";
|
|
2
3
|
const DEFAULT_MAX_PRIMARY_CANDIDATES = 3;
|
|
3
4
|
const DEFAULT_MAX_INTERACTIONS = 5;
|
|
4
5
|
const MAX_LINKS = 15;
|
|
5
6
|
const DEFAULT_WAIT_MS = 700;
|
|
6
7
|
const TAB_WAIT_MS = 500;
|
|
7
8
|
const DEFAULT_DENIED_ACTION_LABELS = ['delete', 'remove', 'destroy', 'archive', 'discard', 'logout', 'sign out', 'signout', 'sign_out', 'erase', 'drop'];
|
|
8
|
-
export async function collectDocInteractions(explorer, state, research, config = {}) {
|
|
9
|
+
export async function collectDocInteractions(explorer, state, research, config = {}, captureState) {
|
|
9
10
|
const sections = parseResearchSections(research);
|
|
10
11
|
const transitions = [];
|
|
11
12
|
const maxInteractions = getPositiveConfigNumber(config.docs?.maxInteractions, DEFAULT_MAX_INTERACTIONS);
|
|
12
13
|
const tabGroup = findTabGroup(sections);
|
|
13
14
|
if (tabGroup) {
|
|
14
|
-
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions)));
|
|
15
|
+
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions, captureState)));
|
|
15
16
|
}
|
|
16
17
|
for (const candidate of findActionCandidates(sections, config)) {
|
|
17
18
|
if (transitions.length >= maxInteractions) {
|
|
18
19
|
break;
|
|
19
20
|
}
|
|
20
|
-
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS);
|
|
21
|
+
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
21
22
|
if (!transition) {
|
|
22
23
|
continue;
|
|
23
24
|
}
|
|
@@ -32,7 +33,7 @@ export function pickDocActionCandidates(research, config = {}) {
|
|
|
32
33
|
section: candidate.sectionName,
|
|
33
34
|
}));
|
|
34
35
|
}
|
|
35
|
-
async function exploreTabGroup(explorer, tabGroup, restoreUrl, maxInteractions) {
|
|
36
|
+
async function exploreTabGroup(explorer, tabGroup, restoreUrl, maxInteractions, captureState) {
|
|
36
37
|
const transitions = [];
|
|
37
38
|
for (const element of tabGroup.elements) {
|
|
38
39
|
if (transitions.length >= maxInteractions) {
|
|
@@ -43,7 +44,7 @@ async function exploreTabGroup(explorer, tabGroup, restoreUrl, maxInteractions)
|
|
|
43
44
|
container: tabGroup.container,
|
|
44
45
|
role: 'tab',
|
|
45
46
|
sectionName: tabGroup.sectionName,
|
|
46
|
-
}, restoreUrl, TAB_WAIT_MS);
|
|
47
|
+
}, restoreUrl, TAB_WAIT_MS, captureState);
|
|
47
48
|
if (!transition) {
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
@@ -52,7 +53,7 @@ async function exploreTabGroup(explorer, tabGroup, restoreUrl, maxInteractions)
|
|
|
52
53
|
await restoreInteractionState(explorer, restoreUrl, buildPrimaryCommand(tabGroup.elements[0], tabGroup.container));
|
|
53
54
|
return transitions;
|
|
54
55
|
}
|
|
55
|
-
async function executeInteraction(explorer, candidate, restoreUrl, waitMs) {
|
|
56
|
+
async function executeInteraction(explorer, candidate, restoreUrl, waitMs, captureState) {
|
|
56
57
|
const beforeState = explorer.getStateManager().getCurrentState();
|
|
57
58
|
if (!beforeState) {
|
|
58
59
|
return null;
|
|
@@ -73,7 +74,13 @@ async function executeInteraction(explorer, candidate, restoreUrl, waitMs) {
|
|
|
73
74
|
newElements: ariaChanges.newCount,
|
|
74
75
|
removedElements: ariaChanges.removedCount,
|
|
75
76
|
});
|
|
76
|
-
if (
|
|
77
|
+
if (captureState && isMeaningfulStateTransition(transition)) {
|
|
78
|
+
const screenshot = await captureState(afterState, transition);
|
|
79
|
+
if (screenshot) {
|
|
80
|
+
transition.screenshot = screenshot;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (urlChanged || ariaChanges.newCount > 0) {
|
|
77
84
|
await restoreInteractionState(explorer, restoreUrl);
|
|
78
85
|
}
|
|
79
86
|
return transition;
|
|
@@ -81,7 +88,7 @@ async function executeInteraction(explorer, candidate, restoreUrl, waitMs) {
|
|
|
81
88
|
async function attemptInteraction(explorer, candidate) {
|
|
82
89
|
const action = explorer.createAction();
|
|
83
90
|
for (const command of buildClickCommands(candidate.element, candidate.container)) {
|
|
84
|
-
const success = await action.attempt(command, buildPurpose(candidate)
|
|
91
|
+
const success = await action.attempt(command, buildPurpose(candidate));
|
|
85
92
|
if (success) {
|
|
86
93
|
return true;
|
|
87
94
|
}
|
|
@@ -91,14 +98,14 @@ async function attemptInteraction(explorer, candidate) {
|
|
|
91
98
|
async function restoreInteractionState(explorer, restoreUrl, primaryCommand) {
|
|
92
99
|
if (primaryCommand) {
|
|
93
100
|
const action = explorer.createAction();
|
|
94
|
-
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}
|
|
101
|
+
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`);
|
|
95
102
|
if (restored) {
|
|
96
103
|
await wait(TAB_WAIT_MS);
|
|
97
104
|
return;
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
107
|
const action = explorer.createAction();
|
|
101
|
-
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}
|
|
108
|
+
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`);
|
|
102
109
|
}
|
|
103
110
|
function buildTransition(candidate, beforeState, afterState, changes) {
|
|
104
111
|
const transition = {
|
|
@@ -109,12 +116,42 @@ function buildTransition(candidate, beforeState, afterState, changes) {
|
|
|
109
116
|
newCapabilities: collectDiscoveryNotes(afterState, changes),
|
|
110
117
|
element: buildInteractionElement(candidate),
|
|
111
118
|
changes,
|
|
119
|
+
targetState: describeTargetState(beforeState, afterState, candidate),
|
|
112
120
|
};
|
|
113
121
|
if (changes.urlChanged) {
|
|
114
122
|
transition.targetUrl = afterState.url;
|
|
115
123
|
}
|
|
116
124
|
return transition;
|
|
117
125
|
}
|
|
126
|
+
function describeTargetState(beforeState, afterState, candidate) {
|
|
127
|
+
const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
|
|
128
|
+
const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
|
|
129
|
+
if (afterFocus.detected && (!beforeFocus.detected || beforeFocus.name !== afterFocus.name)) {
|
|
130
|
+
return {
|
|
131
|
+
kind: afterFocus.type || 'dialog',
|
|
132
|
+
label: afterFocus.name || candidate.element.name.trim(),
|
|
133
|
+
url: afterState.url,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const beforePath = beforeState.url.split('?')[0].split('#')[0];
|
|
137
|
+
const afterPath = afterState.url.split('?')[0].split('#')[0];
|
|
138
|
+
const headings = collectHeadings(afterState);
|
|
139
|
+
let kind = 'page';
|
|
140
|
+
if (beforePath === afterPath) {
|
|
141
|
+
kind = 'section';
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
kind,
|
|
145
|
+
label: headings[0] || afterState.title || candidate.element.name.trim(),
|
|
146
|
+
url: afterState.url,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function isMeaningfulStateTransition(transition) {
|
|
150
|
+
if (transition.targetUrl || transition.changes?.urlChanged) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return (transition.changes?.newElements || 0) > 0;
|
|
154
|
+
}
|
|
118
155
|
function buildInteractionElement(candidate) {
|
|
119
156
|
const element = {
|
|
120
157
|
role: candidate.role,
|
|
@@ -7,9 +7,10 @@ import { sanitizeFilename } from "../../../src/utils/strings.js";
|
|
|
7
7
|
import { Documentarian } from "./ai/documentarian.js";
|
|
8
8
|
import { DocbotConfigParser } from "./config.js";
|
|
9
9
|
import { renderPageDocumentation, renderSpecIndex } from "./docs-renderer.js";
|
|
10
|
+
import { renderMermaidBody } from "./state-diagram.js";
|
|
10
11
|
import { getDocPageKey, shouldCrawlDocPath } from "./path-filter.js";
|
|
11
12
|
import { extractResearchNavigationTargets } from "./research-navigation.js";
|
|
12
|
-
import { captureDocumentationScreenshots } from "./screenshots.js";
|
|
13
|
+
import { captureDocumentationScreenshots, captureInteractionScreenshot } from "./screenshots.js";
|
|
13
14
|
class DocBot {
|
|
14
15
|
explorBot;
|
|
15
16
|
configParser;
|
|
@@ -96,7 +97,17 @@ class DocBot {
|
|
|
96
97
|
screenshot: this.shouldUseScreenshots(),
|
|
97
98
|
force: true,
|
|
98
99
|
});
|
|
99
|
-
const
|
|
100
|
+
const pagePath = this.getPageFilePath(state.url);
|
|
101
|
+
const documentation = await this.documentarian.document(state, research, async (interactionState, transition) => {
|
|
102
|
+
if (!this.shouldUseScreenshots()) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return captureInteractionScreenshot(this.explorBot.getExplorer(), interactionState, transition, {
|
|
106
|
+
pageFilePath: pagePath,
|
|
107
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
108
|
+
config: this.config,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
100
111
|
const lowSignalReason = this.getLowSignalReason(documentation, research);
|
|
101
112
|
if (lowSignalReason) {
|
|
102
113
|
skipped.push({
|
|
@@ -118,6 +129,7 @@ class DocBot {
|
|
|
118
129
|
mightActions: documentation.might.map((item) => item.action),
|
|
119
130
|
interactionActions: (documentation.interactions || []).map((item) => item.action),
|
|
120
131
|
qualityNotes: documentation.qualityNotes || [],
|
|
132
|
+
interactions: documentation.interactions || [],
|
|
121
133
|
filePath,
|
|
122
134
|
});
|
|
123
135
|
documented.add(pageKey);
|
|
@@ -142,11 +154,12 @@ class DocBot {
|
|
|
142
154
|
});
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
|
-
const indexPath = this.saveIndex(effectiveStartPath, pages, skipped, effectiveMaxPages);
|
|
157
|
+
const { indexPath, diagramPath } = this.saveIndex(effectiveStartPath, pages, skipped, effectiveMaxPages);
|
|
146
158
|
return {
|
|
147
159
|
pages,
|
|
148
160
|
skipped,
|
|
149
161
|
indexPath,
|
|
162
|
+
diagramPath,
|
|
150
163
|
outputDir: this.configParser.getOutputDir(),
|
|
151
164
|
};
|
|
152
165
|
}
|
|
@@ -338,6 +351,9 @@ class DocBot {
|
|
|
338
351
|
if (documentation.can.length >= minCanActions) {
|
|
339
352
|
return null;
|
|
340
353
|
}
|
|
354
|
+
if ((documentation.interactions || []).length > 0) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
341
357
|
const interactiveCount = this.countInteractiveElements(research);
|
|
342
358
|
if (interactiveCount >= minInteractiveElements) {
|
|
343
359
|
return null;
|
|
@@ -365,9 +381,12 @@ class DocBot {
|
|
|
365
381
|
});
|
|
366
382
|
}
|
|
367
383
|
saveIndex(startPath, pages, skipped, maxPages) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
384
|
+
const outputDir = this.configParser.getOutputDir();
|
|
385
|
+
const indexPath = path.join(outputDir, 'index.md');
|
|
386
|
+
writeFileSync(indexPath, renderSpecIndex(outputDir, startPath, pages, skipped, maxPages), 'utf8');
|
|
387
|
+
const diagramPath = path.join(outputDir, 'state-diagram.mmd');
|
|
388
|
+
writeFileSync(diagramPath, renderMermaidBody(outputDir, pages), 'utf8');
|
|
389
|
+
return { indexPath, diagramPath };
|
|
371
390
|
}
|
|
372
391
|
getPagesDir() {
|
|
373
392
|
return path.join(this.configParser.getOutputDir(), 'pages');
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { buildStateGraph, renderMermaidFromGraph, renderStateMapFromGraph } from "./state-diagram.js";
|
|
3
|
+
import { normalizeInlineText } from "../../../src/utils/strings.js";
|
|
2
4
|
function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
3
5
|
const lines = [];
|
|
4
6
|
lines.push(`# ${state.url}`);
|
|
@@ -40,6 +42,10 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
|
40
42
|
}
|
|
41
43
|
lines.push('');
|
|
42
44
|
}
|
|
45
|
+
if (transition.screenshot) {
|
|
46
|
+
lines.push(``);
|
|
47
|
+
lines.push('');
|
|
48
|
+
}
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
lines.push('## User Can');
|
|
@@ -90,6 +96,18 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
|
90
96
|
lines.push(`Pages skipped: ${skipped.length}`);
|
|
91
97
|
lines.push(`Max pages: ${maxPages}`);
|
|
92
98
|
lines.push('');
|
|
99
|
+
const graph = buildStateGraph(outputDir, pages);
|
|
100
|
+
lines.push('## State Transitions');
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push(`\`\`\`mermaid\n${renderMermaidFromGraph(graph)}\n\`\`\``);
|
|
103
|
+
lines.push('');
|
|
104
|
+
const stateMap = renderStateMapFromGraph(graph);
|
|
105
|
+
if (stateMap) {
|
|
106
|
+
lines.push('### State Index');
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(stateMap);
|
|
109
|
+
lines.push('');
|
|
110
|
+
}
|
|
93
111
|
lines.push('## Pages');
|
|
94
112
|
lines.push('');
|
|
95
113
|
if (pages.length === 0) {
|
|
@@ -192,7 +210,4 @@ function ensureSentence(text) {
|
|
|
192
210
|
}
|
|
193
211
|
return `${trimmed}.`;
|
|
194
212
|
}
|
|
195
|
-
function normalizeInlineText(text) {
|
|
196
|
-
return text.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
197
|
-
}
|
|
198
213
|
export { renderPageDocumentation, renderSpecIndex, ensureSentence, normalizeAction };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseResearchSections } from "../../../src/ai/researcher/parser.js";
|
|
4
|
+
import { detectFocusArea } from "../../../src/utils/aria.js";
|
|
4
5
|
import { safeFilename, sanitizeFilename } from "../../../src/utils/strings.js";
|
|
5
6
|
const DEFAULT_MAX_SECTION_SCREENSHOTS = 8;
|
|
6
7
|
export async function captureDocumentationScreenshots(explorer, state, research, options) {
|
|
@@ -46,6 +47,34 @@ export function getScreenshotSections(research) {
|
|
|
46
47
|
}
|
|
47
48
|
return sections;
|
|
48
49
|
}
|
|
50
|
+
export async function captureInteractionScreenshot(explorer, state, transition, options) {
|
|
51
|
+
const page = explorer.playwrightHelper?.page;
|
|
52
|
+
if (!page) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
56
|
+
const pageName = sanitizeFilename(state.url || 'page') || 'page';
|
|
57
|
+
const stateName = sanitizeFilename(transition.targetState?.label || transition.action) || 'state';
|
|
58
|
+
const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_${stateName}`, '.png'));
|
|
59
|
+
const focus = detectFocusArea(state.ariaSnapshot || null);
|
|
60
|
+
try {
|
|
61
|
+
if (focus.detected) {
|
|
62
|
+
await page.locator('[role="dialog"], [role="alertdialog"], [aria-modal="true"]').last().screenshot({ path: filePath });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await page.screenshot({ path: filePath });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
title: transition.targetState?.label || transition.action,
|
|
73
|
+
path: filePath,
|
|
74
|
+
relativePath: toMarkdownPath(options.pageFilePath, filePath),
|
|
75
|
+
kind: 'state',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
49
78
|
async function captureFullPageScreenshot(page, pageName, options) {
|
|
50
79
|
const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_page`, '.png'));
|
|
51
80
|
try {
|