@hasna/browser 0.0.1

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.
Files changed (95) hide show
  1. package/dashboard/dist/assets/index-fnTihDNb.js +40 -0
  2. package/dashboard/dist/index.html +16 -0
  3. package/dist/cli/index.d.ts +3 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +8625 -0
  6. package/dist/cli/index.test.d.ts +2 -0
  7. package/dist/cli/index.test.d.ts.map +1 -0
  8. package/dist/db/agents.d.ts +16 -0
  9. package/dist/db/agents.d.ts.map +1 -0
  10. package/dist/db/agents.test.d.ts +2 -0
  11. package/dist/db/agents.test.d.ts.map +1 -0
  12. package/dist/db/console-log.d.ts +6 -0
  13. package/dist/db/console-log.d.ts.map +1 -0
  14. package/dist/db/crawl-results.d.ts +6 -0
  15. package/dist/db/crawl-results.d.ts.map +1 -0
  16. package/dist/db/heartbeats.d.ts +6 -0
  17. package/dist/db/heartbeats.d.ts.map +1 -0
  18. package/dist/db/network-log.d.ts +7 -0
  19. package/dist/db/network-log.d.ts.map +1 -0
  20. package/dist/db/projects.d.ts +9 -0
  21. package/dist/db/projects.d.ts.map +1 -0
  22. package/dist/db/projects.test.d.ts +2 -0
  23. package/dist/db/projects.test.d.ts.map +1 -0
  24. package/dist/db/recordings.d.ts +11 -0
  25. package/dist/db/recordings.d.ts.map +1 -0
  26. package/dist/db/recordings.test.d.ts +2 -0
  27. package/dist/db/recordings.test.d.ts.map +1 -0
  28. package/dist/db/schema.d.ts +5 -0
  29. package/dist/db/schema.d.ts.map +1 -0
  30. package/dist/db/schema.test.d.ts +2 -0
  31. package/dist/db/schema.test.d.ts.map +1 -0
  32. package/dist/db/sessions.d.ts +17 -0
  33. package/dist/db/sessions.d.ts.map +1 -0
  34. package/dist/db/sessions.test.d.ts +2 -0
  35. package/dist/db/sessions.test.d.ts.map +1 -0
  36. package/dist/db/snapshots.d.ts +7 -0
  37. package/dist/db/snapshots.d.ts.map +1 -0
  38. package/dist/engines/cdp.d.ts +26 -0
  39. package/dist/engines/cdp.d.ts.map +1 -0
  40. package/dist/engines/lightpanda.d.ts +25 -0
  41. package/dist/engines/lightpanda.d.ts.map +1 -0
  42. package/dist/engines/playwright.d.ts +27 -0
  43. package/dist/engines/playwright.d.ts.map +1 -0
  44. package/dist/engines/selector.d.ts +17 -0
  45. package/dist/engines/selector.d.ts.map +1 -0
  46. package/dist/engines/selector.test.d.ts +2 -0
  47. package/dist/engines/selector.test.d.ts.map +1 -0
  48. package/dist/index.d.ts +28 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +2080 -0
  51. package/dist/lib/actions.d.ts +33 -0
  52. package/dist/lib/actions.d.ts.map +1 -0
  53. package/dist/lib/actions.test.d.ts +2 -0
  54. package/dist/lib/actions.test.d.ts.map +1 -0
  55. package/dist/lib/agents.d.ts +9 -0
  56. package/dist/lib/agents.d.ts.map +1 -0
  57. package/dist/lib/agents.test.d.ts +2 -0
  58. package/dist/lib/agents.test.d.ts.map +1 -0
  59. package/dist/lib/console.d.ts +6 -0
  60. package/dist/lib/console.d.ts.map +1 -0
  61. package/dist/lib/crawler.d.ts +3 -0
  62. package/dist/lib/crawler.d.ts.map +1 -0
  63. package/dist/lib/extractor.d.ts +14 -0
  64. package/dist/lib/extractor.d.ts.map +1 -0
  65. package/dist/lib/extractor.test.d.ts +2 -0
  66. package/dist/lib/extractor.test.d.ts.map +1 -0
  67. package/dist/lib/network.d.ts +11 -0
  68. package/dist/lib/network.d.ts.map +1 -0
  69. package/dist/lib/network.test.d.ts +2 -0
  70. package/dist/lib/network.test.d.ts.map +1 -0
  71. package/dist/lib/performance.d.ts +13 -0
  72. package/dist/lib/performance.d.ts.map +1 -0
  73. package/dist/lib/recorder.d.ts +11 -0
  74. package/dist/lib/recorder.d.ts.map +1 -0
  75. package/dist/lib/recorder.test.d.ts +2 -0
  76. package/dist/lib/recorder.test.d.ts.map +1 -0
  77. package/dist/lib/screenshot.d.ts +9 -0
  78. package/dist/lib/screenshot.d.ts.map +1 -0
  79. package/dist/lib/session.d.ts +21 -0
  80. package/dist/lib/session.d.ts.map +1 -0
  81. package/dist/lib/storage.d.ts +19 -0
  82. package/dist/lib/storage.d.ts.map +1 -0
  83. package/dist/mcp/index.d.ts +3 -0
  84. package/dist/mcp/index.d.ts.map +1 -0
  85. package/dist/mcp/index.js +5944 -0
  86. package/dist/mcp/index.test.d.ts +2 -0
  87. package/dist/mcp/index.test.d.ts.map +1 -0
  88. package/dist/server/index.d.ts +2 -0
  89. package/dist/server/index.d.ts.map +1 -0
  90. package/dist/server/index.js +1580 -0
  91. package/dist/server/index.test.d.ts +2 -0
  92. package/dist/server/index.test.d.ts.map +1 -0
  93. package/dist/types/index.d.ts +313 -0
  94. package/dist/types/index.d.ts.map +1 -0
  95. package/package.json +74 -0
package/dist/index.js ADDED
@@ -0,0 +1,2080 @@
1
+ // @bun
2
+ // src/types/index.ts
3
+ var UseCase;
4
+ ((UseCase2) => {
5
+ UseCase2["SCRAPE"] = "scrape";
6
+ UseCase2["EXTRACT_LINKS"] = "extract_links";
7
+ UseCase2["STATUS_CHECK"] = "status_check";
8
+ UseCase2["FORM_FILL"] = "form_fill";
9
+ UseCase2["SPA_NAVIGATE"] = "spa_navigate";
10
+ UseCase2["SCREENSHOT"] = "screenshot";
11
+ UseCase2["AUTH_FLOW"] = "auth_flow";
12
+ UseCase2["MULTI_TAB"] = "multi_tab";
13
+ UseCase2["NETWORK_MONITOR"] = "network_monitor";
14
+ UseCase2["HAR_CAPTURE"] = "har_capture";
15
+ UseCase2["PERF_PROFILE"] = "perf_profile";
16
+ UseCase2["SCRIPT_INJECT"] = "script_inject";
17
+ UseCase2["COVERAGE"] = "coverage";
18
+ UseCase2["RECORD_REPLAY"] = "record_replay";
19
+ })(UseCase ||= {});
20
+
21
+ class BrowserError extends Error {
22
+ code;
23
+ retryable;
24
+ constructor(message, code = "BROWSER_ERROR", retryable = false) {
25
+ super(message);
26
+ this.code = code;
27
+ this.retryable = retryable;
28
+ this.name = "BrowserError";
29
+ }
30
+ }
31
+
32
+ class SessionNotFoundError extends BrowserError {
33
+ constructor(id) {
34
+ super(`Session not found: ${id}`, "SESSION_NOT_FOUND", false);
35
+ this.name = "SessionNotFoundError";
36
+ }
37
+ }
38
+
39
+ class EngineNotAvailableError extends BrowserError {
40
+ constructor(engine, reason) {
41
+ super(`Engine '${engine}' is not available${reason ? `: ${reason}` : ""}`, "ENGINE_NOT_AVAILABLE", false);
42
+ this.name = "EngineNotAvailableError";
43
+ }
44
+ }
45
+
46
+ class NavigationError extends BrowserError {
47
+ constructor(url, reason) {
48
+ super(`Navigation to '${url}' failed${reason ? `: ${reason}` : ""}`, "NAVIGATION_ERROR", true);
49
+ this.name = "NavigationError";
50
+ }
51
+ }
52
+
53
+ class ElementNotFoundError extends BrowserError {
54
+ constructor(selector) {
55
+ super(`Element not found: ${selector}`, "ELEMENT_NOT_FOUND", false);
56
+ this.name = "ElementNotFoundError";
57
+ }
58
+ }
59
+
60
+ class RecordingNotFoundError extends BrowserError {
61
+ constructor(id) {
62
+ super(`Recording not found: ${id}`, "RECORDING_NOT_FOUND", false);
63
+ this.name = "RecordingNotFoundError";
64
+ }
65
+ }
66
+
67
+ class AgentNotFoundError extends BrowserError {
68
+ constructor(id) {
69
+ super(`Agent not found: ${id}`, "AGENT_NOT_FOUND", false);
70
+ this.name = "AgentNotFoundError";
71
+ }
72
+ }
73
+
74
+ class ProjectNotFoundError extends BrowserError {
75
+ constructor(id) {
76
+ super(`Project not found: ${id}`, "PROJECT_NOT_FOUND", false);
77
+ this.name = "ProjectNotFoundError";
78
+ }
79
+ }
80
+ // src/db/schema.ts
81
+ import { Database } from "bun:sqlite";
82
+ import { join } from "path";
83
+ import { mkdirSync } from "fs";
84
+ import { homedir } from "os";
85
+ function getDataDir() {
86
+ return process.env["BROWSER_DATA_DIR"] ?? join(homedir(), ".browser");
87
+ }
88
+ var _db = null;
89
+ var _dbPath = null;
90
+ function getDatabase(path) {
91
+ const resolvedPath = path ?? process.env["BROWSER_DB_PATH"] ?? join(getDataDir(), "browser.db");
92
+ if (_db && _dbPath === resolvedPath)
93
+ return _db;
94
+ if (_db) {
95
+ try {
96
+ _db.close();
97
+ } catch {}
98
+ _db = null;
99
+ }
100
+ mkdirSync(join(resolvedPath, ".."), { recursive: true });
101
+ _db = new Database(resolvedPath);
102
+ _dbPath = resolvedPath;
103
+ _db.exec("PRAGMA journal_mode=WAL;");
104
+ _db.exec("PRAGMA foreign_keys=ON;");
105
+ runMigrations(_db);
106
+ return _db;
107
+ }
108
+ function resetDatabase() {
109
+ if (_db) {
110
+ try {
111
+ _db.close();
112
+ } catch {}
113
+ }
114
+ _db = null;
115
+ _dbPath = null;
116
+ }
117
+ function runMigrations(db) {
118
+ db.exec(`
119
+ CREATE TABLE IF NOT EXISTS schema_migrations (
120
+ version INTEGER PRIMARY KEY,
121
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
122
+ );
123
+ `);
124
+ const applied = new Set(db.query("SELECT version FROM schema_migrations").all().map((r) => r.version));
125
+ const migrations = [
126
+ {
127
+ version: 1,
128
+ sql: `
129
+ CREATE TABLE IF NOT EXISTS projects (
130
+ id TEXT PRIMARY KEY,
131
+ name TEXT NOT NULL UNIQUE,
132
+ path TEXT NOT NULL,
133
+ description TEXT,
134
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
135
+ );
136
+
137
+ CREATE TABLE IF NOT EXISTS agents (
138
+ id TEXT PRIMARY KEY,
139
+ name TEXT NOT NULL,
140
+ description TEXT,
141
+ session_id TEXT,
142
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
143
+ working_dir TEXT,
144
+ last_seen TEXT NOT NULL DEFAULT (datetime('now')),
145
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
146
+ );
147
+
148
+ CREATE TABLE IF NOT EXISTS heartbeats (
149
+ id TEXT PRIMARY KEY,
150
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
151
+ session_id TEXT,
152
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
153
+ );
154
+
155
+ CREATE TABLE IF NOT EXISTS sessions (
156
+ id TEXT PRIMARY KEY,
157
+ engine TEXT NOT NULL,
158
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
159
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
160
+ start_url TEXT,
161
+ status TEXT NOT NULL DEFAULT 'active',
162
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
163
+ closed_at TEXT
164
+ );
165
+
166
+ CREATE TABLE IF NOT EXISTS snapshots (
167
+ id TEXT PRIMARY KEY,
168
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
169
+ url TEXT NOT NULL,
170
+ title TEXT,
171
+ html TEXT,
172
+ screenshot_path TEXT,
173
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
174
+ );
175
+
176
+ CREATE TABLE IF NOT EXISTS network_log (
177
+ id TEXT PRIMARY KEY,
178
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
179
+ method TEXT NOT NULL,
180
+ url TEXT NOT NULL,
181
+ status_code INTEGER,
182
+ request_headers TEXT,
183
+ response_headers TEXT,
184
+ request_body TEXT,
185
+ body_size INTEGER,
186
+ duration_ms INTEGER,
187
+ resource_type TEXT,
188
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
189
+ );
190
+
191
+ CREATE TABLE IF NOT EXISTS console_log (
192
+ id TEXT PRIMARY KEY,
193
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
194
+ level TEXT NOT NULL DEFAULT 'log',
195
+ message TEXT NOT NULL,
196
+ source TEXT,
197
+ line_number INTEGER,
198
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
199
+ );
200
+
201
+ CREATE TABLE IF NOT EXISTS recordings (
202
+ id TEXT PRIMARY KEY,
203
+ name TEXT NOT NULL,
204
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
205
+ start_url TEXT,
206
+ steps TEXT NOT NULL DEFAULT '[]',
207
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
208
+ );
209
+
210
+ CREATE TABLE IF NOT EXISTS crawl_results (
211
+ id TEXT PRIMARY KEY,
212
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
213
+ start_url TEXT NOT NULL,
214
+ depth INTEGER NOT NULL DEFAULT 1,
215
+ pages TEXT NOT NULL DEFAULT '[]',
216
+ links TEXT NOT NULL DEFAULT '[]',
217
+ errors TEXT NOT NULL DEFAULT '[]',
218
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
219
+ );
220
+
221
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
222
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
223
+ CREATE INDEX IF NOT EXISTS idx_snapshots_session ON snapshots(session_id);
224
+ CREATE INDEX IF NOT EXISTS idx_network_log_session ON network_log(session_id);
225
+ CREATE INDEX IF NOT EXISTS idx_console_log_session ON console_log(session_id);
226
+ CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
227
+ CREATE INDEX IF NOT EXISTS idx_heartbeats_agent ON heartbeats(agent_id);
228
+ CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id);
229
+ CREATE INDEX IF NOT EXISTS idx_crawl_results_project ON crawl_results(project_id);
230
+ `
231
+ }
232
+ ];
233
+ for (const m of migrations) {
234
+ if (!applied.has(m.version)) {
235
+ db.transaction(() => {
236
+ db.exec(m.sql);
237
+ db.prepare("INSERT INTO schema_migrations (version) VALUES (?)").run(m.version);
238
+ })();
239
+ }
240
+ }
241
+ }
242
+ // src/db/projects.ts
243
+ import { randomUUID } from "crypto";
244
+ function createProject(data) {
245
+ const db = getDatabase();
246
+ const id = randomUUID();
247
+ db.prepare("INSERT INTO projects (id, name, path, description) VALUES (?, ?, ?, ?)").run(id, data.name, data.path, data.description ?? null);
248
+ return getProject(id);
249
+ }
250
+ function ensureProject(name, path, description) {
251
+ const db = getDatabase();
252
+ const existing = db.query("SELECT * FROM projects WHERE name = ?").get(name);
253
+ if (existing)
254
+ return existing;
255
+ return createProject({ name, path, description });
256
+ }
257
+ function getProject(id) {
258
+ const db = getDatabase();
259
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
260
+ if (!row)
261
+ throw new ProjectNotFoundError(id);
262
+ return row;
263
+ }
264
+ function getProjectByName(name) {
265
+ const db = getDatabase();
266
+ return db.query("SELECT * FROM projects WHERE name = ?").get(name) ?? null;
267
+ }
268
+ function listProjects() {
269
+ const db = getDatabase();
270
+ return db.query("SELECT * FROM projects ORDER BY created_at DESC").all();
271
+ }
272
+ function updateProject(id, data) {
273
+ const db = getDatabase();
274
+ const fields = [];
275
+ const values = [];
276
+ if (data.name !== undefined) {
277
+ fields.push("name = ?");
278
+ values.push(data.name);
279
+ }
280
+ if (data.path !== undefined) {
281
+ fields.push("path = ?");
282
+ values.push(data.path);
283
+ }
284
+ if (data.description !== undefined) {
285
+ fields.push("description = ?");
286
+ values.push(data.description ?? null);
287
+ }
288
+ if (fields.length === 0)
289
+ return getProject(id);
290
+ values.push(id);
291
+ db.prepare(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`).run(...values);
292
+ return getProject(id);
293
+ }
294
+ function deleteProject(id) {
295
+ const db = getDatabase();
296
+ db.prepare("DELETE FROM projects WHERE id = ?").run(id);
297
+ }
298
+ // src/db/agents.ts
299
+ import { randomUUID as randomUUID2 } from "crypto";
300
+ function registerAgent(name, opts = {}) {
301
+ const db = getDatabase();
302
+ const existing = db.query("SELECT * FROM agents WHERE name = ?").get(name);
303
+ if (existing) {
304
+ db.prepare("UPDATE agents SET last_seen = datetime('now'), session_id = ?, project_id = ?, working_dir = ? WHERE name = ?").run(opts.sessionId ?? existing.session_id ?? null, opts.projectId ?? existing.project_id ?? null, opts.workingDir ?? existing.working_dir ?? null, name);
305
+ return getAgentByName(name);
306
+ }
307
+ const id = randomUUID2();
308
+ db.prepare("INSERT INTO agents (id, name, description, session_id, project_id, working_dir) VALUES (?, ?, ?, ?, ?, ?)").run(id, name, opts.description ?? null, opts.sessionId ?? null, opts.projectId ?? null, opts.workingDir ?? null);
309
+ return getAgent(id);
310
+ }
311
+ function heartbeat(agentId) {
312
+ const db = getDatabase();
313
+ const agent = db.query("SELECT * FROM agents WHERE id = ?").get(agentId);
314
+ if (!agent)
315
+ throw new AgentNotFoundError(agentId);
316
+ db.prepare("UPDATE agents SET last_seen = datetime('now') WHERE id = ?").run(agentId);
317
+ db.prepare("INSERT INTO heartbeats (id, agent_id, session_id) VALUES (?, ?, ?)").run(randomUUID2(), agentId, agent.session_id ?? null);
318
+ }
319
+ function getAgent(id) {
320
+ const db = getDatabase();
321
+ const row = db.query("SELECT * FROM agents WHERE id = ?").get(id);
322
+ if (!row)
323
+ throw new AgentNotFoundError(id);
324
+ return row;
325
+ }
326
+ function getAgentByName(name) {
327
+ const db = getDatabase();
328
+ return db.query("SELECT * FROM agents WHERE name = ?").get(name) ?? null;
329
+ }
330
+ function listAgents(projectId) {
331
+ const db = getDatabase();
332
+ if (projectId) {
333
+ return db.query("SELECT * FROM agents WHERE project_id = ? ORDER BY last_seen DESC").all(projectId);
334
+ }
335
+ return db.query("SELECT * FROM agents ORDER BY last_seen DESC").all();
336
+ }
337
+ function updateAgent(id, data) {
338
+ const db = getDatabase();
339
+ const fields = [];
340
+ const values = [];
341
+ if (data.name !== undefined) {
342
+ fields.push("name = ?");
343
+ values.push(data.name ?? null);
344
+ }
345
+ if (data.description !== undefined) {
346
+ fields.push("description = ?");
347
+ values.push(data.description ?? null);
348
+ }
349
+ if (data.session_id !== undefined) {
350
+ fields.push("session_id = ?");
351
+ values.push(data.session_id ?? null);
352
+ }
353
+ if (data.project_id !== undefined) {
354
+ fields.push("project_id = ?");
355
+ values.push(data.project_id ?? null);
356
+ }
357
+ if (data.working_dir !== undefined) {
358
+ fields.push("working_dir = ?");
359
+ values.push(data.working_dir ?? null);
360
+ }
361
+ if (fields.length === 0)
362
+ return getAgent(id);
363
+ values.push(id);
364
+ db.prepare(`UPDATE agents SET ${fields.join(", ")} WHERE id = ?`).run(...values);
365
+ return getAgent(id);
366
+ }
367
+ function deleteAgent(id) {
368
+ const db = getDatabase();
369
+ db.prepare("DELETE FROM agents WHERE id = ?").run(id);
370
+ }
371
+ function cleanStaleAgents(thresholdMs) {
372
+ const db = getDatabase();
373
+ const cutoff = new Date(Date.now() - thresholdMs).toISOString().replace("T", " ").split(".")[0];
374
+ const result = db.prepare("DELETE FROM agents WHERE last_seen < ?").run(cutoff);
375
+ return result.changes;
376
+ }
377
+ // src/db/sessions.ts
378
+ import { randomUUID as randomUUID3 } from "crypto";
379
+ function createSession(data) {
380
+ const db = getDatabase();
381
+ const id = randomUUID3();
382
+ db.prepare("INSERT INTO sessions (id, engine, project_id, agent_id, start_url) VALUES (?, ?, ?, ?, ?)").run(id, data.engine, data.projectId ?? null, data.agentId ?? null, data.startUrl ?? null);
383
+ return getSession(id);
384
+ }
385
+ function getSession(id) {
386
+ const db = getDatabase();
387
+ const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id);
388
+ if (!row)
389
+ throw new SessionNotFoundError(id);
390
+ return row;
391
+ }
392
+ function listSessions(filter) {
393
+ const db = getDatabase();
394
+ const conditions = [];
395
+ const values = [];
396
+ if (filter?.status) {
397
+ conditions.push("status = ?");
398
+ values.push(filter.status);
399
+ }
400
+ if (filter?.projectId) {
401
+ conditions.push("project_id = ?");
402
+ values.push(filter.projectId);
403
+ }
404
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
405
+ return db.query(`SELECT * FROM sessions ${where} ORDER BY created_at DESC`).all(...values);
406
+ }
407
+ function updateSessionStatus(id, status) {
408
+ const db = getDatabase();
409
+ const closedAt = status === "closed" || status === "error" ? "datetime('now')" : "NULL";
410
+ db.prepare(`UPDATE sessions SET status = ?, closed_at = ${closedAt === "NULL" ? "NULL" : "(datetime('now'))"} WHERE id = ?`).run(status, id);
411
+ return getSession(id);
412
+ }
413
+ function closeSession(id) {
414
+ return updateSessionStatus(id, "closed");
415
+ }
416
+ function deleteSession(id) {
417
+ const db = getDatabase();
418
+ db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
419
+ }
420
+ // src/db/snapshots.ts
421
+ import { randomUUID as randomUUID4 } from "crypto";
422
+ function createSnapshot(data) {
423
+ const db = getDatabase();
424
+ const id = randomUUID4();
425
+ db.prepare("INSERT INTO snapshots (id, session_id, url, title, html, screenshot_path) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.url, data.title ?? null, data.html ?? null, data.screenshot_path ?? null);
426
+ return getSnapshot(id);
427
+ }
428
+ function getSnapshot(id) {
429
+ const db = getDatabase();
430
+ return db.query("SELECT * FROM snapshots WHERE id = ?").get(id) ?? null;
431
+ }
432
+ function listSnapshots(sessionId) {
433
+ const db = getDatabase();
434
+ return db.query("SELECT * FROM snapshots WHERE session_id = ? ORDER BY timestamp DESC").all(sessionId);
435
+ }
436
+ function deleteSnapshot(id) {
437
+ const db = getDatabase();
438
+ db.prepare("DELETE FROM snapshots WHERE id = ?").run(id);
439
+ }
440
+ function deleteSnapshotsBySession(sessionId) {
441
+ const db = getDatabase();
442
+ db.prepare("DELETE FROM snapshots WHERE session_id = ?").run(sessionId);
443
+ }
444
+ // src/db/network-log.ts
445
+ import { randomUUID as randomUUID5 } from "crypto";
446
+ function logRequest(data) {
447
+ const db = getDatabase();
448
+ const id = randomUUID5();
449
+ db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
450
+ response_headers, request_body, body_size, duration_ms, resource_type)
451
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
452
+ return getNetworkRequest(id);
453
+ }
454
+ function getNetworkRequest(id) {
455
+ const db = getDatabase();
456
+ return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
457
+ }
458
+ function getNetworkLog(sessionId) {
459
+ const db = getDatabase();
460
+ return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
461
+ }
462
+ function clearNetworkLog(sessionId) {
463
+ const db = getDatabase();
464
+ db.prepare("DELETE FROM network_log WHERE session_id = ?").run(sessionId);
465
+ }
466
+ function deleteNetworkRequest(id) {
467
+ const db = getDatabase();
468
+ db.prepare("DELETE FROM network_log WHERE id = ?").run(id);
469
+ }
470
+ // src/db/console-log.ts
471
+ import { randomUUID as randomUUID6 } from "crypto";
472
+ function logConsoleMessage(data) {
473
+ const db = getDatabase();
474
+ const id = randomUUID6();
475
+ db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
476
+ return getConsoleMessage(id);
477
+ }
478
+ function getConsoleMessage(id) {
479
+ const db = getDatabase();
480
+ return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
481
+ }
482
+ function getConsoleLog(sessionId, level) {
483
+ const db = getDatabase();
484
+ if (level) {
485
+ return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
486
+ }
487
+ return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
488
+ }
489
+ function clearConsoleLog(sessionId) {
490
+ const db = getDatabase();
491
+ db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
492
+ }
493
+ // src/db/recordings.ts
494
+ import { randomUUID as randomUUID7 } from "crypto";
495
+ function deserialize(row) {
496
+ return {
497
+ ...row,
498
+ project_id: row.project_id ?? undefined,
499
+ start_url: row.start_url ?? undefined,
500
+ steps: JSON.parse(row.steps)
501
+ };
502
+ }
503
+ function createRecording(data) {
504
+ const db = getDatabase();
505
+ const id = randomUUID7();
506
+ db.prepare("INSERT INTO recordings (id, name, project_id, start_url, steps) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.project_id ?? null, data.start_url ?? null, JSON.stringify(data.steps ?? []));
507
+ return getRecording(id);
508
+ }
509
+ function getRecording(id) {
510
+ const db = getDatabase();
511
+ const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
512
+ if (!row)
513
+ throw new RecordingNotFoundError(id);
514
+ return deserialize(row);
515
+ }
516
+ function listRecordings(projectId) {
517
+ const db = getDatabase();
518
+ const rows = projectId ? db.query("SELECT * FROM recordings WHERE project_id = ? ORDER BY created_at DESC").all(projectId) : db.query("SELECT * FROM recordings ORDER BY created_at DESC").all();
519
+ return rows.map(deserialize);
520
+ }
521
+ function updateRecording(id, data) {
522
+ const db = getDatabase();
523
+ const fields = [];
524
+ const values = [];
525
+ if (data.name !== undefined) {
526
+ fields.push("name = ?");
527
+ values.push(data.name);
528
+ }
529
+ if (data.steps !== undefined) {
530
+ fields.push("steps = ?");
531
+ values.push(JSON.stringify(data.steps));
532
+ }
533
+ if (data.start_url !== undefined) {
534
+ fields.push("start_url = ?");
535
+ values.push(data.start_url ?? null);
536
+ }
537
+ if (fields.length === 0)
538
+ return getRecording(id);
539
+ values.push(id);
540
+ db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
541
+ return getRecording(id);
542
+ }
543
+ function deleteRecording(id) {
544
+ const db = getDatabase();
545
+ db.prepare("DELETE FROM recordings WHERE id = ?").run(id);
546
+ }
547
+ // src/db/crawl-results.ts
548
+ import { randomUUID as randomUUID8 } from "crypto";
549
+ function deserialize2(row) {
550
+ const pages = JSON.parse(row.pages);
551
+ return {
552
+ id: row.id,
553
+ project_id: row.project_id ?? undefined,
554
+ start_url: row.start_url,
555
+ depth: row.depth,
556
+ pages,
557
+ total_links: pages.reduce((acc, p) => acc + p.links.length, 0),
558
+ errors: JSON.parse(row.errors),
559
+ created_at: row.created_at
560
+ };
561
+ }
562
+ function createCrawlResult(data) {
563
+ const db = getDatabase();
564
+ const id = randomUUID8();
565
+ db.prepare("INSERT INTO crawl_results (id, project_id, start_url, depth, pages, links, errors) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, data.project_id ?? null, data.start_url, data.depth, JSON.stringify(data.pages), JSON.stringify(data.pages.flatMap((p) => p.links)), JSON.stringify(data.errors));
566
+ return getCrawlResult(id);
567
+ }
568
+ function getCrawlResult(id) {
569
+ const db = getDatabase();
570
+ const row = db.query("SELECT * FROM crawl_results WHERE id = ?").get(id);
571
+ return row ? deserialize2(row) : null;
572
+ }
573
+ function listCrawlResults(projectId) {
574
+ const db = getDatabase();
575
+ const rows = projectId ? db.query("SELECT * FROM crawl_results WHERE project_id = ? ORDER BY created_at DESC").all(projectId) : db.query("SELECT * FROM crawl_results ORDER BY created_at DESC").all();
576
+ return rows.map(deserialize2);
577
+ }
578
+ function deleteCrawlResult(id) {
579
+ const db = getDatabase();
580
+ db.prepare("DELETE FROM crawl_results WHERE id = ?").run(id);
581
+ }
582
+ // src/db/heartbeats.ts
583
+ import { randomUUID as randomUUID9 } from "crypto";
584
+ function recordHeartbeat(agentId, sessionId) {
585
+ const db = getDatabase();
586
+ const id = randomUUID9();
587
+ db.prepare("INSERT INTO heartbeats (id, agent_id, session_id) VALUES (?, ?, ?)").run(id, agentId, sessionId ?? null);
588
+ db.prepare("UPDATE agents SET last_seen = datetime('now') WHERE id = ?").run(agentId);
589
+ return getLastHeartbeat(agentId);
590
+ }
591
+ function getLastHeartbeat(agentId) {
592
+ const db = getDatabase();
593
+ return db.query("SELECT * FROM heartbeats WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 1").get(agentId) ?? null;
594
+ }
595
+ function listHeartbeats(agentId, limit = 50) {
596
+ const db = getDatabase();
597
+ return db.query("SELECT * FROM heartbeats WHERE agent_id = ? ORDER BY timestamp DESC LIMIT ?").all(agentId, limit);
598
+ }
599
+ function cleanOldHeartbeats(olderThanMs) {
600
+ const db = getDatabase();
601
+ const cutoff = new Date(Date.now() - olderThanMs).toISOString().replace("T", " ").split(".")[0];
602
+ const result = db.prepare("DELETE FROM heartbeats WHERE timestamp < ?").run(cutoff);
603
+ return result.changes;
604
+ }
605
+ // src/engines/playwright.ts
606
+ import { chromium } from "playwright";
607
+ var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
608
+ async function launchPlaywright(options) {
609
+ try {
610
+ return await chromium.launch({
611
+ headless: options?.headless ?? true,
612
+ executablePath: options?.executablePath
613
+ });
614
+ } catch (err) {
615
+ throw new BrowserError(`Failed to launch Playwright browser: ${err instanceof Error ? err.message : String(err)}`, "PLAYWRIGHT_LAUNCH_FAILED", true);
616
+ }
617
+ }
618
+ async function getPage(browser, options) {
619
+ const context = await browser.newContext({
620
+ viewport: options?.viewport ?? DEFAULT_VIEWPORT,
621
+ userAgent: options?.userAgent,
622
+ locale: options?.locale
623
+ });
624
+ return context.newPage();
625
+ }
626
+ async function closeBrowser(browser) {
627
+ try {
628
+ await browser.close();
629
+ } catch {}
630
+ }
631
+ async function closePage(page) {
632
+ try {
633
+ await page.context().close();
634
+ } catch {}
635
+ }
636
+
637
+ class BrowserPool {
638
+ pool = [];
639
+ maxSize;
640
+ options;
641
+ constructor(maxSize = 3, options) {
642
+ this.maxSize = maxSize;
643
+ this.options = options;
644
+ }
645
+ async acquire() {
646
+ const available = this.pool.find((e) => !e.inUse);
647
+ if (available) {
648
+ available.inUse = true;
649
+ return available.browser;
650
+ }
651
+ if (this.pool.length < this.maxSize) {
652
+ const browser = await launchPlaywright(this.options);
653
+ this.pool.push({ browser, inUse: true, createdAt: Date.now() });
654
+ return browser;
655
+ }
656
+ return new Promise((resolve) => {
657
+ const interval = setInterval(() => {
658
+ const free = this.pool.find((e) => !e.inUse);
659
+ if (free) {
660
+ clearInterval(interval);
661
+ free.inUse = true;
662
+ resolve(free.browser);
663
+ }
664
+ }, 100);
665
+ });
666
+ }
667
+ release(browser) {
668
+ const entry = this.pool.find((e) => e.browser === browser);
669
+ if (entry)
670
+ entry.inUse = false;
671
+ }
672
+ async destroyAll() {
673
+ await Promise.all(this.pool.map((e) => e.browser.close().catch(() => {})));
674
+ this.pool = [];
675
+ }
676
+ get size() {
677
+ return this.pool.length;
678
+ }
679
+ get available() {
680
+ return this.pool.filter((e) => !e.inUse).length;
681
+ }
682
+ }
683
+ // src/engines/cdp.ts
684
+ class CDPClient {
685
+ session;
686
+ networkEnabled = false;
687
+ performanceEnabled = false;
688
+ constructor(session) {
689
+ this.session = session;
690
+ }
691
+ static async fromPage(page) {
692
+ try {
693
+ const session = await page.context().newCDPSession(page);
694
+ return new CDPClient(session);
695
+ } catch (err) {
696
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
697
+ }
698
+ }
699
+ async send(method, params) {
700
+ try {
701
+ return await this.session.send(method, params);
702
+ } catch (err) {
703
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
704
+ }
705
+ }
706
+ on(event, handler) {
707
+ this.session.on(event, handler);
708
+ }
709
+ off(event, handler) {
710
+ this.session.off(event, handler);
711
+ }
712
+ async enableNetwork() {
713
+ if (!this.networkEnabled) {
714
+ await this.send("Network.enable");
715
+ this.networkEnabled = true;
716
+ }
717
+ }
718
+ async enablePerformance() {
719
+ if (!this.performanceEnabled) {
720
+ await this.send("Performance.enable");
721
+ this.performanceEnabled = true;
722
+ }
723
+ }
724
+ async getPerformanceMetrics() {
725
+ await this.enablePerformance();
726
+ const result = await this.send("Performance.getMetrics");
727
+ const m = {};
728
+ for (const metric of result.metrics) {
729
+ m[metric.name] = metric.value;
730
+ }
731
+ return {
732
+ js_heap_size_used: m["JSHeapUsedSize"],
733
+ js_heap_size_total: m["JSHeapTotalSize"],
734
+ dom_interactive: m["DOMInteractive"],
735
+ dom_complete: m["DOMComplete"],
736
+ load_event: m["LoadEventEnd"]
737
+ };
738
+ }
739
+ async startJSCoverage() {
740
+ await this.send("Profiler.enable");
741
+ await this.send("Debugger.enable");
742
+ await this.send("Profiler.startPreciseCoverage", {
743
+ callCount: false,
744
+ detailed: true
745
+ });
746
+ }
747
+ async stopJSCoverage() {
748
+ const result = await this.send("Profiler.takePreciseCoverage");
749
+ await this.send("Profiler.stopPreciseCoverage");
750
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
751
+ url: r.url,
752
+ text: "",
753
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
754
+ }));
755
+ }
756
+ async getCoverage() {
757
+ await this.startJSCoverage();
758
+ const js = await this.stopJSCoverage();
759
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
760
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
761
+ }
762
+ async captureHAREntries(page, handler) {
763
+ await this.enableNetwork();
764
+ const requestTimings = new Map;
765
+ const onRequest = (params) => {
766
+ requestTimings.set(params.requestId, params.timestamp);
767
+ };
768
+ const onResponse = (params) => {
769
+ const start = requestTimings.get(params.requestId);
770
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
771
+ handler({
772
+ method: "GET",
773
+ url: params.response.url,
774
+ status: params.response.status,
775
+ duration
776
+ });
777
+ };
778
+ this.on("Network.requestWillBeSent", onRequest);
779
+ this.on("Network.responseReceived", onResponse);
780
+ return () => {
781
+ this.off("Network.requestWillBeSent", onRequest);
782
+ this.off("Network.responseReceived", onResponse);
783
+ };
784
+ }
785
+ async detach() {
786
+ try {
787
+ await this.session.detach();
788
+ } catch {}
789
+ }
790
+ }
791
+ // src/engines/lightpanda.ts
792
+ import { execSync, spawn } from "child_process";
793
+ import { chromium as chromium2 } from "playwright";
794
+ var DEFAULT_LIGHTPANDA_PORT = 9222;
795
+ var LIGHTPANDA_BINARY = process.env["LIGHTPANDA_BINARY"] ?? "lightpanda";
796
+ function isLightpandaAvailable() {
797
+ try {
798
+ execSync(`which ${LIGHTPANDA_BINARY}`, { stdio: "ignore" });
799
+ return true;
800
+ } catch {
801
+ const paths = [
802
+ "/usr/local/bin/lightpanda",
803
+ "/usr/bin/lightpanda",
804
+ `${process.env["HOME"]}/.browser/bin/lightpanda`
805
+ ];
806
+ return paths.some((p) => {
807
+ try {
808
+ execSync(`test -x ${p}`, { stdio: "ignore" });
809
+ return true;
810
+ } catch {
811
+ return false;
812
+ }
813
+ });
814
+ }
815
+ }
816
+ function getLightpandaBinaryPath() {
817
+ if (process.env["LIGHTPANDA_BINARY"])
818
+ return process.env["LIGHTPANDA_BINARY"];
819
+ const paths = [
820
+ "lightpanda",
821
+ "/usr/local/bin/lightpanda",
822
+ "/usr/bin/lightpanda",
823
+ `${process.env["HOME"]}/.browser/bin/lightpanda`
824
+ ];
825
+ for (const p of paths) {
826
+ try {
827
+ execSync(`which ${p}`, { stdio: "ignore" });
828
+ return p;
829
+ } catch {
830
+ try {
831
+ execSync(`test -x ${p}`, { stdio: "ignore" });
832
+ return p;
833
+ } catch {
834
+ continue;
835
+ }
836
+ }
837
+ }
838
+ throw new EngineNotAvailableError("lightpanda", "binary not found. Run: browser install-browser --engine lightpanda");
839
+ }
840
+ var _lpProcess = null;
841
+ async function launchLightpanda(port) {
842
+ if (_lpProcess)
843
+ return _lpProcess;
844
+ if (!isLightpandaAvailable()) {
845
+ throw new EngineNotAvailableError("lightpanda", "binary not found. Run: browser install-browser --engine lightpanda");
846
+ }
847
+ const usePort = port ?? DEFAULT_LIGHTPANDA_PORT;
848
+ const binary = getLightpandaBinaryPath();
849
+ const proc = spawn(binary, ["--cdp-host", "127.0.0.1", "--cdp-port", String(usePort)], {
850
+ stdio: "ignore",
851
+ detached: false
852
+ });
853
+ await new Promise((resolve, reject) => {
854
+ const timeout = setTimeout(() => reject(new BrowserError("Lightpanda startup timeout", "LIGHTPANDA_TIMEOUT")), 5000);
855
+ const check = setInterval(async () => {
856
+ try {
857
+ const resp = await fetch(`http://127.0.0.1:${usePort}/json/version`);
858
+ if (resp.ok) {
859
+ clearInterval(check);
860
+ clearTimeout(timeout);
861
+ resolve();
862
+ }
863
+ } catch {}
864
+ }, 100);
865
+ proc.on("error", (err) => {
866
+ clearInterval(check);
867
+ clearTimeout(timeout);
868
+ reject(new BrowserError(`Lightpanda failed to start: ${err.message}`, "LIGHTPANDA_ERROR"));
869
+ });
870
+ });
871
+ _lpProcess = {
872
+ process: proc,
873
+ port: usePort,
874
+ wsUrl: `ws://127.0.0.1:${usePort}`
875
+ };
876
+ return _lpProcess;
877
+ }
878
+ async function connectLightpanda(port) {
879
+ const lp = await launchLightpanda(port);
880
+ try {
881
+ const resp = await fetch(`http://127.0.0.1:${lp.port}/json/version`);
882
+ const info = await resp.json();
883
+ const wsUrl = info.webSocketDebuggerUrl ?? `ws://127.0.0.1:${lp.port}`;
884
+ return await chromium2.connectOverCDP(wsUrl);
885
+ } catch (err) {
886
+ throw new BrowserError(`Failed to connect to Lightpanda: ${err instanceof Error ? err.message : String(err)}`, "LIGHTPANDA_CONNECT_FAILED");
887
+ }
888
+ }
889
+ function stopLightpanda() {
890
+ if (_lpProcess) {
891
+ _lpProcess.process.kill();
892
+ _lpProcess = null;
893
+ }
894
+ }
895
+
896
+ class LightpandaPage {
897
+ page;
898
+ constructor(page) {
899
+ this.page = page;
900
+ }
901
+ static async create(port) {
902
+ const browser = await connectLightpanda(port);
903
+ const context = await browser.newContext();
904
+ const page = await context.newPage();
905
+ return new LightpandaPage(page);
906
+ }
907
+ async navigate(url) {
908
+ await this.page.goto(url, { waitUntil: "domcontentloaded" });
909
+ }
910
+ async getContent() {
911
+ return this.page.content();
912
+ }
913
+ async getTitle() {
914
+ return this.page.title();
915
+ }
916
+ async getLinks() {
917
+ return this.page.evaluate(() => Array.from(document.querySelectorAll("a[href]")).map((a) => a.href).filter((h) => h.startsWith("http")));
918
+ }
919
+ async getText(selector) {
920
+ if (selector) {
921
+ const el = await this.page.$(selector);
922
+ return el ? await el.textContent() ?? "" : "";
923
+ }
924
+ return this.page.evaluate(() => document.body.innerText ?? "");
925
+ }
926
+ get rawPage() {
927
+ return this.page;
928
+ }
929
+ async close() {
930
+ await this.page.context().close();
931
+ }
932
+ }
933
+ // src/engines/selector.ts
934
+ var ENGINE_MAP = {
935
+ ["scrape" /* SCRAPE */]: "lightpanda",
936
+ ["extract_links" /* EXTRACT_LINKS */]: "lightpanda",
937
+ ["status_check" /* STATUS_CHECK */]: "lightpanda",
938
+ ["form_fill" /* FORM_FILL */]: "playwright",
939
+ ["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
940
+ ["screenshot" /* SCREENSHOT */]: "playwright",
941
+ ["auth_flow" /* AUTH_FLOW */]: "playwright",
942
+ ["multi_tab" /* MULTI_TAB */]: "playwright",
943
+ ["record_replay" /* RECORD_REPLAY */]: "playwright",
944
+ ["network_monitor" /* NETWORK_MONITOR */]: "cdp",
945
+ ["har_capture" /* HAR_CAPTURE */]: "cdp",
946
+ ["perf_profile" /* PERF_PROFILE */]: "cdp",
947
+ ["script_inject" /* SCRIPT_INJECT */]: "cdp",
948
+ ["coverage" /* COVERAGE */]: "cdp"
949
+ };
950
+ function selectEngine(useCase, explicit) {
951
+ if (explicit && explicit !== "auto")
952
+ return explicit;
953
+ const preferred = ENGINE_MAP[useCase];
954
+ if (preferred === "lightpanda" && !isLightpandaAvailable()) {
955
+ return "playwright";
956
+ }
957
+ return preferred;
958
+ }
959
+ function isEngineAvailable(engine) {
960
+ if (engine === "auto")
961
+ return true;
962
+ if (engine === "playwright")
963
+ return true;
964
+ if (engine === "cdp")
965
+ return true;
966
+ if (engine === "lightpanda")
967
+ return isLightpandaAvailable();
968
+ return false;
969
+ }
970
+ function inferUseCase(label) {
971
+ const map = {
972
+ scrape: "scrape" /* SCRAPE */,
973
+ extract: "extract_links" /* EXTRACT_LINKS */,
974
+ links: "extract_links" /* EXTRACT_LINKS */,
975
+ status: "status_check" /* STATUS_CHECK */,
976
+ check: "status_check" /* STATUS_CHECK */,
977
+ form: "form_fill" /* FORM_FILL */,
978
+ fill: "form_fill" /* FORM_FILL */,
979
+ spa: "spa_navigate" /* SPA_NAVIGATE */,
980
+ navigate: "spa_navigate" /* SPA_NAVIGATE */,
981
+ screenshot: "screenshot" /* SCREENSHOT */,
982
+ auth: "auth_flow" /* AUTH_FLOW */,
983
+ login: "auth_flow" /* AUTH_FLOW */,
984
+ "multi-tab": "multi_tab" /* MULTI_TAB */,
985
+ tabs: "multi_tab" /* MULTI_TAB */,
986
+ network: "network_monitor" /* NETWORK_MONITOR */,
987
+ har: "har_capture" /* HAR_CAPTURE */,
988
+ perf: "perf_profile" /* PERF_PROFILE */,
989
+ performance: "perf_profile" /* PERF_PROFILE */,
990
+ inject: "script_inject" /* SCRIPT_INJECT */,
991
+ coverage: "coverage" /* COVERAGE */,
992
+ record: "record_replay" /* RECORD_REPLAY */,
993
+ replay: "record_replay" /* RECORD_REPLAY */
994
+ };
995
+ return map[label.toLowerCase()] ?? "spa_navigate" /* SPA_NAVIGATE */;
996
+ }
997
+ // src/lib/session.ts
998
+ var handles = new Map;
999
+ async function createSession2(opts = {}) {
1000
+ const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
1001
+ const resolvedEngine = engine === "auto" ? "playwright" : engine;
1002
+ let browser;
1003
+ let page;
1004
+ if (resolvedEngine === "lightpanda") {
1005
+ browser = await connectLightpanda();
1006
+ const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
1007
+ page = await context.newPage();
1008
+ } else {
1009
+ browser = await launchPlaywright({
1010
+ headless: opts.headless ?? true,
1011
+ viewport: opts.viewport,
1012
+ userAgent: opts.userAgent
1013
+ });
1014
+ page = await getPage(browser, {
1015
+ viewport: opts.viewport,
1016
+ userAgent: opts.userAgent
1017
+ });
1018
+ }
1019
+ const session = createSession({
1020
+ engine: resolvedEngine,
1021
+ projectId: opts.projectId,
1022
+ agentId: opts.agentId,
1023
+ startUrl: opts.startUrl
1024
+ });
1025
+ handles.set(session.id, { browser, page, engine: resolvedEngine });
1026
+ if (opts.startUrl) {
1027
+ try {
1028
+ await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
1029
+ } catch (err) {}
1030
+ }
1031
+ return { session, page };
1032
+ }
1033
+ function getSessionPage(sessionId) {
1034
+ const handle = handles.get(sessionId);
1035
+ if (!handle)
1036
+ throw new SessionNotFoundError(sessionId);
1037
+ return handle.page;
1038
+ }
1039
+ function getSessionBrowser(sessionId) {
1040
+ const handle = handles.get(sessionId);
1041
+ if (!handle)
1042
+ throw new SessionNotFoundError(sessionId);
1043
+ return handle.browser;
1044
+ }
1045
+ function getSessionEngine(sessionId) {
1046
+ const handle = handles.get(sessionId);
1047
+ if (!handle)
1048
+ throw new SessionNotFoundError(sessionId);
1049
+ return handle.engine;
1050
+ }
1051
+ function hasActiveHandle(sessionId) {
1052
+ return handles.has(sessionId);
1053
+ }
1054
+ async function closeSession2(sessionId) {
1055
+ const handle = handles.get(sessionId);
1056
+ if (handle) {
1057
+ try {
1058
+ await handle.page.context().close();
1059
+ } catch {}
1060
+ try {
1061
+ await closeBrowser(handle.browser);
1062
+ } catch {}
1063
+ handles.delete(sessionId);
1064
+ }
1065
+ return closeSession(sessionId);
1066
+ }
1067
+ function listSessions2(filter) {
1068
+ return listSessions(filter);
1069
+ }
1070
+ function getActiveSessions() {
1071
+ return listSessions({ status: "active" });
1072
+ }
1073
+ async function closeAllSessions() {
1074
+ for (const [id] of handles) {
1075
+ await closeSession2(id).catch(() => {});
1076
+ }
1077
+ }
1078
+ // src/lib/actions.ts
1079
+ async function click(page, selector, opts) {
1080
+ try {
1081
+ await page.click(selector, {
1082
+ button: opts?.button ?? "left",
1083
+ clickCount: opts?.clickCount ?? 1,
1084
+ delay: opts?.delay,
1085
+ timeout: opts?.timeout ?? 1e4
1086
+ });
1087
+ } catch (err) {
1088
+ if (err instanceof Error && err.message.includes("not found")) {
1089
+ throw new ElementNotFoundError(selector);
1090
+ }
1091
+ throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
1092
+ }
1093
+ }
1094
+ async function type(page, selector, text, opts) {
1095
+ try {
1096
+ if (opts?.clear) {
1097
+ await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
1098
+ }
1099
+ await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
1100
+ } catch (err) {
1101
+ if (err instanceof Error && err.message.includes("not found")) {
1102
+ throw new ElementNotFoundError(selector);
1103
+ }
1104
+ throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
1105
+ }
1106
+ }
1107
+ async function fill(page, selector, value, timeout = 1e4) {
1108
+ try {
1109
+ await page.fill(selector, value, { timeout });
1110
+ } catch (err) {
1111
+ throw new ElementNotFoundError(selector);
1112
+ }
1113
+ }
1114
+ async function scroll(page, direction = "down", amount = 300) {
1115
+ const x = direction === "left" ? -amount : direction === "right" ? amount : 0;
1116
+ const y = direction === "up" ? -amount : direction === "down" ? amount : 0;
1117
+ await page.evaluate(({ x: x2, y: y2 }) => window.scrollBy(x2, y2), { x, y });
1118
+ }
1119
+ async function scrollTo(page, selector) {
1120
+ try {
1121
+ await page.locator(selector).scrollIntoViewIfNeeded();
1122
+ } catch (err) {
1123
+ throw new ElementNotFoundError(selector);
1124
+ }
1125
+ }
1126
+ async function hover(page, selector, timeout = 1e4) {
1127
+ try {
1128
+ await page.hover(selector, { timeout });
1129
+ } catch (err) {
1130
+ throw new ElementNotFoundError(selector);
1131
+ }
1132
+ }
1133
+ async function selectOption(page, selector, value, timeout = 1e4) {
1134
+ try {
1135
+ return await page.selectOption(selector, value, { timeout });
1136
+ } catch (err) {
1137
+ throw new ElementNotFoundError(selector);
1138
+ }
1139
+ }
1140
+ async function checkBox(page, selector, checked, timeout = 1e4) {
1141
+ try {
1142
+ if (checked) {
1143
+ await page.check(selector, { timeout });
1144
+ } else {
1145
+ await page.uncheck(selector, { timeout });
1146
+ }
1147
+ } catch (err) {
1148
+ throw new ElementNotFoundError(selector);
1149
+ }
1150
+ }
1151
+ async function uploadFile(page, selector, filePaths, timeout = 1e4) {
1152
+ try {
1153
+ await page.setInputFiles(selector, filePaths, { timeout });
1154
+ } catch (err) {
1155
+ throw new ElementNotFoundError(selector);
1156
+ }
1157
+ }
1158
+ async function goBack(page, timeout = 1e4) {
1159
+ try {
1160
+ await page.goBack({ timeout, waitUntil: "domcontentloaded" });
1161
+ } catch (err) {
1162
+ throw new NavigationError("back", err instanceof Error ? err.message : String(err));
1163
+ }
1164
+ }
1165
+ async function goForward(page, timeout = 1e4) {
1166
+ try {
1167
+ await page.goForward({ timeout, waitUntil: "domcontentloaded" });
1168
+ } catch (err) {
1169
+ throw new NavigationError("forward", err instanceof Error ? err.message : String(err));
1170
+ }
1171
+ }
1172
+ async function reload(page, timeout = 1e4) {
1173
+ try {
1174
+ await page.reload({ timeout, waitUntil: "domcontentloaded" });
1175
+ } catch (err) {
1176
+ throw new NavigationError("reload", err instanceof Error ? err.message : String(err));
1177
+ }
1178
+ }
1179
+ async function navigate(page, url, timeout = 30000) {
1180
+ try {
1181
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout });
1182
+ } catch (err) {
1183
+ throw new NavigationError(url, err instanceof Error ? err.message : String(err));
1184
+ }
1185
+ }
1186
+ async function waitForSelector(page, selector, opts) {
1187
+ try {
1188
+ await page.waitForSelector(selector, {
1189
+ state: opts?.state ?? "visible",
1190
+ timeout: opts?.timeout ?? 1e4
1191
+ });
1192
+ } catch (err) {
1193
+ throw new ElementNotFoundError(selector);
1194
+ }
1195
+ }
1196
+ async function waitForNavigation(page, timeout = 30000) {
1197
+ try {
1198
+ await page.waitForLoadState("domcontentloaded", { timeout });
1199
+ } catch (err) {
1200
+ throw new NavigationError("navigation", err instanceof Error ? err.message : String(err));
1201
+ }
1202
+ }
1203
+ async function pressKey(page, key) {
1204
+ await page.keyboard.press(key);
1205
+ }
1206
+ // src/lib/extractor.ts
1207
+ async function getText(page, selector) {
1208
+ if (selector) {
1209
+ const el = await page.$(selector);
1210
+ if (!el)
1211
+ return "";
1212
+ return await el.textContent() ?? "";
1213
+ }
1214
+ return page.evaluate(() => document.body.innerText ?? "");
1215
+ }
1216
+ async function getHTML(page, selector) {
1217
+ if (selector) {
1218
+ const el = await page.$(selector);
1219
+ if (!el)
1220
+ return "";
1221
+ return await el.innerHTML() ?? "";
1222
+ }
1223
+ return page.content();
1224
+ }
1225
+ async function getLinks(page, baseUrl) {
1226
+ return page.evaluate((base) => {
1227
+ return Array.from(document.querySelectorAll("a[href]")).map((a) => {
1228
+ const href = a.href;
1229
+ if (!href)
1230
+ return null;
1231
+ if (href.startsWith("http"))
1232
+ return href;
1233
+ if (base && href.startsWith("/"))
1234
+ return new URL(href, base).href;
1235
+ return null;
1236
+ }).filter((h) => h !== null);
1237
+ }, baseUrl ?? page.url());
1238
+ }
1239
+ async function getTitle(page) {
1240
+ return page.title();
1241
+ }
1242
+ async function getUrl(page) {
1243
+ return page.url();
1244
+ }
1245
+ async function getMetaTags(page) {
1246
+ return page.evaluate(() => {
1247
+ const meta = {};
1248
+ document.querySelectorAll("meta[name], meta[property]").forEach((el) => {
1249
+ const key = el.getAttribute("name") ?? el.getAttribute("property") ?? "";
1250
+ const value = el.getAttribute("content") ?? "";
1251
+ if (key)
1252
+ meta[key] = value;
1253
+ });
1254
+ return meta;
1255
+ });
1256
+ }
1257
+ async function findElements(page, selector) {
1258
+ return page.$$(selector);
1259
+ }
1260
+ async function extractStructured(page, schema) {
1261
+ const result = {};
1262
+ for (const [field, selector] of Object.entries(schema)) {
1263
+ const elements = await page.$$(selector);
1264
+ if (elements.length === 0) {
1265
+ result[field] = "";
1266
+ } else if (elements.length === 1) {
1267
+ result[field] = (await elements[0].textContent())?.trim() ?? "";
1268
+ } else {
1269
+ result[field] = await Promise.all(elements.map(async (el) => (await el.textContent())?.trim() ?? ""));
1270
+ }
1271
+ }
1272
+ return result;
1273
+ }
1274
+ async function extractTable(page, selector) {
1275
+ return page.evaluate((sel) => {
1276
+ const table = document.querySelector(sel);
1277
+ if (!table)
1278
+ return [];
1279
+ const rows = Array.from(table.querySelectorAll("tr"));
1280
+ return rows.map((row) => Array.from(row.querySelectorAll("th, td")).map((cell) => cell.textContent?.trim() ?? ""));
1281
+ }, selector);
1282
+ }
1283
+ async function getAriaSnapshot(page) {
1284
+ try {
1285
+ return await page.ariaSnapshot?.() ?? page.evaluate(() => {
1286
+ function walk(el, indent = 0) {
1287
+ const role = el.getAttribute("role") ?? el.tagName.toLowerCase();
1288
+ const label = el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 50) ?? "";
1289
+ const line = " ".repeat(indent) + `[${role}] ${label}`;
1290
+ const children = Array.from(el.children).map((c) => walk(c, indent + 1)).join(`
1291
+ `);
1292
+ return children ? `${line}
1293
+ ${children}` : line;
1294
+ }
1295
+ return walk(document.body);
1296
+ });
1297
+ } catch {
1298
+ return page.evaluate(() => document.body.innerText?.slice(0, 2000) ?? "");
1299
+ }
1300
+ }
1301
+ async function extract(page, opts = {}) {
1302
+ const result = {};
1303
+ const format = opts.format ?? "text";
1304
+ switch (format) {
1305
+ case "text":
1306
+ result.text = await getText(page, opts.selector);
1307
+ break;
1308
+ case "html":
1309
+ result.html = await getHTML(page, opts.selector);
1310
+ break;
1311
+ case "links":
1312
+ result.links = await getLinks(page);
1313
+ break;
1314
+ case "table":
1315
+ result.table = opts.selector ? await extractTable(page, opts.selector) : [];
1316
+ break;
1317
+ case "structured":
1318
+ if (opts.schema)
1319
+ result.structured = await extractStructured(page, opts.schema);
1320
+ break;
1321
+ }
1322
+ return result;
1323
+ }
1324
+ // src/lib/network.ts
1325
+ function enableNetworkLogging(page, sessionId) {
1326
+ const requestStart = new Map;
1327
+ const onRequest = (req) => {
1328
+ requestStart.set(req.url(), Date.now());
1329
+ };
1330
+ const onResponse = (res) => {
1331
+ const start = requestStart.get(res.url()) ?? Date.now();
1332
+ const duration = Date.now() - start;
1333
+ const req = res.request();
1334
+ try {
1335
+ logRequest({
1336
+ session_id: sessionId,
1337
+ method: req.method(),
1338
+ url: res.url(),
1339
+ status_code: res.status(),
1340
+ request_headers: JSON.stringify(req.headers()),
1341
+ response_headers: JSON.stringify(res.headers()),
1342
+ body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
1343
+ duration_ms: duration,
1344
+ resource_type: req.resourceType()
1345
+ });
1346
+ } catch {}
1347
+ };
1348
+ page.on("request", onRequest);
1349
+ page.on("response", onResponse);
1350
+ return () => {
1351
+ page.off("request", onRequest);
1352
+ page.off("response", onResponse);
1353
+ };
1354
+ }
1355
+ async function addInterceptRule(page, rule) {
1356
+ await page.route(rule.pattern, async (route) => {
1357
+ if (rule.action === "block") {
1358
+ await route.abort();
1359
+ } else if (rule.action === "modify" && rule.response) {
1360
+ await route.fulfill({
1361
+ status: rule.response.status,
1362
+ body: rule.response.body,
1363
+ headers: rule.response.headers
1364
+ });
1365
+ } else {
1366
+ await route.continue();
1367
+ }
1368
+ });
1369
+ }
1370
+ async function clearInterceptRules(page) {
1371
+ await page.unrouteAll();
1372
+ }
1373
+ function startHAR(page) {
1374
+ const entries = [];
1375
+ const requestStart = new Map;
1376
+ const onRequest = (req) => {
1377
+ requestStart.set(req.url() + req.method(), {
1378
+ time: Date.now(),
1379
+ method: req.method(),
1380
+ headers: req.headers(),
1381
+ postData: req.postData() ?? undefined
1382
+ });
1383
+ };
1384
+ const onResponse = async (res) => {
1385
+ const key = res.url() + res.request().method();
1386
+ const start = requestStart.get(key);
1387
+ if (!start)
1388
+ return;
1389
+ const duration = Date.now() - start.time;
1390
+ const entry = {
1391
+ startedDateTime: new Date(start.time).toISOString(),
1392
+ time: duration,
1393
+ request: {
1394
+ method: start.method,
1395
+ url: res.url(),
1396
+ headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
1397
+ postData: start.postData ? { text: start.postData } : undefined
1398
+ },
1399
+ response: {
1400
+ status: res.status(),
1401
+ statusText: res.statusText(),
1402
+ headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
1403
+ content: {
1404
+ size: parseInt(res.headers()["content-length"] ?? "0") || 0,
1405
+ mimeType: res.headers()["content-type"] ?? "application/octet-stream"
1406
+ }
1407
+ },
1408
+ timings: { send: 0, wait: duration, receive: 0 }
1409
+ };
1410
+ entries.push(entry);
1411
+ requestStart.delete(key);
1412
+ };
1413
+ page.on("request", onRequest);
1414
+ page.on("response", onResponse);
1415
+ return {
1416
+ entries,
1417
+ stop: () => {
1418
+ page.off("request", onRequest);
1419
+ page.off("response", onResponse);
1420
+ return {
1421
+ log: {
1422
+ version: "1.2",
1423
+ creator: { name: "@hasna/browser", version: "0.0.1" },
1424
+ entries
1425
+ }
1426
+ };
1427
+ }
1428
+ };
1429
+ }
1430
+ // src/lib/performance.ts
1431
+ async function getPerformanceMetrics(page) {
1432
+ const navTiming = await page.evaluate(() => {
1433
+ const t = performance.timing;
1434
+ const nav = performance.getEntriesByType("navigation")[0];
1435
+ return {
1436
+ ttfb: nav ? nav.responseStart - nav.requestStart : t.responseStart - t.requestStart,
1437
+ domInteractive: nav ? nav.domInteractive : t.domInteractive - t.navigationStart,
1438
+ domComplete: nav ? nav.domComplete : t.domComplete - t.navigationStart,
1439
+ loadEvent: nav ? nav.loadEventEnd : t.loadEventEnd - t.navigationStart
1440
+ };
1441
+ });
1442
+ const paintEntries = await page.evaluate(() => {
1443
+ const entries = performance.getEntriesByType("paint");
1444
+ const fcp = entries.find((e) => e.name === "first-contentful-paint");
1445
+ return { fcp: fcp?.startTime };
1446
+ });
1447
+ let heapMetrics = {};
1448
+ try {
1449
+ const cdp = await CDPClient.fromPage(page);
1450
+ const cdpMetrics = await cdp.getPerformanceMetrics();
1451
+ heapMetrics = {
1452
+ js_heap_size_used: cdpMetrics.js_heap_size_used,
1453
+ js_heap_size_total: cdpMetrics.js_heap_size_total
1454
+ };
1455
+ } catch {}
1456
+ return {
1457
+ fcp: paintEntries.fcp,
1458
+ ttfb: navTiming.ttfb,
1459
+ dom_interactive: navTiming.domInteractive,
1460
+ dom_complete: navTiming.domComplete,
1461
+ load_event: navTiming.loadEvent,
1462
+ ...heapMetrics
1463
+ };
1464
+ }
1465
+ async function getMemoryUsage(page) {
1466
+ try {
1467
+ const cdp = await CDPClient.fromPage(page);
1468
+ const metrics = await cdp.getPerformanceMetrics();
1469
+ return {
1470
+ used: metrics.js_heap_size_used ?? 0,
1471
+ total: metrics.js_heap_size_total ?? 0
1472
+ };
1473
+ } catch {
1474
+ return null;
1475
+ }
1476
+ }
1477
+ async function getTimingEntries(page) {
1478
+ return page.evaluate(() => performance.getEntriesByType("resource").map((e) => e.toJSON()));
1479
+ }
1480
+ async function startCoverage(page) {
1481
+ const [jsCoverage, cssCoverage] = await Promise.all([
1482
+ page.coverage.startJSCoverage(),
1483
+ page.coverage.startCSSCoverage()
1484
+ ]);
1485
+ return {
1486
+ stop: async () => {
1487
+ const [jsEntries, cssEntries] = await Promise.all([
1488
+ page.coverage.stopJSCoverage(),
1489
+ page.coverage.stopCSSCoverage()
1490
+ ]);
1491
+ const jsFlat = jsEntries.map((e) => {
1492
+ const text = e.source ?? "";
1493
+ const ranges = e.functions.flatMap((f) => f.ranges.filter((r) => r.count > 0).map((r) => ({ start: r.startOffset, end: r.endOffset })));
1494
+ return { url: e.url, text, ranges };
1495
+ });
1496
+ const cssFlat = cssEntries.map((e) => ({
1497
+ url: e.url,
1498
+ text: e.text ?? "",
1499
+ ranges: e.ranges.map((r) => ({ start: r.start, end: r.end }))
1500
+ }));
1501
+ const totalJs = jsFlat.reduce((acc, e) => acc + e.text.length, 0);
1502
+ const usedJs = jsFlat.reduce((acc, e) => acc + e.ranges.reduce((s, r) => s + (r.end - r.start), 0), 0);
1503
+ const totalCss = cssFlat.reduce((acc, e) => acc + e.text.length, 0);
1504
+ const usedCss = cssFlat.reduce((acc, e) => acc + e.ranges.reduce((s, r) => s + (r.end - r.start), 0), 0);
1505
+ const totalBytes = totalJs + totalCss;
1506
+ const usedBytes = usedJs + usedCss;
1507
+ return {
1508
+ js: jsFlat,
1509
+ css: cssFlat,
1510
+ totalBytes,
1511
+ usedBytes,
1512
+ unusedPercent: totalBytes > 0 ? (totalBytes - usedBytes) / totalBytes * 100 : 0
1513
+ };
1514
+ }
1515
+ };
1516
+ }
1517
+ // src/lib/console.ts
1518
+ function enableConsoleCapture(page, sessionId) {
1519
+ const onConsole = (msg) => {
1520
+ const levelMap = {
1521
+ log: "log",
1522
+ warn: "warn",
1523
+ error: "error",
1524
+ debug: "debug",
1525
+ info: "info",
1526
+ warning: "warn"
1527
+ };
1528
+ const level = levelMap[msg.type()] ?? "log";
1529
+ const location = msg.location();
1530
+ try {
1531
+ logConsoleMessage({
1532
+ session_id: sessionId,
1533
+ level,
1534
+ message: msg.text(),
1535
+ source: location.url || undefined,
1536
+ line_number: location.lineNumber || undefined
1537
+ });
1538
+ } catch {}
1539
+ };
1540
+ page.on("console", onConsole);
1541
+ return () => page.off("console", onConsole);
1542
+ }
1543
+ async function capturePageErrors(page, sessionId) {
1544
+ const onError = (err) => {
1545
+ try {
1546
+ logConsoleMessage({
1547
+ session_id: sessionId,
1548
+ level: "error",
1549
+ message: `[Page Error] ${err.message}`,
1550
+ source: err.stack?.split(`
1551
+ `)[1]?.trim()
1552
+ });
1553
+ } catch {}
1554
+ };
1555
+ page.on("pageerror", onError);
1556
+ return () => page.off("pageerror", onError);
1557
+ }
1558
+ // src/lib/screenshot.ts
1559
+ import { join as join2 } from "path";
1560
+ import { mkdirSync as mkdirSync2 } from "fs";
1561
+ import { homedir as homedir2 } from "os";
1562
+ var DATA_DIR = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
1563
+ function getScreenshotDir(projectId) {
1564
+ const base = join2(DATA_DIR, "screenshots");
1565
+ const date = new Date().toISOString().split("T")[0];
1566
+ const dir = projectId ? join2(base, projectId, date) : join2(base, date);
1567
+ mkdirSync2(dir, { recursive: true });
1568
+ return dir;
1569
+ }
1570
+ async function takeScreenshot(page, opts) {
1571
+ try {
1572
+ const dir = getScreenshotDir(opts?.projectId);
1573
+ const timestamp = Date.now();
1574
+ const format = opts?.format ?? "png";
1575
+ const screenshotPath = opts?.path ?? join2(dir, `${timestamp}.${format}`);
1576
+ const screenshotOpts = {
1577
+ path: screenshotPath,
1578
+ fullPage: opts?.fullPage ?? false,
1579
+ type: format === "webp" ? "jpeg" : format,
1580
+ quality: format === "jpeg" || format === "webp" ? opts?.quality ?? 90 : undefined
1581
+ };
1582
+ let buffer;
1583
+ if (opts?.selector) {
1584
+ const el = await page.$(opts.selector);
1585
+ if (!el)
1586
+ throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
1587
+ buffer = await el.screenshot({ ...screenshotOpts });
1588
+ } else {
1589
+ buffer = await page.screenshot(screenshotOpts);
1590
+ }
1591
+ const viewportSize = page.viewportSize() ?? { width: 1280, height: 720 };
1592
+ return {
1593
+ path: screenshotPath,
1594
+ base64: buffer.toString("base64"),
1595
+ width: viewportSize.width,
1596
+ height: viewportSize.height,
1597
+ size_bytes: buffer.length
1598
+ };
1599
+ } catch (err) {
1600
+ if (err instanceof BrowserError)
1601
+ throw err;
1602
+ throw new BrowserError(`Screenshot failed: ${err instanceof Error ? err.message : String(err)}`, "SCREENSHOT_FAILED");
1603
+ }
1604
+ }
1605
+ async function generatePDF(page, opts) {
1606
+ try {
1607
+ const base = join2(DATA_DIR, "pdfs");
1608
+ const date = new Date().toISOString().split("T")[0];
1609
+ const dir = opts?.projectId ? join2(base, opts.projectId, date) : join2(base, date);
1610
+ mkdirSync2(dir, { recursive: true });
1611
+ const timestamp = Date.now();
1612
+ const pdfPath = opts?.path ?? join2(dir, `${timestamp}.pdf`);
1613
+ const buffer = await page.pdf({
1614
+ path: pdfPath,
1615
+ format: opts?.format ?? "A4",
1616
+ landscape: opts?.landscape ?? false,
1617
+ margin: opts?.margin,
1618
+ printBackground: opts?.printBackground ?? true
1619
+ });
1620
+ return {
1621
+ path: pdfPath,
1622
+ base64: buffer.toString("base64"),
1623
+ size_bytes: buffer.length
1624
+ };
1625
+ } catch (err) {
1626
+ throw new BrowserError(`PDF generation failed: ${err instanceof Error ? err.message : String(err)}`, "PDF_FAILED");
1627
+ }
1628
+ }
1629
+ // src/lib/storage.ts
1630
+ async function getCookies(page, filter) {
1631
+ const cookies = await page.context().cookies();
1632
+ if (filter?.name)
1633
+ return cookies.filter((c) => c.name === filter.name);
1634
+ if (filter?.domain)
1635
+ return cookies.filter((c) => c.domain?.includes(filter.domain));
1636
+ return cookies;
1637
+ }
1638
+ async function setCookie(page, cookie) {
1639
+ await page.context().addCookies([cookie]);
1640
+ }
1641
+ async function clearCookies(page, filter) {
1642
+ if (!filter) {
1643
+ await page.context().clearCookies();
1644
+ return;
1645
+ }
1646
+ const existing = await getCookies(page, filter);
1647
+ for (const cookie of existing) {
1648
+ await page.context().addCookies([{ ...cookie, expires: 0 }]);
1649
+ }
1650
+ }
1651
+ async function getLocalStorage(page, key) {
1652
+ return page.evaluate((k) => {
1653
+ if (k)
1654
+ return localStorage.getItem(k);
1655
+ const result = {};
1656
+ for (let i = 0;i < localStorage.length; i++) {
1657
+ const itemKey = localStorage.key(i);
1658
+ result[itemKey] = localStorage.getItem(itemKey);
1659
+ }
1660
+ return result;
1661
+ }, key ?? null);
1662
+ }
1663
+ async function setLocalStorage(page, key, value) {
1664
+ await page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]);
1665
+ }
1666
+ async function clearLocalStorage(page) {
1667
+ await page.evaluate(() => localStorage.clear());
1668
+ }
1669
+ async function getSessionStorage(page, key) {
1670
+ return page.evaluate((k) => {
1671
+ if (k)
1672
+ return sessionStorage.getItem(k);
1673
+ const result = {};
1674
+ for (let i = 0;i < sessionStorage.length; i++) {
1675
+ const itemKey = sessionStorage.key(i);
1676
+ result[itemKey] = sessionStorage.getItem(itemKey);
1677
+ }
1678
+ return result;
1679
+ }, key ?? null);
1680
+ }
1681
+ async function setSessionStorage(page, key, value) {
1682
+ await page.evaluate(([k, v]) => sessionStorage.setItem(k, v), [key, value]);
1683
+ }
1684
+ async function clearSessionStorage(page) {
1685
+ await page.evaluate(() => sessionStorage.clear());
1686
+ }
1687
+ async function getIndexedDB(page, dbName, storeName) {
1688
+ return page.evaluate(([db, store]) => new Promise((resolve, reject) => {
1689
+ const req = indexedDB.open(db);
1690
+ req.onsuccess = () => {
1691
+ const database = req.result;
1692
+ const tx = database.transaction(store, "readonly");
1693
+ const objectStore = tx.objectStore(store);
1694
+ const all = objectStore.getAll();
1695
+ all.onsuccess = () => resolve(all.result);
1696
+ all.onerror = () => reject(all.error);
1697
+ };
1698
+ req.onerror = () => reject(req.error);
1699
+ }), [dbName, storeName]);
1700
+ }
1701
+ // src/lib/recorder.ts
1702
+ var activeRecordings = new Map;
1703
+ function startRecording(sessionId, name, startUrl) {
1704
+ const steps = [];
1705
+ const recording = createRecording({ name, start_url: startUrl, steps });
1706
+ activeRecordings.set(recording.id, {
1707
+ id: recording.id,
1708
+ steps,
1709
+ cleanup: () => {}
1710
+ });
1711
+ return recording;
1712
+ }
1713
+ function attachPageListeners(page, recordingId) {
1714
+ const active = activeRecordings.get(recordingId);
1715
+ if (!active)
1716
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
1717
+ const onFrameNav = () => {
1718
+ active.steps.push({
1719
+ type: "navigate",
1720
+ url: page.url(),
1721
+ timestamp: Date.now()
1722
+ });
1723
+ };
1724
+ page.on("framenavigated", onFrameNav);
1725
+ const cleanup = () => {
1726
+ page.off("framenavigated", onFrameNav);
1727
+ };
1728
+ active.cleanup = cleanup;
1729
+ }
1730
+ function recordStep(recordingId, step) {
1731
+ const active = activeRecordings.get(recordingId);
1732
+ if (!active)
1733
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
1734
+ active.steps.push({ ...step, timestamp: Date.now() });
1735
+ }
1736
+ function stopRecording(recordingId) {
1737
+ const active = activeRecordings.get(recordingId);
1738
+ if (!active)
1739
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
1740
+ active.cleanup();
1741
+ activeRecordings.delete(recordingId);
1742
+ return updateRecording(recordingId, { steps: active.steps });
1743
+ }
1744
+ async function replayRecording(recordingId, page) {
1745
+ const recording = getRecording(recordingId);
1746
+ const startTime = Date.now();
1747
+ let executed = 0;
1748
+ let failed = 0;
1749
+ const errors = [];
1750
+ for (const step of recording.steps) {
1751
+ try {
1752
+ switch (step.type) {
1753
+ case "navigate":
1754
+ if (step.url)
1755
+ await navigate(page, step.url);
1756
+ break;
1757
+ case "click":
1758
+ if (step.selector)
1759
+ await click(page, step.selector);
1760
+ break;
1761
+ case "type":
1762
+ if (step.selector && step.value)
1763
+ await type(page, step.selector, step.value);
1764
+ break;
1765
+ case "scroll":
1766
+ await scroll(page, "down");
1767
+ break;
1768
+ case "hover":
1769
+ if (step.selector) {
1770
+ const el = await page.$(step.selector);
1771
+ if (el)
1772
+ await el.hover();
1773
+ }
1774
+ break;
1775
+ case "evaluate":
1776
+ if (step.value)
1777
+ await page.evaluate(step.value);
1778
+ break;
1779
+ case "wait":
1780
+ if (step.selector) {
1781
+ await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
1782
+ }
1783
+ break;
1784
+ }
1785
+ executed++;
1786
+ } catch (err) {
1787
+ failed++;
1788
+ errors.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
1789
+ }
1790
+ await new Promise((r) => setTimeout(r, 100));
1791
+ }
1792
+ return {
1793
+ recording_id: recordingId,
1794
+ success: failed === 0,
1795
+ steps_executed: executed,
1796
+ steps_failed: failed,
1797
+ errors,
1798
+ duration_ms: Date.now() - startTime
1799
+ };
1800
+ }
1801
+ function exportRecording(recordingId, format = "json") {
1802
+ const recording = getRecording(recordingId);
1803
+ if (format === "json") {
1804
+ return JSON.stringify(recording, null, 2);
1805
+ }
1806
+ if (format === "playwright") {
1807
+ const lines2 = [
1808
+ `import { test, expect } from '@playwright/test';`,
1809
+ ``,
1810
+ `test('${recording.name}', async ({ page }) => {`
1811
+ ];
1812
+ for (const step of recording.steps) {
1813
+ switch (step.type) {
1814
+ case "navigate":
1815
+ lines2.push(` await page.goto('${step.url}');`);
1816
+ break;
1817
+ case "click":
1818
+ lines2.push(` await page.click('${step.selector}');`);
1819
+ break;
1820
+ case "type":
1821
+ lines2.push(` await page.type('${step.selector}', '${step.value}');`);
1822
+ break;
1823
+ case "scroll":
1824
+ lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
1825
+ break;
1826
+ case "evaluate":
1827
+ lines2.push(` await page.evaluate(${step.value});`);
1828
+ break;
1829
+ }
1830
+ }
1831
+ lines2.push(`});`);
1832
+ return lines2.join(`
1833
+ `);
1834
+ }
1835
+ const lines = [
1836
+ `const puppeteer = require('puppeteer');`,
1837
+ ``,
1838
+ `(async () => {`,
1839
+ ` const browser = await puppeteer.launch();`,
1840
+ ` const page = await browser.newPage();`
1841
+ ];
1842
+ for (const step of recording.steps) {
1843
+ switch (step.type) {
1844
+ case "navigate":
1845
+ lines.push(` await page.goto('${step.url}');`);
1846
+ break;
1847
+ case "click":
1848
+ lines.push(` await page.click('${step.selector}');`);
1849
+ break;
1850
+ case "type":
1851
+ lines.push(` await page.type('${step.selector}', '${step.value}');`);
1852
+ break;
1853
+ }
1854
+ }
1855
+ lines.push(` await browser.close();`, `})();`);
1856
+ return lines.join(`
1857
+ `);
1858
+ }
1859
+ // src/lib/crawler.ts
1860
+ async function crawl(startUrl, opts = {}) {
1861
+ const maxDepth = opts.maxDepth ?? 2;
1862
+ const maxPages = opts.maxPages ?? 50;
1863
+ const sameDomain = opts.sameDomain ?? true;
1864
+ const engine = selectEngine("extract_links" /* EXTRACT_LINKS */, opts.engine);
1865
+ const startDomain = new URL(startUrl).hostname;
1866
+ const visited = new Set;
1867
+ const pages = [];
1868
+ const errors = [];
1869
+ let browser;
1870
+ if (engine === "lightpanda") {
1871
+ browser = await connectLightpanda();
1872
+ } else {
1873
+ browser = await launchPlaywright({ headless: true });
1874
+ }
1875
+ const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
1876
+ async function crawlPage(url, depth) {
1877
+ if (depth > maxDepth || pages.length >= maxPages || visited.has(url))
1878
+ return;
1879
+ if (sameDomain && new URL(url).hostname !== startDomain)
1880
+ return;
1881
+ if (opts.filter && !opts.filter(url))
1882
+ return;
1883
+ visited.add(url);
1884
+ const page = await context.newPage();
1885
+ const crawled = { url, depth, links: [], error: undefined };
1886
+ try {
1887
+ const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
1888
+ crawled.title = await page.title();
1889
+ crawled.status_code = response?.status();
1890
+ crawled.links = await getLinks(page, url);
1891
+ pages.push(crawled);
1892
+ await page.close();
1893
+ for (const link of crawled.links) {
1894
+ if (pages.length >= maxPages)
1895
+ break;
1896
+ await crawlPage(link, depth + 1);
1897
+ }
1898
+ } catch (err) {
1899
+ crawled.error = err instanceof Error ? err.message : String(err);
1900
+ errors.push(`${url}: ${crawled.error}`);
1901
+ pages.push(crawled);
1902
+ await page.close().catch(() => {});
1903
+ }
1904
+ }
1905
+ try {
1906
+ await crawlPage(startUrl, 0);
1907
+ } finally {
1908
+ await browser.close().catch(() => {});
1909
+ }
1910
+ const result = createCrawlResult({
1911
+ project_id: opts.projectId,
1912
+ start_url: startUrl,
1913
+ depth: maxDepth,
1914
+ pages,
1915
+ errors
1916
+ });
1917
+ return result;
1918
+ }
1919
+ // src/lib/agents.ts
1920
+ function registerAgent2(name, opts = {}) {
1921
+ return registerAgent(name, opts);
1922
+ }
1923
+ function heartbeat2(agentId) {
1924
+ heartbeat(agentId);
1925
+ }
1926
+ function isAgentStale(agent, thresholdMs = 5 * 60 * 1000) {
1927
+ const lastSeen = new Date(agent.last_seen).getTime();
1928
+ return Date.now() - lastSeen > thresholdMs;
1929
+ }
1930
+ function getActiveAgents(thresholdMs = 5 * 60 * 1000) {
1931
+ return listAgents().filter((a) => !isAgentStale(a, thresholdMs));
1932
+ }
1933
+ export {
1934
+ waitForSelector,
1935
+ waitForNavigation,
1936
+ uploadFile,
1937
+ updateSessionStatus,
1938
+ updateRecording,
1939
+ updateProject,
1940
+ updateAgent,
1941
+ type,
1942
+ takeScreenshot,
1943
+ stopRecording,
1944
+ stopLightpanda,
1945
+ startRecording,
1946
+ startHAR,
1947
+ startCoverage,
1948
+ setSessionStorage,
1949
+ setLocalStorage,
1950
+ setCookie,
1951
+ selectOption,
1952
+ selectEngine,
1953
+ scrollTo,
1954
+ scroll,
1955
+ resetDatabase,
1956
+ replayRecording,
1957
+ reload,
1958
+ registerAgent2 as registerAgent,
1959
+ recordStep,
1960
+ recordHeartbeat,
1961
+ pressKey,
1962
+ navigate,
1963
+ logRequest,
1964
+ logConsoleMessage,
1965
+ listSnapshots,
1966
+ listSessions2 as listSessions,
1967
+ listRecordings,
1968
+ listProjects,
1969
+ listHeartbeats,
1970
+ listCrawlResults,
1971
+ listAgents,
1972
+ launchPlaywright,
1973
+ launchLightpanda,
1974
+ isLightpandaAvailable,
1975
+ isEngineAvailable,
1976
+ isAgentStale,
1977
+ inferUseCase,
1978
+ hover,
1979
+ heartbeat2 as heartbeat,
1980
+ hasActiveHandle,
1981
+ goForward,
1982
+ goBack,
1983
+ getUrl,
1984
+ getTitle,
1985
+ getTimingEntries,
1986
+ getText,
1987
+ getSnapshot,
1988
+ getSessionStorage,
1989
+ getSessionPage,
1990
+ getSessionEngine,
1991
+ getSessionBrowser,
1992
+ getSession,
1993
+ getRecording,
1994
+ getProjectByName,
1995
+ getProject,
1996
+ getPerformanceMetrics,
1997
+ getPage,
1998
+ getNetworkRequest,
1999
+ getNetworkLog,
2000
+ getMetaTags,
2001
+ getMemoryUsage,
2002
+ getLocalStorage,
2003
+ getLinks,
2004
+ getLightpandaBinaryPath,
2005
+ getLastHeartbeat,
2006
+ getIndexedDB,
2007
+ getHTML,
2008
+ getDatabase,
2009
+ getDataDir,
2010
+ getCrawlResult,
2011
+ getCookies,
2012
+ getConsoleMessage,
2013
+ getConsoleLog,
2014
+ getAriaSnapshot,
2015
+ getAgentByName,
2016
+ getAgent,
2017
+ getActiveSessions,
2018
+ getActiveAgents,
2019
+ generatePDF,
2020
+ findElements,
2021
+ fill,
2022
+ extractTable,
2023
+ extractStructured,
2024
+ extract,
2025
+ exportRecording,
2026
+ ensureProject,
2027
+ enableNetworkLogging,
2028
+ enableConsoleCapture,
2029
+ deleteSnapshotsBySession,
2030
+ deleteSnapshot,
2031
+ deleteSession,
2032
+ deleteRecording,
2033
+ deleteProject,
2034
+ deleteNetworkRequest,
2035
+ deleteCrawlResult,
2036
+ deleteAgent,
2037
+ registerAgent as dbRegisterAgent,
2038
+ listSessions as dbListSessions,
2039
+ listRecordings as dbListRecordings,
2040
+ listAgents as dbListAgents,
2041
+ heartbeat as dbHeartbeat,
2042
+ createSession as dbCreateSession,
2043
+ closeSession as dbCloseSession,
2044
+ createSnapshot,
2045
+ createSession2 as createSession,
2046
+ createRecording,
2047
+ createProject,
2048
+ createCrawlResult,
2049
+ crawl,
2050
+ connectLightpanda,
2051
+ closeSession2 as closeSession,
2052
+ closePage,
2053
+ closeBrowser,
2054
+ closeAllSessions,
2055
+ click,
2056
+ clearSessionStorage,
2057
+ clearNetworkLog,
2058
+ clearLocalStorage,
2059
+ clearInterceptRules,
2060
+ clearCookies,
2061
+ clearConsoleLog,
2062
+ cleanStaleAgents,
2063
+ cleanOldHeartbeats,
2064
+ checkBox,
2065
+ capturePageErrors,
2066
+ attachPageListeners,
2067
+ addInterceptRule,
2068
+ UseCase,
2069
+ SessionNotFoundError,
2070
+ RecordingNotFoundError,
2071
+ ProjectNotFoundError,
2072
+ NavigationError,
2073
+ LightpandaPage,
2074
+ EngineNotAvailableError,
2075
+ ElementNotFoundError,
2076
+ CDPClient,
2077
+ BrowserPool,
2078
+ BrowserError,
2079
+ AgentNotFoundError
2080
+ };