@closedrouter/cli 1.1.10 → 1.1.12
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 +7 -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 +519 -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,133 @@ 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, aliases = {}) {
|
|
9504
|
+
const rewrite = (value) => {
|
|
9505
|
+
if (typeof value === "string") {
|
|
9506
|
+
for (const [alias, actual] of Object.entries(aliases))
|
|
9507
|
+
value = value.split(alias).join(actual);
|
|
9508
|
+
return value;
|
|
9509
|
+
}
|
|
9510
|
+
if (Array.isArray(value))
|
|
9511
|
+
return value.map(rewrite);
|
|
9512
|
+
if (value && typeof value === "object")
|
|
9513
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewrite(item)]));
|
|
9514
|
+
return value;
|
|
9515
|
+
};
|
|
9516
|
+
for (const key of ["system", "instructions", "systemInstruction"])
|
|
9517
|
+
if (payload[key] !== undefined)
|
|
9518
|
+
payload[key] = rewrite(payload[key]);
|
|
9519
|
+
if (Array.isArray(payload.messages))
|
|
9520
|
+
payload.messages = payload.messages.map((message) => {
|
|
9521
|
+
if (message && typeof message === "object" && ["system", "developer"].includes(message.role))
|
|
9522
|
+
return { ...message, content: rewrite(message.content) };
|
|
9523
|
+
return message;
|
|
9524
|
+
});
|
|
9525
|
+
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.`;
|
|
9526
|
+
if (protocol === "anthropic") {
|
|
9527
|
+
const system = payload.system;
|
|
9528
|
+
payload.system = typeof system === "string" ? `${system}
|
|
9529
|
+
|
|
9530
|
+
${text}` : [...Array.isArray(system) ? system : [], { type: "text", text }];
|
|
9531
|
+
} else if (protocol === "responses") {
|
|
9532
|
+
payload.instructions = `${typeof payload.instructions === "string" ? payload.instructions + `
|
|
9533
|
+
|
|
9534
|
+
` : ""}${text}`;
|
|
9535
|
+
} else if (protocol === "chat") {
|
|
9536
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
9537
|
+
const firstUser = messages.findIndex((item) => item && typeof item === "object" && !["system", "developer"].includes(item.role));
|
|
9538
|
+
const index = firstUser < 0 ? messages.length : firstUser;
|
|
9539
|
+
payload.messages = [...messages.slice(0, index), { role: "system", content: text }, ...messages.slice(index)];
|
|
9540
|
+
} else {
|
|
9541
|
+
const system = payload.systemInstruction;
|
|
9542
|
+
const instruction = system && typeof system === "object" && !Array.isArray(system) ? system : {};
|
|
9543
|
+
payload.systemInstruction = { ...instruction, parts: [...Array.isArray(instruction.parts) ? instruction.parts : [], { text }] };
|
|
9544
|
+
}
|
|
9545
|
+
}
|
|
9546
|
+
|
|
9547
|
+
// src/bridge/launch-proxy.ts
|
|
9421
9548
|
var HOP_HEADERS = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host"]);
|
|
9422
9549
|
async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey, modelOverride) {
|
|
9423
9550
|
const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
|
|
@@ -9440,8 +9567,15 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey,
|
|
|
9440
9567
|
const local = new URL(incoming.url, "http://localhost");
|
|
9441
9568
|
target.pathname = upstream.pathname.replace(/\/$/, "") + local.pathname;
|
|
9442
9569
|
target.search = local.search;
|
|
9570
|
+
const catalog = typeof modelOverride === "object" ? modelOverride : undefined;
|
|
9571
|
+
if (catalog && incoming.method === "GET" && ["/v1/models", "/v1beta/models"].includes(local.pathname)) {
|
|
9572
|
+
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" }));
|
|
9573
|
+
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"] })) }));
|
|
9574
|
+
return;
|
|
9575
|
+
}
|
|
9443
9576
|
let rewrittenBody;
|
|
9444
|
-
|
|
9577
|
+
const gemini = /^\/v1beta\/models\/(.+):(generateContent|streamGenerateContent)$/.exec(local.pathname);
|
|
9578
|
+
if (modelOverride && incoming.method === "POST" && (gemini || /^\/v1\/(?:messages(?:\/count_tokens)?|chat\/completions|responses)$/.test(local.pathname))) {
|
|
9445
9579
|
try {
|
|
9446
9580
|
const chunks = [];
|
|
9447
9581
|
let length = 0;
|
|
@@ -9459,8 +9593,18 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey,
|
|
|
9459
9593
|
return;
|
|
9460
9594
|
}
|
|
9461
9595
|
const payload = body;
|
|
9462
|
-
const
|
|
9463
|
-
|
|
9596
|
+
const requested = gemini ? decodeURIComponent(gemini[1]) : typeof payload.model === "string" ? payload.model : catalog?.defaultModel || "";
|
|
9597
|
+
const model = catalog ? resolveLaunchModel(catalog, requested) : modelOverride;
|
|
9598
|
+
if (!model) {
|
|
9599
|
+
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." } }));
|
|
9600
|
+
return;
|
|
9601
|
+
}
|
|
9602
|
+
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);
|
|
9603
|
+
if (catalog)
|
|
9604
|
+
addModelIdentity(payload, model, gemini ? "gemini" : local.pathname.startsWith("/v1/messages") ? "anthropic" : local.pathname === "/v1/responses" ? "responses" : "chat", catalog.aliases);
|
|
9605
|
+
if (gemini)
|
|
9606
|
+
target.pathname = upstream.pathname.replace(/\/$/, "") + `/v1beta/models/${encodeURIComponent(model)}:${gemini[2]}`;
|
|
9607
|
+
rewrittenBody = JSON.stringify({ ...payload, ...!gemini ? { model } : {}, ...tinyProbe ? { max_tokens: 512 } : {} });
|
|
9464
9608
|
} catch {
|
|
9465
9609
|
outgoing.writeHead(400).end();
|
|
9466
9610
|
return;
|
|
@@ -9529,7 +9673,7 @@ async function startLaunchProxy(gatewayUrl, apiKey, harness, localKey = apiKey,
|
|
|
9529
9673
|
import { createServer as createServer3 } from "node:http";
|
|
9530
9674
|
import { request as httpRequest2 } from "node:http";
|
|
9531
9675
|
import { request as httpsRequest2 } from "node:https";
|
|
9532
|
-
async function startAgyModelProxy(gatewayUrl, apiKey, model) {
|
|
9676
|
+
async function startAgyModelProxy(gatewayUrl, apiKey, model, catalog) {
|
|
9533
9677
|
const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
|
|
9534
9678
|
if (!["https:", "http:"].includes(upstream.protocol)) {
|
|
9535
9679
|
throw new Error("ClosedRouter server URL must use HTTP or HTTPS.");
|
|
@@ -9541,12 +9685,17 @@ async function startAgyModelProxy(gatewayUrl, apiKey, model) {
|
|
|
9541
9685
|
return;
|
|
9542
9686
|
}
|
|
9543
9687
|
const requestUrl = new URL(incoming.url || "/", "http://localhost");
|
|
9544
|
-
const match = /^\/v1beta\/models\/[^/:]
|
|
9688
|
+
const match = /^\/v1beta\/models\/([^/:]+):(generateContent|streamGenerateContent)$/.exec(requestUrl.pathname);
|
|
9545
9689
|
if (incoming.method !== "POST" || !match) {
|
|
9546
9690
|
outgoing.writeHead(404).end();
|
|
9547
9691
|
return;
|
|
9548
9692
|
}
|
|
9549
|
-
const
|
|
9693
|
+
const selected = catalog ? resolveLaunchModel(catalog, decodeURIComponent(match[1])) : model;
|
|
9694
|
+
if (!selected) {
|
|
9695
|
+
outgoing.writeHead(400).end(JSON.stringify({ error: { message: "Choose a supported ClosedRouter model." } }));
|
|
9696
|
+
return;
|
|
9697
|
+
}
|
|
9698
|
+
const target = new URL(`v1beta/models/${encodeURIComponent(selected)}:${match[2]}${requestUrl.search}`, upstream);
|
|
9550
9699
|
const headers = { ...incoming.headers, host: target.host, "x-goog-api-key": apiKey };
|
|
9551
9700
|
const upstreamRequest = requestUpstream(target, { method: "POST", headers }, (response) => {
|
|
9552
9701
|
outgoing.writeHead(response.statusCode || 502, response.headers);
|
|
@@ -9586,28 +9735,26 @@ import { join as join9 } from "node:path";
|
|
|
9586
9735
|
function record(value) {
|
|
9587
9736
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9588
9737
|
}
|
|
9589
|
-
function
|
|
9738
|
+
function registerAgyLaunchCatalog(homeDir, catalog, legacyTransport) {
|
|
9590
9739
|
const path = join9(homeDir, ".gemini", "antigravity-cli", "settings.json");
|
|
9591
9740
|
const settings = JSON.parse(readFileSync8(path, "utf8"));
|
|
9592
|
-
if (!record(settings) || settings.modelProvider !== "gemini")
|
|
9741
|
+
if (!record(settings) || settings.modelProvider !== "gemini")
|
|
9593
9742
|
throw new Error(`Set "modelProvider": "gemini" in ${path} before starting Antigravity through ClosedRouter.`);
|
|
9594
|
-
}
|
|
9595
9743
|
const config = settings.customModelsConfig ?? {};
|
|
9596
9744
|
if (!record(config))
|
|
9597
9745
|
throw new Error(`customModelsConfig must be an object in ${path}.`);
|
|
9598
|
-
const
|
|
9599
|
-
if (!record(
|
|
9746
|
+
const previous = config.customModels ?? {};
|
|
9747
|
+
if (!record(previous))
|
|
9600
9748
|
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))) {
|
|
9749
|
+
const models = { ...previous };
|
|
9750
|
+
for (const id of catalog.models) {
|
|
9751
|
+
const label = modelLabel(id), name = transportModelId(id, "gemini"), existing = previous[label];
|
|
9752
|
+
if (existing !== undefined && (!record(existing) || existing.apiProvider !== "gemini" || ![name, legacyTransport].includes(String(existing.modelName)) || Object.keys(existing).some((key) => !["modelName", "apiProvider"].includes(key)))) {
|
|
9606
9753
|
throw new Error(`The custom model '${label}' already has different settings in ${path}. Rename that entry before launching it with ClosedRouter.`);
|
|
9607
9754
|
}
|
|
9608
|
-
|
|
9755
|
+
models[label] = { modelName: name, apiProvider: "gemini" };
|
|
9609
9756
|
}
|
|
9610
|
-
settings.customModelsConfig = { ...config, customModels:
|
|
9757
|
+
settings.customModelsConfig = { ...config, customModels: models };
|
|
9611
9758
|
const temporary = `${path}.closedrouter-${randomUUID2()}.tmp`;
|
|
9612
9759
|
try {
|
|
9613
9760
|
writeFileSync2(temporary, `${JSON.stringify(settings, null, 2)}
|
|
@@ -9617,7 +9764,7 @@ function registerAgyLaunchModel(homeDir, model, transportModel) {
|
|
|
9617
9764
|
if (existsSync9(temporary))
|
|
9618
9765
|
unlinkSync(temporary);
|
|
9619
9766
|
}
|
|
9620
|
-
return
|
|
9767
|
+
return modelLabel(catalog.defaultModel);
|
|
9621
9768
|
}
|
|
9622
9769
|
|
|
9623
9770
|
// src/commands/cursor-launch.ts
|
|
@@ -9815,7 +9962,7 @@ function createDesktopProfile(tool) {
|
|
|
9815
9962
|
mkdirSync2(root, { recursive: true, mode: 448 });
|
|
9816
9963
|
return mkdtempSync(join11(root, "session-"));
|
|
9817
9964
|
}
|
|
9818
|
-
function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey, model, toolArgs = [], platform = process.platform) {
|
|
9965
|
+
function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey, model, toolArgs = [], platform = process.platform, models = model ? [model] : []) {
|
|
9819
9966
|
validateDesktopArgs(toolArgs);
|
|
9820
9967
|
const env = { ...process.env };
|
|
9821
9968
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
@@ -9835,6 +9982,8 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
|
|
|
9835
9982
|
`model = ${JSON.stringify(model)}`,
|
|
9836
9983
|
'model_provider = "closedrouter"',
|
|
9837
9984
|
"",
|
|
9985
|
+
`model_catalog_json = ${JSON.stringify(join11(codexHome, "closedrouter-models.json"))}`,
|
|
9986
|
+
"",
|
|
9838
9987
|
"[model_providers.closedrouter]",
|
|
9839
9988
|
'name = "ClosedRouter"',
|
|
9840
9989
|
`base_url = ${JSON.stringify(`${baseUrl.replace(/\/+$/, "")}/v1`)}`,
|
|
@@ -9844,11 +9993,11 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
|
|
|
9844
9993
|
].join(`
|
|
9845
9994
|
`) });
|
|
9846
9995
|
files.push({ path: join11(codexHome, "auth.json"), contents: JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: localKey }) });
|
|
9996
|
+
files.push({ path: join11(codexHome, "closedrouter-models.json"), contents: JSON.stringify(codexModelCatalog([...new Set([model, ...models])])) });
|
|
9847
9997
|
}
|
|
9848
9998
|
if (tool === "claude" && baseUrl && localKey && model) {
|
|
9849
9999
|
userData = join11(profileDir, "user-data-3p");
|
|
9850
10000
|
const id = randomUUID3();
|
|
9851
|
-
const transportModel = model.startsWith("claude-") || model.startsWith("anthropic/claude-") ? model : "claude-sonnet-4-6";
|
|
9852
10001
|
const json = (value) => `${JSON.stringify(value, null, 2)}
|
|
9853
10002
|
`;
|
|
9854
10003
|
files.push({ path: join11(userData, "claude_desktop_config.json"), contents: json({ deploymentMode: "3p" }) });
|
|
@@ -9859,7 +10008,7 @@ function buildDesktopLaunchPlan(tool, executable, profileDir, baseUrl, localKey,
|
|
|
9859
10008
|
inferenceGatewayApiKey: localKey,
|
|
9860
10009
|
inferenceGatewayAuthScheme: "bearer",
|
|
9861
10010
|
disableDeploymentModeChooser: true,
|
|
9862
|
-
inferenceModels: [{ name:
|
|
10011
|
+
inferenceModels: [...new Set([model, ...models])].map((id) => ({ name: transportModelId(id, "claude"), labelOverride: modelLabel(id) }))
|
|
9863
10012
|
}) });
|
|
9864
10013
|
}
|
|
9865
10014
|
Object.assign(env, appEnv);
|
|
@@ -9970,8 +10119,9 @@ function defaultCursorDatabase() {
|
|
|
9970
10119
|
const root = process.platform === "darwin" ? join12(homedir11(), "Library", "Application Support") : process.platform === "win32" ? process.env.APPDATA || join12(homedir11(), "AppData", "Roaming") : join12(homedir11(), ".config");
|
|
9971
10120
|
return join12(root, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
9972
10121
|
}
|
|
9973
|
-
async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, sourceDb = process.env.CLOSEDROUTER_CURSOR_AUTH_DB ?? defaultCursorDatabase()) {
|
|
10122
|
+
async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, sourceDb = process.env.CLOSEDROUTER_CURSOR_AUTH_DB ?? defaultCursorDatabase(), models = [model]) {
|
|
9974
10123
|
const cursorModel = model;
|
|
10124
|
+
const catalog = [...new Set([model, ...models])];
|
|
9975
10125
|
const dbPath = join12(profileDir, "user-data", "User", "globalStorage", "state.vscdb");
|
|
9976
10126
|
mkdirSync3(dirname2(dbPath), { recursive: true, mode: 448 });
|
|
9977
10127
|
const items = new Map;
|
|
@@ -9992,14 +10142,14 @@ async function configureCursorDesktop(profileDir, baseUrl, apiKey, model, source
|
|
|
9992
10142
|
items.set(CURSOR_APPLICATION_STORAGE, JSON.stringify({
|
|
9993
10143
|
useOpenAIKey: true,
|
|
9994
10144
|
openAIBaseUrl: `${baseUrl.replace(/\/+$/, "")}/v1`,
|
|
9995
|
-
availableAPIKeyModels:
|
|
10145
|
+
availableAPIKeyModels: catalog,
|
|
9996
10146
|
havePerformedSettingsServiceMigration: true,
|
|
9997
|
-
localProviderModelIds:
|
|
9998
|
-
localProviderAgentModelIds:
|
|
10147
|
+
localProviderModelIds: catalog,
|
|
10148
|
+
localProviderAgentModelIds: catalog,
|
|
9999
10149
|
...membershipType ? { membershipType } : {},
|
|
10000
10150
|
aiSettings: {
|
|
10001
|
-
userAddedModels:
|
|
10002
|
-
modelOverrideEnabled:
|
|
10151
|
+
userAddedModels: catalog,
|
|
10152
|
+
modelOverrideEnabled: catalog,
|
|
10003
10153
|
modelOverrideDisabled: [],
|
|
10004
10154
|
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
10155
|
}
|
|
@@ -10024,6 +10174,129 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
10024
10174
|
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
|
|
10025
10175
|
import { homedir as homedir12 } from "node:os";
|
|
10026
10176
|
import { join as join13 } from "node:path";
|
|
10177
|
+
|
|
10178
|
+
// src/commands/agy-desktop-catalog.ts
|
|
10179
|
+
function patchAntigravityCatalog(source, catalog) {
|
|
10180
|
+
const provider = /this\.customModelsProvider=([\w$]+)\(this\.rootProvider,([\w$]+)=>\(\{customModels:\2\.appState\?\.customModelsConfig\?\.customModels\?\?\{\}\}\)\)/;
|
|
10181
|
+
const context = /function ([\w$]+)\(\{children:([\w$]+),userStatus:([\w$]+),lastSelectedAgentModel:([\w$]+),setLastSelectedAgentModel:/;
|
|
10182
|
+
const match = context.exec(source);
|
|
10183
|
+
if (!provider.test(source) || !match)
|
|
10184
|
+
throw new Error("This Antigravity build has an unsupported custom-model picker. Update the ClosedRouter launcher.");
|
|
10185
|
+
const entries = catalog.models.map((id) => [`ClosedRouter: ${id}`, Object.keys(catalog.aliases).find((alias) => alias.startsWith("gemini-closedrouter-") && catalog.aliases[alias] === id)]);
|
|
10186
|
+
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})})]))`;
|
|
10187
|
+
const customState = /([\w$]+)=!!([\w$]+),([\w$]+)=\(0,([\w$]+)\.useMemo\)\(\(\)=>\(\{customModels:([\w$]+)\?\.customModels\?\?([\w$]+)\}\),\[\5\]\)/;
|
|
10188
|
+
if (!customState.test(source))
|
|
10189
|
+
throw new Error("This Antigravity build has an unsupported custom-model context.");
|
|
10190
|
+
source = source.replace(customState, (_all, supported, _feature, state, react, input) => `${supported}=true,${state}=(0,${react}.useMemo)(()=>({customModels:${models}}),[${input}])`);
|
|
10191
|
+
const customHeader = /showCustomModelHeader:[\w$]+,onAddCustomModel:/;
|
|
10192
|
+
if (!customHeader.test(source))
|
|
10193
|
+
throw new Error("This Antigravity build has an unsupported custom-model menu.");
|
|
10194
|
+
source = source.replace(/showCustomModelHeader:[\w$]+,onAddCustomModel:[\w$]+,onEditCustomModel:[\w$]+/, "showCustomModelHeader:true,onAddCustomModel:undefined,onEditCustomModel:undefined");
|
|
10195
|
+
source = source.replace(/([\w$]+)=([\w$]+)\.some\(([\w$]+)=>\3\.options\.length>0\);([\w$]+)=/, "$1=true;$4=");
|
|
10196
|
+
source = source.replace(provider, (_all, selector, variable) => `this.customModelsProvider=${selector}(this.rootProvider,${variable}=>({customModels:${models}}))`);
|
|
10197
|
+
const start = source.indexOf(match[0]);
|
|
10198
|
+
const end = source.indexOf(`function `, start + match[0].length);
|
|
10199
|
+
let body = source.slice(start, end);
|
|
10200
|
+
const state = /\[([\w$]+),([\w$]+)\]=\(0,([\w$]+)\.useState\)\(null\)/;
|
|
10201
|
+
if (!state.test(body) || !body.includes("clientModelConfigs||[]"))
|
|
10202
|
+
throw new Error("This Antigravity build has an unsupported model-selection context.");
|
|
10203
|
+
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, "[]");
|
|
10204
|
+
return source.slice(0, start) + body + source.slice(end);
|
|
10205
|
+
}
|
|
10206
|
+
function antigravityCatalogHook(catalog, marker) {
|
|
10207
|
+
return `
|
|
10208
|
+
const {app} = require('electron');
|
|
10209
|
+
const patchCatalog = ${patchAntigravityCatalog.toString()};
|
|
10210
|
+
app.on('web-contents-created', (_event, contents) => {
|
|
10211
|
+
const debuggerClient = contents.debugger;
|
|
10212
|
+
try {
|
|
10213
|
+
debuggerClient.attach('1.3');
|
|
10214
|
+
debuggerClient.sendCommand('Fetch.enable', {patterns:[{urlPattern:'https://127.0.0.1:*/main.js',requestStage:'Response'}]}).catch(() => {});
|
|
10215
|
+
debuggerClient.on('message', async (_event, method, params) => {
|
|
10216
|
+
if (method !== 'Fetch.requestPaused') return;
|
|
10217
|
+
try {
|
|
10218
|
+
const url = new URL(params.request.url);
|
|
10219
|
+
if (url.hostname !== '127.0.0.1' || url.pathname !== '/main.js' || params.responseStatusCode !== 200) {
|
|
10220
|
+
await debuggerClient.sendCommand('Fetch.continueRequest', {requestId:params.requestId}); return;
|
|
10221
|
+
}
|
|
10222
|
+
const result = await debuggerClient.sendCommand('Fetch.getResponseBody', {requestId:params.requestId});
|
|
10223
|
+
const source = result.base64Encoded ? Buffer.from(result.body,'base64').toString('utf8') : result.body;
|
|
10224
|
+
const patched = patchCatalog(source, ${JSON.stringify(catalog)});
|
|
10225
|
+
await debuggerClient.sendCommand('Fetch.fulfillRequest', {requestId:params.requestId,responseCode:200,
|
|
10226
|
+
responseHeaders:(params.responseHeaders || []).filter(h=>!['content-length','content-encoding','transfer-encoding'].includes(h.name.toLowerCase())),
|
|
10227
|
+
body:Buffer.from(patched).toString('base64')});
|
|
10228
|
+
require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'ready', {mode:0o600});
|
|
10229
|
+
await debuggerClient.sendCommand('Fetch.disable');
|
|
10230
|
+
debuggerClient.detach();
|
|
10231
|
+
} catch (error) {
|
|
10232
|
+
require('node:fs').writeFileSync(${JSON.stringify(marker + ".error")}, String(error.message), {mode:0o600});
|
|
10233
|
+
await debuggerClient.sendCommand('Fetch.failRequest', {requestId:params.requestId,errorReason:'Failed'}).catch(()=>{});
|
|
10234
|
+
}
|
|
10235
|
+
});
|
|
10236
|
+
} catch (error) {
|
|
10237
|
+
require('node:fs').writeFileSync(${JSON.stringify(marker + ".error")}, String(error.message), {mode:0o600});
|
|
10238
|
+
}
|
|
10239
|
+
});
|
|
10240
|
+
`;
|
|
10241
|
+
}
|
|
10242
|
+
|
|
10243
|
+
// src/commands/model-branding.ts
|
|
10244
|
+
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>';
|
|
10245
|
+
function installModelBranding(models, artwork) {
|
|
10246
|
+
if (document.querySelector("[data-closedrouter-branding]"))
|
|
10247
|
+
return;
|
|
10248
|
+
const marker = document.createElement("meta");
|
|
10249
|
+
marker.dataset.closedrouterBranding = "true";
|
|
10250
|
+
document.head.append(marker);
|
|
10251
|
+
const paint = () => {
|
|
10252
|
+
for (const element of document.querySelectorAll('button,[role="button"],[role="option"],[role="menuitem"],[role="menuitemradio"],[data-testid="model-selector-item"]')) {
|
|
10253
|
+
const text = element.innerText.trim().split(`
|
|
10254
|
+
`)[0];
|
|
10255
|
+
if (!models.some((id) => text === id || text === `ClosedRouter: ${id}`))
|
|
10256
|
+
continue;
|
|
10257
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
10258
|
+
while (walker.nextNode()) {
|
|
10259
|
+
const node = walker.currentNode;
|
|
10260
|
+
const value = node.textContent || "";
|
|
10261
|
+
if (models.some((id) => value.trim() === `ClosedRouter: ${id}`))
|
|
10262
|
+
node.textContent = value.replace("ClosedRouter: ", "");
|
|
10263
|
+
}
|
|
10264
|
+
if (element.querySelector(":scope > [data-closedrouter-logo]"))
|
|
10265
|
+
continue;
|
|
10266
|
+
const logo = document.createElement("img");
|
|
10267
|
+
logo.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(artwork)}`;
|
|
10268
|
+
logo.alt = "ClosedRouter";
|
|
10269
|
+
logo.title = "Routed through ClosedRouter";
|
|
10270
|
+
logo.dataset.closedrouterLogo = "true";
|
|
10271
|
+
logo.style.cssText = "width:16px;height:16px;flex:0 0 16px;display:inline-block;vertical-align:middle;margin-inline-end:6px;pointer-events:none";
|
|
10272
|
+
element.prepend(logo);
|
|
10273
|
+
}
|
|
10274
|
+
};
|
|
10275
|
+
let scheduled = false;
|
|
10276
|
+
const observer = new MutationObserver(() => {
|
|
10277
|
+
if (scheduled)
|
|
10278
|
+
return;
|
|
10279
|
+
scheduled = true;
|
|
10280
|
+
requestAnimationFrame(() => {
|
|
10281
|
+
scheduled = false;
|
|
10282
|
+
paint();
|
|
10283
|
+
});
|
|
10284
|
+
});
|
|
10285
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
|
|
10286
|
+
paint();
|
|
10287
|
+
}
|
|
10288
|
+
function modelBrandingHook(models) {
|
|
10289
|
+
const script = `(${installModelBranding.toString()})(${JSON.stringify(models)},${JSON.stringify(CLOSEDROUTER_LOGO)})`;
|
|
10290
|
+
return `
|
|
10291
|
+
require('electron').app.on('web-contents-created', (_event, contents) => {
|
|
10292
|
+
contents.on('did-finish-load', () => {
|
|
10293
|
+
contents.executeJavaScript(${JSON.stringify(script)}).catch(() => {});
|
|
10294
|
+
});
|
|
10295
|
+
});
|
|
10296
|
+
`;
|
|
10297
|
+
}
|
|
10298
|
+
|
|
10299
|
+
// src/commands/agy-desktop.ts
|
|
10027
10300
|
var MODS_CONTROL_URL = "http://127.0.0.1:59998";
|
|
10028
10301
|
var MODS_PROXY_URL = "http://127.0.0.1:59997";
|
|
10029
10302
|
async function assertAntigravityModsAvailable() {
|
|
@@ -10060,7 +10333,7 @@ function assertAntigravityDesktopHookSupport(executable) {
|
|
|
10060
10333
|
if (index < 0 || binary[index + 32 + 5] !== 49)
|
|
10061
10334
|
throw new Error("This Antigravity build disables launch inspector hooks. Use --open-only.");
|
|
10062
10335
|
}
|
|
10063
|
-
async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_PROXY_URL) {
|
|
10336
|
+
async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_PROXY_URL, catalog) {
|
|
10064
10337
|
const capability = randomUUID4();
|
|
10065
10338
|
const controllers = new Set;
|
|
10066
10339
|
const server = createServer4(async (incoming, outgoing) => {
|
|
@@ -10099,9 +10372,15 @@ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_
|
|
|
10099
10372
|
return;
|
|
10100
10373
|
}
|
|
10101
10374
|
const body = JSON.parse(raw.toString("utf8"));
|
|
10375
|
+
const requested = body.modelName || body.model || model;
|
|
10376
|
+
const selected = catalog ? resolveLaunchModel(catalog, requested) : model;
|
|
10377
|
+
if (!selected) {
|
|
10378
|
+
outgoing.writeHead(400).end(JSON.stringify({ error: { message: "Choose a supported ClosedRouter model." } }));
|
|
10379
|
+
return;
|
|
10380
|
+
}
|
|
10102
10381
|
const request = body.request || body;
|
|
10103
10382
|
payload = Buffer.from(JSON.stringify(request));
|
|
10104
|
-
target = `${gatewayUrl}/v1beta/models/${encodeURIComponent(
|
|
10383
|
+
target = `${gatewayUrl}/v1beta/models/${encodeURIComponent(selected)}:${generation[1]}${generation[1] === "streamGenerateContent" ? "?alt=sse" : ""}`;
|
|
10105
10384
|
headers.authorization = `Bearer ${localKey}`;
|
|
10106
10385
|
headers["content-type"] = "application/json";
|
|
10107
10386
|
} else {
|
|
@@ -10182,8 +10461,9 @@ async function startAgyDesktopProxy(gatewayUrl, localKey, model, modsUrl = MODS_
|
|
|
10182
10461
|
})
|
|
10183
10462
|
};
|
|
10184
10463
|
}
|
|
10185
|
-
function addAntigravityDesktopHook(plan, endpoint) {
|
|
10464
|
+
function addAntigravityDesktopHook(plan, endpoint, catalog) {
|
|
10186
10465
|
const path = join13(plan.profileDir, "antigravity-hook.cjs");
|
|
10466
|
+
const marker = join13(plan.profileDir, "model-catalog-ready");
|
|
10187
10467
|
const contents = `const cp = require('node:child_process');
|
|
10188
10468
|
const spawn = cp.spawn;
|
|
10189
10469
|
cp.spawn = function(command, args, options) {
|
|
@@ -10205,10 +10485,10 @@ cp.spawn = function(command, args, options) {
|
|
|
10205
10485
|
return spawn.call(this, command, args, options);
|
|
10206
10486
|
};
|
|
10207
10487
|
require('node:fs').writeFileSync(${JSON.stringify(join13(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
|
|
10208
|
-
|
|
10488
|
+
` + (catalog ? antigravityCatalogHook(catalog, marker) + modelBrandingHook(catalog.models) : "");
|
|
10209
10489
|
plan.files.push({ path, contents });
|
|
10210
10490
|
const logPath = join13(plan.profileDir, "inspector.log");
|
|
10211
|
-
plan.inspectorHook = { path, logPath };
|
|
10491
|
+
plan.inspectorHook = { path, logPath, ...catalog ? { readyPath: marker } : {} };
|
|
10212
10492
|
plan.args.push("--inspect-brk=127.0.0.1:0");
|
|
10213
10493
|
if (plan.command === "/usr/bin/open") {
|
|
10214
10494
|
const index = plan.args.indexOf("--args");
|
|
@@ -10218,9 +10498,92 @@ require('node:fs').writeFileSync(${JSON.stringify(join13(plan.profileDir, "hook-
|
|
|
10218
10498
|
|
|
10219
10499
|
// src/commands/claude-desktop.ts
|
|
10220
10500
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10221
|
-
import {
|
|
10501
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
10502
|
+
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
10503
|
import { homedir as homedir13 } from "node:os";
|
|
10223
|
-
import { join as join14 } from "node:path";
|
|
10504
|
+
import { dirname as dirname3, join as join14, relative as relative2 } from "node:path";
|
|
10505
|
+
function patchClaudeModelIdentity(source, aliases) {
|
|
10506
|
+
return source.replace(/function ([\w$]+)\(\{modelId:([\w$]+),provider:([\w$]+),labelOverride:([\w$]+)\}\)\{/, (start, _name, id) => start + `const closedrouterIdentity=${JSON.stringify(aliases)}[${id}];if(closedrouterIdentity)return 'Your configured inference model ID is '+closedrouterIdentity+'. Your inference provider is ClosedRouter. The client and its transport alias do not identify the underlying inference model.';`);
|
|
10507
|
+
}
|
|
10508
|
+
function cloneClaudeElectronRuntime(source, destination) {
|
|
10509
|
+
const clone = (from, to) => {
|
|
10510
|
+
const info = lstatSync(from);
|
|
10511
|
+
if (info.isSymbolicLink())
|
|
10512
|
+
symlinkSync(readlinkSync(from), to);
|
|
10513
|
+
else if (info.isDirectory()) {
|
|
10514
|
+
mkdirSync4(to, { mode: 448 });
|
|
10515
|
+
for (const name of readdirSync2(from))
|
|
10516
|
+
clone(join14(from, name), join14(to, name));
|
|
10517
|
+
} else if (info.isFile())
|
|
10518
|
+
linkSync(from, to);
|
|
10519
|
+
else
|
|
10520
|
+
throw new Error("Unsupported file in the shared Electron runtime.");
|
|
10521
|
+
};
|
|
10522
|
+
clone(source, destination);
|
|
10523
|
+
const contents = join14(destination, "Contents");
|
|
10524
|
+
const roots = [contents, ...readdirSync2(join14(contents, "Frameworks")).filter((name) => name.endsWith(".app")).map((name) => join14(contents, "Frameworks", name, "Contents"))];
|
|
10525
|
+
const owned = [];
|
|
10526
|
+
for (const root of roots) {
|
|
10527
|
+
if (!realpathSync(root).startsWith(realpathSync(destination) + "/"))
|
|
10528
|
+
throw new Error("Electron helper signing must stay inside the owned runtime.");
|
|
10529
|
+
for (const name of readdirSync2(join14(root, "MacOS"))) {
|
|
10530
|
+
const target = join14(root, "MacOS", name);
|
|
10531
|
+
if (!lstatSync(target).isFile())
|
|
10532
|
+
throw new Error("Electron helper executables must be regular files.");
|
|
10533
|
+
const original = join14(source, relative2(destination, target));
|
|
10534
|
+
unlinkSync2(target);
|
|
10535
|
+
copyFileSync(original, target);
|
|
10536
|
+
owned.push(target);
|
|
10537
|
+
}
|
|
10538
|
+
rmSync(join14(root, "_CodeSignature"), { recursive: true, force: true });
|
|
10539
|
+
}
|
|
10540
|
+
return owned;
|
|
10541
|
+
}
|
|
10542
|
+
function prepareClaudeElectronRuntime(runtime, sourceApp) {
|
|
10543
|
+
const source = dirname3(dirname3(dirname3(realpathSync(runtime))));
|
|
10544
|
+
if (!source.endsWith(".app"))
|
|
10545
|
+
throw new Error("Claude's shared runtime must be a macOS Electron app bundle.");
|
|
10546
|
+
const framework = join14(source, "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework");
|
|
10547
|
+
const fingerprint = createHash3("sha256").update(JSON.stringify({
|
|
10548
|
+
schema: 1,
|
|
10549
|
+
source,
|
|
10550
|
+
sourceApp,
|
|
10551
|
+
executable: statSync2(runtime).mtimeMs,
|
|
10552
|
+
framework: statSync2(framework).mtimeMs,
|
|
10553
|
+
appInfo: readFileSync12(join14(sourceApp, "Contents/Info.plist"), "utf8")
|
|
10554
|
+
})).digest("hex").slice(0, 24);
|
|
10555
|
+
const root = join14(getConfigDir(), "desktop-runtime", "claude");
|
|
10556
|
+
mkdirSync4(root, { recursive: true, mode: 448 });
|
|
10557
|
+
const cache = join14(root, fingerprint), app = join14(cache, "Electron.app");
|
|
10558
|
+
const executable = join14(app, "Contents/MacOS", runtime.split("/").at(-1));
|
|
10559
|
+
const marker = join14(cache, "ready.json");
|
|
10560
|
+
if (existsSync14(marker)) {
|
|
10561
|
+
const saved = JSON.parse(readFileSync12(marker, "utf8"));
|
|
10562
|
+
if (saved.fingerprint !== fingerprint || saved.source !== source)
|
|
10563
|
+
throw new Error("Claude's owned runtime cache has unexpected metadata.");
|
|
10564
|
+
execFileSync2("/usr/bin/codesign", ["--verify", app], { stdio: "pipe" });
|
|
10565
|
+
return executable;
|
|
10566
|
+
}
|
|
10567
|
+
const staging = mkdtempSync2(join14(root, ".preparing-"));
|
|
10568
|
+
try {
|
|
10569
|
+
const target = join14(staging, "Electron.app");
|
|
10570
|
+
const owned = cloneClaudeElectronRuntime(source, target);
|
|
10571
|
+
const entitlements = join14(staging, "entitlements.plist");
|
|
10572
|
+
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 });
|
|
10573
|
+
for (const path of [...owned.slice(1), owned[0]]) {
|
|
10574
|
+
if (statSync2(path).nlink !== 1)
|
|
10575
|
+
throw new Error("Refusing to sign a shared Electron executable.");
|
|
10576
|
+
execFileSync2("/usr/bin/codesign", ["--force", "--sign", "-", "--entitlements", entitlements, path], { stdio: "pipe" });
|
|
10577
|
+
}
|
|
10578
|
+
execFileSync2("/usr/bin/codesign", ["--verify", target], { stdio: "pipe" });
|
|
10579
|
+
writeFileSync4(join14(staging, "ready.json"), JSON.stringify({ fingerprint, source, sourceApp }), { mode: 384 });
|
|
10580
|
+
renameSync2(staging, cache);
|
|
10581
|
+
return executable;
|
|
10582
|
+
} catch (error) {
|
|
10583
|
+
rmSync(staging, { recursive: true, force: true });
|
|
10584
|
+
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 });
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10224
10587
|
function findClaudeSharedElectron(sourceApp, home = homedir13()) {
|
|
10225
10588
|
const explicit = process.env.CLOSEDROUTER_CLAUDE_ELECTRON || process.env.CLAUDE_DESKTOP_AUTH_ELECTRON;
|
|
10226
10589
|
if (explicit && existsSync14(explicit))
|
|
@@ -10233,11 +10596,12 @@ function findClaudeSharedElectron(sourceApp, home = homedir13()) {
|
|
|
10233
10596
|
const versions = readdirSync2(root).filter((version) => version.startsWith(`${major}.`)).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
10234
10597
|
return versions.map((version) => join14(root, version, `darwin-${process.arch}`, "Electron.app/Contents/MacOS/Electron")).find((path) => existsSync14(path));
|
|
10235
10598
|
}
|
|
10236
|
-
function useClaudeSharedElectron(plan, runtime, sourceApp, toolArgs = []) {
|
|
10237
|
-
plan.
|
|
10238
|
-
plan.
|
|
10599
|
+
function useClaudeSharedElectron(plan, runtime, sourceApp, toolArgs = [], launchServices = false) {
|
|
10600
|
+
const args = [join14(sourceApp, "Contents/Resources/app.asar"), `--user-data-dir=${join14(plan.profileDir, "user-data-3p")}`, ...toolArgs];
|
|
10601
|
+
plan.command = launchServices ? "/usr/bin/open" : runtime;
|
|
10602
|
+
plan.args = launchServices ? ["-W", "-n", "-a", dirname3(dirname3(dirname3(runtime))), "--args", ...args] : args;
|
|
10239
10603
|
}
|
|
10240
|
-
function addClaudeDesktopHook(plan, baseUrl, localKey, sourceApp) {
|
|
10604
|
+
function addClaudeDesktopHook(plan, baseUrl, localKey, sourceApp, models) {
|
|
10241
10605
|
const path = join14(plan.profileDir, "claude-hook.cjs");
|
|
10242
10606
|
plan.files.push({ path, contents: `${sourceApp ? `const electron = require('electron');
|
|
10243
10607
|
Object.defineProperty(process, 'resourcesPath', {value: ${JSON.stringify(join14(sourceApp, "Contents/Resources"))}});
|
|
@@ -10260,7 +10624,22 @@ if (autoUpdater) {
|
|
|
10260
10624
|
autoUpdater.quitAndInstall = () => {};
|
|
10261
10625
|
}
|
|
10262
10626
|
require('node:fs').writeFileSync(${JSON.stringify(join14(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
|
|
10263
|
-
`
|
|
10627
|
+
` + (models && sourceApp ? `
|
|
10628
|
+
const moduleHooks = require('node:module');
|
|
10629
|
+
const patchIdentity = ${patchClaudeModelIdentity.toString()};
|
|
10630
|
+
if (typeof moduleHooks.registerHooks === 'function') moduleHooks.registerHooks({load(url, context, nextLoad) {
|
|
10631
|
+
const loaded = nextLoad(url, context);
|
|
10632
|
+
if (url.startsWith('file:') && require('node:url').fileURLToPath(url).startsWith(${JSON.stringify(join14(sourceApp, "Contents/Resources/app.asar/.vite/build/"))}) && loaded.source != null) {
|
|
10633
|
+
const source = typeof loaded.source === 'string' ? loaded.source : Buffer.from(loaded.source).toString('utf8');
|
|
10634
|
+
const patched = patchIdentity(source, ${JSON.stringify(Object.fromEntries(models.map((id) => [transportModelId(id, "claude"), id])))});
|
|
10635
|
+
if (patched !== source) {
|
|
10636
|
+
require('node:fs').writeFileSync(${JSON.stringify(join14(plan.profileDir, "model-identity-hook-ready"))}, 'ready', {mode:0o600});
|
|
10637
|
+
return {...loaded, source:patched};
|
|
10638
|
+
}
|
|
10639
|
+
}
|
|
10640
|
+
return loaded;
|
|
10641
|
+
}});
|
|
10642
|
+
` : "") + (models ? modelBrandingHook(models) : "") });
|
|
10264
10643
|
const logPath = join14(plan.profileDir, "inspector.log");
|
|
10265
10644
|
plan.inspectorHook = { path, logPath };
|
|
10266
10645
|
plan.args.push("--inspect-brk=127.0.0.1:0");
|
|
@@ -10269,30 +10648,69 @@ require('node:fs').writeFileSync(${JSON.stringify(join14(plan.profileDir, "hook-
|
|
|
10269
10648
|
}
|
|
10270
10649
|
|
|
10271
10650
|
// src/commands/cursor-desktop-hook.ts
|
|
10272
|
-
import { readFileSync as
|
|
10273
|
-
import { dirname as
|
|
10651
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
|
|
10652
|
+
import { dirname as dirname4, join as join15 } from "node:path";
|
|
10274
10653
|
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(
|
|
10654
|
+
function addCursorDesktopLocalHook(plan, app, catalog) {
|
|
10655
|
+
const resources = app.endsWith(".app") ? join15(app, "Contents/Resources/app") : join15(dirname4(app), "resources/app");
|
|
10277
10656
|
const source = join15(resources, "out/main.js");
|
|
10278
|
-
const
|
|
10279
|
-
const mainSource = readFileSync12(source, "utf8");
|
|
10280
|
-
const rendererSource = readFileSync12(renderer, "utf8");
|
|
10657
|
+
const mainSource = readFileSync13(source, "utf8");
|
|
10281
10658
|
const mainMatch = FLAGS.exec(mainSource);
|
|
10282
|
-
|
|
10283
|
-
if (!mainMatch || !rendererMatch || !rendererSource.includes("runLocalAgentInExtensionHost"))
|
|
10659
|
+
if (!mainMatch)
|
|
10284
10660
|
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
10661
|
const nativeKey = mainMatch[1];
|
|
10286
|
-
const
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10662
|
+
const redirects = {};
|
|
10663
|
+
for (const variant of ["desktop", "glass"]) {
|
|
10664
|
+
const renderer = join15(resources, `out/vs/workbench/workbench.${variant}.main.js`);
|
|
10665
|
+
if (!existsSync15(renderer) && variant === "glass")
|
|
10666
|
+
continue;
|
|
10667
|
+
const rendererSource = readFileSync13(renderer, "utf8");
|
|
10668
|
+
const rendererMatch = FLAGS.exec(rendererSource);
|
|
10669
|
+
if (!rendererMatch || !rendererSource.includes("runLocalAgentInExtensionHost"))
|
|
10670
|
+
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.");
|
|
10671
|
+
const authMethod = /isAuthenticated\(\)\{const ([\w$]+)=this\.accessToken\(\),([\w$]+)=this\.refreshToken\(\);return this\.scheduleTeamPolicyCheck\(\),!!\(\1&&\2\)\}/;
|
|
10672
|
+
if (!authMethod.test(rendererSource))
|
|
10673
|
+
throw new Error("This Cursor Desktop build has an unsupported login controller. Update the ClosedRouter launcher before using local provider mode.");
|
|
10674
|
+
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)}`);
|
|
10675
|
+
if (catalog) {
|
|
10676
|
+
const nativeCatalog = "getAvailableDefaultModels(){return[...this._availableDefaultModels()]}";
|
|
10677
|
+
if (!modifiedRenderer.includes(nativeCatalog))
|
|
10678
|
+
throw new Error("This Cursor Desktop build has an unsupported model catalog controller. Update the ClosedRouter launcher.");
|
|
10679
|
+
const entries = catalog.models.map((id) => ({
|
|
10680
|
+
name: id,
|
|
10681
|
+
serverModelName: id,
|
|
10682
|
+
clientDisplayName: id,
|
|
10683
|
+
inputboxShortModelName: id,
|
|
10684
|
+
defaultOn: true,
|
|
10685
|
+
supportsAgent: true,
|
|
10686
|
+
supportsPlanMode: true,
|
|
10687
|
+
supportsNonMaxMode: true,
|
|
10688
|
+
supportsImages: false,
|
|
10689
|
+
supportsThinking: false,
|
|
10690
|
+
supportsCmdK: true,
|
|
10691
|
+
supportsSmartModeClassifier: true,
|
|
10692
|
+
parameterDefinitions: [],
|
|
10693
|
+
variants: [],
|
|
10694
|
+
legacySlugs: [],
|
|
10695
|
+
idAliases: [],
|
|
10696
|
+
namedModelSectionIndex: 0,
|
|
10697
|
+
tooltipData: { markdownContent: "Via your ClosedRouter pool" }
|
|
10698
|
+
}));
|
|
10699
|
+
modifiedRenderer = modifiedRenderer.replace(nativeCatalog, `getAvailableDefaultModels(){return ${JSON.stringify(entries)}}`);
|
|
10700
|
+
const display = /function ([\w$]+)\(([\w$]+),([\w$]+),([\w$]+)\)\{if\(\2\.includes\(","\)\)/;
|
|
10701
|
+
if (!display.test(modifiedRenderer))
|
|
10702
|
+
throw new Error("This Cursor Desktop build has an unsupported model-label formatter. Update the ClosedRouter launcher.");
|
|
10703
|
+
modifiedRenderer = modifiedRenderer.replace(display, (all, _fn, model) => all.replace("{if(", `{if(${JSON.stringify(catalog.models)}.includes(${model}))return 'ClosedRouter: '+${model};if(`));
|
|
10704
|
+
}
|
|
10705
|
+
const patched = join15(plan.profileDir, `cursor-local-workbench${variant === "desktop" ? "" : "-glass"}.js`);
|
|
10706
|
+
redirects[renderer] = patched;
|
|
10707
|
+
plan.files.push({ path: patched, contents: modifiedRenderer });
|
|
10708
|
+
}
|
|
10291
10709
|
const hook = join15(plan.profileDir, "cursor-local-hook.cjs");
|
|
10292
10710
|
const marker = join15(plan.profileDir, "main-local-mode-enabled");
|
|
10293
|
-
plan.files.push({ path: patched, contents: modifiedRenderer });
|
|
10294
10711
|
plan.files.push({ path: hook, contents: `const {app, session} = require('electron');
|
|
10295
10712
|
const fs = require('node:fs');
|
|
10713
|
+
const redirects = ${JSON.stringify(redirects)};
|
|
10296
10714
|
function installProtocolHook() {
|
|
10297
10715
|
const protocol = session.defaultSession.protocol;
|
|
10298
10716
|
const prototype = Object.getPrototypeOf(protocol);
|
|
@@ -10300,9 +10718,9 @@ function installProtocolHook() {
|
|
|
10300
10718
|
prototype.registerFileProtocol = function(scheme, handler, ...rest) {
|
|
10301
10719
|
if (scheme !== 'vscode-file') return register.call(this, scheme, handler, ...rest);
|
|
10302
10720
|
return register.call(this, scheme, (request, callback) => handler(request, result => {
|
|
10303
|
-
if (
|
|
10304
|
-
if (result && typeof result === 'object' && result.path
|
|
10305
|
-
result = {...result, path:
|
|
10721
|
+
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});
|
|
10722
|
+
if (result && typeof result === 'object' && Object.hasOwn(redirects, result.path)) {
|
|
10723
|
+
result = {...result, path: redirects[result.path]};
|
|
10306
10724
|
fs.writeFileSync(${JSON.stringify(join15(plan.profileDir, "renderer-local-mode-served"))}, 'ready', {mode: 0o600});
|
|
10307
10725
|
}
|
|
10308
10726
|
callback(result);
|
|
@@ -10312,7 +10730,7 @@ function installProtocolHook() {
|
|
|
10312
10730
|
}
|
|
10313
10731
|
if (app.isReady()) installProtocolHook(); else app.once('ready', installProtocolHook);
|
|
10314
10732
|
require('node:fs').writeFileSync(${JSON.stringify(join15(plan.profileDir, "hook-loaded"))}, 'ready', {mode: 0o600});
|
|
10315
|
-
` });
|
|
10733
|
+
` + (catalog ? modelBrandingHook(catalog.models) : "") });
|
|
10316
10734
|
const point = mainSource.slice(0, mainMatch.index + mainMatch[0].length).split(`
|
|
10317
10735
|
`);
|
|
10318
10736
|
const logPath = join15(plan.profileDir, "inspector.log");
|
|
@@ -10348,7 +10766,7 @@ var TOOL_OPTIONS = [
|
|
|
10348
10766
|
var DESKTOP_TOOL_OPTIONS = [
|
|
10349
10767
|
{ value: "codex-desktop", label: "Codex Desktop", hint: "Responses API with an isolated app profile" },
|
|
10350
10768
|
{ value: "antigravity-desktop", label: "Antigravity Desktop", hint: "ClosedRouter routing through your antigravity-mods hook" },
|
|
10351
|
-
{ value: "cursor-desktop", label: "Cursor Desktop", hint: "
|
|
10769
|
+
{ value: "cursor-desktop", label: "Cursor Desktop", hint: "Local model engine using your ClosedRouter pool" },
|
|
10352
10770
|
{ value: "claude-desktop", label: "Claude Desktop", hint: "Inference gateway with an isolated app profile" }
|
|
10353
10771
|
];
|
|
10354
10772
|
function getStartToolOptions(desktopOnly = false) {
|
|
@@ -10378,7 +10796,7 @@ function assertNoManagedCodexArgs(args) {
|
|
|
10378
10796
|
throw new Error(`Remove ${conflicting}; ClosedRouter manages the Codex profile and model. Put other arguments after --.`);
|
|
10379
10797
|
}
|
|
10380
10798
|
}
|
|
10381
|
-
function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir14(), executablePath, agyRouting) {
|
|
10799
|
+
function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir14(), executablePath, agyRouting, catalog) {
|
|
10382
10800
|
const baseUrl = normalizeServerUrl(serverUrl);
|
|
10383
10801
|
const env = { ...process.env };
|
|
10384
10802
|
const command = executablePath || tool;
|
|
@@ -10398,7 +10816,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
10398
10816
|
return {
|
|
10399
10817
|
tool,
|
|
10400
10818
|
command,
|
|
10401
|
-
args: ["--model", model, ...toolArgs],
|
|
10819
|
+
args: ["--model", model, ...catalog ? ["--settings", JSON.stringify({ modelPicker: claudeModelPicker(catalog.models) })] : [], ...toolArgs],
|
|
10402
10820
|
env
|
|
10403
10821
|
};
|
|
10404
10822
|
}
|
|
@@ -10407,9 +10825,11 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
10407
10825
|
const codexHome = process.env.CODEX_HOME || join16(homeDir, ".codex");
|
|
10408
10826
|
const profileName = "closedrouter-launch";
|
|
10409
10827
|
const configPath = join16(codexHome, `${profileName}.config.toml`);
|
|
10828
|
+
const catalogPath = join16(codexHome, "closedrouter-models.json");
|
|
10410
10829
|
const configLines = [
|
|
10411
10830
|
`model = ${tomlString(model)}`,
|
|
10412
10831
|
`model_provider = ${tomlString(profileName)}`,
|
|
10832
|
+
...catalog ? [`model_catalog_json = ${tomlString(catalogPath)}`] : [],
|
|
10413
10833
|
"",
|
|
10414
10834
|
`[model_providers.${profileName}]`,
|
|
10415
10835
|
`name = ${tomlString("ClosedRouter")}`,
|
|
@@ -10425,7 +10845,8 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
10425
10845
|
args: ["--profile", profileName, ...toolArgs],
|
|
10426
10846
|
env,
|
|
10427
10847
|
configFile: { path: configPath, contents: configLines.join(`
|
|
10428
|
-
`) }
|
|
10848
|
+
`) },
|
|
10849
|
+
...catalog ? { extraFiles: [{ path: catalogPath, contents: JSON.stringify(codexModelCatalog(catalog.models)) }] } : {}
|
|
10429
10850
|
};
|
|
10430
10851
|
}
|
|
10431
10852
|
if (tool === "gemini") {
|
|
@@ -10441,7 +10862,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
10441
10862
|
env,
|
|
10442
10863
|
configFile: {
|
|
10443
10864
|
path: configPath,
|
|
10444
|
-
contents: `${JSON.stringify({ security: { auth: { selectedType: "gemini-api-key" } } }, null, 2)}
|
|
10865
|
+
contents: `${JSON.stringify({ security: { auth: { selectedType: "gemini-api-key" } }, ...catalog ? geminiModelCatalog(catalog.models, model) : {} }, null, 2)}
|
|
10445
10866
|
`
|
|
10446
10867
|
}
|
|
10447
10868
|
};
|
|
@@ -10467,8 +10888,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
10467
10888
|
return {
|
|
10468
10889
|
tool,
|
|
10469
10890
|
command,
|
|
10470
|
-
args: ["--model", `openai/${model}`, ...toolArgs],
|
|
10471
|
-
env
|
|
10891
|
+
args: ["--model", `openai/${catalog ? "closedrouter/" : ""}${model}`, ...catalog ? ["--model-metadata-file", join16(homeDir, ".closedrouter", "aider-models.json")] : [], ...toolArgs],
|
|
10892
|
+
env,
|
|
10893
|
+
...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
10894
|
};
|
|
10473
10895
|
}
|
|
10474
10896
|
const config = {
|
|
@@ -10481,7 +10903,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
10481
10903
|
baseURL: `${baseUrl}/v1`,
|
|
10482
10904
|
apiKey: "{env:CLOSEDROUTER_API_KEY}"
|
|
10483
10905
|
},
|
|
10484
|
-
models:
|
|
10906
|
+
models: Object.fromEntries((catalog?.models || [model]).map((id) => [id, { name: modelLabel(id) }]))
|
|
10485
10907
|
}
|
|
10486
10908
|
}
|
|
10487
10909
|
};
|
|
@@ -10495,9 +10917,9 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
10495
10917
|
};
|
|
10496
10918
|
}
|
|
10497
10919
|
function writeOwnedConfig(file) {
|
|
10498
|
-
|
|
10920
|
+
mkdirSync5(dirname5(file.path), { recursive: true, mode: 448 });
|
|
10499
10921
|
try {
|
|
10500
|
-
const existing =
|
|
10922
|
+
const existing = readFileSync14(file.path, "utf8");
|
|
10501
10923
|
if (file.path.endsWith(".config.toml") && !existing.includes('name = "ClosedRouter"')) {
|
|
10502
10924
|
throw new Error(`Refusing to replace an existing Codex profile at ${file.path}.`);
|
|
10503
10925
|
}
|
|
@@ -10505,7 +10927,7 @@ function writeOwnedConfig(file) {
|
|
|
10505
10927
|
if (error instanceof Error && error.message.startsWith("Refusing to replace"))
|
|
10506
10928
|
throw error;
|
|
10507
10929
|
}
|
|
10508
|
-
|
|
10930
|
+
writeFileSync5(file.path, file.contents, { encoding: "utf8", mode: 384 });
|
|
10509
10931
|
}
|
|
10510
10932
|
function modelSupportsHarness(model, tool) {
|
|
10511
10933
|
const parameters = model.supported_parameters;
|
|
@@ -10536,7 +10958,7 @@ function assertAgyCustomModel(command, label, apiKey) {
|
|
|
10536
10958
|
function assertAgyGeminiMode(homeDir) {
|
|
10537
10959
|
const settingsPath = join16(homeDir, ".gemini", "antigravity-cli", "settings.json");
|
|
10538
10960
|
try {
|
|
10539
|
-
const settings = JSON.parse(
|
|
10961
|
+
const settings = JSON.parse(readFileSync14(settingsPath, "utf8"));
|
|
10540
10962
|
if (settings?.modelProvider === "gemini")
|
|
10541
10963
|
return;
|
|
10542
10964
|
} catch {}
|
|
@@ -10600,11 +11022,12 @@ async function startCommand(options = {}) {
|
|
|
10600
11022
|
}
|
|
10601
11023
|
if (desktop && tool === "claude") {
|
|
10602
11024
|
assertClaudeDesktopGatewaySupport(executablePath);
|
|
10603
|
-
if (
|
|
11025
|
+
if (process.platform === "darwin" && executablePath.endsWith(".app")) {
|
|
10604
11026
|
sharedClaudeElectron = findClaudeSharedElectron(executablePath);
|
|
10605
|
-
if (sharedClaudeElectron)
|
|
10606
|
-
|
|
10607
|
-
|
|
11027
|
+
if (sharedClaudeElectron) {
|
|
11028
|
+
sharedClaudeElectron = prepareClaudeElectronRuntime(sharedClaudeElectron, executablePath);
|
|
11029
|
+
M2.info("Using the lightweight Claude launcher with Cowork virtualization support. Existing framework files are shared.");
|
|
11030
|
+
} else if (options.desktopInspector)
|
|
10608
11031
|
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
11032
|
}
|
|
10610
11033
|
}
|
|
@@ -10643,14 +11066,15 @@ async function startCommand(options = {}) {
|
|
|
10643
11066
|
if (!isGatewayModelAvailable(availableModels, model)) {
|
|
10644
11067
|
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
11068
|
}
|
|
11069
|
+
const catalog = createLaunchCatalog(availableModels, model);
|
|
10646
11070
|
if (desktop && tool === "cursor") {
|
|
10647
11071
|
const localKey = randomUUID5();
|
|
10648
|
-
const relay = await startLaunchProxy(serverUrl, apiKey, "cursor-desktop", localKey,
|
|
11072
|
+
const relay = await startLaunchProxy(serverUrl, apiKey, "cursor-desktop", localKey, catalog);
|
|
10649
11073
|
try {
|
|
10650
11074
|
const plan = buildDesktopLaunchPlan("cursor", executablePath, createDesktopProfile("cursor"), undefined, undefined, undefined, options.toolArgs);
|
|
10651
|
-
addCursorDesktopLocalHook(plan, executablePath);
|
|
11075
|
+
addCursorDesktopLocalHook(plan, executablePath, catalog);
|
|
10652
11076
|
writeDesktopLaunchPlan(plan);
|
|
10653
|
-
await configureCursorDesktop(plan.profileDir, relay.baseUrl, localKey, model);
|
|
11077
|
+
await configureCursorDesktop(plan.profileDir, relay.baseUrl, localKey, model, undefined, catalog.models);
|
|
10654
11078
|
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
11079
|
process.exitCode = await runDesktopLaunch(plan);
|
|
10656
11080
|
} finally {
|
|
@@ -10660,18 +11084,18 @@ async function startCommand(options = {}) {
|
|
|
10660
11084
|
}
|
|
10661
11085
|
if (desktop && (tool === "codex" || tool === "claude" || tool === "agy")) {
|
|
10662
11086
|
const localKey = randomUUID5();
|
|
10663
|
-
const relay = await startLaunchProxy(serverUrl, apiKey, `${tool}-desktop`, localKey,
|
|
11087
|
+
const relay = await startLaunchProxy(serverUrl, apiKey, `${tool}-desktop`, localKey, catalog);
|
|
10664
11088
|
let agyDesktopProxy;
|
|
10665
11089
|
try {
|
|
10666
|
-
const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), relay.baseUrl, localKey, model, options.toolArgs);
|
|
10667
|
-
if (
|
|
11090
|
+
const plan = buildDesktopLaunchPlan(tool, executablePath, createDesktopProfile(tool), relay.baseUrl, localKey, model, options.toolArgs, process.platform, catalog.models);
|
|
11091
|
+
if (sharedClaudeElectron && tool === "claude") {
|
|
10668
11092
|
if (sharedClaudeElectron)
|
|
10669
|
-
useClaudeSharedElectron(plan, sharedClaudeElectron, sourceApp, options.toolArgs);
|
|
10670
|
-
addClaudeDesktopHook(plan, relay.baseUrl, localKey,
|
|
11093
|
+
useClaudeSharedElectron(plan, sharedClaudeElectron, sourceApp, options.toolArgs, true);
|
|
11094
|
+
addClaudeDesktopHook(plan, relay.baseUrl, localKey, sourceApp, catalog.models);
|
|
10671
11095
|
}
|
|
10672
11096
|
if (tool === "agy") {
|
|
10673
|
-
agyDesktopProxy = await startAgyDesktopProxy(relay.baseUrl, localKey, model);
|
|
10674
|
-
addAntigravityDesktopHook(plan, agyDesktopProxy.baseUrl);
|
|
11097
|
+
agyDesktopProxy = await startAgyDesktopProxy(relay.baseUrl, localKey, model, undefined, catalog);
|
|
11098
|
+
addAntigravityDesktopHook(plan, agyDesktopProxy.baseUrl, catalog);
|
|
10675
11099
|
}
|
|
10676
11100
|
writeDesktopLaunchPlan(plan);
|
|
10677
11101
|
M2.info(`Starting ${tool} desktop with ${model} through ${serverUrl}. Profile: ${plan.profileDir}. Keep cr running until you close this app instance.`);
|
|
@@ -10684,16 +11108,18 @@ async function startCommand(options = {}) {
|
|
|
10684
11108
|
}
|
|
10685
11109
|
if (tool === "agy")
|
|
10686
11110
|
assertAgyGeminiMode(homedir14());
|
|
10687
|
-
const recognizedModel = tool === "agy" ?
|
|
11111
|
+
const recognizedModel = tool === "agy" ? registerAgyLaunchCatalog(homedir14(), catalog, discoverAgyTransportModel(options.executablePath || "agy", apiKey)) : undefined;
|
|
10688
11112
|
if (recognizedModel)
|
|
10689
11113
|
assertAgyCustomModel(options.executablePath || "agy", recognizedModel, apiKey);
|
|
10690
|
-
const relay = await startLaunchProxy(serverUrl, apiKey, tool);
|
|
11114
|
+
const relay = await startLaunchProxy(serverUrl, apiKey, tool, apiKey, catalog);
|
|
10691
11115
|
let agyProxy;
|
|
10692
11116
|
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);
|
|
11117
|
+
agyProxy = tool === "agy" ? await startAgyModelProxy(relay.baseUrl, apiKey, model, catalog) : undefined;
|
|
11118
|
+
const plan = buildLaunchPlan(tool, relay.baseUrl, apiKey, model, options.toolArgs, homedir14(), executablePath, agyProxy && recognizedModel ? { baseUrl: agyProxy.baseUrl, recognizedModel } : undefined, catalog);
|
|
10695
11119
|
if (plan.configFile)
|
|
10696
11120
|
writeOwnedConfig(plan.configFile);
|
|
11121
|
+
for (const file of plan.extraFiles || [])
|
|
11122
|
+
writeOwnedConfig(file);
|
|
10697
11123
|
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
11124
|
const exitCode = await runTool(plan);
|
|
10699
11125
|
process.exitCode = exitCode;
|
|
@@ -10748,7 +11174,7 @@ process.on("warning", (warning) => {
|
|
|
10748
11174
|
console.warn(warning.name, warning.message);
|
|
10749
11175
|
});
|
|
10750
11176
|
var program2 = new Command2;
|
|
10751
|
-
var cliVersion = JSON.parse(
|
|
11177
|
+
var cliVersion = JSON.parse(readFileSync15(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
10752
11178
|
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
11179
|
await onboardCommand({ server: options.server });
|
|
10754
11180
|
});
|