browser-cookie-bridge 1.4.2 → 1.4.4
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 +23 -3
- package/extension-template/manifest.json +1 -1
- package/macos-app/Info.plist +2 -2
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +32 -8
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +147 -12
- package/package.json +1 -1
- package/src/chromium-reader.js +29 -4
- package/src/cli.js +35 -8
- package/src/codex-direct-import.js +100 -0
- package/src/config.js +11 -1
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** | ◐
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "1.4.4",
|
|
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"],
|
package/macos-app/Info.plist
CHANGED
|
@@ -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.
|
|
14
|
-
<key>CFBundleVersion</key><string>
|
|
13
|
+
<key>CFBundleShortVersionString</key><string>1.4.4</string>
|
|
14
|
+
<key>CFBundleVersion</key><string>32</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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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" &&
|
|
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
package/src/chromium-reader.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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,22 @@ 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
|
+
const source = requireClosedCodexSource({
|
|
313
|
+
sourceBrowser: config.sourceBrowser,
|
|
314
|
+
siteStorage,
|
|
315
|
+
});
|
|
309
316
|
const payload = readChromiumProfile({
|
|
310
|
-
browser:
|
|
317
|
+
browser: source,
|
|
311
318
|
imports: config.imports || { cookies: true, history: false },
|
|
312
319
|
});
|
|
313
|
-
console.log(`Read ${payload.cookies.length} cookies and ${payload.history.length} history URLs from ${
|
|
320
|
+
console.log(`Read ${payload.cookies.length} of ${payload.cookieStats.total} cookies (${payload.cookieStats.skipped} unavailable) and ${payload.history.length} history URLs from ${source}.`);
|
|
314
321
|
const result = directImportToCodex({
|
|
315
322
|
cookies: payload.cookies,
|
|
316
323
|
history: payload.history,
|
|
324
|
+
sourceProfilePath: payload.profilePath,
|
|
325
|
+
siteStorage,
|
|
326
|
+
sourceCookieSkipped: payload.cookieStats.skipped,
|
|
317
327
|
codexRunning: false,
|
|
318
328
|
});
|
|
319
329
|
console.log(directCodexSummary(result));
|
|
@@ -344,6 +354,18 @@ async function sync(args, { signal } = {}) {
|
|
|
344
354
|
return result;
|
|
345
355
|
}
|
|
346
356
|
|
|
357
|
+
export function requireClosedCodexSource({
|
|
358
|
+
sourceBrowser,
|
|
359
|
+
siteStorage,
|
|
360
|
+
runningCheck = isChromiumBrowserRunning,
|
|
361
|
+
}) {
|
|
362
|
+
const source = sourceBrowser || "brave";
|
|
363
|
+
if (siteStorage && runningCheck({ browser: source })) {
|
|
364
|
+
throw new Error(`Quit ${source} completely before importing full site data into Codex.`);
|
|
365
|
+
}
|
|
366
|
+
return source;
|
|
367
|
+
}
|
|
368
|
+
|
|
347
369
|
function browserlessPreflight() {
|
|
348
370
|
assertMacOS();
|
|
349
371
|
const config = readConfig();
|
|
@@ -363,10 +385,15 @@ export function directCodexSummary(result) {
|
|
|
363
385
|
const imported = result.imported + result.historyImported;
|
|
364
386
|
const skipped = result.skipped + result.historySkipped;
|
|
365
387
|
const failures = result.failed + result.historyFailed;
|
|
366
|
-
|
|
367
|
-
|
|
388
|
+
const unavailable = result.sourceCookieSkipped || 0;
|
|
389
|
+
const siteStorage = result.siteStorageImported > 0
|
|
390
|
+
? ` Full site data: ${result.siteStorageImported} storage area${result.siteStorageImported === 1 ? "" : "s"} replaced from the source profile.`
|
|
391
|
+
: "";
|
|
392
|
+
if (failures > 0 || unavailable > 0) {
|
|
393
|
+
const sourceNote = unavailable > 0 ? ` ${unavailable} source cookie${unavailable === 1 ? " was" : "s were"} unreadable or unsupported.` : "";
|
|
394
|
+
return `Direct Codex sync completed with warnings: ${imported} imported, ${skipped} skipped, ${failures} failed.${sourceNote}${siteStorage} Backup: ${result.backupPath}`;
|
|
368
395
|
}
|
|
369
|
-
return `Direct Codex sync complete: ${imported} imported and ${skipped} skipped
|
|
396
|
+
return `Direct Codex sync complete: ${imported} imported and ${skipped} skipped.${siteStorage} Reopen Codex to use the updated sessions. Backup: ${result.backupPath}`;
|
|
370
397
|
}
|
|
371
398
|
|
|
372
399
|
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 = {
|
|
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");
|