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