@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
@@ -0,0 +1,1580 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
13
+
14
+ // src/types/index.ts
15
+ var BrowserError, SessionNotFoundError, EngineNotAvailableError, NavigationError, ElementNotFoundError, RecordingNotFoundError, AgentNotFoundError, ProjectNotFoundError;
16
+ var init_types = __esm(() => {
17
+ BrowserError = class BrowserError extends Error {
18
+ code;
19
+ retryable;
20
+ constructor(message, code = "BROWSER_ERROR", retryable = false) {
21
+ super(message);
22
+ this.code = code;
23
+ this.retryable = retryable;
24
+ this.name = "BrowserError";
25
+ }
26
+ };
27
+ SessionNotFoundError = class SessionNotFoundError extends BrowserError {
28
+ constructor(id) {
29
+ super(`Session not found: ${id}`, "SESSION_NOT_FOUND", false);
30
+ this.name = "SessionNotFoundError";
31
+ }
32
+ };
33
+ EngineNotAvailableError = class EngineNotAvailableError extends BrowserError {
34
+ constructor(engine, reason) {
35
+ super(`Engine '${engine}' is not available${reason ? `: ${reason}` : ""}`, "ENGINE_NOT_AVAILABLE", false);
36
+ this.name = "EngineNotAvailableError";
37
+ }
38
+ };
39
+ NavigationError = class NavigationError extends BrowserError {
40
+ constructor(url, reason) {
41
+ super(`Navigation to '${url}' failed${reason ? `: ${reason}` : ""}`, "NAVIGATION_ERROR", true);
42
+ this.name = "NavigationError";
43
+ }
44
+ };
45
+ ElementNotFoundError = class ElementNotFoundError extends BrowserError {
46
+ constructor(selector) {
47
+ super(`Element not found: ${selector}`, "ELEMENT_NOT_FOUND", false);
48
+ this.name = "ElementNotFoundError";
49
+ }
50
+ };
51
+ RecordingNotFoundError = class RecordingNotFoundError extends BrowserError {
52
+ constructor(id) {
53
+ super(`Recording not found: ${id}`, "RECORDING_NOT_FOUND", false);
54
+ this.name = "RecordingNotFoundError";
55
+ }
56
+ };
57
+ AgentNotFoundError = class AgentNotFoundError extends BrowserError {
58
+ constructor(id) {
59
+ super(`Agent not found: ${id}`, "AGENT_NOT_FOUND", false);
60
+ this.name = "AgentNotFoundError";
61
+ }
62
+ };
63
+ ProjectNotFoundError = class ProjectNotFoundError extends BrowserError {
64
+ constructor(id) {
65
+ super(`Project not found: ${id}`, "PROJECT_NOT_FOUND", false);
66
+ this.name = "ProjectNotFoundError";
67
+ }
68
+ };
69
+ });
70
+
71
+ // src/db/schema.ts
72
+ import { Database } from "bun:sqlite";
73
+ import { join } from "path";
74
+ import { mkdirSync } from "fs";
75
+ import { homedir } from "os";
76
+ function getDataDir() {
77
+ return process.env["BROWSER_DATA_DIR"] ?? join(homedir(), ".browser");
78
+ }
79
+ function getDatabase(path) {
80
+ const resolvedPath = path ?? process.env["BROWSER_DB_PATH"] ?? join(getDataDir(), "browser.db");
81
+ if (_db && _dbPath === resolvedPath)
82
+ return _db;
83
+ if (_db) {
84
+ try {
85
+ _db.close();
86
+ } catch {}
87
+ _db = null;
88
+ }
89
+ mkdirSync(join(resolvedPath, ".."), { recursive: true });
90
+ _db = new Database(resolvedPath);
91
+ _dbPath = resolvedPath;
92
+ _db.exec("PRAGMA journal_mode=WAL;");
93
+ _db.exec("PRAGMA foreign_keys=ON;");
94
+ runMigrations(_db);
95
+ return _db;
96
+ }
97
+ function runMigrations(db) {
98
+ db.exec(`
99
+ CREATE TABLE IF NOT EXISTS schema_migrations (
100
+ version INTEGER PRIMARY KEY,
101
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
102
+ );
103
+ `);
104
+ const applied = new Set(db.query("SELECT version FROM schema_migrations").all().map((r) => r.version));
105
+ const migrations = [
106
+ {
107
+ version: 1,
108
+ sql: `
109
+ CREATE TABLE IF NOT EXISTS projects (
110
+ id TEXT PRIMARY KEY,
111
+ name TEXT NOT NULL UNIQUE,
112
+ path TEXT NOT NULL,
113
+ description TEXT,
114
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
115
+ );
116
+
117
+ CREATE TABLE IF NOT EXISTS agents (
118
+ id TEXT PRIMARY KEY,
119
+ name TEXT NOT NULL,
120
+ description TEXT,
121
+ session_id TEXT,
122
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
123
+ working_dir TEXT,
124
+ last_seen TEXT NOT NULL DEFAULT (datetime('now')),
125
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
126
+ );
127
+
128
+ CREATE TABLE IF NOT EXISTS heartbeats (
129
+ id TEXT PRIMARY KEY,
130
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
131
+ session_id TEXT,
132
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS sessions (
136
+ id TEXT PRIMARY KEY,
137
+ engine TEXT NOT NULL,
138
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
139
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
140
+ start_url TEXT,
141
+ status TEXT NOT NULL DEFAULT 'active',
142
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
143
+ closed_at TEXT
144
+ );
145
+
146
+ CREATE TABLE IF NOT EXISTS snapshots (
147
+ id TEXT PRIMARY KEY,
148
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
149
+ url TEXT NOT NULL,
150
+ title TEXT,
151
+ html TEXT,
152
+ screenshot_path TEXT,
153
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
154
+ );
155
+
156
+ CREATE TABLE IF NOT EXISTS network_log (
157
+ id TEXT PRIMARY KEY,
158
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
159
+ method TEXT NOT NULL,
160
+ url TEXT NOT NULL,
161
+ status_code INTEGER,
162
+ request_headers TEXT,
163
+ response_headers TEXT,
164
+ request_body TEXT,
165
+ body_size INTEGER,
166
+ duration_ms INTEGER,
167
+ resource_type TEXT,
168
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
169
+ );
170
+
171
+ CREATE TABLE IF NOT EXISTS console_log (
172
+ id TEXT PRIMARY KEY,
173
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
174
+ level TEXT NOT NULL DEFAULT 'log',
175
+ message TEXT NOT NULL,
176
+ source TEXT,
177
+ line_number INTEGER,
178
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
179
+ );
180
+
181
+ CREATE TABLE IF NOT EXISTS recordings (
182
+ id TEXT PRIMARY KEY,
183
+ name TEXT NOT NULL,
184
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
185
+ start_url TEXT,
186
+ steps TEXT NOT NULL DEFAULT '[]',
187
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
188
+ );
189
+
190
+ CREATE TABLE IF NOT EXISTS crawl_results (
191
+ id TEXT PRIMARY KEY,
192
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
193
+ start_url TEXT NOT NULL,
194
+ depth INTEGER NOT NULL DEFAULT 1,
195
+ pages TEXT NOT NULL DEFAULT '[]',
196
+ links TEXT NOT NULL DEFAULT '[]',
197
+ errors TEXT NOT NULL DEFAULT '[]',
198
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
199
+ );
200
+
201
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
202
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
203
+ CREATE INDEX IF NOT EXISTS idx_snapshots_session ON snapshots(session_id);
204
+ CREATE INDEX IF NOT EXISTS idx_network_log_session ON network_log(session_id);
205
+ CREATE INDEX IF NOT EXISTS idx_console_log_session ON console_log(session_id);
206
+ CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
207
+ CREATE INDEX IF NOT EXISTS idx_heartbeats_agent ON heartbeats(agent_id);
208
+ CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id);
209
+ CREATE INDEX IF NOT EXISTS idx_crawl_results_project ON crawl_results(project_id);
210
+ `
211
+ }
212
+ ];
213
+ for (const m of migrations) {
214
+ if (!applied.has(m.version)) {
215
+ db.transaction(() => {
216
+ db.exec(m.sql);
217
+ db.prepare("INSERT INTO schema_migrations (version) VALUES (?)").run(m.version);
218
+ })();
219
+ }
220
+ }
221
+ }
222
+ var _db = null, _dbPath = null;
223
+ var init_schema = () => {};
224
+
225
+ // src/db/recordings.ts
226
+ var exports_recordings = {};
227
+ __export(exports_recordings, {
228
+ updateRecording: () => updateRecording,
229
+ listRecordings: () => listRecordings,
230
+ getRecording: () => getRecording,
231
+ deleteRecording: () => deleteRecording,
232
+ createRecording: () => createRecording
233
+ });
234
+ import { randomUUID as randomUUID5 } from "crypto";
235
+ function deserialize2(row) {
236
+ return {
237
+ ...row,
238
+ project_id: row.project_id ?? undefined,
239
+ start_url: row.start_url ?? undefined,
240
+ steps: JSON.parse(row.steps)
241
+ };
242
+ }
243
+ function createRecording(data) {
244
+ const db = getDatabase();
245
+ const id = randomUUID5();
246
+ 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 ?? []));
247
+ return getRecording(id);
248
+ }
249
+ function getRecording(id) {
250
+ const db = getDatabase();
251
+ const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
252
+ if (!row)
253
+ throw new RecordingNotFoundError(id);
254
+ return deserialize2(row);
255
+ }
256
+ function listRecordings(projectId) {
257
+ const db = getDatabase();
258
+ 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();
259
+ return rows.map(deserialize2);
260
+ }
261
+ function updateRecording(id, data) {
262
+ const db = getDatabase();
263
+ const fields = [];
264
+ const values = [];
265
+ if (data.name !== undefined) {
266
+ fields.push("name = ?");
267
+ values.push(data.name);
268
+ }
269
+ if (data.steps !== undefined) {
270
+ fields.push("steps = ?");
271
+ values.push(JSON.stringify(data.steps));
272
+ }
273
+ if (data.start_url !== undefined) {
274
+ fields.push("start_url = ?");
275
+ values.push(data.start_url ?? null);
276
+ }
277
+ if (fields.length === 0)
278
+ return getRecording(id);
279
+ values.push(id);
280
+ db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
281
+ return getRecording(id);
282
+ }
283
+ function deleteRecording(id) {
284
+ const db = getDatabase();
285
+ db.prepare("DELETE FROM recordings WHERE id = ?").run(id);
286
+ }
287
+ var init_recordings = __esm(() => {
288
+ init_schema();
289
+ init_types();
290
+ });
291
+
292
+ // src/db/agents.ts
293
+ var exports_agents = {};
294
+ __export(exports_agents, {
295
+ updateAgent: () => updateAgent,
296
+ registerAgent: () => registerAgent,
297
+ listAgents: () => listAgents,
298
+ heartbeat: () => heartbeat,
299
+ getAgentByName: () => getAgentByName,
300
+ getAgent: () => getAgent,
301
+ deleteAgent: () => deleteAgent,
302
+ cleanStaleAgents: () => cleanStaleAgents
303
+ });
304
+ import { randomUUID as randomUUID6 } from "crypto";
305
+ function registerAgent(name, opts = {}) {
306
+ const db = getDatabase();
307
+ const existing = db.query("SELECT * FROM agents WHERE name = ?").get(name);
308
+ if (existing) {
309
+ 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);
310
+ return getAgentByName(name);
311
+ }
312
+ const id = randomUUID6();
313
+ 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);
314
+ return getAgent(id);
315
+ }
316
+ function heartbeat(agentId) {
317
+ const db = getDatabase();
318
+ const agent = db.query("SELECT * FROM agents WHERE id = ?").get(agentId);
319
+ if (!agent)
320
+ throw new AgentNotFoundError(agentId);
321
+ db.prepare("UPDATE agents SET last_seen = datetime('now') WHERE id = ?").run(agentId);
322
+ db.prepare("INSERT INTO heartbeats (id, agent_id, session_id) VALUES (?, ?, ?)").run(randomUUID6(), agentId, agent.session_id ?? null);
323
+ }
324
+ function getAgent(id) {
325
+ const db = getDatabase();
326
+ const row = db.query("SELECT * FROM agents WHERE id = ?").get(id);
327
+ if (!row)
328
+ throw new AgentNotFoundError(id);
329
+ return row;
330
+ }
331
+ function getAgentByName(name) {
332
+ const db = getDatabase();
333
+ return db.query("SELECT * FROM agents WHERE name = ?").get(name) ?? null;
334
+ }
335
+ function listAgents(projectId) {
336
+ const db = getDatabase();
337
+ if (projectId) {
338
+ return db.query("SELECT * FROM agents WHERE project_id = ? ORDER BY last_seen DESC").all(projectId);
339
+ }
340
+ return db.query("SELECT * FROM agents ORDER BY last_seen DESC").all();
341
+ }
342
+ function updateAgent(id, data) {
343
+ const db = getDatabase();
344
+ const fields = [];
345
+ const values = [];
346
+ if (data.name !== undefined) {
347
+ fields.push("name = ?");
348
+ values.push(data.name ?? null);
349
+ }
350
+ if (data.description !== undefined) {
351
+ fields.push("description = ?");
352
+ values.push(data.description ?? null);
353
+ }
354
+ if (data.session_id !== undefined) {
355
+ fields.push("session_id = ?");
356
+ values.push(data.session_id ?? null);
357
+ }
358
+ if (data.project_id !== undefined) {
359
+ fields.push("project_id = ?");
360
+ values.push(data.project_id ?? null);
361
+ }
362
+ if (data.working_dir !== undefined) {
363
+ fields.push("working_dir = ?");
364
+ values.push(data.working_dir ?? null);
365
+ }
366
+ if (fields.length === 0)
367
+ return getAgent(id);
368
+ values.push(id);
369
+ db.prepare(`UPDATE agents SET ${fields.join(", ")} WHERE id = ?`).run(...values);
370
+ return getAgent(id);
371
+ }
372
+ function deleteAgent(id) {
373
+ const db = getDatabase();
374
+ db.prepare("DELETE FROM agents WHERE id = ?").run(id);
375
+ }
376
+ function cleanStaleAgents(thresholdMs) {
377
+ const db = getDatabase();
378
+ const cutoff = new Date(Date.now() - thresholdMs).toISOString().replace("T", " ").split(".")[0];
379
+ const result = db.prepare("DELETE FROM agents WHERE last_seen < ?").run(cutoff);
380
+ return result.changes;
381
+ }
382
+ var init_agents = __esm(() => {
383
+ init_schema();
384
+ init_types();
385
+ });
386
+
387
+ // src/db/snapshots.ts
388
+ var exports_snapshots = {};
389
+ __export(exports_snapshots, {
390
+ listSnapshots: () => listSnapshots,
391
+ getSnapshot: () => getSnapshot,
392
+ deleteSnapshotsBySession: () => deleteSnapshotsBySession,
393
+ deleteSnapshot: () => deleteSnapshot,
394
+ createSnapshot: () => createSnapshot
395
+ });
396
+ import { randomUUID as randomUUID8 } from "crypto";
397
+ function createSnapshot(data) {
398
+ const db = getDatabase();
399
+ const id = randomUUID8();
400
+ 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);
401
+ return getSnapshot(id);
402
+ }
403
+ function getSnapshot(id) {
404
+ const db = getDatabase();
405
+ return db.query("SELECT * FROM snapshots WHERE id = ?").get(id) ?? null;
406
+ }
407
+ function listSnapshots(sessionId) {
408
+ const db = getDatabase();
409
+ return db.query("SELECT * FROM snapshots WHERE session_id = ? ORDER BY timestamp DESC").all(sessionId);
410
+ }
411
+ function deleteSnapshot(id) {
412
+ const db = getDatabase();
413
+ db.prepare("DELETE FROM snapshots WHERE id = ?").run(id);
414
+ }
415
+ function deleteSnapshotsBySession(sessionId) {
416
+ const db = getDatabase();
417
+ db.prepare("DELETE FROM snapshots WHERE session_id = ?").run(sessionId);
418
+ }
419
+ var init_snapshots = __esm(() => {
420
+ init_schema();
421
+ });
422
+
423
+ // src/server/index.ts
424
+ import { join as join3 } from "path";
425
+ import { existsSync } from "fs";
426
+
427
+ // src/lib/session.ts
428
+ init_types();
429
+ init_types();
430
+
431
+ // src/db/sessions.ts
432
+ init_schema();
433
+ init_types();
434
+ import { randomUUID } from "crypto";
435
+ function createSession(data) {
436
+ const db = getDatabase();
437
+ const id = randomUUID();
438
+ 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);
439
+ return getSession(id);
440
+ }
441
+ function getSession(id) {
442
+ const db = getDatabase();
443
+ const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id);
444
+ if (!row)
445
+ throw new SessionNotFoundError(id);
446
+ return row;
447
+ }
448
+ function listSessions(filter) {
449
+ const db = getDatabase();
450
+ const conditions = [];
451
+ const values = [];
452
+ if (filter?.status) {
453
+ conditions.push("status = ?");
454
+ values.push(filter.status);
455
+ }
456
+ if (filter?.projectId) {
457
+ conditions.push("project_id = ?");
458
+ values.push(filter.projectId);
459
+ }
460
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
461
+ return db.query(`SELECT * FROM sessions ${where} ORDER BY created_at DESC`).all(...values);
462
+ }
463
+ function updateSessionStatus(id, status) {
464
+ const db = getDatabase();
465
+ const closedAt = status === "closed" || status === "error" ? "datetime('now')" : "NULL";
466
+ db.prepare(`UPDATE sessions SET status = ?, closed_at = ${closedAt === "NULL" ? "NULL" : "(datetime('now'))"} WHERE id = ?`).run(status, id);
467
+ return getSession(id);
468
+ }
469
+ function closeSession(id) {
470
+ return updateSessionStatus(id, "closed");
471
+ }
472
+
473
+ // src/engines/playwright.ts
474
+ init_types();
475
+ import { chromium } from "playwright";
476
+ var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
477
+ async function launchPlaywright(options) {
478
+ try {
479
+ return await chromium.launch({
480
+ headless: options?.headless ?? true,
481
+ executablePath: options?.executablePath
482
+ });
483
+ } catch (err) {
484
+ throw new BrowserError(`Failed to launch Playwright browser: ${err instanceof Error ? err.message : String(err)}`, "PLAYWRIGHT_LAUNCH_FAILED", true);
485
+ }
486
+ }
487
+ async function getPage(browser, options) {
488
+ const context = await browser.newContext({
489
+ viewport: options?.viewport ?? DEFAULT_VIEWPORT,
490
+ userAgent: options?.userAgent,
491
+ locale: options?.locale
492
+ });
493
+ return context.newPage();
494
+ }
495
+ async function closeBrowser(browser) {
496
+ try {
497
+ await browser.close();
498
+ } catch {}
499
+ }
500
+
501
+ // src/engines/lightpanda.ts
502
+ init_types();
503
+ import { execSync, spawn } from "child_process";
504
+ import { chromium as chromium2 } from "playwright";
505
+ var DEFAULT_LIGHTPANDA_PORT = 9222;
506
+ var LIGHTPANDA_BINARY = process.env["LIGHTPANDA_BINARY"] ?? "lightpanda";
507
+ function isLightpandaAvailable() {
508
+ try {
509
+ execSync(`which ${LIGHTPANDA_BINARY}`, { stdio: "ignore" });
510
+ return true;
511
+ } catch {
512
+ const paths = [
513
+ "/usr/local/bin/lightpanda",
514
+ "/usr/bin/lightpanda",
515
+ `${process.env["HOME"]}/.browser/bin/lightpanda`
516
+ ];
517
+ return paths.some((p) => {
518
+ try {
519
+ execSync(`test -x ${p}`, { stdio: "ignore" });
520
+ return true;
521
+ } catch {
522
+ return false;
523
+ }
524
+ });
525
+ }
526
+ }
527
+ function getLightpandaBinaryPath() {
528
+ if (process.env["LIGHTPANDA_BINARY"])
529
+ return process.env["LIGHTPANDA_BINARY"];
530
+ const paths = [
531
+ "lightpanda",
532
+ "/usr/local/bin/lightpanda",
533
+ "/usr/bin/lightpanda",
534
+ `${process.env["HOME"]}/.browser/bin/lightpanda`
535
+ ];
536
+ for (const p of paths) {
537
+ try {
538
+ execSync(`which ${p}`, { stdio: "ignore" });
539
+ return p;
540
+ } catch {
541
+ try {
542
+ execSync(`test -x ${p}`, { stdio: "ignore" });
543
+ return p;
544
+ } catch {
545
+ continue;
546
+ }
547
+ }
548
+ }
549
+ throw new EngineNotAvailableError("lightpanda", "binary not found. Run: browser install-browser --engine lightpanda");
550
+ }
551
+ var _lpProcess = null;
552
+ async function launchLightpanda(port) {
553
+ if (_lpProcess)
554
+ return _lpProcess;
555
+ if (!isLightpandaAvailable()) {
556
+ throw new EngineNotAvailableError("lightpanda", "binary not found. Run: browser install-browser --engine lightpanda");
557
+ }
558
+ const usePort = port ?? DEFAULT_LIGHTPANDA_PORT;
559
+ const binary = getLightpandaBinaryPath();
560
+ const proc = spawn(binary, ["--cdp-host", "127.0.0.1", "--cdp-port", String(usePort)], {
561
+ stdio: "ignore",
562
+ detached: false
563
+ });
564
+ await new Promise((resolve, reject) => {
565
+ const timeout = setTimeout(() => reject(new BrowserError("Lightpanda startup timeout", "LIGHTPANDA_TIMEOUT")), 5000);
566
+ const check = setInterval(async () => {
567
+ try {
568
+ const resp = await fetch(`http://127.0.0.1:${usePort}/json/version`);
569
+ if (resp.ok) {
570
+ clearInterval(check);
571
+ clearTimeout(timeout);
572
+ resolve();
573
+ }
574
+ } catch {}
575
+ }, 100);
576
+ proc.on("error", (err) => {
577
+ clearInterval(check);
578
+ clearTimeout(timeout);
579
+ reject(new BrowserError(`Lightpanda failed to start: ${err.message}`, "LIGHTPANDA_ERROR"));
580
+ });
581
+ });
582
+ _lpProcess = {
583
+ process: proc,
584
+ port: usePort,
585
+ wsUrl: `ws://127.0.0.1:${usePort}`
586
+ };
587
+ return _lpProcess;
588
+ }
589
+ async function connectLightpanda(port) {
590
+ const lp = await launchLightpanda(port);
591
+ try {
592
+ const resp = await fetch(`http://127.0.0.1:${lp.port}/json/version`);
593
+ const info = await resp.json();
594
+ const wsUrl = info.webSocketDebuggerUrl ?? `ws://127.0.0.1:${lp.port}`;
595
+ return await chromium2.connectOverCDP(wsUrl);
596
+ } catch (err) {
597
+ throw new BrowserError(`Failed to connect to Lightpanda: ${err instanceof Error ? err.message : String(err)}`, "LIGHTPANDA_CONNECT_FAILED");
598
+ }
599
+ }
600
+
601
+ // src/engines/selector.ts
602
+ init_types();
603
+ var ENGINE_MAP = {
604
+ ["scrape" /* SCRAPE */]: "lightpanda",
605
+ ["extract_links" /* EXTRACT_LINKS */]: "lightpanda",
606
+ ["status_check" /* STATUS_CHECK */]: "lightpanda",
607
+ ["form_fill" /* FORM_FILL */]: "playwright",
608
+ ["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
609
+ ["screenshot" /* SCREENSHOT */]: "playwright",
610
+ ["auth_flow" /* AUTH_FLOW */]: "playwright",
611
+ ["multi_tab" /* MULTI_TAB */]: "playwright",
612
+ ["record_replay" /* RECORD_REPLAY */]: "playwright",
613
+ ["network_monitor" /* NETWORK_MONITOR */]: "cdp",
614
+ ["har_capture" /* HAR_CAPTURE */]: "cdp",
615
+ ["perf_profile" /* PERF_PROFILE */]: "cdp",
616
+ ["script_inject" /* SCRIPT_INJECT */]: "cdp",
617
+ ["coverage" /* COVERAGE */]: "cdp"
618
+ };
619
+ function selectEngine(useCase, explicit) {
620
+ if (explicit && explicit !== "auto")
621
+ return explicit;
622
+ const preferred = ENGINE_MAP[useCase];
623
+ if (preferred === "lightpanda" && !isLightpandaAvailable()) {
624
+ return "playwright";
625
+ }
626
+ return preferred;
627
+ }
628
+
629
+ // src/lib/session.ts
630
+ var handles = new Map;
631
+ async function createSession2(opts = {}) {
632
+ const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
633
+ const resolvedEngine = engine === "auto" ? "playwright" : engine;
634
+ let browser;
635
+ let page;
636
+ if (resolvedEngine === "lightpanda") {
637
+ browser = await connectLightpanda();
638
+ const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
639
+ page = await context.newPage();
640
+ } else {
641
+ browser = await launchPlaywright({
642
+ headless: opts.headless ?? true,
643
+ viewport: opts.viewport,
644
+ userAgent: opts.userAgent
645
+ });
646
+ page = await getPage(browser, {
647
+ viewport: opts.viewport,
648
+ userAgent: opts.userAgent
649
+ });
650
+ }
651
+ const session = createSession({
652
+ engine: resolvedEngine,
653
+ projectId: opts.projectId,
654
+ agentId: opts.agentId,
655
+ startUrl: opts.startUrl
656
+ });
657
+ handles.set(session.id, { browser, page, engine: resolvedEngine });
658
+ if (opts.startUrl) {
659
+ try {
660
+ await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
661
+ } catch (err) {}
662
+ }
663
+ return { session, page };
664
+ }
665
+ function getSessionPage(sessionId) {
666
+ const handle = handles.get(sessionId);
667
+ if (!handle)
668
+ throw new SessionNotFoundError(sessionId);
669
+ return handle.page;
670
+ }
671
+ async function closeSession2(sessionId) {
672
+ const handle = handles.get(sessionId);
673
+ if (handle) {
674
+ try {
675
+ await handle.page.context().close();
676
+ } catch {}
677
+ try {
678
+ await closeBrowser(handle.browser);
679
+ } catch {}
680
+ handles.delete(sessionId);
681
+ }
682
+ return closeSession(sessionId);
683
+ }
684
+ function listSessions2(filter) {
685
+ return listSessions(filter);
686
+ }
687
+
688
+ // src/lib/actions.ts
689
+ init_types();
690
+ async function click(page, selector, opts) {
691
+ try {
692
+ await page.click(selector, {
693
+ button: opts?.button ?? "left",
694
+ clickCount: opts?.clickCount ?? 1,
695
+ delay: opts?.delay,
696
+ timeout: opts?.timeout ?? 1e4
697
+ });
698
+ } catch (err) {
699
+ if (err instanceof Error && err.message.includes("not found")) {
700
+ throw new ElementNotFoundError(selector);
701
+ }
702
+ throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
703
+ }
704
+ }
705
+ async function type(page, selector, text, opts) {
706
+ try {
707
+ if (opts?.clear) {
708
+ await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
709
+ }
710
+ await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
711
+ } catch (err) {
712
+ if (err instanceof Error && err.message.includes("not found")) {
713
+ throw new ElementNotFoundError(selector);
714
+ }
715
+ throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
716
+ }
717
+ }
718
+ async function scroll(page, direction = "down", amount = 300) {
719
+ const x = direction === "left" ? -amount : direction === "right" ? amount : 0;
720
+ const y = direction === "up" ? -amount : direction === "down" ? amount : 0;
721
+ await page.evaluate(({ x: x2, y: y2 }) => window.scrollBy(x2, y2), { x, y });
722
+ }
723
+ async function navigate(page, url, timeout = 30000) {
724
+ try {
725
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout });
726
+ } catch (err) {
727
+ throw new NavigationError(url, err instanceof Error ? err.message : String(err));
728
+ }
729
+ }
730
+
731
+ // src/lib/extractor.ts
732
+ async function getText(page, selector) {
733
+ if (selector) {
734
+ const el = await page.$(selector);
735
+ if (!el)
736
+ return "";
737
+ return await el.textContent() ?? "";
738
+ }
739
+ return page.evaluate(() => document.body.innerText ?? "");
740
+ }
741
+ async function getHTML(page, selector) {
742
+ if (selector) {
743
+ const el = await page.$(selector);
744
+ if (!el)
745
+ return "";
746
+ return await el.innerHTML() ?? "";
747
+ }
748
+ return page.content();
749
+ }
750
+ async function getLinks(page, baseUrl) {
751
+ return page.evaluate((base) => {
752
+ return Array.from(document.querySelectorAll("a[href]")).map((a) => {
753
+ const href = a.href;
754
+ if (!href)
755
+ return null;
756
+ if (href.startsWith("http"))
757
+ return href;
758
+ if (base && href.startsWith("/"))
759
+ return new URL(href, base).href;
760
+ return null;
761
+ }).filter((h) => h !== null);
762
+ }, baseUrl ?? page.url());
763
+ }
764
+ async function extractStructured(page, schema) {
765
+ const result = {};
766
+ for (const [field, selector] of Object.entries(schema)) {
767
+ const elements = await page.$$(selector);
768
+ if (elements.length === 0) {
769
+ result[field] = "";
770
+ } else if (elements.length === 1) {
771
+ result[field] = (await elements[0].textContent())?.trim() ?? "";
772
+ } else {
773
+ result[field] = await Promise.all(elements.map(async (el) => (await el.textContent())?.trim() ?? ""));
774
+ }
775
+ }
776
+ return result;
777
+ }
778
+ async function extractTable(page, selector) {
779
+ return page.evaluate((sel) => {
780
+ const table = document.querySelector(sel);
781
+ if (!table)
782
+ return [];
783
+ const rows = Array.from(table.querySelectorAll("tr"));
784
+ return rows.map((row) => Array.from(row.querySelectorAll("th, td")).map((cell) => cell.textContent?.trim() ?? ""));
785
+ }, selector);
786
+ }
787
+ async function extract(page, opts = {}) {
788
+ const result = {};
789
+ const format = opts.format ?? "text";
790
+ switch (format) {
791
+ case "text":
792
+ result.text = await getText(page, opts.selector);
793
+ break;
794
+ case "html":
795
+ result.html = await getHTML(page, opts.selector);
796
+ break;
797
+ case "links":
798
+ result.links = await getLinks(page);
799
+ break;
800
+ case "table":
801
+ result.table = opts.selector ? await extractTable(page, opts.selector) : [];
802
+ break;
803
+ case "structured":
804
+ if (opts.schema)
805
+ result.structured = await extractStructured(page, opts.schema);
806
+ break;
807
+ }
808
+ return result;
809
+ }
810
+
811
+ // src/lib/screenshot.ts
812
+ init_types();
813
+ import { join as join2 } from "path";
814
+ import { mkdirSync as mkdirSync2 } from "fs";
815
+ import { homedir as homedir2 } from "os";
816
+ var DATA_DIR = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
817
+ function getScreenshotDir(projectId) {
818
+ const base = join2(DATA_DIR, "screenshots");
819
+ const date = new Date().toISOString().split("T")[0];
820
+ const dir = projectId ? join2(base, projectId, date) : join2(base, date);
821
+ mkdirSync2(dir, { recursive: true });
822
+ return dir;
823
+ }
824
+ async function takeScreenshot(page, opts) {
825
+ try {
826
+ const dir = getScreenshotDir(opts?.projectId);
827
+ const timestamp = Date.now();
828
+ const format = opts?.format ?? "png";
829
+ const screenshotPath = opts?.path ?? join2(dir, `${timestamp}.${format}`);
830
+ const screenshotOpts = {
831
+ path: screenshotPath,
832
+ fullPage: opts?.fullPage ?? false,
833
+ type: format === "webp" ? "jpeg" : format,
834
+ quality: format === "jpeg" || format === "webp" ? opts?.quality ?? 90 : undefined
835
+ };
836
+ let buffer;
837
+ if (opts?.selector) {
838
+ const el = await page.$(opts.selector);
839
+ if (!el)
840
+ throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
841
+ buffer = await el.screenshot({ ...screenshotOpts });
842
+ } else {
843
+ buffer = await page.screenshot(screenshotOpts);
844
+ }
845
+ const viewportSize = page.viewportSize() ?? { width: 1280, height: 720 };
846
+ return {
847
+ path: screenshotPath,
848
+ base64: buffer.toString("base64"),
849
+ width: viewportSize.width,
850
+ height: viewportSize.height,
851
+ size_bytes: buffer.length
852
+ };
853
+ } catch (err) {
854
+ if (err instanceof BrowserError)
855
+ throw err;
856
+ throw new BrowserError(`Screenshot failed: ${err instanceof Error ? err.message : String(err)}`, "SCREENSHOT_FAILED");
857
+ }
858
+ }
859
+
860
+ // src/db/network-log.ts
861
+ init_schema();
862
+ import { randomUUID as randomUUID2 } from "crypto";
863
+ function logRequest(data) {
864
+ const db = getDatabase();
865
+ const id = randomUUID2();
866
+ db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
867
+ response_headers, request_body, body_size, duration_ms, resource_type)
868
+ 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);
869
+ return getNetworkRequest(id);
870
+ }
871
+ function getNetworkRequest(id) {
872
+ const db = getDatabase();
873
+ return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
874
+ }
875
+ function getNetworkLog(sessionId) {
876
+ const db = getDatabase();
877
+ return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
878
+ }
879
+ function clearNetworkLog(sessionId) {
880
+ const db = getDatabase();
881
+ db.prepare("DELETE FROM network_log WHERE session_id = ?").run(sessionId);
882
+ }
883
+
884
+ // src/lib/network.ts
885
+ function enableNetworkLogging(page, sessionId) {
886
+ const requestStart = new Map;
887
+ const onRequest = (req) => {
888
+ requestStart.set(req.url(), Date.now());
889
+ };
890
+ const onResponse = (res) => {
891
+ const start = requestStart.get(res.url()) ?? Date.now();
892
+ const duration = Date.now() - start;
893
+ const req = res.request();
894
+ try {
895
+ logRequest({
896
+ session_id: sessionId,
897
+ method: req.method(),
898
+ url: res.url(),
899
+ status_code: res.status(),
900
+ request_headers: JSON.stringify(req.headers()),
901
+ response_headers: JSON.stringify(res.headers()),
902
+ body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
903
+ duration_ms: duration,
904
+ resource_type: req.resourceType()
905
+ });
906
+ } catch {}
907
+ };
908
+ page.on("request", onRequest);
909
+ page.on("response", onResponse);
910
+ return () => {
911
+ page.off("request", onRequest);
912
+ page.off("response", onResponse);
913
+ };
914
+ }
915
+ function startHAR(page) {
916
+ const entries = [];
917
+ const requestStart = new Map;
918
+ const onRequest = (req) => {
919
+ requestStart.set(req.url() + req.method(), {
920
+ time: Date.now(),
921
+ method: req.method(),
922
+ headers: req.headers(),
923
+ postData: req.postData() ?? undefined
924
+ });
925
+ };
926
+ const onResponse = async (res) => {
927
+ const key = res.url() + res.request().method();
928
+ const start = requestStart.get(key);
929
+ if (!start)
930
+ return;
931
+ const duration = Date.now() - start.time;
932
+ const entry = {
933
+ startedDateTime: new Date(start.time).toISOString(),
934
+ time: duration,
935
+ request: {
936
+ method: start.method,
937
+ url: res.url(),
938
+ headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
939
+ postData: start.postData ? { text: start.postData } : undefined
940
+ },
941
+ response: {
942
+ status: res.status(),
943
+ statusText: res.statusText(),
944
+ headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
945
+ content: {
946
+ size: parseInt(res.headers()["content-length"] ?? "0") || 0,
947
+ mimeType: res.headers()["content-type"] ?? "application/octet-stream"
948
+ }
949
+ },
950
+ timings: { send: 0, wait: duration, receive: 0 }
951
+ };
952
+ entries.push(entry);
953
+ requestStart.delete(key);
954
+ };
955
+ page.on("request", onRequest);
956
+ page.on("response", onResponse);
957
+ return {
958
+ entries,
959
+ stop: () => {
960
+ page.off("request", onRequest);
961
+ page.off("response", onResponse);
962
+ return {
963
+ log: {
964
+ version: "1.2",
965
+ creator: { name: "@hasna/browser", version: "0.0.1" },
966
+ entries
967
+ }
968
+ };
969
+ }
970
+ };
971
+ }
972
+
973
+ // src/engines/cdp.ts
974
+ init_types();
975
+
976
+ class CDPClient {
977
+ session;
978
+ networkEnabled = false;
979
+ performanceEnabled = false;
980
+ constructor(session) {
981
+ this.session = session;
982
+ }
983
+ static async fromPage(page) {
984
+ try {
985
+ const session = await page.context().newCDPSession(page);
986
+ return new CDPClient(session);
987
+ } catch (err) {
988
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
989
+ }
990
+ }
991
+ async send(method, params) {
992
+ try {
993
+ return await this.session.send(method, params);
994
+ } catch (err) {
995
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
996
+ }
997
+ }
998
+ on(event, handler) {
999
+ this.session.on(event, handler);
1000
+ }
1001
+ off(event, handler) {
1002
+ this.session.off(event, handler);
1003
+ }
1004
+ async enableNetwork() {
1005
+ if (!this.networkEnabled) {
1006
+ await this.send("Network.enable");
1007
+ this.networkEnabled = true;
1008
+ }
1009
+ }
1010
+ async enablePerformance() {
1011
+ if (!this.performanceEnabled) {
1012
+ await this.send("Performance.enable");
1013
+ this.performanceEnabled = true;
1014
+ }
1015
+ }
1016
+ async getPerformanceMetrics() {
1017
+ await this.enablePerformance();
1018
+ const result = await this.send("Performance.getMetrics");
1019
+ const m = {};
1020
+ for (const metric of result.metrics) {
1021
+ m[metric.name] = metric.value;
1022
+ }
1023
+ return {
1024
+ js_heap_size_used: m["JSHeapUsedSize"],
1025
+ js_heap_size_total: m["JSHeapTotalSize"],
1026
+ dom_interactive: m["DOMInteractive"],
1027
+ dom_complete: m["DOMComplete"],
1028
+ load_event: m["LoadEventEnd"]
1029
+ };
1030
+ }
1031
+ async startJSCoverage() {
1032
+ await this.send("Profiler.enable");
1033
+ await this.send("Debugger.enable");
1034
+ await this.send("Profiler.startPreciseCoverage", {
1035
+ callCount: false,
1036
+ detailed: true
1037
+ });
1038
+ }
1039
+ async stopJSCoverage() {
1040
+ const result = await this.send("Profiler.takePreciseCoverage");
1041
+ await this.send("Profiler.stopPreciseCoverage");
1042
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
1043
+ url: r.url,
1044
+ text: "",
1045
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
1046
+ }));
1047
+ }
1048
+ async getCoverage() {
1049
+ await this.startJSCoverage();
1050
+ const js = await this.stopJSCoverage();
1051
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
1052
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
1053
+ }
1054
+ async captureHAREntries(page, handler) {
1055
+ await this.enableNetwork();
1056
+ const requestTimings = new Map;
1057
+ const onRequest = (params) => {
1058
+ requestTimings.set(params.requestId, params.timestamp);
1059
+ };
1060
+ const onResponse = (params) => {
1061
+ const start = requestTimings.get(params.requestId);
1062
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
1063
+ handler({
1064
+ method: "GET",
1065
+ url: params.response.url,
1066
+ status: params.response.status,
1067
+ duration
1068
+ });
1069
+ };
1070
+ this.on("Network.requestWillBeSent", onRequest);
1071
+ this.on("Network.responseReceived", onResponse);
1072
+ return () => {
1073
+ this.off("Network.requestWillBeSent", onRequest);
1074
+ this.off("Network.responseReceived", onResponse);
1075
+ };
1076
+ }
1077
+ async detach() {
1078
+ try {
1079
+ await this.session.detach();
1080
+ } catch {}
1081
+ }
1082
+ }
1083
+
1084
+ // src/lib/performance.ts
1085
+ async function getPerformanceMetrics(page) {
1086
+ const navTiming = await page.evaluate(() => {
1087
+ const t = performance.timing;
1088
+ const nav = performance.getEntriesByType("navigation")[0];
1089
+ return {
1090
+ ttfb: nav ? nav.responseStart - nav.requestStart : t.responseStart - t.requestStart,
1091
+ domInteractive: nav ? nav.domInteractive : t.domInteractive - t.navigationStart,
1092
+ domComplete: nav ? nav.domComplete : t.domComplete - t.navigationStart,
1093
+ loadEvent: nav ? nav.loadEventEnd : t.loadEventEnd - t.navigationStart
1094
+ };
1095
+ });
1096
+ const paintEntries = await page.evaluate(() => {
1097
+ const entries = performance.getEntriesByType("paint");
1098
+ const fcp = entries.find((e) => e.name === "first-contentful-paint");
1099
+ return { fcp: fcp?.startTime };
1100
+ });
1101
+ let heapMetrics = {};
1102
+ try {
1103
+ const cdp = await CDPClient.fromPage(page);
1104
+ const cdpMetrics = await cdp.getPerformanceMetrics();
1105
+ heapMetrics = {
1106
+ js_heap_size_used: cdpMetrics.js_heap_size_used,
1107
+ js_heap_size_total: cdpMetrics.js_heap_size_total
1108
+ };
1109
+ } catch {}
1110
+ return {
1111
+ fcp: paintEntries.fcp,
1112
+ ttfb: navTiming.ttfb,
1113
+ dom_interactive: navTiming.domInteractive,
1114
+ dom_complete: navTiming.domComplete,
1115
+ load_event: navTiming.loadEvent,
1116
+ ...heapMetrics
1117
+ };
1118
+ }
1119
+
1120
+ // src/db/console-log.ts
1121
+ init_schema();
1122
+ import { randomUUID as randomUUID3 } from "crypto";
1123
+ function logConsoleMessage(data) {
1124
+ const db = getDatabase();
1125
+ const id = randomUUID3();
1126
+ 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);
1127
+ return getConsoleMessage(id);
1128
+ }
1129
+ function getConsoleMessage(id) {
1130
+ const db = getDatabase();
1131
+ return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
1132
+ }
1133
+ function getConsoleLog(sessionId, level) {
1134
+ const db = getDatabase();
1135
+ if (level) {
1136
+ return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
1137
+ }
1138
+ return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
1139
+ }
1140
+
1141
+ // src/lib/console.ts
1142
+ function enableConsoleCapture(page, sessionId) {
1143
+ const onConsole = (msg) => {
1144
+ const levelMap = {
1145
+ log: "log",
1146
+ warn: "warn",
1147
+ error: "error",
1148
+ debug: "debug",
1149
+ info: "info",
1150
+ warning: "warn"
1151
+ };
1152
+ const level = levelMap[msg.type()] ?? "log";
1153
+ const location = msg.location();
1154
+ try {
1155
+ logConsoleMessage({
1156
+ session_id: sessionId,
1157
+ level,
1158
+ message: msg.text(),
1159
+ source: location.url || undefined,
1160
+ line_number: location.lineNumber || undefined
1161
+ });
1162
+ } catch {}
1163
+ };
1164
+ page.on("console", onConsole);
1165
+ return () => page.off("console", onConsole);
1166
+ }
1167
+
1168
+ // src/lib/crawler.ts
1169
+ init_types();
1170
+
1171
+ // src/db/crawl-results.ts
1172
+ init_schema();
1173
+ import { randomUUID as randomUUID4 } from "crypto";
1174
+ function deserialize(row) {
1175
+ const pages = JSON.parse(row.pages);
1176
+ return {
1177
+ id: row.id,
1178
+ project_id: row.project_id ?? undefined,
1179
+ start_url: row.start_url,
1180
+ depth: row.depth,
1181
+ pages,
1182
+ total_links: pages.reduce((acc, p) => acc + p.links.length, 0),
1183
+ errors: JSON.parse(row.errors),
1184
+ created_at: row.created_at
1185
+ };
1186
+ }
1187
+ function createCrawlResult(data) {
1188
+ const db = getDatabase();
1189
+ const id = randomUUID4();
1190
+ 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));
1191
+ return getCrawlResult(id);
1192
+ }
1193
+ function getCrawlResult(id) {
1194
+ const db = getDatabase();
1195
+ const row = db.query("SELECT * FROM crawl_results WHERE id = ?").get(id);
1196
+ return row ? deserialize(row) : null;
1197
+ }
1198
+
1199
+ // src/lib/crawler.ts
1200
+ async function crawl(startUrl, opts = {}) {
1201
+ const maxDepth = opts.maxDepth ?? 2;
1202
+ const maxPages = opts.maxPages ?? 50;
1203
+ const sameDomain = opts.sameDomain ?? true;
1204
+ const engine = selectEngine("extract_links" /* EXTRACT_LINKS */, opts.engine);
1205
+ const startDomain = new URL(startUrl).hostname;
1206
+ const visited = new Set;
1207
+ const pages = [];
1208
+ const errors = [];
1209
+ let browser;
1210
+ if (engine === "lightpanda") {
1211
+ browser = await connectLightpanda();
1212
+ } else {
1213
+ browser = await launchPlaywright({ headless: true });
1214
+ }
1215
+ const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
1216
+ async function crawlPage(url, depth) {
1217
+ if (depth > maxDepth || pages.length >= maxPages || visited.has(url))
1218
+ return;
1219
+ if (sameDomain && new URL(url).hostname !== startDomain)
1220
+ return;
1221
+ if (opts.filter && !opts.filter(url))
1222
+ return;
1223
+ visited.add(url);
1224
+ const page = await context.newPage();
1225
+ const crawled = { url, depth, links: [], error: undefined };
1226
+ try {
1227
+ const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
1228
+ crawled.title = await page.title();
1229
+ crawled.status_code = response?.status();
1230
+ crawled.links = await getLinks(page, url);
1231
+ pages.push(crawled);
1232
+ await page.close();
1233
+ for (const link of crawled.links) {
1234
+ if (pages.length >= maxPages)
1235
+ break;
1236
+ await crawlPage(link, depth + 1);
1237
+ }
1238
+ } catch (err) {
1239
+ crawled.error = err instanceof Error ? err.message : String(err);
1240
+ errors.push(`${url}: ${crawled.error}`);
1241
+ pages.push(crawled);
1242
+ await page.close().catch(() => {});
1243
+ }
1244
+ }
1245
+ try {
1246
+ await crawlPage(startUrl, 0);
1247
+ } finally {
1248
+ await browser.close().catch(() => {});
1249
+ }
1250
+ const result = createCrawlResult({
1251
+ project_id: opts.projectId,
1252
+ start_url: startUrl,
1253
+ depth: maxDepth,
1254
+ pages,
1255
+ errors
1256
+ });
1257
+ return result;
1258
+ }
1259
+
1260
+ // src/lib/recorder.ts
1261
+ init_recordings();
1262
+ init_types();
1263
+ var activeRecordings = new Map;
1264
+ async function replayRecording(recordingId, page) {
1265
+ const recording = getRecording(recordingId);
1266
+ const startTime = Date.now();
1267
+ let executed = 0;
1268
+ let failed = 0;
1269
+ const errors = [];
1270
+ for (const step of recording.steps) {
1271
+ try {
1272
+ switch (step.type) {
1273
+ case "navigate":
1274
+ if (step.url)
1275
+ await navigate(page, step.url);
1276
+ break;
1277
+ case "click":
1278
+ if (step.selector)
1279
+ await click(page, step.selector);
1280
+ break;
1281
+ case "type":
1282
+ if (step.selector && step.value)
1283
+ await type(page, step.selector, step.value);
1284
+ break;
1285
+ case "scroll":
1286
+ await scroll(page, "down");
1287
+ break;
1288
+ case "hover":
1289
+ if (step.selector) {
1290
+ const el = await page.$(step.selector);
1291
+ if (el)
1292
+ await el.hover();
1293
+ }
1294
+ break;
1295
+ case "evaluate":
1296
+ if (step.value)
1297
+ await page.evaluate(step.value);
1298
+ break;
1299
+ case "wait":
1300
+ if (step.selector) {
1301
+ await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
1302
+ }
1303
+ break;
1304
+ }
1305
+ executed++;
1306
+ } catch (err) {
1307
+ failed++;
1308
+ errors.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
1309
+ }
1310
+ await new Promise((r) => setTimeout(r, 100));
1311
+ }
1312
+ return {
1313
+ recording_id: recordingId,
1314
+ success: failed === 0,
1315
+ steps_executed: executed,
1316
+ steps_failed: failed,
1317
+ errors,
1318
+ duration_ms: Date.now() - startTime
1319
+ };
1320
+ }
1321
+
1322
+ // src/lib/agents.ts
1323
+ init_agents();
1324
+ function registerAgent2(name, opts = {}) {
1325
+ return registerAgent(name, opts);
1326
+ }
1327
+ function heartbeat2(agentId) {
1328
+ heartbeat(agentId);
1329
+ }
1330
+
1331
+ // src/db/projects.ts
1332
+ init_schema();
1333
+ init_types();
1334
+ import { randomUUID as randomUUID7 } from "crypto";
1335
+ function createProject(data) {
1336
+ const db = getDatabase();
1337
+ const id = randomUUID7();
1338
+ db.prepare("INSERT INTO projects (id, name, path, description) VALUES (?, ?, ?, ?)").run(id, data.name, data.path, data.description ?? null);
1339
+ return getProject(id);
1340
+ }
1341
+ function ensureProject(name, path, description) {
1342
+ const db = getDatabase();
1343
+ const existing = db.query("SELECT * FROM projects WHERE name = ?").get(name);
1344
+ if (existing)
1345
+ return existing;
1346
+ return createProject({ name, path, description });
1347
+ }
1348
+ function getProject(id) {
1349
+ const db = getDatabase();
1350
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
1351
+ if (!row)
1352
+ throw new ProjectNotFoundError(id);
1353
+ return row;
1354
+ }
1355
+ function listProjects() {
1356
+ const db = getDatabase();
1357
+ return db.query("SELECT * FROM projects ORDER BY created_at DESC").all();
1358
+ }
1359
+
1360
+ // src/server/index.ts
1361
+ init_recordings();
1362
+ var PORT = parseInt(process.env["BROWSER_SERVER_PORT"] ?? "7030");
1363
+ var CORS_HEADERS = {
1364
+ "Access-Control-Allow-Origin": "*",
1365
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1366
+ "Access-Control-Allow-Headers": "Content-Type"
1367
+ };
1368
+ var networkCleanup = new Map;
1369
+ var consoleCleanup = new Map;
1370
+ var harCaptures = new Map;
1371
+ function ok(data, status = 200) {
1372
+ return new Response(JSON.stringify(data), {
1373
+ status,
1374
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS }
1375
+ });
1376
+ }
1377
+ function notFound(msg) {
1378
+ return new Response(JSON.stringify({ error: msg }), {
1379
+ status: 404,
1380
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS }
1381
+ });
1382
+ }
1383
+ function badRequest(msg) {
1384
+ return new Response(JSON.stringify({ error: msg }), {
1385
+ status: 400,
1386
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS }
1387
+ });
1388
+ }
1389
+ function serverError(e) {
1390
+ const msg = e instanceof Error ? e.message : String(e);
1391
+ return new Response(JSON.stringify({ error: msg }), {
1392
+ status: 500,
1393
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS }
1394
+ });
1395
+ }
1396
+ var server = Bun.serve({
1397
+ port: PORT,
1398
+ async fetch(req) {
1399
+ const url = new URL(req.url);
1400
+ const path = url.pathname;
1401
+ const method = req.method;
1402
+ if (method === "OPTIONS") {
1403
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
1404
+ }
1405
+ try {
1406
+ if (path === "/api/sessions" && method === "GET") {
1407
+ const status = url.searchParams.get("status");
1408
+ const projectId = url.searchParams.get("project_id") ?? undefined;
1409
+ return ok({ sessions: listSessions2(status ? { status, projectId } : { projectId }) });
1410
+ }
1411
+ if (path === "/api/sessions" && method === "POST") {
1412
+ const body = await req.json();
1413
+ const { session } = await createSession2({
1414
+ engine: body.engine ?? "auto",
1415
+ projectId: body.project_id,
1416
+ agentId: body.agent_id,
1417
+ startUrl: body.start_url,
1418
+ headless: body.headless ?? true
1419
+ });
1420
+ return ok({ session }, 201);
1421
+ }
1422
+ const sessionMatch = path.match(/^\/api\/sessions\/([^/]+)$/);
1423
+ if (sessionMatch && method === "DELETE") {
1424
+ const id = sessionMatch[1];
1425
+ networkCleanup.get(id)?.();
1426
+ consoleCleanup.get(id)?.();
1427
+ networkCleanup.delete(id);
1428
+ consoleCleanup.delete(id);
1429
+ harCaptures.delete(id);
1430
+ const session = await closeSession2(id);
1431
+ return ok({ session });
1432
+ }
1433
+ if (path === "/api/navigate" && method === "POST") {
1434
+ const body = await req.json();
1435
+ if (!body.session_id || !body.url)
1436
+ return badRequest("session_id and url required");
1437
+ const page = getSessionPage(body.session_id);
1438
+ await navigate(page, body.url);
1439
+ return ok({ url: body.url, title: await page.title(), current_url: page.url() });
1440
+ }
1441
+ if (path === "/api/extract" && method === "POST") {
1442
+ const body = await req.json();
1443
+ if (!body.session_id)
1444
+ return badRequest("session_id required");
1445
+ const page = getSessionPage(body.session_id);
1446
+ const result = await extract(page, { format: body.format, selector: body.selector });
1447
+ return ok(result);
1448
+ }
1449
+ if (path === "/api/screenshot" && method === "POST") {
1450
+ const body = await req.json();
1451
+ if (!body.session_id)
1452
+ return badRequest("session_id required");
1453
+ const page = getSessionPage(body.session_id);
1454
+ const result = await takeScreenshot(page, { selector: body.selector, fullPage: body.full_page });
1455
+ return ok(result);
1456
+ }
1457
+ if (path.match(/^\/api\/screenshots\/([^/]+)$/) && method === "GET") {
1458
+ const sessionId = path.split("/")[3];
1459
+ const { listSnapshots: listSnapshots2 } = await Promise.resolve().then(() => (init_snapshots(), exports_snapshots));
1460
+ return ok({ snapshots: listSnapshots2(sessionId) });
1461
+ }
1462
+ if (path.match(/^\/api\/network-log\/([^/]+)$/) && method === "GET") {
1463
+ const sessionId = path.split("/")[3];
1464
+ if (!networkCleanup.has(sessionId)) {
1465
+ const page = getSessionPage(sessionId);
1466
+ networkCleanup.set(sessionId, enableNetworkLogging(page, sessionId));
1467
+ }
1468
+ return ok({ requests: getNetworkLog(sessionId) });
1469
+ }
1470
+ if (path.match(/^\/api\/network-log\/([^/]+)$/) && method === "DELETE") {
1471
+ const sessionId = path.split("/")[3];
1472
+ clearNetworkLog(sessionId);
1473
+ return ok({ cleared: true });
1474
+ }
1475
+ if (path.match(/^\/api\/console-log\/([^/]+)$/) && method === "GET") {
1476
+ const sessionId = path.split("/")[3];
1477
+ if (!consoleCleanup.has(sessionId)) {
1478
+ const page = getSessionPage(sessionId);
1479
+ consoleCleanup.set(sessionId, enableConsoleCapture(page, sessionId));
1480
+ }
1481
+ return ok({ messages: getConsoleLog(sessionId) });
1482
+ }
1483
+ if (path.match(/^\/api\/performance\/([^/]+)$/) && method === "GET") {
1484
+ const sessionId = path.split("/")[3];
1485
+ const page = getSessionPage(sessionId);
1486
+ return ok({ metrics: await getPerformanceMetrics(page) });
1487
+ }
1488
+ if (path === "/api/har/start" && method === "POST") {
1489
+ const body = await req.json();
1490
+ const page = getSessionPage(body.session_id);
1491
+ harCaptures.set(body.session_id, startHAR(page));
1492
+ return ok({ started: true });
1493
+ }
1494
+ if (path === "/api/har/stop" && method === "POST") {
1495
+ const body = await req.json();
1496
+ const capture = harCaptures.get(body.session_id);
1497
+ if (!capture)
1498
+ return notFound("No active HAR capture");
1499
+ const har = capture.stop();
1500
+ harCaptures.delete(body.session_id);
1501
+ return ok({ har });
1502
+ }
1503
+ if (path === "/api/recordings" && method === "GET") {
1504
+ return ok({ recordings: listRecordings(url.searchParams.get("project_id") ?? undefined) });
1505
+ }
1506
+ if (path.match(/^\/api\/recordings\/([^/]+)\/replay$/) && method === "POST") {
1507
+ const id = path.split("/")[3];
1508
+ const body = await req.json();
1509
+ const page = getSessionPage(body.session_id);
1510
+ const result = await replayRecording(id, page);
1511
+ return ok(result);
1512
+ }
1513
+ if (path.match(/^\/api\/recordings\/([^/]+)$/) && method === "DELETE") {
1514
+ const id = path.split("/")[3];
1515
+ const { deleteRecording: deleteRecording2 } = await Promise.resolve().then(() => (init_recordings(), exports_recordings));
1516
+ deleteRecording2(id);
1517
+ return ok({ deleted: id });
1518
+ }
1519
+ if (path === "/api/crawl" && method === "POST") {
1520
+ const body = await req.json();
1521
+ if (!body.url)
1522
+ return badRequest("url required");
1523
+ const result = await crawl(body.url, {
1524
+ maxDepth: body.max_depth ?? 2,
1525
+ maxPages: body.max_pages ?? 50,
1526
+ engine: body.engine
1527
+ });
1528
+ return ok(result);
1529
+ }
1530
+ if (path === "/api/agents" && method === "GET") {
1531
+ return ok({ agents: listAgents(url.searchParams.get("project_id") ?? undefined) });
1532
+ }
1533
+ if (path === "/api/agents" && method === "POST") {
1534
+ const body = await req.json();
1535
+ if (!body.name)
1536
+ return badRequest("name required");
1537
+ const agent = registerAgent2(body.name, { description: body.description, projectId: body.project_id, sessionId: body.session_id, workingDir: body.working_dir });
1538
+ return ok({ agent }, 201);
1539
+ }
1540
+ if (path.match(/^\/api\/agents\/([^/]+)\/heartbeat$/) && method === "PUT") {
1541
+ const id = path.split("/")[3];
1542
+ heartbeat2(id);
1543
+ return ok({ ok: true, agent_id: id, timestamp: new Date().toISOString() });
1544
+ }
1545
+ if (path.match(/^\/api\/agents\/([^/]+)$/) && method === "DELETE") {
1546
+ const id = path.split("/")[3];
1547
+ const { deleteAgent: deleteAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
1548
+ deleteAgent2(id);
1549
+ return ok({ deleted: id });
1550
+ }
1551
+ if (path === "/api/projects" && method === "GET") {
1552
+ return ok({ projects: listProjects() });
1553
+ }
1554
+ if (path === "/api/projects" && method === "POST") {
1555
+ const body = await req.json();
1556
+ if (!body.name || !body.path)
1557
+ return badRequest("name and path required");
1558
+ const project = ensureProject(body.name, body.path, body.description);
1559
+ return ok({ project }, 201);
1560
+ }
1561
+ const dashboardDist = join3(import.meta.dir, "../../dashboard/dist");
1562
+ if (existsSync(dashboardDist)) {
1563
+ const filePath = path === "/" ? join3(dashboardDist, "index.html") : join3(dashboardDist, path);
1564
+ if (existsSync(filePath)) {
1565
+ return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
1566
+ }
1567
+ return new Response(Bun.file(join3(dashboardDist, "index.html")), { headers: CORS_HEADERS });
1568
+ }
1569
+ if (path === "/" || path === "") {
1570
+ return new Response("@hasna/browser REST API running. Dashboard not built.", {
1571
+ headers: { "Content-Type": "text/plain", ...CORS_HEADERS }
1572
+ });
1573
+ }
1574
+ return notFound(`Route not found: ${method} ${path}`);
1575
+ } catch (e) {
1576
+ return serverError(e);
1577
+ }
1578
+ }
1579
+ });
1580
+ console.error(`@hasna/browser server running on http://localhost:${PORT}`);