@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/README.md +8 -6
- package/dist/bridge/launch-proxy.d.ts +2 -1
- package/dist/bridge/launch-proxy.d.ts.map +1 -1
- package/dist/commands/agy-desktop-catalog.d.ts +8 -0
- package/dist/commands/agy-desktop-catalog.d.ts.map +1 -0
- package/dist/commands/agy-desktop.d.ts +3 -2
- package/dist/commands/agy-desktop.d.ts.map +1 -1
- package/dist/commands/agy-model.d.ts +2 -0
- package/dist/commands/agy-model.d.ts.map +1 -1
- package/dist/commands/agy-proxy.d.ts +2 -1
- package/dist/commands/agy-proxy.d.ts.map +1 -1
- package/dist/commands/claude-desktop.d.ts +6 -2
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/cursor-desktop-hook.d.ts +2 -1
- package/dist/commands/cursor-desktop-hook.d.ts.map +1 -1
- package/dist/commands/cursor-desktop.d.ts +1 -1
- package/dist/commands/cursor-desktop.d.ts.map +1 -1
- package/dist/commands/desktop-launch.d.ts +1 -1
- package/dist/commands/desktop-launch.d.ts.map +1 -1
- package/dist/commands/launch-catalog.d.ts +85 -0
- package/dist/commands/launch-catalog.d.ts.map +1 -0
- package/dist/commands/model-branding.d.ts +6 -0
- package/dist/commands/model-branding.d.ts.map +1 -0
- package/dist/commands/start.d.ts +3 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/index.js +480 -93
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
9463
|
-
|
|
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\/[^/:]
|
|
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
|
|
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
|
|
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
|
|
9599
|
-
if (!record(
|
|
9725
|
+
const previous = config.customModels ?? {};
|
|
9726
|
+
if (!record(previous))
|
|
9600
9727
|
throw new Error(`customModelsConfig.customModels must be an object in ${path}.`);
|
|
9601
|
-
const
|
|
9602
|
-
const
|
|
9603
|
-
|
|
9604
|
-
|
|
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
|
-
|
|
9734
|
+
models[label] = { modelName: name, apiProvider: "gemini" };
|
|
9609
9735
|
}
|
|
9610
|
-
settings.customModelsConfig = { ...config, customModels:
|
|
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
|
|
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:
|
|
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:
|
|
10124
|
+
availableAPIKeyModels: catalog,
|
|
9996
10125
|
havePerformedSettingsServiceMigration: true,
|
|
9997
|
-
localProviderModelIds:
|
|
9998
|
-
localProviderAgentModelIds:
|
|
10126
|
+
localProviderModelIds: catalog,
|
|
10127
|
+
localProviderAgentModelIds: catalog,
|
|
9999
10128
|
...membershipType ? { membershipType } : {},
|
|
10000
10129
|
aiSettings: {
|
|
10001
|
-
userAddedModels:
|
|
10002
|
-
modelOverrideEnabled:
|
|
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(
|
|
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 {
|
|
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.
|
|
10238
|
-
plan.
|
|
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
|
|
10273
|
-
import { dirname as
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
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 (
|
|
10304
|
-
if (result && typeof result === 'object' && result.path
|
|
10305
|
-
result = {...result, path:
|
|
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: "
|
|
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:
|
|
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
|
-
|
|
10881
|
+
mkdirSync5(dirname5(file.path), { recursive: true, mode: 448 });
|
|
10499
10882
|
try {
|
|
10500
|
-
const existing =
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
10986
|
+
if (process.platform === "darwin" && executablePath.endsWith(".app")) {
|
|
10604
10987
|
sharedClaudeElectron = findClaudeSharedElectron(executablePath);
|
|
10605
|
-
if (sharedClaudeElectron)
|
|
10606
|
-
|
|
10607
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
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,
|
|
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" ?
|
|
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(
|
|
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
|
});
|