browserwire 0.1.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/LICENSE +21 -0
- package/README.md +113 -0
- package/cli/api/bridge.js +64 -0
- package/cli/api/openapi.js +175 -0
- package/cli/api/router.js +280 -0
- package/cli/api/swagger-ui.js +26 -0
- package/cli/discovery/classify.js +304 -0
- package/cli/discovery/compile.js +392 -0
- package/cli/discovery/enrich.js +376 -0
- package/cli/discovery/entities.js +356 -0
- package/cli/discovery/llm-client.js +352 -0
- package/cli/discovery/locators.js +326 -0
- package/cli/discovery/perceive.js +476 -0
- package/cli/discovery/session.js +930 -0
- package/cli/discovery/synthesize-workflows.js +295 -0
- package/cli/index.js +63 -0
- package/cli/manifest-store.js +140 -0
- package/cli/server.js +539 -0
- package/extension/background.js +1512 -0
- package/extension/content-script.js +491 -0
- package/extension/discovery.js +495 -0
- package/extension/executor.js +392 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +33 -0
- package/extension/shared/protocol.js +50 -0
- package/extension/sidepanel.html +277 -0
- package/extension/sidepanel.js +211 -0
- package/extension/vendor/LICENSE +22 -0
- package/extension/vendor/rrweb-record.min.js +84 -0
- package/package.json +49 -0
package/cli/server.js
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
import {
|
|
6
|
+
createEnvelope,
|
|
7
|
+
MessageType,
|
|
8
|
+
parseEnvelope,
|
|
9
|
+
PROTOCOL_VERSION
|
|
10
|
+
} from "../extension/shared/protocol.js";
|
|
11
|
+
import { classifyInteractables } from "./discovery/classify.js";
|
|
12
|
+
import { groupEntities } from "./discovery/entities.js";
|
|
13
|
+
import { synthesizeAllLocators } from "./discovery/locators.js";
|
|
14
|
+
import { compileManifest } from "./discovery/compile.js";
|
|
15
|
+
import { enrichManifest } from "./discovery/enrich.js";
|
|
16
|
+
import { DiscoverySession } from "./discovery/session.js";
|
|
17
|
+
import { createBridge } from "./api/bridge.js";
|
|
18
|
+
import { createHttpHandler } from "./api/router.js";
|
|
19
|
+
import { ManifestStore } from "./manifest-store.js";
|
|
20
|
+
|
|
21
|
+
const CLIENT_STATUS_INTERVAL_MS = 15000;
|
|
22
|
+
|
|
23
|
+
/** Active discovery sessions keyed by sessionId */
|
|
24
|
+
const activeSessions = new Map();
|
|
25
|
+
|
|
26
|
+
/** Module-level state shared between HTTP and WS */
|
|
27
|
+
const manifestStore = new ManifestStore();
|
|
28
|
+
const siteManifests = new Map(); // origin → manifest (in-memory cache)
|
|
29
|
+
let extensionSocket = null;
|
|
30
|
+
const bridge = createBridge();
|
|
31
|
+
|
|
32
|
+
export const startServer = async ({
|
|
33
|
+
host = "127.0.0.1",
|
|
34
|
+
port = 8787,
|
|
35
|
+
debug = false
|
|
36
|
+
} = {}) => {
|
|
37
|
+
// Load persisted site manifests on startup
|
|
38
|
+
const sites = await manifestStore.listSites();
|
|
39
|
+
for (const site of sites) {
|
|
40
|
+
const m = await manifestStore.load(site.origin);
|
|
41
|
+
if (m) {
|
|
42
|
+
siteManifests.set(site.origin, m);
|
|
43
|
+
const slug = ManifestStore.originSlug(site.origin);
|
|
44
|
+
console.log(`[browserwire-cli] loaded manifest for ${site.origin} → http://${host}:${port}/api/sites/${slug}/docs`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (sites.length > 0) {
|
|
48
|
+
console.log(`[browserwire-cli] loaded ${sites.length} site manifest(s)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const httpHandler = createHttpHandler({
|
|
52
|
+
getManifestBySlug: (slug) => {
|
|
53
|
+
for (const [origin, m] of siteManifests) {
|
|
54
|
+
if (ManifestStore.originSlug(origin) === slug) return m;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
listSites: () => [...siteManifests.entries()].map(([origin, m]) => ({
|
|
59
|
+
origin,
|
|
60
|
+
slug: ManifestStore.originSlug(origin),
|
|
61
|
+
domain: m.domain || null,
|
|
62
|
+
entityCount: m.entities?.length || 0,
|
|
63
|
+
actionCount: m.actions?.length || 0,
|
|
64
|
+
viewCount: m.views?.length || 0,
|
|
65
|
+
updatedAt: m.metadata?.updatedAt || m.metadata?.createdAt || null
|
|
66
|
+
})),
|
|
67
|
+
bridge,
|
|
68
|
+
getSocket: () => extensionSocket,
|
|
69
|
+
host,
|
|
70
|
+
port
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const httpServer = createServer(httpHandler);
|
|
74
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
75
|
+
|
|
76
|
+
httpServer.listen(port, host, () => {
|
|
77
|
+
console.log(`[browserwire-cli] listening on http://${host}:${port}`);
|
|
78
|
+
console.log(`[browserwire-cli] site index at http://${host}:${port}/api/docs`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
wss.on("connection", (socket, req) => {
|
|
82
|
+
const clientId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
83
|
+
const source = req.socket.remoteAddress || "unknown";
|
|
84
|
+
|
|
85
|
+
console.log(`[browserwire-cli] client connected ${clientId} from ${source}`);
|
|
86
|
+
|
|
87
|
+
const statusTimer = setInterval(() => {
|
|
88
|
+
if (socket.readyState === 1) {
|
|
89
|
+
socket.send(
|
|
90
|
+
JSON.stringify(
|
|
91
|
+
createEnvelope(MessageType.STATUS, {
|
|
92
|
+
state: "connected",
|
|
93
|
+
serverTime: new Date().toISOString()
|
|
94
|
+
})
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}, CLIENT_STATUS_INTERVAL_MS);
|
|
99
|
+
|
|
100
|
+
socket.on("message", async (data) => {
|
|
101
|
+
const message = parseEnvelope(data.toString());
|
|
102
|
+
|
|
103
|
+
if (!message) {
|
|
104
|
+
socket.send(
|
|
105
|
+
JSON.stringify(
|
|
106
|
+
createEnvelope(MessageType.ERROR, {
|
|
107
|
+
code: "invalid_message",
|
|
108
|
+
message: "Expected JSON message with a string 'type' field."
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (message.type === MessageType.HELLO) {
|
|
116
|
+
// Track as extension socket
|
|
117
|
+
extensionSocket = socket;
|
|
118
|
+
|
|
119
|
+
socket.send(
|
|
120
|
+
JSON.stringify(
|
|
121
|
+
createEnvelope(
|
|
122
|
+
MessageType.HELLO_ACK,
|
|
123
|
+
{
|
|
124
|
+
accepted: true,
|
|
125
|
+
server: "browserwire-cli",
|
|
126
|
+
protocolVersion: PROTOCOL_VERSION
|
|
127
|
+
},
|
|
128
|
+
message.requestId
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (message.type === MessageType.PING) {
|
|
136
|
+
socket.send(
|
|
137
|
+
JSON.stringify(
|
|
138
|
+
createEnvelope(
|
|
139
|
+
MessageType.PONG,
|
|
140
|
+
{
|
|
141
|
+
serverTime: new Date().toISOString()
|
|
142
|
+
},
|
|
143
|
+
message.requestId
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Bridge result messages (from extension executing REST API commands) ──
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
message.type === MessageType.EXECUTE_RESULT ||
|
|
154
|
+
message.type === MessageType.READ_RESULT ||
|
|
155
|
+
message.type === MessageType.WORKFLOW_RESULT
|
|
156
|
+
) {
|
|
157
|
+
if (bridge.handleWsResult(message)) return;
|
|
158
|
+
// Not matched — fall through to log
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Discovery Session Messages ─────────────────────────
|
|
162
|
+
|
|
163
|
+
if (message.type === MessageType.DISCOVERY_SESSION_START) {
|
|
164
|
+
const payload = message.payload || {};
|
|
165
|
+
const sessionId = payload.sessionId || crypto.randomUUID();
|
|
166
|
+
const site = payload.url || "unknown";
|
|
167
|
+
|
|
168
|
+
const session = new DiscoverySession(sessionId, site);
|
|
169
|
+
activeSessions.set(sessionId, session);
|
|
170
|
+
|
|
171
|
+
// Derive origin and seed with prior manifest if available
|
|
172
|
+
let origin = null;
|
|
173
|
+
try { origin = new URL(site).origin; } catch { /* ignore */ }
|
|
174
|
+
session._siteOrigin = origin;
|
|
175
|
+
|
|
176
|
+
if (origin) {
|
|
177
|
+
const prior = siteManifests.get(origin) || await manifestStore.load(origin);
|
|
178
|
+
if (prior) {
|
|
179
|
+
session.seedWithManifest(prior);
|
|
180
|
+
console.log(`[browserwire-cli] session seeded with prior manifest for ${origin}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(
|
|
185
|
+
`[browserwire-cli] session started: ${sessionId} site=${site}`
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
socket.send(
|
|
189
|
+
JSON.stringify(
|
|
190
|
+
createEnvelope(
|
|
191
|
+
MessageType.DISCOVERY_SESSION_STATUS,
|
|
192
|
+
session.getStats(),
|
|
193
|
+
message.requestId
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (message.type === MessageType.DISCOVERY_SESSION_STOP) {
|
|
201
|
+
const payload = message.payload || {};
|
|
202
|
+
const sessionId = payload.sessionId;
|
|
203
|
+
const session = activeSessions.get(sessionId);
|
|
204
|
+
|
|
205
|
+
if (!session) {
|
|
206
|
+
console.warn(`[browserwire-cli] session stop for unknown session: ${sessionId}`);
|
|
207
|
+
socket.send(
|
|
208
|
+
JSON.stringify(
|
|
209
|
+
createEnvelope(
|
|
210
|
+
MessageType.ERROR,
|
|
211
|
+
{ code: "unknown_session", message: `Session ${sessionId} not found` },
|
|
212
|
+
message.requestId
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(`[browserwire-cli] session stopping: ${sessionId}`);
|
|
220
|
+
|
|
221
|
+
// Process any remaining buffered snapshots sent with the stop payload
|
|
222
|
+
const remainingSnapshots = Array.isArray(payload.pendingSnapshots) ? payload.pendingSnapshots : [];
|
|
223
|
+
const finalizeSession = remainingSnapshots.length > 0
|
|
224
|
+
? (async () => {
|
|
225
|
+
console.log(`[browserwire-cli] processing ${remainingSnapshots.length} remaining buffered snapshots before finalize`);
|
|
226
|
+
for (const snap of remainingSnapshots) {
|
|
227
|
+
await session.addSnapshot(snap);
|
|
228
|
+
}
|
|
229
|
+
return session.finalize();
|
|
230
|
+
})()
|
|
231
|
+
: session.finalize();
|
|
232
|
+
|
|
233
|
+
finalizeSession
|
|
234
|
+
.then((result) => {
|
|
235
|
+
const { manifest, draftManifest, enrichedManifest } = result;
|
|
236
|
+
|
|
237
|
+
if (!manifest) {
|
|
238
|
+
console.log(`[browserwire-cli] session ${sessionId} produced no manifest`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Write output files
|
|
243
|
+
const sessionDir = resolve(process.cwd(), `logs/session-${sessionId}`);
|
|
244
|
+
|
|
245
|
+
return mkdir(sessionDir, { recursive: true })
|
|
246
|
+
.then(() => Promise.all([
|
|
247
|
+
writeFile(
|
|
248
|
+
resolve(sessionDir, "manifest-draft.json"),
|
|
249
|
+
JSON.stringify(draftManifest || manifest, null, 2),
|
|
250
|
+
"utf8"
|
|
251
|
+
),
|
|
252
|
+
writeFile(
|
|
253
|
+
resolve(sessionDir, "manifest.json"),
|
|
254
|
+
JSON.stringify(manifest, null, 2),
|
|
255
|
+
"utf8"
|
|
256
|
+
),
|
|
257
|
+
writeFile(
|
|
258
|
+
resolve(sessionDir, "session.json"),
|
|
259
|
+
JSON.stringify({
|
|
260
|
+
sessionId: session.sessionId,
|
|
261
|
+
site: session.site,
|
|
262
|
+
startedAt: session.startedAt,
|
|
263
|
+
stoppedAt: new Date().toISOString(),
|
|
264
|
+
snapshotCount: session.snapshots.length,
|
|
265
|
+
snapshots: session.snapshots.map((s) => ({
|
|
266
|
+
snapshotId: s.snapshotId,
|
|
267
|
+
trigger: s.trigger,
|
|
268
|
+
url: s.url,
|
|
269
|
+
title: s.title,
|
|
270
|
+
capturedAt: s.capturedAt,
|
|
271
|
+
stats: s.stats
|
|
272
|
+
}))
|
|
273
|
+
}, null, 2),
|
|
274
|
+
"utf8"
|
|
275
|
+
)
|
|
276
|
+
]))
|
|
277
|
+
.then(async () => {
|
|
278
|
+
console.log(`[browserwire-cli] session ${sessionId} output written to ${sessionDir}`);
|
|
279
|
+
|
|
280
|
+
// Save to site-centric manifest store
|
|
281
|
+
const origin = session._siteOrigin;
|
|
282
|
+
if (origin) {
|
|
283
|
+
siteManifests.set(origin, enrichedManifest);
|
|
284
|
+
await manifestStore.save(origin, enrichedManifest, sessionId);
|
|
285
|
+
const slug = ManifestStore.originSlug(origin);
|
|
286
|
+
console.log(`[browserwire-cli] REST API ready at http://${host}:${port}/api/sites/${slug}/docs`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Send manifest to extension immediately so sidepanel can render
|
|
290
|
+
socket.send(JSON.stringify(createEnvelope(
|
|
291
|
+
MessageType.MANIFEST_READY,
|
|
292
|
+
{ sessionId, manifest: enrichedManifest }
|
|
293
|
+
)));
|
|
294
|
+
});
|
|
295
|
+
})
|
|
296
|
+
.then(() => {
|
|
297
|
+
socket.send(
|
|
298
|
+
JSON.stringify(
|
|
299
|
+
createEnvelope(
|
|
300
|
+
MessageType.DISCOVERY_SESSION_STATUS,
|
|
301
|
+
{ ...session.getStats(), finalized: true },
|
|
302
|
+
message.requestId
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
})
|
|
307
|
+
.catch((error) => {
|
|
308
|
+
console.error(`[browserwire-cli] session finalization failed:`, error);
|
|
309
|
+
})
|
|
310
|
+
.finally(() => {
|
|
311
|
+
activeSessions.delete(sessionId);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (message.type === MessageType.CHECKPOINT) {
|
|
318
|
+
const payload = message.payload || {};
|
|
319
|
+
const { sessionId, snapshots = [], note } = payload;
|
|
320
|
+
const session = activeSessions.get(sessionId);
|
|
321
|
+
|
|
322
|
+
if (!session) {
|
|
323
|
+
console.warn(`[browserwire-cli] checkpoint for unknown session: ${sessionId}`);
|
|
324
|
+
socket.send(JSON.stringify(createEnvelope(MessageType.ERROR, {
|
|
325
|
+
code: "unknown_session", message: `Session ${sessionId} not found`
|
|
326
|
+
}, message.requestId)));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(`[browserwire-cli] checkpoint: sessionId=${sessionId} snapshots=${snapshots.length}${note ? ` note="${note}"` : ""}`);
|
|
331
|
+
|
|
332
|
+
const processCheckpoint = async () => {
|
|
333
|
+
for (const snap of snapshots) {
|
|
334
|
+
await session.addSnapshot(snap);
|
|
335
|
+
}
|
|
336
|
+
return session.compileCheckpoint(note);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
processCheckpoint()
|
|
340
|
+
.then(async (result) => {
|
|
341
|
+
const { manifest, checkpointIndex } = result;
|
|
342
|
+
|
|
343
|
+
const sessionDir = resolve(process.cwd(), `logs/session-${sessionId}`);
|
|
344
|
+
|
|
345
|
+
if (manifest) {
|
|
346
|
+
await mkdir(sessionDir, { recursive: true });
|
|
347
|
+
await writeFile(
|
|
348
|
+
resolve(sessionDir, `checkpoint-${checkpointIndex}.json`),
|
|
349
|
+
JSON.stringify(manifest, null, 2),
|
|
350
|
+
"utf8"
|
|
351
|
+
);
|
|
352
|
+
console.log(`[browserwire-cli] checkpoint-${checkpointIndex} written for session ${sessionId}`);
|
|
353
|
+
|
|
354
|
+
// Save to site-centric manifest store
|
|
355
|
+
if (session._siteOrigin) {
|
|
356
|
+
siteManifests.set(session._siteOrigin, manifest);
|
|
357
|
+
manifestStore.save(session._siteOrigin, manifest, sessionId).catch(console.error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
socket.send(JSON.stringify(createEnvelope(
|
|
362
|
+
MessageType.CHECKPOINT_COMPLETE,
|
|
363
|
+
{ sessionId, manifest: manifest || null, checkpointIndex },
|
|
364
|
+
message.requestId
|
|
365
|
+
)));
|
|
366
|
+
})
|
|
367
|
+
.catch((error) => {
|
|
368
|
+
console.error(`[browserwire-cli] checkpoint processing failed:`, error);
|
|
369
|
+
socket.send(JSON.stringify(createEnvelope(MessageType.ERROR, {
|
|
370
|
+
code: "checkpoint_failed", message: error.message
|
|
371
|
+
}, message.requestId)));
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (message.type === MessageType.DISCOVERY_INCREMENTAL) {
|
|
378
|
+
const payload = message.payload || {};
|
|
379
|
+
const sessionId = payload.sessionId;
|
|
380
|
+
const session = activeSessions.get(sessionId);
|
|
381
|
+
|
|
382
|
+
if (!session) {
|
|
383
|
+
console.warn(`[browserwire-cli] incremental snapshot for unknown session: ${sessionId}`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const skeletonCount = Array.isArray(payload.skeleton) ? payload.skeleton.length : 0;
|
|
388
|
+
const screenshotKB = payload.screenshot ? Math.round(payload.screenshot.length * 0.75 / 1024) : 0;
|
|
389
|
+
console.log(
|
|
390
|
+
`[browserwire-cli] incremental snapshot: sessionId=${sessionId} skeleton=${skeletonCount} trigger=${payload.trigger?.kind || "unknown"} screenshot=${payload.screenshot ? screenshotKB + "KB" : "null"}`
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
session.addSnapshot(payload)
|
|
394
|
+
.then((stats) => {
|
|
395
|
+
// Broadcast updated stats back to extension
|
|
396
|
+
socket.send(
|
|
397
|
+
JSON.stringify(
|
|
398
|
+
createEnvelope(
|
|
399
|
+
MessageType.DISCOVERY_SESSION_STATUS,
|
|
400
|
+
stats
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Write screenshot as JPEG (always) + full snapshot JSON (debug only)
|
|
406
|
+
const snapName = payload.snapshotId || `snap_${stats.snapshotCount}`;
|
|
407
|
+
const snapDir = resolve(process.cwd(), `logs/session-${sessionId}`);
|
|
408
|
+
|
|
409
|
+
if (payload.screenshot) {
|
|
410
|
+
mkdir(snapDir, { recursive: true })
|
|
411
|
+
.then(() => writeFile(
|
|
412
|
+
resolve(snapDir, `${snapName}.jpg`),
|
|
413
|
+
Buffer.from(payload.screenshot, "base64")
|
|
414
|
+
))
|
|
415
|
+
.catch((err) => {
|
|
416
|
+
console.error(`[browserwire-cli] failed to write screenshot:`, err);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (debug) {
|
|
421
|
+
mkdir(snapDir, { recursive: true })
|
|
422
|
+
.then(() => writeFile(
|
|
423
|
+
resolve(snapDir, `${snapName}.json`),
|
|
424
|
+
JSON.stringify({ ...payload, screenshot: payload.screenshot ? "<base64>" : null }, null, 2),
|
|
425
|
+
"utf8"
|
|
426
|
+
))
|
|
427
|
+
.catch((err) => {
|
|
428
|
+
console.error(`[browserwire-cli] failed to write snapshot:`, err);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
.catch((error) => {
|
|
433
|
+
console.error(`[browserwire-cli] snapshot processing failed:`, error);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ─── Legacy One-Shot Discovery (kept for backward compat) ──
|
|
440
|
+
|
|
441
|
+
if (message.type === MessageType.DISCOVERY_SNAPSHOT) {
|
|
442
|
+
const payload = message.payload || {};
|
|
443
|
+
const elements = Array.isArray(payload.elements) ? payload.elements : [];
|
|
444
|
+
const a11y = Array.isArray(payload.a11y) ? payload.a11y : [];
|
|
445
|
+
const url = payload.url || "unknown";
|
|
446
|
+
const title = payload.title || "unknown";
|
|
447
|
+
const pageText = typeof payload.pageText === "string" ? payload.pageText : "";
|
|
448
|
+
|
|
449
|
+
console.log(
|
|
450
|
+
`[browserwire-cli] discovery snapshot url=${url} title="${title}" elements=${elements.length} a11y=${a11y.length}`
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const { interactables, stats } = classifyInteractables(elements, a11y);
|
|
454
|
+
const { entities, stats: entityStats } = groupEntities(elements, a11y, interactables);
|
|
455
|
+
const { locators, stats: locatorStats } = synthesizeAllLocators(elements, a11y, interactables);
|
|
456
|
+
const { manifest, stats: manifestStats } = compileManifest({
|
|
457
|
+
url, title,
|
|
458
|
+
capturedAt: payload.capturedAt,
|
|
459
|
+
elements, a11y, interactables, entities, locators
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
console.log(
|
|
463
|
+
`[browserwire-cli] manifest compiled: ${manifestStats.entityCount} entities, ${manifestStats.actionCount} actions`
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const manifestLogPath = resolve(process.cwd(), "logs/discovery-manifest.json");
|
|
467
|
+
mkdir(dirname(manifestLogPath), { recursive: true })
|
|
468
|
+
.then(() => enrichManifest(manifest, pageText, payload.capturedAt))
|
|
469
|
+
.then((result) => {
|
|
470
|
+
const finalManifest = result ? result.enriched : manifest;
|
|
471
|
+
return writeFile(manifestLogPath, JSON.stringify(finalManifest, null, 2), "utf8");
|
|
472
|
+
})
|
|
473
|
+
.then(() => {
|
|
474
|
+
console.log(`[browserwire-cli] manifest written to ${manifestLogPath}`);
|
|
475
|
+
})
|
|
476
|
+
.catch((error) => {
|
|
477
|
+
console.error(`[browserwire-cli] manifest write failed:`, error);
|
|
478
|
+
return writeFile(manifestLogPath, JSON.stringify(manifest, null, 2), "utf8").catch(() => {});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
socket.send(
|
|
482
|
+
JSON.stringify(
|
|
483
|
+
createEnvelope(
|
|
484
|
+
MessageType.DISCOVERY_ACK,
|
|
485
|
+
{
|
|
486
|
+
elementCount: elements.length,
|
|
487
|
+
a11yCount: a11y.length,
|
|
488
|
+
interactableCount: interactables.length,
|
|
489
|
+
entityCount: manifestStats.entityCount,
|
|
490
|
+
actionCount: manifestStats.actionCount,
|
|
491
|
+
url,
|
|
492
|
+
ackedAt: new Date().toISOString()
|
|
493
|
+
},
|
|
494
|
+
message.requestId
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ─── Unsupported ──────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
socket.send(
|
|
504
|
+
JSON.stringify(
|
|
505
|
+
createEnvelope(
|
|
506
|
+
MessageType.ERROR,
|
|
507
|
+
{
|
|
508
|
+
code: "unsupported_type",
|
|
509
|
+
message: `Unsupported message type '${message.type}'.`
|
|
510
|
+
},
|
|
511
|
+
message.requestId
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
socket.on("close", () => {
|
|
518
|
+
clearInterval(statusTimer);
|
|
519
|
+
console.log(`[browserwire-cli] client disconnected ${clientId}`);
|
|
520
|
+
|
|
521
|
+
// If this was the extension socket, reject all pending bridge requests
|
|
522
|
+
if (socket === extensionSocket) {
|
|
523
|
+
extensionSocket = null;
|
|
524
|
+
bridge.rejectAll("Extension disconnected");
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
socket.on("error", (error) => {
|
|
529
|
+
clearInterval(statusTimer);
|
|
530
|
+
console.error(`[browserwire-cli] socket error ${clientId}`, error);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
wss.on("close", () => {
|
|
535
|
+
console.log("[browserwire-cli] server stopped");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return httpServer;
|
|
539
|
+
};
|