browser-cookie-bridge 1.3.0 → 1.4.0

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
@@ -44,7 +44,7 @@
44
44
 
45
45
  ---
46
46
 
47
- > **Version 1.3.0 makes large Browserless uploads observable and recoverable.** The app measures the local profile and IndexedDB without reading their contents, streams capture progress and elapsed time, supports cancellation with deterministic temporary-data cleanup, reports auto-fit omissions, and verifies the cloud profile after upload.
47
+ > **Version 1.4.0 adds an optional one-click Codex restart flow.** When enabled for a manual sync, Browser Cookie Bridge force quits Codex, waits for its database to close, performs the verified local transfer, preserves the imported/skipped/failed counts, and reopens Codex only after success. The option is off by default.
48
48
 
49
49
  ## Why Browser Cookie Bridge
50
50
 
@@ -57,6 +57,7 @@ Browser Cookie Bridge gives those browser profiles a small, native control panel
57
57
  - 🍪 **Cookie and session transfer** — cookies are enabled by default, including supported domain, path, expiry, security, `SameSite`, and partition attributes.
58
58
  - 🌐 **Seven Chromium browsers** — Brave, Chrome, Edge, Arc, Vivaldi, Opera, and Perplexity Comet can be sources or destinations.
59
59
  - ✨ **ChatGPT Codex import** — merge selected local browser data into Codex's built-in browser; Codex is destination-only.
60
+ - 🔁 **Optional Codex restart** — manual Sync can force quit a running Codex instance and reopen it only after a successful local transfer; off by default.
60
61
  - ☁️ **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.
61
62
  - 🕘 **Background automation** — sync when you sign in, at a fixed daily time, or whenever you choose.
62
63
  - ◉ **Native menu-bar app** — closing the window removes the Dock icon while the helper continues running.
@@ -125,11 +126,11 @@ The final command compiles the native SwiftUI app, enables **Open at login**, **
125
126
  No browser extension is needed for this path.
126
127
 
127
128
  1. Select a source browser and **ChatGPT Codex** as the destination.
128
- 2. Quit Codex completely. Closing only its browser panel is not enough.
129
+ 2. Quit Codex completely, or enable **Restart Codex automatically**. Closing only its browser panel is not enough.
129
130
  3. Choose **Cookies** and, optionally, **History URLs**.
130
- 4. Press **Sync now** and reopen Codex after the success message.
131
+ 4. Press **Sync now** and reopen Codex after the success message if automatic restart is off.
131
132
 
132
- If Codex is open, the app blocks the transfer and tells you what to close. It never force-quits Codex.
133
+ By default, an open Codex blocks the transfer. The optional **Restart Codex automatically** setting applies only to a manual Sync: it force quits Codex, waits for its database to close, and reopens Codex only after the transfer succeeds. Scheduled and login syncs never force quit it.
133
134
 
134
135
  ### Browser → browser
135
136
 
@@ -171,6 +172,7 @@ The official CLI records the Browserless upload-disclaimer acceptance timestamp
171
172
  | **Sync at login** | Runs once whenever you sign in; on by default |
172
173
  | **Open at login** | Starts the background app after macOS login; on by default |
173
174
  | **Show in menu bar** | Keeps sync, status, updates, and support actions close at hand |
175
+ | **Restart Codex automatically** | Force quits Codex for a manual sync and reopens it only after success; off by default |
174
176
  | **Check for updates** | Finds a newer GitHub release, verifies its DMG, and offers install + relaunch |
175
177
 
176
178
  Automation uses the saved source, destination, and data choices for local transfers. A scheduled Codex sync safely exits without making changes when Codex is open. Browserless cloud uploads are always manual and require an explicit upload action.
@@ -180,7 +182,7 @@ Automation uses the saved source, destination, and data choices for local transf
180
182
  ```bash
181
183
  browser-cookie-bridge install-app [--no-open]
182
184
  browser-cookie-bridge setup [--hour 9] [--minute 0] [--no-schedule]
183
- browser-cookie-bridge preferences --source brave --target codex --cookies on --history off
185
+ browser-cookie-bridge preferences --source brave --target codex --cookies on --history off --auto-restart-codex off
184
186
  browser-cookie-bridge sync [--timeout 300] [--allow-cloud-upload]
185
187
  browser-cookie-bridge browserless-preflight
186
188
  browser-cookie-bridge doctor
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Cookie Bridge",
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
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.3.0</string>
14
- <key>CFBundleVersion</key><string>27</string>
13
+ <key>CFBundleShortVersionString</key><string>1.4.0</string>
14
+ <key>CFBundleVersion</key><string>28</string>
15
15
  <key>LSMinimumSystemVersion</key><string>13.5</string>
16
16
  <key>NSHighResolutionCapable</key><true/>
17
17
  </dict>
@@ -578,6 +578,18 @@ struct PreferencesPanel: View {
578
578
  PreferenceRow(icon: "key.slash", color: .secondary, title: "Passwords", detail: "Browser extensions cannot access passwords", muted: true) {
579
579
  FixedBadge("Unavailable")
580
580
  }
581
+ if model.selectedTargetID == "codex" {
582
+ 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)
591
+ }
592
+ }
581
593
  }
582
594
 
583
595
  SectionLabel(title: "Automation", detail: "Runs in the background", separated: true)
@@ -77,6 +77,7 @@ final class SyncModel: ObservableObject {
77
77
  @Published var openAtLogin = false
78
78
  @Published var menuBarEnabled = true
79
79
  @Published var autoCheckUpdates = true
80
+ @Published var autoRestartCodex = false
80
81
  @Published var isCheckingForUpdates = false
81
82
  @Published var isInstallingUpdate = false
82
83
  @Published var availableUpdateVersion: String?
@@ -128,7 +129,7 @@ final class SyncModel: ObservableObject {
128
129
  var targetName: String {
129
130
  isBrowserlessTarget ? "Browserless Cloud" : selectedTargetBrowser?.name ?? "ChatGPT Codex"
130
131
  }
131
- var codexBlocked: Bool { selectedTargetID == "codex" && codexRunning }
132
+ var codexBlocked: Bool { selectedTargetID == "codex" && codexRunning && !autoRestartCodex }
132
133
  var browserlessBlocked: Bool {
133
134
  isBrowserlessTarget && (!browserlessConfigured || sourceBrowserRunning || selectedSourceID == "comet")
134
135
  }
@@ -261,6 +262,7 @@ final class SyncModel: ObservableObject {
261
262
  historyEnabled = config.imports?.history ?? false
262
263
  menuBarEnabled = config.ui?.menuBar ?? true
263
264
  autoCheckUpdates = config.ui?.autoCheckUpdates ?? true
265
+ autoRestartCodex = config.ui?.autoRestartCodex ?? false
264
266
  browserlessProfileName = config.browserless?.profileName ?? "browser-cookie-bridge"
265
267
  browserlessRegion = config.browserless?.region ?? "sfo"
266
268
  browserlessOnlyDomains = (config.browserless?.onlyDomains ?? []).joined(separator: ", ")
@@ -347,6 +349,11 @@ final class SyncModel: ObservableObject {
347
349
  if enabled { checkForUpdates() }
348
350
  }
349
351
 
352
+ func setAutoRestartCodex(_ enabled: Bool) {
353
+ autoRestartCodex = enabled
354
+ persistPreferences(successMessage: enabled ? "Automatic Codex restart enabled" : "Automatic Codex restart disabled")
355
+ }
356
+
350
357
  func checkForUpdates(showAlert: Bool = false) {
351
358
  guard !isCheckingForUpdates, !isInstallingUpdate else { return }
352
359
  guard let url = URL(string: "https://api.github.com/repos/apoorvdarshan/browser-cookie-bridge/releases/latest") else { return }
@@ -449,6 +456,68 @@ final class SyncModel: ObservableObject {
449
456
  }
450
457
  return
451
458
  }
459
+ if selectedTargetID == "codex" && codexRunning && autoRestartCodex {
460
+ forceQuitCodexThenSync(showMenuBarAlert: showMenuBarAlert)
461
+ return
462
+ }
463
+ startSync(showMenuBarAlert: showMenuBarAlert, reopenCodexOnSuccess: false)
464
+ }
465
+
466
+ private func forceQuitCodexThenSync(showMenuBarAlert: Bool) {
467
+ isSyncing = true
468
+ uploadCanceling = false
469
+ state = .syncing
470
+ primaryStatus = "Closing Codex for sync"
471
+ secondaryStatus = "Force quitting ChatGPT Codex and waiting for its browser database to close…"
472
+ let applications = NSWorkspace.shared.runningApplications.filter {
473
+ $0.bundleIdentifier == "com.openai.codex"
474
+ }
475
+ guard !applications.isEmpty, applications.allSatisfy({ $0.forceTerminate() }) else {
476
+ finishCodexPreparationFailure(
477
+ message: "macOS could not force quit ChatGPT Codex. Quit it manually, then try again.",
478
+ showMenuBarAlert: showMenuBarAlert
479
+ )
480
+ return
481
+ }
482
+ waitForCodexToQuit(attemptsRemaining: 50, showMenuBarAlert: showMenuBarAlert)
483
+ }
484
+
485
+ private func waitForCodexToQuit(attemptsRemaining: Int, showMenuBarAlert: Bool) {
486
+ let stillRunning = NSWorkspace.shared.runningApplications.contains {
487
+ $0.bundleIdentifier == "com.openai.codex"
488
+ }
489
+ if !stillRunning {
490
+ codexRunning = false
491
+ secondaryStatus = "Codex is closed — waiting briefly for its database to be released…"
492
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
493
+ self?.startSync(showMenuBarAlert: showMenuBarAlert, reopenCodexOnSuccess: true)
494
+ }
495
+ return
496
+ }
497
+ guard attemptsRemaining > 0 else {
498
+ finishCodexPreparationFailure(
499
+ message: "ChatGPT Codex did not close within 10 seconds. Quit it manually, then try again.",
500
+ showMenuBarAlert: showMenuBarAlert
501
+ )
502
+ return
503
+ }
504
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
505
+ self?.waitForCodexToQuit(attemptsRemaining: attemptsRemaining - 1, showMenuBarAlert: showMenuBarAlert)
506
+ }
507
+ }
508
+
509
+ private func finishCodexPreparationFailure(message: String, showMenuBarAlert: Bool) {
510
+ isSyncing = false
511
+ state = .error
512
+ primaryStatus = "Could not close Codex"
513
+ secondaryStatus = message
514
+ updateEndpointRunningStatus()
515
+ if showMenuBarAlert {
516
+ postNativeAlert(title: primaryStatus, message: secondaryStatus, kind: .error)
517
+ }
518
+ }
519
+
520
+ private func startSync(showMenuBarAlert: Bool, reopenCodexOnSuccess: Bool) {
452
521
  isSyncing = true
453
522
  uploadCanceling = false
454
523
  state = .syncing
@@ -507,7 +576,13 @@ final class SyncModel: ObservableObject {
507
576
  : "Keep both browsers open and check the extensions")
508
577
  }
509
578
  self.updateEndpointRunningStatus()
510
- if showMenuBarAlert {
579
+ if success && reopenCodexOnSuccess {
580
+ self.reopenCodexAfterSuccessfulSync(
581
+ partial: partial,
582
+ syncSummary: self.secondaryStatus,
583
+ showMenuBarAlert: showMenuBarAlert
584
+ )
585
+ } else if showMenuBarAlert {
511
586
  self.postNativeAlert(
512
587
  title: self.primaryStatus,
513
588
  message: self.secondaryStatus,
@@ -517,6 +592,44 @@ final class SyncModel: ObservableObject {
517
592
  }
518
593
  }
519
594
 
595
+ private func reopenCodexAfterSuccessfulSync(partial: Bool, syncSummary: String, showMenuBarAlert: Bool) {
596
+ let transferResult = syncSummary.replacingOccurrences(
597
+ of: "Reopen Codex to use the updated sessions. ",
598
+ with: ""
599
+ )
600
+ guard let codexURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.openai.codex") else {
601
+ state = .warning
602
+ 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."
604
+ if showMenuBarAlert {
605
+ postNativeAlert(title: primaryStatus, message: secondaryStatus, kind: .warning)
606
+ }
607
+ return
608
+ }
609
+ NSWorkspace.shared.openApplication(at: codexURL, configuration: NSWorkspace.OpenConfiguration()) { [weak self] _, error in
610
+ Task { @MainActor in
611
+ guard let self else { return }
612
+ if let error {
613
+ self.state = .warning
614
+ self.primaryStatus = "Codex sessions updated, but Codex was not reopened"
615
+ self.secondaryStatus = "\(transferResult)\n\nCodex could not be reopened: \(error.localizedDescription)"
616
+ } else {
617
+ 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."
621
+ }
622
+ if showMenuBarAlert {
623
+ self.postNativeAlert(
624
+ title: self.primaryStatus,
625
+ message: self.secondaryStatus,
626
+ kind: self.state == .success ? .information : .warning
627
+ )
628
+ }
629
+ }
630
+ }
631
+ }
632
+
520
633
  func cancelSync() {
521
634
  guard isBrowserlessTarget, isSyncing, !uploadCanceling else { return }
522
635
  uploadCanceling = true
@@ -627,6 +740,7 @@ final class SyncModel: ObservableObject {
627
740
  "--history", historyEnabled ? "on" : "off",
628
741
  "--menu-bar", menuBarEnabled ? "on" : "off",
629
742
  "--auto-check-updates", autoCheckUpdates ? "on" : "off",
743
+ "--auto-restart-codex", autoRestartCodex ? "on" : "off",
630
744
  "--browserless-profile", browserlessProfileName,
631
745
  "--browserless-region", browserlessRegion,
632
746
  "--browserless-domains", browserlessOnlyDomains,
@@ -827,7 +941,13 @@ final class SyncModel: ObservableObject {
827
941
  $0.bundleIdentifier == selectedBrowser.bundleIdentifier
828
942
  }
829
943
  guard !isSyncing else { return }
830
- if selectedTargetID == "codex" && codexRunning {
944
+ if selectedTargetID == "codex" && codexRunning && autoRestartCodex {
945
+ if state == .ready || primaryStatus == "Quit Codex before syncing" {
946
+ state = .ready
947
+ primaryStatus = "Ready to sync and restart Codex"
948
+ secondaryStatus = "Sync will force quit Codex and reopen it only after a successful transfer"
949
+ }
950
+ } else if selectedTargetID == "codex" && codexRunning {
831
951
  state = .warning
832
952
  primaryStatus = "Quit Codex before syncing"
833
953
  secondaryStatus = "Close ChatGPT Codex completely so its local cookie database can be updated safely"
@@ -948,6 +1068,7 @@ private struct AppConfig: Decodable {
948
1068
  let menuBar: Bool?
949
1069
  let openAtLogin: Bool?
950
1070
  let autoCheckUpdates: Bool?
1071
+ let autoRestartCodex: Bool?
951
1072
  }
952
1073
 
953
1074
  struct BrowserlessSettings: Decodable {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-cookie-bridge",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Local-first cookie and session transfer for macOS with optional Browserless upload",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -45,7 +45,7 @@ Commands:
45
45
  setup [--hour 9] [--minute 0] [--no-schedule]
46
46
  install-app [--no-open]
47
47
  bootstrap-bundled --app-path /Applications/Browser Cookie Bridge.app
48
- preferences --source brave --target codex --cookies on --history off --menu-bar on --auto-check-updates on
48
+ preferences --source brave --target codex --cookies on --history off --menu-bar on --auto-check-updates on --auto-restart-codex off
49
49
  sync [--timeout 300] [--allow-cloud-upload]
50
50
  browserless-preflight
51
51
  doctor
@@ -128,12 +128,13 @@ function preferences(args) {
128
128
  menuBar: booleanFlag(args, "--menu-bar", existing.ui?.menuBar === true),
129
129
  openAtLogin: booleanFlag(args, "--open-at-login", existing.ui?.openAtLogin !== false),
130
130
  autoCheckUpdates: booleanFlag(args, "--auto-check-updates", existing.ui?.autoCheckUpdates !== false),
131
+ autoRestartCodex: booleanFlag(args, "--auto-restart-codex", existing.ui?.autoRestartCodex === true),
131
132
  browserlessProfileName: stringFlag(args, "--browserless-profile", existing.browserless?.profileName || "browser-cookie-bridge"),
132
133
  browserlessRegion: stringFlag(args, "--browserless-region", existing.browserless?.region || "sfo"),
133
134
  browserlessOnlyDomains: optionalStringFlag(args, "--browserless-domains", (existing.browserless?.onlyDomains || []).join(",")),
134
135
  });
135
136
  console.log(
136
- `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"}`,
137
+ `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"}`,
137
138
  );
138
139
  }
139
140
 
@@ -435,6 +436,7 @@ function setAppLogin(enabled) {
435
436
  menuBar: existing.ui?.menuBar === true,
436
437
  openAtLogin: enabled,
437
438
  autoCheckUpdates: existing.ui?.autoCheckUpdates !== false,
439
+ autoRestartCodex: existing.ui?.autoRestartCodex === true,
438
440
  browserlessProfileName: existing.browserless?.profileName,
439
441
  browserlessRegion: existing.browserless?.region,
440
442
  browserlessOnlyDomains: existing.browserless?.onlyDomains,
package/src/config.js CHANGED
@@ -50,6 +50,7 @@ export function installConfig({ home, hour = 9, minute = 0, nodePath = process.e
50
50
  menuBar: existing.ui?.menuBar !== false,
51
51
  openAtLogin: existing.ui?.openAtLogin !== false,
52
52
  autoCheckUpdates: existing.ui?.autoCheckUpdates !== false,
53
+ autoRestartCodex: existing.ui?.autoRestartCodex === true,
53
54
  },
54
55
  browserless: {
55
56
  profileName: cleanProfileName(existing.browserless?.profileName) || "browser-cookie-bridge",
@@ -77,6 +78,7 @@ export function updatePreferences({
77
78
  menuBar,
78
79
  openAtLogin,
79
80
  autoCheckUpdates,
81
+ autoRestartCodex,
80
82
  browserlessProfileName,
81
83
  browserlessRegion,
82
84
  browserlessOnlyDomains,
@@ -98,6 +100,7 @@ export function updatePreferences({
98
100
  menuBar: Boolean(menuBar),
99
101
  openAtLogin: Boolean(openAtLogin),
100
102
  autoCheckUpdates: Boolean(autoCheckUpdates),
103
+ autoRestartCodex: Boolean(autoRestartCodex),
101
104
  };
102
105
  const region = browserlessRegion ?? config.browserless?.region ?? "sfo";
103
106
  if (!["sfo", "lon", "ams"].includes(region)) throw new Error("Browserless region must be sfo, lon, or ams");