@ulpi/browse 1.0.1 → 1.0.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.mjs CHANGED
@@ -127,7 +127,7 @@ var require_package = __commonJS({
127
127
  "package.json"(exports, module) {
128
128
  module.exports = {
129
129
  name: "@ulpi/browse",
130
- version: "1.0.1",
130
+ version: "1.0.2",
131
131
  repository: {
132
132
  type: "git",
133
133
  url: "https://github.com/ulpi-io/browse"
@@ -181,6 +181,7 @@ var require_package = __commonJS({
181
181
  devDependencies: {
182
182
  "@types/better-sqlite3": "^7.0.0",
183
183
  "@types/node": "^25.5.0",
184
+ "@vitest/coverage-v8": "^4.1.0",
184
185
  esbuild: "^0.25.0",
185
186
  tsx: "^4.0.0",
186
187
  typescript: "^5.9.3",
@@ -452,7 +453,31 @@ stderr: ${stderrData.slice(0, 2e3)}` : "")
452
453
  });
453
454
 
454
455
  // src/buffers.ts
455
- var SessionBuffers;
456
+ var buffers_exports = {};
457
+ __export(buffers_exports, {
458
+ SessionBuffers: () => SessionBuffers,
459
+ addConsoleEntry: () => addConsoleEntry,
460
+ addNetworkEntry: () => addNetworkEntry,
461
+ consoleBuffer: () => consoleBuffer,
462
+ consoleTotalAdded: () => consoleTotalAdded,
463
+ networkBuffer: () => networkBuffer,
464
+ networkTotalAdded: () => networkTotalAdded
465
+ });
466
+ function addConsoleEntry(entry) {
467
+ if (consoleBuffer.length >= DEFAULTS.BUFFER_HIGH_WATER_MARK) {
468
+ consoleBuffer.shift();
469
+ }
470
+ consoleBuffer.push(entry);
471
+ consoleTotalAdded++;
472
+ }
473
+ function addNetworkEntry(entry) {
474
+ if (networkBuffer.length >= DEFAULTS.BUFFER_HIGH_WATER_MARK) {
475
+ networkBuffer.shift();
476
+ }
477
+ networkBuffer.push(entry);
478
+ networkTotalAdded++;
479
+ }
480
+ var SessionBuffers, consoleBuffer, networkBuffer, consoleTotalAdded, networkTotalAdded;
456
481
  var init_buffers = __esm({
457
482
  "src/buffers.ts"() {
458
483
  "use strict";
@@ -480,10 +505,39 @@ var init_buffers = __esm({
480
505
  this.networkTotalAdded++;
481
506
  }
482
507
  };
508
+ consoleBuffer = [];
509
+ networkBuffer = [];
510
+ consoleTotalAdded = 0;
511
+ networkTotalAdded = 0;
512
+ }
513
+ });
514
+
515
+ // src/sanitize.ts
516
+ function sanitizeName(name) {
517
+ const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
518
+ if (!sanitized || /^[._]+$/.test(sanitized)) {
519
+ throw new Error(`Invalid name: "${name}"`);
520
+ }
521
+ return sanitized;
522
+ }
523
+ var init_sanitize = __esm({
524
+ "src/sanitize.ts"() {
525
+ "use strict";
483
526
  }
484
527
  });
485
528
 
486
529
  // src/browser-manager.ts
530
+ var browser_manager_exports = {};
531
+ __export(browser_manager_exports, {
532
+ BrowserManager: () => BrowserManager,
533
+ deleteProfile: () => deleteProfile,
534
+ getProfileDir: () => getProfileDir,
535
+ listDevices: () => listDevices,
536
+ listProfiles: () => listProfiles,
537
+ resolveDevice: () => resolveDevice
538
+ });
539
+ import * as path4 from "path";
540
+ import * as fs4 from "fs";
487
541
  import { chromium, devices as playwrightDevices } from "playwright";
488
542
  function resolveDevice(name) {
489
543
  const alias = DEVICE_ALIASES[name.toLowerCase()];
@@ -512,11 +566,49 @@ function listDevices() {
512
566
  ]);
513
567
  return [...all].sort();
514
568
  }
569
+ function getProfileDir(localDir, name) {
570
+ const sanitized = sanitizeName(name);
571
+ if (!sanitized) throw new Error("Invalid profile name");
572
+ return path4.join(localDir, "profiles", sanitized);
573
+ }
574
+ function listProfiles(localDir) {
575
+ const profilesDir = path4.join(localDir, "profiles");
576
+ if (!fs4.existsSync(profilesDir)) return [];
577
+ return fs4.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
578
+ const dir = path4.join(profilesDir, d.name);
579
+ const stat = fs4.statSync(dir);
580
+ let totalSize = 0;
581
+ try {
582
+ const files = fs4.readdirSync(dir, { recursive: true, withFileTypes: true });
583
+ for (const f of files) {
584
+ if (f.isFile()) {
585
+ try {
586
+ totalSize += fs4.statSync(path4.join(f.parentPath || f.path || dir, f.name)).size;
587
+ } catch {
588
+ }
589
+ }
590
+ }
591
+ } catch {
592
+ }
593
+ const sizeMB = (totalSize / 1024 / 1024).toFixed(1);
594
+ return {
595
+ name: d.name,
596
+ size: `${sizeMB}MB`,
597
+ lastUsed: stat.mtime.toISOString().split("T")[0]
598
+ };
599
+ });
600
+ }
601
+ function deleteProfile(localDir, name) {
602
+ const dir = getProfileDir(localDir, name);
603
+ if (!fs4.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
604
+ fs4.rmSync(dir, { recursive: true, force: true });
605
+ }
515
606
  var DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
516
607
  var init_browser_manager = __esm({
517
608
  "src/browser-manager.ts"() {
518
609
  "use strict";
519
610
  init_buffers();
611
+ init_sanitize();
520
612
  DEVICE_ALIASES = {
521
613
  "iphone": "iPhone 15",
522
614
  "iphone-12": "iPhone 12",
@@ -626,6 +718,8 @@ var init_browser_manager = __esm({
626
718
  domainFilter = null;
627
719
  // Whether this instance owns (and should close) the Browser process
628
720
  ownsBrowser = false;
721
+ // Whether this instance uses a persistent browser context (profile mode)
722
+ isPersistent = false;
629
723
  constructor(buffers) {
630
724
  this.buffers = buffers || new SessionBuffers();
631
725
  }
@@ -668,7 +762,66 @@ var init_browser_manager = __esm({
668
762
  });
669
763
  await this.newTab();
670
764
  }
765
+ /**
766
+ * Launch with a persistent browser profile directory.
767
+ * Data (cookies, localStorage, cache) persists across restarts.
768
+ * The context IS the browser — closing it closes everything.
769
+ */
770
+ async launchPersistent(profileDir, onCrash) {
771
+ let context;
772
+ try {
773
+ context = await chromium.launchPersistentContext(profileDir, {
774
+ headless: process.env.BROWSE_HEADED !== "1",
775
+ viewport: { width: 1920, height: 1080 },
776
+ ...this.customUserAgent ? { userAgent: this.customUserAgent } : {}
777
+ });
778
+ } catch (err) {
779
+ if (err.message?.includes("Failed to launch") || err.message?.includes("Target closed")) {
780
+ const fs16 = await import("fs");
781
+ console.error(`[browse] Profile directory corrupted, recreating: ${profileDir}`);
782
+ fs16.rmSync(profileDir, { recursive: true, force: true });
783
+ context = await chromium.launchPersistentContext(profileDir, {
784
+ headless: process.env.BROWSE_HEADED !== "1",
785
+ viewport: { width: 1920, height: 1080 }
786
+ });
787
+ } else {
788
+ throw err;
789
+ }
790
+ }
791
+ this.context = context;
792
+ this.browser = context.browser();
793
+ this.isPersistent = true;
794
+ this.ownsBrowser = true;
795
+ if (this.browser) {
796
+ this.browser.on("disconnected", () => {
797
+ if (onCrash) onCrash();
798
+ });
799
+ }
800
+ const pages = context.pages();
801
+ if (pages.length > 0) {
802
+ for (const page of pages) {
803
+ const tabId = this.nextTabId++;
804
+ this.wirePageEvents(page);
805
+ this.pages.set(tabId, page);
806
+ this.activeTabId = tabId;
807
+ }
808
+ } else {
809
+ await this.newTab();
810
+ }
811
+ }
671
812
  async close() {
813
+ if (this.isPersistent) {
814
+ this.pages.clear();
815
+ this.tabSnapshots.clear();
816
+ this.refMap.clear();
817
+ if (this.context) {
818
+ await this.context.close().catch(() => {
819
+ });
820
+ this.context = null;
821
+ this.browser = null;
822
+ }
823
+ return;
824
+ }
672
825
  for (const [, page] of this.pages) {
673
826
  await page.close().catch(() => {
674
827
  });
@@ -690,6 +843,9 @@ var init_browser_manager = __esm({
690
843
  isHealthy() {
691
844
  return this.browser !== null && this.browser.isConnected();
692
845
  }
846
+ getIsPersistent() {
847
+ return this.isPersistent;
848
+ }
693
849
  // ─── Tab Management ────────────────────────────────────────
694
850
  async newTab(url) {
695
851
  if (!this.context) throw new Error("Browser not launched");
@@ -950,6 +1106,11 @@ var init_browser_manager = __esm({
950
1106
  * Cannot preserve: localStorage/sessionStorage (bound to old context).
951
1107
  */
952
1108
  async recreateContext(contextOptions) {
1109
+ if (this.isPersistent) {
1110
+ throw new Error(
1111
+ "Cannot change device/viewport/user-agent in profile mode \u2014 profiles use a fixed browser context. Use --session instead."
1112
+ );
1113
+ }
953
1114
  if (!this.browser) return;
954
1115
  if (this.videoRecording && !contextOptions.recordVideo) {
955
1116
  contextOptions = {
@@ -1149,8 +1310,8 @@ var init_browser_manager = __esm({
1149
1310
  // ─── Video Recording ──────────────────────────────────────
1150
1311
  async startVideoRecording(dir) {
1151
1312
  if (this.videoRecording) throw new Error("Video recording already active");
1152
- const fs15 = await import("fs");
1153
- fs15.mkdirSync(dir, { recursive: true });
1313
+ const fs16 = await import("fs");
1314
+ fs16.mkdirSync(dir, { recursive: true });
1154
1315
  this.videoRecording = { dir, startedAt: Date.now() };
1155
1316
  const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
1156
1317
  await this.recreateContext({
@@ -1425,24 +1586,10 @@ var init_domain_filter = __esm({
1425
1586
  }
1426
1587
  });
1427
1588
 
1428
- // src/sanitize.ts
1429
- function sanitizeName(name) {
1430
- const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
1431
- if (!sanitized || /^[._]+$/.test(sanitized)) {
1432
- throw new Error(`Invalid name: "${name}"`);
1433
- }
1434
- return sanitized;
1435
- }
1436
- var init_sanitize = __esm({
1437
- "src/sanitize.ts"() {
1438
- "use strict";
1439
- }
1440
- });
1441
-
1442
1589
  // src/encryption.ts
1443
1590
  import * as crypto from "crypto";
1444
- import * as fs4 from "fs";
1445
- import * as path4 from "path";
1591
+ import * as fs5 from "fs";
1592
+ import * as path5 from "path";
1446
1593
  function resolveEncryptionKey(localDir) {
1447
1594
  const envKey = process.env.BROWSE_ENCRYPTION_KEY;
1448
1595
  if (envKey) {
@@ -1451,13 +1598,13 @@ function resolveEncryptionKey(localDir) {
1451
1598
  }
1452
1599
  return Buffer.from(envKey, "hex");
1453
1600
  }
1454
- const keyPath = path4.join(localDir, ".encryption-key");
1455
- if (fs4.existsSync(keyPath)) {
1456
- const hex = fs4.readFileSync(keyPath, "utf-8").trim();
1601
+ const keyPath = path5.join(localDir, ".encryption-key");
1602
+ if (fs5.existsSync(keyPath)) {
1603
+ const hex = fs5.readFileSync(keyPath, "utf-8").trim();
1457
1604
  return Buffer.from(hex, "hex");
1458
1605
  }
1459
1606
  const key = crypto.randomBytes(32);
1460
- fs4.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
1607
+ fs5.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
1461
1608
  return key;
1462
1609
  }
1463
1610
  function encrypt(plaintext, key) {
@@ -1497,8 +1644,8 @@ __export(session_persist_exports, {
1497
1644
  loadSessionState: () => loadSessionState,
1498
1645
  saveSessionState: () => saveSessionState
1499
1646
  });
1500
- import * as fs5 from "fs";
1501
- import * as path5 from "path";
1647
+ import * as fs6 from "fs";
1648
+ import * as path6 from "path";
1502
1649
  async function saveSessionState(sessionDir, context, encryptionKey) {
1503
1650
  try {
1504
1651
  const state = await context.storageState();
@@ -1510,20 +1657,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
1510
1657
  } else {
1511
1658
  content = json;
1512
1659
  }
1513
- fs5.mkdirSync(sessionDir, { recursive: true });
1514
- fs5.writeFileSync(path5.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
1660
+ fs6.mkdirSync(sessionDir, { recursive: true });
1661
+ fs6.writeFileSync(path6.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
1515
1662
  } catch (err) {
1516
1663
  console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
1517
1664
  }
1518
1665
  }
1519
1666
  async function loadSessionState(sessionDir, context, encryptionKey) {
1520
- const statePath = path5.join(sessionDir, STATE_FILENAME);
1521
- if (!fs5.existsSync(statePath)) {
1667
+ const statePath = path6.join(sessionDir, STATE_FILENAME);
1668
+ if (!fs6.existsSync(statePath)) {
1522
1669
  return false;
1523
1670
  }
1524
1671
  let stateData;
1525
1672
  try {
1526
- const raw = fs5.readFileSync(statePath, "utf-8");
1673
+ const raw = fs6.readFileSync(statePath, "utf-8");
1527
1674
  const parsed = JSON.parse(raw);
1528
1675
  if (parsed.encrypted) {
1529
1676
  if (!encryptionKey) {
@@ -1579,24 +1726,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
1579
1726
  }
1580
1727
  }
1581
1728
  function hasPersistedState(sessionDir) {
1582
- return fs5.existsSync(path5.join(sessionDir, STATE_FILENAME));
1729
+ return fs6.existsSync(path6.join(sessionDir, STATE_FILENAME));
1583
1730
  }
1584
1731
  function cleanOldStates(localDir, maxAgeDays) {
1585
1732
  const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
1586
1733
  const now = Date.now();
1587
1734
  let deleted = 0;
1588
- const statesDir = path5.join(localDir, "states");
1589
- if (fs5.existsSync(statesDir)) {
1735
+ const statesDir = path6.join(localDir, "states");
1736
+ if (fs6.existsSync(statesDir)) {
1590
1737
  try {
1591
- const entries = fs5.readdirSync(statesDir);
1738
+ const entries = fs6.readdirSync(statesDir);
1592
1739
  for (const entry of entries) {
1593
1740
  if (!entry.endsWith(".json")) continue;
1594
- const filePath = path5.join(statesDir, entry);
1741
+ const filePath = path6.join(statesDir, entry);
1595
1742
  try {
1596
- const stat = fs5.statSync(filePath);
1743
+ const stat = fs6.statSync(filePath);
1597
1744
  if (!stat.isFile()) continue;
1598
1745
  if (now - stat.mtimeMs > maxAgeMs) {
1599
- fs5.unlinkSync(filePath);
1746
+ fs6.unlinkSync(filePath);
1600
1747
  deleted++;
1601
1748
  }
1602
1749
  } catch (_) {
@@ -1605,23 +1752,23 @@ function cleanOldStates(localDir, maxAgeDays) {
1605
1752
  } catch (_) {
1606
1753
  }
1607
1754
  }
1608
- const sessionsDir = path5.join(localDir, "sessions");
1609
- if (fs5.existsSync(sessionsDir)) {
1755
+ const sessionsDir = path6.join(localDir, "sessions");
1756
+ if (fs6.existsSync(sessionsDir)) {
1610
1757
  try {
1611
- const sessionDirs = fs5.readdirSync(sessionsDir);
1758
+ const sessionDirs = fs6.readdirSync(sessionsDir);
1612
1759
  for (const dir of sessionDirs) {
1613
- const dirPath = path5.join(sessionsDir, dir);
1760
+ const dirPath = path6.join(sessionsDir, dir);
1614
1761
  try {
1615
- const dirStat = fs5.statSync(dirPath);
1762
+ const dirStat = fs6.statSync(dirPath);
1616
1763
  if (!dirStat.isDirectory()) continue;
1617
1764
  } catch (_) {
1618
1765
  continue;
1619
1766
  }
1620
- const statePath = path5.join(dirPath, STATE_FILENAME);
1767
+ const statePath = path6.join(dirPath, STATE_FILENAME);
1621
1768
  try {
1622
- const stat = fs5.statSync(statePath);
1769
+ const stat = fs6.statSync(statePath);
1623
1770
  if (now - stat.mtimeMs > maxAgeMs) {
1624
- fs5.unlinkSync(statePath);
1771
+ fs6.unlinkSync(statePath);
1625
1772
  deleted++;
1626
1773
  }
1627
1774
  } catch (_) {
@@ -1642,8 +1789,8 @@ var init_session_persist = __esm({
1642
1789
  });
1643
1790
 
1644
1791
  // src/session-manager.ts
1645
- import * as fs6 from "fs";
1646
- import * as path6 from "path";
1792
+ import * as fs7 from "fs";
1793
+ import * as path7 from "path";
1647
1794
  var SessionManager;
1648
1795
  var init_session_manager = __esm({
1649
1796
  "src/session-manager.ts"() {
@@ -1706,8 +1853,8 @@ var init_session_manager = __esm({
1706
1853
  }
1707
1854
  return session;
1708
1855
  }
1709
- const outputDir = path6.join(this.localDir, "sessions", sanitizeName(sessionId));
1710
- fs6.mkdirSync(outputDir, { recursive: true });
1856
+ const outputDir = path7.join(this.localDir, "sessions", sanitizeName(sessionId));
1857
+ fs7.mkdirSync(outputDir, { recursive: true });
1711
1858
  const buffers = new SessionBuffers();
1712
1859
  const manager = new BrowserManager(buffers);
1713
1860
  await manager.launchWithBrowser(this.browser);
@@ -1843,7 +1990,7 @@ var read_exports = {};
1843
1990
  __export(read_exports, {
1844
1991
  handleReadCommand: () => handleReadCommand
1845
1992
  });
1846
- import * as fs7 from "fs";
1993
+ import * as fs8 from "fs";
1847
1994
  async function handleReadCommand(command, args, bm, buffers) {
1848
1995
  const page = bm.getPage();
1849
1996
  const evalCtx = await bm.getFrameContext() || page;
@@ -1938,8 +2085,8 @@ async function handleReadCommand(command, args, bm, buffers) {
1938
2085
  case "eval": {
1939
2086
  const filePath = args[0];
1940
2087
  if (!filePath) throw new Error("Usage: browse eval <js-file>");
1941
- if (!fs7.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
1942
- const code = fs7.readFileSync(filePath, "utf-8");
2088
+ if (!fs8.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
2089
+ const code = fs8.readFileSync(filePath, "utf-8");
1943
2090
  const result = await evalCtx.evaluate(code);
1944
2091
  return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
1945
2092
  }
@@ -2190,7 +2337,7 @@ var write_exports = {};
2190
2337
  __export(write_exports, {
2191
2338
  handleWriteCommand: () => handleWriteCommand
2192
2339
  });
2193
- import * as fs8 from "fs";
2340
+ import * as fs9 from "fs";
2194
2341
  async function rebuildRoutes(context, bm, domainFilter) {
2195
2342
  await context.unrouteAll();
2196
2343
  for (const r of bm.getUserRoutes()) {
@@ -2394,14 +2541,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
2394
2541
  const file = args[1];
2395
2542
  if (!file) throw new Error("Usage: browse cookie export <file>");
2396
2543
  const cookies = await page.context().cookies();
2397
- fs8.writeFileSync(file, JSON.stringify(cookies, null, 2));
2544
+ fs9.writeFileSync(file, JSON.stringify(cookies, null, 2));
2398
2545
  return `Exported ${cookies.length} cookie(s) to ${file}`;
2399
2546
  }
2400
2547
  if (cookieStr === "import") {
2401
2548
  const file = args[1];
2402
2549
  if (!file) throw new Error("Usage: browse cookie import <file>");
2403
- if (!fs8.existsSync(file)) throw new Error(`File not found: ${file}`);
2404
- const cookies = JSON.parse(fs8.readFileSync(file, "utf-8"));
2550
+ if (!fs9.existsSync(file)) throw new Error(`File not found: ${file}`);
2551
+ const cookies = JSON.parse(fs9.readFileSync(file, "utf-8"));
2405
2552
  if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
2406
2553
  await page.context().addCookies(cookies);
2407
2554
  return `Imported ${cookies.length} cookie(s) from ${file}`;
@@ -2468,7 +2615,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
2468
2615
  const [selector, ...filePaths] = args;
2469
2616
  if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
2470
2617
  for (const fp of filePaths) {
2471
- if (!fs8.existsSync(fp)) throw new Error(`File not found: ${fp}`);
2618
+ if (!fs9.existsSync(fp)) throw new Error(`File not found: ${fp}`);
2472
2619
  }
2473
2620
  const resolved = bm.resolveRef(selector);
2474
2621
  if ("locator" in resolved) {
@@ -3535,11 +3682,11 @@ var init_lib = __esm({
3535
3682
  }
3536
3683
  }
3537
3684
  },
3538
- addToPath: function addToPath(path12, added, removed, oldPosInc, options) {
3539
- var last = path12.lastComponent;
3685
+ addToPath: function addToPath(path13, added, removed, oldPosInc, options) {
3686
+ var last = path13.lastComponent;
3540
3687
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
3541
3688
  return {
3542
- oldPos: path12.oldPos + oldPosInc,
3689
+ oldPos: path13.oldPos + oldPosInc,
3543
3690
  lastComponent: {
3544
3691
  count: last.count + 1,
3545
3692
  added,
@@ -3549,7 +3696,7 @@ var init_lib = __esm({
3549
3696
  };
3550
3697
  } else {
3551
3698
  return {
3552
- oldPos: path12.oldPos + oldPosInc,
3699
+ oldPos: path13.oldPos + oldPosInc,
3553
3700
  lastComponent: {
3554
3701
  count: 1,
3555
3702
  added,
@@ -3607,7 +3754,7 @@ var init_lib = __esm({
3607
3754
  tokenize: function tokenize(value) {
3608
3755
  return Array.from(value);
3609
3756
  },
3610
- join: function join8(chars) {
3757
+ join: function join9(chars) {
3611
3758
  return chars.join("");
3612
3759
  },
3613
3760
  postProcess: function postProcess(changeObjects) {
@@ -3772,14 +3919,14 @@ var policy_exports = {};
3772
3919
  __export(policy_exports, {
3773
3920
  PolicyChecker: () => PolicyChecker
3774
3921
  });
3775
- import * as fs9 from "fs";
3776
- import * as path7 from "path";
3922
+ import * as fs10 from "fs";
3923
+ import * as path8 from "path";
3777
3924
  function findFileUpward(filename) {
3778
3925
  let dir = process.cwd();
3779
3926
  for (let i = 0; i < 20; i++) {
3780
- const candidate = path7.join(dir, filename);
3781
- if (fs9.existsSync(candidate)) return candidate;
3782
- const parent = path7.dirname(dir);
3927
+ const candidate = path8.join(dir, filename);
3928
+ if (fs10.existsSync(candidate)) return candidate;
3929
+ const parent = path8.dirname(dir);
3783
3930
  if (parent === dir) break;
3784
3931
  dir = parent;
3785
3932
  }
@@ -3807,10 +3954,10 @@ var init_policy = __esm({
3807
3954
  reload() {
3808
3955
  if (!this.filePath) return;
3809
3956
  try {
3810
- const stat = fs9.statSync(this.filePath);
3957
+ const stat = fs10.statSync(this.filePath);
3811
3958
  if (stat.mtimeMs === this.lastMtime) return;
3812
3959
  this.lastMtime = stat.mtimeMs;
3813
- const raw = fs9.readFileSync(this.filePath, "utf-8");
3960
+ const raw = fs10.readFileSync(this.filePath, "utf-8");
3814
3961
  this.policy = JSON.parse(raw);
3815
3962
  } catch {
3816
3963
  }
@@ -4029,8 +4176,8 @@ var auth_vault_exports = {};
4029
4176
  __export(auth_vault_exports, {
4030
4177
  AuthVault: () => AuthVault
4031
4178
  });
4032
- import * as fs10 from "fs";
4033
- import * as path8 from "path";
4179
+ import * as fs11 from "fs";
4180
+ import * as path9 from "path";
4034
4181
  async function autoDetectSelector(page, field) {
4035
4182
  if (field === "username") {
4036
4183
  const candidates2 = [
@@ -4088,11 +4235,11 @@ var init_auth_vault = __esm({
4088
4235
  authDir;
4089
4236
  encryptionKey;
4090
4237
  constructor(localDir) {
4091
- this.authDir = path8.join(localDir, "auth");
4238
+ this.authDir = path9.join(localDir, "auth");
4092
4239
  this.encryptionKey = resolveEncryptionKey(localDir);
4093
4240
  }
4094
4241
  save(name, url, username, password, selectors) {
4095
- fs10.mkdirSync(this.authDir, { recursive: true });
4242
+ fs11.mkdirSync(this.authDir, { recursive: true });
4096
4243
  const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
4097
4244
  const now = (/* @__PURE__ */ new Date()).toISOString();
4098
4245
  const credential = {
@@ -4109,15 +4256,15 @@ var init_auth_vault = __esm({
4109
4256
  createdAt: now,
4110
4257
  updatedAt: now
4111
4258
  };
4112
- const filePath = path8.join(this.authDir, `${sanitizeName(name)}.json`);
4113
- fs10.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
4259
+ const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4260
+ fs11.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
4114
4261
  }
4115
4262
  load(name) {
4116
- const filePath = path8.join(this.authDir, `${sanitizeName(name)}.json`);
4117
- if (!fs10.existsSync(filePath)) {
4263
+ const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4264
+ if (!fs11.existsSync(filePath)) {
4118
4265
  throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
4119
4266
  }
4120
- return JSON.parse(fs10.readFileSync(filePath, "utf-8"));
4267
+ return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
4121
4268
  }
4122
4269
  async login(name, bm) {
4123
4270
  const cred = this.load(name);
@@ -4138,11 +4285,11 @@ var init_auth_vault = __esm({
4138
4285
  return `Logged in as ${cred.username} at ${page.url()}`;
4139
4286
  }
4140
4287
  list() {
4141
- if (!fs10.existsSync(this.authDir)) return [];
4142
- const files = fs10.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
4288
+ if (!fs11.existsSync(this.authDir)) return [];
4289
+ const files = fs11.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
4143
4290
  return files.map((f) => {
4144
4291
  try {
4145
- const data = JSON.parse(fs10.readFileSync(path8.join(this.authDir, f), "utf-8"));
4292
+ const data = JSON.parse(fs11.readFileSync(path9.join(this.authDir, f), "utf-8"));
4146
4293
  return {
4147
4294
  name: data.name,
4148
4295
  url: data.url,
@@ -4156,11 +4303,11 @@ var init_auth_vault = __esm({
4156
4303
  }).filter(Boolean);
4157
4304
  }
4158
4305
  delete(name) {
4159
- const filePath = path8.join(this.authDir, `${sanitizeName(name)}.json`);
4160
- if (!fs10.existsSync(filePath)) {
4306
+ const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4307
+ if (!fs11.existsSync(filePath)) {
4161
4308
  throw new Error(`Credential "${name}" not found.`);
4162
4309
  }
4163
- fs10.unlinkSync(filePath);
4310
+ fs11.unlinkSync(filePath);
4164
4311
  }
4165
4312
  };
4166
4313
  }
@@ -4177,15 +4324,15 @@ __export(cookie_import_exports, {
4177
4324
  import Database from "better-sqlite3";
4178
4325
  import { spawn as spawn2 } from "child_process";
4179
4326
  import * as crypto2 from "crypto";
4180
- import * as fs11 from "fs";
4181
- import * as path9 from "path";
4327
+ import * as fs12 from "fs";
4328
+ import * as path10 from "path";
4182
4329
  import * as os2 from "os";
4183
4330
  function findInstalledBrowsers() {
4184
- const appSupport = path9.join(os2.homedir(), "Library", "Application Support");
4331
+ const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
4185
4332
  return BROWSER_REGISTRY.filter((b) => {
4186
- const dbPath = path9.join(appSupport, b.dataDir, "Default", "Cookies");
4333
+ const dbPath = path10.join(appSupport, b.dataDir, "Default", "Cookies");
4187
4334
  try {
4188
- return fs11.existsSync(dbPath);
4335
+ return fs12.existsSync(dbPath);
4189
4336
  } catch {
4190
4337
  return false;
4191
4338
  }
@@ -4268,9 +4415,9 @@ function validateProfile(profile) {
4268
4415
  }
4269
4416
  function getCookieDbPath(browser2, profile) {
4270
4417
  validateProfile(profile);
4271
- const appSupport = path9.join(os2.homedir(), "Library", "Application Support");
4272
- const dbPath = path9.join(appSupport, browser2.dataDir, profile, "Cookies");
4273
- if (!fs11.existsSync(dbPath)) {
4418
+ const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
4419
+ const dbPath = path10.join(appSupport, browser2.dataDir, profile, "Cookies");
4420
+ if (!fs12.existsSync(dbPath)) {
4274
4421
  throw new CookieImportError(
4275
4422
  `${browser2.name} is not installed (no cookie database at ${dbPath})`,
4276
4423
  "not_installed"
@@ -4297,32 +4444,32 @@ function openDb(dbPath, browserName) {
4297
4444
  function openDbFromCopy(dbPath, browserName) {
4298
4445
  const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto2.randomUUID()}.db`;
4299
4446
  try {
4300
- fs11.copyFileSync(dbPath, tmpPath);
4447
+ fs12.copyFileSync(dbPath, tmpPath);
4301
4448
  const walPath = dbPath + "-wal";
4302
4449
  const shmPath = dbPath + "-shm";
4303
- if (fs11.existsSync(walPath)) fs11.copyFileSync(walPath, tmpPath + "-wal");
4304
- if (fs11.existsSync(shmPath)) fs11.copyFileSync(shmPath, tmpPath + "-shm");
4450
+ if (fs12.existsSync(walPath)) fs12.copyFileSync(walPath, tmpPath + "-wal");
4451
+ if (fs12.existsSync(shmPath)) fs12.copyFileSync(shmPath, tmpPath + "-shm");
4305
4452
  const db = new Database(tmpPath, { readonly: true });
4306
4453
  const origClose = db.close.bind(db);
4307
4454
  db.close = (() => {
4308
4455
  origClose();
4309
4456
  try {
4310
- fs11.unlinkSync(tmpPath);
4457
+ fs12.unlinkSync(tmpPath);
4311
4458
  } catch {
4312
4459
  }
4313
4460
  try {
4314
- fs11.unlinkSync(tmpPath + "-wal");
4461
+ fs12.unlinkSync(tmpPath + "-wal");
4315
4462
  } catch {
4316
4463
  }
4317
4464
  try {
4318
- fs11.unlinkSync(tmpPath + "-shm");
4465
+ fs12.unlinkSync(tmpPath + "-shm");
4319
4466
  } catch {
4320
4467
  }
4321
4468
  });
4322
4469
  return db;
4323
4470
  } catch {
4324
4471
  try {
4325
- fs11.unlinkSync(tmpPath);
4472
+ fs12.unlinkSync(tmpPath);
4326
4473
  } catch {
4327
4474
  }
4328
4475
  throw new CookieImportError(
@@ -4620,7 +4767,7 @@ var init_record_export = __esm({
4620
4767
  });
4621
4768
 
4622
4769
  // src/commands/meta.ts
4623
- import * as fs12 from "fs";
4770
+ import * as fs13 from "fs";
4624
4771
  async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2, currentSession) {
4625
4772
  switch (command) {
4626
4773
  // ─── Tabs ──────────────────────────────────────────
@@ -4702,7 +4849,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
4702
4849
  const lines = entries.map(
4703
4850
  (e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
4704
4851
  ).join("\n") + "\n";
4705
- fs12.appendFileSync(consolePath, lines);
4852
+ fs13.appendFileSync(consolePath, lines);
4706
4853
  buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
4707
4854
  }
4708
4855
  const newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
@@ -4712,7 +4859,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
4712
4859
  const lines = entries.map(
4713
4860
  (e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
4714
4861
  ).join("\n") + "\n";
4715
- fs12.appendFileSync(networkPath, lines);
4862
+ fs13.appendFileSync(networkPath, lines);
4716
4863
  buffers.lastNetworkFlushed = buffers.networkTotalAdded;
4717
4864
  }
4718
4865
  }
@@ -4729,22 +4876,22 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
4729
4876
  const statesDir = `${LOCAL_DIR}/states`;
4730
4877
  const statePath = `${statesDir}/${name}.json`;
4731
4878
  if (subcommand === "list") {
4732
- if (!fs12.existsSync(statesDir)) return "(no saved states)";
4733
- const files = fs12.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
4879
+ if (!fs13.existsSync(statesDir)) return "(no saved states)";
4880
+ const files = fs13.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
4734
4881
  if (files.length === 0) return "(no saved states)";
4735
4882
  const lines = [];
4736
4883
  for (const file of files) {
4737
4884
  const fp = `${statesDir}/${file}`;
4738
- const stat = fs12.statSync(fp);
4885
+ const stat = fs13.statSync(fp);
4739
4886
  lines.push(` ${file.replace(".json", "")} ${stat.size}B ${new Date(stat.mtimeMs).toISOString()}`);
4740
4887
  }
4741
4888
  return lines.join("\n");
4742
4889
  }
4743
4890
  if (subcommand === "show") {
4744
- if (!fs12.existsSync(statePath)) {
4891
+ if (!fs13.existsSync(statePath)) {
4745
4892
  throw new Error(`State file not found: ${statePath}`);
4746
4893
  }
4747
- const data = JSON.parse(fs12.readFileSync(statePath, "utf-8"));
4894
+ const data = JSON.parse(fs13.readFileSync(statePath, "utf-8"));
4748
4895
  const cookieCount = data.cookies?.length || 0;
4749
4896
  const originCount = data.origins?.length || 0;
4750
4897
  const storageItems = (data.origins || []).reduce((sum, o) => sum + (o.localStorage?.length || 0), 0);
@@ -4769,15 +4916,15 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
4769
4916
  const context = bm.getContext();
4770
4917
  if (!context) throw new Error("No browser context");
4771
4918
  const state = await context.storageState();
4772
- fs12.mkdirSync(statesDir, { recursive: true });
4773
- fs12.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
4919
+ fs13.mkdirSync(statesDir, { recursive: true });
4920
+ fs13.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
4774
4921
  return `State saved: ${statePath}`;
4775
4922
  }
4776
4923
  if (subcommand === "load") {
4777
- if (!fs12.existsSync(statePath)) {
4924
+ if (!fs13.existsSync(statePath)) {
4778
4925
  throw new Error(`State file not found: ${statePath}. Run "browse state save ${name}" first.`);
4779
4926
  }
4780
- const stateData = JSON.parse(fs12.readFileSync(statePath, "utf-8"));
4927
+ const stateData = JSON.parse(fs13.readFileSync(statePath, "utf-8"));
4781
4928
  const context = bm.getContext();
4782
4929
  if (!context) throw new Error("No browser context");
4783
4930
  const warnings = [];
@@ -4936,9 +5083,9 @@ ${legend.join("\n")}`;
4936
5083
  try {
4937
5084
  for (const vp of viewports) {
4938
5085
  await page.setViewportSize({ width: vp.width, height: vp.height });
4939
- const path12 = `${prefix}-${vp.name}.png`;
4940
- await page.screenshot({ path: path12, fullPage: true });
4941
- results.push(`${vp.name} (${vp.width}x${vp.height}): ${path12}`);
5086
+ const path13 = `${prefix}-${vp.name}.png`;
5087
+ await page.screenshot({ path: path13, fullPage: true });
5088
+ results.push(`${vp.name} (${vp.width}x${vp.height}): ${path13}`);
4942
5089
  }
4943
5090
  } finally {
4944
5091
  if (originalViewport) {
@@ -5082,13 +5229,13 @@ ${legend.join("\n")}`;
5082
5229
  const diffArgs = args.filter((a) => a !== "--full");
5083
5230
  const baseline = diffArgs[0];
5084
5231
  if (!baseline) throw new Error("Usage: browse screenshot-diff <baseline> [current] [--threshold 0.1] [--full]");
5085
- if (!fs12.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
5232
+ if (!fs13.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
5086
5233
  let thresholdPct = 0.1;
5087
5234
  const threshIdx = diffArgs.indexOf("--threshold");
5088
5235
  if (threshIdx !== -1 && diffArgs[threshIdx + 1]) {
5089
5236
  thresholdPct = parseFloat(diffArgs[threshIdx + 1]);
5090
5237
  }
5091
- const baselineBuffer = fs12.readFileSync(baseline);
5238
+ const baselineBuffer = fs13.readFileSync(baseline);
5092
5239
  let currentBuffer;
5093
5240
  let currentPath;
5094
5241
  for (let i = 1; i < diffArgs.length; i++) {
@@ -5102,8 +5249,8 @@ ${legend.join("\n")}`;
5102
5249
  }
5103
5250
  }
5104
5251
  if (currentPath) {
5105
- if (!fs12.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
5106
- currentBuffer = fs12.readFileSync(currentPath);
5252
+ if (!fs13.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
5253
+ currentBuffer = fs13.readFileSync(currentPath);
5107
5254
  } else {
5108
5255
  const page = bm.getPage();
5109
5256
  currentBuffer = await page.screenshot({ fullPage: isFullPageDiff });
@@ -5113,7 +5260,7 @@ ${legend.join("\n")}`;
5113
5260
  const extIdx = baseline.lastIndexOf(".");
5114
5261
  const diffPath = extIdx > 0 ? baseline.slice(0, extIdx) + "-diff" + baseline.slice(extIdx) : baseline + "-diff.png";
5115
5262
  if (!result.passed && result.diffImage) {
5116
- fs12.writeFileSync(diffPath, result.diffImage);
5263
+ fs13.writeFileSync(diffPath, result.diffImage);
5117
5264
  }
5118
5265
  return [
5119
5266
  `Pixels: ${result.totalPixels}`,
@@ -5249,7 +5396,7 @@ ${legend.join("\n")}`;
5249
5396
  const { formatAsHar: formatAsHar2 } = await Promise.resolve().then(() => (init_har(), har_exports));
5250
5397
  const har = formatAsHar2(sessionBuffers.networkBuffer, recording.startTime);
5251
5398
  const harPath = args[1] || (currentSession ? `${currentSession.outputDir}/recording.har` : `${LOCAL_DIR}/browse-recording.har`);
5252
- fs12.writeFileSync(harPath, JSON.stringify(har, null, 2));
5399
+ fs13.writeFileSync(harPath, JSON.stringify(har, null, 2));
5253
5400
  const entryCount = har.log.entries.length;
5254
5401
  return `HAR saved: ${harPath} (${entryCount} entries)`;
5255
5402
  }
@@ -5469,13 +5616,52 @@ Manual: npm install -g @ulpi/browse`;
5469
5616
  }
5470
5617
  const filePath = args[2];
5471
5618
  if (filePath) {
5472
- fs12.writeFileSync(filePath, output);
5619
+ fs13.writeFileSync(filePath, output);
5473
5620
  return `Exported ${steps.length} steps as ${format}: ${filePath}`;
5474
5621
  }
5475
5622
  return output;
5476
5623
  }
5477
5624
  throw new Error("Usage: browse record start | stop | status | export browse|replay [path]");
5478
5625
  }
5626
+ // ─── Profile Management ────────────────────────────────
5627
+ case "profile": {
5628
+ const subcommand = args[0];
5629
+ if (!subcommand) throw new Error("Usage: browse profile list | delete <name> | clean [--older-than <days>]");
5630
+ if (subcommand === "list") {
5631
+ const { listProfiles: listProfiles2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
5632
+ const profiles = listProfiles2(LOCAL_DIR);
5633
+ if (profiles.length === 0) return "No profiles found";
5634
+ return profiles.map((p) => `${p.name} ${p.size} last used: ${p.lastUsed}`).join("\n");
5635
+ }
5636
+ if (subcommand === "delete") {
5637
+ const name = args[1];
5638
+ if (!name) throw new Error("Usage: browse profile delete <name>");
5639
+ const { deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
5640
+ deleteProfile2(LOCAL_DIR, name);
5641
+ return `Profile "${name}" deleted`;
5642
+ }
5643
+ if (subcommand === "clean") {
5644
+ const { listProfiles: listProfiles2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
5645
+ let maxDays = 7;
5646
+ const olderIdx = args.indexOf("--older-than");
5647
+ if (olderIdx !== -1 && args[olderIdx + 1]) {
5648
+ maxDays = parseInt(args[olderIdx + 1], 10);
5649
+ if (isNaN(maxDays)) throw new Error("Usage: browse profile clean --older-than <days>");
5650
+ }
5651
+ const profiles = listProfiles2(LOCAL_DIR);
5652
+ const cutoff = /* @__PURE__ */ new Date();
5653
+ cutoff.setDate(cutoff.getDate() - maxDays);
5654
+ let cleaned = 0;
5655
+ for (const p of profiles) {
5656
+ if (new Date(p.lastUsed) < cutoff) {
5657
+ deleteProfile2(LOCAL_DIR, p.name);
5658
+ cleaned++;
5659
+ }
5660
+ }
5661
+ return cleaned > 0 ? `Cleaned ${cleaned} profile(s) older than ${maxDays} days` : "No profiles to clean";
5662
+ }
5663
+ throw new Error("Usage: browse profile list | delete <name> | clean [--older-than <days>]");
5664
+ }
5479
5665
  default:
5480
5666
  throw new Error(`Unknown meta command: ${command}`);
5481
5667
  }
@@ -5494,8 +5680,8 @@ var init_meta = __esm({
5494
5680
 
5495
5681
  // src/server.ts
5496
5682
  var server_exports = {};
5497
- import * as fs13 from "fs";
5498
- import * as path10 from "path";
5683
+ import * as fs14 from "fs";
5684
+ import * as path11 from "path";
5499
5685
  import * as crypto3 from "crypto";
5500
5686
  import * as http from "http";
5501
5687
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -5531,9 +5717,13 @@ function validateAuth(req) {
5531
5717
  const header = req.headers.get("authorization");
5532
5718
  return header === `Bearer ${AUTH_TOKEN}`;
5533
5719
  }
5534
- function flushAllBuffers(sessionManager2, final = false) {
5535
- for (const session of sessionManager2.getAllSessions()) {
5536
- flushSessionBuffers(session, final);
5720
+ function flushAllBuffers(sm, final = false) {
5721
+ if (sm) {
5722
+ for (const session of sm.getAllSessions()) {
5723
+ flushSessionBuffers(session, final);
5724
+ }
5725
+ } else if (profileSession) {
5726
+ flushSessionBuffers(profileSession, final);
5537
5727
  }
5538
5728
  }
5539
5729
  function flushSessionBuffers(session, final) {
@@ -5547,7 +5737,7 @@ function flushSessionBuffers(session, final) {
5547
5737
  const lines = newEntries.map(
5548
5738
  (e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
5549
5739
  ).join("\n") + "\n";
5550
- fs13.appendFileSync(consolePath, lines);
5740
+ fs14.appendFileSync(consolePath, lines);
5551
5741
  buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
5552
5742
  }
5553
5743
  let newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
@@ -5572,7 +5762,7 @@ function flushSessionBuffers(session, final) {
5572
5762
  const lines = prefix.map(
5573
5763
  (e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
5574
5764
  ).join("\n") + "\n";
5575
- fs13.appendFileSync(networkPath, lines);
5765
+ fs14.appendFileSync(networkPath, lines);
5576
5766
  buffers.lastNetworkFlushed += prefixLen;
5577
5767
  }
5578
5768
  }
@@ -5686,7 +5876,7 @@ async function handleCommand(body, session, opts) {
5686
5876
  } else if (WRITE_COMMANDS.has(command)) {
5687
5877
  result = await handleWriteCommand(command, args, session.manager, session.domainFilter);
5688
5878
  } else if (META_COMMANDS.has(command)) {
5689
- result = await handleMetaCommand(command, args, session.manager, shutdown, sessionManager, session);
5879
+ result = await handleMetaCommand(command, args, session.manager, shutdown, sessionManager ?? void 0, session);
5690
5880
  } else {
5691
5881
  const error = `Unknown command: ${command}`;
5692
5882
  const hint = `Available commands: ${[...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS].sort().join(", ")}`;
@@ -5745,18 +5935,23 @@ async function shutdown() {
5745
5935
  clearInterval(flushInterval);
5746
5936
  clearInterval(sessionCleanupInterval);
5747
5937
  flushAllBuffers(sessionManager, true);
5748
- await sessionManager.closeAll();
5749
- if (browser && !isRemoteBrowser) {
5750
- browser.removeAllListeners("disconnected");
5751
- await browser.close().catch(() => {
5938
+ if (profileSession) {
5939
+ await profileSession.manager.close().catch(() => {
5752
5940
  });
5941
+ } else if (sessionManager) {
5942
+ await sessionManager.closeAll();
5943
+ if (browser && !isRemoteBrowser) {
5944
+ browser.removeAllListeners("disconnected");
5945
+ await browser.close().catch(() => {
5946
+ });
5947
+ }
5753
5948
  }
5754
5949
  await activeRuntime?.close?.().catch(() => {
5755
5950
  });
5756
5951
  try {
5757
- const currentState = JSON.parse(fs13.readFileSync(STATE_FILE, "utf-8"));
5952
+ const currentState = JSON.parse(fs14.readFileSync(STATE_FILE, "utf-8"));
5758
5953
  if (currentState.pid === process.pid || currentState.token === AUTH_TOKEN) {
5759
- fs13.unlinkSync(STATE_FILE);
5954
+ fs14.unlinkSync(STATE_FILE);
5760
5955
  }
5761
5956
  } catch {
5762
5957
  }
@@ -5768,38 +5963,65 @@ async function start() {
5768
5963
  const runtime = await getRuntime(runtimeName);
5769
5964
  activeRuntime = runtime;
5770
5965
  console.log(`[browse] Runtime: ${runtime.name}`);
5771
- const cdpUrl = process.env.BROWSE_CDP_URL;
5772
- if (cdpUrl) {
5773
- browser = await runtime.chromium.connectOverCDP(cdpUrl);
5774
- isRemoteBrowser = true;
5775
- console.log(`[browse] Connected to remote Chrome via CDP: ${cdpUrl}`);
5776
- } else if (runtime.browser) {
5777
- browser = runtime.browser;
5778
- browser.on("disconnected", () => {
5966
+ const profileName = process.env.BROWSE_PROFILE;
5967
+ if (profileName) {
5968
+ const { BrowserManager: BrowserManager2, getProfileDir: getProfileDir2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
5969
+ const { SessionBuffers: SessionBuffers2 } = await Promise.resolve().then(() => (init_buffers(), buffers_exports));
5970
+ const profileDir = getProfileDir2(LOCAL_DIR2, profileName);
5971
+ fs14.mkdirSync(profileDir, { recursive: true });
5972
+ const bm = new BrowserManager2();
5973
+ await bm.launchPersistent(profileDir, () => {
5779
5974
  if (isShuttingDown) return;
5780
- console.error("[browse] Browser disconnected. Shutting down.");
5975
+ console.error("[browse] Chromium disconnected (profile mode). Shutting down.");
5781
5976
  shutdown();
5782
5977
  });
5978
+ const outputDir = path11.join(LOCAL_DIR2, "sessions", profileName);
5979
+ fs14.mkdirSync(outputDir, { recursive: true });
5980
+ profileSession = {
5981
+ id: profileName,
5982
+ manager: bm,
5983
+ buffers: new SessionBuffers2(),
5984
+ domainFilter: null,
5985
+ recording: null,
5986
+ outputDir,
5987
+ lastActivity: Date.now(),
5988
+ createdAt: Date.now()
5989
+ };
5990
+ console.log(`[browse] Profile mode: "${profileName}" (${profileDir})`);
5783
5991
  } else {
5784
- const launchOptions = { headless: process.env.BROWSE_HEADED !== "1" };
5785
- if (DEBUG_PORT > 0) {
5786
- launchOptions.args = [`--remote-debugging-port=${DEBUG_PORT}`];
5787
- }
5788
- const proxyServer = process.env.BROWSE_PROXY;
5789
- if (proxyServer) {
5790
- launchOptions.proxy = { server: proxyServer };
5791
- if (process.env.BROWSE_PROXY_BYPASS) {
5792
- launchOptions.proxy.bypass = process.env.BROWSE_PROXY_BYPASS;
5992
+ const cdpUrl = process.env.BROWSE_CDP_URL;
5993
+ if (cdpUrl) {
5994
+ browser = await runtime.chromium.connectOverCDP(cdpUrl);
5995
+ isRemoteBrowser = true;
5996
+ console.log(`[browse] Connected to remote Chrome via CDP: ${cdpUrl}`);
5997
+ } else if (runtime.browser) {
5998
+ browser = runtime.browser;
5999
+ browser.on("disconnected", () => {
6000
+ if (isShuttingDown) return;
6001
+ console.error("[browse] Browser disconnected. Shutting down.");
6002
+ shutdown();
6003
+ });
6004
+ } else {
6005
+ const launchOptions = { headless: process.env.BROWSE_HEADED !== "1" };
6006
+ if (DEBUG_PORT > 0) {
6007
+ launchOptions.args = [`--remote-debugging-port=${DEBUG_PORT}`];
5793
6008
  }
6009
+ const proxyServer = process.env.BROWSE_PROXY;
6010
+ if (proxyServer) {
6011
+ launchOptions.proxy = { server: proxyServer };
6012
+ if (process.env.BROWSE_PROXY_BYPASS) {
6013
+ launchOptions.proxy.bypass = process.env.BROWSE_PROXY_BYPASS;
6014
+ }
6015
+ }
6016
+ browser = await runtime.chromium.launch(launchOptions);
6017
+ browser.on("disconnected", () => {
6018
+ if (isShuttingDown) return;
6019
+ console.error("[browse] Chromium disconnected. Shutting down.");
6020
+ shutdown();
6021
+ });
5794
6022
  }
5795
- browser = await runtime.chromium.launch(launchOptions);
5796
- browser.on("disconnected", () => {
5797
- if (isShuttingDown) return;
5798
- console.error("[browse] Chromium disconnected. Shutting down.");
5799
- shutdown();
5800
- });
6023
+ sessionManager = new SessionManager(browser, LOCAL_DIR2);
5801
6024
  }
5802
- sessionManager = new SessionManager(browser, LOCAL_DIR2);
5803
6025
  const startTime = Date.now();
5804
6026
  const server = nodeServe({
5805
6027
  port,
@@ -5807,11 +6029,20 @@ async function start() {
5807
6029
  fetch: async (req) => {
5808
6030
  const url = new URL(req.url);
5809
6031
  if (url.pathname === "/health") {
5810
- const healthy = !isShuttingDown && browser.isConnected();
6032
+ let healthy;
6033
+ let sessionCount;
6034
+ if (profileSession) {
6035
+ healthy = !isShuttingDown && !!profileSession.manager.getContext();
6036
+ sessionCount = 1;
6037
+ } else {
6038
+ healthy = !isShuttingDown && !!browser && browser.isConnected();
6039
+ sessionCount = sessionManager ? sessionManager.getSessionCount() : 0;
6040
+ }
5811
6041
  return new Response(JSON.stringify({
5812
6042
  status: healthy ? "healthy" : "unhealthy",
5813
6043
  uptime: Math.floor((Date.now() - startTime) / 1e3),
5814
- sessions: sessionManager.getSessionCount()
6044
+ sessions: sessionCount,
6045
+ ...profileName ? { profile: profileName } : {}
5815
6046
  }), {
5816
6047
  status: 200,
5817
6048
  headers: { "Content-Type": "application/json" }
@@ -5825,15 +6056,21 @@ async function start() {
5825
6056
  }
5826
6057
  if (url.pathname === "/command" && req.method === "POST") {
5827
6058
  const body = await req.json();
5828
- const sessionId = req.headers.get("x-browse-session") || "default";
5829
- const allowedDomains = req.headers.get("x-browse-allowed-domains") || void 0;
5830
- const session = await sessionManager.getOrCreate(sessionId, allowedDomains);
6059
+ let session;
6060
+ if (profileSession) {
6061
+ session = profileSession;
6062
+ session.lastActivity = Date.now();
6063
+ } else {
6064
+ const sessionId = req.headers.get("x-browse-session") || "default";
6065
+ const allowedDomains = req.headers.get("x-browse-allowed-domains") || void 0;
6066
+ session = await sessionManager.getOrCreate(sessionId, allowedDomains);
6067
+ }
5831
6068
  const stateFilePath = req.headers.get("x-browse-state");
5832
6069
  if (stateFilePath) {
5833
6070
  const context = session.manager.getContext();
5834
6071
  if (context) {
5835
6072
  try {
5836
- const stateData = JSON.parse(fs13.readFileSync(stateFilePath, "utf-8"));
6073
+ const stateData = JSON.parse(fs14.readFileSync(stateFilePath, "utf-8"));
5837
6074
  if (stateData.cookies?.length) {
5838
6075
  await context.addCookies(stateData.cookies);
5839
6076
  }
@@ -5860,17 +6097,20 @@ async function start() {
5860
6097
  port,
5861
6098
  token: AUTH_TOKEN,
5862
6099
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5863
- serverPath: path10.resolve(path10.dirname(fileURLToPath2(import.meta.url)), "server.ts")
6100
+ serverPath: path11.resolve(path11.dirname(fileURLToPath2(import.meta.url)), "server.ts")
5864
6101
  };
6102
+ if (profileName) {
6103
+ state.profile = profileName;
6104
+ }
5865
6105
  if (DEBUG_PORT > 0) {
5866
6106
  state.debugPort = DEBUG_PORT;
5867
6107
  }
5868
- fs13.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
6108
+ fs14.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
5869
6109
  console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
5870
6110
  console.log(`[browse] State file: ${STATE_FILE}`);
5871
6111
  console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1e3}s`);
5872
6112
  }
5873
- var AUTH_TOKEN, DEBUG_PORT, BROWSE_PORT, BROWSE_INSTANCE, INSTANCE_SUFFIX, LOCAL_DIR2, STATE_FILE, IDLE_TIMEOUT_MS, sessionManager, browser, activeRuntime, isShuttingDown, isRemoteBrowser, policyChecker, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, RECORDING_SKIP, PAGE_CONTENT_COMMANDS, BOUNDARY_NONCE, flushInterval, sessionCleanupInterval;
6113
+ var AUTH_TOKEN, DEBUG_PORT, BROWSE_PORT, BROWSE_INSTANCE, INSTANCE_SUFFIX, LOCAL_DIR2, STATE_FILE, IDLE_TIMEOUT_MS, sessionManager, browser, profileSession, activeRuntime, isShuttingDown, isRemoteBrowser, policyChecker, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, RECORDING_SKIP, PAGE_CONTENT_COMMANDS, BOUNDARY_NONCE, flushInterval, sessionCleanupInterval;
5874
6114
  var init_server = __esm({
5875
6115
  "src/server.ts"() {
5876
6116
  "use strict";
@@ -5889,6 +6129,9 @@ var init_server = __esm({
5889
6129
  LOCAL_DIR2 = process.env.BROWSE_LOCAL_DIR || "/tmp";
5890
6130
  STATE_FILE = process.env.BROWSE_STATE_FILE || `${LOCAL_DIR2}/browse-server${INSTANCE_SUFFIX}.json`;
5891
6131
  IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || String(DEFAULTS.IDLE_TIMEOUT_MS), 10);
6132
+ sessionManager = null;
6133
+ browser = null;
6134
+ profileSession = null;
5892
6135
  isShuttingDown = false;
5893
6136
  isRemoteBrowser = false;
5894
6137
  policyChecker = new PolicyChecker();
@@ -5986,7 +6229,8 @@ var init_server = __esm({
5986
6229
  "record",
5987
6230
  "cookie-import",
5988
6231
  "doctor",
5989
- "upgrade"
6232
+ "upgrade",
6233
+ "profile"
5990
6234
  ]);
5991
6235
  RECORDING_SKIP = /* @__PURE__ */ new Set([
5992
6236
  "record",
@@ -6017,10 +6261,19 @@ var init_server = __esm({
6017
6261
  process.on("SIGTERM", shutdown);
6018
6262
  process.on("SIGINT", shutdown);
6019
6263
  flushInterval = setInterval(() => {
6020
- if (sessionManager) flushAllBuffers(sessionManager);
6264
+ if (sessionManager || profileSession) flushAllBuffers(sessionManager);
6021
6265
  }, DEFAULTS.BUFFER_FLUSH_INTERVAL_MS);
6022
6266
  sessionCleanupInterval = setInterval(async () => {
6023
- if (!sessionManager || isShuttingDown) return;
6267
+ if (isShuttingDown) return;
6268
+ if (profileSession) {
6269
+ const idleMs = Date.now() - profileSession.lastActivity;
6270
+ if (idleMs > IDLE_TIMEOUT_MS) {
6271
+ console.log(`[browse] Profile session idle for ${IDLE_TIMEOUT_MS / 1e3}s \u2014 shutting down`);
6272
+ shutdown();
6273
+ }
6274
+ return;
6275
+ }
6276
+ if (!sessionManager) return;
6024
6277
  const closed = await sessionManager.closeIdleSessions(IDLE_TIMEOUT_MS, (session) => flushSessionBuffers(session, true));
6025
6278
  for (const id of closed) {
6026
6279
  console.log(`[browse] Session "${id}" idle for ${IDLE_TIMEOUT_MS / 1e3}s \u2014 closed`);
@@ -6039,8 +6292,8 @@ var init_server = __esm({
6039
6292
 
6040
6293
  // src/cli.ts
6041
6294
  init_constants();
6042
- import * as fs14 from "fs";
6043
- import * as path11 from "path";
6295
+ import * as fs15 from "fs";
6296
+ import * as path12 from "path";
6044
6297
  import { spawn as spawn3 } from "child_process";
6045
6298
  import { fileURLToPath as fileURLToPath3 } from "url";
6046
6299
 
@@ -6083,7 +6336,8 @@ var cliFlags = {
6083
6336
  headed: false,
6084
6337
  stateFile: "",
6085
6338
  maxOutput: 0,
6086
- cdpUrl: ""
6339
+ cdpUrl: "",
6340
+ profile: ""
6087
6341
  };
6088
6342
  var stateFileApplied = false;
6089
6343
  var BROWSE_PORT2 = parseInt(process.env.BROWSE_PORT || "0", 10);
@@ -6092,50 +6346,50 @@ var INSTANCE_SUFFIX2 = BROWSE_PORT2 ? `-${BROWSE_PORT2}` : BROWSE_INSTANCE2 ? `-
6092
6346
  function resolveLocalDir() {
6093
6347
  if (process.env.BROWSE_LOCAL_DIR) {
6094
6348
  try {
6095
- fs14.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
6349
+ fs15.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
6096
6350
  } catch {
6097
6351
  }
6098
6352
  return process.env.BROWSE_LOCAL_DIR;
6099
6353
  }
6100
6354
  let dir = process.cwd();
6101
6355
  for (let i = 0; i < 20; i++) {
6102
- if (fs14.existsSync(path11.join(dir, ".git")) || fs14.existsSync(path11.join(dir, ".claude"))) {
6103
- const browseDir = path11.join(dir, ".browse");
6356
+ if (fs15.existsSync(path12.join(dir, ".git")) || fs15.existsSync(path12.join(dir, ".claude"))) {
6357
+ const browseDir = path12.join(dir, ".browse");
6104
6358
  try {
6105
- fs14.mkdirSync(browseDir, { recursive: true });
6106
- const gi = path11.join(browseDir, ".gitignore");
6107
- if (!fs14.existsSync(gi)) {
6108
- fs14.writeFileSync(gi, "*\n");
6359
+ fs15.mkdirSync(browseDir, { recursive: true });
6360
+ const gi = path12.join(browseDir, ".gitignore");
6361
+ if (!fs15.existsSync(gi)) {
6362
+ fs15.writeFileSync(gi, "*\n");
6109
6363
  }
6110
6364
  } catch {
6111
6365
  }
6112
6366
  return browseDir;
6113
6367
  }
6114
- const parent = path11.dirname(dir);
6368
+ const parent = path12.dirname(dir);
6115
6369
  if (parent === dir) break;
6116
6370
  dir = parent;
6117
6371
  }
6118
6372
  return "/tmp";
6119
6373
  }
6120
6374
  var LOCAL_DIR3 = resolveLocalDir();
6121
- var STATE_FILE2 = process.env.BROWSE_STATE_FILE || path11.join(LOCAL_DIR3, `browse-server${INSTANCE_SUFFIX2}.json`);
6375
+ var STATE_FILE2 = process.env.BROWSE_STATE_FILE || path12.join(LOCAL_DIR3, `browse-server${INSTANCE_SUFFIX2}.json`);
6122
6376
  var MAX_START_WAIT = 8e3;
6123
6377
  var LOCK_FILE = STATE_FILE2 + ".lock";
6124
6378
  var LOCK_STALE_MS = DEFAULTS.LOCK_STALE_THRESHOLD_MS;
6125
6379
  var __filename_cli = fileURLToPath3(import.meta.url);
6126
- var __dirname_cli = path11.dirname(__filename_cli);
6380
+ var __dirname_cli = path12.dirname(__filename_cli);
6127
6381
  function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
6128
6382
  if (env.BROWSE_SERVER_SCRIPT) {
6129
6383
  return env.BROWSE_SERVER_SCRIPT;
6130
6384
  }
6131
6385
  if (metaDir.startsWith("/")) {
6132
- const direct = path11.resolve(metaDir, "server.ts");
6133
- if (fs14.existsSync(direct)) {
6386
+ const direct = path12.resolve(metaDir, "server.ts");
6387
+ if (fs15.existsSync(direct)) {
6134
6388
  return direct;
6135
6389
  }
6136
6390
  }
6137
6391
  const selfPath = fileURLToPath3(import.meta.url);
6138
- if (fs14.existsSync(selfPath)) {
6392
+ if (fs15.existsSync(selfPath)) {
6139
6393
  return "__self__";
6140
6394
  }
6141
6395
  throw new Error(
@@ -6145,7 +6399,7 @@ function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
6145
6399
  var SERVER_SCRIPT = resolveServerScript();
6146
6400
  function readState() {
6147
6401
  try {
6148
- const data = fs14.readFileSync(STATE_FILE2, "utf-8");
6402
+ const data = fs15.readFileSync(STATE_FILE2, "utf-8");
6149
6403
  return JSON.parse(data);
6150
6404
  } catch {
6151
6405
  return null;
@@ -6161,7 +6415,7 @@ function isProcessAlive(pid) {
6161
6415
  }
6162
6416
  async function listInstances() {
6163
6417
  try {
6164
- const files = fs14.readdirSync(LOCAL_DIR3).filter(
6418
+ const files = fs15.readdirSync(LOCAL_DIR3).filter(
6165
6419
  (f) => f.startsWith("browse-server") && f.endsWith(".json") && !f.endsWith(".lock")
6166
6420
  );
6167
6421
  if (files.length === 0) {
@@ -6171,7 +6425,7 @@ async function listInstances() {
6171
6425
  let found = false;
6172
6426
  for (const file of files) {
6173
6427
  try {
6174
- const data = JSON.parse(fs14.readFileSync(path11.join(LOCAL_DIR3, file), "utf-8"));
6428
+ const data = JSON.parse(fs15.readFileSync(path12.join(LOCAL_DIR3, file), "utf-8"));
6175
6429
  if (!data.pid || !data.port) continue;
6176
6430
  const alive = isProcessAlive(data.pid);
6177
6431
  let status = "dead";
@@ -6194,7 +6448,7 @@ async function listInstances() {
6194
6448
  found = true;
6195
6449
  if (!alive) {
6196
6450
  try {
6197
- fs14.unlinkSync(path11.join(LOCAL_DIR3, file));
6451
+ fs15.unlinkSync(path12.join(LOCAL_DIR3, file));
6198
6452
  } catch {
6199
6453
  }
6200
6454
  }
@@ -6217,15 +6471,15 @@ function isBrowseProcess(pid) {
6217
6471
  }
6218
6472
  function acquireLock() {
6219
6473
  try {
6220
- fs14.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
6474
+ fs15.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
6221
6475
  return true;
6222
6476
  } catch (err) {
6223
6477
  if (err.code === "EEXIST") {
6224
6478
  try {
6225
- const stat = fs14.statSync(LOCK_FILE);
6479
+ const stat = fs15.statSync(LOCK_FILE);
6226
6480
  if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
6227
6481
  try {
6228
- fs14.unlinkSync(LOCK_FILE);
6482
+ fs15.unlinkSync(LOCK_FILE);
6229
6483
  } catch {
6230
6484
  }
6231
6485
  return acquireLock();
@@ -6239,7 +6493,7 @@ function acquireLock() {
6239
6493
  }
6240
6494
  function releaseLock() {
6241
6495
  try {
6242
- fs14.unlinkSync(LOCK_FILE);
6496
+ fs15.unlinkSync(LOCK_FILE);
6243
6497
  } catch {
6244
6498
  }
6245
6499
  }
@@ -6256,7 +6510,7 @@ async function startServer() {
6256
6510
  }
6257
6511
  await sleep(100);
6258
6512
  }
6259
- if (!fs14.existsSync(LOCK_FILE) || fs14.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
6513
+ if (!fs15.existsSync(LOCK_FILE) || fs15.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
6260
6514
  const state = readState();
6261
6515
  if (state && isProcessAlive(state.pid)) return state;
6262
6516
  throw new Error("Server failed to start (another process is starting it)");
@@ -6266,13 +6520,13 @@ async function startServer() {
6266
6520
  try {
6267
6521
  const oldState = readState();
6268
6522
  if (oldState && !isProcessAlive(oldState.pid)) {
6269
- fs14.unlinkSync(STATE_FILE2);
6523
+ fs15.unlinkSync(STATE_FILE2);
6270
6524
  }
6271
6525
  } catch {
6272
6526
  }
6273
6527
  const selfPath = fileURLToPath3(import.meta.url);
6274
6528
  const spawnCmd = SERVER_SCRIPT === "__self__" ? [process.execPath, selfPath] : [process.execPath, "--import", "tsx", SERVER_SCRIPT];
6275
- const spawnEnv = { ...process.env, __BROWSE_SERVER_MODE: "1", BROWSE_LOCAL_DIR: LOCAL_DIR3, BROWSE_INSTANCE: BROWSE_INSTANCE2, ...cliFlags.headed ? { BROWSE_HEADED: "1" } : {}, ...cliFlags.cdpUrl ? { BROWSE_CDP_URL: cliFlags.cdpUrl } : {} };
6529
+ const spawnEnv = { ...process.env, __BROWSE_SERVER_MODE: "1", BROWSE_LOCAL_DIR: LOCAL_DIR3, BROWSE_INSTANCE: BROWSE_INSTANCE2, ...cliFlags.headed ? { BROWSE_HEADED: "1" } : {}, ...cliFlags.cdpUrl ? { BROWSE_CDP_URL: cliFlags.cdpUrl } : {}, ...cliFlags.profile ? { BROWSE_PROFILE: cliFlags.profile } : {} };
6276
6530
  const proc = spawn3(spawnCmd[0], spawnCmd.slice(1), {
6277
6531
  stdio: ["ignore", "ignore", "pipe"],
6278
6532
  env: spawnEnv,
@@ -6339,7 +6593,7 @@ async function ensureServer() {
6339
6593
  }
6340
6594
  if (state) {
6341
6595
  try {
6342
- fs14.unlinkSync(STATE_FILE2);
6596
+ fs15.unlinkSync(STATE_FILE2);
6343
6597
  } catch {
6344
6598
  }
6345
6599
  }
@@ -6349,21 +6603,21 @@ async function ensureServer() {
6349
6603
  }
6350
6604
  function cleanOrphanedServers() {
6351
6605
  try {
6352
- const files = fs14.readdirSync(LOCAL_DIR3);
6606
+ const files = fs15.readdirSync(LOCAL_DIR3);
6353
6607
  for (const file of files) {
6354
6608
  if (!file.startsWith("browse-server") || !file.endsWith(".json") || file.endsWith(".lock")) continue;
6355
- const filePath = path11.join(LOCAL_DIR3, file);
6609
+ const filePath = path12.join(LOCAL_DIR3, file);
6356
6610
  if (filePath === STATE_FILE2) continue;
6357
6611
  try {
6358
- const data = JSON.parse(fs14.readFileSync(filePath, "utf-8"));
6612
+ const data = JSON.parse(fs15.readFileSync(filePath, "utf-8"));
6359
6613
  if (!data.pid) {
6360
- fs14.unlinkSync(filePath);
6614
+ fs15.unlinkSync(filePath);
6361
6615
  continue;
6362
6616
  }
6363
6617
  const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
6364
6618
  if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
6365
6619
  if (!isProcessAlive(data.pid)) {
6366
- fs14.unlinkSync(filePath);
6620
+ fs15.unlinkSync(filePath);
6367
6621
  continue;
6368
6622
  }
6369
6623
  if (isBrowseProcess(data.pid)) {
@@ -6374,7 +6628,7 @@ function cleanOrphanedServers() {
6374
6628
  }
6375
6629
  } catch {
6376
6630
  try {
6377
- fs14.unlinkSync(filePath);
6631
+ fs15.unlinkSync(filePath);
6378
6632
  } catch {
6379
6633
  }
6380
6634
  }
@@ -6475,7 +6729,7 @@ async function sendCommand(state, command, args, retries = 0, sessionId) {
6475
6729
  await sleep(300);
6476
6730
  }
6477
6731
  try {
6478
- fs14.unlinkSync(STATE_FILE2);
6732
+ fs15.unlinkSync(STATE_FILE2);
6479
6733
  } catch {
6480
6734
  }
6481
6735
  if (command === "restart") {
@@ -6523,7 +6777,7 @@ async function main() {
6523
6777
  function findCommandIndex(a) {
6524
6778
  for (let i = 0; i < a.length; i++) {
6525
6779
  if (!a[i].startsWith("-")) return i;
6526
- if (a[i] === "--session" || a[i] === "--allowed-domains" || a[i] === "--cdp" || a[i] === "--state") i++;
6780
+ if (a[i] === "--session" || a[i] === "--allowed-domains" || a[i] === "--cdp" || a[i] === "--state" || a[i] === "--profile") i++;
6527
6781
  }
6528
6782
  return a.length;
6529
6783
  }
@@ -6538,6 +6792,21 @@ async function main() {
6538
6792
  args.splice(sessionIdx, 2);
6539
6793
  }
6540
6794
  sessionId = sessionId || process.env.BROWSE_SESSION || config.session || void 0;
6795
+ let profileName;
6796
+ const profileIdx = args.indexOf("--profile");
6797
+ if (profileIdx !== -1 && profileIdx < findCommandIndex(args)) {
6798
+ profileName = args[profileIdx + 1];
6799
+ if (!profileName || profileName.startsWith("-")) {
6800
+ console.error("Usage: browse --profile <name> <command> [args...]");
6801
+ process.exit(1);
6802
+ }
6803
+ args.splice(profileIdx, 2);
6804
+ }
6805
+ profileName = profileName || process.env.BROWSE_PROFILE || void 0;
6806
+ if (sessionId && profileName) {
6807
+ console.error("Cannot use --profile and --session together. Profiles use their own Chromium; sessions share one.");
6808
+ process.exit(1);
6809
+ }
6541
6810
  let jsonMode = false;
6542
6811
  const jsonIdx = args.indexOf("--json");
6543
6812
  if (jsonIdx !== -1 && jsonIdx < findCommandIndex(args)) {
@@ -6636,6 +6905,7 @@ async function main() {
6636
6905
  cliFlags.stateFile = stateFile;
6637
6906
  cliFlags.maxOutput = maxOutput;
6638
6907
  cliFlags.cdpUrl = cdpUrl;
6908
+ cliFlags.profile = profileName || "";
6639
6909
  if (args[0] === "version" || args[0] === "--version" || args[0] === "-V") {
6640
6910
  const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
6641
6911
  console.log(pkg.version);
@@ -6704,6 +6974,7 @@ Setup: install-skill [path]
6704
6974
 
6705
6975
  Options:
6706
6976
  --session <id> Named session (isolates tabs, refs, cookies)
6977
+ --profile <name> Persistent browser profile (own Chromium, full state persistence)
6707
6978
  --json Wrap output as {success, data, command}
6708
6979
  --content-boundaries Wrap page content in nonce-delimited markers
6709
6980
  --allowed-domains <d,d> Block navigation/resources outside allowlist
@@ -6743,7 +7014,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
6743
7014
  }
6744
7015
  if (process.env.__BROWSE_SERVER_MODE === "1") {
6745
7016
  Promise.resolve().then(() => init_server());
6746
- } else if (process.argv[1] && fs14.realpathSync(process.argv[1]) === fs14.realpathSync(__filename_cli)) {
7017
+ } else if (process.argv[1] && fs15.realpathSync(process.argv[1]) === fs15.realpathSync(__filename_cli)) {
6747
7018
  main().catch((err) => {
6748
7019
  console.error(`[browse] ${err.message}`);
6749
7020
  process.exit(1);