@ulpi/browse 1.3.0 → 1.3.2

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/browse.cjs CHANGED
@@ -62,310 +62,634 @@ var init_constants = __esm({
62
62
  }
63
63
  });
64
64
 
65
- // src/chrome-discover.ts
66
- var chrome_discover_exports = {};
67
- __export(chrome_discover_exports, {
68
- discoverChrome: () => discoverChrome,
69
- fetchWsUrl: () => fetchWsUrl,
70
- findChromeExecutable: () => findChromeExecutable,
71
- getChromeUserDataDir: () => getChromeUserDataDir,
72
- isChromeRunning: () => isChromeRunning,
73
- launchChrome: () => launchChrome
65
+ // src/cookie-import.ts
66
+ var cookie_import_exports = {};
67
+ __export(cookie_import_exports, {
68
+ CookieImportError: () => CookieImportError,
69
+ findInstalledBrowsers: () => findInstalledBrowsers,
70
+ importCookies: () => importCookies,
71
+ listDomains: () => listDomains
74
72
  });
75
- async function fetchWsUrl(port) {
73
+ function findInstalledBrowsers() {
74
+ const appSupport = path2.join(os.homedir(), "Library", "Application Support");
75
+ return BROWSER_REGISTRY.filter((b) => {
76
+ const dbPath = path2.join(appSupport, b.dataDir, "Default", "Cookies");
77
+ try {
78
+ return fs2.existsSync(dbPath);
79
+ } catch {
80
+ return false;
81
+ }
82
+ });
83
+ }
84
+ function listDomains(browserName, profile = "Default") {
85
+ const browser2 = resolveBrowser(browserName);
86
+ const dbPath = getCookieDbPath(browser2, profile);
87
+ const db = openDb(dbPath, browser2.name);
76
88
  try {
77
- const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
78
- signal: AbortSignal.timeout(2e3)
79
- });
80
- if (!res.ok) return null;
81
- const data = await res.json();
82
- return data.webSocketDebuggerUrl ?? null;
83
- } catch {
84
- return null;
89
+ const now = chromiumNow();
90
+ const rows = db.prepare(
91
+ `SELECT host_key AS domain, COUNT(*) AS count
92
+ FROM cookies
93
+ WHERE has_expires = 0 OR expires_utc > ?
94
+ GROUP BY host_key
95
+ ORDER BY count DESC`
96
+ ).all(now);
97
+ return { domains: rows, browser: browser2.name };
98
+ } finally {
99
+ db.close();
85
100
  }
86
101
  }
87
- function readDevToolsPort(filePath) {
102
+ async function importCookies(browserName, domains, profile = "Default") {
103
+ if (domains.length === 0) return { cookies: [], count: 0, failed: 0, domainCounts: {} };
104
+ const browser2 = resolveBrowser(browserName);
105
+ const derivedKey = await getDerivedKey(browser2);
106
+ const dbPath = getCookieDbPath(browser2, profile);
107
+ const db = openDb(dbPath, browser2.name);
88
108
  try {
89
- const content = fs2.readFileSync(filePath, "utf-8");
90
- const port = parseInt(content.split("\n")[0], 10);
91
- return Number.isFinite(port) && port > 0 ? port : null;
92
- } catch {
93
- return null;
109
+ const now = chromiumNow();
110
+ const placeholders = domains.map(() => "?").join(",");
111
+ const rows = db.prepare(
112
+ `SELECT host_key, name, value, encrypted_value, path, expires_utc,
113
+ is_secure, is_httponly, has_expires, samesite
114
+ FROM cookies
115
+ WHERE host_key IN (${placeholders})
116
+ AND (has_expires = 0 OR expires_utc > ?)
117
+ ORDER BY host_key, name`
118
+ ).all(...domains, now);
119
+ const cookies = [];
120
+ let failed = 0;
121
+ const domainCounts = {};
122
+ for (const row of rows) {
123
+ try {
124
+ const value = decryptCookieValue(row, derivedKey);
125
+ const cookie = toPlaywrightCookie(row, value);
126
+ cookies.push(cookie);
127
+ domainCounts[row.host_key] = (domainCounts[row.host_key] || 0) + 1;
128
+ } catch {
129
+ failed++;
130
+ }
131
+ }
132
+ return { cookies, count: cookies.length, failed, domainCounts };
133
+ } finally {
134
+ db.close();
94
135
  }
95
136
  }
96
- async function discoverChrome() {
97
- const home = os.homedir();
98
- for (const profile of PROFILE_PATHS) {
99
- const filePath = process.platform === "darwin" ? path2.join(home, "Library", "Application Support", profile, "DevToolsActivePort") : path2.join(home, ".config", profile.toLowerCase().replace(/ /g, "-"), "DevToolsActivePort");
100
- const port = readDevToolsPort(filePath);
101
- if (port) {
102
- const wsUrl = await fetchWsUrl(port);
103
- if (wsUrl) return wsUrl;
104
- }
137
+ function resolveBrowser(nameOrAlias) {
138
+ const needle = nameOrAlias.toLowerCase().trim();
139
+ const found = BROWSER_REGISTRY.find(
140
+ (b) => b.aliases.includes(needle) || b.name.toLowerCase() === needle
141
+ );
142
+ if (!found) {
143
+ const supported = BROWSER_REGISTRY.flatMap((b) => b.aliases).join(", ");
144
+ throw new CookieImportError(
145
+ `Unknown browser '${nameOrAlias}'. Supported: ${supported}`,
146
+ "unknown_browser"
147
+ );
105
148
  }
106
- for (const port of PROBE_PORTS) {
107
- const wsUrl = await fetchWsUrl(port);
108
- if (wsUrl) return wsUrl;
149
+ return found;
150
+ }
151
+ function validateProfile(profile) {
152
+ if (/[/\\]|\.\./.test(profile) || /[\x00-\x1f]/.test(profile)) {
153
+ throw new CookieImportError(
154
+ `Invalid profile name: '${profile}'`,
155
+ "bad_request"
156
+ );
109
157
  }
110
- return null;
111
158
  }
112
- function findChromeExecutable() {
113
- const envPath = process.env.BROWSE_CHROME_PATH;
114
- if (envPath) {
115
- if (fs2.existsSync(envPath)) return envPath;
116
- return null;
159
+ function getCookieDbPath(browser2, profile) {
160
+ validateProfile(profile);
161
+ const appSupport = path2.join(os.homedir(), "Library", "Application Support");
162
+ const dbPath = path2.join(appSupport, browser2.dataDir, profile, "Cookies");
163
+ if (!fs2.existsSync(dbPath)) {
164
+ throw new CookieImportError(
165
+ `${browser2.name} is not installed (no cookie database at ${dbPath})`,
166
+ "not_installed"
167
+ );
117
168
  }
118
- if (process.platform === "darwin") {
119
- const candidates = [
120
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
121
- "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
122
- ];
123
- for (const c of candidates) {
124
- if (fs2.existsSync(c)) return c;
169
+ return dbPath;
170
+ }
171
+ function openDb(dbPath, browserName) {
172
+ try {
173
+ return new import_better_sqlite3.default(dbPath, { readonly: true });
174
+ } catch (err) {
175
+ if (err.message?.includes("SQLITE_BUSY") || err.message?.includes("database is locked")) {
176
+ return openDbFromCopy(dbPath, browserName);
125
177
  }
126
- } else if (process.platform === "linux") {
127
- const names = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
128
- for (const name of names) {
129
- try {
130
- const result = (0, import_child_process.execSync)(`which ${name}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
131
- if (result) return result;
132
- } catch {
133
- }
178
+ if (err.message?.includes("SQLITE_CORRUPT") || err.message?.includes("malformed")) {
179
+ throw new CookieImportError(
180
+ "Cookie database is corrupt",
181
+ "db_corrupt"
182
+ );
134
183
  }
184
+ throw err;
135
185
  }
136
- return null;
137
186
  }
138
- function isChromeRunning() {
139
- if (process.platform !== "darwin" && process.platform !== "linux") {
140
- return { running: false, hasDebugPort: false };
141
- }
187
+ function openDbFromCopy(dbPath, browserName) {
188
+ const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto.randomUUID()}.db`;
142
189
  try {
143
- const pgrepPattern = process.platform === "darwin" ? "Google Chrome" : "google-chrome|chromium";
144
- const pids = (0, import_child_process.execSync)(`pgrep -f "${pgrepPattern}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
145
- if (pids.length === 0) return { running: false, hasDebugPort: false };
146
- for (const pid of pids) {
190
+ fs2.copyFileSync(dbPath, tmpPath);
191
+ const walPath = dbPath + "-wal";
192
+ const shmPath = dbPath + "-shm";
193
+ if (fs2.existsSync(walPath)) fs2.copyFileSync(walPath, tmpPath + "-wal");
194
+ if (fs2.existsSync(shmPath)) fs2.copyFileSync(shmPath, tmpPath + "-shm");
195
+ const db = new import_better_sqlite3.default(tmpPath, { readonly: true });
196
+ const origClose = db.close.bind(db);
197
+ db.close = (() => {
198
+ origClose();
147
199
  try {
148
- const cmdline = (0, import_child_process.execSync)(`ps -p ${pid} -o command=`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
149
- const match = cmdline.match(/--remote-debugging-port=(\d+)/);
150
- if (match) {
151
- return { running: true, hasDebugPort: true, debugPort: parseInt(match[1], 10) };
152
- }
200
+ fs2.unlinkSync(tmpPath);
153
201
  } catch {
154
202
  }
155
- }
156
- return { running: true, hasDebugPort: false };
157
- } catch {
158
- return { running: false, hasDebugPort: false };
159
- }
160
- }
161
- function getChromeUserDataDir() {
162
- const home = os.homedir();
163
- if (process.platform === "darwin") {
164
- const dir = path2.join(home, "Library", "Application Support", "Google", "Chrome");
165
- if (fs2.existsSync(dir)) return dir;
166
- } else if (process.platform === "linux") {
167
- const dir = path2.join(home, ".config", "google-chrome");
168
- if (fs2.existsSync(dir)) return dir;
169
- }
170
- return null;
171
- }
172
- function isPortFree(port) {
173
- return new Promise((resolve4) => {
174
- const srv = net.createServer();
175
- srv.once("error", () => resolve4(false));
176
- srv.once("listening", () => {
177
- srv.close(() => resolve4(true));
178
- });
179
- srv.listen(port, "127.0.0.1");
180
- });
181
- }
182
- async function pickDebugPort() {
183
- if (await isPortFree(DEFAULTS.CHROME_DEBUG_PORT)) return DEFAULTS.CHROME_DEBUG_PORT;
184
- for (let port = DEFAULTS.PORT_RANGE_START; port <= DEFAULTS.PORT_RANGE_END; port++) {
185
- if (await isPortFree(port)) return port;
186
- }
187
- throw new Error("No available port for Chrome debug port");
188
- }
189
- function cleanStaleLock(dataDir) {
190
- const lockFiles = ["SingletonLock", "lockfile"];
191
- for (const name of lockFiles) {
192
- const lockPath = path2.join(dataDir, name);
193
- try {
194
- const target = fs2.readlinkSync(lockPath);
195
- const pidMatch = target.match(/-(\d+)$/);
196
- if (pidMatch) {
197
- const pid = parseInt(pidMatch[1], 10);
198
- try {
199
- process.kill(pid, 0);
200
- } catch {
201
- fs2.unlinkSync(lockPath);
202
- }
203
+ try {
204
+ fs2.unlinkSync(tmpPath + "-wal");
205
+ } catch {
203
206
  }
204
- } catch {
205
207
  try {
206
- fs2.unlinkSync(lockPath);
208
+ fs2.unlinkSync(tmpPath + "-shm");
207
209
  } catch {
208
210
  }
211
+ });
212
+ return db;
213
+ } catch {
214
+ try {
215
+ fs2.unlinkSync(tmpPath);
216
+ } catch {
209
217
  }
210
- }
211
- }
212
- async function launchChrome() {
213
- const chromePath = findChromeExecutable();
214
- if (!chromePath) {
215
- throw new Error(
216
- "Chrome not found. Install Google Chrome or set BROWSE_CHROME_PATH environment variable."
218
+ throw new CookieImportError(
219
+ `Cookie database is locked (${browserName} may be running). Try closing ${browserName} first.`,
220
+ "db_locked",
221
+ "retry"
217
222
  );
218
223
  }
219
- const status = isChromeRunning();
220
- if (status.running && status.hasDebugPort && status.debugPort) {
221
- const wsUrl2 = await fetchWsUrl(status.debugPort);
222
- if (wsUrl2) {
223
- const pw2 = await import("playwright");
224
- const browser3 = await pw2.chromium.connectOverCDP(wsUrl2);
225
- return { browser: browser3, child: null, close: async () => {
226
- } };
227
- }
228
- }
229
- const port = await pickDebugPort();
230
- let dataDir;
231
- const localDir = process.env.BROWSE_LOCAL_DIR || ".browse";
232
- dataDir = path2.join(localDir, "chrome-data");
233
- fs2.mkdirSync(dataDir, { recursive: true });
234
- cleanStaleLock(dataDir);
235
- const chromeArgs = [
236
- `--remote-debugging-port=${port}`,
237
- "--no-first-run",
238
- "--no-default-browser-check",
239
- `--user-data-dir=${dataDir}`
240
- ];
241
- const child = (0, import_child_process.spawn)(chromePath, chromeArgs, {
242
- stdio: ["ignore", "pipe", "pipe"],
243
- detached: false
244
- });
245
- let stderrData = "";
246
- if (child.stderr) {
247
- child.stderr.setEncoding("utf8");
248
- child.stderr.on("data", (chunk) => {
249
- stderrData += chunk;
250
- });
251
- }
252
- const deadline = Date.now() + DEFAULTS.CHROME_CDP_TIMEOUT_MS;
253
- let wsUrl;
254
- while (Date.now() < deadline) {
255
- if (child.exitCode !== null) {
256
- throw new Error(
257
- `Chrome exited before CDP became ready (exit code: ${child.exitCode})` + (stderrData ? `
258
- stderr: ${stderrData.slice(0, 2e3)}` : "")
224
+ }
225
+ async function getDerivedKey(browser2) {
226
+ const cached = keyCache.get(browser2.keychainService);
227
+ if (cached) return cached;
228
+ const password = await getKeychainPassword(browser2.keychainService);
229
+ const derived = crypto.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
230
+ keyCache.set(browser2.keychainService, derived);
231
+ return derived;
232
+ }
233
+ async function getKeychainPassword(service) {
234
+ const proc = (0, import_child_process.spawn)("security", ["find-generic-password", "-s", service, "-w"], {
235
+ stdio: ["ignore", "pipe", "pipe"]
236
+ });
237
+ let stdout = "";
238
+ let stderr = "";
239
+ proc.stdout.setEncoding("utf8");
240
+ proc.stderr.setEncoding("utf8");
241
+ proc.stdout.on("data", (chunk) => {
242
+ stdout += chunk;
243
+ });
244
+ proc.stderr.on("data", (chunk) => {
245
+ stderr += chunk;
246
+ });
247
+ const exitPromise = new Promise(
248
+ (resolve4) => proc.on("close", (code) => resolve4(code))
249
+ );
250
+ const timeout = new Promise(
251
+ (_, reject) => setTimeout(() => {
252
+ proc.kill();
253
+ reject(new CookieImportError(
254
+ `macOS is waiting for Keychain permission. Look for a dialog asking to allow access to "${service}".`,
255
+ "keychain_timeout",
256
+ "retry"
257
+ ));
258
+ }, 1e4)
259
+ );
260
+ try {
261
+ const exitCode = await Promise.race([exitPromise, timeout]);
262
+ if (exitCode !== 0) {
263
+ const errText = stderr.trim().toLowerCase();
264
+ if (errText.includes("user canceled") || errText.includes("denied") || errText.includes("interaction not allowed")) {
265
+ throw new CookieImportError(
266
+ `Keychain access denied. Click Allow in the macOS dialog for "${service}".`,
267
+ "keychain_denied",
268
+ "retry"
269
+ );
270
+ }
271
+ if (errText.includes("could not be found") || errText.includes("not found")) {
272
+ throw new CookieImportError(
273
+ `No Keychain entry for "${service}".`,
274
+ "keychain_not_found"
275
+ );
276
+ }
277
+ throw new CookieImportError(
278
+ `Could not read Keychain: ${stderr.trim()}`,
279
+ "keychain_error",
280
+ "retry"
259
281
  );
260
282
  }
261
- const url = await fetchWsUrl(port);
262
- if (url) {
263
- wsUrl = url;
264
- break;
265
- }
266
- await new Promise((r) => setTimeout(r, 200));
283
+ return stdout.trim();
284
+ } catch (err) {
285
+ if (err instanceof CookieImportError) throw err;
286
+ throw new CookieImportError(
287
+ `Could not read Keychain: ${err.message}`,
288
+ "keychain_error",
289
+ "retry"
290
+ );
267
291
  }
268
- if (!wsUrl) {
269
- child.kill();
270
- throw new Error(`Chrome failed to start on port ${port} within ${DEFAULTS.CHROME_CDP_TIMEOUT_MS / 1e3} seconds`);
292
+ }
293
+ function decryptCookieValue(row, key) {
294
+ if (row.value && row.value.length > 0) return row.value;
295
+ const ev = Buffer.from(row.encrypted_value);
296
+ if (ev.length === 0) return "";
297
+ const prefix = ev.slice(0, 3).toString("utf-8");
298
+ if (prefix !== "v10") {
299
+ throw new Error(`Unknown encryption prefix: ${prefix}`);
271
300
  }
272
- const pw = await import("playwright");
273
- const browser2 = await pw.chromium.connectOverCDP(wsUrl);
301
+ const ciphertext = ev.slice(3);
302
+ const iv = Buffer.alloc(16, 32);
303
+ const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
304
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
305
+ if (plaintext.length <= 32) return "";
306
+ return plaintext.slice(32).toString("utf-8");
307
+ }
308
+ function toPlaywrightCookie(row, value) {
274
309
  return {
275
- browser: browser2,
276
- child,
277
- close: async () => {
278
- await browser2.close().catch(() => {
279
- });
280
- child.kill("SIGTERM");
281
- }
310
+ name: row.name,
311
+ value,
312
+ domain: row.host_key,
313
+ path: row.path || "/",
314
+ expires: chromiumEpochToUnix(row.expires_utc, row.has_expires),
315
+ secure: row.is_secure === 1,
316
+ httpOnly: row.is_httponly === 1,
317
+ sameSite: mapSameSite(row.samesite)
282
318
  };
283
319
  }
284
- var os, fs2, path2, net, import_child_process, PROFILE_PATHS, PROBE_PORTS;
285
- var init_chrome_discover = __esm({
286
- "src/chrome-discover.ts"() {
320
+ function chromiumNow() {
321
+ return BigInt(Date.now()) * 1000n + CHROMIUM_EPOCH_OFFSET;
322
+ }
323
+ function chromiumEpochToUnix(epoch, hasExpires) {
324
+ if (hasExpires === 0 || epoch === 0 || epoch === 0n) return -1;
325
+ const epochBig = BigInt(epoch);
326
+ const unixMicro = epochBig - CHROMIUM_EPOCH_OFFSET;
327
+ return Number(unixMicro / 1000000n);
328
+ }
329
+ function mapSameSite(value) {
330
+ switch (value) {
331
+ case 0:
332
+ return "None";
333
+ case 1:
334
+ return "Lax";
335
+ case 2:
336
+ return "Strict";
337
+ default:
338
+ return "Lax";
339
+ }
340
+ }
341
+ var import_better_sqlite3, import_child_process, crypto, fs2, path2, os, CookieImportError, BROWSER_REGISTRY, keyCache, CHROMIUM_EPOCH_OFFSET;
342
+ var init_cookie_import = __esm({
343
+ "src/cookie-import.ts"() {
287
344
  "use strict";
288
- os = __toESM(require("os"), 1);
345
+ import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
346
+ import_child_process = require("child_process");
347
+ crypto = __toESM(require("crypto"), 1);
289
348
  fs2 = __toESM(require("fs"), 1);
290
349
  path2 = __toESM(require("path"), 1);
291
- net = __toESM(require("net"), 1);
292
- import_child_process = require("child_process");
293
- init_constants();
294
- PROFILE_PATHS = [
295
- "Google/Chrome",
296
- "Arc/User Data",
297
- "BraveSoftware/Brave-Browser",
298
- "Microsoft Edge"
350
+ os = __toESM(require("os"), 1);
351
+ CookieImportError = class extends Error {
352
+ constructor(message, code, action) {
353
+ super(message);
354
+ this.code = code;
355
+ this.action = action;
356
+ this.name = "CookieImportError";
357
+ }
358
+ };
359
+ BROWSER_REGISTRY = [
360
+ { name: "Chrome", dataDir: "Google/Chrome/", keychainService: "Chrome Safe Storage", aliases: ["chrome", "google-chrome"] },
361
+ { name: "Arc", dataDir: "Arc/User Data/", keychainService: "Arc Safe Storage", aliases: ["arc"] },
362
+ { name: "Brave", dataDir: "BraveSoftware/Brave-Browser/", keychainService: "Brave Safe Storage", aliases: ["brave"] },
363
+ { name: "Edge", dataDir: "Microsoft Edge/", keychainService: "Microsoft Edge Safe Storage", aliases: ["edge"] }
299
364
  ];
300
- PROBE_PORTS = [9222, 9229];
365
+ keyCache = /* @__PURE__ */ new Map();
366
+ CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
301
367
  }
302
368
  });
303
369
 
304
- // src/encryption.ts
305
- function resolveEncryptionKey(localDir) {
306
- const envKey = process.env.BROWSE_ENCRYPTION_KEY;
307
- if (envKey) {
308
- if (envKey.length !== 64) {
309
- throw new Error("BROWSE_ENCRYPTION_KEY must be 64 hex characters (32 bytes)");
310
- }
311
- return Buffer.from(envKey, "hex");
312
- }
313
- const keyPath = path3.join(localDir, ".encryption-key");
314
- if (fs3.existsSync(keyPath)) {
315
- const hex = fs3.readFileSync(keyPath, "utf-8").trim();
316
- return Buffer.from(hex, "hex");
370
+ // src/chrome-discover.ts
371
+ var chrome_discover_exports = {};
372
+ __export(chrome_discover_exports, {
373
+ discoverChrome: () => discoverChrome,
374
+ fetchWsUrl: () => fetchWsUrl,
375
+ findChromeExecutable: () => findChromeExecutable,
376
+ getChromeUserDataDir: () => getChromeUserDataDir,
377
+ isChromeRunning: () => isChromeRunning,
378
+ launchChrome: () => launchChrome
379
+ });
380
+ async function fetchWsUrl(port) {
381
+ try {
382
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
383
+ signal: AbortSignal.timeout(2e3)
384
+ });
385
+ if (!res.ok) return null;
386
+ const data = await res.json();
387
+ return data.webSocketDebuggerUrl ?? null;
388
+ } catch {
389
+ return null;
317
390
  }
318
- const key = crypto.randomBytes(32);
319
- fs3.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
320
- return key;
321
- }
322
- function encrypt(plaintext, key) {
323
- const iv = crypto.randomBytes(12);
324
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
325
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
326
- return {
327
- ciphertext: encrypted.toString("base64"),
328
- iv: iv.toString("base64"),
329
- authTag: cipher.getAuthTag().toString("base64")
330
- };
331
391
  }
332
- function decrypt(ciphertext, iv, authTag, key) {
333
- const decipher = crypto.createDecipheriv(
334
- "aes-256-gcm",
335
- key,
336
- Buffer.from(iv, "base64")
337
- );
338
- decipher.setAuthTag(Buffer.from(authTag, "base64"));
339
- const decrypted = Buffer.concat([
340
- decipher.update(Buffer.from(ciphertext, "base64")),
341
- decipher.final()
342
- ]);
343
- return decrypted.toString("utf-8");
392
+ function readDevToolsPort(filePath) {
393
+ try {
394
+ const content = fs3.readFileSync(filePath, "utf-8");
395
+ const port = parseInt(content.split("\n")[0], 10);
396
+ return Number.isFinite(port) && port > 0 ? port : null;
397
+ } catch {
398
+ return null;
399
+ }
344
400
  }
345
- var crypto, fs3, path3;
346
- var init_encryption = __esm({
347
- "src/encryption.ts"() {
348
- "use strict";
349
- crypto = __toESM(require("crypto"), 1);
350
- fs3 = __toESM(require("fs"), 1);
351
- path3 = __toESM(require("path"), 1);
401
+ async function discoverChrome() {
402
+ const home = os2.homedir();
403
+ for (const profile of PROFILE_PATHS) {
404
+ const filePath = process.platform === "darwin" ? path3.join(home, "Library", "Application Support", profile, "DevToolsActivePort") : path3.join(home, ".config", profile.toLowerCase().replace(/ /g, "-"), "DevToolsActivePort");
405
+ const port = readDevToolsPort(filePath);
406
+ if (port) {
407
+ const wsUrl = await fetchWsUrl(port);
408
+ if (wsUrl) return wsUrl;
409
+ }
352
410
  }
353
- });
354
-
355
- // src/sanitize.ts
356
- function sanitizeName(name) {
357
- const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
358
- if (!sanitized || /^[._]+$/.test(sanitized)) {
359
- throw new Error(`Invalid name: "${name}"`);
411
+ for (const port of PROBE_PORTS) {
412
+ const wsUrl = await fetchWsUrl(port);
413
+ if (wsUrl) return wsUrl;
360
414
  }
361
- return sanitized;
415
+ return null;
362
416
  }
363
- var init_sanitize = __esm({
364
- "src/sanitize.ts"() {
365
- "use strict";
366
- }
367
- });
368
-
417
+ function findChromeExecutable() {
418
+ const envPath = process.env.BROWSE_CHROME_PATH;
419
+ if (envPath) {
420
+ if (fs3.existsSync(envPath)) return envPath;
421
+ return null;
422
+ }
423
+ if (process.platform === "darwin") {
424
+ const candidates = [
425
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
426
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
427
+ ];
428
+ for (const c of candidates) {
429
+ if (fs3.existsSync(c)) return c;
430
+ }
431
+ } else if (process.platform === "linux") {
432
+ const names = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
433
+ for (const name of names) {
434
+ try {
435
+ const result = (0, import_child_process2.execSync)(`which ${name}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
436
+ if (result) return result;
437
+ } catch {
438
+ }
439
+ }
440
+ }
441
+ return null;
442
+ }
443
+ function isChromeRunning() {
444
+ if (process.platform !== "darwin" && process.platform !== "linux") {
445
+ return { running: false, hasDebugPort: false };
446
+ }
447
+ try {
448
+ const pgrepPattern = process.platform === "darwin" ? "Google Chrome" : "google-chrome|chromium";
449
+ const pids = (0, import_child_process2.execSync)(`pgrep -f "${pgrepPattern}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
450
+ if (pids.length === 0) return { running: false, hasDebugPort: false };
451
+ for (const pid of pids) {
452
+ try {
453
+ const cmdline = (0, import_child_process2.execSync)(`ps -p ${pid} -o command=`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
454
+ const match = cmdline.match(/--remote-debugging-port=(\d+)/);
455
+ if (match) {
456
+ return { running: true, hasDebugPort: true, debugPort: parseInt(match[1], 10) };
457
+ }
458
+ } catch {
459
+ }
460
+ }
461
+ return { running: true, hasDebugPort: false };
462
+ } catch {
463
+ return { running: false, hasDebugPort: false };
464
+ }
465
+ }
466
+ function getChromeUserDataDir() {
467
+ const home = os2.homedir();
468
+ if (process.platform === "darwin") {
469
+ const dir = path3.join(home, "Library", "Application Support", "Google", "Chrome");
470
+ if (fs3.existsSync(dir)) return dir;
471
+ } else if (process.platform === "linux") {
472
+ const dir = path3.join(home, ".config", "google-chrome");
473
+ if (fs3.existsSync(dir)) return dir;
474
+ }
475
+ return null;
476
+ }
477
+ function isPortFree(port) {
478
+ return new Promise((resolve4) => {
479
+ const srv = net.createServer();
480
+ srv.once("error", () => resolve4(false));
481
+ srv.once("listening", () => {
482
+ srv.close(() => resolve4(true));
483
+ });
484
+ srv.listen(port, "127.0.0.1");
485
+ });
486
+ }
487
+ async function pickDebugPort() {
488
+ if (await isPortFree(DEFAULTS.CHROME_DEBUG_PORT)) return DEFAULTS.CHROME_DEBUG_PORT;
489
+ for (let port = DEFAULTS.PORT_RANGE_START; port <= DEFAULTS.PORT_RANGE_END; port++) {
490
+ if (await isPortFree(port)) return port;
491
+ }
492
+ throw new Error("No available port for Chrome debug port");
493
+ }
494
+ function cleanStaleLock(dataDir) {
495
+ const lockFiles = ["SingletonLock", "lockfile"];
496
+ for (const name of lockFiles) {
497
+ const lockPath = path3.join(dataDir, name);
498
+ try {
499
+ const target = fs3.readlinkSync(lockPath);
500
+ const pidMatch = target.match(/-(\d+)$/);
501
+ if (pidMatch) {
502
+ const pid = parseInt(pidMatch[1], 10);
503
+ try {
504
+ process.kill(pid, 0);
505
+ } catch {
506
+ fs3.unlinkSync(lockPath);
507
+ }
508
+ }
509
+ } catch {
510
+ try {
511
+ fs3.unlinkSync(lockPath);
512
+ } catch {
513
+ }
514
+ }
515
+ }
516
+ }
517
+ async function launchChrome() {
518
+ const chromePath = findChromeExecutable();
519
+ if (!chromePath) {
520
+ throw new Error(
521
+ "Chrome not found. Install Google Chrome or set BROWSE_CHROME_PATH environment variable."
522
+ );
523
+ }
524
+ const status = isChromeRunning();
525
+ if (status.running && status.hasDebugPort && status.debugPort) {
526
+ const wsUrl2 = await fetchWsUrl(status.debugPort);
527
+ if (wsUrl2) {
528
+ const pw2 = await import("playwright");
529
+ const browser3 = await pw2.chromium.connectOverCDP(wsUrl2);
530
+ return { browser: browser3, child: null, close: async () => {
531
+ } };
532
+ }
533
+ }
534
+ const port = await pickDebugPort();
535
+ const localDir = process.env.BROWSE_LOCAL_DIR || ".browse";
536
+ const dataDir = path3.join(localDir, "chrome-data");
537
+ const realDataDir = getChromeUserDataDir();
538
+ fs3.mkdirSync(dataDir, { recursive: true });
539
+ cleanStaleLock(dataDir);
540
+ const chromeArgs = [
541
+ `--remote-debugging-port=${port}`,
542
+ "--no-first-run",
543
+ "--no-default-browser-check",
544
+ `--user-data-dir=${dataDir}`
545
+ ];
546
+ const child = (0, import_child_process2.spawn)(chromePath, chromeArgs, {
547
+ stdio: ["ignore", "pipe", "pipe"],
548
+ detached: false
549
+ });
550
+ let stderrData = "";
551
+ if (child.stderr) {
552
+ child.stderr.setEncoding("utf8");
553
+ child.stderr.on("data", (chunk) => {
554
+ stderrData += chunk;
555
+ });
556
+ }
557
+ const deadline = Date.now() + DEFAULTS.CHROME_CDP_TIMEOUT_MS;
558
+ let wsUrl;
559
+ while (Date.now() < deadline) {
560
+ if (child.exitCode !== null) {
561
+ throw new Error(
562
+ `Chrome exited before CDP became ready (exit code: ${child.exitCode})` + (stderrData ? `
563
+ stderr: ${stderrData.slice(0, 2e3)}` : "")
564
+ );
565
+ }
566
+ const url = await fetchWsUrl(port);
567
+ if (url) {
568
+ wsUrl = url;
569
+ break;
570
+ }
571
+ await new Promise((r) => setTimeout(r, 200));
572
+ }
573
+ if (!wsUrl) {
574
+ child.kill();
575
+ throw new Error(`Chrome failed to start on port ${port} within ${DEFAULTS.CHROME_CDP_TIMEOUT_MS / 1e3} seconds`);
576
+ }
577
+ const pw = await import("playwright");
578
+ const browser2 = await pw.chromium.connectOverCDP(wsUrl);
579
+ if (realDataDir) {
580
+ try {
581
+ const { importCookies: importCookies2, listDomains: listDomains2 } = await Promise.resolve().then(() => (init_cookie_import(), cookie_import_exports));
582
+ const { domains } = listDomains2("chrome");
583
+ if (domains.length > 0) {
584
+ const allDomains = domains.map((d) => d.domain);
585
+ const result = await importCookies2("chrome", allDomains);
586
+ if (result.cookies.length > 0) {
587
+ const context = browser2.contexts()[0];
588
+ if (context) {
589
+ await context.addCookies(result.cookies);
590
+ console.log(`[browse] Imported ${result.cookies.length} cookies from Chrome.`);
591
+ }
592
+ }
593
+ }
594
+ } catch (err) {
595
+ console.log(`[browse] Cookie import skipped: ${err.message}`);
596
+ }
597
+ }
598
+ return {
599
+ browser: browser2,
600
+ child,
601
+ close: async () => {
602
+ await browser2.close().catch(() => {
603
+ });
604
+ child.kill("SIGTERM");
605
+ }
606
+ };
607
+ }
608
+ var os2, fs3, path3, net, import_child_process2, PROFILE_PATHS, PROBE_PORTS;
609
+ var init_chrome_discover = __esm({
610
+ "src/chrome-discover.ts"() {
611
+ "use strict";
612
+ os2 = __toESM(require("os"), 1);
613
+ fs3 = __toESM(require("fs"), 1);
614
+ path3 = __toESM(require("path"), 1);
615
+ net = __toESM(require("net"), 1);
616
+ import_child_process2 = require("child_process");
617
+ init_constants();
618
+ PROFILE_PATHS = [
619
+ "Google/Chrome",
620
+ "Arc/User Data",
621
+ "BraveSoftware/Brave-Browser",
622
+ "Microsoft Edge"
623
+ ];
624
+ PROBE_PORTS = [9222, 9229];
625
+ }
626
+ });
627
+
628
+ // src/encryption.ts
629
+ function resolveEncryptionKey(localDir) {
630
+ const envKey = process.env.BROWSE_ENCRYPTION_KEY;
631
+ if (envKey) {
632
+ if (envKey.length !== 64) {
633
+ throw new Error("BROWSE_ENCRYPTION_KEY must be 64 hex characters (32 bytes)");
634
+ }
635
+ return Buffer.from(envKey, "hex");
636
+ }
637
+ const keyPath = path4.join(localDir, ".encryption-key");
638
+ if (fs4.existsSync(keyPath)) {
639
+ const hex = fs4.readFileSync(keyPath, "utf-8").trim();
640
+ return Buffer.from(hex, "hex");
641
+ }
642
+ const key = crypto2.randomBytes(32);
643
+ fs4.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
644
+ return key;
645
+ }
646
+ function encrypt(plaintext, key) {
647
+ const iv = crypto2.randomBytes(12);
648
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, iv);
649
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
650
+ return {
651
+ ciphertext: encrypted.toString("base64"),
652
+ iv: iv.toString("base64"),
653
+ authTag: cipher.getAuthTag().toString("base64")
654
+ };
655
+ }
656
+ function decrypt(ciphertext, iv, authTag, key) {
657
+ const decipher = crypto2.createDecipheriv(
658
+ "aes-256-gcm",
659
+ key,
660
+ Buffer.from(iv, "base64")
661
+ );
662
+ decipher.setAuthTag(Buffer.from(authTag, "base64"));
663
+ const decrypted = Buffer.concat([
664
+ decipher.update(Buffer.from(ciphertext, "base64")),
665
+ decipher.final()
666
+ ]);
667
+ return decrypted.toString("utf-8");
668
+ }
669
+ var crypto2, fs4, path4;
670
+ var init_encryption = __esm({
671
+ "src/encryption.ts"() {
672
+ "use strict";
673
+ crypto2 = __toESM(require("crypto"), 1);
674
+ fs4 = __toESM(require("fs"), 1);
675
+ path4 = __toESM(require("path"), 1);
676
+ }
677
+ });
678
+
679
+ // src/sanitize.ts
680
+ function sanitizeName(name) {
681
+ const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
682
+ if (!sanitized || /^[._]+$/.test(sanitized)) {
683
+ throw new Error(`Invalid name: "${name}"`);
684
+ }
685
+ return sanitized;
686
+ }
687
+ var init_sanitize = __esm({
688
+ "src/sanitize.ts"() {
689
+ "use strict";
690
+ }
691
+ });
692
+
369
693
  // src/cloud-providers.ts
370
694
  var cloud_providers_exports = {};
371
695
  __export(cloud_providers_exports, {
@@ -400,12 +724,12 @@ async function cleanupProvider(providerName, apiKey, sessionId) {
400
724
  await provider.cleanup(apiKey, sessionId);
401
725
  }
402
726
  }
403
- var fs4, path4, providers, ProviderVault;
727
+ var fs5, path5, providers, ProviderVault;
404
728
  var init_cloud_providers = __esm({
405
729
  "src/cloud-providers.ts"() {
406
730
  "use strict";
407
- fs4 = __toESM(require("fs"), 1);
408
- path4 = __toESM(require("path"), 1);
731
+ fs5 = __toESM(require("fs"), 1);
732
+ path5 = __toESM(require("path"), 1);
409
733
  init_encryption();
410
734
  init_sanitize();
411
735
  providers = {
@@ -451,12 +775,12 @@ var init_cloud_providers = __esm({
451
775
  dir;
452
776
  encryptionKey;
453
777
  constructor(localDir) {
454
- this.dir = path4.join(localDir, "providers");
778
+ this.dir = path5.join(localDir, "providers");
455
779
  this.encryptionKey = resolveEncryptionKey(localDir);
456
780
  }
457
781
  save(name, apiKey) {
458
782
  getProvider(name);
459
- fs4.mkdirSync(this.dir, { recursive: true });
783
+ fs5.mkdirSync(this.dir, { recursive: true });
460
784
  const { ciphertext, iv, authTag } = encrypt(apiKey, this.encryptionKey);
461
785
  const stored = {
462
786
  name,
@@ -466,34 +790,34 @@ var init_cloud_providers = __esm({
466
790
  data: ciphertext,
467
791
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
468
792
  };
469
- const filePath = path4.join(this.dir, `${sanitizeName(name)}.json`);
470
- fs4.writeFileSync(filePath, JSON.stringify(stored, null, 2), { mode: 384 });
793
+ const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
794
+ fs5.writeFileSync(filePath, JSON.stringify(stored, null, 2), { mode: 384 });
471
795
  }
472
796
  load(name) {
473
- const filePath = path4.join(this.dir, `${sanitizeName(name)}.json`);
474
- if (!fs4.existsSync(filePath)) {
797
+ const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
798
+ if (!fs5.existsSync(filePath)) {
475
799
  throw new Error(
476
800
  `No API key saved for "${name}". Run: browse provider save ${name} <api-key>`
477
801
  );
478
802
  }
479
- const stored = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
803
+ const stored = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
480
804
  return decrypt(stored.data, stored.iv, stored.authTag, this.encryptionKey);
481
805
  }
482
806
  list() {
483
- if (!fs4.existsSync(this.dir)) return [];
484
- return fs4.readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
807
+ if (!fs5.existsSync(this.dir)) return [];
808
+ return fs5.readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
485
809
  const stored = JSON.parse(
486
- fs4.readFileSync(path4.join(this.dir, f), "utf-8")
810
+ fs5.readFileSync(path5.join(this.dir, f), "utf-8")
487
811
  );
488
812
  return { name: stored.name, createdAt: stored.createdAt };
489
813
  });
490
814
  }
491
815
  delete(name) {
492
- const filePath = path4.join(this.dir, `${sanitizeName(name)}.json`);
493
- if (!fs4.existsSync(filePath)) {
816
+ const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
817
+ if (!fs5.existsSync(filePath)) {
494
818
  throw new Error(`No saved key for "${name}"`);
495
819
  }
496
- fs4.unlinkSync(filePath);
820
+ fs5.unlinkSync(filePath);
497
821
  }
498
822
  };
499
823
  }
@@ -504,7 +828,7 @@ var require_package = __commonJS({
504
828
  "package.json"(exports2, module2) {
505
829
  module2.exports = {
506
830
  name: "@ulpi/browse",
507
- version: "1.3.0",
831
+ version: "1.3.2",
508
832
  repository: {
509
833
  type: "git",
510
834
  url: "https://github.com/ulpi-io/browse"
@@ -580,45 +904,45 @@ __export(install_skill_exports, {
580
904
  });
581
905
  function installSkill(targetDir) {
582
906
  const dir = targetDir || process.cwd();
583
- const hasGit = fs5.existsSync(path5.join(dir, ".git"));
584
- const hasClaude = fs5.existsSync(path5.join(dir, ".claude"));
907
+ const hasGit = fs6.existsSync(path6.join(dir, ".git"));
908
+ const hasClaude = fs6.existsSync(path6.join(dir, ".claude"));
585
909
  if (!hasGit && !hasClaude) {
586
910
  console.error(`Not a project root: ${dir}`);
587
911
  console.error("Run from a directory with .git or .claude, or pass the path as an argument.");
588
912
  process.exit(1);
589
913
  }
590
- const skillDir = path5.join(dir, ".claude", "skills", "browse");
591
- fs5.mkdirSync(skillDir, { recursive: true });
592
- const skillSourceDir = path5.resolve(path5.dirname((0, import_url.fileURLToPath)(__import_meta_url)), "..", "skill");
593
- if (!fs5.existsSync(skillSourceDir)) {
914
+ const skillDir = path6.join(dir, ".claude", "skills", "browse");
915
+ fs6.mkdirSync(skillDir, { recursive: true });
916
+ const skillSourceDir = path6.resolve(path6.dirname((0, import_url.fileURLToPath)(__import_meta_url)), "..", "skill");
917
+ if (!fs6.existsSync(skillSourceDir)) {
594
918
  console.error(`Skill directory not found at ${skillSourceDir}`);
595
919
  console.error("Is @ulpi/browse installed correctly?");
596
920
  process.exit(1);
597
921
  }
598
- const mdFiles = fs5.readdirSync(skillSourceDir).filter((f) => f.endsWith(".md"));
922
+ const mdFiles = fs6.readdirSync(skillSourceDir).filter((f) => f.endsWith(".md"));
599
923
  if (mdFiles.length === 0) {
600
924
  console.error(`No .md files found in ${skillSourceDir}`);
601
925
  process.exit(1);
602
926
  }
603
927
  for (const file of mdFiles) {
604
- fs5.copyFileSync(path5.join(skillSourceDir, file), path5.join(skillDir, file));
605
- console.log(`Skill installed: ${path5.relative(dir, path5.join(skillDir, file))}`);
606
- }
607
- const refsSourceDir = path5.join(skillSourceDir, "references");
608
- if (fs5.existsSync(refsSourceDir)) {
609
- const refsDestDir = path5.join(skillDir, "references");
610
- fs5.mkdirSync(refsDestDir, { recursive: true });
611
- const refFiles = fs5.readdirSync(refsSourceDir).filter((f) => f.endsWith(".md"));
928
+ fs6.copyFileSync(path6.join(skillSourceDir, file), path6.join(skillDir, file));
929
+ console.log(`Skill installed: ${path6.relative(dir, path6.join(skillDir, file))}`);
930
+ }
931
+ const refsSourceDir = path6.join(skillSourceDir, "references");
932
+ if (fs6.existsSync(refsSourceDir)) {
933
+ const refsDestDir = path6.join(skillDir, "references");
934
+ fs6.mkdirSync(refsDestDir, { recursive: true });
935
+ const refFiles = fs6.readdirSync(refsSourceDir).filter((f) => f.endsWith(".md"));
612
936
  for (const file of refFiles) {
613
- fs5.copyFileSync(path5.join(refsSourceDir, file), path5.join(refsDestDir, file));
614
- console.log(`Skill installed: ${path5.relative(dir, path5.join(refsDestDir, file))}`);
937
+ fs6.copyFileSync(path6.join(refsSourceDir, file), path6.join(refsDestDir, file));
938
+ console.log(`Skill installed: ${path6.relative(dir, path6.join(refsDestDir, file))}`);
615
939
  }
616
940
  }
617
- const settingsPath = path5.join(dir, ".claude", "settings.json");
941
+ const settingsPath = path6.join(dir, ".claude", "settings.json");
618
942
  let settings = {};
619
- if (fs5.existsSync(settingsPath)) {
943
+ if (fs6.existsSync(settingsPath)) {
620
944
  try {
621
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
945
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
622
946
  } catch {
623
947
  console.error(`Warning: could not parse ${settingsPath}, creating fresh`);
624
948
  settings = {};
@@ -634,20 +958,20 @@ function installSkill(targetDir) {
634
958
  added++;
635
959
  }
636
960
  }
637
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
961
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
638
962
  if (added > 0) {
639
- console.log(`Permissions: ${added} rules added to ${path5.relative(dir, settingsPath)}`);
963
+ console.log(`Permissions: ${added} rules added to ${path6.relative(dir, settingsPath)}`);
640
964
  } else {
641
965
  console.log(`Permissions: already configured`);
642
966
  }
643
967
  console.log("\nDone. Claude Code will now use browse for web tasks automatically.");
644
968
  }
645
- var fs5, path5, import_url, PERMISSIONS;
969
+ var fs6, path6, import_url, PERMISSIONS;
646
970
  var init_install_skill = __esm({
647
971
  "src/install-skill.ts"() {
648
972
  "use strict";
649
- fs5 = __toESM(require("fs"), 1);
650
- path5 = __toESM(require("path"), 1);
973
+ fs6 = __toESM(require("fs"), 1);
974
+ path6 = __toESM(require("path"), 1);
651
975
  import_url = require("url");
652
976
  PERMISSIONS = [
653
977
  "Bash(browse:*)",
@@ -809,21 +1133,21 @@ function listDevices() {
809
1133
  function getProfileDir(localDir, name) {
810
1134
  const sanitized = sanitizeName(name);
811
1135
  if (!sanitized) throw new Error("Invalid profile name");
812
- return path6.join(localDir, "profiles", sanitized);
1136
+ return path7.join(localDir, "profiles", sanitized);
813
1137
  }
814
1138
  function listProfiles(localDir) {
815
- const profilesDir = path6.join(localDir, "profiles");
816
- if (!fs6.existsSync(profilesDir)) return [];
817
- return fs6.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
818
- const dir = path6.join(profilesDir, d.name);
819
- const stat = fs6.statSync(dir);
1139
+ const profilesDir = path7.join(localDir, "profiles");
1140
+ if (!fs7.existsSync(profilesDir)) return [];
1141
+ return fs7.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
1142
+ const dir = path7.join(profilesDir, d.name);
1143
+ const stat = fs7.statSync(dir);
820
1144
  let totalSize = 0;
821
1145
  try {
822
- const files = fs6.readdirSync(dir, { recursive: true, withFileTypes: true });
1146
+ const files = fs7.readdirSync(dir, { recursive: true, withFileTypes: true });
823
1147
  for (const f of files) {
824
1148
  if (f.isFile()) {
825
1149
  try {
826
- totalSize += fs6.statSync(path6.join(f.parentPath || f.path || dir, f.name)).size;
1150
+ totalSize += fs7.statSync(path7.join(f.parentPath || f.path || dir, f.name)).size;
827
1151
  } catch {
828
1152
  }
829
1153
  }
@@ -840,15 +1164,15 @@ function listProfiles(localDir) {
840
1164
  }
841
1165
  function deleteProfile(localDir, name) {
842
1166
  const dir = getProfileDir(localDir, name);
843
- if (!fs6.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
844
- fs6.rmSync(dir, { recursive: true, force: true });
1167
+ if (!fs7.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
1168
+ fs7.rmSync(dir, { recursive: true, force: true });
845
1169
  }
846
- var path6, fs6, import_playwright, DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
1170
+ var path7, fs7, import_playwright, DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
847
1171
  var init_browser_manager = __esm({
848
1172
  "src/browser-manager.ts"() {
849
1173
  "use strict";
850
- path6 = __toESM(require("path"), 1);
851
- fs6 = __toESM(require("fs"), 1);
1174
+ path7 = __toESM(require("path"), 1);
1175
+ fs7 = __toESM(require("fs"), 1);
852
1176
  import_playwright = require("playwright");
853
1177
  init_buffers();
854
1178
  init_sanitize();
@@ -1001,17 +1325,34 @@ var init_browser_manager = __esm({
1001
1325
  * Creates a new BrowserContext on the shared browser.
1002
1326
  * This instance does NOT own the browser — close() only closes the context.
1003
1327
  */
1004
- async launchWithBrowser(browser2) {
1328
+ async launchWithBrowser(browser2, reuseContext = false) {
1005
1329
  this.browser = browser2;
1006
1330
  this.ownsBrowser = false;
1007
- this.context = await browser2.newContext({
1008
- viewport: { width: 1920, height: 1080 },
1009
- ...this.customUserAgent ? { userAgent: this.customUserAgent } : {}
1010
- });
1011
- await this.newTab();
1012
- }
1013
- /**
1014
- * Launch with a persistent browser profile directory.
1331
+ if (reuseContext) {
1332
+ const contexts = browser2.contexts();
1333
+ this.context = contexts[0] || await browser2.newContext({
1334
+ viewport: { width: 1920, height: 1080 }
1335
+ });
1336
+ const pages = this.context.pages();
1337
+ if (pages.length > 0) {
1338
+ for (const page of pages) {
1339
+ const tabId = this.nextTabId++;
1340
+ this.wirePageEvents(page);
1341
+ this.pages.set(tabId, page);
1342
+ this.activeTabId = tabId;
1343
+ }
1344
+ return;
1345
+ }
1346
+ } else {
1347
+ this.context = await browser2.newContext({
1348
+ viewport: { width: 1920, height: 1080 },
1349
+ ...this.customUserAgent ? { userAgent: this.customUserAgent } : {}
1350
+ });
1351
+ }
1352
+ await this.newTab();
1353
+ }
1354
+ /**
1355
+ * Launch with a persistent browser profile directory.
1015
1356
  * Data (cookies, localStorage, cache) persists across restarts.
1016
1357
  * The context IS the browser — closing it closes everything.
1017
1358
  */
@@ -2201,8 +2542,8 @@ async function handleReadCommand(command, args, bm, buffers) {
2201
2542
  case "eval": {
2202
2543
  const filePath = args[0];
2203
2544
  if (!filePath) throw new Error("Usage: browse eval <js-file>");
2204
- if (!fs7.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
2205
- const code = fs7.readFileSync(filePath, "utf-8");
2545
+ if (!fs8.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
2546
+ const code = fs8.readFileSync(filePath, "utf-8");
2206
2547
  const result = await evalCtx.evaluate(code);
2207
2548
  return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
2208
2549
  }
@@ -2440,13 +2781,13 @@ async function handleReadCommand(command, args, bm, buffers) {
2440
2781
  throw new Error(`Unknown read command: ${command}`);
2441
2782
  }
2442
2783
  }
2443
- var fs7;
2784
+ var fs8;
2444
2785
  var init_read = __esm({
2445
2786
  "src/commands/read.ts"() {
2446
2787
  "use strict";
2447
2788
  init_browser_manager();
2448
2789
  init_constants();
2449
- fs7 = __toESM(require("fs"), 1);
2790
+ fs8 = __toESM(require("fs"), 1);
2450
2791
  }
2451
2792
  });
2452
2793
 
@@ -2673,14 +3014,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
2673
3014
  const file = args[1];
2674
3015
  if (!file) throw new Error("Usage: browse cookie export <file>");
2675
3016
  const cookies = await page.context().cookies();
2676
- fs8.writeFileSync(file, JSON.stringify(cookies, null, 2));
3017
+ fs9.writeFileSync(file, JSON.stringify(cookies, null, 2));
2677
3018
  return `Exported ${cookies.length} cookie(s) to ${file}`;
2678
3019
  }
2679
3020
  if (cookieStr === "import") {
2680
3021
  const file = args[1];
2681
3022
  if (!file) throw new Error("Usage: browse cookie import <file>");
2682
- if (!fs8.existsSync(file)) throw new Error(`File not found: ${file}`);
2683
- const cookies = JSON.parse(fs8.readFileSync(file, "utf-8"));
3023
+ if (!fs9.existsSync(file)) throw new Error(`File not found: ${file}`);
3024
+ const cookies = JSON.parse(fs9.readFileSync(file, "utf-8"));
2684
3025
  if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
2685
3026
  await page.context().addCookies(cookies);
2686
3027
  return `Imported ${cookies.length} cookie(s) from ${file}`;
@@ -2747,7 +3088,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
2747
3088
  const [selector, ...filePaths] = args;
2748
3089
  if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
2749
3090
  for (const fp of filePaths) {
2750
- if (!fs8.existsSync(fp)) throw new Error(`File not found: ${fp}`);
3091
+ if (!fs9.existsSync(fp)) throw new Error(`File not found: ${fp}`);
2751
3092
  }
2752
3093
  const resolved = bm.resolveRef(selector);
2753
3094
  if ("locator" in resolved) {
@@ -3083,13 +3424,13 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
3083
3424
  throw new Error(`Unknown write command: ${command}`);
3084
3425
  }
3085
3426
  }
3086
- var fs8;
3427
+ var fs9;
3087
3428
  var init_write = __esm({
3088
3429
  "src/commands/write.ts"() {
3089
3430
  "use strict";
3090
3431
  init_browser_manager();
3091
3432
  init_constants();
3092
- fs8 = __toESM(require("fs"), 1);
3433
+ fs9 = __toESM(require("fs"), 1);
3093
3434
  }
3094
3435
  });
3095
3436
 
@@ -3893,7 +4234,7 @@ var init_lib = __esm({
3893
4234
  tokenize: function tokenize(value) {
3894
4235
  return Array.from(value);
3895
4236
  },
3896
- join: function join7(chars) {
4237
+ join: function join8(chars) {
3897
4238
  return chars.join("");
3898
4239
  },
3899
4240
  postProcess: function postProcess(changeObjects) {
@@ -4072,20 +4413,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
4072
4413
  } else {
4073
4414
  content = json;
4074
4415
  }
4075
- fs9.mkdirSync(sessionDir, { recursive: true });
4076
- fs9.writeFileSync(path7.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
4416
+ fs10.mkdirSync(sessionDir, { recursive: true });
4417
+ fs10.writeFileSync(path8.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
4077
4418
  } catch (err) {
4078
4419
  console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
4079
4420
  }
4080
4421
  }
4081
4422
  async function loadSessionState(sessionDir, context, encryptionKey) {
4082
- const statePath = path7.join(sessionDir, STATE_FILENAME);
4083
- if (!fs9.existsSync(statePath)) {
4423
+ const statePath = path8.join(sessionDir, STATE_FILENAME);
4424
+ if (!fs10.existsSync(statePath)) {
4084
4425
  return false;
4085
4426
  }
4086
4427
  let stateData;
4087
4428
  try {
4088
- const raw = fs9.readFileSync(statePath, "utf-8");
4429
+ const raw = fs10.readFileSync(statePath, "utf-8");
4089
4430
  const parsed = JSON.parse(raw);
4090
4431
  if (parsed.encrypted) {
4091
4432
  if (!encryptionKey) {
@@ -4141,24 +4482,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
4141
4482
  }
4142
4483
  }
4143
4484
  function hasPersistedState(sessionDir) {
4144
- return fs9.existsSync(path7.join(sessionDir, STATE_FILENAME));
4485
+ return fs10.existsSync(path8.join(sessionDir, STATE_FILENAME));
4145
4486
  }
4146
4487
  function cleanOldStates(localDir, maxAgeDays) {
4147
4488
  const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
4148
4489
  const now = Date.now();
4149
4490
  let deleted = 0;
4150
- const statesDir = path7.join(localDir, "states");
4151
- if (fs9.existsSync(statesDir)) {
4491
+ const statesDir = path8.join(localDir, "states");
4492
+ if (fs10.existsSync(statesDir)) {
4152
4493
  try {
4153
- const entries = fs9.readdirSync(statesDir);
4494
+ const entries = fs10.readdirSync(statesDir);
4154
4495
  for (const entry of entries) {
4155
4496
  if (!entry.endsWith(".json")) continue;
4156
- const filePath = path7.join(statesDir, entry);
4497
+ const filePath = path8.join(statesDir, entry);
4157
4498
  try {
4158
- const stat = fs9.statSync(filePath);
4499
+ const stat = fs10.statSync(filePath);
4159
4500
  if (!stat.isFile()) continue;
4160
4501
  if (now - stat.mtimeMs > maxAgeMs) {
4161
- fs9.unlinkSync(filePath);
4502
+ fs10.unlinkSync(filePath);
4162
4503
  deleted++;
4163
4504
  }
4164
4505
  } catch (_) {
@@ -4167,23 +4508,23 @@ function cleanOldStates(localDir, maxAgeDays) {
4167
4508
  } catch (_) {
4168
4509
  }
4169
4510
  }
4170
- const sessionsDir = path7.join(localDir, "sessions");
4171
- if (fs9.existsSync(sessionsDir)) {
4511
+ const sessionsDir = path8.join(localDir, "sessions");
4512
+ if (fs10.existsSync(sessionsDir)) {
4172
4513
  try {
4173
- const sessionDirs = fs9.readdirSync(sessionsDir);
4514
+ const sessionDirs = fs10.readdirSync(sessionsDir);
4174
4515
  for (const dir of sessionDirs) {
4175
- const dirPath = path7.join(sessionsDir, dir);
4516
+ const dirPath = path8.join(sessionsDir, dir);
4176
4517
  try {
4177
- const dirStat = fs9.statSync(dirPath);
4518
+ const dirStat = fs10.statSync(dirPath);
4178
4519
  if (!dirStat.isDirectory()) continue;
4179
4520
  } catch (_) {
4180
4521
  continue;
4181
4522
  }
4182
- const statePath = path7.join(dirPath, STATE_FILENAME);
4523
+ const statePath = path8.join(dirPath, STATE_FILENAME);
4183
4524
  try {
4184
- const stat = fs9.statSync(statePath);
4525
+ const stat = fs10.statSync(statePath);
4185
4526
  if (now - stat.mtimeMs > maxAgeMs) {
4186
- fs9.unlinkSync(statePath);
4527
+ fs10.unlinkSync(statePath);
4187
4528
  deleted++;
4188
4529
  }
4189
4530
  } catch (_) {
@@ -4194,12 +4535,12 @@ function cleanOldStates(localDir, maxAgeDays) {
4194
4535
  }
4195
4536
  return { deleted };
4196
4537
  }
4197
- var fs9, path7, STATE_FILENAME;
4538
+ var fs10, path8, STATE_FILENAME;
4198
4539
  var init_session_persist = __esm({
4199
4540
  "src/session-persist.ts"() {
4200
4541
  "use strict";
4201
- fs9 = __toESM(require("fs"), 1);
4202
- path7 = __toESM(require("path"), 1);
4542
+ fs10 = __toESM(require("fs"), 1);
4543
+ path8 = __toESM(require("path"), 1);
4203
4544
  init_encryption();
4204
4545
  STATE_FILENAME = "state.json";
4205
4546
  }
@@ -4213,20 +4554,20 @@ __export(policy_exports, {
4213
4554
  function findFileUpward(filename) {
4214
4555
  let dir = process.cwd();
4215
4556
  for (let i = 0; i < 20; i++) {
4216
- const candidate = path8.join(dir, filename);
4217
- if (fs10.existsSync(candidate)) return candidate;
4218
- const parent = path8.dirname(dir);
4557
+ const candidate = path9.join(dir, filename);
4558
+ if (fs11.existsSync(candidate)) return candidate;
4559
+ const parent = path9.dirname(dir);
4219
4560
  if (parent === dir) break;
4220
4561
  dir = parent;
4221
4562
  }
4222
4563
  return null;
4223
4564
  }
4224
- var fs10, path8, PolicyChecker;
4565
+ var fs11, path9, PolicyChecker;
4225
4566
  var init_policy = __esm({
4226
4567
  "src/policy.ts"() {
4227
4568
  "use strict";
4228
- fs10 = __toESM(require("fs"), 1);
4229
- path8 = __toESM(require("path"), 1);
4569
+ fs11 = __toESM(require("fs"), 1);
4570
+ path9 = __toESM(require("path"), 1);
4230
4571
  PolicyChecker = class {
4231
4572
  filePath = null;
4232
4573
  lastMtime = 0;
@@ -4245,10 +4586,10 @@ var init_policy = __esm({
4245
4586
  reload() {
4246
4587
  if (!this.filePath) return;
4247
4588
  try {
4248
- const stat = fs10.statSync(this.filePath);
4589
+ const stat = fs11.statSync(this.filePath);
4249
4590
  if (stat.mtimeMs === this.lastMtime) return;
4250
4591
  this.lastMtime = stat.mtimeMs;
4251
- const raw = fs10.readFileSync(this.filePath, "utf-8");
4592
+ const raw = fs11.readFileSync(this.filePath, "utf-8");
4252
4593
  this.policy = JSON.parse(raw);
4253
4594
  } catch {
4254
4595
  }
@@ -4396,516 +4737,211 @@ function generateDiffImage(base, curr, colorThreshold) {
4396
4737
  diffData[di + 1] = 0;
4397
4738
  diffData[di + 2] = 0;
4398
4739
  diffData[di + 3] = 255;
4399
- continue;
4400
- }
4401
- const bi = (y * base.width + x) * 4;
4402
- const ci = (y * curr.width + x) * 4;
4403
- const dr = base.data[bi] - curr.data[ci];
4404
- const dg = base.data[bi + 1] - curr.data[ci + 1];
4405
- const db = base.data[bi + 2] - curr.data[ci + 2];
4406
- const distSq = dr * dr + dg * dg + db * db;
4407
- const isDiff = colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq;
4408
- if (isDiff) {
4409
- diffData[di] = 255;
4410
- diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4411
- diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4412
- diffData[di + 3] = 255;
4413
- } else {
4414
- diffData[di] = curr.data[ci] / 3 | 0;
4415
- diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4416
- diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4417
- diffData[di + 3] = 128;
4418
- }
4419
- }
4420
- }
4421
- return encodePNG({ width: w, height: h, data: diffData });
4422
- }
4423
- function compareScreenshots(baselineBuf, currentBuf, thresholdPct = 0.1, colorThreshold = 30) {
4424
- const base = decodePNG(baselineBuf);
4425
- const curr = decodePNG(currentBuf);
4426
- const w = Math.max(base.width, curr.width);
4427
- const h = Math.max(base.height, curr.height);
4428
- const totalPixels = w * h;
4429
- let diffPixels = 0;
4430
- const colorThreshSq = colorThreshold * colorThreshold * 3;
4431
- for (let y = 0; y < h; y++) {
4432
- for (let x = 0; x < w; x++) {
4433
- const inBase = x < base.width && y < base.height;
4434
- const inCurr = x < curr.width && y < curr.height;
4435
- if (!inBase || !inCurr) {
4436
- diffPixels++;
4437
- continue;
4438
- }
4439
- const bi = (y * base.width + x) * 4;
4440
- const ci = (y * curr.width + x) * 4;
4441
- const dr = base.data[bi] - curr.data[ci];
4442
- const dg = base.data[bi + 1] - curr.data[ci + 1];
4443
- const db = base.data[bi + 2] - curr.data[ci + 2];
4444
- const distSq = dr * dr + dg * dg + db * db;
4445
- if (colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq) diffPixels++;
4446
- }
4447
- }
4448
- const mismatchPct = totalPixels > 0 ? diffPixels / totalPixels * 100 : 0;
4449
- const passed = mismatchPct <= thresholdPct;
4450
- const result = { totalPixels, diffPixels, mismatchPct, passed };
4451
- if (!passed) {
4452
- result.diffImage = generateDiffImage(base, curr, colorThreshold);
4453
- }
4454
- return result;
4455
- }
4456
- var zlib, PNG_MAGIC;
4457
- var init_png_compare = __esm({
4458
- "src/png-compare.ts"() {
4459
- "use strict";
4460
- zlib = __toESM(require("zlib"), 1);
4461
- PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
4462
- }
4463
- });
4464
-
4465
- // src/auth-vault.ts
4466
- var auth_vault_exports = {};
4467
- __export(auth_vault_exports, {
4468
- AuthVault: () => AuthVault
4469
- });
4470
- async function autoDetectSelector(page, field) {
4471
- if (field === "username") {
4472
- const candidates2 = [
4473
- 'input[type="email"]',
4474
- 'input[name="email"]',
4475
- 'input[name="username"]',
4476
- 'input[name="user"]',
4477
- 'input[name="login"]',
4478
- 'input[autocomplete="username"]',
4479
- 'input[autocomplete="email"]',
4480
- 'input[type="text"]:first-of-type'
4481
- ];
4482
- for (const sel of candidates2) {
4483
- const count = await page.locator(sel).count();
4484
- if (count > 0) return sel;
4485
- }
4486
- throw new Error("Could not auto-detect username field. Save with explicit selectors: browse auth save <name> <url> <user> <pass> --user-sel <sel> --pass-sel <sel> --submit-sel <sel>");
4487
- }
4488
- if (field === "password") {
4489
- const candidates2 = [
4490
- 'input[type="password"]',
4491
- 'input[name="password"]',
4492
- 'input[name="pass"]',
4493
- 'input[autocomplete="current-password"]'
4494
- ];
4495
- for (const sel of candidates2) {
4496
- const count = await page.locator(sel).count();
4497
- if (count > 0) return sel;
4498
- }
4499
- throw new Error("Could not auto-detect password field.");
4500
- }
4501
- const candidates = [
4502
- 'button[type="submit"]',
4503
- 'input[type="submit"]',
4504
- "form button",
4505
- 'button:has-text("Log in")',
4506
- 'button:has-text("Sign in")',
4507
- 'button:has-text("Login")',
4508
- 'button:has-text("Submit")'
4509
- ];
4510
- for (const sel of candidates) {
4511
- const count = await page.locator(sel).count();
4512
- if (count > 0) return sel;
4513
- }
4514
- throw new Error("Could not auto-detect submit button.");
4515
- }
4516
- var fs11, path9, AuthVault;
4517
- var init_auth_vault = __esm({
4518
- "src/auth-vault.ts"() {
4519
- "use strict";
4520
- fs11 = __toESM(require("fs"), 1);
4521
- path9 = __toESM(require("path"), 1);
4522
- init_constants();
4523
- init_encryption();
4524
- init_sanitize();
4525
- AuthVault = class {
4526
- authDir;
4527
- encryptionKey;
4528
- constructor(localDir) {
4529
- this.authDir = path9.join(localDir, "auth");
4530
- this.encryptionKey = resolveEncryptionKey(localDir);
4531
- }
4532
- save(name, url, username, password, selectors) {
4533
- fs11.mkdirSync(this.authDir, { recursive: true });
4534
- const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
4535
- const now = (/* @__PURE__ */ new Date()).toISOString();
4536
- const credential = {
4537
- name,
4538
- url,
4539
- username,
4540
- encrypted: true,
4541
- iv,
4542
- authTag,
4543
- data: ciphertext,
4544
- usernameSelector: selectors?.username,
4545
- passwordSelector: selectors?.password,
4546
- submitSelector: selectors?.submit,
4547
- createdAt: now,
4548
- updatedAt: now
4549
- };
4550
- const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4551
- fs11.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
4552
- }
4553
- load(name) {
4554
- const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4555
- if (!fs11.existsSync(filePath)) {
4556
- throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
4557
- }
4558
- return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
4559
- }
4560
- async login(name, bm) {
4561
- const cred = this.load(name);
4562
- const password = decrypt(cred.data, cred.iv, cred.authTag, this.encryptionKey);
4563
- const page = bm.getPage();
4564
- await page.goto(cred.url, {
4565
- waitUntil: "domcontentloaded",
4566
- timeout: DEFAULTS.COMMAND_TIMEOUT_MS
4567
- });
4568
- const userSel = cred.usernameSelector || await autoDetectSelector(page, "username");
4569
- const passSel = cred.passwordSelector || await autoDetectSelector(page, "password");
4570
- const submitSel = cred.submitSelector || await autoDetectSelector(page, "submit");
4571
- await page.fill(userSel, cred.username, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4572
- await page.fill(passSel, password, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4573
- await page.click(submitSel, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4574
- await page.waitForLoadState("domcontentloaded").catch(() => {
4575
- });
4576
- return `Logged in as ${cred.username} at ${page.url()}`;
4577
- }
4578
- list() {
4579
- if (!fs11.existsSync(this.authDir)) return [];
4580
- const files = fs11.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
4581
- return files.map((f) => {
4582
- try {
4583
- const data = JSON.parse(fs11.readFileSync(path9.join(this.authDir, f), "utf-8"));
4584
- return {
4585
- name: data.name,
4586
- url: data.url,
4587
- username: data.username,
4588
- hasPassword: true,
4589
- createdAt: data.createdAt
4590
- };
4591
- } catch {
4592
- return null;
4593
- }
4594
- }).filter(Boolean);
4595
- }
4596
- delete(name) {
4597
- const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4598
- if (!fs11.existsSync(filePath)) {
4599
- throw new Error(`Credential "${name}" not found.`);
4600
- }
4601
- fs11.unlinkSync(filePath);
4602
- }
4603
- };
4604
- }
4605
- });
4606
-
4607
- // src/cookie-import.ts
4608
- var cookie_import_exports = {};
4609
- __export(cookie_import_exports, {
4610
- CookieImportError: () => CookieImportError,
4611
- findInstalledBrowsers: () => findInstalledBrowsers,
4612
- importCookies: () => importCookies,
4613
- listDomains: () => listDomains
4614
- });
4615
- function findInstalledBrowsers() {
4616
- const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
4617
- return BROWSER_REGISTRY.filter((b) => {
4618
- const dbPath = path10.join(appSupport, b.dataDir, "Default", "Cookies");
4619
- try {
4620
- return fs12.existsSync(dbPath);
4621
- } catch {
4622
- return false;
4623
- }
4624
- });
4625
- }
4626
- function listDomains(browserName, profile = "Default") {
4627
- const browser2 = resolveBrowser(browserName);
4628
- const dbPath = getCookieDbPath(browser2, profile);
4629
- const db = openDb(dbPath, browser2.name);
4630
- try {
4631
- const now = chromiumNow();
4632
- const rows = db.prepare(
4633
- `SELECT host_key AS domain, COUNT(*) AS count
4634
- FROM cookies
4635
- WHERE has_expires = 0 OR expires_utc > ?
4636
- GROUP BY host_key
4637
- ORDER BY count DESC`
4638
- ).all(now);
4639
- return { domains: rows, browser: browser2.name };
4640
- } finally {
4641
- db.close();
4642
- }
4643
- }
4644
- async function importCookies(browserName, domains, profile = "Default") {
4645
- if (domains.length === 0) return { cookies: [], count: 0, failed: 0, domainCounts: {} };
4646
- const browser2 = resolveBrowser(browserName);
4647
- const derivedKey = await getDerivedKey(browser2);
4648
- const dbPath = getCookieDbPath(browser2, profile);
4649
- const db = openDb(dbPath, browser2.name);
4650
- try {
4651
- const now = chromiumNow();
4652
- const placeholders = domains.map(() => "?").join(",");
4653
- const rows = db.prepare(
4654
- `SELECT host_key, name, value, encrypted_value, path, expires_utc,
4655
- is_secure, is_httponly, has_expires, samesite
4656
- FROM cookies
4657
- WHERE host_key IN (${placeholders})
4658
- AND (has_expires = 0 OR expires_utc > ?)
4659
- ORDER BY host_key, name`
4660
- ).all(...domains, now);
4661
- const cookies = [];
4662
- let failed = 0;
4663
- const domainCounts = {};
4664
- for (const row of rows) {
4665
- try {
4666
- const value = decryptCookieValue(row, derivedKey);
4667
- const cookie = toPlaywrightCookie(row, value);
4668
- cookies.push(cookie);
4669
- domainCounts[row.host_key] = (domainCounts[row.host_key] || 0) + 1;
4670
- } catch {
4671
- failed++;
4672
- }
4673
- }
4674
- return { cookies, count: cookies.length, failed, domainCounts };
4675
- } finally {
4676
- db.close();
4677
- }
4678
- }
4679
- function resolveBrowser(nameOrAlias) {
4680
- const needle = nameOrAlias.toLowerCase().trim();
4681
- const found = BROWSER_REGISTRY.find(
4682
- (b) => b.aliases.includes(needle) || b.name.toLowerCase() === needle
4683
- );
4684
- if (!found) {
4685
- const supported = BROWSER_REGISTRY.flatMap((b) => b.aliases).join(", ");
4686
- throw new CookieImportError(
4687
- `Unknown browser '${nameOrAlias}'. Supported: ${supported}`,
4688
- "unknown_browser"
4689
- );
4690
- }
4691
- return found;
4692
- }
4693
- function validateProfile(profile) {
4694
- if (/[/\\]|\.\./.test(profile) || /[\x00-\x1f]/.test(profile)) {
4695
- throw new CookieImportError(
4696
- `Invalid profile name: '${profile}'`,
4697
- "bad_request"
4698
- );
4699
- }
4700
- }
4701
- function getCookieDbPath(browser2, profile) {
4702
- validateProfile(profile);
4703
- const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
4704
- const dbPath = path10.join(appSupport, browser2.dataDir, profile, "Cookies");
4705
- if (!fs12.existsSync(dbPath)) {
4706
- throw new CookieImportError(
4707
- `${browser2.name} is not installed (no cookie database at ${dbPath})`,
4708
- "not_installed"
4709
- );
4710
- }
4711
- return dbPath;
4712
- }
4713
- function openDb(dbPath, browserName) {
4714
- try {
4715
- return new import_better_sqlite3.default(dbPath, { readonly: true });
4716
- } catch (err) {
4717
- if (err.message?.includes("SQLITE_BUSY") || err.message?.includes("database is locked")) {
4718
- return openDbFromCopy(dbPath, browserName);
4719
- }
4720
- if (err.message?.includes("SQLITE_CORRUPT") || err.message?.includes("malformed")) {
4721
- throw new CookieImportError(
4722
- "Cookie database is corrupt",
4723
- "db_corrupt"
4724
- );
4725
- }
4726
- throw err;
4727
- }
4728
- }
4729
- function openDbFromCopy(dbPath, browserName) {
4730
- const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto2.randomUUID()}.db`;
4731
- try {
4732
- fs12.copyFileSync(dbPath, tmpPath);
4733
- const walPath = dbPath + "-wal";
4734
- const shmPath = dbPath + "-shm";
4735
- if (fs12.existsSync(walPath)) fs12.copyFileSync(walPath, tmpPath + "-wal");
4736
- if (fs12.existsSync(shmPath)) fs12.copyFileSync(shmPath, tmpPath + "-shm");
4737
- const db = new import_better_sqlite3.default(tmpPath, { readonly: true });
4738
- const origClose = db.close.bind(db);
4739
- db.close = (() => {
4740
- origClose();
4741
- try {
4742
- fs12.unlinkSync(tmpPath);
4743
- } catch {
4744
- }
4745
- try {
4746
- fs12.unlinkSync(tmpPath + "-wal");
4747
- } catch {
4748
- }
4749
- try {
4750
- fs12.unlinkSync(tmpPath + "-shm");
4751
- } catch {
4752
- }
4753
- });
4754
- return db;
4755
- } catch {
4756
- try {
4757
- fs12.unlinkSync(tmpPath);
4758
- } catch {
4759
- }
4760
- throw new CookieImportError(
4761
- `Cookie database is locked (${browserName} may be running). Try closing ${browserName} first.`,
4762
- "db_locked",
4763
- "retry"
4764
- );
4765
- }
4766
- }
4767
- async function getDerivedKey(browser2) {
4768
- const cached = keyCache.get(browser2.keychainService);
4769
- if (cached) return cached;
4770
- const password = await getKeychainPassword(browser2.keychainService);
4771
- const derived = crypto2.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
4772
- keyCache.set(browser2.keychainService, derived);
4773
- return derived;
4774
- }
4775
- async function getKeychainPassword(service) {
4776
- const proc = (0, import_child_process2.spawn)("security", ["find-generic-password", "-s", service, "-w"], {
4777
- stdio: ["ignore", "pipe", "pipe"]
4778
- });
4779
- let stdout = "";
4780
- let stderr = "";
4781
- proc.stdout.setEncoding("utf8");
4782
- proc.stderr.setEncoding("utf8");
4783
- proc.stdout.on("data", (chunk) => {
4784
- stdout += chunk;
4785
- });
4786
- proc.stderr.on("data", (chunk) => {
4787
- stderr += chunk;
4788
- });
4789
- const exitPromise = new Promise(
4790
- (resolve4) => proc.on("close", (code) => resolve4(code))
4791
- );
4792
- const timeout = new Promise(
4793
- (_, reject) => setTimeout(() => {
4794
- proc.kill();
4795
- reject(new CookieImportError(
4796
- `macOS is waiting for Keychain permission. Look for a dialog asking to allow access to "${service}".`,
4797
- "keychain_timeout",
4798
- "retry"
4799
- ));
4800
- }, 1e4)
4801
- );
4802
- try {
4803
- const exitCode = await Promise.race([exitPromise, timeout]);
4804
- if (exitCode !== 0) {
4805
- const errText = stderr.trim().toLowerCase();
4806
- if (errText.includes("user canceled") || errText.includes("denied") || errText.includes("interaction not allowed")) {
4807
- throw new CookieImportError(
4808
- `Keychain access denied. Click Allow in the macOS dialog for "${service}".`,
4809
- "keychain_denied",
4810
- "retry"
4811
- );
4812
- }
4813
- if (errText.includes("could not be found") || errText.includes("not found")) {
4814
- throw new CookieImportError(
4815
- `No Keychain entry for "${service}".`,
4816
- "keychain_not_found"
4817
- );
4740
+ continue;
4741
+ }
4742
+ const bi = (y * base.width + x) * 4;
4743
+ const ci = (y * curr.width + x) * 4;
4744
+ const dr = base.data[bi] - curr.data[ci];
4745
+ const dg = base.data[bi + 1] - curr.data[ci + 1];
4746
+ const db = base.data[bi + 2] - curr.data[ci + 2];
4747
+ const distSq = dr * dr + dg * dg + db * db;
4748
+ const isDiff = colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq;
4749
+ if (isDiff) {
4750
+ diffData[di] = 255;
4751
+ diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4752
+ diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4753
+ diffData[di + 3] = 255;
4754
+ } else {
4755
+ diffData[di] = curr.data[ci] / 3 | 0;
4756
+ diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4757
+ diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4758
+ diffData[di + 3] = 128;
4818
4759
  }
4819
- throw new CookieImportError(
4820
- `Could not read Keychain: ${stderr.trim()}`,
4821
- "keychain_error",
4822
- "retry"
4823
- );
4824
4760
  }
4825
- return stdout.trim();
4826
- } catch (err) {
4827
- if (err instanceof CookieImportError) throw err;
4828
- throw new CookieImportError(
4829
- `Could not read Keychain: ${err.message}`,
4830
- "keychain_error",
4831
- "retry"
4832
- );
4833
4761
  }
4762
+ return encodePNG({ width: w, height: h, data: diffData });
4834
4763
  }
4835
- function decryptCookieValue(row, key) {
4836
- if (row.value && row.value.length > 0) return row.value;
4837
- const ev = Buffer.from(row.encrypted_value);
4838
- if (ev.length === 0) return "";
4839
- const prefix = ev.slice(0, 3).toString("utf-8");
4840
- if (prefix !== "v10") {
4841
- throw new Error(`Unknown encryption prefix: ${prefix}`);
4764
+ function compareScreenshots(baselineBuf, currentBuf, thresholdPct = 0.1, colorThreshold = 30) {
4765
+ const base = decodePNG(baselineBuf);
4766
+ const curr = decodePNG(currentBuf);
4767
+ const w = Math.max(base.width, curr.width);
4768
+ const h = Math.max(base.height, curr.height);
4769
+ const totalPixels = w * h;
4770
+ let diffPixels = 0;
4771
+ const colorThreshSq = colorThreshold * colorThreshold * 3;
4772
+ for (let y = 0; y < h; y++) {
4773
+ for (let x = 0; x < w; x++) {
4774
+ const inBase = x < base.width && y < base.height;
4775
+ const inCurr = x < curr.width && y < curr.height;
4776
+ if (!inBase || !inCurr) {
4777
+ diffPixels++;
4778
+ continue;
4779
+ }
4780
+ const bi = (y * base.width + x) * 4;
4781
+ const ci = (y * curr.width + x) * 4;
4782
+ const dr = base.data[bi] - curr.data[ci];
4783
+ const dg = base.data[bi + 1] - curr.data[ci + 1];
4784
+ const db = base.data[bi + 2] - curr.data[ci + 2];
4785
+ const distSq = dr * dr + dg * dg + db * db;
4786
+ if (colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq) diffPixels++;
4787
+ }
4842
4788
  }
4843
- const ciphertext = ev.slice(3);
4844
- const iv = Buffer.alloc(16, 32);
4845
- const decipher = crypto2.createDecipheriv("aes-128-cbc", key, iv);
4846
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
4847
- if (plaintext.length <= 32) return "";
4848
- return plaintext.slice(32).toString("utf-8");
4849
- }
4850
- function toPlaywrightCookie(row, value) {
4851
- return {
4852
- name: row.name,
4853
- value,
4854
- domain: row.host_key,
4855
- path: row.path || "/",
4856
- expires: chromiumEpochToUnix(row.expires_utc, row.has_expires),
4857
- secure: row.is_secure === 1,
4858
- httpOnly: row.is_httponly === 1,
4859
- sameSite: mapSameSite(row.samesite)
4860
- };
4861
- }
4862
- function chromiumNow() {
4863
- return BigInt(Date.now()) * 1000n + CHROMIUM_EPOCH_OFFSET;
4864
- }
4865
- function chromiumEpochToUnix(epoch, hasExpires) {
4866
- if (hasExpires === 0 || epoch === 0 || epoch === 0n) return -1;
4867
- const epochBig = BigInt(epoch);
4868
- const unixMicro = epochBig - CHROMIUM_EPOCH_OFFSET;
4869
- return Number(unixMicro / 1000000n);
4789
+ const mismatchPct = totalPixels > 0 ? diffPixels / totalPixels * 100 : 0;
4790
+ const passed = mismatchPct <= thresholdPct;
4791
+ const result = { totalPixels, diffPixels, mismatchPct, passed };
4792
+ if (!passed) {
4793
+ result.diffImage = generateDiffImage(base, curr, colorThreshold);
4794
+ }
4795
+ return result;
4870
4796
  }
4871
- function mapSameSite(value) {
4872
- switch (value) {
4873
- case 0:
4874
- return "None";
4875
- case 1:
4876
- return "Lax";
4877
- case 2:
4878
- return "Strict";
4879
- default:
4880
- return "Lax";
4797
+ var zlib, PNG_MAGIC;
4798
+ var init_png_compare = __esm({
4799
+ "src/png-compare.ts"() {
4800
+ "use strict";
4801
+ zlib = __toESM(require("zlib"), 1);
4802
+ PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
4803
+ }
4804
+ });
4805
+
4806
+ // src/auth-vault.ts
4807
+ var auth_vault_exports = {};
4808
+ __export(auth_vault_exports, {
4809
+ AuthVault: () => AuthVault
4810
+ });
4811
+ async function autoDetectSelector(page, field) {
4812
+ if (field === "username") {
4813
+ const candidates2 = [
4814
+ 'input[type="email"]',
4815
+ 'input[name="email"]',
4816
+ 'input[name="username"]',
4817
+ 'input[name="user"]',
4818
+ 'input[name="login"]',
4819
+ 'input[autocomplete="username"]',
4820
+ 'input[autocomplete="email"]',
4821
+ 'input[type="text"]:first-of-type'
4822
+ ];
4823
+ for (const sel of candidates2) {
4824
+ const count = await page.locator(sel).count();
4825
+ if (count > 0) return sel;
4826
+ }
4827
+ throw new Error("Could not auto-detect username field. Save with explicit selectors: browse auth save <name> <url> <user> <pass> --user-sel <sel> --pass-sel <sel> --submit-sel <sel>");
4828
+ }
4829
+ if (field === "password") {
4830
+ const candidates2 = [
4831
+ 'input[type="password"]',
4832
+ 'input[name="password"]',
4833
+ 'input[name="pass"]',
4834
+ 'input[autocomplete="current-password"]'
4835
+ ];
4836
+ for (const sel of candidates2) {
4837
+ const count = await page.locator(sel).count();
4838
+ if (count > 0) return sel;
4839
+ }
4840
+ throw new Error("Could not auto-detect password field.");
4841
+ }
4842
+ const candidates = [
4843
+ 'button[type="submit"]',
4844
+ 'input[type="submit"]',
4845
+ "form button",
4846
+ 'button:has-text("Log in")',
4847
+ 'button:has-text("Sign in")',
4848
+ 'button:has-text("Login")',
4849
+ 'button:has-text("Submit")'
4850
+ ];
4851
+ for (const sel of candidates) {
4852
+ const count = await page.locator(sel).count();
4853
+ if (count > 0) return sel;
4881
4854
  }
4855
+ throw new Error("Could not auto-detect submit button.");
4882
4856
  }
4883
- var import_better_sqlite3, import_child_process2, crypto2, fs12, path10, os2, CookieImportError, BROWSER_REGISTRY, keyCache, CHROMIUM_EPOCH_OFFSET;
4884
- var init_cookie_import = __esm({
4885
- "src/cookie-import.ts"() {
4857
+ var fs12, path10, AuthVault;
4858
+ var init_auth_vault = __esm({
4859
+ "src/auth-vault.ts"() {
4886
4860
  "use strict";
4887
- import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
4888
- import_child_process2 = require("child_process");
4889
- crypto2 = __toESM(require("crypto"), 1);
4890
4861
  fs12 = __toESM(require("fs"), 1);
4891
4862
  path10 = __toESM(require("path"), 1);
4892
- os2 = __toESM(require("os"), 1);
4893
- CookieImportError = class extends Error {
4894
- constructor(message, code, action) {
4895
- super(message);
4896
- this.code = code;
4897
- this.action = action;
4898
- this.name = "CookieImportError";
4863
+ init_constants();
4864
+ init_encryption();
4865
+ init_sanitize();
4866
+ AuthVault = class {
4867
+ authDir;
4868
+ encryptionKey;
4869
+ constructor(localDir) {
4870
+ this.authDir = path10.join(localDir, "auth");
4871
+ this.encryptionKey = resolveEncryptionKey(localDir);
4872
+ }
4873
+ save(name, url, username, password, selectors) {
4874
+ fs12.mkdirSync(this.authDir, { recursive: true });
4875
+ const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
4876
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4877
+ const credential = {
4878
+ name,
4879
+ url,
4880
+ username,
4881
+ encrypted: true,
4882
+ iv,
4883
+ authTag,
4884
+ data: ciphertext,
4885
+ usernameSelector: selectors?.username,
4886
+ passwordSelector: selectors?.password,
4887
+ submitSelector: selectors?.submit,
4888
+ createdAt: now,
4889
+ updatedAt: now
4890
+ };
4891
+ const filePath = path10.join(this.authDir, `${sanitizeName(name)}.json`);
4892
+ fs12.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
4893
+ }
4894
+ load(name) {
4895
+ const filePath = path10.join(this.authDir, `${sanitizeName(name)}.json`);
4896
+ if (!fs12.existsSync(filePath)) {
4897
+ throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
4898
+ }
4899
+ return JSON.parse(fs12.readFileSync(filePath, "utf-8"));
4900
+ }
4901
+ async login(name, bm) {
4902
+ const cred = this.load(name);
4903
+ const password = decrypt(cred.data, cred.iv, cred.authTag, this.encryptionKey);
4904
+ const page = bm.getPage();
4905
+ await page.goto(cred.url, {
4906
+ waitUntil: "domcontentloaded",
4907
+ timeout: DEFAULTS.COMMAND_TIMEOUT_MS
4908
+ });
4909
+ const userSel = cred.usernameSelector || await autoDetectSelector(page, "username");
4910
+ const passSel = cred.passwordSelector || await autoDetectSelector(page, "password");
4911
+ const submitSel = cred.submitSelector || await autoDetectSelector(page, "submit");
4912
+ await page.fill(userSel, cred.username, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4913
+ await page.fill(passSel, password, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4914
+ await page.click(submitSel, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4915
+ await page.waitForLoadState("domcontentloaded").catch(() => {
4916
+ });
4917
+ return `Logged in as ${cred.username} at ${page.url()}`;
4918
+ }
4919
+ list() {
4920
+ if (!fs12.existsSync(this.authDir)) return [];
4921
+ const files = fs12.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
4922
+ return files.map((f) => {
4923
+ try {
4924
+ const data = JSON.parse(fs12.readFileSync(path10.join(this.authDir, f), "utf-8"));
4925
+ return {
4926
+ name: data.name,
4927
+ url: data.url,
4928
+ username: data.username,
4929
+ hasPassword: true,
4930
+ createdAt: data.createdAt
4931
+ };
4932
+ } catch {
4933
+ return null;
4934
+ }
4935
+ }).filter(Boolean);
4936
+ }
4937
+ delete(name) {
4938
+ const filePath = path10.join(this.authDir, `${sanitizeName(name)}.json`);
4939
+ if (!fs12.existsSync(filePath)) {
4940
+ throw new Error(`Credential "${name}" not found.`);
4941
+ }
4942
+ fs12.unlinkSync(filePath);
4899
4943
  }
4900
4944
  };
4901
- BROWSER_REGISTRY = [
4902
- { name: "Chrome", dataDir: "Google/Chrome/", keychainService: "Chrome Safe Storage", aliases: ["chrome", "google-chrome"] },
4903
- { name: "Arc", dataDir: "Arc/User Data/", keychainService: "Arc Safe Storage", aliases: ["arc"] },
4904
- { name: "Brave", dataDir: "BraveSoftware/Brave-Browser/", keychainService: "Brave Safe Storage", aliases: ["brave"] },
4905
- { name: "Edge", dataDir: "Microsoft Edge/", keychainService: "Microsoft Edge Safe Storage", aliases: ["edge"] }
4906
- ];
4907
- keyCache = /* @__PURE__ */ new Map();
4908
- CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
4909
4945
  }
4910
4946
  });
4911
4947
 
@@ -8377,9 +8413,11 @@ var init_session_manager = __esm({
8377
8413
  browser;
8378
8414
  localDir;
8379
8415
  encryptionKey;
8380
- constructor(browser2, localDir = "/tmp") {
8416
+ reuseContext;
8417
+ constructor(browser2, localDir = "/tmp", reuseContext = false) {
8381
8418
  this.browser = browser2;
8382
8419
  this.localDir = localDir;
8420
+ this.reuseContext = reuseContext;
8383
8421
  try {
8384
8422
  this.encryptionKey = resolveEncryptionKey(localDir);
8385
8423
  } catch {
@@ -8428,7 +8466,7 @@ var init_session_manager = __esm({
8428
8466
  fs15.mkdirSync(outputDir, { recursive: true });
8429
8467
  const buffers = new SessionBuffers();
8430
8468
  const manager = new BrowserManager(buffers);
8431
- await manager.launchWithBrowser(this.browser);
8469
+ await manager.launchWithBrowser(this.browser, this.reuseContext && this.sessions.size === 0);
8432
8470
  let domainFilter = null;
8433
8471
  if (allowedDomains) {
8434
8472
  const domains = allowedDomains.split(",").map((d) => d.trim()).filter(Boolean);
@@ -8913,7 +8951,8 @@ async function start() {
8913
8951
  shutdown();
8914
8952
  });
8915
8953
  }
8916
- sessionManager = new SessionManager(browser, LOCAL_DIR2);
8954
+ const reuseContext = runtime.name === "chrome";
8955
+ sessionManager = new SessionManager(browser, LOCAL_DIR2, reuseContext);
8917
8956
  }
8918
8957
  const startTime = Date.now();
8919
8958
  const server = nodeServe({
@@ -9873,6 +9912,9 @@ async function main() {
9873
9912
  runtimeName = "chrome";
9874
9913
  headed = true;
9875
9914
  }
9915
+ if (runtimeName === "chrome") {
9916
+ headed = true;
9917
+ }
9876
9918
  cliFlags.json = jsonMode;
9877
9919
  cliFlags.contentBoundaries = contentBoundaries;
9878
9920
  cliFlags.allowedDomains = allowedDomains || "";