@hasna/browser 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,8 +16,249 @@ var __toESM = (mod, isNodeMode, target) => {
16
16
  return to;
17
17
  };
18
18
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
29
  var __require = import.meta.require;
20
30
 
31
+ // src/db/schema.ts
32
+ import { Database } from "bun:sqlite";
33
+ import { join } from "path";
34
+ import { mkdirSync } from "fs";
35
+ import { homedir } from "os";
36
+ function getDataDir() {
37
+ return process.env["BROWSER_DATA_DIR"] ?? join(homedir(), ".browser");
38
+ }
39
+ function getDatabase(path) {
40
+ const resolvedPath = path ?? process.env["BROWSER_DB_PATH"] ?? join(getDataDir(), "browser.db");
41
+ if (_db && _dbPath === resolvedPath)
42
+ return _db;
43
+ if (_db) {
44
+ try {
45
+ _db.close();
46
+ } catch {}
47
+ _db = null;
48
+ }
49
+ mkdirSync(join(resolvedPath, ".."), { recursive: true });
50
+ _db = new Database(resolvedPath);
51
+ _dbPath = resolvedPath;
52
+ _db.exec("PRAGMA journal_mode=WAL;");
53
+ _db.exec("PRAGMA foreign_keys=ON;");
54
+ runMigrations(_db);
55
+ return _db;
56
+ }
57
+ function resetDatabase() {
58
+ if (_db) {
59
+ try {
60
+ _db.close();
61
+ } catch {}
62
+ }
63
+ _db = null;
64
+ _dbPath = null;
65
+ }
66
+ function runMigrations(db) {
67
+ db.exec(`
68
+ CREATE TABLE IF NOT EXISTS schema_migrations (
69
+ version INTEGER PRIMARY KEY,
70
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
71
+ );
72
+ `);
73
+ const applied = new Set(db.query("SELECT version FROM schema_migrations").all().map((r) => r.version));
74
+ const migrations = [
75
+ {
76
+ version: 1,
77
+ sql: `
78
+ CREATE TABLE IF NOT EXISTS projects (
79
+ id TEXT PRIMARY KEY,
80
+ name TEXT NOT NULL UNIQUE,
81
+ path TEXT NOT NULL,
82
+ description TEXT,
83
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS agents (
87
+ id TEXT PRIMARY KEY,
88
+ name TEXT NOT NULL,
89
+ description TEXT,
90
+ session_id TEXT,
91
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
92
+ working_dir TEXT,
93
+ last_seen TEXT NOT NULL DEFAULT (datetime('now')),
94
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS heartbeats (
98
+ id TEXT PRIMARY KEY,
99
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
100
+ session_id TEXT,
101
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
102
+ );
103
+
104
+ CREATE TABLE IF NOT EXISTS sessions (
105
+ id TEXT PRIMARY KEY,
106
+ engine TEXT NOT NULL,
107
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
108
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
109
+ start_url TEXT,
110
+ status TEXT NOT NULL DEFAULT 'active',
111
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
112
+ closed_at TEXT
113
+ );
114
+
115
+ CREATE TABLE IF NOT EXISTS snapshots (
116
+ id TEXT PRIMARY KEY,
117
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
118
+ url TEXT NOT NULL,
119
+ title TEXT,
120
+ html TEXT,
121
+ screenshot_path TEXT,
122
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
123
+ );
124
+
125
+ CREATE TABLE IF NOT EXISTS network_log (
126
+ id TEXT PRIMARY KEY,
127
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
128
+ method TEXT NOT NULL,
129
+ url TEXT NOT NULL,
130
+ status_code INTEGER,
131
+ request_headers TEXT,
132
+ response_headers TEXT,
133
+ request_body TEXT,
134
+ body_size INTEGER,
135
+ duration_ms INTEGER,
136
+ resource_type TEXT,
137
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
138
+ );
139
+
140
+ CREATE TABLE IF NOT EXISTS console_log (
141
+ id TEXT PRIMARY KEY,
142
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
143
+ level TEXT NOT NULL DEFAULT 'log',
144
+ message TEXT NOT NULL,
145
+ source TEXT,
146
+ line_number INTEGER,
147
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
148
+ );
149
+
150
+ CREATE TABLE IF NOT EXISTS recordings (
151
+ id TEXT PRIMARY KEY,
152
+ name TEXT NOT NULL,
153
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
154
+ start_url TEXT,
155
+ steps TEXT NOT NULL DEFAULT '[]',
156
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
157
+ );
158
+
159
+ CREATE TABLE IF NOT EXISTS crawl_results (
160
+ id TEXT PRIMARY KEY,
161
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
162
+ start_url TEXT NOT NULL,
163
+ depth INTEGER NOT NULL DEFAULT 1,
164
+ pages TEXT NOT NULL DEFAULT '[]',
165
+ links TEXT NOT NULL DEFAULT '[]',
166
+ errors TEXT NOT NULL DEFAULT '[]',
167
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
168
+ );
169
+
170
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
171
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
172
+ CREATE INDEX IF NOT EXISTS idx_snapshots_session ON snapshots(session_id);
173
+ CREATE INDEX IF NOT EXISTS idx_network_log_session ON network_log(session_id);
174
+ CREATE INDEX IF NOT EXISTS idx_console_log_session ON console_log(session_id);
175
+ CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
176
+ CREATE INDEX IF NOT EXISTS idx_heartbeats_agent ON heartbeats(agent_id);
177
+ CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id);
178
+ CREATE INDEX IF NOT EXISTS idx_crawl_results_project ON crawl_results(project_id);
179
+ `
180
+ },
181
+ {
182
+ version: 2,
183
+ sql: `
184
+ -- Gallery entries
185
+ CREATE TABLE IF NOT EXISTS gallery_entries (
186
+ id TEXT PRIMARY KEY,
187
+ session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
188
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
189
+ url TEXT,
190
+ title TEXT,
191
+ path TEXT NOT NULL,
192
+ thumbnail_path TEXT,
193
+ format TEXT,
194
+ width INTEGER,
195
+ height INTEGER,
196
+ original_size_bytes INTEGER,
197
+ compressed_size_bytes INTEGER,
198
+ compression_ratio REAL,
199
+ tags TEXT NOT NULL DEFAULT '[]',
200
+ notes TEXT,
201
+ is_favorite INTEGER NOT NULL DEFAULT 0,
202
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
203
+ );
204
+
205
+ -- Session name column (migration 2 adds it)
206
+ ALTER TABLE sessions ADD COLUMN name TEXT;
207
+
208
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_name ON sessions(name) WHERE name IS NOT NULL;
209
+ CREATE INDEX IF NOT EXISTS idx_gallery_session ON gallery_entries(session_id);
210
+ CREATE INDEX IF NOT EXISTS idx_gallery_project ON gallery_entries(project_id);
211
+ CREATE INDEX IF NOT EXISTS idx_gallery_favorite ON gallery_entries(is_favorite);
212
+ CREATE INDEX IF NOT EXISTS idx_gallery_created ON gallery_entries(created_at);
213
+ `
214
+ }
215
+ ];
216
+ for (const m of migrations) {
217
+ if (!applied.has(m.version)) {
218
+ db.transaction(() => {
219
+ db.exec(m.sql);
220
+ db.prepare("INSERT INTO schema_migrations (version) VALUES (?)").run(m.version);
221
+ })();
222
+ }
223
+ }
224
+ }
225
+ var _db = null, _dbPath = null;
226
+ var init_schema = () => {};
227
+
228
+ // src/db/console-log.ts
229
+ var exports_console_log = {};
230
+ __export(exports_console_log, {
231
+ logConsoleMessage: () => logConsoleMessage,
232
+ getConsoleMessage: () => getConsoleMessage,
233
+ getConsoleLog: () => getConsoleLog,
234
+ clearConsoleLog: () => clearConsoleLog
235
+ });
236
+ import { randomUUID as randomUUID6 } from "crypto";
237
+ function logConsoleMessage(data) {
238
+ const db = getDatabase();
239
+ const id = randomUUID6();
240
+ 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);
241
+ return getConsoleMessage(id);
242
+ }
243
+ function getConsoleMessage(id) {
244
+ const db = getDatabase();
245
+ return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
246
+ }
247
+ function getConsoleLog(sessionId, level) {
248
+ const db = getDatabase();
249
+ if (level) {
250
+ return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
251
+ }
252
+ return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
253
+ }
254
+ function clearConsoleLog(sessionId) {
255
+ const db = getDatabase();
256
+ db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
257
+ }
258
+ var init_console_log = __esm(() => {
259
+ init_schema();
260
+ });
261
+
21
262
  // node_modules/sharp/lib/is.js
22
263
  var require_is = __commonJS((exports, module) => {
23
264
  /*!
@@ -3680,7 +3921,7 @@ var require_color = __commonJS((exports, module) => {
3680
3921
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3681
3922
  var __getOwnPropNames2 = Object.getOwnPropertyNames;
3682
3923
  var __hasOwnProp2 = Object.prototype.hasOwnProperty;
3683
- var __export = (target, all) => {
3924
+ var __export2 = (target, all) => {
3684
3925
  for (var name in all)
3685
3926
  __defProp2(target, name, { get: all[name], enumerable: true });
3686
3927
  };
@@ -3694,7 +3935,7 @@ var require_color = __commonJS((exports, module) => {
3694
3935
  };
3695
3936
  var __toCommonJS = (mod) => __copyProps(__defProp2({}, "__esModule", { value: true }), mod);
3696
3937
  var index_exports = {};
3697
- __export(index_exports, {
3938
+ __export2(index_exports, {
3698
3939
  default: () => index_default
3699
3940
  });
3700
3941
  module.exports = __toCommonJS(index_exports);
@@ -6491,209 +6732,18 @@ class ProjectNotFoundError extends BrowserError {
6491
6732
  this.name = "ProjectNotFoundError";
6492
6733
  }
6493
6734
  }
6494
- // src/db/schema.ts
6495
- import { Database } from "bun:sqlite";
6496
- import { join } from "path";
6497
- import { mkdirSync } from "fs";
6498
- import { homedir } from "os";
6499
- function getDataDir() {
6500
- return process.env["BROWSER_DATA_DIR"] ?? join(homedir(), ".browser");
6501
- }
6502
- var _db = null;
6503
- var _dbPath = null;
6504
- function getDatabase(path) {
6505
- const resolvedPath = path ?? process.env["BROWSER_DB_PATH"] ?? join(getDataDir(), "browser.db");
6506
- if (_db && _dbPath === resolvedPath)
6507
- return _db;
6508
- if (_db) {
6509
- try {
6510
- _db.close();
6511
- } catch {}
6512
- _db = null;
6513
- }
6514
- mkdirSync(join(resolvedPath, ".."), { recursive: true });
6515
- _db = new Database(resolvedPath);
6516
- _dbPath = resolvedPath;
6517
- _db.exec("PRAGMA journal_mode=WAL;");
6518
- _db.exec("PRAGMA foreign_keys=ON;");
6519
- runMigrations(_db);
6520
- return _db;
6521
- }
6522
- function resetDatabase() {
6523
- if (_db) {
6524
- try {
6525
- _db.close();
6526
- } catch {}
6527
- }
6528
- _db = null;
6529
- _dbPath = null;
6530
- }
6531
- function runMigrations(db) {
6532
- db.exec(`
6533
- CREATE TABLE IF NOT EXISTS schema_migrations (
6534
- version INTEGER PRIMARY KEY,
6535
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
6536
- );
6537
- `);
6538
- const applied = new Set(db.query("SELECT version FROM schema_migrations").all().map((r) => r.version));
6539
- const migrations = [
6540
- {
6541
- version: 1,
6542
- sql: `
6543
- CREATE TABLE IF NOT EXISTS projects (
6544
- id TEXT PRIMARY KEY,
6545
- name TEXT NOT NULL UNIQUE,
6546
- path TEXT NOT NULL,
6547
- description TEXT,
6548
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
6549
- );
6550
-
6551
- CREATE TABLE IF NOT EXISTS agents (
6552
- id TEXT PRIMARY KEY,
6553
- name TEXT NOT NULL,
6554
- description TEXT,
6555
- session_id TEXT,
6556
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
6557
- working_dir TEXT,
6558
- last_seen TEXT NOT NULL DEFAULT (datetime('now')),
6559
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
6560
- );
6561
-
6562
- CREATE TABLE IF NOT EXISTS heartbeats (
6563
- id TEXT PRIMARY KEY,
6564
- agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
6565
- session_id TEXT,
6566
- timestamp TEXT NOT NULL DEFAULT (datetime('now'))
6567
- );
6568
-
6569
- CREATE TABLE IF NOT EXISTS sessions (
6570
- id TEXT PRIMARY KEY,
6571
- engine TEXT NOT NULL,
6572
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
6573
- agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
6574
- start_url TEXT,
6575
- status TEXT NOT NULL DEFAULT 'active',
6576
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
6577
- closed_at TEXT
6578
- );
6579
-
6580
- CREATE TABLE IF NOT EXISTS snapshots (
6581
- id TEXT PRIMARY KEY,
6582
- session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
6583
- url TEXT NOT NULL,
6584
- title TEXT,
6585
- html TEXT,
6586
- screenshot_path TEXT,
6587
- timestamp TEXT NOT NULL DEFAULT (datetime('now'))
6588
- );
6589
-
6590
- CREATE TABLE IF NOT EXISTS network_log (
6591
- id TEXT PRIMARY KEY,
6592
- session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
6593
- method TEXT NOT NULL,
6594
- url TEXT NOT NULL,
6595
- status_code INTEGER,
6596
- request_headers TEXT,
6597
- response_headers TEXT,
6598
- request_body TEXT,
6599
- body_size INTEGER,
6600
- duration_ms INTEGER,
6601
- resource_type TEXT,
6602
- timestamp TEXT NOT NULL DEFAULT (datetime('now'))
6603
- );
6604
-
6605
- CREATE TABLE IF NOT EXISTS console_log (
6606
- id TEXT PRIMARY KEY,
6607
- session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
6608
- level TEXT NOT NULL DEFAULT 'log',
6609
- message TEXT NOT NULL,
6610
- source TEXT,
6611
- line_number INTEGER,
6612
- timestamp TEXT NOT NULL DEFAULT (datetime('now'))
6613
- );
6614
-
6615
- CREATE TABLE IF NOT EXISTS recordings (
6616
- id TEXT PRIMARY KEY,
6617
- name TEXT NOT NULL,
6618
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
6619
- start_url TEXT,
6620
- steps TEXT NOT NULL DEFAULT '[]',
6621
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
6622
- );
6623
-
6624
- CREATE TABLE IF NOT EXISTS crawl_results (
6625
- id TEXT PRIMARY KEY,
6626
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
6627
- start_url TEXT NOT NULL,
6628
- depth INTEGER NOT NULL DEFAULT 1,
6629
- pages TEXT NOT NULL DEFAULT '[]',
6630
- links TEXT NOT NULL DEFAULT '[]',
6631
- errors TEXT NOT NULL DEFAULT '[]',
6632
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
6633
- );
6634
-
6635
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
6636
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
6637
- CREATE INDEX IF NOT EXISTS idx_snapshots_session ON snapshots(session_id);
6638
- CREATE INDEX IF NOT EXISTS idx_network_log_session ON network_log(session_id);
6639
- CREATE INDEX IF NOT EXISTS idx_console_log_session ON console_log(session_id);
6640
- CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
6641
- CREATE INDEX IF NOT EXISTS idx_heartbeats_agent ON heartbeats(agent_id);
6642
- CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id);
6643
- CREATE INDEX IF NOT EXISTS idx_crawl_results_project ON crawl_results(project_id);
6644
- `
6645
- },
6646
- {
6647
- version: 2,
6648
- sql: `
6649
- -- Gallery entries
6650
- CREATE TABLE IF NOT EXISTS gallery_entries (
6651
- id TEXT PRIMARY KEY,
6652
- session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
6653
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
6654
- url TEXT,
6655
- title TEXT,
6656
- path TEXT NOT NULL,
6657
- thumbnail_path TEXT,
6658
- format TEXT,
6659
- width INTEGER,
6660
- height INTEGER,
6661
- original_size_bytes INTEGER,
6662
- compressed_size_bytes INTEGER,
6663
- compression_ratio REAL,
6664
- tags TEXT NOT NULL DEFAULT '[]',
6665
- notes TEXT,
6666
- is_favorite INTEGER NOT NULL DEFAULT 0,
6667
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
6668
- );
6669
-
6670
- -- Session name column (migration 2 adds it)
6671
- ALTER TABLE sessions ADD COLUMN name TEXT;
6672
-
6673
- CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_name ON sessions(name) WHERE name IS NOT NULL;
6674
- CREATE INDEX IF NOT EXISTS idx_gallery_session ON gallery_entries(session_id);
6675
- CREATE INDEX IF NOT EXISTS idx_gallery_project ON gallery_entries(project_id);
6676
- CREATE INDEX IF NOT EXISTS idx_gallery_favorite ON gallery_entries(is_favorite);
6677
- CREATE INDEX IF NOT EXISTS idx_gallery_created ON gallery_entries(created_at);
6678
- `
6679
- }
6680
- ];
6681
- for (const m of migrations) {
6682
- if (!applied.has(m.version)) {
6683
- db.transaction(() => {
6684
- db.exec(m.sql);
6685
- db.prepare("INSERT INTO schema_migrations (version) VALUES (?)").run(m.version);
6686
- })();
6687
- }
6688
- }
6689
- }
6690
- // src/db/projects.ts
6691
- import { randomUUID } from "crypto";
6692
- function createProject(data) {
6693
- const db = getDatabase();
6694
- const id = randomUUID();
6695
- db.prepare("INSERT INTO projects (id, name, path, description) VALUES (?, ?, ?, ?)").run(id, data.name, data.path, data.description ?? null);
6696
- return getProject(id);
6735
+
6736
+ // src/index.ts
6737
+ init_schema();
6738
+
6739
+ // src/db/projects.ts
6740
+ init_schema();
6741
+ import { randomUUID } from "crypto";
6742
+ function createProject(data) {
6743
+ const db = getDatabase();
6744
+ const id = randomUUID();
6745
+ db.prepare("INSERT INTO projects (id, name, path, description) VALUES (?, ?, ?, ?)").run(id, data.name, data.path, data.description ?? null);
6746
+ return getProject(id);
6697
6747
  }
6698
6748
  function ensureProject(name, path, description) {
6699
6749
  const db = getDatabase();
@@ -6744,6 +6794,7 @@ function deleteProject(id) {
6744
6794
  db.prepare("DELETE FROM projects WHERE id = ?").run(id);
6745
6795
  }
6746
6796
  // src/db/agents.ts
6797
+ init_schema();
6747
6798
  import { randomUUID as randomUUID2 } from "crypto";
6748
6799
  function registerAgent(name, opts = {}) {
6749
6800
  const db = getDatabase();
@@ -6823,11 +6874,19 @@ function cleanStaleAgents(thresholdMs) {
6823
6874
  return result.changes;
6824
6875
  }
6825
6876
  // src/db/sessions.ts
6877
+ init_schema();
6826
6878
  import { randomUUID as randomUUID3 } from "crypto";
6827
6879
  function createSession(data) {
6828
6880
  const db = getDatabase();
6829
6881
  const id = randomUUID3();
6830
- db.prepare("INSERT INTO sessions (id, engine, project_id, agent_id, start_url, name) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.engine, data.projectId ?? null, data.agentId ?? null, data.startUrl ?? null, data.name ?? null);
6882
+ let name = data.name ?? null;
6883
+ if (name) {
6884
+ const existing = db.query("SELECT id FROM sessions WHERE name = ?").get(name);
6885
+ if (existing) {
6886
+ name = `${name}-${id.slice(0, 6)}`;
6887
+ }
6888
+ }
6889
+ db.prepare("INSERT INTO sessions (id, engine, project_id, agent_id, start_url, name) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.engine, data.projectId ?? null, data.agentId ?? null, data.startUrl ?? null, name);
6831
6890
  return getSession(id);
6832
6891
  }
6833
6892
  function getSessionByName(name) {
@@ -6875,6 +6934,7 @@ function deleteSession(id) {
6875
6934
  db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
6876
6935
  }
6877
6936
  // src/db/snapshots.ts
6937
+ init_schema();
6878
6938
  import { randomUUID as randomUUID4 } from "crypto";
6879
6939
  function createSnapshot(data) {
6880
6940
  const db = getDatabase();
@@ -6899,6 +6959,7 @@ function deleteSnapshotsBySession(sessionId) {
6899
6959
  db.prepare("DELETE FROM snapshots WHERE session_id = ?").run(sessionId);
6900
6960
  }
6901
6961
  // src/db/network-log.ts
6962
+ init_schema();
6902
6963
  import { randomUUID as randomUUID5 } from "crypto";
6903
6964
  function logRequest(data) {
6904
6965
  const db = getDatabase();
@@ -6924,30 +6985,12 @@ function deleteNetworkRequest(id) {
6924
6985
  const db = getDatabase();
6925
6986
  db.prepare("DELETE FROM network_log WHERE id = ?").run(id);
6926
6987
  }
6927
- // src/db/console-log.ts
6928
- import { randomUUID as randomUUID6 } from "crypto";
6929
- function logConsoleMessage(data) {
6930
- const db = getDatabase();
6931
- const id = randomUUID6();
6932
- 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);
6933
- return getConsoleMessage(id);
6934
- }
6935
- function getConsoleMessage(id) {
6936
- const db = getDatabase();
6937
- return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
6938
- }
6939
- function getConsoleLog(sessionId, level) {
6940
- const db = getDatabase();
6941
- if (level) {
6942
- return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
6943
- }
6944
- return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
6945
- }
6946
- function clearConsoleLog(sessionId) {
6947
- const db = getDatabase();
6948
- db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
6949
- }
6988
+
6989
+ // src/index.ts
6990
+ init_console_log();
6991
+
6950
6992
  // src/db/recordings.ts
6993
+ init_schema();
6951
6994
  import { randomUUID as randomUUID7 } from "crypto";
6952
6995
  function deserialize(row) {
6953
6996
  return {
@@ -7002,6 +7045,7 @@ function deleteRecording(id) {
7002
7045
  db.prepare("DELETE FROM recordings WHERE id = ?").run(id);
7003
7046
  }
7004
7047
  // src/db/crawl-results.ts
7048
+ init_schema();
7005
7049
  import { randomUUID as randomUUID8 } from "crypto";
7006
7050
  function deserialize2(row) {
7007
7051
  const pages = JSON.parse(row.pages);
@@ -7037,6 +7081,7 @@ function deleteCrawlResult(id) {
7037
7081
  db.prepare("DELETE FROM crawl_results WHERE id = ?").run(id);
7038
7082
  }
7039
7083
  // src/db/heartbeats.ts
7084
+ init_schema();
7040
7085
  import { randomUUID as randomUUID9 } from "crypto";
7041
7086
  function recordHeartbeat(agentId, sessionId) {
7042
7087
  const db = getDatabase();
@@ -7387,14 +7432,413 @@ class LightpandaPage {
7387
7432
  await this.page.context().close();
7388
7433
  }
7389
7434
  }
7435
+ // src/engines/bun-webview.ts
7436
+ import { join as join2 } from "path";
7437
+ import { mkdirSync as mkdirSync2 } from "fs";
7438
+ import { homedir as homedir2 } from "os";
7439
+ function isBunWebViewAvailable() {
7440
+ return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.WebView !== "undefined";
7441
+ }
7442
+ function getProfileDir(profileName) {
7443
+ const base = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
7444
+ const dir = join2(base, "profiles", profileName);
7445
+ mkdirSync2(dir, { recursive: true });
7446
+ return dir;
7447
+ }
7448
+
7449
+ class BunWebViewSession {
7450
+ view;
7451
+ _sessionId;
7452
+ _eventListeners = new Map;
7453
+ constructor(opts = {}) {
7454
+ if (!isBunWebViewAvailable()) {
7455
+ throw new Error("Bun.WebView is not available. Install Bun canary: bun upgrade --canary");
7456
+ }
7457
+ const BunWebView = globalThis.Bun.WebView;
7458
+ const constructorOpts = {
7459
+ width: opts.width ?? 1280,
7460
+ height: opts.height ?? 720
7461
+ };
7462
+ if (opts.profile) {
7463
+ constructorOpts.dataStore = { directory: getProfileDir(opts.profile) };
7464
+ } else {
7465
+ constructorOpts.dataStore = "ephemeral";
7466
+ }
7467
+ if (opts.onConsole) {
7468
+ constructorOpts.console = opts.onConsole;
7469
+ }
7470
+ this.view = new BunWebView(constructorOpts);
7471
+ this.view.onNavigated = (url) => {
7472
+ this._emit("navigated", url);
7473
+ };
7474
+ this.view.onNavigationFailed = (error) => {
7475
+ this._emit("navigationfailed", error);
7476
+ };
7477
+ }
7478
+ async goto(url, opts) {
7479
+ await this.view.navigate(url);
7480
+ await new Promise((r) => setTimeout(r, 200));
7481
+ }
7482
+ async goBack() {
7483
+ await this.view.goBack();
7484
+ }
7485
+ async goForward() {
7486
+ await this.view.goForward();
7487
+ }
7488
+ async reload() {
7489
+ await this.view.reload();
7490
+ }
7491
+ async evaluate(fnOrExpr, ...args) {
7492
+ let expr;
7493
+ if (typeof fnOrExpr === "function") {
7494
+ const serializedArgs = args.map((a) => JSON.stringify(a)).join(", ");
7495
+ expr = `(${fnOrExpr.toString()})(${serializedArgs})`;
7496
+ } else {
7497
+ expr = fnOrExpr;
7498
+ }
7499
+ return this.view.evaluate(expr);
7500
+ }
7501
+ async screenshot(opts) {
7502
+ const uint8 = await this.view.screenshot();
7503
+ return Buffer.from(uint8);
7504
+ }
7505
+ async click(selector, opts) {
7506
+ await this.view.click(selector, opts ? { button: opts.button } : undefined);
7507
+ }
7508
+ async type(selector, text, opts) {
7509
+ try {
7510
+ await this.view.click(selector);
7511
+ } catch {}
7512
+ await this.view.type(text);
7513
+ }
7514
+ async fill(selector, value) {
7515
+ await this.view.evaluate(`
7516
+ (() => {
7517
+ const el = document.querySelector(${JSON.stringify(selector)});
7518
+ if (el) { el.value = ''; el.dispatchEvent(new Event('input')); }
7519
+ })()
7520
+ `);
7521
+ await this.type(selector, value);
7522
+ }
7523
+ async press(key, opts) {
7524
+ await this.view.press(key, opts);
7525
+ }
7526
+ async scroll(direction, amount) {
7527
+ const dx = direction === "left" ? -amount : direction === "right" ? amount : 0;
7528
+ const dy = direction === "up" ? -amount : direction === "down" ? amount : 0;
7529
+ await this.view.scroll(dx, dy);
7530
+ }
7531
+ async scrollIntoView(selector) {
7532
+ await this.view.scrollTo(selector);
7533
+ }
7534
+ async hover(selector) {
7535
+ try {
7536
+ await this.view.scrollTo(selector);
7537
+ } catch {}
7538
+ }
7539
+ async resize(width, height) {
7540
+ await this.view.resize(width, height);
7541
+ }
7542
+ async $(selector) {
7543
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
7544
+ if (!exists)
7545
+ return null;
7546
+ return {
7547
+ textContent: async () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`)
7548
+ };
7549
+ }
7550
+ async $$(selector) {
7551
+ const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
7552
+ return Array.from({ length: count }, (_, i) => ({
7553
+ textContent: async () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${i}]?.textContent ?? null`)
7554
+ }));
7555
+ }
7556
+ async inputValue(selector) {
7557
+ return this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.value ?? ''`);
7558
+ }
7559
+ async isChecked(selector) {
7560
+ return this.view.evaluate(`!!(document.querySelector(${JSON.stringify(selector)})?.checked)`);
7561
+ }
7562
+ async isVisible(selector) {
7563
+ return this.view.evaluate(`
7564
+ (() => {
7565
+ const el = document.querySelector(${JSON.stringify(selector)});
7566
+ if (!el) return false;
7567
+ const style = window.getComputedStyle(el);
7568
+ return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
7569
+ })()
7570
+ `);
7571
+ }
7572
+ async isEnabled(selector) {
7573
+ return this.view.evaluate(`!(document.querySelector(${JSON.stringify(selector)})?.disabled)`);
7574
+ }
7575
+ async selectOption(selector, value) {
7576
+ await this.view.evaluate(`
7577
+ (() => {
7578
+ const el = document.querySelector(${JSON.stringify(selector)});
7579
+ if (el) {
7580
+ el.value = ${JSON.stringify(value)};
7581
+ el.dispatchEvent(new Event('change'));
7582
+ }
7583
+ })()
7584
+ `);
7585
+ return [value];
7586
+ }
7587
+ async check(selector) {
7588
+ await this.view.evaluate(`
7589
+ (() => {
7590
+ const el = document.querySelector(${JSON.stringify(selector)});
7591
+ if (el && !el.checked) { el.checked = true; el.dispatchEvent(new Event('change')); }
7592
+ })()
7593
+ `);
7594
+ }
7595
+ async uncheck(selector) {
7596
+ await this.view.evaluate(`
7597
+ (() => {
7598
+ const el = document.querySelector(${JSON.stringify(selector)});
7599
+ if (el && el.checked) { el.checked = false; el.dispatchEvent(new Event('change')); }
7600
+ })()
7601
+ `);
7602
+ }
7603
+ async setInputFiles(selector, files) {
7604
+ throw new Error("File upload not supported in Bun.WebView engine. Use engine: 'playwright' instead.");
7605
+ }
7606
+ getByRole(role, opts) {
7607
+ const name = opts?.name?.toString() ?? "";
7608
+ const selector = name ? `[role="${role}"][aria-label*="${name}"], ${role}[aria-label*="${name}"]` : `[role="${role}"], ${role}`;
7609
+ return {
7610
+ click: (clickOpts) => this.click(selector, clickOpts),
7611
+ fill: (value) => this.fill(selector, value),
7612
+ check: () => this.check(selector),
7613
+ uncheck: () => this.uncheck(selector),
7614
+ isVisible: () => this.isVisible(selector),
7615
+ textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
7616
+ inputValue: () => this.inputValue(selector),
7617
+ first: () => ({
7618
+ click: (clickOpts) => this.click(selector, clickOpts),
7619
+ fill: (value) => this.fill(selector, value),
7620
+ textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
7621
+ isVisible: () => this.isVisible(selector),
7622
+ hover: () => this.hover(selector),
7623
+ boundingBox: async () => null,
7624
+ scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
7625
+ evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
7626
+ waitFor: (opts2) => {
7627
+ return new Promise((resolve, reject) => {
7628
+ const timeout = opts2?.timeout ?? 1e4;
7629
+ const start = Date.now();
7630
+ const check = async () => {
7631
+ const visible = await this.isVisible(selector);
7632
+ if (visible)
7633
+ return resolve();
7634
+ if (Date.now() - start > timeout)
7635
+ return reject(new Error(`Timeout waiting for ${selector}`));
7636
+ setTimeout(check, 100);
7637
+ };
7638
+ check();
7639
+ });
7640
+ }
7641
+ }),
7642
+ count: async () => {
7643
+ const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
7644
+ return count;
7645
+ },
7646
+ nth: (n) => ({
7647
+ click: (clickOpts) => this.click(selector, clickOpts),
7648
+ textContent: () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${n}]?.textContent ?? null`),
7649
+ isVisible: () => this.isVisible(selector)
7650
+ })
7651
+ };
7652
+ }
7653
+ getByText(text, opts) {
7654
+ const selector = opts?.exact ? `*:is(button, a, span, div, p, h1, h2, h3, h4, label)` : "*";
7655
+ return {
7656
+ first: () => ({
7657
+ click: async (clickOpts) => {
7658
+ await this.view.evaluate(`
7659
+ (() => {
7660
+ const text = ${JSON.stringify(text)};
7661
+ const all = document.querySelectorAll('*');
7662
+ for (const el of all) {
7663
+ if (el.children.length === 0 && el.textContent?.trim() === text) {
7664
+ el.click(); return;
7665
+ }
7666
+ }
7667
+ for (const el of all) {
7668
+ if (el.textContent?.includes(text)) { el.click(); return; }
7669
+ }
7670
+ })()
7671
+ `);
7672
+ },
7673
+ waitFor: (waitOpts) => {
7674
+ const timeout = waitOpts?.timeout ?? 1e4;
7675
+ return new Promise((resolve, reject) => {
7676
+ const start = Date.now();
7677
+ const check = async () => {
7678
+ const found = await this.view.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)})`);
7679
+ if (found)
7680
+ return resolve();
7681
+ if (Date.now() - start > timeout)
7682
+ return reject(new Error(`Timeout: text "${text}" not found`));
7683
+ setTimeout(check, 100);
7684
+ };
7685
+ check();
7686
+ });
7687
+ }
7688
+ })
7689
+ };
7690
+ }
7691
+ locator(selector) {
7692
+ return {
7693
+ click: (opts) => this.click(selector, opts),
7694
+ fill: (value) => this.fill(selector, value),
7695
+ scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
7696
+ first: () => this.getByRole("*").first(),
7697
+ evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
7698
+ waitFor: (opts) => {
7699
+ const timeout = opts?.timeout ?? 1e4;
7700
+ return new Promise((resolve, reject) => {
7701
+ const start = Date.now();
7702
+ const check = async () => {
7703
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
7704
+ if (exists)
7705
+ return resolve();
7706
+ if (Date.now() - start > timeout)
7707
+ return reject(new Error(`Timeout: ${selector}`));
7708
+ setTimeout(check, 100);
7709
+ };
7710
+ check();
7711
+ });
7712
+ }
7713
+ };
7714
+ }
7715
+ url() {
7716
+ return this.view.url;
7717
+ }
7718
+ async title() {
7719
+ return this.view.title || await this.evaluate("document.title");
7720
+ }
7721
+ viewportSize() {
7722
+ return { width: 1280, height: 720 };
7723
+ }
7724
+ async waitForLoadState(state, opts) {
7725
+ await new Promise((r) => setTimeout(r, 200));
7726
+ }
7727
+ async waitForURL(pattern, opts) {
7728
+ const timeout = opts?.timeout ?? 30000;
7729
+ const start = Date.now();
7730
+ while (Date.now() - start < timeout) {
7731
+ const url = this.view.url;
7732
+ const matches = pattern instanceof RegExp ? pattern.test(url) : url.includes(pattern);
7733
+ if (matches)
7734
+ return;
7735
+ await new Promise((r) => setTimeout(r, 100));
7736
+ }
7737
+ throw new Error(`Timeout waiting for URL to match ${pattern}`);
7738
+ }
7739
+ async waitForSelector(selector, opts) {
7740
+ const timeout = opts?.timeout ?? 1e4;
7741
+ const start = Date.now();
7742
+ while (Date.now() - start < timeout) {
7743
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
7744
+ if (exists)
7745
+ return;
7746
+ await new Promise((r) => setTimeout(r, 100));
7747
+ }
7748
+ throw new Error(`Timeout waiting for ${selector}`);
7749
+ }
7750
+ async setContent(html) {
7751
+ await this.view.navigate(`data:text/html,${encodeURIComponent(html)}`);
7752
+ await new Promise((r) => setTimeout(r, 100));
7753
+ }
7754
+ async content() {
7755
+ return this.view.evaluate("document.documentElement.outerHTML");
7756
+ }
7757
+ async addInitScript(script) {
7758
+ const expr = typeof script === "function" ? `(${script.toString()})()` : script;
7759
+ await this.view.evaluate(expr);
7760
+ }
7761
+ keyboard = {
7762
+ press: (key) => this.view.press(key)
7763
+ };
7764
+ context() {
7765
+ return {
7766
+ close: async () => {
7767
+ await this.close();
7768
+ },
7769
+ newPage: async () => {
7770
+ throw new Error("Multi-tab not supported in Bun.WebView. Use engine: 'playwright'");
7771
+ },
7772
+ cookies: async () => [],
7773
+ addCookies: async (_) => {},
7774
+ clearCookies: async () => {},
7775
+ newCDPSession: async () => {
7776
+ throw new Error("CDP session via context not available in Bun.WebView. Use view.cdp() when shipped.");
7777
+ },
7778
+ route: async (_pattern, _handler) => {
7779
+ throw new Error("Network interception not supported in Bun.WebView. Use engine: 'cdp' or 'playwright'.");
7780
+ },
7781
+ unrouteAll: async () => {},
7782
+ pages: () => [],
7783
+ addInitScript: async (script) => {
7784
+ await this.addInitScript(script);
7785
+ }
7786
+ };
7787
+ }
7788
+ on(event, handler) {
7789
+ if (!this._eventListeners.has(event))
7790
+ this._eventListeners.set(event, []);
7791
+ this._eventListeners.get(event).push(handler);
7792
+ return this;
7793
+ }
7794
+ off(event, handler) {
7795
+ const listeners = this._eventListeners.get(event) ?? [];
7796
+ this._eventListeners.set(event, listeners.filter((l) => l !== handler));
7797
+ return this;
7798
+ }
7799
+ _emit(event, ...args) {
7800
+ for (const handler of this._eventListeners.get(event) ?? []) {
7801
+ try {
7802
+ handler(...args);
7803
+ } catch {}
7804
+ }
7805
+ }
7806
+ async pdf(_opts) {
7807
+ throw new Error("PDF generation not supported in Bun.WebView. Use engine: 'playwright'.");
7808
+ }
7809
+ coverage = {
7810
+ startJSCoverage: async () => {},
7811
+ stopJSCoverage: async () => [],
7812
+ startCSSCoverage: async () => {},
7813
+ stopCSSCoverage: async () => []
7814
+ };
7815
+ setSessionId(id) {
7816
+ this._sessionId = id;
7817
+ }
7818
+ getSessionId() {
7819
+ return this._sessionId;
7820
+ }
7821
+ getNativeView() {
7822
+ return this.view;
7823
+ }
7824
+ async close() {
7825
+ try {
7826
+ await this.view.close();
7827
+ } catch {}
7828
+ }
7829
+ [Symbol.asyncDispose]() {
7830
+ return this.close();
7831
+ }
7832
+ }
7833
+
7390
7834
  // src/engines/selector.ts
7391
7835
  var ENGINE_MAP = {
7392
- ["scrape" /* SCRAPE */]: "lightpanda",
7393
- ["extract_links" /* EXTRACT_LINKS */]: "lightpanda",
7394
- ["status_check" /* STATUS_CHECK */]: "lightpanda",
7836
+ ["scrape" /* SCRAPE */]: "bun",
7837
+ ["extract_links" /* EXTRACT_LINKS */]: "bun",
7838
+ ["status_check" /* STATUS_CHECK */]: "bun",
7839
+ ["screenshot" /* SCREENSHOT */]: "bun",
7840
+ ["spa_navigate" /* SPA_NAVIGATE */]: "bun",
7395
7841
  ["form_fill" /* FORM_FILL */]: "playwright",
7396
- ["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
7397
- ["screenshot" /* SCREENSHOT */]: "playwright",
7398
7842
  ["auth_flow" /* AUTH_FLOW */]: "playwright",
7399
7843
  ["multi_tab" /* MULTI_TAB */]: "playwright",
7400
7844
  ["record_replay" /* RECORD_REPLAY */]: "playwright",
@@ -7408,6 +7852,14 @@ function selectEngine(useCase, explicit) {
7408
7852
  if (explicit && explicit !== "auto")
7409
7853
  return explicit;
7410
7854
  const preferred = ENGINE_MAP[useCase];
7855
+ if (preferred === "bun") {
7856
+ if (isBunWebViewAvailable())
7857
+ return "bun";
7858
+ if (useCase === "scrape" /* SCRAPE */ || useCase === "extract_links" /* EXTRACT_LINKS */ || useCase === "status_check" /* STATUS_CHECK */) {
7859
+ return isLightpandaAvailable() ? "lightpanda" : "playwright";
7860
+ }
7861
+ return "playwright";
7862
+ }
7411
7863
  if (preferred === "lightpanda" && !isLightpandaAvailable()) {
7412
7864
  return "playwright";
7413
7865
  }
@@ -7416,6 +7868,8 @@ function selectEngine(useCase, explicit) {
7416
7868
  function isEngineAvailable(engine) {
7417
7869
  if (engine === "auto")
7418
7870
  return true;
7871
+ if (engine === "bun")
7872
+ return isBunWebViewAvailable();
7419
7873
  if (engine === "playwright")
7420
7874
  return true;
7421
7875
  if (engine === "cdp")
@@ -7559,6 +8013,7 @@ function startHAR(page) {
7559
8013
  }
7560
8014
 
7561
8015
  // src/lib/console.ts
8016
+ init_console_log();
7562
8017
  function enableConsoleCapture(page, sessionId) {
7563
8018
  const onConsole = (msg) => {
7564
8019
  const levelMap = {
@@ -7708,12 +8163,30 @@ function setupDialogHandler(page, sessionId) {
7708
8163
 
7709
8164
  // src/lib/session.ts
7710
8165
  var handles = new Map;
8166
+ function createBunProxy(view) {
8167
+ return view;
8168
+ }
7711
8169
  async function createSession2(opts = {}) {
7712
8170
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
7713
8171
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
7714
- let browser;
8172
+ let browser = null;
8173
+ let bunView = null;
7715
8174
  let page;
7716
- if (resolvedEngine === "lightpanda") {
8175
+ if (resolvedEngine === "bun") {
8176
+ if (!isBunWebViewAvailable()) {
8177
+ console.warn("[browser] Bun.WebView requested but not available \u2014 falling back to playwright. Run: bun upgrade --canary");
8178
+ browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
8179
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8180
+ } else {
8181
+ bunView = new BunWebViewSession({
8182
+ width: opts.viewport?.width ?? 1280,
8183
+ height: opts.viewport?.height ?? 720,
8184
+ profile: opts.name ?? undefined
8185
+ });
8186
+ if (opts.stealth) {}
8187
+ page = createBunProxy(bunView);
8188
+ }
8189
+ } else if (resolvedEngine === "lightpanda") {
7717
8190
  browser = await connectLightpanda();
7718
8191
  const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
7719
8192
  page = await context.newPage();
@@ -7723,41 +8196,67 @@ async function createSession2(opts = {}) {
7723
8196
  viewport: opts.viewport,
7724
8197
  userAgent: opts.userAgent
7725
8198
  });
7726
- page = await getPage(browser, {
7727
- viewport: opts.viewport,
7728
- userAgent: opts.userAgent
7729
- });
8199
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
7730
8200
  }
8201
+ const sessionName = opts.name ?? (opts.startUrl ? (() => {
8202
+ try {
8203
+ return new URL(opts.startUrl).hostname;
8204
+ } catch {
8205
+ return;
8206
+ }
8207
+ })() : undefined);
7731
8208
  const session = createSession({
7732
- engine: resolvedEngine,
8209
+ engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
7733
8210
  projectId: opts.projectId,
7734
8211
  agentId: opts.agentId,
7735
8212
  startUrl: opts.startUrl,
7736
- name: opts.name ?? (opts.startUrl ? new URL(opts.startUrl).hostname : undefined)
8213
+ name: sessionName
7737
8214
  });
7738
- if (opts.stealth) {
8215
+ if (opts.stealth && !bunView) {
7739
8216
  try {
7740
8217
  await applyStealthPatches(page);
7741
8218
  } catch {}
7742
8219
  }
7743
8220
  const cleanups = [];
7744
- if (opts.captureNetwork !== false) {
7745
- try {
7746
- cleanups.push(enableNetworkLogging(page, session.id));
7747
- } catch {}
7748
- }
7749
- if (opts.captureConsole !== false) {
8221
+ if (!bunView) {
8222
+ if (opts.captureNetwork !== false) {
8223
+ try {
8224
+ cleanups.push(enableNetworkLogging(page, session.id));
8225
+ } catch {}
8226
+ }
8227
+ if (opts.captureConsole !== false) {
8228
+ try {
8229
+ cleanups.push(enableConsoleCapture(page, session.id));
8230
+ } catch {}
8231
+ }
7750
8232
  try {
7751
- cleanups.push(enableConsoleCapture(page, session.id));
8233
+ cleanups.push(setupDialogHandler(page, session.id));
7752
8234
  } catch {}
8235
+ } else {
8236
+ if (opts.captureConsole !== false) {
8237
+ try {
8238
+ const { logConsoleMessage: logConsoleMessage2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
8239
+ await bunView.addInitScript(`
8240
+ (() => {
8241
+ const orig = { log: console.log, warn: console.warn, error: console.error, debug: console.debug, info: console.info };
8242
+ ['log','warn','error','debug','info'].forEach(level => {
8243
+ console[level] = (...args) => {
8244
+ orig[level](...args);
8245
+ };
8246
+ });
8247
+ })()
8248
+ `);
8249
+ } catch {}
8250
+ }
7753
8251
  }
7754
- try {
7755
- cleanups.push(setupDialogHandler(page, session.id));
7756
- } catch {}
7757
- handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
8252
+ handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
7758
8253
  if (opts.startUrl) {
7759
8254
  try {
7760
- await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
8255
+ if (bunView) {
8256
+ await bunView.goto(opts.startUrl);
8257
+ } else {
8258
+ await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
8259
+ }
7761
8260
  } catch {}
7762
8261
  }
7763
8262
  return { session, page };
@@ -7767,17 +8266,29 @@ function getSessionPage(sessionId) {
7767
8266
  if (!handle)
7768
8267
  throw new SessionNotFoundError(sessionId);
7769
8268
  try {
7770
- handle.page.url();
8269
+ if (handle.bunView) {
8270
+ handle.bunView.url();
8271
+ } else {
8272
+ handle.page.url();
8273
+ }
7771
8274
  } catch {
7772
8275
  handles.delete(sessionId);
7773
8276
  throw new SessionNotFoundError(sessionId);
7774
8277
  }
7775
8278
  return handle.page;
7776
8279
  }
8280
+ function getSessionBunView(sessionId) {
8281
+ return handles.get(sessionId)?.bunView ?? null;
8282
+ }
8283
+ function isBunSession(sessionId) {
8284
+ return handles.get(sessionId)?.engine === "bun";
8285
+ }
7777
8286
  function getSessionBrowser(sessionId) {
7778
8287
  const handle = handles.get(sessionId);
7779
8288
  if (!handle)
7780
8289
  throw new SessionNotFoundError(sessionId);
8290
+ if (!handle.browser)
8291
+ throw new BrowserError("This session uses Bun.WebView (no Playwright browser)", "NO_PLAYWRIGHT_BROWSER");
7781
8292
  return handle.browser;
7782
8293
  }
7783
8294
  function getSessionEngine(sessionId) {
@@ -7803,12 +8314,19 @@ async function closeSession2(sessionId) {
7803
8314
  cleanup();
7804
8315
  } catch {}
7805
8316
  }
7806
- try {
7807
- await handle.page.context().close();
7808
- } catch {}
7809
- try {
7810
- await closeBrowser(handle.browser);
7811
- } catch {}
8317
+ if (handle.bunView) {
8318
+ try {
8319
+ await handle.bunView.close();
8320
+ } catch {}
8321
+ } else {
8322
+ try {
8323
+ await handle.page.context().close();
8324
+ } catch {}
8325
+ try {
8326
+ if (handle.browser)
8327
+ await closeBrowser(handle.browser);
8328
+ } catch {}
8329
+ }
7812
8330
  handles.delete(sessionId);
7813
8331
  }
7814
8332
  return closeSession(sessionId);
@@ -8190,9 +8708,19 @@ async function getLinks(page, baseUrl) {
8190
8708
  }, baseUrl ?? page.url());
8191
8709
  }
8192
8710
  async function getTitle(page) {
8711
+ if (typeof page.getNativeView === "function") {
8712
+ const nativeView = page.getNativeView();
8713
+ const t = nativeView?.title;
8714
+ return typeof t === "string" && t ? t : "";
8715
+ }
8193
8716
  return page.title();
8194
8717
  }
8195
8718
  async function getUrl(page) {
8719
+ if (typeof page.getNativeView === "function") {
8720
+ const nativeView = page.getNativeView();
8721
+ const u = nativeView?.url;
8722
+ return typeof u === "string" ? u : "";
8723
+ }
8196
8724
  return page.url();
8197
8725
  }
8198
8726
  async function getMetaTags(page) {
@@ -8399,11 +8927,12 @@ async function startCoverage(page) {
8399
8927
  }
8400
8928
  // src/lib/screenshot.ts
8401
8929
  var import_sharp = __toESM(require_lib(), 1);
8402
- import { join as join2 } from "path";
8403
- import { mkdirSync as mkdirSync2 } from "fs";
8404
- import { homedir as homedir2 } from "os";
8930
+ import { join as join3 } from "path";
8931
+ import { mkdirSync as mkdirSync3 } from "fs";
8932
+ import { homedir as homedir3 } from "os";
8405
8933
 
8406
8934
  // src/db/gallery.ts
8935
+ init_schema();
8407
8936
  import { randomUUID as randomUUID10 } from "crypto";
8408
8937
  function deserialize3(row) {
8409
8938
  return {
@@ -8446,13 +8975,13 @@ function getEntry(id) {
8446
8975
 
8447
8976
  // src/lib/screenshot.ts
8448
8977
  function getDataDir2() {
8449
- return process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
8978
+ return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
8450
8979
  }
8451
8980
  function getScreenshotDir(projectId) {
8452
- const base = join2(getDataDir2(), "screenshots");
8981
+ const base = join3(getDataDir2(), "screenshots");
8453
8982
  const date = new Date().toISOString().split("T")[0];
8454
- const dir = projectId ? join2(base, projectId, date) : join2(base, date);
8455
- mkdirSync2(dir, { recursive: true });
8983
+ const dir = projectId ? join3(base, projectId, date) : join3(base, date);
8984
+ mkdirSync3(dir, { recursive: true });
8456
8985
  return dir;
8457
8986
  }
8458
8987
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -8467,7 +8996,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
8467
8996
  }
8468
8997
  }
8469
8998
  async function generateThumbnail(raw, dir, stem) {
8470
- const thumbPath = join2(dir, `${stem}.thumb.webp`);
8999
+ const thumbPath = join3(dir, `${stem}.thumb.webp`);
8471
9000
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
8472
9001
  await Bun.write(thumbPath, thumbBuffer);
8473
9002
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -8486,27 +9015,45 @@ async function takeScreenshot(page, opts) {
8486
9015
  type: "png"
8487
9016
  };
8488
9017
  let rawBuffer;
9018
+ const isBunView = typeof page.getNativeView === "function";
8489
9019
  if (opts?.selector) {
8490
- const el = await page.$(opts.selector);
8491
- if (!el)
8492
- throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
8493
- rawBuffer = await el.screenshot(rawOpts);
9020
+ if (isBunView) {
9021
+ const uint8 = await page.screenshot();
9022
+ rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
9023
+ } else {
9024
+ const el = await page.$(opts.selector);
9025
+ if (!el)
9026
+ throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
9027
+ rawBuffer = await el.screenshot(rawOpts);
9028
+ }
9029
+ } else if (isBunView) {
9030
+ const uint8 = await page.screenshot();
9031
+ rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
8494
9032
  } else {
8495
9033
  rawBuffer = await page.screenshot(rawOpts);
8496
9034
  }
8497
9035
  const originalSizeBytes = rawBuffer.length;
8498
9036
  let finalBuffer;
8499
- if (compress && format !== "png") {
8500
- finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
8501
- } else if (compress && format === "png") {
8502
- finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
8503
- } else {
9037
+ let compressed = true;
9038
+ let fallback = false;
9039
+ try {
9040
+ if (compress && format !== "png") {
9041
+ finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
9042
+ } else if (compress && format === "png") {
9043
+ finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
9044
+ } else {
9045
+ finalBuffer = rawBuffer;
9046
+ compressed = false;
9047
+ }
9048
+ } catch (sharpErr) {
9049
+ fallback = true;
9050
+ compressed = false;
8504
9051
  finalBuffer = rawBuffer;
8505
9052
  }
8506
9053
  const compressedSizeBytes = finalBuffer.length;
8507
9054
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
8508
9055
  const ext = format;
8509
- const screenshotPath = opts?.path ?? join2(dir, `${stem}.${ext}`);
9056
+ const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
8510
9057
  await Bun.write(screenshotPath, finalBuffer);
8511
9058
  let thumbnailPath;
8512
9059
  let thumbnailBase64;
@@ -8528,7 +9075,8 @@ async function takeScreenshot(page, opts) {
8528
9075
  compressed_size_bytes: compressedSizeBytes,
8529
9076
  compression_ratio: compressionRatio,
8530
9077
  thumbnail_path: thumbnailPath,
8531
- thumbnail_base64: thumbnailBase64
9078
+ thumbnail_base64: thumbnailBase64,
9079
+ ...fallback ? { fallback: true, compressed: false } : {}
8532
9080
  };
8533
9081
  if (opts?.track !== false) {
8534
9082
  try {
@@ -8565,12 +9113,12 @@ async function takeScreenshot(page, opts) {
8565
9113
  }
8566
9114
  async function generatePDF(page, opts) {
8567
9115
  try {
8568
- const base = join2(getDataDir2(), "pdfs");
9116
+ const base = join3(getDataDir2(), "pdfs");
8569
9117
  const date = new Date().toISOString().split("T")[0];
8570
- const dir = opts?.projectId ? join2(base, opts.projectId, date) : join2(base, date);
8571
- mkdirSync2(dir, { recursive: true });
9118
+ const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
9119
+ mkdirSync3(dir, { recursive: true });
8572
9120
  const timestamp = Date.now();
8573
- const pdfPath = opts?.path ?? join2(dir, `${timestamp}.pdf`);
9121
+ const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
8574
9122
  const buffer = await page.pdf({
8575
9123
  path: pdfPath,
8576
9124
  format: opts?.format ?? "A4",
@@ -8942,6 +9490,7 @@ export {
8942
9490
  launchLightpanda,
8943
9491
  isLightpandaAvailable,
8944
9492
  isEngineAvailable,
9493
+ isBunSession,
8945
9494
  isAgentStale,
8946
9495
  inferUseCase,
8947
9496
  hoverRef,
@@ -8961,6 +9510,7 @@ export {
8961
9510
  getSessionPage,
8962
9511
  getSessionEngine,
8963
9512
  getSessionByName2 as getSessionByName,
9513
+ getSessionBunView,
8964
9514
  getSessionBrowser,
8965
9515
  getSession,
8966
9516
  getRecording,