browser-cookie-bridge 1.4.7 → 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.
@@ -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
- const output = processList ?? spawnSync("/bin/ps", ["-axo", "command="], {
24
- encoding: "utf8",
25
- maxBuffer: 16 * 1024 * 1024,
26
- }).stdout;
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
- /\/ChatGPT\.app\/Contents\/MacOS\/ChatGPT(?:\s|$)/.test(command.trim()),
74
+ definition.processPattern.test(command.trim()),
29
75
  );
30
76
  }
31
77
 
32
- export function directImportToCodex({
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
- codexRunning = isCodexRunning(),
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 (codexRunning) throw new Error(CODEX_RUNNING_ERROR);
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 = codexCookiePaths(home).find((candidate) => fs.existsSync(candidate));
107
+ const cookiePath = definition.cookiePaths(home).find((candidate) => fs.existsSync(candidate));
48
108
  if (!cookiePath) {
49
- throw new Error("Codex browser storage was not found. Open the Codex browser once, quit Codex, then try again.");
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 codexProfilePath = codexProfileRoot(cookiePath);
53
- const backupRoot = path.join(appSupportDir(home), "backups", "codex");
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("Codex is still releasing its browser database. Wait a few seconds after quitting Codex, then try again.");
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
- fs.copyFileSync(cookieBackup, cookieWorking);
69
- fs.chmodSync(cookieWorking, 0o600);
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
- fs.rmSync(cookieWorking, { force: true });
76
- throw new Error("Codex history storage was not found. Open the Codex browser once, quit Codex, then try again.");
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
- fs.rmSync(cookieWorking, { force: true });
149
+ removeDatabaseArtifacts(cookieWorking);
150
+ fs.rmSync(backupPath, { recursive: true, force: true });
83
151
  if (/locked|busy/i.test(error.message)) {
84
- throw new Error("Codex is still releasing its history database. Wait a few seconds after quitting Codex, then try again.");
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
- fs.copyFileSync(historyBackup, historyWorking);
90
- fs.chmodSync(historyWorking, 0o600);
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, codexProfilePath, backupPath });
174
+ siteStoragePlan = prepareSiteStorageTransfer({ sourceProfilePath, targetProfilePath, backupPath });
100
175
  } catch (error) {
101
- fs.rmSync(cookieWorking, { force: true });
102
- if (historyWorking) fs.rmSync(historyWorking, { force: true });
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
- replaceDatabase(cookieWorking, cookiePath);
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
- replaceDatabase(historyWorking, historyPath);
198
+ historyReplaced = true;
199
+ databaseReplacer(historyWorking, historyPath);
116
200
  historyResult.replaced = true;
117
201
  }
118
202
  commitSiteStorageTransfer(siteStoragePlan);
119
203
  } catch (error) {
120
- restoreDatabase(cookieBackup, cookiePath);
121
- if (historyBackup) restoreDatabase(historyBackup, historyPath);
122
- restoreSiteStorageTransfer(siteStoragePlan);
123
- throw new Error(`Codex data was restored from backup after the sync failed: ${error.message}`);
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
- fs.rmSync(cookieWorking, { force: true });
126
- if (historyWorking) fs.rmSync(historyWorking, { force: true });
227
+ removeDatabaseArtifacts(cookieWorking);
228
+ if (historyWorking) removeDatabaseArtifacts(historyWorking);
127
229
  cleanupSiteStorageTransfer(siteStoragePlan);
128
230
  }
129
231
 
130
- pruneBackups(backupRoot, MAX_BACKUPS);
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
- directCodexImport: true,
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 codexProfileRoot(cookiePath) {
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, codexProfilePath, backupPath }) {
278
+ function prepareSiteStorageTransfer({ sourceProfilePath, targetProfilePath, backupPath }) {
153
279
  const plan = [];
154
280
  try {
155
- fs.mkdirSync(codexProfilePath, { recursive: true, mode: 0o700 });
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(codexProfilePath, name);
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
- let failed = 0;
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
- try {
239
- const normalized = normalizeCookie(cookie);
240
- if (!normalized) {
241
- skipped += 1;
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
- let failed = 0;
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
- try {
303
- if (!item || typeof item.url !== "string" || !/^https?:\/\//.test(item.url)) {
304
- skipped += 1;
305
- continue;
306
- }
307
- const existing = findURL.get(item.url);
308
- let urlID;
309
- if (existing) {
310
- urlID = existing.id;
311
- updateURL.run(currentTime, urlID);
312
- } else {
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
- fs.rmSync(`${target}-wal`, { force: true });
354
- fs.rmSync(`${target}-shm`, { force: true });
355
- fs.copyFileSync(backup, target);
356
- fs.chmodSync(target, 0o600);
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 Codex ${table} schema; missing ${missing.join(", ")}`);
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
- for (const name of directories.slice(keep)) {
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: Boolean(history),
106
- siteStorage: Boolean(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
+ }