@closedrouter/cli 1.1.10 → 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 readFileSync14 } 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";
@@ -8659,9 +8659,9 @@ 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
8661
  import { execFileSync as execFileSync3 } from "node:child_process";
8662
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "node:fs";
8662
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "node:fs";
8663
8663
  import { homedir as homedir14 } from "node:os";
8664
- import { dirname as dirname4, join as join16 } from "node:path";
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,6 +9418,112 @@ 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
9528
  async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey, modelOverride) {
9423
9529
  const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
@@ -9440,8 +9546,15 @@ 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
+ }
9443
9555
  let rewrittenBody;
9444
- if (modelOverride && incoming.method === "POST" && /^\/v1\/(?:messages(?:\/count_tokens)?|chat\/completions|responses)$/.test(local.pathname)) {
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))) {
9445
9558
  try {
9446
9559
  const chunks = [];
9447
9560
  let length = 0;
@@ -9459,8 +9572,18 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey,
9459
9572
  return;
9460
9573
  }
9461
9574
  const payload = body;
9462
- const tinyProbe = local.pathname === "/v1/messages" && !modelOverride.startsWith("claude-") && payload.model !== modelOverride && 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);
9463
- rewrittenBody = JSON.stringify({ ...payload, model: modelOverride, ...tinyProbe ? { max_tokens: 512 } : {} });
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 } : {} });
9464
9587
  } catch {
9465
9588
  outgoing.writeHead(400).end();
9466
9589
  return;
@@ -9529,7 +9652,7 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey,
9529
9652
  import { createServer as createServer3 } from "node:http";
9530
9653
  import { request as httpRequest2 } from "node:http";
9531
9654
  import { request as httpsRequest2 } from "node:https";
9532
- async function startAgyModelProxy(gatewayUrl, apiKey, model) {
9655
+ async function startAgyModelProxy(gatewayUrl, apiKey, model, catalog) {
9533
9656
  const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
9534
9657
  if (!["https:", "http:"].includes(upstream.protocol)) {
9535
9658
  throw new Error("ClosedRouter server URL must use HTTP or HTTPS.");
@@ -9541,12 +9664,17 @@ async function startAgyModelProxy(gatewayUrl, apiKey, model) {
9541
9664
  return;
9542
9665
  }
9543
9666
  const requestUrl = new URL(incoming.url || "/", "http://localhost");
9544
- const match = /^\/v1beta\/models\/[^/:]+:(generateContent|streamGenerateContent)$/.exec(requestUrl.pathname);
9667
+ const match = /^\/v1beta\/models\/([^/:]+):(generateContent|streamGenerateContent)$/.exec(requestUrl.pathname);
9545
9668
  if (incoming.method !== "POST" || !match) {
9546
9669
  outgoing.writeHead(404).end();
9547
9670
  return;
9548
9671
  }
9549
- 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);
9550
9678
  const headers = { ...incoming.headers, host: target.host, "x-goog-api-key": apiKey };
9551
9679
  const upstreamRequest = requestUpstream(target, { method: "POST", headers }, (response) => {
9552
9680
  outgoing.writeHead(response.statusCode || 502, response.headers);
@@ -9586,28 +9714,26 @@ import { join as join9 } from "node:path";
9586
9714
  function record(value) {
9587
9715
  return typeof value === "object" && value !== null && !Array.isArray(value);
9588
9716
  }
9589
- function registerAgyLaunchModel(homeDir, model, transportModel) {
9717
+ function registerAgyLaunchCatalog(homeDir, catalog, legacyTransport) {
9590
9718
  const path = join9(homeDir, ".gemini", "antigravity-cli", "settings.json");
9591
9719
  const settings = JSON.parse(readFileSync8(path, "utf8"));
9592
- if (!record(settings) || settings.modelProvider !== "gemini") {
9720
+ if (!record(settings) || settings.modelProvider !== "gemini")
9593
9721
  throw new Error(`Set "modelProvider": "gemini" in ${path} before starting Antigravity through ClosedRouter.`);
9594
- }
9595
9722
  const config = settings.customModelsConfig ?? {};
9596
9723
  if (!record(config))
9597
9724
  throw new Error(`customModelsConfig must be an object in ${path}.`);
9598
- const models = config.customModels ?? {};
9599
- if (!record(models))
9725
+ const previous = config.customModels ?? {};
9726
+ if (!record(previous))
9600
9727
  throw new Error(`customModelsConfig.customModels must be an object in ${path}.`);
9601
- const label = `ClosedRouter: ${model}`;
9602
- const entry = { modelName: transportModel, apiProvider: "gemini" };
9603
- const existing = models[label];
9604
- if (existing !== undefined) {
9605
- 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)))) {
9606
9732
  throw new Error(`The custom model '${label}' already has different settings in ${path}. Rename that entry before launching it with ClosedRouter.`);
9607
9733
  }
9608
- return label;
9734
+ models[label] = { modelName: name, apiProvider: "gemini" };
9609
9735
  }
9610
- settings.customModelsConfig = { ...config, customModels: { ...models, [label]: entry } };
9736
+ settings.customModelsConfig = { ...config, customModels: models };
9611
9737
  const temporary = `${path}.closedrouter-${randomUUID2()}.tmp`;
9612
9738
  try {
9613
9739
  writeFileSync2(temporary, `${JSON.stringify(settings, null, 2)}
@@ -9617,7 +9743,7 @@ function registerAgyLaunchModel(homeDir, model, transportModel) {
9617
9743
  if (existsSync9(temporary))
9618
9744
  unlinkSync(temporary);
9619
9745
  }
9620
- return label;
9746
+ return modelLabel(catalog.defaultModel);
9621
9747
  }
9622
9748
 
9623
9749
  // src/commands/cursor-launch.ts
@@ -9815,7 +9941,7 @@ function createDesktopProfile(tool) {
9815
9941
  mkdirSync2(root, { recursive: true, mode: 448 });
9816
9942
  return mkdtempSync(join11(root, "session-"));
9817
9943
  }
9818
- 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] : []) {
9819
9945
  validateDesktopArgs(toolArgs);
9820
9946
  const env = { ...process.env };
9821
9947
  delete env.ELECTRON_RUN_AS_NODE;
@@ -9835,6 +9961,8 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
9835
9961
  `model = ${JSON.stringify(model)}`,
9836
9962
  'model_provider = "closedrouter"',
9837
9963
  "",
9964
+ `model_catalog_json = ${JSON.stringify(join11(codexHome, "closedrouter-models.json"))}`,
9965
+ "",
9838
9966
  "[model_providers.closedrouter]",
9839
9967
  'name = "ClosedRouter"',
9840
9968
  `base_url = ${JSON.stringify(`${baseUrl.replace(/\/+$/, "")}/v1`)}`,
@@ -9844,11 +9972,11 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
9844
9972
  ].join(`
9845
9973
  `) });
9846
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])])) });
9847
9976
  }
9848
9977
  if (tool === "claude" && baseUrl && localKey && model) {
9849
9978
  userData = join11(profileDir, "user-data-3p");
9850
9979
  const id = randomUUID3();
9851
- const transportModel = model.startsWith("claude-") || model.startsWith("anthropic/claude-") ? model : "claude-sonnet-4-6";
9852
9980
  const json = (value) => `${JSON.stringify(value, null, 2)}
9853
9981
  `;
9854
9982
  files.push({ path: join11(userData, "claude_desktop_config.json"), contents: json({ deploymentMode: "3p" }) });
@@ -9859,7 +9987,7 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
9859
9987
  inferenceGatewayApiKey: localKey,
9860
9988
  inferenceGatewayAuthScheme: "bearer",
9861
9989
  disableDeploymentModeChooser: true,
9862
- inferenceModels: [{ name: transportModel, labelOverride: `ClosedRouter: ${model}` }]
9990
+ inferenceModels: [...new Set([model, ...models])].map((id) => ({ name: transportModelId(id, "claude"), labelOverride: modelLabel(id) }))
9863
9991
  }) });
9864
9992
  }
9865
9993
  Object.assign(env, appEnv);
@@ -9970,8 +10098,9 @@ function defaultCursorDatabase() {
9970
10098
  const root = process.platform === "darwin" ? join12(homedir11(), "Library", "Application Support") : process.platform === "win32" ? process.env.APPDATA || join12(homedir11(), "AppData", "Roaming") : join12(homedir11(), ".config");
9971
10099
  return join12(root, "Cursor", "User", "globalStorage", "state.vscdb");
9972
10100
  }
9973
- async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, sourceDb = process.env.CLOSEDROUTER_CURSOR_AUTH_DB ?? defaultCursorDatabase()) {
10101
+ async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, sourceDb = process.env.CLOSEDROUTER_CURSOR_AUTH_DB ?? defaultCursorDatabase(), models = [model]) {
9974
10102
  const cursorModel = model;
10103
+ const catalog = [...new Set([model, ...models])];
9975
10104
  const dbPath = join12(profileDir, "user-data", "User", "globalStorage", "state.vscdb");
9976
10105
  mkdirSync3(dirname2(dbPath), { recursive: true, mode: 448 });
9977
10106
  const items = new Map;
@@ -9992,14 +10121,14 @@ async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, source
9992
10121
  items.set(CURSOR_APPLICATION_STORAGE, JSON.stringify({
9993
10122
  useOpenAIKey: true,
9994
10123
  openAIBaseUrl: `${baseUrl.replace(/\/+$/, "")}/v1`,
9995
- availableAPIKeyModels: [cursorModel],
10124
+ availableAPIKeyModels: catalog,
9996
10125
  havePerformedSettingsServiceMigration: true,
9997
- localProviderModelIds: [cursorModel],
9998
- localProviderAgentModelIds: [cursorModel],
10126
+ localProviderModelIds: catalog,
10127
+ localProviderAgentModelIds: catalog,
9999
10128
  ...membershipType ? { membershipType } : {},
10000
10129
  aiSettings: {
10001
- userAddedModels: [cursorModel],
10002
- modelOverrideEnabled: [cursorModel],
10130
+ userAddedModels: catalog,
10131
+ modelOverrideEnabled: catalog,
10003
10132
  modelOverrideDisabled: [],
10004
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: [] }] }]))
10005
10134
  }
@@ -10024,6 +10153,129 @@ import { randomUUID as randomUUID4 } from "node:crypto";
10024
10153
  import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
10025
10154
  import { homedir as homedir12 } from "node:os";
10026
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
10027
10279
  var MODS_CONTROL_URL = "http://127.0.0.1:59998";
10028
10280
  var MODS_PROXY_URL = "http://127.0.0.1:59997";
10029
10281
  async function assertAntigravityModsAvailable() {
@@ -10060,7 +10312,7 @@ function assertAntigravityDesktopHookSupport(executable) {
10060
10312
  if (index < 0 || binary[index + 32 + 5] !== 49)
10061
10313
  throw new Error("This Antigravity build disables launch inspector hooks. Use --open-only.");
10062
10314
  }
10063
- async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_PROXY_URL) {
10315
+ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_PROXY_URL, catalog) {
10064
10316
  const capability = randomUUID4();
10065
10317
  const controllers = new Set;
10066
10318
  const server = createServer4(async (incoming, outgoing) => {
@@ -10099,9 +10351,15 @@ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_
10099
10351
  return;
10100
10352
  }
10101
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
+ }
10102
10360
  const request = body.request || body;
10103
10361
  payload = Buffer.from(JSON.stringify(request));
10104
- 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" : ""}`;
10105
10363
  headers.authorization = `Bearer ${localKey}`;
10106
10364
  headers["content-type"] = "application/json";
10107
10365
  } else {
@@ -10182,8 +10440,9 @@ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_
10182
10440
  })
10183
10441
  };
10184
10442
  }
10185
- function addAntigravityDesktopHook(plan, endpoint) {
10443
+ function addAntigravityDesktopHook(plan, endpoint, catalog) {
10186
10444
  const path = join13(plan.profileDir, "antigravity-hook.cjs");
10445
+ const marker = join13(plan.profileDir, "model-catalog-ready");
10187
10446
  const contents = `const cp = require('node:child_process');
10188
10447
  const spawn = cp.spawn;
10189
10448
  cp.spawn = function(command, args, options) {
@@ -10205,10 +10464,10 @@ cp.spawn = function(command, args, options) {
10205
10464
  return spawn.call(this, command, args, options);
10206
10465
  };
10207
10466
  require('node:fs').writeFileSync(${JSON.stringify(join13(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
10208
- `;
10467
+ ` + (catalog ? antigravityCatalogHook(catalog, marker) + modelBrandingHook(catalog.models) : "");
10209
10468
  plan.files.push({ path, contents });
10210
10469
  const logPath = join13(plan.profileDir, "inspector.log");
10211
- plan.inspectorHook = { path, logPath };
10470
+ plan.inspectorHook = { path, logPath, ...catalog ? { readyPath: marker } : {} };
10212
10471
  plan.args.push("--inspect-brk=127.0.0.1:0");
10213
10472
  if (plan.command === "/usr/bin/open") {
10214
10473
  const index = plan.args.indexOf("--args");
@@ -10218,9 +10477,89 @@ require('node:fs').writeFileSync(${JSON.stringify(join13(plan.profileDir, "hook-
10218
10477
 
10219
10478
  // src/commands/claude-desktop.ts
10220
10479
  import { execFileSync as execFileSync2 } from "node:child_process";
10221
- import { existsSync as existsSync14, readdirSync as readdirSync2 } from "node:fs";
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";
10222
10482
  import { homedir as homedir13 } from "node:os";
10223
- import { join as join14 } from "node:path";
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
+ }
10224
10563
  function findClaudeSharedElectron(sourceApp, home = homedir13()) {
10225
10564
  const explicit = process.env.CLOSEDROUTER_CLAUDE_ELECTRON || process.env.CLAUDE_DESKTOP_AUTH_ELECTRON;
10226
10565
  if (explicit && existsSync14(explicit))
@@ -10233,11 +10572,12 @@ function findClaudeSharedElectron(sourceApp, home = homedir13()) {
10233
10572
  const versions = readdirSync2(root).filter((version) => version.startsWith(`${major}.`)).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
10234
10573
  return versions.map((version) => join14(root, version, `darwin-${process.arch}`, "Electron.app/Contents/MacOS/Electron")).find((path) => existsSync14(path));
10235
10574
  }
10236
- function useClaudeSharedElectron(plan, runtime, sourceApp, toolArgs = []) {
10237
- plan.command = runtime;
10238
- plan.args = [join14(sourceApp, "Contents/Resources/app.asar"), `--user-data-dir=${join14(plan.profileDir, "user-data-3p")}`, ...toolArgs];
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;
10239
10579
  }
10240
- function addClaudeDesktopHook(plan, baseUrl, localKey, sourceApp) {
10580
+ function addClaudeDesktopHook(plan, baseUrl, localKey, sourceApp, models) {
10241
10581
  const path = join14(plan.profileDir, "claude-hook.cjs");
10242
10582
  plan.files.push({ path, contents: `${sourceApp ? `const electron = require('electron');
10243
10583
  Object.defineProperty(process, 'resourcesPath', {value: ${JSON.stringify(join14(sourceApp, "Contents/Resources"))}});
@@ -10260,7 +10600,7 @@ if (autoUpdater) {
10260
10600
  autoUpdater.quitAndInstall = () => {};
10261
10601
  }
10262
10602
  require('node:fs').writeFileSync(${JSON.stringify(join14(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
10263
- ` });
10603
+ ` + (models ? modelBrandingHook(models) : "") });
10264
10604
  const logPath = join14(plan.profileDir, "inspector.log");
10265
10605
  plan.inspectorHook = { path, logPath };
10266
10606
  plan.args.push("--inspect-brk=127.0.0.1:0");
@@ -10269,30 +10609,69 @@ require('node:fs').writeFileSync(${JSON.stringify(join14(plan.profileDir, "hook-
10269
10609
  }
10270
10610
 
10271
10611
  // src/commands/cursor-desktop-hook.ts
10272
- import { readFileSync as readFileSync12 } from "node:fs";
10273
- import { dirname as dirname3, join as join15 } from "node:path";
10612
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
10613
+ import { dirname as dirname4, join as join15 } from "node:path";
10274
10614
  var FLAGS = /([\w$]+)=\{extensionIsDev:[^{}]{1,1000}localMode:(?:!1|false)\}/;
10275
- function addCursorDesktopLocalHook(plan, app) {
10276
- const resources = app.endsWith(".app") ? join15(app, "Contents/Resources/app") : join15(dirname3(app), "resources/app");
10615
+ function addCursorDesktopLocalHook(plan, app, catalog) {
10616
+ const resources = app.endsWith(".app") ? join15(app, "Contents/Resources/app") : join15(dirname4(app), "resources/app");
10277
10617
  const source = join15(resources, "out/main.js");
10278
- const renderer = join15(resources, "out/vs/workbench/workbench.desktop.main.js");
10279
- const mainSource = readFileSync12(source, "utf8");
10280
- const rendererSource = readFileSync12(renderer, "utf8");
10618
+ const mainSource = readFileSync13(source, "utf8");
10281
10619
  const mainMatch = FLAGS.exec(mainSource);
10282
- const rendererMatch = FLAGS.exec(rendererSource);
10283
- if (!mainMatch || !rendererMatch || !rendererSource.includes("runLocalAgentInExtensionHost"))
10620
+ if (!mainMatch)
10284
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.");
10285
10622
  const nativeKey = mainMatch[1];
10286
- const authMethod = "isAuthenticated(){const e=this.accessToken(),t=this.refreshToken();return this.scheduleTeamPolicyCheck(),!!(e&&t)}";
10287
- if (!rendererSource.includes(authMethod))
10288
- throw new Error("This Cursor Desktop build has an unsupported login controller. Update the ClosedRouter launcher before using local provider mode.");
10289
- const 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)}`);
10290
- const patched = join15(plan.profileDir, "cursor-local-workbench.js");
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
+ }
10291
10670
  const hook = join15(plan.profileDir, "cursor-local-hook.cjs");
10292
10671
  const marker = join15(plan.profileDir, "main-local-mode-enabled");
10293
- plan.files.push({ path: patched, contents: modifiedRenderer });
10294
10672
  plan.files.push({ path: hook, contents: `const {app, session} = require('electron');
10295
10673
  const fs = require('node:fs');
10674
+ const redirects = ${JSON.stringify(redirects)};
10296
10675
  function installProtocolHook() {
10297
10676
  const protocol = session.defaultSession.protocol;
10298
10677
  const prototype = Object.getPrototypeOf(protocol);
@@ -10300,9 +10679,9 @@ function installProtocolHook() {
10300
10679
  prototype.registerFileProtocol = function(scheme, handler, ...rest) {
10301
10680
  if (scheme !== 'vscode-file') return register.call(this, scheme, handler, ...rest);
10302
10681
  return register.call(this, scheme, (request, callback) => handler(request, result => {
10303
- if (request.url.includes('workbench.desktop.main.js')) fs.writeFileSync(${JSON.stringify(join15(plan.profileDir, "renderer-request.json"))}, JSON.stringify({type: typeof result, path: result?.path}), {mode: 0o600});
10304
- if (result && typeof result === 'object' && result.path === ${JSON.stringify(renderer)}) {
10305
- result = {...result, path: ${JSON.stringify(patched)}};
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]};
10306
10685
  fs.writeFileSync(${JSON.stringify(join15(plan.profileDir, "renderer-local-mode-served"))}, 'ready', {mode: 0o600});
10307
10686
  }
10308
10687
  callback(result);
@@ -10312,7 +10691,7 @@ function installProtocolHook() {
10312
10691
  }
10313
10692
  if (app.isReady()) installProtocolHook(); else app.once('ready', installProtocolHook);
10314
10693
  require('node:fs').writeFileSync(${JSON.stringify(join15(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
10315
- ` });
10694
+ ` + (catalog ? modelBrandingHook(catalog.models) : "") });
10316
10695
  const point = mainSource.slice(0, mainMatch.index + mainMatch[0].length).split(`
10317
10696
  `);
10318
10697
  const logPath = join15(plan.profileDir, "inspector.log");
@@ -10348,7 +10727,7 @@ var TOOL_OPTIONS = [
10348
10727
  var DESKTOP_TOOL_OPTIONS = [
10349
10728
  { value: "codex-desktop", label: "Codex Desktop", hint: "Responses API with an isolated app profile" },
10350
10729
  { value: "antigravity-desktop", label: "Antigravity Desktop", hint: "ClosedRouter routing through your antigravity-mods hook" },
10351
- { 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" },
10352
10731
  { value: "claude-desktop", label: "Claude Desktop", hint: "Inference gateway with an isolated app profile" }
10353
10732
  ];
10354
10733
  function getStartToolOptions(desktopOnly = false) {
@@ -10378,7 +10757,7 @@ function assertNoManagedCodexArgs(args) {
10378
10757
  throw new Error(`Remove ${conflicting}; ClosedRouter manages the Codex profile and model. Put other arguments after --.`);
10379
10758
  }
10380
10759
  }
10381
- function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir14(), executablePath, agyRouting) {
10760
+ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir14(), executablePath, agyRouting, catalog) {
10382
10761
  const baseUrl = normalizeServerUrl(serverUrl);
10383
10762
  const env = { ...process.env };
10384
10763
  const command = executablePath || tool;
@@ -10398,7 +10777,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10398
10777
  return {
10399
10778
  tool,
10400
10779
  command,
10401
- args: ["--model", model, ...toolArgs],
10780
+ args: ["--model", model, ...catalog ? ["--settings", JSON.stringify({ modelPicker: claudeModelPicker(catalog.models) })] : [], ...toolArgs],
10402
10781
  env
10403
10782
  };
10404
10783
  }
@@ -10407,9 +10786,11 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10407
10786
  const codexHome = process.env.CODEX_HOME || join16(homeDir, ".codex");
10408
10787
  const profileName = "closedrouter-launch";
10409
10788
  const configPath = join16(codexHome, `${profileName}.config.toml`);
10789
+ const catalogPath = join16(codexHome, "closedrouter-models.json");
10410
10790
  const configLines = [
10411
10791
  `model = ${tomlString(model)}`,
10412
10792
  `model_provider = ${tomlString(profileName)}`,
10793
+ ...catalog ? [`model_catalog_json = ${tomlString(catalogPath)}`] : [],
10413
10794
  "",
10414
10795
  `[model_providers.${profileName}]`,
10415
10796
  `name = ${tomlString("ClosedRouter")}`,
@@ -10425,7 +10806,8 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10425
10806
  args: ["--profile", profileName, ...toolArgs],
10426
10807
  env,
10427
10808
  configFile: { path: configPath, contents: configLines.join(`
10428
- `) }
10809
+ `) },
10810
+ ...catalog ? { extraFiles: [{ path: catalogPath, contents: JSON.stringify(codexModelCatalog(catalog.models)) }] } : {}
10429
10811
  };
10430
10812
  }
10431
10813
  if (tool === "gemini") {
@@ -10441,7 +10823,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10441
10823
  env,
10442
10824
  configFile: {
10443
10825
  path: configPath,
10444
- 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)}
10445
10827
  `
10446
10828
  }
10447
10829
  };
@@ -10467,8 +10849,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10467
10849
  return {
10468
10850
  tool,
10469
10851
  command,
10470
- args: ["--model", `openai/${model}`, ...toolArgs],
10471
- 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 }]))) } } : {}
10472
10855
  };
10473
10856
  }
10474
10857
  const config = {
@@ -10481,7 +10864,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10481
10864
  baseURL: `${baseUrl}/v1`,
10482
10865
  apiKey: "{env:CLOSEDROUTER_API_KEY}"
10483
10866
  },
10484
- models: { [model]: { name: model } }
10867
+ models: Object.fromEntries((catalog?.models || [model]).map((id) => [id, { name: modelLabel(id) }]))
10485
10868
  }
10486
10869
  }
10487
10870
  };
@@ -10495,9 +10878,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
10495
10878
  };
10496
10879
  }
10497
10880
  function writeOwnedConfig(file) {
10498
- mkdirSync4(dirname4(file.path), { recursive: true, mode: 448 });
10881
+ mkdirSync5(dirname5(file.path), { recursive: true, mode: 448 });
10499
10882
  try {
10500
- const existing = readFileSync13(file.path, "utf8");
10883
+ const existing = readFileSync14(file.path, "utf8");
10501
10884
  if (file.path.endsWith(".config.toml") && !existing.includes('name = "ClosedRouter"')) {
10502
10885
  throw new Error(`Refusing to replace an existing Codex profile at ${file.path}.`);
10503
10886
  }
@@ -10505,7 +10888,7 @@ function writeOwnedConfig(file) {
10505
10888
  if (error instanceof Error && error.message.startsWith("Refusing to replace"))
10506
10889
  throw error;
10507
10890
  }
10508
- writeFileSync4(file.path, file.contents, { encoding: "utf8", mode: 384 });
10891
+ writeFileSync5(file.path, file.contents, { encoding: "utf8", mode: 384 });
10509
10892
  }
10510
10893
  function modelSupportsHarness(model, tool) {
10511
10894
  const parameters = model.supported_parameters;
@@ -10536,7 +10919,7 @@ function assertAgyCustomModel(command, label, apiKey) {
10536
10919
  function assertAgyGeminiMode(homeDir) {
10537
10920
  const settingsPath = join16(homeDir, ".gemini", "antigravity-cli", "settings.json");
10538
10921
  try {
10539
- const settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
10922
+ const settings = JSON.parse(readFileSync14(settingsPath, "utf8"));
10540
10923
  if (settings?.modelProvider === "gemini")
10541
10924
  return;
10542
10925
  } catch {}
@@ -10600,11 +10983,12 @@ async function startCommand(options = {}) {
10600
10983
  }
10601
10984
  if (desktop && tool === "claude") {
10602
10985
  assertClaudeDesktopGatewaySupport(executablePath);
10603
- if (options.desktopInspector && process.platform === "darwin" && executablePath.endsWith(".app")) {
10986
+ if (process.platform === "darwin" && executablePath.endsWith(".app")) {
10604
10987
  sharedClaudeElectron = findClaudeSharedElectron(executablePath);
10605
- if (sharedClaudeElectron)
10606
- M2.info("Using the shared Electron runtime for Claude's startup inspector. The installed app bundle is reused.");
10607
- else
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)
10608
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.");
10609
10993
  }
10610
10994
  }
@@ -10643,14 +11027,15 @@ async function startCommand(options = {}) {
10643
11027
  if (!isGatewayModelAvailable(availableModels, model)) {
10644
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.`);
10645
11029
  }
11030
+ const catalog = createLaunchCatalog(availableModels, model);
10646
11031
  if (desktop && tool === "cursor") {
10647
11032
  const localKey = randomUUID5();
10648
- const relay = await startLaunchProxy(serverUrl, apiKey, "cursor-desktop", localKey, model);
11033
+ const relay = await startLaunchProxy(serverUrl, apiKey, "cursor-desktop", localKey, catalog);
10649
11034
  try {
10650
11035
  const plan = buildDesktopLaunchPlan("cursor", executablePath, createDesktopProfile("cursor"), undefined, undefined, undefined, options.toolArgs);
10651
- addCursorDesktopLocalHook(plan, executablePath);
11036
+ addCursorDesktopLocalHook(plan, executablePath, catalog);
10652
11037
  writeDesktopLaunchPlan(plan);
10653
- await configureCursorDesktop(plan.profileDir, relay.baseUrl, localKey, model);
11038
+ await configureCursorDesktop(plan.profileDir, relay.baseUrl, localKey, model, undefined, catalog.models);
10654
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.`);
10655
11040
  process.exitCode = await runDesktopLaunch(plan);
10656
11041
  } finally {
@@ -10660,18 +11045,18 @@ async function startCommand(options = {}) {
10660
11045
  }
10661
11046
  if (desktop && (tool === "codex" || tool === "claude" || tool === "agy")) {
10662
11047
  const localKey = randomUUID5();
10663
- const relay = await startLaunchProxy(serverUrl, apiKey, `${tool}-desktop`, localKey, tool === "claude" ? model : undefined);
11048
+ const relay = await startLaunchProxy(serverUrl, apiKey, `${tool}-desktop`, localKey, catalog);
10664
11049
  let agyDesktopProxy;
10665
11050
  try {
10666
- const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), relay.baseUrl, localKey, model, options.toolArgs);
10667
- if (options.desktopInspector && tool === "claude" && executablePath.endsWith(".app") && process.platform === "darwin") {
11051
+ const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), relay.baseUrl, localKey, model, options.toolArgs, process.platform, catalog.models);
11052
+ if (sharedClaudeElectron && tool === "claude") {
10668
11053
  if (sharedClaudeElectron)
10669
- useClaudeSharedElectron(plan, sharedClaudeElectron, sourceApp, options.toolArgs);
10670
- addClaudeDesktopHook(plan, relay.baseUrl, localKey, sharedClaudeElectron ? sourceApp : undefined);
11054
+ useClaudeSharedElectron(plan, sharedClaudeElectron, sourceApp, options.toolArgs, true);
11055
+ addClaudeDesktopHook(plan, relay.baseUrl, localKey, sourceApp, catalog.models);
10671
11056
  }
10672
11057
  if (tool === "agy") {
10673
- agyDesktopProxy = await startAgyDesktopProxy(relay.baseUrl, localKey, model);
10674
- addAntigravityDesktopHook(plan, agyDesktopProxy.baseUrl);
11058
+ agyDesktopProxy = await startAgyDesktopProxy(relay.baseUrl, localKey, model, undefined, catalog);
11059
+ addAntigravityDesktopHook(plan, agyDesktopProxy.baseUrl, catalog);
10675
11060
  }
10676
11061
  writeDesktopLaunchPlan(plan);
10677
11062
  M2.info(`Starting ${tool} desktop with ${model} through ${serverUrl}. Profile: ${plan.profileDir}. Keep cr running until you close this app instance.`);
@@ -10684,16 +11069,18 @@ async function startCommand(options = {}) {
10684
11069
  }
10685
11070
  if (tool === "agy")
10686
11071
  assertAgyGeminiMode(homedir14());
10687
- const recognizedModel = tool === "agy" ? registerAgyLaunchModel(homedir14(), model, discoverAgyTransportModel(options.executablePath || "agy", apiKey)) : undefined;
11072
+ const recognizedModel = tool === "agy" ? registerAgyLaunchCatalog(homedir14(), catalog, discoverAgyTransportModel(options.executablePath || "agy", apiKey)) : undefined;
10688
11073
  if (recognizedModel)
10689
11074
  assertAgyCustomModel(options.executablePath || "agy", recognizedModel, apiKey);
10690
- const relay = await startLaunchProxy(serverUrl, apiKey, tool);
11075
+ const relay = await startLaunchProxy(serverUrl, apiKey, tool, apiKey, catalog);
10691
11076
  let agyProxy;
10692
11077
  try {
10693
- agyProxy = tool === "agy" ? await startAgyModelProxy(relay.baseUrl, apiKey, model) : undefined;
10694
- const plan = buildLaunchPlan(tool, relay.baseUrl, apiKey, model, options.toolArgs, homedir14(), 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);
10695
11080
  if (plan.configFile)
10696
11081
  writeOwnedConfig(plan.configFile);
11082
+ for (const file of plan.extraFiles || [])
11083
+ writeOwnedConfig(file);
10697
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)}`);
10698
11085
  const exitCode = await runTool(plan);
10699
11086
  process.exitCode = exitCode;
@@ -10748,7 +11135,7 @@ process.on("warning", (warning) => {
10748
11135
  console.warn(warning.name, warning.message);
10749
11136
  });
10750
11137
  var program2 = new Command2;
10751
- var cliVersion = JSON.parse(readFileSync14(new URL("../package.json", import.meta.url), "utf8")).version;
11138
+ var cliVersion = JSON.parse(readFileSync15(new URL("../package.json", import.meta.url), "utf8")).version;
10752
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) => {
10753
11140
  await onboardCommand({ server: options.server });
10754
11141
  });