browser-cookie-bridge 1.4.6 → 1.5.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 +57 -32
- package/extension-template/manifest.json +2 -2
- package/macos-app/Info.plist +2 -2
- package/macos-app/Resources/BrowserIcons/grok-bot.png +0 -0
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +157 -21
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +183 -30
- package/package.json +4 -3
- package/src/cli.js +110 -23
- package/src/codex-direct-import.js +265 -104
- package/src/config.js +34 -4
- package/src/crc32.js +19 -0
- package/src/grok-bot-export.js +199 -0
- package/src/grok-bot-importer.mjs +178 -0
- package/src/paths.js +6 -1
- package/src/zip-store.js +92 -0
|
@@ -3,11 +3,41 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
|
-
import { appSupportDir, codexCookiePaths } from "./paths.js";
|
|
6
|
+
import { appSupportDir, codexCookiePaths, cursorCookiePaths } from "./paths.js";
|
|
7
7
|
|
|
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
|
+
const CURSOR_RUNNING_ERROR = "Cursor is open. Quit it completely, then click Sync now again.";
|
|
10
11
|
const MAX_BACKUPS = 14;
|
|
12
|
+
const DIRECT_TARGETS = {
|
|
13
|
+
codex: {
|
|
14
|
+
name: "Codex",
|
|
15
|
+
applicationName: "ChatGPT Codex",
|
|
16
|
+
storageName: "Codex browser",
|
|
17
|
+
cookiePaths: codexCookiePaths,
|
|
18
|
+
backupDirectory: "codex",
|
|
19
|
+
processPattern: /^\/.*\.app\/Contents\/MacOS\/ChatGPT$/,
|
|
20
|
+
runningError: CODEX_RUNNING_ERROR,
|
|
21
|
+
historySupported: true,
|
|
22
|
+
siteStorageSupported: true,
|
|
23
|
+
},
|
|
24
|
+
cursor: {
|
|
25
|
+
name: "Cursor",
|
|
26
|
+
applicationName: "Cursor",
|
|
27
|
+
storageName: "Cursor browser",
|
|
28
|
+
cookiePaths: cursorCookiePaths,
|
|
29
|
+
backupDirectory: "cursor",
|
|
30
|
+
processPattern: /^\/.*\.app\/Contents\/MacOS\/Cursor$/,
|
|
31
|
+
runningError: CURSOR_RUNNING_ERROR,
|
|
32
|
+
historySupported: false,
|
|
33
|
+
siteStorageSupported: false,
|
|
34
|
+
cookieSchemaVersion: "24",
|
|
35
|
+
cookieIndex: {
|
|
36
|
+
name: "cookies_unique_index",
|
|
37
|
+
columns: ["host_key", "top_frame_site_key", "has_cross_site_ancestor", "name", "path", "source_scheme", "source_port"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
11
41
|
const SITE_STORAGE_DIRECTORIES = [
|
|
12
42
|
"Local Storage",
|
|
13
43
|
"IndexedDB",
|
|
@@ -20,37 +50,67 @@ const SITE_STORAGE_DIRECTORIES = [
|
|
|
20
50
|
];
|
|
21
51
|
|
|
22
52
|
export function isCodexRunning({ processList } = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
53
|
+
return isDirectTargetRunning({ target: "codex", processList });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isCursorRunning({ processList } = {}) {
|
|
57
|
+
return isDirectTargetRunning({ target: "cursor", processList });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isDirectTargetRunning({ target, processList } = {}) {
|
|
61
|
+
const definition = directTargetDefinition(target);
|
|
62
|
+
let output = processList;
|
|
63
|
+
if (output === undefined) {
|
|
64
|
+
const result = spawnSync("/bin/ps", ["-axo", "comm="], {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
67
|
+
});
|
|
68
|
+
if (result.error || result.status !== 0) {
|
|
69
|
+
throw new Error(`Could not verify whether ${definition.applicationName} is running; refusing to modify its browser data.`);
|
|
70
|
+
}
|
|
71
|
+
output = result.stdout;
|
|
72
|
+
}
|
|
27
73
|
return String(output).split("\n").some((command) =>
|
|
28
|
-
|
|
74
|
+
definition.processPattern.test(command.trim()),
|
|
29
75
|
);
|
|
30
76
|
}
|
|
31
77
|
|
|
32
|
-
export function
|
|
78
|
+
export function directImportToEmbeddedBrowser({
|
|
79
|
+
target = "codex",
|
|
33
80
|
cookies,
|
|
34
81
|
history = [],
|
|
35
82
|
sourceProfilePath,
|
|
36
83
|
siteStorage = false,
|
|
37
84
|
sourceCookieSkipped = 0,
|
|
38
85
|
home = os.homedir(),
|
|
39
|
-
|
|
86
|
+
runningCheck = () => isDirectTargetRunning({ target }),
|
|
87
|
+
databaseReplacer = replaceDatabase,
|
|
88
|
+
databaseRestorer = restoreDatabase,
|
|
40
89
|
now = new Date(),
|
|
41
90
|
} = {}) {
|
|
91
|
+
const definition = directTargetDefinition(target);
|
|
92
|
+
const checkRunning = runningCheck;
|
|
42
93
|
if (!Array.isArray(cookies) || !Array.isArray(history)) {
|
|
43
94
|
throw new TypeError("cookies and history must be arrays");
|
|
44
95
|
}
|
|
45
|
-
if (
|
|
96
|
+
if (checkRunning()) throw new Error(definition.runningError);
|
|
97
|
+
if (!definition.historySupported && history.length > 0) {
|
|
98
|
+
throw new Error(`${definition.storageName} does not expose a compatible history store. Turn off History URLs and try again.`);
|
|
99
|
+
}
|
|
100
|
+
if (!definition.siteStorageSupported && siteStorage) {
|
|
101
|
+
throw new Error(`${definition.storageName} full site-data import is not supported yet. Turn off Full site data and try again.`);
|
|
102
|
+
}
|
|
103
|
+
if (target === "cursor" && cookies.length === 0) {
|
|
104
|
+
throw new Error("No readable cookies were found to import into Cursor.");
|
|
105
|
+
}
|
|
46
106
|
|
|
47
|
-
const cookiePath =
|
|
107
|
+
const cookiePath = definition.cookiePaths(home).find((candidate) => fs.existsSync(candidate));
|
|
48
108
|
if (!cookiePath) {
|
|
49
|
-
throw new Error(
|
|
109
|
+
throw new Error(`${definition.storageName} storage was not found. Open the browser in ${definition.applicationName} once, quit ${definition.applicationName}, then try again.`);
|
|
50
110
|
}
|
|
51
111
|
const historyPath = path.join(path.dirname(cookiePath), "History");
|
|
52
|
-
const
|
|
53
|
-
const backupRoot = path.join(appSupportDir(home), "backups",
|
|
112
|
+
const targetProfilePath = directTargetProfileRoot(cookiePath);
|
|
113
|
+
const backupRoot = path.join(appSupportDir(home), "backups", definition.backupDirectory);
|
|
54
114
|
const backupPath = path.join(backupRoot, backupDirectoryName(now));
|
|
55
115
|
fs.mkdirSync(backupPath, { recursive: true, mode: 0o700 });
|
|
56
116
|
|
|
@@ -60,34 +120,49 @@ export function directImportToCodex({
|
|
|
60
120
|
} catch (error) {
|
|
61
121
|
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
62
122
|
if (/locked|busy/i.test(error.message)) {
|
|
63
|
-
throw new Error(
|
|
123
|
+
throw new Error(`${definition.applicationName} is still releasing its browser database. Wait a few seconds after quitting ${definition.applicationName}, then try again.`);
|
|
64
124
|
}
|
|
65
125
|
throw error;
|
|
66
126
|
}
|
|
67
127
|
const cookieWorking = temporarySibling(cookiePath);
|
|
68
|
-
|
|
69
|
-
|
|
128
|
+
try {
|
|
129
|
+
fs.copyFileSync(cookieBackup, cookieWorking);
|
|
130
|
+
fs.chmodSync(cookieWorking, 0o600);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
removeDatabaseArtifacts(cookieWorking);
|
|
133
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
70
136
|
|
|
71
137
|
let historyBackup = null;
|
|
72
138
|
let historyWorking = null;
|
|
73
139
|
if (history.length > 0) {
|
|
74
140
|
if (!fs.existsSync(historyPath)) {
|
|
75
|
-
|
|
76
|
-
|
|
141
|
+
removeDatabaseArtifacts(cookieWorking);
|
|
142
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
143
|
+
throw new Error(`${definition.storageName} history storage was not found. Open the browser in ${definition.applicationName} once, quit ${definition.applicationName}, then try again.`);
|
|
77
144
|
}
|
|
78
145
|
historyBackup = path.join(backupPath, "History");
|
|
79
146
|
try {
|
|
80
147
|
snapshotDatabase(historyPath, historyBackup);
|
|
81
148
|
} catch (error) {
|
|
82
|
-
|
|
149
|
+
removeDatabaseArtifacts(cookieWorking);
|
|
150
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
83
151
|
if (/locked|busy/i.test(error.message)) {
|
|
84
|
-
throw new Error(
|
|
152
|
+
throw new Error(`${definition.applicationName} is still releasing its history database. Wait a few seconds after quitting ${definition.applicationName}, then try again.`);
|
|
85
153
|
}
|
|
86
154
|
throw error;
|
|
87
155
|
}
|
|
88
156
|
historyWorking = temporarySibling(historyPath);
|
|
89
|
-
|
|
90
|
-
|
|
157
|
+
try {
|
|
158
|
+
fs.copyFileSync(historyBackup, historyWorking);
|
|
159
|
+
fs.chmodSync(historyWorking, 0o600);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
removeDatabaseArtifacts(cookieWorking);
|
|
162
|
+
removeDatabaseArtifacts(historyWorking);
|
|
163
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
91
166
|
}
|
|
92
167
|
|
|
93
168
|
let siteStoragePlan = [];
|
|
@@ -96,38 +171,65 @@ export function directImportToCodex({
|
|
|
96
171
|
if (!sourceProfilePath || !fs.existsSync(sourceProfilePath)) {
|
|
97
172
|
throw new Error("The selected source profile was not found for full site-data import.");
|
|
98
173
|
}
|
|
99
|
-
siteStoragePlan = prepareSiteStorageTransfer({ sourceProfilePath,
|
|
174
|
+
siteStoragePlan = prepareSiteStorageTransfer({ sourceProfilePath, targetProfilePath, backupPath });
|
|
100
175
|
} catch (error) {
|
|
101
|
-
|
|
102
|
-
if (historyWorking)
|
|
176
|
+
removeDatabaseArtifacts(cookieWorking);
|
|
177
|
+
if (historyWorking) removeDatabaseArtifacts(historyWorking);
|
|
178
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
103
179
|
throw error;
|
|
104
180
|
}
|
|
105
181
|
}
|
|
106
182
|
|
|
107
183
|
let cookieResult;
|
|
108
184
|
let historyResult = { imported: 0, skipped: 0, failed: 0 };
|
|
185
|
+
let cookieReplaced = false;
|
|
186
|
+
let historyReplaced = false;
|
|
109
187
|
try {
|
|
110
|
-
cookieResult = mergeCookies(cookieWorking, cookies, now);
|
|
111
|
-
if (historyWorking) historyResult = mergeHistory(historyWorking, history, now);
|
|
112
|
-
|
|
188
|
+
cookieResult = mergeCookies(cookieWorking, cookies, now, definition);
|
|
189
|
+
if (historyWorking) historyResult = mergeHistory(historyWorking, history, now, definition.name);
|
|
190
|
+
if (target === "cursor" && cookieResult.imported === 0) {
|
|
191
|
+
throw new Error("No valid cookies were available to import into Cursor.");
|
|
192
|
+
}
|
|
193
|
+
if (checkRunning()) throw new Error(definition.runningError);
|
|
194
|
+
cookieReplaced = true;
|
|
195
|
+
databaseReplacer(cookieWorking, cookiePath);
|
|
113
196
|
cookieResult.replaced = true;
|
|
114
197
|
if (historyWorking) {
|
|
115
|
-
|
|
198
|
+
historyReplaced = true;
|
|
199
|
+
databaseReplacer(historyWorking, historyPath);
|
|
116
200
|
historyResult.replaced = true;
|
|
117
201
|
}
|
|
118
202
|
commitSiteStorageTransfer(siteStoragePlan);
|
|
119
203
|
} catch (error) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
204
|
+
const siteStorageTouched = siteStoragePlan.some((item) => item.touched);
|
|
205
|
+
const restoreErrors = [];
|
|
206
|
+
if (cookieReplaced) {
|
|
207
|
+
try { databaseRestorer(cookieBackup, cookiePath); } catch (restoreError) { restoreErrors.push(`cookies: ${restoreError.message}`); }
|
|
208
|
+
}
|
|
209
|
+
if (historyReplaced) {
|
|
210
|
+
try { databaseRestorer(historyBackup, historyPath); } catch (restoreError) { restoreErrors.push(`history: ${restoreError.message}`); }
|
|
211
|
+
}
|
|
212
|
+
if (siteStorageTouched) {
|
|
213
|
+
try { restoreSiteStorageTransfer(siteStoragePlan); } catch (restoreError) { restoreErrors.push(`site storage: ${restoreError.message}`); }
|
|
214
|
+
}
|
|
215
|
+
if (restoreErrors.length > 0) {
|
|
216
|
+
pruneBackupsSafely(backupRoot, MAX_BACKUPS, backupPath);
|
|
217
|
+
throw new Error(`${definition.name} sync failed and recovery was incomplete (${restoreErrors.join("; ")}): ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
if (cookieReplaced || historyReplaced || siteStorageTouched) {
|
|
220
|
+
pruneBackupsSafely(backupRoot, MAX_BACKUPS, backupPath);
|
|
221
|
+
throw new Error(`${definition.name} data was restored from backup after the sync failed: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
224
|
+
pruneBackupsSafely(backupRoot, MAX_BACKUPS);
|
|
225
|
+
throw error;
|
|
124
226
|
} finally {
|
|
125
|
-
|
|
126
|
-
if (historyWorking)
|
|
227
|
+
removeDatabaseArtifacts(cookieWorking);
|
|
228
|
+
if (historyWorking) removeDatabaseArtifacts(historyWorking);
|
|
127
229
|
cleanupSiteStorageTransfer(siteStoragePlan);
|
|
128
230
|
}
|
|
129
231
|
|
|
130
|
-
|
|
232
|
+
const backupCleanupWarning = pruneBackupsSafely(backupRoot, MAX_BACKUPS, backupPath);
|
|
131
233
|
return {
|
|
132
234
|
imported: cookieResult.imported,
|
|
133
235
|
skipped: cookieResult.skipped,
|
|
@@ -137,26 +239,50 @@ export function directImportToCodex({
|
|
|
137
239
|
historyFailed: historyResult.failed,
|
|
138
240
|
backupPath,
|
|
139
241
|
targetPath: cookiePath,
|
|
140
|
-
|
|
242
|
+
targetID: target,
|
|
243
|
+
targetName: definition.name,
|
|
244
|
+
directEmbeddedBrowserImport: true,
|
|
245
|
+
directCodexImport: target === "codex",
|
|
141
246
|
siteStorageImported: siteStoragePlan.filter((item) => item.committed).length,
|
|
142
247
|
siteStorageNames: siteStoragePlan.filter((item) => item.committed).map((item) => item.name),
|
|
143
248
|
sourceCookieSkipped: Number.isInteger(sourceCookieSkipped) && sourceCookieSkipped > 0 ? sourceCookieSkipped : 0,
|
|
249
|
+
backupCleanupWarning,
|
|
144
250
|
};
|
|
145
251
|
}
|
|
146
252
|
|
|
147
|
-
function
|
|
253
|
+
export function directImportToCodex(options = {}) {
|
|
254
|
+
return directImportToEmbeddedBrowser({
|
|
255
|
+
...options,
|
|
256
|
+
target: "codex",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function directImportToCursor(options = {}) {
|
|
261
|
+
return directImportToEmbeddedBrowser({
|
|
262
|
+
...options,
|
|
263
|
+
target: "cursor",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function directTargetDefinition(target) {
|
|
268
|
+
const definition = DIRECT_TARGETS[target];
|
|
269
|
+
if (!definition) throw new Error(`Unsupported direct browser target: ${target}`);
|
|
270
|
+
return definition;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function directTargetProfileRoot(cookiePath) {
|
|
148
274
|
const parent = path.dirname(cookiePath);
|
|
149
275
|
return path.basename(parent) === "Network" ? path.dirname(parent) : parent;
|
|
150
276
|
}
|
|
151
277
|
|
|
152
|
-
function prepareSiteStorageTransfer({ sourceProfilePath,
|
|
278
|
+
function prepareSiteStorageTransfer({ sourceProfilePath, targetProfilePath, backupPath }) {
|
|
153
279
|
const plan = [];
|
|
154
280
|
try {
|
|
155
|
-
fs.mkdirSync(
|
|
281
|
+
fs.mkdirSync(targetProfilePath, { recursive: true, mode: 0o700 });
|
|
156
282
|
for (const name of SITE_STORAGE_DIRECTORIES) {
|
|
157
283
|
const source = path.join(sourceProfilePath, name);
|
|
158
284
|
if (!fs.existsSync(source)) continue;
|
|
159
|
-
const destination = path.join(
|
|
285
|
+
const destination = path.join(targetProfilePath, name);
|
|
160
286
|
const backup = path.join(backupPath, "Site Storage", name);
|
|
161
287
|
const stage = `${destination}.browser-cookie-bridge-${process.pid}-${Date.now()}.stage`;
|
|
162
288
|
const existed = fs.existsSync(destination);
|
|
@@ -210,11 +336,11 @@ function copyStorageTree(source, destination) {
|
|
|
210
336
|
});
|
|
211
337
|
}
|
|
212
338
|
|
|
213
|
-
function mergeCookies(databasePath, cookies, now) {
|
|
339
|
+
function mergeCookies(databasePath, cookies, now, definition) {
|
|
214
340
|
const database = new DatabaseSync(databasePath);
|
|
215
341
|
let imported = 0;
|
|
216
342
|
let skipped = 0;
|
|
217
|
-
|
|
343
|
+
const failed = 0;
|
|
218
344
|
try {
|
|
219
345
|
assertIntegrity(database);
|
|
220
346
|
assertTableColumns(database, "cookies", [
|
|
@@ -223,7 +349,8 @@ function mergeCookies(databasePath, cookies, now) {
|
|
|
223
349
|
"last_access_utc", "has_expires", "is_persistent", "priority", "samesite",
|
|
224
350
|
"source_scheme", "source_port", "last_update_utc", "source_type",
|
|
225
351
|
"has_cross_site_ancestor",
|
|
226
|
-
]);
|
|
352
|
+
], definition.name);
|
|
353
|
+
assertCookieSchema(database, definition);
|
|
227
354
|
const insert = database.prepare(`
|
|
228
355
|
INSERT OR REPLACE INTO cookies(
|
|
229
356
|
creation_utc, host_key, top_frame_site_key, name, value, encrypted_value,
|
|
@@ -235,36 +362,32 @@ function mergeCookies(databasePath, cookies, now) {
|
|
|
235
362
|
const currentTime = chromiumTime(now.getTime() / 1000);
|
|
236
363
|
database.exec("BEGIN IMMEDIATE");
|
|
237
364
|
for (const cookie of cookies) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
insert.run(
|
|
245
|
-
currentTime,
|
|
246
|
-
normalized.domain,
|
|
247
|
-
normalized.topFrameSiteKey,
|
|
248
|
-
normalized.name,
|
|
249
|
-
normalized.value,
|
|
250
|
-
Buffer.alloc(0),
|
|
251
|
-
normalized.path,
|
|
252
|
-
normalized.expiresUtc,
|
|
253
|
-
normalized.secure ? 1 : 0,
|
|
254
|
-
normalized.httpOnly ? 1 : 0,
|
|
255
|
-
currentTime,
|
|
256
|
-
normalized.persistent ? 1 : 0,
|
|
257
|
-
normalized.persistent ? 1 : 0,
|
|
258
|
-
normalized.sameSite,
|
|
259
|
-
normalized.secure ? 2 : 1,
|
|
260
|
-
normalized.secure ? 443 : 80,
|
|
261
|
-
currentTime,
|
|
262
|
-
normalized.hasCrossSiteAncestor ? 1 : 0,
|
|
263
|
-
);
|
|
264
|
-
imported += 1;
|
|
265
|
-
} catch {
|
|
266
|
-
failed += 1;
|
|
365
|
+
const normalized = normalizeCookie(cookie);
|
|
366
|
+
if (!normalized) {
|
|
367
|
+
skipped += 1;
|
|
368
|
+
continue;
|
|
267
369
|
}
|
|
370
|
+
insert.run(
|
|
371
|
+
currentTime,
|
|
372
|
+
normalized.domain,
|
|
373
|
+
normalized.topFrameSiteKey,
|
|
374
|
+
normalized.name,
|
|
375
|
+
normalized.value,
|
|
376
|
+
Buffer.alloc(0),
|
|
377
|
+
normalized.path,
|
|
378
|
+
normalized.expiresUtc,
|
|
379
|
+
normalized.secure ? 1 : 0,
|
|
380
|
+
normalized.httpOnly ? 1 : 0,
|
|
381
|
+
currentTime,
|
|
382
|
+
normalized.persistent ? 1 : 0,
|
|
383
|
+
normalized.persistent ? 1 : 0,
|
|
384
|
+
normalized.sameSite,
|
|
385
|
+
normalized.secure ? 2 : 1,
|
|
386
|
+
normalized.secure ? 443 : 80,
|
|
387
|
+
currentTime,
|
|
388
|
+
normalized.hasCrossSiteAncestor ? 1 : 0,
|
|
389
|
+
);
|
|
390
|
+
imported += 1;
|
|
268
391
|
}
|
|
269
392
|
database.exec("COMMIT");
|
|
270
393
|
assertIntegrity(database);
|
|
@@ -277,15 +400,15 @@ function mergeCookies(databasePath, cookies, now) {
|
|
|
277
400
|
return { imported, skipped, failed };
|
|
278
401
|
}
|
|
279
402
|
|
|
280
|
-
function mergeHistory(databasePath, history, now) {
|
|
403
|
+
function mergeHistory(databasePath, history, now, targetName) {
|
|
281
404
|
const database = new DatabaseSync(databasePath);
|
|
282
405
|
let imported = 0;
|
|
283
406
|
let skipped = 0;
|
|
284
|
-
|
|
407
|
+
const failed = 0;
|
|
285
408
|
try {
|
|
286
409
|
assertIntegrity(database);
|
|
287
|
-
assertTableColumns(database, "urls", ["id", "url", "visit_count", "last_visit_time"]);
|
|
288
|
-
assertTableColumns(database, "visits", ["url", "visit_time", "transition"]);
|
|
410
|
+
assertTableColumns(database, "urls", ["id", "url", "visit_count", "last_visit_time"], targetName);
|
|
411
|
+
assertTableColumns(database, "visits", ["url", "visit_time", "transition"], targetName);
|
|
289
412
|
const findURL = database.prepare("SELECT id FROM urls WHERE url = ? LIMIT 1");
|
|
290
413
|
const insertURL = database.prepare(
|
|
291
414
|
"INSERT INTO urls(url, title, visit_count, typed_count, last_visit_time, hidden) VALUES (?, '', 1, 0, ?, 0)",
|
|
@@ -299,24 +422,20 @@ function mergeHistory(databasePath, history, now) {
|
|
|
299
422
|
const currentTime = chromiumTime(now.getTime() / 1000);
|
|
300
423
|
database.exec("BEGIN IMMEDIATE");
|
|
301
424
|
for (const item of history) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
urlID = insertURL.run(item.url, currentTime).lastInsertRowid;
|
|
314
|
-
}
|
|
315
|
-
insertVisit.run(urlID, currentTime);
|
|
316
|
-
imported += 1;
|
|
317
|
-
} catch {
|
|
318
|
-
failed += 1;
|
|
425
|
+
if (!item || typeof item.url !== "string" || !/^https?:\/\//.test(item.url)) {
|
|
426
|
+
skipped += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const existing = findURL.get(item.url);
|
|
430
|
+
let urlID;
|
|
431
|
+
if (existing) {
|
|
432
|
+
urlID = existing.id;
|
|
433
|
+
updateURL.run(currentTime, urlID);
|
|
434
|
+
} else {
|
|
435
|
+
urlID = insertURL.run(item.url, currentTime).lastInsertRowid;
|
|
319
436
|
}
|
|
437
|
+
insertVisit.run(urlID, currentTime);
|
|
438
|
+
imported += 1;
|
|
320
439
|
}
|
|
321
440
|
database.exec("COMMIT");
|
|
322
441
|
assertIntegrity(database);
|
|
@@ -342,18 +461,26 @@ function snapshotDatabase(source, destination) {
|
|
|
342
461
|
}
|
|
343
462
|
|
|
344
463
|
function replaceDatabase(source, target) {
|
|
464
|
+
fs.chmodSync(source, 0o600);
|
|
345
465
|
fs.rmSync(`${target}-wal`, { force: true });
|
|
346
466
|
fs.rmSync(`${target}-shm`, { force: true });
|
|
467
|
+
fs.rmSync(`${target}-journal`, { force: true });
|
|
347
468
|
fs.renameSync(source, target);
|
|
348
|
-
fs.chmodSync(target, 0o600);
|
|
349
469
|
}
|
|
350
470
|
|
|
351
471
|
function restoreDatabase(backup, target) {
|
|
352
472
|
if (!fs.existsSync(backup)) return;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
473
|
+
const staged = temporarySibling(target);
|
|
474
|
+
try {
|
|
475
|
+
fs.copyFileSync(backup, staged);
|
|
476
|
+
fs.chmodSync(staged, 0o600);
|
|
477
|
+
fs.rmSync(`${target}-wal`, { force: true });
|
|
478
|
+
fs.rmSync(`${target}-shm`, { force: true });
|
|
479
|
+
fs.rmSync(`${target}-journal`, { force: true });
|
|
480
|
+
fs.renameSync(staged, target);
|
|
481
|
+
} finally {
|
|
482
|
+
fs.rmSync(staged, { force: true });
|
|
483
|
+
}
|
|
357
484
|
}
|
|
358
485
|
|
|
359
486
|
function assertIntegrity(database) {
|
|
@@ -361,10 +488,26 @@ function assertIntegrity(database) {
|
|
|
361
488
|
if (Object.values(row ?? {})[0] !== "ok") throw new Error("SQLite integrity check failed");
|
|
362
489
|
}
|
|
363
490
|
|
|
364
|
-
function assertTableColumns(database, table, required) {
|
|
491
|
+
function assertTableColumns(database, table, required, targetName) {
|
|
365
492
|
const columns = new Set(database.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
366
493
|
const missing = required.filter((name) => !columns.has(name));
|
|
367
|
-
if (missing.length > 0) throw new Error(`Unsupported
|
|
494
|
+
if (missing.length > 0) throw new Error(`Unsupported ${targetName} ${table} schema; missing ${missing.join(", ")}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function assertCookieSchema(database, definition) {
|
|
498
|
+
if (!definition.cookieSchemaVersion) return;
|
|
499
|
+
const meta = Object.fromEntries(database.prepare("SELECT key, value FROM meta WHERE key IN ('version', 'last_compatible_version')").all().map((row) => [row.key, row.value]));
|
|
500
|
+
if (String(meta.version) !== definition.cookieSchemaVersion || String(meta.last_compatible_version) !== definition.cookieSchemaVersion) {
|
|
501
|
+
throw new Error(`Unsupported ${definition.name} cookies schema version: ${meta.version ?? "missing"}`);
|
|
502
|
+
}
|
|
503
|
+
const expected = definition.cookieIndex;
|
|
504
|
+
const index = database.prepare("PRAGMA index_list(cookies)").all().find((row) => row.name === expected.name);
|
|
505
|
+
const columns = index
|
|
506
|
+
? database.prepare(`PRAGMA index_info(${expected.name})`).all().map((row) => row.name)
|
|
507
|
+
: [];
|
|
508
|
+
if (!index || Number(index.unique) !== 1 || Number(index.partial) !== 0 || columns.join("\n") !== expected.columns.join("\n")) {
|
|
509
|
+
throw new Error(`Unsupported ${definition.name} cookies uniqueness schema`);
|
|
510
|
+
}
|
|
368
511
|
}
|
|
369
512
|
|
|
370
513
|
function normalizeCookie(cookie) {
|
|
@@ -388,18 +531,36 @@ function normalizeCookie(cookie) {
|
|
|
388
531
|
};
|
|
389
532
|
}
|
|
390
533
|
|
|
391
|
-
function pruneBackups(root, keep) {
|
|
534
|
+
function pruneBackups(root, keep, protectedPath) {
|
|
392
535
|
if (!fs.existsSync(root)) return;
|
|
536
|
+
const protectedName = protectedPath ? path.basename(protectedPath) : null;
|
|
393
537
|
const directories = fs.readdirSync(root, { withFileTypes: true })
|
|
394
538
|
.filter((entry) => entry.isDirectory())
|
|
395
539
|
.map((entry) => entry.name)
|
|
396
540
|
.sort()
|
|
397
541
|
.reverse();
|
|
398
|
-
|
|
542
|
+
const removable = directories.filter((name) => name !== protectedName);
|
|
543
|
+
const retainedOthers = protectedName && directories.includes(protectedName) ? Math.max(0, keep - 1) : keep;
|
|
544
|
+
for (const name of removable.slice(retainedOthers)) {
|
|
399
545
|
fs.rmSync(path.join(root, name), { recursive: true, force: true });
|
|
400
546
|
}
|
|
401
547
|
}
|
|
402
548
|
|
|
549
|
+
function pruneBackupsSafely(root, keep, protectedPath) {
|
|
550
|
+
try {
|
|
551
|
+
pruneBackups(root, keep, protectedPath);
|
|
552
|
+
return null;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return `The transfer succeeded, but old backups could not be pruned: ${error.message}`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function removeDatabaseArtifacts(target) {
|
|
559
|
+
for (const suffix of ["", "-journal", "-wal", "-shm"]) {
|
|
560
|
+
fs.rmSync(`${target}${suffix}`, { force: true });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
403
564
|
function backupDirectoryName(now) {
|
|
404
565
|
const timestamp = now.toISOString().replace(/[:.]/g, "-");
|
|
405
566
|
return `${timestamp}-${process.pid}`;
|
|
@@ -417,4 +578,4 @@ function escapeSqlitePath(target) {
|
|
|
417
578
|
return target.replaceAll("'", "''");
|
|
418
579
|
}
|
|
419
580
|
|
|
420
|
-
export { CODEX_RUNNING_ERROR };
|
|
581
|
+
export { CODEX_RUNNING_ERROR, CURSOR_RUNNING_ERROR };
|
package/src/config.js
CHANGED
|
@@ -34,6 +34,12 @@ export function installConfig({ home, hour = 9, minute = 0, nodePath = process.e
|
|
|
34
34
|
|
|
35
35
|
const sourceBrowser = SOURCE_BROWSERS.includes(existing.sourceBrowser) ? existing.sourceBrowser : "brave";
|
|
36
36
|
const configuredTarget = TARGET_BROWSERS.includes(existing.targetBrowser) ? existing.targetBrowser : "codex";
|
|
37
|
+
const rememberedImports = {
|
|
38
|
+
history: existing.rememberedImports?.history === true
|
|
39
|
+
|| (configuredTarget !== "cursor" && existing.imports?.history === true),
|
|
40
|
+
siteStorage: existing.rememberedImports?.siteStorage === true
|
|
41
|
+
|| (configuredTarget !== "cursor" && existing.imports?.siteStorage === true),
|
|
42
|
+
};
|
|
37
43
|
const config = {
|
|
38
44
|
version: 2,
|
|
39
45
|
token: existing.token || crypto.randomBytes(32).toString("base64url"),
|
|
@@ -44,9 +50,10 @@ export function installConfig({ home, hour = 9, minute = 0, nodePath = process.e
|
|
|
44
50
|
imports: {
|
|
45
51
|
cookies: existing.imports?.cookies !== false,
|
|
46
52
|
passwords: false,
|
|
47
|
-
history: existing.imports?.history === true,
|
|
48
|
-
siteStorage: existing.imports?.siteStorage === true,
|
|
53
|
+
history: configuredTarget !== "cursor" && configuredTarget !== "grok-bot" && existing.imports?.history === true,
|
|
54
|
+
siteStorage: configuredTarget !== "cursor" && configuredTarget !== "grok-bot" && existing.imports?.siteStorage === true,
|
|
49
55
|
},
|
|
56
|
+
rememberedImports,
|
|
50
57
|
ui: {
|
|
51
58
|
menuBar: existing.ui?.menuBar !== false,
|
|
52
59
|
openAtLogin: existing.ui?.openAtLogin !== false,
|
|
@@ -59,6 +66,9 @@ export function installConfig({ home, hour = 9, minute = 0, nodePath = process.e
|
|
|
59
66
|
region: ["sfo", "lon", "ams"].includes(existing.browserless?.region) ? existing.browserless.region : "sfo",
|
|
60
67
|
onlyDomains: cleanDomains(existing.browserless?.onlyDomains),
|
|
61
68
|
},
|
|
69
|
+
grokBot: {
|
|
70
|
+
onlyDomains: cleanDomains(existing.grokBot?.onlyDomains),
|
|
71
|
+
},
|
|
62
72
|
schedule: { hour, minute },
|
|
63
73
|
createdAt: existing.createdAt || new Date().toISOString(),
|
|
64
74
|
updatedAt: new Date().toISOString(),
|
|
@@ -67,6 +77,7 @@ export function installConfig({ home, hour = 9, minute = 0, nodePath = process.e
|
|
|
67
77
|
writePrivateJson(target, config);
|
|
68
78
|
for (const browser of SOURCE_BROWSERS) installExtension(config, home, browser, "browser");
|
|
69
79
|
fs.rmSync(installedExtensionDir(home, "codex"), { recursive: true, force: true });
|
|
80
|
+
fs.rmSync(installedExtensionDir(home, "cursor"), { recursive: true, force: true });
|
|
70
81
|
fs.rmSync(installedExtensionDir(home, "atlas"), { recursive: true, force: true });
|
|
71
82
|
return config;
|
|
72
83
|
}
|
|
@@ -86,6 +97,7 @@ export function updatePreferences({
|
|
|
86
97
|
browserlessProfileName,
|
|
87
98
|
browserlessRegion,
|
|
88
99
|
browserlessOnlyDomains,
|
|
100
|
+
grokBotOnlyDomains,
|
|
89
101
|
}) {
|
|
90
102
|
const config = readConfig(home);
|
|
91
103
|
if (!SOURCE_BROWSERS.includes(sourceBrowser)) {
|
|
@@ -97,14 +109,29 @@ export function updatePreferences({
|
|
|
97
109
|
if (sourceBrowser === targetBrowser) {
|
|
98
110
|
throw new Error("Source and target browsers must be different");
|
|
99
111
|
}
|
|
112
|
+
const previousTarget = config.targetBrowser;
|
|
113
|
+
const rememberedImports = {
|
|
114
|
+
history: config.rememberedImports?.history === true,
|
|
115
|
+
siteStorage: config.rememberedImports?.siteStorage === true,
|
|
116
|
+
};
|
|
117
|
+
if (previousTarget !== "cursor" && targetBrowser === "cursor") {
|
|
118
|
+
rememberedImports.history = config.imports?.history === true;
|
|
119
|
+
rememberedImports.siteStorage = config.imports?.siteStorage === true;
|
|
120
|
+
}
|
|
121
|
+
const leavingCursor = previousTarget === "cursor" && targetBrowser !== "cursor";
|
|
122
|
+
const effectiveHistory = leavingCursor ? rememberedImports.history : Boolean(history);
|
|
123
|
+
const effectiveSiteStorage = leavingCursor ? rememberedImports.siteStorage : Boolean(siteStorage);
|
|
100
124
|
config.sourceBrowser = sourceBrowser;
|
|
101
125
|
config.targetBrowser = targetBrowser;
|
|
102
126
|
config.imports = {
|
|
103
127
|
cookies: Boolean(cookies),
|
|
104
128
|
passwords: false,
|
|
105
|
-
history:
|
|
106
|
-
siteStorage:
|
|
129
|
+
history: targetBrowser !== "cursor" && targetBrowser !== "grok-bot" && effectiveHistory,
|
|
130
|
+
siteStorage: targetBrowser !== "cursor" && targetBrowser !== "grok-bot" && effectiveSiteStorage,
|
|
107
131
|
};
|
|
132
|
+
config.rememberedImports = targetBrowser === "cursor"
|
|
133
|
+
? rememberedImports
|
|
134
|
+
: { history: effectiveHistory, siteStorage: effectiveSiteStorage };
|
|
108
135
|
config.ui = {
|
|
109
136
|
menuBar: Boolean(menuBar),
|
|
110
137
|
openAtLogin: Boolean(openAtLogin),
|
|
@@ -119,6 +146,9 @@ export function updatePreferences({
|
|
|
119
146
|
region,
|
|
120
147
|
onlyDomains: cleanDomains(browserlessOnlyDomains ?? config.browserless?.onlyDomains),
|
|
121
148
|
};
|
|
149
|
+
config.grokBot = {
|
|
150
|
+
onlyDomains: cleanDomains(grokBotOnlyDomains ?? config.grokBot?.onlyDomains),
|
|
151
|
+
};
|
|
122
152
|
config.updatedAt = new Date().toISOString();
|
|
123
153
|
writePrivateJson(configPath(home), config);
|
|
124
154
|
return config;
|
package/src/crc32.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const TABLE = (() => {
|
|
2
|
+
const values = new Uint32Array(256);
|
|
3
|
+
for (let index = 0; index < 256; index += 1) {
|
|
4
|
+
let value = index;
|
|
5
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
6
|
+
value = (value & 1) ? (0xEDB88320 ^ (value >>> 1)) : (value >>> 1);
|
|
7
|
+
}
|
|
8
|
+
values[index] = value >>> 0;
|
|
9
|
+
}
|
|
10
|
+
return values;
|
|
11
|
+
})();
|
|
12
|
+
|
|
13
|
+
export default function crc32(buffer) {
|
|
14
|
+
let crc = 0xFFFFFFFF;
|
|
15
|
+
for (const byte of buffer) {
|
|
16
|
+
crc = TABLE[(crc ^ byte) & 0xFF] ^ (crc >>> 8);
|
|
17
|
+
}
|
|
18
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
19
|
+
}
|