ocx-cursor 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  Use your active [OpenCodex](https://github.com/lidge-jun/opencodex) models in Cursor through an authenticated HTTPS endpoint on your Mac.
7
7
 
8
8
  > [!WARNING]
9
- > The installer edits two private workbench files inside `Cursor.app`. This invalidates Cursor's vendor signature. Cursor may report a corrupt installation. The bridge saves the original files under `~/.opencodex/cursor-bridge/cursor-app-backups`. Reinstall Cursor to restore a signed app.
9
+ > The installer edits two private workbench files inside `Cursor.app`. This invalidates Cursor's vendor signature, so the bridge applies a local ad-hoc seal and removes the app's quarantine attribute after each patch to prevent macOS from reporting a corrupt installation. The bridge saves the original files under `~/.opencodex/cursor-bridge/cursor-app-backups`. Reinstall Cursor to restore a vendor-signed app.
10
10
 
11
11
  ## Requirements
12
12
 
@@ -125,14 +125,18 @@ The patch keeps the model metadata that Cursor would discard. It leaves Cursor's
125
125
 
126
126
  The login service refreshes the catalog every 15 seconds. It waits for Cursor to quit before writing the state database or reapplying a patch after a Cursor update.
127
127
 
128
+ On macOS, the patch monitor pauses while Cursor's `ShipIt` updater is running. After Cursor and the updater stop, it waits until all patch targets remain unchanged for five seconds before writing them. This prevents the bridge from modifying `Cursor.app` while an in-app update is replacing it.
129
+
128
130
  ## Commands
129
131
 
130
132
  | Command | Action |
131
133
  | --- | --- |
132
134
  | `ocx-cursor init --base-url URL` | Install the service, test the HTTPS endpoint, configure Cursor, and sync models. |
133
135
  | `ocx-cursor install` | Reinstall the service and check the two metadata patches. |
136
+ | `ocx-cursor stop` | Stop the service without removing its configuration or state. |
137
+ | `ocx-cursor start` | Start the installed service. |
134
138
  | `ocx-cursor update` | Install the current npm release and restart the service. |
135
- | `ocx-cursor sync` | Read the active OpenCodex catalog and update Cursor. |
139
+ | `ocx-cursor sync` | With Cursor closed, pause the service, verify the app patch, sync the active catalog, and restart the service. |
136
140
  | `ocx-cursor status` | Print service, gateway, catalog, and pending-sync status. |
137
141
  | `ocx-cursor uninstall` | Remove the service, command link, and bridge state. |
138
142
 
@@ -159,7 +163,7 @@ The bridge reads `~/.opencodex/service-api-token` when OpenCodex uses service au
159
163
  | Cursor returns 401 | Quit Cursor and rerun `npx ocx-cursor init --base-url https://YOUR_HOST/v1`. |
160
164
  | The hostname returns 502 | Run `ocx-cursor status`, then check that the tunnel points to port `10101`. |
161
165
  | Cloudflare returns 1016 | Run `cloudflared tunnel info ocx-cursor` and check the service. |
162
- | Models or controls are missing | Quit Cursor and run `ocx-cursor sync`. |
166
+ | Models or controls are missing | Quit Cursor and run `ocx-cursor sync`. It repairs the foreground app patch before writing model metadata. |
163
167
  | A Cursor update removed the patch | Quit Cursor and run `ocx-cursor install`. |
164
168
 
165
169
  Read the service logs:
@@ -5,7 +5,7 @@ import process from "node:process";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { configureCursorOpenAI, storedCursorOpenAIBaseUrl } from "../src/cursor-config.mjs";
7
7
  import { cursorIsRunning } from "../src/cursor-state.mjs";
8
- import { installService, prepareInstallSecret, serviceStatus, uninstallService, updateService } from "../src/install.mjs";
8
+ import { installService, prepareInstallSecret, repairCursorMetadataPatch, serviceStatus, startService, stopService, uninstallService, updateService } from "../src/install.mjs";
9
9
  import { cursorOpenAIBaseUrl, pendingFile } from "../src/paths.mjs";
10
10
  import { runService } from "../src/service.mjs";
11
11
  import { normalizeBaseUrl, testEndpoint } from "../src/setup.mjs";
@@ -19,6 +19,8 @@ Usage:
19
19
  Install the service, test the endpoint, configure Cursor,
20
20
  and sync models
21
21
  ocx-cursor install Install and start the macOS companion service
22
+ ocx-cursor stop Stop the companion service without removing its state
23
+ ocx-cursor start Start the installed companion service
22
24
  ocx-cursor update Install the latest companion release and restart it
23
25
  ocx-cursor sync Sync active OpenCodex models into Cursor
24
26
  ocx-cursor launch [--port <port>]
@@ -71,6 +73,10 @@ function printSync(result) {
71
73
  }
72
74
 
73
75
  function printCursorPatches(result) {
76
+ if (!result.cursorInstalled) {
77
+ process.stdout.write("Cursor is not installed; app patches will be applied after it is installed.\n");
78
+ return;
79
+ }
74
80
  if (result.cursorPatches === null) {
75
81
  process.stdout.write("Cursor app patches will be applied after Cursor quits.\n");
76
82
  return;
@@ -131,7 +137,29 @@ async function main() {
131
137
  await updateService();
132
138
  return;
133
139
  }
140
+ if (command === "stop") {
141
+ stopService();
142
+ process.stdout.write("Stopped OpenCodex Cursor Bridge.\n");
143
+ return;
144
+ }
145
+ if (command === "start") {
146
+ await startService();
147
+ process.stdout.write("Started OpenCodex Cursor Bridge.\n");
148
+ return;
149
+ }
134
150
  if (command === "sync") {
151
+ if (!cursorIsRunning()) {
152
+ stopService();
153
+ try {
154
+ const cursorPatches = await repairCursorMetadataPatch();
155
+ const changed = cursorPatches.filter(({ status }) => status === "patched").length;
156
+ process.stdout.write(`Verified ${cursorPatches.length} Cursor metadata patches (${changed} changed).\n`);
157
+ printSync(await syncNow());
158
+ } finally {
159
+ await startService();
160
+ }
161
+ return;
162
+ }
135
163
  printSync(await syncNow());
136
164
  return;
137
165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocx-cursor",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Use OpenCodex models in Cursor through an authenticated HTTPS gateway",
5
5
  "keywords": [
6
6
  "opencodex",
@@ -1,7 +1,9 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import { chmod, copyFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
4
  import { basename, dirname, join } from "node:path";
4
5
  import {
6
+ cursorAppPath,
5
7
  cursorBundleFiles,
6
8
  cursorGlassWorkbenchFile,
7
9
  cursorLocalRuntimeFiles,
@@ -36,6 +38,21 @@ const localRuntimeBuilderExport = /buildBottlerocketPickerModels:\(\)=>(?<builde
36
38
  const transientPatchErrorCodes = new Set(["EACCES", "EBUSY", "EPERM"]);
37
39
  const directoryPatchQueues = new Map();
38
40
 
41
+ export function clearCursorAppQuarantine(options = {}) {
42
+ const execute = options.execFileSync || execFileSync;
43
+ execute("/usr/bin/xattr", ["-dr", "com.apple.quarantine", options.appPath || cursorAppPath], {
44
+ stdio: "ignore",
45
+ });
46
+ }
47
+
48
+ export function finalizeCursorAppPatch(options = {}) {
49
+ const execute = options.execFileSync || execFileSync;
50
+ const appPath = options.appPath || cursorAppPath;
51
+ execute("/usr/bin/codesign", ["--force", "--sign", "-", appPath], { stdio: "ignore" });
52
+ execute("/usr/bin/codesign", ["--verify", "--deep", "--strict", appPath], { stdio: "ignore" });
53
+ clearCursorAppQuarantine({ ...options, appPath, execFileSync: execute });
54
+ }
55
+
39
56
  function replaceSingleMatch(source, pattern, replacement, label) {
40
57
  const matches = [...source.matchAll(pattern)];
41
58
  if (matches.length !== 1) throw new Error(`Expected one Cursor ${label}, found ${matches.length}`);
@@ -354,33 +371,86 @@ export function startCursorModelMetadataPatchMonitor(options = {}) {
354
371
 
355
372
  export function startCursorPatchMonitor(options = {}) {
356
373
  const intervalMs = options.intervalMs || 250;
374
+ const stableMs = options.stableMs || 0;
357
375
  const bundleFiles = options.file ? [options.file] : (options.bundleFiles || options.files || cursorBundleFiles);
358
376
  const runtimeFiles = options.file ? [] : (options.localRuntimeFiles || cursorLocalRuntimeFiles);
359
377
  const files = [...bundleFiles, ...runtimeFiles];
360
378
  const localRuntimeFiles = new Set(runtimeFiles);
361
379
  const lastSignatures = new Map();
380
+ let stableFingerprint = null;
381
+ let stableSince = 0;
362
382
  let patchPromise = null;
363
383
  let stopped = false;
364
384
 
365
- const check = async () => {
366
- if (stopped || patchPromise || options.shouldPatch?.() === false) return patchPromise;
367
- patchPromise = Promise.all(files.map(async (file) => {
368
- let signature;
385
+ const resetStability = () => {
386
+ stableFingerprint = null;
387
+ stableSince = 0;
388
+ };
389
+
390
+ const filesAreStable = async () => {
391
+ if (stableMs === 0) return true;
392
+ const signatures = await Promise.all(files.map(async (file) => {
369
393
  try {
370
- signature = await cursorWorkbenchSignature(file);
394
+ return await cursorWorkbenchSignature(file);
371
395
  } catch {
372
- return;
396
+ return null;
373
397
  }
374
- if (signature === lastSignatures.get(file)) return;
398
+ }));
399
+ if (signatures.some((signature) => signature === null)) {
400
+ resetStability();
401
+ return false;
402
+ }
403
+ const fingerprint = signatures.join("|");
404
+ const now = (options.now || Date.now)();
405
+ if (fingerprint !== stableFingerprint) {
406
+ stableFingerprint = fingerprint;
407
+ stableSince = now;
408
+ return false;
409
+ }
410
+ return now - stableSince >= stableMs;
411
+ };
412
+
413
+ const check = async () => {
414
+ if (stopped || patchPromise) return patchPromise;
415
+ if (options.shouldPatch?.() === false) {
416
+ resetStability();
417
+ lastSignatures.clear();
418
+ return null;
419
+ }
420
+ patchPromise = (async () => {
421
+ if (!await filesAreStable()) return [];
422
+ let retryAfterStabilityDelay = false;
423
+ const results = await Promise.all(files.map(async (file) => {
424
+ let signature;
425
+ try {
426
+ signature = await cursorWorkbenchSignature(file);
427
+ } catch {
428
+ return;
429
+ }
430
+ if (signature === lastSignatures.get(file)) return;
431
+ try {
432
+ const result = await ensureCursorPatchFile(file, localRuntimeFiles, options);
433
+ lastSignatures.set(file, result.signature);
434
+ options.onResult?.(result);
435
+ return result;
436
+ } catch (error) {
437
+ if (transientPatchErrorCodes.has(error.code)) retryAfterStabilityDelay = true;
438
+ else lastSignatures.set(file, signature);
439
+ options.onError?.(error, file);
440
+ }
441
+ }));
442
+ if (retryAfterStabilityDelay) resetStability();
443
+ const patched = results.filter((result) => result?.status === "patched");
375
444
  try {
376
- const result = await ensureCursorPatchFile(file, localRuntimeFiles, options);
377
- lastSignatures.set(file, result.signature);
378
- options.onResult?.(result);
445
+ if (patched.length > 0) await options.onPatched?.(patched);
446
+ if (results.length === files.length && results.every(Boolean)) await options.onReady?.(results);
379
447
  } catch (error) {
380
- if (!transientPatchErrorCodes.has(error.code)) lastSignatures.set(file, signature);
381
- options.onError?.(error, file);
448
+ resetStability();
449
+ lastSignatures.clear();
450
+ options.onError?.(error, options.appPath || cursorAppPath);
382
451
  }
383
- })).finally(() => {
452
+ return results;
453
+ })().finally(() => {
384
454
  patchPromise = null;
385
455
  });
386
456
  return patchPromise;
@@ -64,6 +64,17 @@ export function cursorIsRunning() {
64
64
  }
65
65
  }
66
66
 
67
+ export function cursorUpdateIsRunning(execute = execFileSync) {
68
+ try {
69
+ execute("pgrep", ["-f", "(com\\.todesktop\\.230313mzl4w4u92|co\\.anysphere\\.cursor[^ ]*)\\.ShipIt"], {
70
+ stdio: "ignore",
71
+ });
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
67
78
  export function defaultEffort(model) {
68
79
  const modelId = model.sourceId.split("/").at(-1);
69
80
  if (/^(?:gpt|gemini)-/.test(modelId) && model.reasoningEfforts.includes("medium")) return "medium";
package/src/install.mjs CHANGED
@@ -4,10 +4,11 @@ import { access, chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readlink,
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { ensureCursorModelMetadataPatched } from "./cursor-patch.mjs";
7
+ import { ensureCursorModelMetadataPatched, finalizeCursorAppPatch } from "./cursor-patch.mjs";
8
8
  import { cursorIsRunning } from "./cursor-state.mjs";
9
9
  import {
10
10
  cliLinkFile,
11
+ cursorAppPath,
11
12
  installRoot,
12
13
  installedPackageRoot,
13
14
  launchAgentFile,
@@ -39,16 +40,16 @@ function commandOutput(command, args) {
39
40
  }
40
41
  }
41
42
 
42
- function bootout(label) {
43
+ function bootout(label, execute = execFileSync) {
43
44
  try {
44
- execFileSync("launchctl", ["bootout", `${launchDomain}/${label}`], { stdio: "ignore" });
45
+ execute("launchctl", ["bootout", `${launchDomain}/${label}`], { stdio: "ignore" });
45
46
  } catch {}
46
47
  }
47
48
 
48
- async function bootstrap(plist) {
49
+ async function bootstrap(plist, execute = execFileSync) {
49
50
  for (let attempt = 0; attempt < 5; attempt += 1) {
50
51
  try {
51
- execFileSync("launchctl", ["bootstrap", launchDomain, plist], { stdio: "ignore" });
52
+ execute("launchctl", ["bootstrap", launchDomain, plist], { stdio: "ignore" });
52
53
  return;
53
54
  } catch (error) {
54
55
  if (attempt === 4) throw error;
@@ -168,27 +169,50 @@ function launchAgent(nodePath) {
168
169
  export async function installService() {
169
170
  await mkdir(dirname(launchAgentFile), { recursive: true });
170
171
  const { legacyPlist, secretStatus } = await prepareInstallSecret();
171
- const cursorPatches = cursorIsRunning()
172
- ? null
173
- : await Promise.all(cursorModelMetadataFiles.map(async (file) => ({
174
- file,
175
- ...await ensureCursorModelMetadataPatched({ file }),
176
- })));
177
- await copyPackage();
178
- await installCliLink();
179
-
180
- const nodePath = (await exists("/opt/homebrew/bin/node")) ? "/opt/homebrew/bin/node" : process.execPath;
181
- await writeFile(launchAgentFile, launchAgent(nodePath), { mode: 0o644 });
182
172
  bootout(serviceLabel);
183
173
  bootout(legacyServiceLabel);
184
- await bootstrap(launchAgentFile);
174
+ let cursorInstalled;
175
+ let cursorPatches;
176
+ try {
177
+ cursorInstalled = await exists(cursorAppPath);
178
+ cursorPatches = cursorIsRunning() || !cursorInstalled
179
+ ? null
180
+ : await repairCursorMetadataPatch();
181
+ await copyPackage();
182
+ await installCliLink();
183
+
184
+ const nodePath = (await exists("/opt/homebrew/bin/node")) ? "/opt/homebrew/bin/node" : process.execPath;
185
+ await writeFile(launchAgentFile, launchAgent(nodePath), { mode: 0o644 });
186
+ await bootstrap(launchAgentFile);
187
+ } catch (error) {
188
+ if (await exists(launchAgentFile)) await bootstrap(launchAgentFile).catch(() => {});
189
+ throw error;
190
+ }
185
191
 
186
192
  if (legacyPlist.startsWith(dirname(legacyLaunchAgentFile)) && await exists(legacyPlist)) {
187
193
  const disabled = `${legacyPlist}.disabled-by-${serviceLabel}`;
188
194
  await rm(disabled, { force: true });
189
195
  await rename(legacyPlist, disabled);
190
196
  }
191
- return { installRoot, launchAgentFile, cliLinkFile, secretStatus, cursorPatches };
197
+ return { installRoot, launchAgentFile, cliLinkFile, secretStatus, cursorInstalled, cursorPatches };
198
+ }
199
+
200
+ export async function repairCursorMetadataPatch(options = {}) {
201
+ const running = options.cursorRunning ?? cursorIsRunning();
202
+ if (running) throw new Error("Quit Cursor before repairing its metadata patch");
203
+ const appPath = options.cursorAppPath || cursorAppPath;
204
+ const pathExists = options.exists || exists;
205
+ if (!await pathExists(appPath)) throw new Error("Cursor is not installed in /Applications");
206
+
207
+ const patchFile = options.ensureCursorModelMetadataPatched || ensureCursorModelMetadataPatched;
208
+ const files = options.files || cursorModelMetadataFiles;
209
+ const patches = await Promise.all(files.map(async (file) => ({
210
+ file,
211
+ ...await patchFile({ file }),
212
+ })));
213
+ const finalizePatch = options.finalizeCursorAppPatch || finalizeCursorAppPatch;
214
+ finalizePatch({ appPath });
215
+ return patches;
192
216
  }
193
217
 
194
218
  export async function updateService(options = {}) {
@@ -209,6 +233,21 @@ export async function updateService(options = {}) {
209
233
  }
210
234
  }
211
235
 
236
+ export function stopService(options = {}) {
237
+ bootout(serviceLabel, options.execFileSync || execFileSync);
238
+ }
239
+
240
+ export async function startService(options = {}) {
241
+ const plist = options.launchAgentFile || launchAgentFile;
242
+ const pathExists = options.exists || exists;
243
+ if (!await pathExists(plist)) {
244
+ throw new Error("OpenCodex Cursor Bridge is not installed; run ocx-cursor install first");
245
+ }
246
+ const execute = options.execFileSync || execFileSync;
247
+ bootout(serviceLabel, execute);
248
+ await bootstrap(plist, execute);
249
+ }
250
+
212
251
  export async function uninstallService() {
213
252
  bootout(serviceLabel);
214
253
  await rm(launchAgentFile, { force: true });
package/src/paths.mjs CHANGED
@@ -14,7 +14,8 @@ export const stderrFile = join(installRoot, "service.error.log");
14
14
  export const launchAgentFile = join(homedir(), "Library", "LaunchAgents", `${serviceLabel}.plist`);
15
15
  export const legacyLaunchAgentFile = join(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel}.plist`);
16
16
  export const cursorDatabaseFile = join(homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
17
- export const cursorAppOutDirectory = "/Applications/Cursor.app/Contents/Resources/app/out";
17
+ export const cursorAppPath = "/Applications/Cursor.app";
18
+ export const cursorAppOutDirectory = join(cursorAppPath, "Contents", "Resources", "app", "out");
18
19
  export const cursorWorkbenchFile = join(cursorAppOutDirectory, "vs", "workbench", "workbench.desktop.main.js");
19
20
  export const cursorGlassWorkbenchFile = join(cursorAppOutDirectory, "vs", "workbench", "workbench.glass.main.js");
20
21
  export const cursorModelMetadataFiles = [cursorWorkbenchFile, cursorGlassWorkbenchFile];
package/src/service.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { buildActiveCatalog } from "./catalog.mjs";
4
- import { startCursorModelMetadataPatchMonitor } from "./cursor-patch.mjs";
4
+ import { finalizeCursorAppPatch, startCursorModelMetadataPatchMonitor } from "./cursor-patch.mjs";
5
5
  import {
6
6
  cursorIsRunning,
7
+ cursorUpdateIsRunning,
7
8
  readPendingCatalog,
8
9
  } from "./cursor-state.mjs";
9
10
  import { startGateway } from "./gateway.mjs";
@@ -27,6 +28,7 @@ export async function runService(options = {}) {
27
28
  let refreshPromise = null;
28
29
  let cursorSyncPromise = null;
29
30
  let wasCursorRunning = cursorIsRunning();
31
+ let cursorPatchReady = false;
30
32
  let stopped = false;
31
33
 
32
34
  const refresh = async () => {
@@ -37,7 +39,10 @@ export async function runService(options = {}) {
37
39
  if (JSON.stringify(next) === JSON.stringify(catalog)) return;
38
40
  catalog = next;
39
41
  await saveCatalogSnapshot(catalog);
40
- const result = await applyOrQueueCatalog(catalog, { createBackup: false });
42
+ const result = await applyOrQueueCatalog(catalog, {
43
+ createBackup: false,
44
+ defer: !cursorPatchReady,
45
+ });
41
46
  process.stdout.write(`${result.status === "queued" ? "Queued" : "Synced"} ${result.modelCount} Cursor models\n`);
42
47
  } catch (error) {
43
48
  process.stderr.write(`Catalog refresh failed: ${error.message}\n`);
@@ -61,19 +66,33 @@ export async function runService(options = {}) {
61
66
  const server = startGateway({ secret, getCatalog: () => catalog, host: options.host, port: options.port });
62
67
  const patchMonitor = startCursorModelMetadataPatchMonitor({
63
68
  intervalMs: options.cursorPatchIntervalMs,
64
- shouldPatch: () => !cursorIsRunning(),
69
+ stableMs: options.cursorPatchStableMs ?? 5_000,
70
+ shouldPatch: () => !cursorIsRunning() && !cursorUpdateIsRunning(),
65
71
  onResult: (result) => {
66
72
  if (result.status === "patched") process.stdout.write(`Patched Cursor app file ${result.file} (backup: ${result.backupPath})\n`);
67
73
  },
68
- onError: (error, file) => process.stderr.write(`Cursor patch failed for ${file}: ${error.message}\n`),
74
+ onPatched: () => {
75
+ finalizeCursorAppPatch();
76
+ },
77
+ onReady: (results) => {
78
+ if (results.every(({ status }) => status === "already-patched")) finalizeCursorAppPatch();
79
+ cursorPatchReady = true;
80
+ wasCursorRunning = true;
81
+ },
82
+ onError: (error, file) => {
83
+ cursorPatchReady = false;
84
+ process.stderr.write(`Cursor patch failed for ${file}: ${error.message}\n`);
85
+ },
69
86
  });
70
87
  const refreshTimer = setInterval(refresh, options.refreshIntervalMs || 15_000);
71
88
  const pendingTimer = setInterval(() => {
72
89
  if (cursorSyncPromise) return;
73
90
  if (cursorIsRunning()) {
74
91
  wasCursorRunning = true;
92
+ cursorPatchReady = false;
75
93
  return;
76
94
  }
95
+ if (!cursorPatchReady) return;
77
96
  cursorSyncPromise = (async () => {
78
97
  const pending = await readPendingCatalog();
79
98
  const next = pending || (wasCursorRunning ? catalog : null);
package/src/sync.mjs CHANGED
@@ -25,16 +25,20 @@ export async function saveCatalogSnapshot(catalog) {
25
25
  }
26
26
 
27
27
  export async function applyOrQueueCatalog(catalog, options = {}) {
28
- if (cursorIsRunning()) {
29
- await writePendingCatalog(catalog);
28
+ const running = options.cursorRunning ?? cursorIsRunning();
29
+ const queueCatalog = options.writePendingCatalog || writePendingCatalog;
30
+ if (options.defer || running) {
31
+ await queueCatalog(catalog);
30
32
  return {
31
33
  status: "queued",
32
34
  modelCount: catalog.length,
33
35
  effortCount: catalog.filter(({ reasoningEfforts }) => reasoningEfforts.length > 0).length,
34
36
  };
35
37
  }
36
- const result = await syncCursorDatabase(catalog, options);
37
- await clearPendingCatalog();
38
+ const syncDatabase = options.syncCursorDatabase || syncCursorDatabase;
39
+ const clearPending = options.clearPendingCatalog || clearPendingCatalog;
40
+ const result = await syncDatabase(catalog, options);
41
+ await clearPending();
38
42
  return { status: "synced", ...result };
39
43
  }
40
44