@closedrouter/cli 1.1.9 → 1.1.11

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/dist/index.js CHANGED
@@ -5204,7 +5204,7 @@ var {
5204
5204
  } = exports_commander;
5205
5205
 
5206
5206
  // src/index.ts
5207
- import { readFileSync as readFileSync13 } from "node:fs";
5207
+ import { readFileSync as readFileSync15 } from "node:fs";
5208
5208
 
5209
5209
  // ../node_modules/.bun/@clack+prompts@0.11.0/node_modules/@clack/prompts/dist/index.mjs
5210
5210
  import { stripVTControlCharacters as S2 } from "node:util";
@@ -8658,10 +8658,10 @@ to scan and import local subscriptions!`, "Pool Empty");
8658
8658
  var import_picocolors12 = __toESM(require_picocolors(), 1);
8659
8659
  import { randomUUID as randomUUID5 } from "node:crypto";
8660
8660
  import { spawn as spawn5 } from "node:child_process";
8661
- import { execFileSync as execFileSync2 } from "node:child_process";
8662
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "node:fs";
8663
- import { homedir as homedir13 } from "node:os";
8664
- import { dirname as dirname3, join as join14 } from "node:path";
8661
+ import { execFileSync as execFileSync3 } from "node:child_process";
8662
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "node:fs";
8663
+ import { homedir as homedir14 } from "node:os";
8664
+ import { dirname as dirname5, join as join16 } from "node:path";
8665
8665
 
8666
8666
  // src/model-picker.ts
8667
8667
  import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
@@ -9418,15 +9418,121 @@ async function settingsCommand(options = {}) {
9418
9418
  import { randomUUID } from "node:crypto";
9419
9419
  import { createServer as createServer2, request as httpRequest } from "node:http";
9420
9420
  import { request as httpsRequest } from "node:https";
9421
+
9422
+ // src/commands/launch-catalog.ts
9423
+ import { createHash as createHash2 } from "node:crypto";
9424
+ var modelLabel = (id) => `ClosedRouter: ${id}`;
9425
+ function transportModelId(id, family) {
9426
+ const hash = createHash2("sha256").update(id).digest("hex").slice(0, 20);
9427
+ return `${family === "claude" ? "claude-sonnet-4-6" : "gemini"}-closedrouter-${hash}`;
9428
+ }
9429
+ function createLaunchCatalog(models, selected) {
9430
+ const ids = [...new Set([selected, ...models])];
9431
+ return {
9432
+ defaultModel: selected,
9433
+ models: ids,
9434
+ aliases: Object.fromEntries(ids.flatMap((id) => [
9435
+ [transportModelId(id, "claude"), id],
9436
+ [transportModelId(id, "gemini"), id]
9437
+ ])),
9438
+ fallbacks: ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "gemini-3.8-flash", "gemini-3.8-flash-medium", "gemini-3.8-flash-high", "gemini-2.5-flash", "gemini-2.5-flash-lite"]
9439
+ };
9440
+ }
9441
+ function resolveLaunchModel(catalog, requested) {
9442
+ let id = requested.replace(/^models\//, "").replace(/\[1m\]$/, "");
9443
+ const fireworks = /^accounts\/fireworks\/models\/(.+)#anysphere\/[\w-]+$/.exec(id);
9444
+ if (fireworks)
9445
+ id = fireworks[1];
9446
+ if (catalog.models.includes(id))
9447
+ return id;
9448
+ if (id.startsWith("closedrouter/") && catalog.models.includes(id.slice(13)))
9449
+ return id.slice(13);
9450
+ if (Object.hasOwn(catalog.aliases, id))
9451
+ return catalog.aliases[id];
9452
+ if (catalog.fallbacks.includes(id))
9453
+ return catalog.defaultModel;
9454
+ return;
9455
+ }
9456
+ function claudeModelPicker(models) {
9457
+ return { replaceBuiltInOptions: true, options: models.map((id) => ({ model: id, label: modelLabel(id), description: "Via your ClosedRouter pool", behavesAs: "claude-sonnet-4-6" })) };
9458
+ }
9459
+ function codexModelCatalog(models) {
9460
+ return { models: models.map((id, priority) => ({
9461
+ slug: id,
9462
+ display_name: modelLabel(id),
9463
+ description: "Via your ClosedRouter pool",
9464
+ priority,
9465
+ visibility: "list",
9466
+ supported_in_api: true,
9467
+ supported_reasoning_levels: [],
9468
+ experimental_supported_tools: [],
9469
+ base_instructions: "You are a coding assistant working with the user in their workspace. Follow the user's task and applicable project instructions. Inspect relevant files before editing, preserve unrelated work, and verify changes with appropriate checks. Use the available tools when needed. Report the outcome and any material limitations clearly. The runtime supplies the configured inference model identity through ClosedRouter.",
9470
+ default_reasoning_level: null,
9471
+ supports_reasoning_summaries: false,
9472
+ default_reasoning_summary: "none",
9473
+ shell_type: "shell_command",
9474
+ apply_patch_tool_type: "freeform",
9475
+ web_search_tool_type: "text",
9476
+ input_modalities: ["text"],
9477
+ supports_parallel_tool_calls: false,
9478
+ support_verbosity: false,
9479
+ prefer_websockets: false,
9480
+ supports_image_detail_original: false,
9481
+ context_window: 8192,
9482
+ auto_compact_token_limit: 7000,
9483
+ truncation_policy: { mode: "tokens", limit: 6000 }
9484
+ })) };
9485
+ }
9486
+ function geminiModelCatalog(models, selected) {
9487
+ return {
9488
+ experimental: { dynamicModelConfiguration: true },
9489
+ modelConfigs: {
9490
+ modelDefinitions: Object.fromEntries(models.map((id) => [id, {
9491
+ displayName: modelLabel(id),
9492
+ dialogDescription: "Via your ClosedRouter pool",
9493
+ tier: "custom",
9494
+ family: "closedrouter",
9495
+ isVisible: true,
9496
+ features: { thinking: false }
9497
+ }])),
9498
+ customAliases: Object.fromEntries(models.map((id) => [modelLabel(id), { modelConfig: { model: id } }])),
9499
+ modelIdResolutions: Object.fromEntries(["auto", "pro", "flash", "flash-lite"].map((id) => [id, { default: selected }]))
9500
+ }
9501
+ };
9502
+ }
9503
+ function addModelIdentity(payload, model, protocol) {
9504
+ const text = `ClosedRouter runtime model identity: this request is routed to the configured model ID ${JSON.stringify(model)} through ClosedRouter. The application's name and any vendor-shaped transport alias describe the client, not the underlying inference model. If asked which model you are using, report the configured model ID ${JSON.stringify(model)} and ClosedRouter as the provider. This runtime information supersedes conflicting model identity statements in the application's prompt; it does not change other task instructions.`;
9505
+ if (protocol === "anthropic") {
9506
+ const system = payload.system;
9507
+ payload.system = typeof system === "string" ? `${system}
9508
+
9509
+ ${text}` : [...Array.isArray(system) ? system : [], { type: "text", text }];
9510
+ } else if (protocol === "responses") {
9511
+ payload.instructions = `${typeof payload.instructions === "string" ? payload.instructions + `
9512
+
9513
+ ` : ""}${text}`;
9514
+ } else if (protocol === "chat") {
9515
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
9516
+ const firstUser = messages.findIndex((item) => item && typeof item === "object" && !["system", "developer"].includes(item.role));
9517
+ const index = firstUser < 0 ? messages.length : firstUser;
9518
+ payload.messages = [...messages.slice(0, index), { role: "system", content: text }, ...messages.slice(index)];
9519
+ } else {
9520
+ const system = payload.systemInstruction;
9521
+ const instruction = system && typeof system === "object" && !Array.isArray(system) ? system : {};
9522
+ payload.systemInstruction = { ...instruction, parts: [...Array.isArray(instruction.parts) ? instruction.parts : [], { text }] };
9523
+ }
9524
+ }
9525
+
9526
+ // src/bridge/launch-proxy.ts
9421
9527
  var HOP_HEADERS = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host"]);
9422
- async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey) {
9528
+ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey, modelOverride) {
9423
9529
  const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
9424
9530
  if (!["http:", "https:"].includes(upstream.protocol) || upstream.username || upstream.password) {
9425
9531
  throw new Error("ClosedRouter server URL must use HTTP or HTTPS without embedded credentials.");
9426
9532
  }
9427
9533
  const launchId = randomUUID();
9428
9534
  const transport = upstream.protocol === "https:" ? httpsRequest : httpRequest;
9429
- const server = createServer2((incoming, outgoing) => {
9535
+ const server = createServer2(async (incoming, outgoing) => {
9430
9536
  const authorized = incoming.headers.authorization === `Bearer ${localKey}` || incoming.headers["x-api-key"] === localKey || incoming.headers["x-goog-api-key"] === localKey;
9431
9537
  if (!authorized) {
9432
9538
  outgoing.writeHead(401).end();
@@ -9440,6 +9546,49 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey)
9440
9546
  const local = new URL(incoming.url, "http://localhost");
9441
9547
  target.pathname = upstream.pathname.replace(/\/$/, "") + local.pathname;
9442
9548
  target.search = local.search;
9549
+ const catalog = typeof modelOverride === "object" ? modelOverride : undefined;
9550
+ if (catalog && incoming.method === "GET" && ["/v1/models", "/v1beta/models"].includes(local.pathname)) {
9551
+ const data = catalog.models.map((id) => ({ id, name: modelLabel(id), display_name: modelLabel(id), object: "model", type: "model", created: 0, created_at: "2026-01-01T00:00:00Z", owned_by: "closedrouter" }));
9552
+ outgoing.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(local.pathname === "/v1/models" ? { object: "list", data, has_more: false, first_id: data[0]?.id, last_id: data.at(-1)?.id } : { models: catalog.models.map((id) => ({ name: `models/${id}`, displayName: modelLabel(id), supportedGenerationMethods: ["generateContent", "streamGenerateContent"] })) }));
9553
+ return;
9554
+ }
9555
+ let rewrittenBody;
9556
+ const gemini = /^\/v1beta\/models\/(.+):(generateContent|streamGenerateContent)$/.exec(local.pathname);
9557
+ if (modelOverride && incoming.method === "POST" && (gemini || /^\/v1\/(?:messages(?:\/count_tokens)?|chat\/completions|responses)$/.test(local.pathname))) {
9558
+ try {
9559
+ const chunks = [];
9560
+ let length = 0;
9561
+ for await (const chunk of incoming) {
9562
+ length += chunk.length;
9563
+ if (length > 32 * 1024 * 1024) {
9564
+ outgoing.writeHead(413).end();
9565
+ return;
9566
+ }
9567
+ chunks.push(Buffer.from(chunk));
9568
+ }
9569
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
9570
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
9571
+ outgoing.writeHead(400).end();
9572
+ return;
9573
+ }
9574
+ const payload = body;
9575
+ const requested = gemini ? decodeURIComponent(gemini[1]) : typeof payload.model === "string" ? payload.model : catalog?.defaultModel || "";
9576
+ const model = catalog ? resolveLaunchModel(catalog, requested) : modelOverride;
9577
+ if (!model) {
9578
+ outgoing.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: { type: "invalid_request_error", message: "This model is not in this launch's ClosedRouter catalog. Choose a supported model from the picker." } }));
9579
+ return;
9580
+ }
9581
+ const tinyProbe = local.pathname === "/v1/messages" && !model.startsWith("claude-") && payload.model !== model && typeof payload.max_tokens === "number" && payload.max_tokens <= 16 && Array.isArray(payload.messages) && payload.messages.length === 1 && (!Array.isArray(payload.tools) || payload.tools.length === 0);
9582
+ if (catalog)
9583
+ addModelIdentity(payload, model, gemini ? "gemini" : local.pathname.startsWith("/v1/messages") ? "anthropic" : local.pathname === "/v1/responses" ? "responses" : "chat");
9584
+ if (gemini)
9585
+ target.pathname = upstream.pathname.replace(/\/$/, "") + `/v1beta/models/${encodeURIComponent(model)}:${gemini[2]}`;
9586
+ rewrittenBody = JSON.stringify({ ...payload, ...!gemini ? { model } : {}, ...tinyProbe ? { max_tokens: 512 } : {} });
9587
+ } catch {
9588
+ outgoing.writeHead(400).end();
9589
+ return;
9590
+ }
9591
+ }
9443
9592
  const headers = { ...incoming.headers };
9444
9593
  const connectionHeaders = String(headers.connection || "").split(",").map((x) => x.trim().toLowerCase());
9445
9594
  for (const key of Object.keys(headers)) {
@@ -9453,6 +9602,8 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey)
9453
9602
  delete headers["x-goog-api-key"];
9454
9603
  headers.authorization = `Bearer ${apiKey}`;
9455
9604
  }
9605
+ if (rewrittenBody !== undefined)
9606
+ delete headers["content-length"];
9456
9607
  const request = transport(target, { method: incoming.method, headers }, (response) => {
9457
9608
  const responseHeaders = { ...response.headers };
9458
9609
  for (const key of HOP_HEADERS)
@@ -9472,7 +9623,10 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey)
9472
9623
  if (!outgoing.writableFinished)
9473
9624
  request.destroy();
9474
9625
  });
9475
- incoming.pipe(request);
9626
+ if (rewrittenBody !== undefined)
9627
+ request.end(rewrittenBody);
9628
+ else
9629
+ incoming.pipe(request);
9476
9630
  });
9477
9631
  await new Promise((resolve, reject) => {
9478
9632
  server.once("error", reject);
@@ -9498,7 +9652,7 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey)
9498
9652
  import { createServer as createServer3 } from "node:http";
9499
9653
  import { request as httpRequest2 } from "node:http";
9500
9654
  import { request as httpsRequest2 } from "node:https";
9501
- async function startAgyModelProxy(gatewayUrl, apiKey, model) {
9655
+ async function startAgyModelProxy(gatewayUrl, apiKey, model, catalog) {
9502
9656
  const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
9503
9657
  if (!["https:", "http:"].includes(upstream.protocol)) {
9504
9658
  throw new Error("ClosedRouter server URL must use HTTP or HTTPS.");
@@ -9510,12 +9664,17 @@ async function startAgyModelProxy(gatewayUrl, apiKey, model) {
9510
9664
  return;
9511
9665
  }
9512
9666
  const requestUrl = new URL(incoming.url || "/", "http://localhost");
9513
- const match = /^\/v1beta\/models\/[^/:]+:(generateContent|streamGenerateContent)$/.exec(requestUrl.pathname);
9667
+ const match = /^\/v1beta\/models\/([^/:]+):(generateContent|streamGenerateContent)$/.exec(requestUrl.pathname);
9514
9668
  if (incoming.method !== "POST" || !match) {
9515
9669
  outgoing.writeHead(404).end();
9516
9670
  return;
9517
9671
  }
9518
- const target = new URL(`v1beta/models/${encodeURIComponent(model)}:${match[1]}${requestUrl.search}`, upstream);
9672
+ const selected = catalog ? resolveLaunchModel(catalog, decodeURIComponent(match[1])) : model;
9673
+ if (!selected) {
9674
+ outgoing.writeHead(400).end(JSON.stringify({ error: { message: "Choose a supported ClosedRouter model." } }));
9675
+ return;
9676
+ }
9677
+ const target = new URL(`v1beta/models/${encodeURIComponent(selected)}:${match[2]}${requestUrl.search}`, upstream);
9519
9678
  const headers = { ...incoming.headers, host: target.host, "x-goog-api-key": apiKey };
9520
9679
  const upstreamRequest = requestUpstream(target, { method: "POST", headers }, (response) => {
9521
9680
  outgoing.writeHead(response.statusCode || 502, response.headers);
@@ -9555,28 +9714,26 @@ import { join as join9 } from "node:path";
9555
9714
  function record(value) {
9556
9715
  return typeof value === "object" && value !== null && !Array.isArray(value);
9557
9716
  }
9558
- function registerAgyLaunchModel(homeDir, model, transportModel) {
9717
+ function registerAgyLaunchCatalog(homeDir, catalog, legacyTransport) {
9559
9718
  const path = join9(homeDir, ".gemini", "antigravity-cli", "settings.json");
9560
9719
  const settings = JSON.parse(readFileSync8(path, "utf8"));
9561
- if (!record(settings) || settings.modelProvider !== "gemini") {
9720
+ if (!record(settings) || settings.modelProvider !== "gemini")
9562
9721
  throw new Error(`Set "modelProvider": "gemini" in ${path} before starting Antigravity through ClosedRouter.`);
9563
- }
9564
9722
  const config = settings.customModelsConfig ?? {};
9565
9723
  if (!record(config))
9566
9724
  throw new Error(`customModelsConfig must be an object in ${path}.`);
9567
- const models = config.customModels ?? {};
9568
- if (!record(models))
9725
+ const previous = config.customModels ?? {};
9726
+ if (!record(previous))
9569
9727
  throw new Error(`customModelsConfig.customModels must be an object in ${path}.`);
9570
- const label = `ClosedRouter: ${model}`;
9571
- const entry = { modelName: transportModel, apiProvider: "gemini" };
9572
- const existing = models[label];
9573
- if (existing !== undefined) {
9574
- if (!record(existing) || existing.modelName !== transportModel || existing.apiProvider !== "gemini" || Object.keys(existing).some((key) => !["modelName", "apiProvider"].includes(key))) {
9728
+ const models = { ...previous };
9729
+ for (const id of catalog.models) {
9730
+ const label = modelLabel(id), name = transportModelId(id, "gemini"), existing = previous[label];
9731
+ if (existing !== undefined && (!record(existing) || existing.apiProvider !== "gemini" || ![name, legacyTransport].includes(String(existing.modelName)) || Object.keys(existing).some((key) => !["modelName", "apiProvider"].includes(key)))) {
9575
9732
  throw new Error(`The custom model '${label}' already has different settings in ${path}. Rename that entry before launching it with ClosedRouter.`);
9576
9733
  }
9577
- return label;
9734
+ models[label] = { modelName: name, apiProvider: "gemini" };
9578
9735
  }
9579
- settings.customModelsConfig = { ...config, customModels: { ...models, [label]: entry } };
9736
+ settings.customModelsConfig = { ...config, customModels: models };
9580
9737
  const temporary = `${path}.closedrouter-${randomUUID2()}.tmp`;
9581
9738
  try {
9582
9739
  writeFileSync2(temporary, `${JSON.stringify(settings, null, 2)}
@@ -9586,7 +9743,7 @@ function registerAgyLaunchModel(homeDir, model, transportModel) {
9586
9743
  if (existsSync9(temporary))
9587
9744
  unlinkSync(temporary);
9588
9745
  }
9589
- return label;
9746
+ return modelLabel(catalog.defaultModel);
9590
9747
  }
9591
9748
 
9592
9749
  // src/commands/cursor-launch.ts
@@ -9653,14 +9810,14 @@ function resolveCursorLocalExecutable(executablePath, homeDir = homedir9()) {
9653
9810
  // src/commands/desktop-launch.ts
9654
9811
  import { spawn as spawn3 } from "node:child_process";
9655
9812
  import { randomUUID as randomUUID3 } from "node:crypto";
9656
- import { accessSync, constants, existsSync as existsSync11, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
9813
+ import { accessSync, closeSync, constants, existsSync as existsSync11, mkdirSync as mkdirSync2, mkdtempSync, openSync, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
9657
9814
  import { homedir as homedir10 } from "node:os";
9658
9815
  import { dirname, isAbsolute, join as join11, relative, resolve } from "node:path";
9659
9816
 
9660
9817
  // src/commands/desktop-inspector.ts
9661
9818
  import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
9662
9819
  import { setTimeout as delay } from "node:timers/promises";
9663
- async function installDesktopInspectorHook(hookPath, logPath, signal) {
9820
+ async function installDesktopInspectorHook(hookPath, logPath, signal, options = {}) {
9664
9821
  const deadline = Date.now() + 15000;
9665
9822
  let target;
9666
9823
  while (Date.now() < deadline && !signal.aborted) {
@@ -9671,7 +9828,7 @@ async function installDesktopInspectorHook(hookPath, logPath, signal) {
9671
9828
  await delay(100, undefined, { signal });
9672
9829
  }
9673
9830
  if (!target)
9674
- throw new Error("Antigravity did not expose its launch inspector. Close the paused app and retry with --open-only.");
9831
+ throw new Error("Desktop app did not expose its launch inspector. Close the paused app and retry with --open-only.");
9675
9832
  const Socket = "Bun" in globalThis ? globalThis.WebSocket : wrapper_default;
9676
9833
  const socket = new Socket(target);
9677
9834
  let sequence = 0;
@@ -9685,7 +9842,7 @@ async function installDesktopInspectorHook(hookPath, logPath, signal) {
9685
9842
  signal.addEventListener("abort", abort, { once: true });
9686
9843
  const closed = () => {
9687
9844
  for (const item of pending.values())
9688
- item.reject(new Error("Antigravity launch inspector disconnected before the hook was installed."));
9845
+ item.reject(new Error("Desktop app launch inspector disconnected before the hook was installed."));
9689
9846
  pending.clear();
9690
9847
  };
9691
9848
  socket.addEventListener("close", closed);
@@ -9712,21 +9869,29 @@ async function installDesktopInspectorHook(hookPath, logPath, signal) {
9712
9869
  try {
9713
9870
  await new Promise((resolve, reject) => {
9714
9871
  socket.addEventListener("open", () => resolve(), { once: true });
9715
- socket.addEventListener("error", () => reject(new Error("Could not connect to Antigravity's launch inspector.")), { once: true });
9716
- socket.addEventListener("close", () => reject(new Error("Antigravity's launch inspector closed.")), { once: true });
9872
+ socket.addEventListener("error", () => reject(new Error("Could not connect to Desktop app's launch inspector.")), { once: true });
9873
+ socket.addEventListener("close", () => reject(new Error("Desktop app's launch inspector closed.")), { once: true });
9717
9874
  });
9718
9875
  await rpc("Debugger.enable");
9876
+ if (options.startupBreakpoint)
9877
+ await rpc("Debugger.setBreakpointByUrl", options.startupBreakpoint);
9719
9878
  await rpc("Runtime.runIfWaitingForDebugger");
9720
9879
  await Promise.race([paused, delay(Math.max(1, deadline - Date.now()), undefined, { signal }).then(() => {
9721
- throw new Error("Antigravity did not pause before loading its application.");
9880
+ throw new Error("Desktop app did not pause before loading its application.");
9722
9881
  })]);
9723
9882
  const result = await rpc("Runtime.evaluate", {
9724
9883
  expression: `process.getBuiltinModule('module').createRequire(process.cwd() + '/closedrouter-hook.cjs')(${JSON.stringify(hookPath)}); 'closedrouter-hook-loaded'`,
9725
9884
  returnByValue: true
9726
9885
  });
9727
9886
  if (result.exceptionDetails || result.result?.value !== "closedrouter-hook-loaded")
9728
- throw new Error("Could not install Antigravity's request hook. Close the paused app and use --open-only.");
9887
+ throw new Error("Could not install Desktop app's request hook. Close the paused app and use --open-only.");
9729
9888
  await rpc("Debugger.resume");
9889
+ if (options.readyPath) {
9890
+ while (!existsSync10(options.readyPath) && Date.now() < deadline)
9891
+ await delay(25, undefined, { signal });
9892
+ if (!existsSync10(options.readyPath))
9893
+ throw new Error("The desktop app did not enable its local provider runtime.");
9894
+ }
9730
9895
  await rpc("Runtime.evaluate", { expression: "setTimeout(() => process.getBuiltinModule('inspector').close(), 100).unref(); true", returnByValue: true });
9731
9896
  } finally {
9732
9897
  clearTimeout(timer);
@@ -9776,7 +9941,7 @@ function createDesktopProfile(tool) {
9776
9941
  mkdirSync2(root, { recursive: true, mode: 448 });
9777
9942
  return mkdtempSync(join11(root, "session-"));
9778
9943
  }
9779
- function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey, model, toolArgs = [], platform = process.platform) {
9944
+ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey, model, toolArgs = [], platform = process.platform, models = model ? [model] : []) {
9780
9945
  validateDesktopArgs(toolArgs);
9781
9946
  const env = { ...process.env };
9782
9947
  delete env.ELECTRON_RUN_AS_NODE;
@@ -9796,6 +9961,8 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
9796
9961
  `model = ${JSON.stringify(model)}`,
9797
9962
  'model_provider = "closedrouter"',
9798
9963
  "",
9964
+ `model_catalog_json = ${JSON.stringify(join11(codexHome, "closedrouter-models.json"))}`,
9965
+ "",
9799
9966
  "[model_providers.closedrouter]",
9800
9967
  'name = "ClosedRouter"',
9801
9968
  `base_url = ${JSON.stringify(`${baseUrl.replace(/\/+$/, "")}/v1`)}`,
@@ -9805,6 +9972,7 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
9805
9972
  ].join(`
9806
9973
  `) });
9807
9974
  files.push({ path: join11(codexHome, "auth.json"), contents: JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: localKey }) });
9975
+ files.push({ path: join11(codexHome, "closedrouter-models.json"), contents: JSON.stringify(codexModelCatalog([...new Set([model, ...models])])) });
9808
9976
  }
9809
9977
  if (tool === "claude" && baseUrl && localKey && model) {
9810
9978
  userData = join11(profileDir, "user-data-3p");
@@ -9819,7 +9987,7 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
9819
9987
  inferenceGatewayApiKey: localKey,
9820
9988
  inferenceGatewayAuthScheme: "bearer",
9821
9989
  disableDeploymentModeChooser: true,
9822
- inferenceModels: [{ name: model }]
9990
+ inferenceModels: [...new Set([model, ...models])].map((id) => ({ name: transportModelId(id, "claude"), labelOverride: modelLabel(id) }))
9823
9991
  }) });
9824
9992
  }
9825
9993
  Object.assign(env, appEnv);
@@ -9859,7 +10027,10 @@ function assertClaudeDesktopGatewaySupport(executable) {
9859
10027
  async function runDesktopLaunch(plan) {
9860
10028
  return await new Promise((resolveExit, reject) => {
9861
10029
  const controller = new AbortController;
9862
- const child = spawn3(plan.command, plan.args, { env: plan.env, stdio: "inherit" });
10030
+ const stderrFile = plan.inspectorHook && plan.command !== "/usr/bin/open" ? openSync(plan.inspectorHook.logPath, "a", 384) : undefined;
10031
+ const child = spawn3(plan.command, plan.args, { env: plan.env, stdio: stderrFile === undefined ? "inherit" : ["inherit", "inherit", stderrFile] });
10032
+ if (stderrFile !== undefined)
10033
+ closeSync(stderrFile);
9863
10034
  const interrupt = () => {
9864
10035
  controller.abort();
9865
10036
  child.kill("SIGTERM");
@@ -9880,7 +10051,7 @@ async function runDesktopLaunch(plan) {
9880
10051
  resolveExit(code ?? 1);
9881
10052
  });
9882
10053
  if (plan.inspectorHook) {
9883
- installDesktopInspectorHook(plan.inspectorHook.path, plan.inspectorHook.logPath, controller.signal).catch((error) => {
10054
+ installDesktopInspectorHook(plan.inspectorHook.path, plan.inspectorHook.logPath, controller.signal, plan.inspectorHook).catch((error) => {
9884
10055
  child.kill("SIGTERM");
9885
10056
  cleanup();
9886
10057
  reject(error);
@@ -9895,9 +10066,6 @@ import { chmodSync, existsSync as existsSync12, mkdirSync as mkdirSync3 } from "
9895
10066
  import { homedir as homedir11 } from "node:os";
9896
10067
  import { dirname as dirname2, join as join12 } from "node:path";
9897
10068
  var CURSOR_APPLICATION_STORAGE = "src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser";
9898
- function cursorDesktopSupportsModel(model) {
9899
- return !model.startsWith("claude-") && !model.startsWith("gemini-");
9900
- }
9901
10069
  async function writeSqlite(dbPath, sql) {
9902
10070
  let database;
9903
10071
  if ("Bun" in globalThis) {
@@ -9930,35 +10098,42 @@ function defaultCursorDatabase() {
9930
10098
  const root = process.platform === "darwin" ? join12(homedir11(), "Library", "Application Support") : process.platform === "win32" ? process.env.APPDATA || join12(homedir11(), "AppData", "Roaming") : join12(homedir11(), ".config");
9931
10099
  return join12(root, "Cursor", "User", "globalStorage", "state.vscdb");
9932
10100
  }
9933
- async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, sourceDb = defaultCursorDatabase()) {
9934
- if (!cursorDesktopSupportsModel(model))
9935
- throw new Error("Cursor desktop selects a separate provider for Claude/Gemini model names. Choose an OpenAI-compatible custom model or use cr start cursor with the local CLI runtime.");
10101
+ async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, sourceDb = process.env.CLOSEDROUTER_CURSOR_AUTH_DB ?? defaultCursorDatabase(), models = [model]) {
10102
+ const cursorModel = model;
10103
+ const catalog = [...new Set([model, ...models])];
9936
10104
  const dbPath = join12(profileDir, "user-data", "User", "globalStorage", "state.vscdb");
9937
10105
  mkdirSync3(dirname2(dbPath), { recursive: true, mode: 448 });
9938
10106
  const items = new Map;
10107
+ let membershipType;
9939
10108
  if (existsSync12(sourceDb)) {
9940
10109
  const auth = await querySqliteKeyValue(sourceDb, "cursorAuth/");
9941
- for (const name of ["accessToken", "refreshToken", "cachedEmail", "stripeMembershipAuthId", "membershipType", "cachedSignUpType"]) {
9942
- const key = `cursorAuth/${name}`;
9943
- const value = auth.get(key);
9944
- if (value !== undefined)
9945
- items.set(key, value);
10110
+ if (auth.get("cursorAuth/accessToken") && auth.get("cursorAuth/refreshToken")) {
10111
+ for (const name of ["accessToken", "refreshToken", "cachedEmail", "stripeMembershipAuthId", "stripeMembershipType", "stripeSubscriptionStatus", "membershipType", "cachedSignUpType"]) {
10112
+ const key = `cursorAuth/${name}`;
10113
+ const value = auth.get(key);
10114
+ if (value !== undefined)
10115
+ items.set(key, value);
10116
+ }
10117
+ membershipType = auth.get("cursorAuth/stripeMembershipType") || auth.get("cursorAuth/membershipType");
9946
10118
  }
9947
10119
  }
9948
10120
  items.set("cursorAuth/openAIKey", apiKey);
9949
10121
  items.set(CURSOR_APPLICATION_STORAGE, JSON.stringify({
9950
10122
  useOpenAIKey: true,
9951
10123
  openAIBaseUrl: `${baseUrl.replace(/\/+$/, "")}/v1`,
9952
- availableAPIKeyModels: [model],
10124
+ availableAPIKeyModels: catalog,
9953
10125
  havePerformedSettingsServiceMigration: true,
10126
+ localProviderModelIds: catalog,
10127
+ localProviderAgentModelIds: catalog,
10128
+ ...membershipType ? { membershipType } : {},
9954
10129
  aiSettings: {
9955
- userAddedModels: [model],
9956
- modelOverrideEnabled: [model],
10130
+ userAddedModels: catalog,
10131
+ modelOverrideEnabled: catalog,
9957
10132
  modelOverrideDisabled: [],
9958
- modelConfig: Object.fromEntries(["cmd-k", "composer", "background-composer", "plan-execution", "spec", "quick-agent"].map((feature) => [feature, { modelName: model, maxMode: false, selectedModels: [{ modelId: model, parameters: [] }] }]))
10133
+ modelConfig: Object.fromEntries(["cmd-k", "composer", "background-composer", "plan-execution", "spec", "quick-agent"].map((feature) => [feature, { modelName: cursorModel, maxMode: false, selectedModels: [{ modelId: cursorModel, parameters: [] }] }]))
9959
10134
  }
9960
10135
  }));
9961
- items.set("lastSingleModelPreference", JSON.stringify({ composer: model, "cmd-k": model }));
10136
+ items.set("lastSingleModelPreference", JSON.stringify({ composer: cursorModel, "cmd-k": cursorModel }));
9962
10137
  const quote = (value) => `'${value.replace(/'/g, "''")}'`;
9963
10138
  await writeSqlite(dbPath, [
9964
10139
  "BEGIN IMMEDIATE;",
@@ -9978,6 +10153,129 @@ import { randomUUID as randomUUID4 } from "node:crypto";
9978
10153
  import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
9979
10154
  import { homedir as homedir12 } from "node:os";
9980
10155
  import { join as join13 } from "node:path";
10156
+
10157
+ // src/commands/agy-desktop-catalog.ts
10158
+ function patchAntigravityCatalog(source, catalog) {
10159
+ const provider = /this\.customModelsProvider=([\w$]+)\(this\.rootProvider,([\w$]+)=>\(\{customModels:\2\.appState\?\.customModelsConfig\?\.customModels\?\?\{\}\}\)\)/;
10160
+ const context = /function ([\w$]+)\(\{children:([\w$]+),userStatus:([\w$]+),lastSelectedAgentModel:([\w$]+),setLastSelectedAgentModel:/;
10161
+ const match = context.exec(source);
10162
+ if (!provider.test(source) || !match)
10163
+ throw new Error("This Antigravity build has an unsupported custom-model picker. Update the ClosedRouter launcher.");
10164
+ const entries = catalog.models.map((id) => [`ClosedRouter: ${id}`, Object.keys(catalog.aliases).find((alias) => alias.startsWith("gemini-closedrouter-") && catalog.aliases[alias] === id)]);
10165
+ const models = `Object.fromEntries(${JSON.stringify(entries)}.map(([label,id])=>[label,r(I.ModelInfoSchema,{modelId:I.Model.GOOGLE_GEMINI_INTERNAL_BYOM,apiProvider:I.APIProvider.API_PROVIDER_GOOGLE_EVERGREEN,chatModelName:label,modelName:id,maxTokens:32768,maxOutputTokens:4096,supportsContext:true,modelFeatures:r(I.ModelFeaturesSchema,{zeroShotCapable:true,supportsToolCalls:true,supportsImages:false,supportsThinking:false})})]))`;
10166
+ const customState = /([\w$]+)=!!([\w$]+),([\w$]+)=\(0,([\w$]+)\.useMemo\)\(\(\)=>\(\{customModels:([\w$]+)\?\.customModels\?\?([\w$]+)\}\),\[\5\]\)/;
10167
+ if (!customState.test(source))
10168
+ throw new Error("This Antigravity build has an unsupported custom-model context.");
10169
+ source = source.replace(customState, (_all, supported, _feature, state, react, input) => `${supported}=true,${state}=(0,${react}.useMemo)(()=>({customModels:${models}}),[${input}])`);
10170
+ const customHeader = /showCustomModelHeader:[\w$]+,onAddCustomModel:/;
10171
+ if (!customHeader.test(source))
10172
+ throw new Error("This Antigravity build has an unsupported custom-model menu.");
10173
+ source = source.replace(/showCustomModelHeader:[\w$]+,onAddCustomModel:[\w$]+,onEditCustomModel:[\w$]+/, "showCustomModelHeader:true,onAddCustomModel:undefined,onEditCustomModel:undefined");
10174
+ source = source.replace(/([\w$]+)=([\w$]+)\.some\(([\w$]+)=>\3\.options\.length>0\);([\w$]+)=/, "$1=true;$4=");
10175
+ source = source.replace(provider, (_all, selector, variable) => `this.customModelsProvider=${selector}(this.rootProvider,${variable}=>({customModels:${models}}))`);
10176
+ const start = source.indexOf(match[0]);
10177
+ const end = source.indexOf(`function `, start + match[0].length);
10178
+ let body = source.slice(start, end);
10179
+ const state = /\[([\w$]+),([\w$]+)\]=\(0,([\w$]+)\.useState\)\(null\)/;
10180
+ if (!state.test(body) || !body.includes("clientModelConfigs||[]"))
10181
+ throw new Error("This Antigravity build has an unsupported model-selection context.");
10182
+ body = body.replace(state, (_all, value, setter, react) => `[${value},${setter}]=(0,${react}.useState)(${JSON.stringify(`ClosedRouter: ${catalog.defaultModel}`)})`).replace(/\([\w$]+\?\.cascadeModelConfigData\?\.clientModelConfigs\|\|\[\]\)/g, "([])").replace(/[\w$]+\?\.clientModelConfigs\|\|\[\]/g, "[]").replace(/[\w$]+\?\.clientModelSorts\|\|\[\]/g, "[]");
10183
+ return source.slice(0, start) + body + source.slice(end);
10184
+ }
10185
+ function antigravityCatalogHook(catalog, marker) {
10186
+ return `
10187
+ const {app} = require('electron');
10188
+ const patchCatalog = ${patchAntigravityCatalog.toString()};
10189
+ app.on('web-contents-created', (_event, contents) => {
10190
+ const debuggerClient = contents.debugger;
10191
+ try {
10192
+ debuggerClient.attach('1.3');
10193
+ debuggerClient.sendCommand('Fetch.enable', {patterns:[{urlPattern:'https://127.0.0.1:*/main.js',requestStage:'Response'}]}).catch(() => {});
10194
+ debuggerClient.on('message', async (_event, method, params) => {
10195
+ if (method !== 'Fetch.requestPaused') return;
10196
+ try {
10197
+ const url = new URL(params.request.url);
10198
+ if (url.hostname !== '127.0.0.1' || url.pathname !== '/main.js' || params.responseStatusCode !== 200) {
10199
+ await debuggerClient.sendCommand('Fetch.continueRequest', {requestId:params.requestId}); return;
10200
+ }
10201
+ const result = await debuggerClient.sendCommand('Fetch.getResponseBody', {requestId:params.requestId});
10202
+ const source = result.base64Encoded ? Buffer.from(result.body,'base64').toString('utf8') : result.body;
10203
+ const patched = patchCatalog(source, ${JSON.stringify(catalog)});
10204
+ await debuggerClient.sendCommand('Fetch.fulfillRequest', {requestId:params.requestId,responseCode:200,
10205
+ responseHeaders:(params.responseHeaders || []).filter(h=>!['content-length','content-encoding','transfer-encoding'].includes(h.name.toLowerCase())),
10206
+ body:Buffer.from(patched).toString('base64')});
10207
+ require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'ready', {mode:0o600});
10208
+ await debuggerClient.sendCommand('Fetch.disable');
10209
+ debuggerClient.detach();
10210
+ } catch (error) {
10211
+ require('node:fs').writeFileSync(${JSON.stringify(marker + ".error")}, String(error.message), {mode:0o600});
10212
+ await debuggerClient.sendCommand('Fetch.failRequest', {requestId:params.requestId,errorReason:'Failed'}).catch(()=>{});
10213
+ }
10214
+ });
10215
+ } catch (error) {
10216
+ require('node:fs').writeFileSync(${JSON.stringify(marker + ".error")}, String(error.message), {mode:0o600});
10217
+ }
10218
+ });
10219
+ `;
10220
+ }
10221
+
10222
+ // src/commands/model-branding.ts
10223
+ var CLOSEDROUTER_LOGO = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#D4FE51"/><path d="M16.68 7.82A6.5 6.5 0 1 0 16.68 16.18" fill="none" stroke="#04080A" stroke-width="2.6" stroke-linecap="round"/><circle cx="18.2" cy="12" r="1.9" fill="#04080A"/></svg>';
10224
+ function installModelBranding(models, artwork) {
10225
+ if (document.querySelector("[data-closedrouter-branding]"))
10226
+ return;
10227
+ const marker = document.createElement("meta");
10228
+ marker.dataset.closedrouterBranding = "true";
10229
+ document.head.append(marker);
10230
+ const paint = () => {
10231
+ for (const element of document.querySelectorAll('button,[role="button"],[role="option"],[role="menuitem"],[role="menuitemradio"],[data-testid="model-selector-item"]')) {
10232
+ const text = element.innerText.trim().split(`
10233
+ `)[0];
10234
+ if (!models.some((id) => text === id || text === `ClosedRouter: ${id}`))
10235
+ continue;
10236
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
10237
+ while (walker.nextNode()) {
10238
+ const node = walker.currentNode;
10239
+ const value = node.textContent || "";
10240
+ if (models.some((id) => value.trim() === `ClosedRouter: ${id}`))
10241
+ node.textContent = value.replace("ClosedRouter: ", "");
10242
+ }
10243
+ if (element.querySelector(":scope > [data-closedrouter-logo]"))
10244
+ continue;
10245
+ const logo = document.createElement("img");
10246
+ logo.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(artwork)}`;
10247
+ logo.alt = "ClosedRouter";
10248
+ logo.title = "Routed through ClosedRouter";
10249
+ logo.dataset.closedrouterLogo = "true";
10250
+ logo.style.cssText = "width:16px;height:16px;flex:0 0 16px;display:inline-block;vertical-align:middle;margin-inline-end:6px;pointer-events:none";
10251
+ element.prepend(logo);
10252
+ }
10253
+ };
10254
+ let scheduled = false;
10255
+ const observer = new MutationObserver(() => {
10256
+ if (scheduled)
10257
+ return;
10258
+ scheduled = true;
10259
+ requestAnimationFrame(() => {
10260
+ scheduled = false;
10261
+ paint();
10262
+ });
10263
+ });
10264
+ observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
10265
+ paint();
10266
+ }
10267
+ function modelBrandingHook(models) {
10268
+ const script = `(${installModelBranding.toString()})(${JSON.stringify(models)},${JSON.stringify(CLOSEDROUTER_LOGO)})`;
10269
+ return `
10270
+ require('electron').app.on('web-contents-created', (_event, contents) => {
10271
+ contents.on('did-finish-load', () => {
10272
+ contents.executeJavaScript(${JSON.stringify(script)}).catch(() => {});
10273
+ });
10274
+ });
10275
+ `;
10276
+ }
10277
+
10278
+ // src/commands/agy-desktop.ts
9981
10279
  var MODS_CONTROL_URL = "http://127.0.0.1:59998";
9982
10280
  var MODS_PROXY_URL = "http://127.0.0.1:59997";
9983
10281
  async function assertAntigravityModsAvailable() {
@@ -10014,7 +10312,7 @@ function assertAntigravityDesktopHookSupport(executable) {
10014
10312
  if (index < 0 || binary[index + 32 + 5] !== 49)
10015
10313
  throw new Error("This Antigravity build disables launch inspector hooks. Use --open-only.");
10016
10314
  }
10017
- async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_PROXY_URL) {
10315
+ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_PROXY_URL, catalog) {
10018
10316
  const capability = randomUUID4();
10019
10317
  const controllers = new Set;
10020
10318
  const server = createServer4(async (incoming, outgoing) => {
@@ -10053,9 +10351,15 @@ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_
10053
10351
  return;
10054
10352
  }
10055
10353
  const body = JSON.parse(raw.toString("utf8"));
10354
+ const requested = body.modelName || body.model || model;
10355
+ const selected = catalog ? resolveLaunchModel(catalog, requested) : model;
10356
+ if (!selected) {
10357
+ outgoing.writeHead(400).end(JSON.stringify({ error: { message: "Choose a supported ClosedRouter model." } }));
10358
+ return;
10359
+ }
10056
10360
  const request = body.request || body;
10057
10361
  payload = Buffer.from(JSON.stringify(request));
10058
- target = `${gatewayUrl}/v1beta/models/${encodeURIComponent(model)}:${generation[1]}${generation[1] === "streamGenerateContent" ? "?alt=sse" : ""}`;
10362
+ target = `${gatewayUrl}/v1beta/models/${encodeURIComponent(selected)}:${generation[1]}${generation[1] === "streamGenerateContent" ? "?alt=sse" : ""}`;
10059
10363
  headers.authorization = `Bearer ${localKey}`;
10060
10364
  headers["content-type"] = "application/json";
10061
10365
  } else {
@@ -10136,8 +10440,9 @@ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_
10136
10440
  })
10137
10441
  };
10138
10442
  }
10139
- function addAntigravityDesktopHook(plan, endpoint) {
10443
+ function addAntigravityDesktopHook(plan, endpoint, catalog) {
10140
10444
  const path = join13(plan.profileDir, "antigravity-hook.cjs");
10445
+ const marker = join13(plan.profileDir, "model-catalog-ready");
10141
10446
  const contents = `const cp = require('node:child_process');
10142
10447
  const spawn = cp.spawn;
10143
10448
  cp.spawn = function(command, args, options) {
@@ -10159,10 +10464,10 @@ cp.spawn = function(command, args, options) {
10159
10464
  return spawn.call(this, command, args, options);
10160
10465
  };
10161
10466
  require('node:fs').writeFileSync(${JSON.stringify(join13(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
10162
- `;
10467
+ ` + (catalog ? antigravityCatalogHook(catalog, marker) + modelBrandingHook(catalog.models) : "");
10163
10468
  plan.files.push({ path, contents });
10164
10469
  const logPath = join13(plan.profileDir, "inspector.log");
10165
- plan.inspectorHook = { path, logPath };
10470
+ plan.inspectorHook = { path, logPath, ...catalog ? { readyPath: marker } : {} };
10166
10471
  plan.args.push("--inspect-brk=127.0.0.1:0");
10167
10472
  if (plan.command === "/usr/bin/open") {
10168
10473
  const index = plan.args.indexOf("--args");
@@ -10170,6 +10475,242 @@ require('node:fs').writeFileSync(${JSON.stringify(join13(plan.profileDir, "hook-
10170
10475
  }
10171
10476
  }
10172
10477
 
10478
+ // src/commands/claude-desktop.ts
10479
+ import { execFileSync as execFileSync2 } from "node:child_process";
10480
+ import { createHash as createHash3 } from "node:crypto";
10481
+ import { copyFileSync, existsSync as existsSync14, linkSync, lstatSync, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, readdirSync as readdirSync2, readlinkSync, realpathSync, renameSync as renameSync2, rmSync, statSync as statSync2, symlinkSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
10482
+ import { homedir as homedir13 } from "node:os";
10483
+ import { dirname as dirname3, join as join14, relative as relative2 } from "node:path";
10484
+ function cloneClaudeElectronRuntime(source, destination) {
10485
+ const clone = (from, to) => {
10486
+ const info = lstatSync(from);
10487
+ if (info.isSymbolicLink())
10488
+ symlinkSync(readlinkSync(from), to);
10489
+ else if (info.isDirectory()) {
10490
+ mkdirSync4(to, { mode: 448 });
10491
+ for (const name of readdirSync2(from))
10492
+ clone(join14(from, name), join14(to, name));
10493
+ } else if (info.isFile())
10494
+ linkSync(from, to);
10495
+ else
10496
+ throw new Error("Unsupported file in the shared Electron runtime.");
10497
+ };
10498
+ clone(source, destination);
10499
+ const contents = join14(destination, "Contents");
10500
+ const roots = [contents, ...readdirSync2(join14(contents, "Frameworks")).filter((name) => name.endsWith(".app")).map((name) => join14(contents, "Frameworks", name, "Contents"))];
10501
+ const owned = [];
10502
+ for (const root of roots) {
10503
+ if (!realpathSync(root).startsWith(realpathSync(destination) + "/"))
10504
+ throw new Error("Electron helper signing must stay inside the owned runtime.");
10505
+ for (const name of readdirSync2(join14(root, "MacOS"))) {
10506
+ const target = join14(root, "MacOS", name);
10507
+ if (!lstatSync(target).isFile())
10508
+ throw new Error("Electron helper executables must be regular files.");
10509
+ const original = join14(source, relative2(destination, target));
10510
+ unlinkSync2(target);
10511
+ copyFileSync(original, target);
10512
+ owned.push(target);
10513
+ }
10514
+ rmSync(join14(root, "_CodeSignature"), { recursive: true, force: true });
10515
+ }
10516
+ return owned;
10517
+ }
10518
+ function prepareClaudeElectronRuntime(runtime, sourceApp) {
10519
+ const source = dirname3(dirname3(dirname3(realpathSync(runtime))));
10520
+ if (!source.endsWith(".app"))
10521
+ throw new Error("Claude's shared runtime must be a macOS Electron app bundle.");
10522
+ const framework = join14(source, "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework");
10523
+ const fingerprint = createHash3("sha256").update(JSON.stringify({
10524
+ schema: 1,
10525
+ source,
10526
+ sourceApp,
10527
+ executable: statSync2(runtime).mtimeMs,
10528
+ framework: statSync2(framework).mtimeMs,
10529
+ appInfo: readFileSync12(join14(sourceApp, "Contents/Info.plist"), "utf8")
10530
+ })).digest("hex").slice(0, 24);
10531
+ const root = join14(getConfigDir(), "desktop-runtime", "claude");
10532
+ mkdirSync4(root, { recursive: true, mode: 448 });
10533
+ const cache = join14(root, fingerprint), app = join14(cache, "Electron.app");
10534
+ const executable = join14(app, "Contents/MacOS", runtime.split("/").at(-1));
10535
+ const marker = join14(cache, "ready.json");
10536
+ if (existsSync14(marker)) {
10537
+ const saved = JSON.parse(readFileSync12(marker, "utf8"));
10538
+ if (saved.fingerprint !== fingerprint || saved.source !== source)
10539
+ throw new Error("Claude's owned runtime cache has unexpected metadata.");
10540
+ execFileSync2("/usr/bin/codesign", ["--verify", app], { stdio: "pipe" });
10541
+ return executable;
10542
+ }
10543
+ const staging = mkdtempSync2(join14(root, ".preparing-"));
10544
+ try {
10545
+ const target = join14(staging, "Electron.app");
10546
+ const owned = cloneClaudeElectronRuntime(source, target);
10547
+ const entitlements = join14(staging, "entitlements.plist");
10548
+ writeFileSync4(entitlements, '<?xml version="1.0"?><plist version="1.0"><dict><key>com.apple.security.cs.allow-jit</key><true/><key>com.apple.security.cs.disable-library-validation</key><true/><key>com.apple.security.virtualization</key><true/></dict></plist>', { mode: 384 });
10549
+ for (const path of [...owned.slice(1), owned[0]]) {
10550
+ if (statSync2(path).nlink !== 1)
10551
+ throw new Error("Refusing to sign a shared Electron executable.");
10552
+ execFileSync2("/usr/bin/codesign", ["--force", "--sign", "-", "--entitlements", entitlements, path], { stdio: "pipe" });
10553
+ }
10554
+ execFileSync2("/usr/bin/codesign", ["--verify", target], { stdio: "pipe" });
10555
+ writeFileSync4(join14(staging, "ready.json"), JSON.stringify({ fingerprint, source, sourceApp }), { mode: 384 });
10556
+ renameSync2(staging, cache);
10557
+ return executable;
10558
+ } catch (error) {
10559
+ rmSync(staging, { recursive: true, force: true });
10560
+ throw new Error("Could not prepare Claude's lightweight runtime. The shared runtime and ClosedRouter config directory must be on the same filesystem, with codesign available.", { cause: error });
10561
+ }
10562
+ }
10563
+ function findClaudeSharedElectron(sourceApp, home = homedir13()) {
10564
+ const explicit = process.env.CLOSEDROUTER_CLAUDE_ELECTRON || process.env.CLAUDE_DESKTOP_AUTH_ELECTRON;
10565
+ if (explicit && existsSync14(explicit))
10566
+ return explicit;
10567
+ const info = JSON.parse(execFileSync2("/usr/bin/plutil", ["-convert", "json", "-o", "-", join14(sourceApp, "Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/Info.plist")], { encoding: "utf8" }));
10568
+ const major = info.CFBundleVersion.split(".")[0];
10569
+ const root = join14(home, ".antigravity_cockpit", "electron_runtime");
10570
+ if (!existsSync14(root))
10571
+ return;
10572
+ const versions = readdirSync2(root).filter((version) => version.startsWith(`${major}.`)).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
10573
+ return versions.map((version) => join14(root, version, `darwin-${process.arch}`, "Electron.app/Contents/MacOS/Electron")).find((path) => existsSync14(path));
10574
+ }
10575
+ function useClaudeSharedElectron(plan, runtime, sourceApp, toolArgs = [], launchServices = false) {
10576
+ const args = [join14(sourceApp, "Contents/Resources/app.asar"), `--user-data-dir=${join14(plan.profileDir, "user-data-3p")}`, ...toolArgs];
10577
+ plan.command = launchServices ? "/usr/bin/open" : runtime;
10578
+ plan.args = launchServices ? ["-W", "-n", "-a", dirname3(dirname3(dirname3(runtime))), "--args", ...args] : args;
10579
+ }
10580
+ function addClaudeDesktopHook(plan, baseUrl, localKey, sourceApp, models) {
10581
+ const path = join14(plan.profileDir, "claude-hook.cjs");
10582
+ plan.files.push({ path, contents: `${sourceApp ? `const electron = require('electron');
10583
+ Object.defineProperty(process, 'resourcesPath', {value: ${JSON.stringify(join14(sourceApp, "Contents/Resources"))}});
10584
+ Object.defineProperty(electron.app, 'isPackaged', {get: () => true});
10585
+ electron.app.setPath('userData', ${JSON.stringify(join14(plan.profileDir, "user-data-3p"))});
10586
+ process.env.CLAUDE_USER_DATA_DIR = ${JSON.stringify(join14(plan.profileDir, "user-data-3p"))};
10587
+ ` : ""}const cp = require('node:child_process');
10588
+ const spawn = cp.spawn;
10589
+ cp.spawn = function(command, args, options) {
10590
+ if (typeof command === 'string' && /(?:^|[\\\\/])claude(?:\\.exe)?$/.test(command)) {
10591
+ options = { ...options, env: { ...process.env, ...options?.env,
10592
+ ANTHROPIC_BASE_URL: ${JSON.stringify(baseUrl)}, ANTHROPIC_API_KEY: ${JSON.stringify(localKey)} } };
10593
+ delete options.env.ANTHROPIC_AUTH_TOKEN;
10594
+ }
10595
+ return spawn.call(this, command, args, options);
10596
+ };
10597
+ const { autoUpdater } = require('electron');
10598
+ if (autoUpdater) {
10599
+ autoUpdater.checkForUpdates = () => {};
10600
+ autoUpdater.quitAndInstall = () => {};
10601
+ }
10602
+ require('node:fs').writeFileSync(${JSON.stringify(join14(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
10603
+ ` + (models ? modelBrandingHook(models) : "") });
10604
+ const logPath = join14(plan.profileDir, "inspector.log");
10605
+ plan.inspectorHook = { path, logPath };
10606
+ plan.args.push("--inspect-brk=127.0.0.1:0");
10607
+ if (plan.command === "/usr/bin/open")
10608
+ plan.args.splice(plan.args.indexOf("--args"), 0, "--stderr", logPath);
10609
+ }
10610
+
10611
+ // src/commands/cursor-desktop-hook.ts
10612
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
10613
+ import { dirname as dirname4, join as join15 } from "node:path";
10614
+ var FLAGS = /([\w$]+)=\{extensionIsDev:[^{}]{1,1000}localMode:(?:!1|false)\}/;
10615
+ function addCursorDesktopLocalHook(plan, app, catalog) {
10616
+ const resources = app.endsWith(".app") ? join15(app, "Contents/Resources/app") : join15(dirname4(app), "resources/app");
10617
+ const source = join15(resources, "out/main.js");
10618
+ const mainSource = readFileSync13(source, "utf8");
10619
+ const mainMatch = FLAGS.exec(mainSource);
10620
+ if (!mainMatch)
10621
+ throw new Error("This Cursor Desktop build does not include the supported local agent runtime. Update Cursor or use cr start cursor with Cursor Agent Local.");
10622
+ const nativeKey = mainMatch[1];
10623
+ const redirects = {};
10624
+ for (const variant of ["desktop", "glass"]) {
10625
+ const renderer = join15(resources, `out/vs/workbench/workbench.${variant}.main.js`);
10626
+ if (!existsSync15(renderer) && variant === "glass")
10627
+ continue;
10628
+ const rendererSource = readFileSync13(renderer, "utf8");
10629
+ const rendererMatch = FLAGS.exec(rendererSource);
10630
+ if (!rendererMatch || !rendererSource.includes("runLocalAgentInExtensionHost"))
10631
+ throw new Error("This Cursor Desktop build does not include the supported local agent runtime. Update Cursor or use cr start cursor with Cursor Agent Local.");
10632
+ const authMethod = /isAuthenticated\(\)\{const ([\w$]+)=this\.accessToken\(\),([\w$]+)=this\.refreshToken\(\);return this\.scheduleTeamPolicyCheck\(\),!!\(\1&&\2\)\}/;
10633
+ if (!authMethod.test(rendererSource))
10634
+ throw new Error("This Cursor Desktop build has an unsupported login controller. Update the ClosedRouter launcher before using local provider mode.");
10635
+ let modifiedRenderer = rendererSource.replace(rendererMatch[0], rendererMatch[0].replace(/localMode:(?:!1|false)/, "localMode:!0")).replace(authMethod, `isAuthenticated(){const e=this.accessToken(),t=this.refreshToken();if(!e&&!t&&${rendererMatch[1]}.localMode&&this.byokProviderKeyStorage?.openAIKey()&&this._reactiveStorageService.applicationUserPersistentStorage.openAIBaseUrl)return true;return this.scheduleTeamPolicyCheck(),!!(e&&t)}`);
10636
+ if (catalog) {
10637
+ const nativeCatalog = "getAvailableDefaultModels(){return[...this._availableDefaultModels()]}";
10638
+ if (!modifiedRenderer.includes(nativeCatalog))
10639
+ throw new Error("This Cursor Desktop build has an unsupported model catalog controller. Update the ClosedRouter launcher.");
10640
+ const entries = catalog.models.map((id) => ({
10641
+ name: id,
10642
+ serverModelName: id,
10643
+ clientDisplayName: id,
10644
+ inputboxShortModelName: id,
10645
+ defaultOn: true,
10646
+ supportsAgent: true,
10647
+ supportsPlanMode: true,
10648
+ supportsNonMaxMode: true,
10649
+ supportsImages: false,
10650
+ supportsThinking: false,
10651
+ supportsCmdK: true,
10652
+ supportsSmartModeClassifier: true,
10653
+ parameterDefinitions: [],
10654
+ variants: [],
10655
+ legacySlugs: [],
10656
+ idAliases: [],
10657
+ namedModelSectionIndex: 0,
10658
+ tooltipData: { markdownContent: "Via your ClosedRouter pool" }
10659
+ }));
10660
+ modifiedRenderer = modifiedRenderer.replace(nativeCatalog, `getAvailableDefaultModels(){return ${JSON.stringify(entries)}}`);
10661
+ const display = /function ([\w$]+)\(([\w$]+),([\w$]+),([\w$]+)\)\{if\(\2\.includes\(","\)\)/;
10662
+ if (!display.test(modifiedRenderer))
10663
+ throw new Error("This Cursor Desktop build has an unsupported model-label formatter. Update the ClosedRouter launcher.");
10664
+ modifiedRenderer = modifiedRenderer.replace(display, (all, _fn, model) => all.replace("{if(", `{if(${JSON.stringify(catalog.models)}.includes(${model}))return 'ClosedRouter: '+${model};if(`));
10665
+ }
10666
+ const patched = join15(plan.profileDir, `cursor-local-workbench${variant === "desktop" ? "" : "-glass"}.js`);
10667
+ redirects[renderer] = patched;
10668
+ plan.files.push({ path: patched, contents: modifiedRenderer });
10669
+ }
10670
+ const hook = join15(plan.profileDir, "cursor-local-hook.cjs");
10671
+ const marker = join15(plan.profileDir, "main-local-mode-enabled");
10672
+ plan.files.push({ path: hook, contents: `const {app, session} = require('electron');
10673
+ const fs = require('node:fs');
10674
+ const redirects = ${JSON.stringify(redirects)};
10675
+ function installProtocolHook() {
10676
+ const protocol = session.defaultSession.protocol;
10677
+ const prototype = Object.getPrototypeOf(protocol);
10678
+ const register = prototype.registerFileProtocol;
10679
+ prototype.registerFileProtocol = function(scheme, handler, ...rest) {
10680
+ if (scheme !== 'vscode-file') return register.call(this, scheme, handler, ...rest);
10681
+ return register.call(this, scheme, (request, callback) => handler(request, result => {
10682
+ if (/workbench.(?:desktop|glass).main.js/.test(request.url)) fs.writeFileSync(${JSON.stringify(join15(plan.profileDir, "renderer-request.json"))}, JSON.stringify({type: typeof result, path: result?.path}), {mode: 0o600});
10683
+ if (result && typeof result === 'object' && Object.hasOwn(redirects, result.path)) {
10684
+ result = {...result, path: redirects[result.path]};
10685
+ fs.writeFileSync(${JSON.stringify(join15(plan.profileDir, "renderer-local-mode-served"))}, 'ready', {mode: 0o600});
10686
+ }
10687
+ callback(result);
10688
+ }), ...rest);
10689
+ };
10690
+ fs.writeFileSync(${JSON.stringify(join15(plan.profileDir, "protocol-hook-loaded"))}, 'ready', {mode: 0o600});
10691
+ }
10692
+ if (app.isReady()) installProtocolHook(); else app.once('ready', installProtocolHook);
10693
+ require('node:fs').writeFileSync(${JSON.stringify(join15(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
10694
+ ` + (catalog ? modelBrandingHook(catalog.models) : "") });
10695
+ const point = mainSource.slice(0, mainMatch.index + mainMatch[0].length).split(`
10696
+ `);
10697
+ const logPath = join15(plan.profileDir, "inspector.log");
10698
+ plan.inspectorHook = {
10699
+ path: hook,
10700
+ logPath,
10701
+ startupBreakpoint: {
10702
+ urlRegex: source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "$",
10703
+ lineNumber: point.length - 1,
10704
+ columnNumber: point.at(-1).length,
10705
+ condition: `typeof ${nativeKey} === 'object' && (${nativeKey}.localMode = true, process.getBuiltinModule('fs').writeFileSync(${JSON.stringify(marker)}, 'ready', {mode: 0o600}), false)`
10706
+ },
10707
+ readyPath: marker
10708
+ };
10709
+ plan.args.push("--inspect-brk=127.0.0.1:0");
10710
+ if (plan.command === "/usr/bin/open")
10711
+ plan.args.splice(plan.args.indexOf("--args"), 0, "--stderr", logPath);
10712
+ }
10713
+
10173
10714
  // src/commands/start.ts
10174
10715
  function isGatewayModelAvailable(availableModels, model) {
10175
10716
  return availableModels.includes(model);
@@ -10186,7 +10727,7 @@ var TOOL_OPTIONS = [
10186
10727
  var DESKTOP_TOOL_OPTIONS = [
10187
10728
  { value: "codex-desktop", label: "Codex Desktop", hint: "Responses API with an isolated app profile" },
10188
10729
  { value: "antigravity-desktop", label: "Antigravity Desktop", hint: "ClosedRouter routing through your antigravity-mods hook" },
10189
- { value: "cursor-desktop", label: "Cursor Desktop", hint: "Automatic API-key setup; requires a public gateway" },
10730
+ { value: "cursor-desktop", label: "Cursor Desktop", hint: "Local model engine using your ClosedRouter pool" },
10190
10731
  { value: "claude-desktop", label: "Claude Desktop", hint: "Inference gateway with an isolated app profile" }
10191
10732
  ];
10192
10733
  function getStartToolOptions(desktopOnly = false) {
@@ -10216,7 +10757,7 @@ function assertNoManagedCodexArgs(args) {
10216
10757
  throw new Error(`Remove ${conflicting}; ClosedRouter manages the Codex profile and model. Put other arguments after --.`);
10217
10758
  }
10218
10759
  }
10219
- function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir13(), executablePath, agyRouting) {
10760
+ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir14(), executablePath, agyRouting, catalog) {
10220
10761
  const baseUrl = normalizeServerUrl(serverUrl);
10221
10762
  const env = { ...process.env };
10222
10763
  const command = executablePath || tool;
@@ -10236,18 +10777,20 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10236
10777
  return {
10237
10778
  tool,
10238
10779
  command,
10239
- args: ["--model", model, ...toolArgs],
10780
+ args: ["--model", model, ...catalog ? ["--settings", JSON.stringify({ modelPicker: claudeModelPicker(catalog.models) })] : [], ...toolArgs],
10240
10781
  env
10241
10782
  };
10242
10783
  }
10243
10784
  if (tool === "codex") {
10244
10785
  assertNoManagedCodexArgs(toolArgs);
10245
- const codexHome = process.env.CODEX_HOME || join14(homeDir, ".codex");
10786
+ const codexHome = process.env.CODEX_HOME || join16(homeDir, ".codex");
10246
10787
  const profileName = "closedrouter-launch";
10247
- const configPath = join14(codexHome, `${profileName}.config.toml`);
10788
+ const configPath = join16(codexHome, `${profileName}.config.toml`);
10789
+ const catalogPath = join16(codexHome, "closedrouter-models.json");
10248
10790
  const configLines = [
10249
10791
  `model = ${tomlString(model)}`,
10250
10792
  `model_provider = ${tomlString(profileName)}`,
10793
+ ...catalog ? [`model_catalog_json = ${tomlString(catalogPath)}`] : [],
10251
10794
  "",
10252
10795
  `[model_providers.${profileName}]`,
10253
10796
  `name = ${tomlString("ClosedRouter")}`,
@@ -10263,14 +10806,15 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10263
10806
  args: ["--profile", profileName, ...toolArgs],
10264
10807
  env,
10265
10808
  configFile: { path: configPath, contents: configLines.join(`
10266
- `) }
10809
+ `) },
10810
+ ...catalog ? { extraFiles: [{ path: catalogPath, contents: JSON.stringify(codexModelCatalog(catalog.models)) }] } : {}
10267
10811
  };
10268
10812
  }
10269
10813
  if (tool === "gemini") {
10270
10814
  env.GEMINI_API_KEY = apiKey;
10271
10815
  env.GEMINI_MODEL = model;
10272
10816
  env.GOOGLE_GEMINI_BASE_URL = baseUrl;
10273
- const configPath = join14(homeDir, ".closedrouter", "gemini-launch.settings.json");
10817
+ const configPath = join16(homeDir, ".closedrouter", "gemini-launch.settings.json");
10274
10818
  env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = configPath;
10275
10819
  return {
10276
10820
  tool,
@@ -10279,7 +10823,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10279
10823
  env,
10280
10824
  configFile: {
10281
10825
  path: configPath,
10282
- contents: `${JSON.stringify({ security: { auth: { selectedType: "gemini-api-key" } } }, null, 2)}
10826
+ contents: `${JSON.stringify({ security: { auth: { selectedType: "gemini-api-key" } }, ...catalog ? geminiModelCatalog(catalog.models, model) : {} }, null, 2)}
10283
10827
  `
10284
10828
  }
10285
10829
  };
@@ -10305,8 +10849,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10305
10849
  return {
10306
10850
  tool,
10307
10851
  command,
10308
- args: ["--model", `openai/${model}`, ...toolArgs],
10309
- env
10852
+ args: ["--model", `openai/${catalog ? "closedrouter/" : ""}${model}`, ...catalog ? ["--model-metadata-file", join16(homeDir, ".closedrouter", "aider-models.json")] : [], ...toolArgs],
10853
+ env,
10854
+ ...catalog ? { configFile: { path: join16(homeDir, ".closedrouter", "aider-models.json"), contents: JSON.stringify(Object.fromEntries(catalog.models.map((id) => [`openai/closedrouter/${id}`, { litellm_provider: "openai", mode: "chat", max_input_tokens: 8192, max_output_tokens: 4096, supports_function_calling: true }]))) } } : {}
10310
10855
  };
10311
10856
  }
10312
10857
  const config = {
@@ -10319,7 +10864,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10319
10864
  baseURL: `${baseUrl}/v1`,
10320
10865
  apiKey: "{env:CLOSEDROUTER_API_KEY}"
10321
10866
  },
10322
- models: { [model]: { name: model } }
10867
+ models: Object.fromEntries((catalog?.models || [model]).map((id) => [id, { name: modelLabel(id) }]))
10323
10868
  }
10324
10869
  }
10325
10870
  };
@@ -10333,9 +10878,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10333
10878
  };
10334
10879
  }
10335
10880
  function writeOwnedConfig(file) {
10336
- mkdirSync4(dirname3(file.path), { recursive: true, mode: 448 });
10881
+ mkdirSync5(dirname5(file.path), { recursive: true, mode: 448 });
10337
10882
  try {
10338
- const existing = readFileSync12(file.path, "utf8");
10883
+ const existing = readFileSync14(file.path, "utf8");
10339
10884
  if (file.path.endsWith(".config.toml") && !existing.includes('name = "ClosedRouter"')) {
10340
10885
  throw new Error(`Refusing to replace an existing Codex profile at ${file.path}.`);
10341
10886
  }
@@ -10343,14 +10888,14 @@ function writeOwnedConfig(file) {
10343
10888
  if (error instanceof Error && error.message.startsWith("Refusing to replace"))
10344
10889
  throw error;
10345
10890
  }
10346
- writeFileSync4(file.path, file.contents, { encoding: "utf8", mode: 384 });
10891
+ writeFileSync5(file.path, file.contents, { encoding: "utf8", mode: 384 });
10347
10892
  }
10348
10893
  function modelSupportsHarness(model, tool) {
10349
10894
  const parameters = model.supported_parameters;
10350
10895
  return (model.status === undefined || model.status === "available") && (model.available_tool_account_count === undefined || model.available_tool_account_count > 0) && (!Array.isArray(parameters) || parameters.includes("tools"));
10351
10896
  }
10352
10897
  function discoverAgyTransportModel(command, apiKey) {
10353
- const output = execFileSync2(command, ["models"], {
10898
+ const output = execFileSync3(command, ["models"], {
10354
10899
  encoding: "utf8",
10355
10900
  timeout: 1e4,
10356
10901
  stdio: ["ignore", "pipe", "ignore"],
@@ -10363,7 +10908,7 @@ function discoverAgyTransportModel(command, apiKey) {
10363
10908
  }
10364
10909
  function assertAgyCustomModel(command, label, apiKey) {
10365
10910
  try {
10366
- const output = execFileSync2(command, ["models"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, GEMINI_API_KEY: apiKey, GOOGLE_GEMINI_BASE_URL: "http://127.0.0.1:1" } });
10911
+ const output = execFileSync3(command, ["models"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, GEMINI_API_KEY: apiKey, GOOGLE_GEMINI_BASE_URL: "http://127.0.0.1:1" } });
10367
10912
  if (output.split(/\r?\n/).some((line) => line.split("\t")[0] === label))
10368
10913
  return;
10369
10914
  } catch {
@@ -10372,9 +10917,9 @@ function assertAgyCustomModel(command, label, apiKey) {
10372
10917
  throw new Error(`This version of agy did not recognize '${label}'. Update agy to use ClosedRouter's accurate model display.`);
10373
10918
  }
10374
10919
  function assertAgyGeminiMode(homeDir) {
10375
- const settingsPath = join14(homeDir, ".gemini", "antigravity-cli", "settings.json");
10920
+ const settingsPath = join16(homeDir, ".gemini", "antigravity-cli", "settings.json");
10376
10921
  try {
10377
- const settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
10922
+ const settings = JSON.parse(readFileSync14(settingsPath, "utf8"));
10378
10923
  if (settings?.modelProvider === "gemini")
10379
10924
  return;
10380
10925
  } catch {}
@@ -10416,13 +10961,17 @@ async function runTool(plan) {
10416
10961
  async function startCommand(options = {}) {
10417
10962
  const groupingOverride = options.groupBy === undefined ? undefined : parseModelGrouping(options.groupBy);
10418
10963
  const { tool, desktop } = await selectTool(options.tool, options.desktop);
10964
+ if (options.desktopInspector && (!desktop || tool !== "claude"))
10965
+ throw new Error("--desktop-inspector is supported for Claude Desktop only.");
10419
10966
  if (options.openOnly && !desktop)
10420
10967
  throw new Error("--open-only requires --desktop.");
10421
10968
  if (desktop && !isDesktopTool(tool))
10422
10969
  throw new Error(`Desktop launch is not supported for ${tool}. Choose codex, claude, cursor, or antigravity.`);
10423
10970
  if (desktop)
10424
10971
  validateDesktopArgs(options.toolArgs || []);
10425
- const executablePath = desktop && isDesktopTool(tool) ? resolveDesktopExecutable(tool, options.executablePath) : tool === "cursor" ? resolveCursorLocalExecutable(options.executablePath) : options.executablePath;
10972
+ let executablePath = desktop && isDesktopTool(tool) ? resolveDesktopExecutable(tool, options.executablePath) : tool === "cursor" ? resolveCursorLocalExecutable(options.executablePath) : options.executablePath;
10973
+ const sourceApp = executablePath;
10974
+ let sharedClaudeElectron;
10426
10975
  if (desktop && isDesktopTool(tool) && options.openOnly) {
10427
10976
  if (options.model || options.key || options.server)
10428
10977
  throw new Error("Open-only desktop launches do not use --model, --key, or --server.");
@@ -10432,8 +10981,17 @@ async function startCommand(options = {}) {
10432
10981
  process.exitCode = await runDesktopLaunch(plan);
10433
10982
  return;
10434
10983
  }
10435
- if (desktop && tool === "claude")
10984
+ if (desktop && tool === "claude") {
10436
10985
  assertClaudeDesktopGatewaySupport(executablePath);
10986
+ if (process.platform === "darwin" && executablePath.endsWith(".app")) {
10987
+ sharedClaudeElectron = findClaudeSharedElectron(executablePath);
10988
+ if (sharedClaudeElectron) {
10989
+ sharedClaudeElectron = prepareClaudeElectronRuntime(sharedClaudeElectron, executablePath);
10990
+ M2.info("Using the lightweight Claude launcher with Cowork virtualization support. Existing framework files are shared.");
10991
+ } else if (options.desktopInspector)
10992
+ throw new Error("No compatible shared Electron runtime was found. Set CLOSEDROUTER_CLAUDE_ELECTRON to an installed Electron executable or omit --desktop-inspector to use Claude's native gateway mode.");
10993
+ }
10994
+ }
10437
10995
  if (desktop && tool === "agy") {
10438
10996
  assertAntigravityDesktopHookSupport(executablePath);
10439
10997
  await assertAntigravityModsAvailable();
@@ -10444,7 +11002,7 @@ async function startCommand(options = {}) {
10444
11002
  const apiKey = await ensurePlatformKey(tool, client, options);
10445
11003
  client.setApiKey(apiKey);
10446
11004
  const models = await client.listGatewayModels({ preferApiKey: true });
10447
- const eligibleModels = models.filter((item) => modelSupportsHarness(item, tool) && (!desktop || tool !== "cursor" || cursorDesktopSupportsModel(gatewayModelId(item))));
11005
+ const eligibleModels = models.filter((item) => modelSupportsHarness(item, tool));
10448
11006
  const availableModels = [...new Set(eligibleModels.map(gatewayModelId).filter(Boolean))].sort();
10449
11007
  let model = options.model?.trim();
10450
11008
  if (!model) {
@@ -10469,24 +11027,36 @@ async function startCommand(options = {}) {
10469
11027
  if (!isGatewayModelAvailable(availableModels, model)) {
10470
11028
  throw new Error(`Model '${model}' is not available for ${tool} with this platform key. Agent clients require a provider that supports gateway function tools. Choose another model from ClosedRouter's model catalog.`);
10471
11029
  }
11030
+ const catalog = createLaunchCatalog(availableModels, model);
10472
11031
  if (desktop && tool === "cursor") {
10473
- const plan = buildDesktopLaunchPlan("cursor", executablePath, createDesktopProfile("cursor"), undefined, undefined, undefined, options.toolArgs);
10474
- writeDesktopLaunchPlan(plan);
10475
- await configureCursorDesktop(plan.profileDir, serverUrl, apiKey, model);
10476
- M2.info(`Starting Cursor desktop with ${model}. API key, gateway URL, and model selection are configured. Profile: ${plan.profileDir}.`);
10477
- M2.warn("Cursor's standard editor sends BYOK requests through Cursor's servers, so the gateway URL must be publicly reachable. Cursor account and feature restrictions still apply.");
10478
- process.exitCode = await runDesktopLaunch(plan);
11032
+ const localKey = randomUUID5();
11033
+ const relay = await startLaunchProxy(serverUrl, apiKey, "cursor-desktop", localKey, catalog);
11034
+ try {
11035
+ const plan = buildDesktopLaunchPlan("cursor", executablePath, createDesktopProfile("cursor"), undefined, undefined, undefined, options.toolArgs);
11036
+ addCursorDesktopLocalHook(plan, executablePath, catalog);
11037
+ writeDesktopLaunchPlan(plan);
11038
+ await configureCursorDesktop(plan.profileDir, relay.baseUrl, localKey, model, undefined, catalog.models);
11039
+ M2.info(`Starting Cursor desktop local provider mode with ${model}. Model requests use ClosedRouter, without a paid Cursor subscription. Profile: ${plan.profileDir}. Keep cr running until you close this app instance.`);
11040
+ process.exitCode = await runDesktopLaunch(plan);
11041
+ } finally {
11042
+ await relay.close();
11043
+ }
10479
11044
  return;
10480
11045
  }
10481
11046
  if (desktop && (tool === "codex" || tool === "claude" || tool === "agy")) {
10482
11047
  const localKey = randomUUID5();
10483
- const relay = await startLaunchProxy(serverUrl, apiKey, `${tool}-desktop`, localKey);
11048
+ const relay = await startLaunchProxy(serverUrl, apiKey, `${tool}-desktop`, localKey, catalog);
10484
11049
  let agyDesktopProxy;
10485
11050
  try {
10486
- const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), relay.baseUrl, localKey, model, options.toolArgs);
11051
+ const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), relay.baseUrl, localKey, model, options.toolArgs, process.platform, catalog.models);
11052
+ if (sharedClaudeElectron && tool === "claude") {
11053
+ if (sharedClaudeElectron)
11054
+ useClaudeSharedElectron(plan, sharedClaudeElectron, sourceApp, options.toolArgs, true);
11055
+ addClaudeDesktopHook(plan, relay.baseUrl, localKey, sourceApp, catalog.models);
11056
+ }
10487
11057
  if (tool === "agy") {
10488
- agyDesktopProxy = await startAgyDesktopProxy(relay.baseUrl, localKey, model);
10489
- addAntigravityDesktopHook(plan, agyDesktopProxy.baseUrl);
11058
+ agyDesktopProxy = await startAgyDesktopProxy(relay.baseUrl, localKey, model, undefined, catalog);
11059
+ addAntigravityDesktopHook(plan, agyDesktopProxy.baseUrl, catalog);
10490
11060
  }
10491
11061
  writeDesktopLaunchPlan(plan);
10492
11062
  M2.info(`Starting ${tool} desktop with ${model} through ${serverUrl}. Profile: ${plan.profileDir}. Keep cr running until you close this app instance.`);
@@ -10498,17 +11068,19 @@ async function startCommand(options = {}) {
10498
11068
  return;
10499
11069
  }
10500
11070
  if (tool === "agy")
10501
- assertAgyGeminiMode(homedir13());
10502
- const recognizedModel = tool === "agy" ? registerAgyLaunchModel(homedir13(), model, discoverAgyTransportModel(options.executablePath || "agy", apiKey)) : undefined;
11071
+ assertAgyGeminiMode(homedir14());
11072
+ const recognizedModel = tool === "agy" ? registerAgyLaunchCatalog(homedir14(), catalog, discoverAgyTransportModel(options.executablePath || "agy", apiKey)) : undefined;
10503
11073
  if (recognizedModel)
10504
11074
  assertAgyCustomModel(options.executablePath || "agy", recognizedModel, apiKey);
10505
- const relay = await startLaunchProxy(serverUrl, apiKey, tool);
11075
+ const relay = await startLaunchProxy(serverUrl, apiKey, tool, apiKey, catalog);
10506
11076
  let agyProxy;
10507
11077
  try {
10508
- agyProxy = tool === "agy" ? await startAgyModelProxy(relay.baseUrl, apiKey, model) : undefined;
10509
- const plan = buildLaunchPlan(tool, relay.baseUrl, apiKey, model, options.toolArgs, homedir13(), executablePath, agyProxy && recognizedModel ? { baseUrl: agyProxy.baseUrl, recognizedModel } : undefined);
11078
+ agyProxy = tool === "agy" ? await startAgyModelProxy(relay.baseUrl, apiKey, model, catalog) : undefined;
11079
+ const plan = buildLaunchPlan(tool, relay.baseUrl, apiKey, model, options.toolArgs, homedir14(), executablePath, agyProxy && recognizedModel ? { baseUrl: agyProxy.baseUrl, recognizedModel } : undefined, catalog);
10510
11080
  if (plan.configFile)
10511
11081
  writeOwnedConfig(plan.configFile);
11082
+ for (const file of plan.extraFiles || [])
11083
+ writeOwnedConfig(file);
10512
11084
  M2.info(`${import_picocolors12.default.green("Starting")} ${import_picocolors12.default.bold(TOOL_OPTIONS.find((option) => option.value === tool)?.label || tool)} with ${import_picocolors12.default.cyan(model)} through ${import_picocolors12.default.dim(serverUrl)}`);
10513
11085
  const exitCode = await runTool(plan);
10514
11086
  process.exitCode = exitCode;
@@ -10563,7 +11135,7 @@ process.on("warning", (warning) => {
10563
11135
  console.warn(warning.name, warning.message);
10564
11136
  });
10565
11137
  var program2 = new Command2;
10566
- var cliVersion = JSON.parse(readFileSync13(new URL("../package.json", import.meta.url), "utf8")).version;
11138
+ var cliVersion = JSON.parse(readFileSync15(new URL("../package.json", import.meta.url), "utf8")).version;
10567
11139
  program2.name("closedrouter").description("ClosedRouter CLI: Import local AI subscriptions & API keys into your shared token pool").version(cliVersion).option("-s, --server <url>", "ClosedRouter server base URL (default: http://localhost:8000)").action(async (options) => {
10568
11140
  await onboardCommand({ server: options.server });
10569
11141
  });
@@ -10614,7 +11186,7 @@ program2.command("gateway").alias("daemon").alias("bridge").description("Start t
10614
11186
  program2.command("settings").description("Configure the CLI model selector").option("--model-grouping <grouping>", "Group models by model-provider, account-provider, or none").action(async (options) => {
10615
11187
  await settingsCommand({ modelGrouping: options.modelGrouping });
10616
11188
  });
10617
- program2.command("start [tool] [tool-args...]").alias("launch").description("Configure and start a coding CLI or desktop app through ClosedRouter").option("--desktop", "Launch the desktop app for codex, claude, cursor, or antigravity").option("--open-only", "Open an isolated desktop app without gateway configuration").option("-s, --server <url>", "ClosedRouter server base URL").option("-k, --key <key>", "Platform API key; otherwise use the saved CLI key").option("-m, --model <model>", "Gateway model to use").option("--group-by <grouping>", "Override model grouping for this launch: model-provider, account-provider, or none").option("--path <path>", "Path to the installed CLI, desktop executable, or macOS app bundle").allowUnknownOption().action(async (tool, toolArgs, options) => {
11189
+ program2.command("start [tool] [tool-args...]").alias("launch").description("Configure and start a coding CLI or desktop app through ClosedRouter").option("--desktop", "Launch the desktop app for codex, claude, cursor, or antigravity").option("--open-only", "Open an isolated desktop app without gateway configuration").option("--desktop-inspector", "Enable Claude's startup hook using an installed shared Electron runtime").option("-s, --server <url>", "ClosedRouter server base URL").option("-k, --key <key>", "Platform API key; otherwise use the saved CLI key").option("-m, --model <model>", "Gateway model to use").option("--group-by <grouping>", "Override model grouping for this launch: model-provider, account-provider, or none").option("--path <path>", "Path to the installed CLI, desktop executable, or macOS app bundle").allowUnknownOption().action(async (tool, toolArgs, options) => {
10618
11190
  await startCommand({
10619
11191
  tool,
10620
11192
  toolArgs: toolArgs || [],
@@ -10624,7 +11196,8 @@ program2.command("start [tool] [tool-args...]").alias("launch").description("Con
10624
11196
  groupBy: options.groupBy,
10625
11197
  executablePath: options.path,
10626
11198
  desktop: options.desktop,
10627
- openOnly: options.openOnly
11199
+ openOnly: options.openOnly,
11200
+ desktopInspector: options.desktopInspector
10628
11201
  });
10629
11202
  });
10630
11203
  program2.parseAsync(process.argv).catch((error) => {