browser-cookie-bridge 1.4.2 → 1.4.3

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/README.md CHANGED
@@ -12,6 +12,7 @@
12
12
  <img src="https://img.shields.io/badge/macOS-13.5%2B-111111?logo=apple&logoColor=white" alt="macOS 13.5+" />
13
13
  <img src="https://img.shields.io/badge/Swift-6-8E2735?logo=swift&logoColor=white" alt="Swift 6" />
14
14
  <img src="https://img.shields.io/badge/Node.js-24%2B-3C873A?logo=nodedotjs&logoColor=white" alt="Node.js 24+" />
15
+ <img src="https://img.shields.io/badge/Homebrew-Cask-FBB040?logo=homebrew&logoColor=black" alt="Homebrew Cask" />
15
16
  <a href="https://www.npmjs.com/package/browser-cookie-bridge"><img src="https://img.shields.io/npm/v/browser-cookie-bridge?logo=npm&color=CB3837" alt="browser-cookie-bridge on npm" /></a>
16
17
  <img src="https://img.shields.io/badge/local--first-cloud%20optional-C68B3C" alt="Local-first, cloud optional" />
17
18
  <img src="https://img.shields.io/github/stars/apoorvdarshan/browser-cookie-bridge?logo=github&color=C68B3C" alt="GitHub stars" />
@@ -22,6 +23,7 @@
22
23
 
23
24
  <p>
24
25
  <a href="#installation"><b>Install</b></a> ·
26
+ <a href="https://github.com/apoorvdarshan/homebrew-tap">Homebrew</a> ·
25
27
  <a href="https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest">DMG</a> ·
26
28
  <a href="https://www.npmjs.com/package/browser-cookie-bridge">npm</a> ·
27
29
  <a href="#how-it-works">How it works</a> ·
@@ -61,9 +63,11 @@ Browser Cookie Bridge currently ships for macOS. A separate Windows app is being
61
63
  ## Features
62
64
 
63
65
  - 🍪 **Cookie and session transfer** — cookies are enabled by default, including supported domain, path, expiry, security, `SameSite`, and partition attributes.
66
+ - 🗃️ **Optional full site data for Codex** — replace Codex's compatible Local Storage, IndexedDB, Session Storage, service-worker, and related origin stores from a closed source profile, with backup and rollback. Off by default.
64
67
  - 🌐 **Seven Chromium browsers** — Brave, Chrome, Edge, Arc, Vivaldi, Opera, and Perplexity Comet can be sources or destinations.
65
68
  - ✨ **ChatGPT Codex import** — merge selected local browser data into Codex's built-in browser; Codex is destination-only.
66
69
  - 🔁 **Optional Codex restart** — manual Sync can force quit a running Codex instance and reopen it only after a successful local transfer; off by default.
70
+ - 🔄 **Optional two-app restart** — Full site data can force quit the selected source browser and Codex, then reopen only the apps that were running after a successful transfer; off by default.
67
71
  - ☁️ **Optional Browserless upload** — create or refresh a Browserless authenticated profile with cookies, local storage, and IndexedDB; see a local size preflight, live progress, cancellation, and post-upload verification.
68
72
  - 🕘 **Background automation** — sync when you sign in, at a fixed daily time, or whenever you choose.
69
73
  - ◉ **Native menu-bar app** — closing the window removes the Dock icon while the helper continues running.
@@ -76,8 +80,9 @@ Browser Cookie Bridge currently ships for macOS. A separate Windows app is being
76
80
  | Data | Support | Notes |
77
81
  |---|---:|---|
78
82
  | **Cookies and sessions** | ✅ Default | Transfers supported cookie values and attributes |
83
+ | **Full site data → Codex** | Optional | Replaces compatible origin storage from a closed Chromium profile; backs up Codex first |
79
84
  | **History URLs** | ◐ Optional | Original visit times and page titles cannot be preserved |
80
- | **Local storage and IndexedDB** | ◐ Browserless only | Included only in an explicit Browserless authenticated-profile upload |
85
+ | **Local storage and IndexedDB** | ◐ Optional | Full site data into Codex, or an explicit Browserless authenticated-profile upload |
81
86
  | **Passwords** | — Never | Chromium extensions cannot read the browser password store |
82
87
  | **Bookmarks, autofill, payments** | — Never | Not requested or accessed |
83
88
  | **iCloud Keychain** | — Never | Remains completely separate |
@@ -93,6 +98,15 @@ The DMG is self-contained. Node.js 24+ and Xcode Command Line Tools are required
93
98
 
94
99
  ## Installation
95
100
 
101
+ ### Via Homebrew — recommended
102
+
103
+ ```bash
104
+ brew install --cask apoorvdarshan/tap/browser-cookie-bridge
105
+ ```
106
+
107
+ Homebrew selects the correct Apple-silicon or Intel DMG, verifies its SHA-256
108
+ checksum, and installs the signed and notarized app into `/Applications`.
109
+
96
110
  ### Download a DMG — recommended
97
111
 
98
112
  - **[Apple silicon DMG](https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest/download/Browser-Cookie-Bridge-arm64.dmg)** — M1, M2, M3, M4, and newer
@@ -110,7 +124,11 @@ npx browser-cookie-bridge install-app
110
124
 
111
125
  No administrator password is needed. The app is built from source on your Mac and installed in your user Applications folder. If a signed `/Applications/Browser Cookie Bridge.app` already exists, the CLI preserves it instead of replacing its Developer ID signature with an ad-hoc local build; use the app's built-in updater for signed releases.
112
126
 
113
- Both installation methods use the same bundle identifier and the same settings under `~/Library/Application Support/BraveCodexCookieSync`. Installing the DMG after npm does not create a separate product identity: the system Applications copy becomes canonical, and a stale matching user Applications copy is moved to the Trash on launch. Future updates replace that same app in place.
127
+ All installation methods use the same bundle identifier and settings under
128
+ `~/Library/Application Support/BraveCodexCookieSync`. Homebrew and the DMG
129
+ install the canonical system Applications copy. Installing either after npm
130
+ does not create a separate product identity: a stale matching user Applications
131
+ copy is moved to Trash on launch, and future updates replace the same app.
114
132
 
115
133
  **[View `browser-cookie-bridge` on npm →](https://www.npmjs.com/package/browser-cookie-bridge)**
116
134
 
@@ -188,7 +206,7 @@ Automation uses the saved source, destination, and data choices for local transf
188
206
  ```bash
189
207
  browser-cookie-bridge install-app [--no-open] [--replace-system-from-source]
190
208
  browser-cookie-bridge setup [--hour 9] [--minute 0] [--no-schedule]
191
- browser-cookie-bridge preferences --source brave --target codex --cookies on --history off --auto-restart-codex off
209
+ browser-cookie-bridge preferences --source brave --target codex --cookies on --history off --site-storage off --auto-restart-codex off --auto-restart-both off
192
210
  browser-cookie-bridge sync [--timeout 300] [--allow-cloud-upload]
193
211
  browser-cookie-bridge browserless-preflight
194
212
  browser-cookie-bridge doctor
@@ -216,6 +234,7 @@ The broker validates a random token and extension origin, limits payload size, a
216
234
  - Cookie values and history URLs are never logged.
217
235
  - Browser-to-browser data is held only in broker memory.
218
236
  - Codex backups are stored with user-only permissions under `~/Library/Application Support/BraveCodexCookieSync/backups/codex`; the newest 14 are retained.
237
+ - Full site-data import is opt-in and requires both Codex and the source browser to be closed. It replaces compatible Codex origin-storage directories rather than attempting an unsafe LevelDB merge; the previous directories are included in the same backup.
219
238
  - The app refuses unknown Codex database schemas instead of guessing.
220
239
  - Imported Codex rows use an empty `encrypted_value` because OpenAI's Safe Storage key is protected by a private macOS Keychain access group. Those imported rows can therefore remain readable to software running as your macOS user until the website refreshes them.
221
240
  - Anyone who can use your logged-in macOS account or modify a generated extension may be able to access transferred browser sessions.
@@ -247,6 +266,7 @@ If Browser Cookie Bridge is useful to you:
247
266
 
248
267
  - ⭐ **Star** the repository
249
268
  - ⬇️ **[Download the latest DMG](https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest)**
269
+ - 🍺 **[Install from Homebrew](https://github.com/apoorvdarshan/homebrew-tap)**
250
270
  - 📦 **[Install from npm](https://www.npmjs.com/package/browser-cookie-bridge)**
251
271
  - 🐛 **[Report a bug](https://github.com/apoorvdarshan/browser-cookie-bridge/issues/new?template=bug_report.yml)** — never include cookie values or tokens
252
272
  - ☕ **[Support on Ko-fi](https://ko-fi.com/apoorvdarshan)**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Cookie Bridge",
4
- "version": "1.4.2",
4
+ "version": "1.4.3",
5
5
  "description": "Transfers selected browser data locally between supported browsers and into ChatGPT Codex.",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5o6lDkXzU/23Jm2QRk4mfuxNnr3VlEIjRHcZr93Gdo7PI7asHJQhigaMGT6UOz0lkaTn1F+TGIPlsJo9GNBSsxbCfxWoBSXcFQFrPolY5LEfKaMtZQQ6eb4UAnp+bbUwb1den4On0piWU47Yzl6i0Jcdg4EG5H1kCSF/6nJsbKgF7OTaR/3drULu40yRvflzSXRhly3UDO7c5mgHJsaMwVV+VILsxbcPOoRINjuo3zDC737HFs67dpf38xF3tjSVL0HURAWKKobMvfABiDfVEm5O0Lvf+57d1Ib12QLT2qtVXNDy3iYwX+w2S0wH2vIMsfvoFPlRdmeqft70kZOqXwIDAQAB",
7
7
  "permissions": ["alarms", "cookies", "history"],
@@ -10,8 +10,8 @@
10
10
  <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
11
11
  <key>CFBundleName</key><string>Browser Cookie Bridge</string>
12
12
  <key>CFBundlePackageType</key><string>APPL</string>
13
- <key>CFBundleShortVersionString</key><string>1.4.2</string>
14
- <key>CFBundleVersion</key><string>30</string>
13
+ <key>CFBundleShortVersionString</key><string>1.4.3</string>
14
+ <key>CFBundleVersion</key><string>31</string>
15
15
  <key>LSMinimumSystemVersion</key><string>13.5</string>
16
16
  <key>NSHighResolutionCapable</key><true/>
17
17
  </dict>
@@ -569,6 +569,18 @@ struct PreferencesPanel: View {
569
569
  Toggle("", isOn: Binding(get: { model.cookiesEnabled }, set: { model.setCookiesEnabled($0) }))
570
570
  .labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
571
571
  }
572
+ if model.selectedTargetID == "codex" {
573
+ RowDivider()
574
+ PreferenceRow(
575
+ icon: "externaldrive.badge.plus",
576
+ color: Theme.accent,
577
+ title: "Full site data",
578
+ detail: "Local Storage, IndexedDB, sessions, and service workers · both apps must be closed"
579
+ ) {
580
+ Toggle("", isOn: Binding(get: { model.siteStorageEnabled }, set: { model.setSiteStorageEnabled($0) }))
581
+ .labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking || model.isSyncing)
582
+ }
583
+ }
572
584
  RowDivider()
573
585
  PreferenceRow(icon: "clock.arrow.circlepath", color: Theme.accent, title: "History URLs", detail: "Original visit times are not preserved") {
574
586
  Toggle("", isOn: Binding(get: { model.historyEnabled }, set: { model.setHistoryEnabled($0) }))
@@ -580,14 +592,26 @@ struct PreferencesPanel: View {
580
592
  }
581
593
  if model.selectedTargetID == "codex" {
582
594
  RowDivider()
583
- PreferenceRow(
584
- icon: "arrow.clockwise.circle",
585
- color: Theme.accent,
586
- title: "Restart Codex automatically",
587
- detail: "Force quit before manual sync; reopen only after success"
588
- ) {
589
- Toggle("", isOn: Binding(get: { model.autoRestartCodex }, set: { model.setAutoRestartCodex($0) }))
590
- .labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking || model.isSyncing)
595
+ if model.siteStorageEnabled {
596
+ PreferenceRow(
597
+ icon: "arrow.triangle.2.circlepath",
598
+ color: Theme.accent,
599
+ title: "Restart both automatically",
600
+ detail: "Manual sync force quits both; reopens running apps only after success"
601
+ ) {
602
+ Toggle("", isOn: Binding(get: { model.autoRestartBoth }, set: { model.setAutoRestartBoth($0) }))
603
+ .labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking || model.isSyncing)
604
+ }
605
+ } else {
606
+ PreferenceRow(
607
+ icon: "arrow.clockwise.circle",
608
+ color: Theme.accent,
609
+ title: "Restart Codex automatically",
610
+ detail: "Force quit before manual sync; reopen only after success"
611
+ ) {
612
+ Toggle("", isOn: Binding(get: { model.autoRestartCodex }, set: { model.setAutoRestartCodex($0) }))
613
+ .labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking || model.isSyncing)
614
+ }
591
615
  }
592
616
  }
593
617
  }
@@ -78,6 +78,7 @@ final class SyncModel: ObservableObject {
78
78
  @Published var menuBarEnabled = true
79
79
  @Published var autoCheckUpdates = true
80
80
  @Published var autoRestartCodex = false
81
+ @Published var autoRestartBoth = false
81
82
  @Published var isCheckingForUpdates = false
82
83
  @Published var isInstallingUpdate = false
83
84
  @Published var availableUpdateVersion: String?
@@ -85,6 +86,7 @@ final class SyncModel: ObservableObject {
85
86
  @Published var extensionsReady = false
86
87
  @Published var cookiesEnabled = true
87
88
  @Published var historyEnabled = false
89
+ @Published var siteStorageEnabled = false
88
90
  @Published var selectedSourceID = "brave"
89
91
  @Published var selectedTargetID = "codex"
90
92
  @Published var codexRunning = false
@@ -129,11 +131,16 @@ final class SyncModel: ObservableObject {
129
131
  var targetName: String {
130
132
  isBrowserlessTarget ? "Browserless Cloud" : selectedTargetBrowser?.name ?? "ChatGPT Codex"
131
133
  }
132
- var codexBlocked: Bool { selectedTargetID == "codex" && codexRunning && !autoRestartCodex }
134
+ var codexBlocked: Bool {
135
+ selectedTargetID == "codex" && codexRunning && !autoRestartCodex && !(siteStorageEnabled && autoRestartBoth)
136
+ }
137
+ var sourceSiteDataBlocked: Bool {
138
+ selectedTargetID == "codex" && siteStorageEnabled && sourceBrowserRunning && !autoRestartBoth
139
+ }
133
140
  var browserlessBlocked: Bool {
134
141
  isBrowserlessTarget && (!browserlessConfigured || sourceBrowserRunning || selectedSourceID == "comet")
135
142
  }
136
- var syncBlocked: Bool { codexBlocked || browserlessBlocked }
143
+ var syncBlocked: Bool { codexBlocked || sourceSiteDataBlocked || browserlessBlocked }
137
144
  var formattedUploadElapsed: String {
138
145
  let minutes = uploadElapsedSeconds / 60
139
146
  let seconds = uploadElapsedSeconds % 60
@@ -260,9 +267,11 @@ final class SyncModel: ObservableObject {
260
267
  if selectedTargetID == selectedSourceID { selectedTargetID = "codex" }
261
268
  cookiesEnabled = config.imports?.cookies ?? true
262
269
  historyEnabled = config.imports?.history ?? false
270
+ siteStorageEnabled = config.imports?.siteStorage ?? false
263
271
  menuBarEnabled = config.ui?.menuBar ?? true
264
272
  autoCheckUpdates = config.ui?.autoCheckUpdates ?? true
265
273
  autoRestartCodex = config.ui?.autoRestartCodex ?? false
274
+ autoRestartBoth = config.ui?.autoRestartBoth ?? false
266
275
  browserlessProfileName = config.browserless?.profileName ?? "browser-cookie-bridge"
267
276
  browserlessRegion = config.browserless?.region ?? "sfo"
268
277
  browserlessOnlyDomains = (config.browserless?.onlyDomains ?? []).joined(separator: ", ")
@@ -309,6 +318,11 @@ final class SyncModel: ObservableObject {
309
318
  persistPreferences(successMessage: enabled ? "History URL import enabled" : "History import disabled")
310
319
  }
311
320
 
321
+ func setSiteStorageEnabled(_ enabled: Bool) {
322
+ siteStorageEnabled = enabled
323
+ persistPreferences(successMessage: enabled ? "Full site-data import enabled" : "Full site-data import disabled")
324
+ }
325
+
312
326
  func saveBrowserlessSettings(token: String, profileName: String, region: String, onlyDomains: String) {
313
327
  let cleanedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
314
328
  let cleanedName = profileName.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -354,6 +368,11 @@ final class SyncModel: ObservableObject {
354
368
  persistPreferences(successMessage: enabled ? "Automatic Codex restart enabled" : "Automatic Codex restart disabled")
355
369
  }
356
370
 
371
+ func setAutoRestartBoth(_ enabled: Bool) {
372
+ autoRestartBoth = enabled
373
+ persistPreferences(successMessage: enabled ? "Automatic source and Codex restart enabled" : "Automatic source and Codex restart disabled")
374
+ }
375
+
357
376
  func checkForUpdates(showAlert: Bool = false) {
358
377
  guard !isCheckingForUpdates, !isInstallingUpdate else { return }
359
378
  guard let url = URL(string: "https://api.github.com/repos/apoorvdarshan/browser-cookie-bridge/releases/latest") else { return }
@@ -456,6 +475,10 @@ final class SyncModel: ObservableObject {
456
475
  }
457
476
  return
458
477
  }
478
+ if selectedTargetID == "codex" && siteStorageEnabled && autoRestartBoth && (sourceBrowserRunning || codexRunning) {
479
+ forceQuitBothThenSync(showMenuBarAlert: showMenuBarAlert)
480
+ return
481
+ }
459
482
  if selectedTargetID == "codex" && codexRunning && autoRestartCodex {
460
483
  forceQuitCodexThenSync(showMenuBarAlert: showMenuBarAlert)
461
484
  return
@@ -463,6 +486,75 @@ final class SyncModel: ObservableObject {
463
486
  startSync(showMenuBarAlert: showMenuBarAlert, reopenCodexOnSuccess: false)
464
487
  }
465
488
 
489
+ private func forceQuitBothThenSync(showMenuBarAlert: Bool) {
490
+ let sourceApplications = NSWorkspace.shared.runningApplications.filter {
491
+ $0.bundleIdentifier == selectedBrowser.bundleIdentifier
492
+ }
493
+ let codexApplications = NSWorkspace.shared.runningApplications.filter {
494
+ $0.bundleIdentifier == "com.openai.codex"
495
+ }
496
+ let reopenSource = !sourceApplications.isEmpty
497
+ let reopenCodex = !codexApplications.isEmpty
498
+ let applications = sourceApplications + codexApplications
499
+
500
+ isSyncing = true
501
+ uploadCanceling = false
502
+ state = .syncing
503
+ primaryStatus = "Closing both apps for full sync"
504
+ secondaryStatus = "Force quitting \(selectedBrowser.name) and ChatGPT Codex, then waiting for their browser storage to close…"
505
+ guard applications.allSatisfy({ $0.forceTerminate() }) else {
506
+ finishCodexPreparationFailure(
507
+ message: "macOS could not force quit both apps. Close \(selectedBrowser.name) and ChatGPT Codex manually, then try again.",
508
+ showMenuBarAlert: showMenuBarAlert
509
+ )
510
+ return
511
+ }
512
+ waitForBothToQuit(
513
+ attemptsRemaining: 50,
514
+ reopenSource: reopenSource,
515
+ reopenCodex: reopenCodex,
516
+ showMenuBarAlert: showMenuBarAlert
517
+ )
518
+ }
519
+
520
+ private func waitForBothToQuit(
521
+ attemptsRemaining: Int,
522
+ reopenSource: Bool,
523
+ reopenCodex: Bool,
524
+ showMenuBarAlert: Bool
525
+ ) {
526
+ let runningBundleIDs = Set(NSWorkspace.shared.runningApplications.compactMap(\.bundleIdentifier))
527
+ let stillRunning = runningBundleIDs.contains(selectedBrowser.bundleIdentifier) || runningBundleIDs.contains("com.openai.codex")
528
+ if !stillRunning {
529
+ sourceBrowserRunning = false
530
+ codexRunning = false
531
+ secondaryStatus = "Both apps are closed — waiting briefly for browser storage to be released…"
532
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
533
+ self?.startSync(
534
+ showMenuBarAlert: showMenuBarAlert,
535
+ reopenCodexOnSuccess: reopenCodex,
536
+ reopenSourceOnSuccess: reopenSource
537
+ )
538
+ }
539
+ return
540
+ }
541
+ guard attemptsRemaining > 0 else {
542
+ finishCodexPreparationFailure(
543
+ message: "The apps did not close within 10 seconds. Close both manually, then try again.",
544
+ showMenuBarAlert: showMenuBarAlert
545
+ )
546
+ return
547
+ }
548
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
549
+ self?.waitForBothToQuit(
550
+ attemptsRemaining: attemptsRemaining - 1,
551
+ reopenSource: reopenSource,
552
+ reopenCodex: reopenCodex,
553
+ showMenuBarAlert: showMenuBarAlert
554
+ )
555
+ }
556
+ }
557
+
466
558
  private func forceQuitCodexThenSync(showMenuBarAlert: Bool) {
467
559
  isSyncing = true
468
560
  uploadCanceling = false
@@ -517,7 +609,7 @@ final class SyncModel: ObservableObject {
517
609
  }
518
610
  }
519
611
 
520
- private func startSync(showMenuBarAlert: Bool, reopenCodexOnSuccess: Bool) {
612
+ private func startSync(showMenuBarAlert: Bool, reopenCodexOnSuccess: Bool, reopenSourceOnSuccess: Bool = false) {
521
613
  isSyncing = true
522
614
  uploadCanceling = false
523
615
  state = .syncing
@@ -576,10 +668,12 @@ final class SyncModel: ObservableObject {
576
668
  : "Keep both browsers open and check the extensions")
577
669
  }
578
670
  self.updateEndpointRunningStatus()
579
- if success && reopenCodexOnSuccess {
580
- self.reopenCodexAfterSuccessfulSync(
671
+ if success && (reopenCodexOnSuccess || reopenSourceOnSuccess) {
672
+ self.reopenApplicationsAfterSuccessfulSync(
581
673
  partial: partial,
582
674
  syncSummary: self.secondaryStatus,
675
+ reopenCodex: reopenCodexOnSuccess,
676
+ reopenSource: reopenSourceOnSuccess,
583
677
  showMenuBarAlert: showMenuBarAlert
584
678
  )
585
679
  } else if showMenuBarAlert {
@@ -592,15 +686,44 @@ final class SyncModel: ObservableObject {
592
686
  }
593
687
  }
594
688
 
595
- private func reopenCodexAfterSuccessfulSync(partial: Bool, syncSummary: String, showMenuBarAlert: Bool) {
689
+ private func reopenApplicationsAfterSuccessfulSync(
690
+ partial: Bool,
691
+ syncSummary: String,
692
+ reopenCodex: Bool,
693
+ reopenSource: Bool,
694
+ showMenuBarAlert: Bool
695
+ ) {
596
696
  let transferResult = syncSummary.replacingOccurrences(
597
697
  of: "Reopen Codex to use the updated sessions. ",
598
698
  with: ""
599
699
  )
700
+ var restartMessages: [String] = []
701
+ var sourceRestartFailed = false
702
+ if reopenSource {
703
+ if let sourceURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: selectedBrowser.bundleIdentifier),
704
+ NSWorkspace.shared.open(sourceURL) {
705
+ sourceBrowserRunning = true
706
+ restartMessages.append("\(selectedBrowser.name) reopened successfully.")
707
+ } else {
708
+ sourceRestartFailed = true
709
+ restartMessages.append("\(selectedBrowser.name) could not be reopened; open it manually.")
710
+ }
711
+ }
712
+ let completedRestartMessages = restartMessages
713
+ let didSourceRestartFail = sourceRestartFailed
714
+ guard reopenCodex else {
715
+ state = partial || didSourceRestartFail ? .warning : .success
716
+ primaryStatus = didSourceRestartFail ? "Sync complete, but the source did not reopen" : (partial ? "Codex sync completed with warnings" : "Full site-data sync complete")
717
+ secondaryStatus = ([transferResult] + completedRestartMessages).joined(separator: "\n\n")
718
+ if showMenuBarAlert {
719
+ postNativeAlert(title: primaryStatus, message: secondaryStatus, kind: state == .success ? .information : .warning)
720
+ }
721
+ return
722
+ }
600
723
  guard let codexURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.openai.codex") else {
601
724
  state = .warning
602
725
  primaryStatus = "Codex sessions updated, but Codex was not reopened"
603
- secondaryStatus = "\(transferResult)\n\nCodex could not be found. Open it manually to use the updated sessions."
726
+ secondaryStatus = ([transferResult] + completedRestartMessages + ["Codex could not be found. Open it manually to use the updated sessions."]).joined(separator: "\n\n")
604
727
  if showMenuBarAlert {
605
728
  postNativeAlert(title: primaryStatus, message: secondaryStatus, kind: .warning)
606
729
  }
@@ -612,12 +735,12 @@ final class SyncModel: ObservableObject {
612
735
  if let error {
613
736
  self.state = .warning
614
737
  self.primaryStatus = "Codex sessions updated, but Codex was not reopened"
615
- self.secondaryStatus = "\(transferResult)\n\nCodex could not be reopened: \(error.localizedDescription)"
738
+ self.secondaryStatus = ([transferResult] + completedRestartMessages + ["Codex could not be reopened: \(error.localizedDescription)"]).joined(separator: "\n\n")
616
739
  } else {
617
740
  self.codexRunning = true
618
- self.state = partial ? .warning : .success
619
- self.primaryStatus = partial ? "Codex sync completed with warnings" : "Codex sessions updated"
620
- self.secondaryStatus = "\(transferResult)\n\nChatGPT Codex reopened successfully."
741
+ self.state = partial || didSourceRestartFail ? .warning : .success
742
+ self.primaryStatus = didSourceRestartFail ? "Sync complete, but the source did not reopen" : (partial ? "Codex sync completed with warnings" : "Codex sessions updated")
743
+ self.secondaryStatus = ([transferResult] + completedRestartMessages + ["ChatGPT Codex reopened successfully."]).joined(separator: "\n\n")
621
744
  }
622
745
  if showMenuBarAlert {
623
746
  self.postNativeAlert(
@@ -738,9 +861,11 @@ final class SyncModel: ObservableObject {
738
861
  "--target", selectedTargetID,
739
862
  "--cookies", cookiesEnabled ? "on" : "off",
740
863
  "--history", historyEnabled ? "on" : "off",
864
+ "--site-storage", siteStorageEnabled ? "on" : "off",
741
865
  "--menu-bar", menuBarEnabled ? "on" : "off",
742
866
  "--auto-check-updates", autoCheckUpdates ? "on" : "off",
743
867
  "--auto-restart-codex", autoRestartCodex ? "on" : "off",
868
+ "--auto-restart-both", autoRestartBoth ? "on" : "off",
744
869
  "--browserless-profile", browserlessProfileName,
745
870
  "--browserless-region", browserlessRegion,
746
871
  "--browserless-domains", browserlessOnlyDomains,
@@ -941,7 +1066,15 @@ final class SyncModel: ObservableObject {
941
1066
  $0.bundleIdentifier == selectedBrowser.bundleIdentifier
942
1067
  }
943
1068
  guard !isSyncing else { return }
944
- if selectedTargetID == "codex" && codexRunning && autoRestartCodex {
1069
+ if selectedTargetID == "codex" && siteStorageEnabled && autoRestartBoth && (sourceBrowserRunning || codexRunning) {
1070
+ state = .ready
1071
+ primaryStatus = "Ready to sync and restart both apps"
1072
+ secondaryStatus = "Manual sync will force quit \(selectedBrowser.name) and Codex, then reopen only the apps that were running"
1073
+ } else if sourceSiteDataBlocked {
1074
+ state = .warning
1075
+ primaryStatus = "Quit \(selectedBrowser.name) before syncing"
1076
+ secondaryStatus = "Full site data uses live LevelDB files. Close the source browser completely so they can be copied safely"
1077
+ } else if selectedTargetID == "codex" && codexRunning && autoRestartCodex {
945
1078
  if state == .ready || primaryStatus == "Quit Codex before syncing" {
946
1079
  state = .ready
947
1080
  primaryStatus = "Ready to sync and restart Codex"
@@ -1062,6 +1195,7 @@ private struct AppConfig: Decodable {
1062
1195
  struct Imports: Decodable {
1063
1196
  let cookies: Bool
1064
1197
  let history: Bool
1198
+ let siteStorage: Bool?
1065
1199
  }
1066
1200
 
1067
1201
  struct UISettings: Decodable {
@@ -1069,6 +1203,7 @@ private struct AppConfig: Decodable {
1069
1203
  let openAtLogin: Bool?
1070
1204
  let autoCheckUpdates: Bool?
1071
1205
  let autoRestartCodex: Bool?
1206
+ let autoRestartBoth: Bool?
1072
1207
  }
1073
1208
 
1074
1209
  struct BrowserlessSettings: Decodable {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-cookie-bridge",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Local-first cookie and session transfer for macOS with optional Browserless upload",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,40 +12,57 @@ const BROWSERS = {
12
12
  root: ["BraveSoftware", "Brave-Browser"],
13
13
  safeStorageService: "Brave Safe Storage",
14
14
  safeStorageAccount: "Brave",
15
+ processPattern: /\/Brave Browser\.app\/Contents\/MacOS\/Brave Browser(?:\s|$)/,
15
16
  },
16
17
  chrome: {
17
18
  root: ["Google", "Chrome"],
18
19
  safeStorageService: "Chrome Safe Storage",
19
20
  safeStorageAccount: "Chrome",
21
+ processPattern: /\/Google Chrome\.app\/Contents\/MacOS\/Google Chrome(?:\s|$)/,
20
22
  },
21
23
  edge: {
22
24
  root: ["Microsoft Edge"],
23
25
  safeStorageService: "Microsoft Edge Safe Storage",
24
26
  safeStorageAccount: "Microsoft Edge",
27
+ processPattern: /\/Microsoft Edge\.app\/Contents\/MacOS\/Microsoft Edge(?:\s|$)/,
25
28
  },
26
29
  arc: {
27
30
  root: ["Arc", "User Data"],
28
31
  safeStorageService: "Arc Safe Storage",
29
32
  safeStorageAccount: "Arc",
33
+ processPattern: /\/Arc\.app\/Contents\/MacOS\/Arc(?:\s|$)/,
30
34
  },
31
35
  vivaldi: {
32
36
  root: ["Vivaldi"],
33
37
  safeStorageService: "Vivaldi Safe Storage",
34
38
  safeStorageAccount: "Vivaldi",
39
+ processPattern: /\/Vivaldi\.app\/Contents\/MacOS\/Vivaldi(?:\s|$)/,
35
40
  },
36
41
  opera: {
37
42
  root: ["com.operasoftware.Opera"],
38
43
  directProfile: true,
39
44
  safeStorageService: "Opera Safe Storage",
40
45
  safeStorageAccount: "Opera",
46
+ processPattern: /\/Opera\.app\/Contents\/MacOS\/Opera(?:\s|$)/,
41
47
  },
42
48
  comet: {
43
49
  root: ["Comet"],
44
50
  safeStorageService: "Comet Safe Storage",
45
51
  safeStorageAccount: "Comet",
52
+ processPattern: /\/Comet\.app\/Contents\/MacOS\/Comet(?:\s|$)/,
46
53
  },
47
54
  };
48
55
 
56
+ export function isChromiumBrowserRunning({ browser, processList } = {}) {
57
+ const definition = BROWSERS[browser];
58
+ if (!definition) throw new Error(`Unsupported Chromium source: ${browser}`);
59
+ const output = processList ?? spawnSync("/bin/ps", ["-axo", "command="], {
60
+ encoding: "utf8",
61
+ maxBuffer: 16 * 1024 * 1024,
62
+ }).stdout;
63
+ return String(output).split("\n").some((command) => definition.processPattern.test(command.trim()));
64
+ }
65
+
49
66
  export function readChromiumProfile({
50
67
  browser,
51
68
  imports = { cookies: true, history: false },
@@ -61,7 +78,7 @@ export function readChromiumProfile({
61
78
  throw new Error(`${browserDisplayName(browser)} profile not found at ${profilePath}`);
62
79
  }
63
80
 
64
- const cookies = imports.cookies
81
+ const cookieResult = imports.cookies
65
82
  ? readCookies({
66
83
  databasePath: firstExisting([
67
84
  path.join(profilePath, "Network", "Cookies"),
@@ -69,12 +86,18 @@ export function readChromiumProfile({
69
86
  ]),
70
87
  password: password ?? readSafeStoragePassword(definition, browser),
71
88
  })
72
- : [];
89
+ : { cookies: [], total: 0, skipped: 0 };
73
90
  const history = imports.history
74
91
  ? readHistory(path.join(profilePath, "History"))
75
92
  : [];
76
93
 
77
- return { cookies, history, profileName, profilePath };
94
+ return {
95
+ cookies: cookieResult.cookies,
96
+ cookieStats: { total: cookieResult.total, imported: cookieResult.cookies.length, skipped: cookieResult.skipped },
97
+ history,
98
+ profileName,
99
+ profilePath,
100
+ };
78
101
  }
79
102
 
80
103
  export function decryptChromiumCookieValue({ domain, encryptedValue, password }) {
@@ -105,6 +128,7 @@ function readCookies({ databasePath, password }) {
105
128
  FROM cookies
106
129
  `).all();
107
130
  const cookies = [];
131
+ let skipped = 0;
108
132
  for (const row of rows) {
109
133
  try {
110
134
  const value = row.value || decryptChromiumCookieValue({
@@ -133,9 +157,10 @@ function readCookies({ databasePath, password }) {
133
157
  });
134
158
  } catch {
135
159
  // An individual malformed or obsolete cookie must not block the rest of the profile.
160
+ skipped += 1;
136
161
  }
137
162
  }
138
- return cookies;
163
+ return { cookies, total: rows.length, skipped };
139
164
  } finally {
140
165
  database.close();
141
166
  }
package/src/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  directImportToCodex,
9
9
  isCodexRunning,
10
10
  } from "./codex-direct-import.js";
11
- import { readChromiumProfile } from "./chromium-reader.js";
11
+ import { isChromiumBrowserRunning, readChromiumProfile } from "./chromium-reader.js";
12
12
  import { uploadBrowserlessProfile } from "./browserless.js";
13
13
  import { inspectBrowserlessProfile } from "./browserless-preflight.js";
14
14
  import { installConfig, installRuntime, readConfig, updatePreferences } from "./config.js";
@@ -46,7 +46,7 @@ Commands:
46
46
  setup [--hour 9] [--minute 0] [--no-schedule]
47
47
  install-app [--no-open] [--replace-system-from-source]
48
48
  bootstrap-bundled --app-path /Applications/Browser Cookie Bridge.app
49
- preferences --source brave --target codex --cookies on --history off --menu-bar on --auto-check-updates on --auto-restart-codex off
49
+ preferences --source brave --target codex --cookies on --history off --site-storage off --menu-bar on --auto-check-updates on --auto-restart-codex off --auto-restart-both off
50
50
  sync [--timeout 300] [--allow-cloud-upload]
51
51
  browserless-preflight
52
52
  doctor
@@ -124,18 +124,20 @@ function preferences(args) {
124
124
  const config = updatePreferences({
125
125
  cookies: booleanFlag(args, "--cookies", existing.imports?.cookies !== false),
126
126
  history: booleanFlag(args, "--history", existing.imports?.history === true),
127
+ siteStorage: booleanFlag(args, "--site-storage", existing.imports?.siteStorage === true),
127
128
  sourceBrowser: stringFlag(args, "--source", existing.sourceBrowser || "brave"),
128
129
  targetBrowser: stringFlag(args, "--target", existing.targetBrowser || "codex"),
129
130
  menuBar: booleanFlag(args, "--menu-bar", existing.ui?.menuBar === true),
130
131
  openAtLogin: booleanFlag(args, "--open-at-login", existing.ui?.openAtLogin !== false),
131
132
  autoCheckUpdates: booleanFlag(args, "--auto-check-updates", existing.ui?.autoCheckUpdates !== false),
132
133
  autoRestartCodex: booleanFlag(args, "--auto-restart-codex", existing.ui?.autoRestartCodex === true),
134
+ autoRestartBoth: booleanFlag(args, "--auto-restart-both", existing.ui?.autoRestartBoth === true),
133
135
  browserlessProfileName: stringFlag(args, "--browserless-profile", existing.browserless?.profileName || "browser-cookie-bridge"),
134
136
  browserlessRegion: stringFlag(args, "--browserless-region", existing.browserless?.region || "sfo"),
135
137
  browserlessOnlyDomains: optionalStringFlag(args, "--browserless-domains", (existing.browserless?.onlyDomains || []).join(",")),
136
138
  });
137
139
  console.log(
138
- `Saved: source=${config.sourceBrowser}, target=${config.targetBrowser}, cookies=${config.imports.cookies ? "on" : "off"}, history=${config.imports.history ? "on" : "off"}, menu-bar=${config.ui.menuBar ? "on" : "off"}, open-at-login=${config.ui.openAtLogin ? "on" : "off"}, auto-check-updates=${config.ui.autoCheckUpdates ? "on" : "off"}, auto-restart-codex=${config.ui.autoRestartCodex ? "on" : "off"}`,
140
+ `Saved: source=${config.sourceBrowser}, target=${config.targetBrowser}, cookies=${config.imports.cookies ? "on" : "off"}, history=${config.imports.history ? "on" : "off"}, site-storage=${config.imports.siteStorage ? "on" : "off"}, menu-bar=${config.ui.menuBar ? "on" : "off"}, open-at-login=${config.ui.openAtLogin ? "on" : "off"}, auto-check-updates=${config.ui.autoCheckUpdates ? "on" : "off"}, auto-restart-codex=${config.ui.autoRestartCodex ? "on" : "off"}, auto-restart-both=${config.ui.autoRestartBoth ? "on" : "off"}`,
139
141
  );
140
142
  }
141
143
 
@@ -306,14 +308,21 @@ async function sync(args, { signal } = {}) {
306
308
  }
307
309
  if (isCodexTarget) {
308
310
  if (isCodexRunning()) throw new Error(CODEX_RUNNING_ERROR);
311
+ const siteStorage = config.imports?.siteStorage === true;
312
+ if (siteStorage && isChromiumBrowserRunning({ browser: source })) {
313
+ throw new Error(`Quit ${source} completely before importing full site data into Codex.`);
314
+ }
309
315
  const payload = readChromiumProfile({
310
316
  browser: config.sourceBrowser || "brave",
311
317
  imports: config.imports || { cookies: true, history: false },
312
318
  });
313
- console.log(`Read ${payload.cookies.length} cookies and ${payload.history.length} history URLs from ${config.sourceBrowser || "brave"}.`);
319
+ console.log(`Read ${payload.cookies.length} of ${payload.cookieStats.total} cookies (${payload.cookieStats.skipped} unavailable) and ${payload.history.length} history URLs from ${config.sourceBrowser || "brave"}.`);
314
320
  const result = directImportToCodex({
315
321
  cookies: payload.cookies,
316
322
  history: payload.history,
323
+ sourceProfilePath: payload.profilePath,
324
+ siteStorage,
325
+ sourceCookieSkipped: payload.cookieStats.skipped,
317
326
  codexRunning: false,
318
327
  });
319
328
  console.log(directCodexSummary(result));
@@ -363,10 +372,15 @@ export function directCodexSummary(result) {
363
372
  const imported = result.imported + result.historyImported;
364
373
  const skipped = result.skipped + result.historySkipped;
365
374
  const failures = result.failed + result.historyFailed;
366
- if (failures > 0) {
367
- return `Direct Codex sync completed with warnings: ${imported} imported, ${skipped} skipped, ${failures} failed. Backup: ${result.backupPath}`;
375
+ const unavailable = result.sourceCookieSkipped || 0;
376
+ const siteStorage = result.siteStorageImported > 0
377
+ ? ` Full site data: ${result.siteStorageImported} storage area${result.siteStorageImported === 1 ? "" : "s"} replaced from the source profile.`
378
+ : "";
379
+ if (failures > 0 || unavailable > 0) {
380
+ const sourceNote = unavailable > 0 ? ` ${unavailable} source cookie${unavailable === 1 ? " was" : "s were"} unreadable or unsupported.` : "";
381
+ return `Direct Codex sync completed with warnings: ${imported} imported, ${skipped} skipped, ${failures} failed.${sourceNote}${siteStorage} Backup: ${result.backupPath}`;
368
382
  }
369
- return `Direct Codex sync complete: ${imported} imported and ${skipped} skipped. Reopen Codex to use the updated sessions. Backup: ${result.backupPath}`;
383
+ return `Direct Codex sync complete: ${imported} imported and ${skipped} skipped.${siteStorage} Reopen Codex to use the updated sessions. Backup: ${result.backupPath}`;
370
384
  }
371
385
 
372
386
  export function transferSummary(result) {
@@ -8,6 +8,16 @@ import { appSupportDir, codexCookiePaths } from "./paths.js";
8
8
  const CHROMIUM_EPOCH_OFFSET_SECONDS = 11_644_473_600;
9
9
  const CODEX_RUNNING_ERROR = "ChatGPT Codex is open. Quit it completely, then click Sync now again.";
10
10
  const MAX_BACKUPS = 14;
11
+ const SITE_STORAGE_DIRECTORIES = [
12
+ "Local Storage",
13
+ "IndexedDB",
14
+ "Session Storage",
15
+ "Service Worker",
16
+ "File System",
17
+ "WebStorage",
18
+ "shared_proto_db",
19
+ "Shared Dictionary",
20
+ ];
11
21
 
12
22
  export function isCodexRunning({ processList } = {}) {
13
23
  const output = processList ?? spawnSync("/bin/ps", ["-axo", "command="], {
@@ -22,6 +32,9 @@ export function isCodexRunning({ processList } = {}) {
22
32
  export function directImportToCodex({
23
33
  cookies,
24
34
  history = [],
35
+ sourceProfilePath,
36
+ siteStorage = false,
37
+ sourceCookieSkipped = 0,
25
38
  home = os.homedir(),
26
39
  codexRunning = isCodexRunning(),
27
40
  now = new Date(),
@@ -36,6 +49,7 @@ export function directImportToCodex({
36
49
  throw new Error("Codex browser storage was not found. Open the Codex browser once, quit Codex, then try again.");
37
50
  }
38
51
  const historyPath = path.join(path.dirname(cookiePath), "History");
52
+ const codexProfilePath = codexProfileRoot(cookiePath);
39
53
  const backupRoot = path.join(appSupportDir(home), "backups", "codex");
40
54
  const backupPath = path.join(backupRoot, backupDirectoryName(now));
41
55
  fs.mkdirSync(backupPath, { recursive: true, mode: 0o700 });
@@ -76,6 +90,20 @@ export function directImportToCodex({
76
90
  fs.chmodSync(historyWorking, 0o600);
77
91
  }
78
92
 
93
+ let siteStoragePlan = [];
94
+ if (siteStorage) {
95
+ try {
96
+ if (!sourceProfilePath || !fs.existsSync(sourceProfilePath)) {
97
+ throw new Error("The selected source profile was not found for full site-data import.");
98
+ }
99
+ siteStoragePlan = prepareSiteStorageTransfer({ sourceProfilePath, codexProfilePath, backupPath });
100
+ } catch (error) {
101
+ fs.rmSync(cookieWorking, { force: true });
102
+ if (historyWorking) fs.rmSync(historyWorking, { force: true });
103
+ throw error;
104
+ }
105
+ }
106
+
79
107
  let cookieResult;
80
108
  let historyResult = { imported: 0, skipped: 0, failed: 0 };
81
109
  try {
@@ -87,13 +115,16 @@ export function directImportToCodex({
87
115
  replaceDatabase(historyWorking, historyPath);
88
116
  historyResult.replaced = true;
89
117
  }
118
+ commitSiteStorageTransfer(siteStoragePlan);
90
119
  } catch (error) {
91
120
  restoreDatabase(cookieBackup, cookiePath);
92
121
  if (historyBackup) restoreDatabase(historyBackup, historyPath);
122
+ restoreSiteStorageTransfer(siteStoragePlan);
93
123
  throw new Error(`Codex data was restored from backup after the sync failed: ${error.message}`);
94
124
  } finally {
95
125
  fs.rmSync(cookieWorking, { force: true });
96
126
  if (historyWorking) fs.rmSync(historyWorking, { force: true });
127
+ cleanupSiteStorageTransfer(siteStoragePlan);
97
128
  }
98
129
 
99
130
  pruneBackups(backupRoot, MAX_BACKUPS);
@@ -107,9 +138,78 @@ export function directImportToCodex({
107
138
  backupPath,
108
139
  targetPath: cookiePath,
109
140
  directCodexImport: true,
141
+ siteStorageImported: siteStoragePlan.filter((item) => item.committed).length,
142
+ siteStorageNames: siteStoragePlan.filter((item) => item.committed).map((item) => item.name),
143
+ sourceCookieSkipped: Number.isInteger(sourceCookieSkipped) && sourceCookieSkipped > 0 ? sourceCookieSkipped : 0,
110
144
  };
111
145
  }
112
146
 
147
+ function codexProfileRoot(cookiePath) {
148
+ const parent = path.dirname(cookiePath);
149
+ return path.basename(parent) === "Network" ? path.dirname(parent) : parent;
150
+ }
151
+
152
+ function prepareSiteStorageTransfer({ sourceProfilePath, codexProfilePath, backupPath }) {
153
+ const plan = [];
154
+ try {
155
+ fs.mkdirSync(codexProfilePath, { recursive: true, mode: 0o700 });
156
+ for (const name of SITE_STORAGE_DIRECTORIES) {
157
+ const source = path.join(sourceProfilePath, name);
158
+ if (!fs.existsSync(source)) continue;
159
+ const destination = path.join(codexProfilePath, name);
160
+ const backup = path.join(backupPath, "Site Storage", name);
161
+ const stage = `${destination}.browser-cookie-bridge-${process.pid}-${Date.now()}.stage`;
162
+ const existed = fs.existsSync(destination);
163
+ const item = { name, destination, backup, stage, existed, touched: false, committed: false };
164
+ plan.push(item);
165
+ if (existed) copyStorageTree(destination, backup);
166
+ copyStorageTree(source, stage);
167
+ }
168
+ if (plan.length === 0) {
169
+ throw new Error("No compatible Local Storage, IndexedDB, session, or service-worker data was found in the source profile.");
170
+ }
171
+ return plan;
172
+ } catch (error) {
173
+ cleanupSiteStorageTransfer(plan);
174
+ throw error;
175
+ }
176
+ }
177
+
178
+ function commitSiteStorageTransfer(plan) {
179
+ for (const item of plan) {
180
+ item.touched = true;
181
+ fs.rmSync(item.destination, { recursive: true, force: true });
182
+ fs.renameSync(item.stage, item.destination);
183
+ item.committed = true;
184
+ }
185
+ }
186
+
187
+ function restoreSiteStorageTransfer(plan) {
188
+ for (const item of plan) {
189
+ if (!item.touched) continue;
190
+ fs.rmSync(item.destination, { recursive: true, force: true });
191
+ if (item.existed && fs.existsSync(item.backup)) copyStorageTree(item.backup, item.destination);
192
+ item.touched = false;
193
+ item.committed = false;
194
+ }
195
+ }
196
+
197
+ function cleanupSiteStorageTransfer(plan) {
198
+ for (const item of plan) fs.rmSync(item.stage, { recursive: true, force: true });
199
+ }
200
+
201
+ function copyStorageTree(source, destination) {
202
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
203
+ fs.cpSync(source, destination, {
204
+ recursive: true,
205
+ force: true,
206
+ preserveTimestamps: true,
207
+ filter(candidate) {
208
+ try { return !fs.lstatSync(candidate).isSymbolicLink(); } catch { return false; }
209
+ },
210
+ });
211
+ }
212
+
113
213
  function mergeCookies(databasePath, cookies, now) {
114
214
  const database = new DatabaseSync(databasePath);
115
215
  let imported = 0;
package/src/config.js CHANGED
@@ -45,12 +45,14 @@ export function installConfig({ home, hour = 9, minute = 0, nodePath = process.e
45
45
  cookies: existing.imports?.cookies !== false,
46
46
  passwords: false,
47
47
  history: existing.imports?.history === true,
48
+ siteStorage: existing.imports?.siteStorage === true,
48
49
  },
49
50
  ui: {
50
51
  menuBar: existing.ui?.menuBar !== false,
51
52
  openAtLogin: existing.ui?.openAtLogin !== false,
52
53
  autoCheckUpdates: existing.ui?.autoCheckUpdates !== false,
53
54
  autoRestartCodex: existing.ui?.autoRestartCodex === true,
55
+ autoRestartBoth: existing.ui?.autoRestartBoth === true,
54
56
  },
55
57
  browserless: {
56
58
  profileName: cleanProfileName(existing.browserless?.profileName) || "browser-cookie-bridge",
@@ -73,12 +75,14 @@ export function updatePreferences({
73
75
  home,
74
76
  cookies,
75
77
  history,
78
+ siteStorage,
76
79
  sourceBrowser,
77
80
  targetBrowser,
78
81
  menuBar,
79
82
  openAtLogin,
80
83
  autoCheckUpdates,
81
84
  autoRestartCodex,
85
+ autoRestartBoth,
82
86
  browserlessProfileName,
83
87
  browserlessRegion,
84
88
  browserlessOnlyDomains,
@@ -95,12 +99,18 @@ export function updatePreferences({
95
99
  }
96
100
  config.sourceBrowser = sourceBrowser;
97
101
  config.targetBrowser = targetBrowser;
98
- config.imports = { cookies: Boolean(cookies), passwords: false, history: Boolean(history) };
102
+ config.imports = {
103
+ cookies: Boolean(cookies),
104
+ passwords: false,
105
+ history: Boolean(history),
106
+ siteStorage: Boolean(siteStorage),
107
+ };
99
108
  config.ui = {
100
109
  menuBar: Boolean(menuBar),
101
110
  openAtLogin: Boolean(openAtLogin),
102
111
  autoCheckUpdates: Boolean(autoCheckUpdates),
103
112
  autoRestartCodex: Boolean(autoRestartCodex),
113
+ autoRestartBoth: Boolean(autoRestartBoth),
104
114
  };
105
115
  const region = browserlessRegion ?? config.browserless?.region ?? "sfo";
106
116
  if (!["sfo", "lon", "ams"].includes(region)) throw new Error("Browserless region must be sfo, lon, or ams");