@closedrouter/cli 1.1.3 → 1.1.5
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
CHANGED
|
@@ -135,7 +135,7 @@ cr start
|
|
|
135
135
|
cr start claude --model claude-sonnet-4-6
|
|
136
136
|
cr start codex --model gpt-4.1
|
|
137
137
|
cr start gemini --model gemini-2.5-flash
|
|
138
|
-
cr start agy --model
|
|
138
|
+
cr start agy --model gpt-5-nano
|
|
139
139
|
cr start aider --model gpt-4o-mini
|
|
140
140
|
cr start opencode --model gpt-4o-mini
|
|
141
141
|
|
|
@@ -143,7 +143,7 @@ cr start opencode --model gpt-4o-mini
|
|
|
143
143
|
cr start claude --model claude-sonnet-4-6 -- --resume
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
Claude Code uses the Anthropic-compatible endpoint and can route through any eligible provider account. Codex gets a separate `~/.codex/closedrouter-launch.config.toml` profile using the Responses API. Gemini gets an app-owned authentication settings file under `~/.closedrouter/`, and OpenCode receives inline launch configuration. Antigravity CLI requires `modelProvider: "gemini"` in `~/.gemini/antigravity-cli/settings.json
|
|
146
|
+
Claude Code uses the Anthropic-compatible endpoint and can route through any eligible provider account. Codex gets a separate `~/.codex/closedrouter-launch.config.toml` profile using the Responses API. Gemini gets an app-owned authentication settings file under `~/.closedrouter/`, and OpenCode receives inline launch configuration. Antigravity CLI requires `modelProvider: "gemini"` in `~/.gemini/antigravity-cli/settings.json`. Its launcher starts a local adapter that maps Antigravity's built-in Gemini model name to the tool-capable pool model you select. The adapter binds only to localhost and stops when Antigravity exits. The launchers do not change the tools' main configuration files.
|
|
147
147
|
|
|
148
148
|
Cursor SDK keys can also be connected in the web account dialog or imported from `CURSOR_API_KEY`. ClosedRouter discovers models using Cursor's SDK and sends text-only, tools-disabled SDK runs through its normal gateway and telemetry path. This credential type is separate from Cursor OAuth tokens imported from the editor.
|
|
149
149
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface AgyModelProxy {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
close(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
/** Antigravity only accepts its built-in Gemini slugs. Translate those slugs
|
|
6
|
+
* at the transport boundary so its normal Gemini requests reach the selected
|
|
7
|
+
* ClosedRouter pool model, including title and other background requests. */
|
|
8
|
+
export declare function startAgyModelProxy(gatewayUrl: string, apiKey: string, model: string): Promise<AgyModelProxy>;
|
|
9
|
+
//# sourceMappingURL=agy-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agy-proxy.d.ts","sourceRoot":"","sources":["../../src/commands/agy-proxy.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;6EAE6E;AAC7E,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,CAAC,CA+CxB"}
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -25,7 +25,10 @@ export interface LaunchPlan {
|
|
|
25
25
|
configFile?: ConfigFile;
|
|
26
26
|
}
|
|
27
27
|
export declare function isGatewayModelAvailable(availableModels: string[], model: string): boolean;
|
|
28
|
-
export declare function buildLaunchPlan(tool: CliTool, serverUrl: string, apiKey: string, model: string, toolArgs?: string[], homeDir?: string, executablePath?: string
|
|
28
|
+
export declare function buildLaunchPlan(tool: CliTool, serverUrl: string, apiKey: string, model: string, toolArgs?: string[], homeDir?: string, executablePath?: string, agyRouting?: {
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
recognizedModel: string;
|
|
31
|
+
}): LaunchPlan;
|
|
29
32
|
export declare function modelSupportsHarness(model: Record<string, any>, tool: StartTool): boolean;
|
|
30
33
|
export declare function startCommand(options?: StartCommandOptions): Promise<void>;
|
|
31
34
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,OAAO,GAAG,UAAU,CAAC;AACrF,KAAK,OAAO,GAAG,SAAS,CAAC;AAEzB,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzF;AA8BD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,QAAQ,GAAE,MAAM,EAAO,EACvB,OAAO,SAAY,EACnB,cAAc,CAAC,EAAE,MAAM,EACvB,UAAU,CAAC,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,GACxD,UAAU,CAiHZ;AAmBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAKzF;AAiED,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDnF"}
|
package/dist/index.js
CHANGED
|
@@ -5162,6 +5162,9 @@ var {
|
|
|
5162
5162
|
Help
|
|
5163
5163
|
} = import__.default;
|
|
5164
5164
|
|
|
5165
|
+
// src/index.ts
|
|
5166
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
5167
|
+
|
|
5165
5168
|
// ../node_modules/.bun/@clack+prompts@0.11.0/node_modules/@clack/prompts/dist/index.mjs
|
|
5166
5169
|
import { stripVTControlCharacters as S2 } from "node:util";
|
|
5167
5170
|
|
|
@@ -8611,6 +8614,62 @@ import { execFileSync } from "node:child_process";
|
|
|
8611
8614
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8612
8615
|
import { homedir as homedir9 } from "node:os";
|
|
8613
8616
|
import { dirname, join as join9 } from "node:path";
|
|
8617
|
+
|
|
8618
|
+
// src/commands/agy-proxy.ts
|
|
8619
|
+
import { createServer as createServer2 } from "node:http";
|
|
8620
|
+
import { request as httpRequest } from "node:http";
|
|
8621
|
+
import { request as httpsRequest } from "node:https";
|
|
8622
|
+
async function startAgyModelProxy(gatewayUrl, apiKey, model) {
|
|
8623
|
+
const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
|
|
8624
|
+
if (!["https:", "http:"].includes(upstream.protocol)) {
|
|
8625
|
+
throw new Error("ClosedRouter server URL must use HTTP or HTTPS.");
|
|
8626
|
+
}
|
|
8627
|
+
const requestUpstream = upstream.protocol === "https:" ? httpsRequest : httpRequest;
|
|
8628
|
+
const server = createServer2((incoming, outgoing) => {
|
|
8629
|
+
if (incoming.headers["x-goog-api-key"] !== apiKey) {
|
|
8630
|
+
outgoing.writeHead(401).end();
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
const requestUrl = new URL(incoming.url || "/", "http://localhost");
|
|
8634
|
+
const match = /^\/v1beta\/models\/[^/:]+:(generateContent|streamGenerateContent)$/.exec(requestUrl.pathname);
|
|
8635
|
+
if (incoming.method !== "POST" || !match) {
|
|
8636
|
+
outgoing.writeHead(404).end();
|
|
8637
|
+
return;
|
|
8638
|
+
}
|
|
8639
|
+
const target = new URL(`v1beta/models/${encodeURIComponent(model)}:${match[1]}${requestUrl.search}`, upstream);
|
|
8640
|
+
const headers = { ...incoming.headers, host: target.host, "x-goog-api-key": apiKey };
|
|
8641
|
+
const upstreamRequest = requestUpstream(target, { method: "POST", headers }, (response) => {
|
|
8642
|
+
outgoing.writeHead(response.statusCode || 502, response.headers);
|
|
8643
|
+
response.pipe(outgoing);
|
|
8644
|
+
});
|
|
8645
|
+
upstreamRequest.on("error", () => {
|
|
8646
|
+
if (!outgoing.headersSent) {
|
|
8647
|
+
outgoing.writeHead(502, { "content-type": "application/json" });
|
|
8648
|
+
outgoing.end(JSON.stringify({ error: { message: "ClosedRouter gateway connection failed." } }));
|
|
8649
|
+
} else {
|
|
8650
|
+
outgoing.destroy();
|
|
8651
|
+
}
|
|
8652
|
+
});
|
|
8653
|
+
incoming.on("aborted", () => upstreamRequest.destroy());
|
|
8654
|
+
incoming.pipe(upstreamRequest);
|
|
8655
|
+
});
|
|
8656
|
+
await new Promise((resolve, reject) => {
|
|
8657
|
+
server.once("error", reject);
|
|
8658
|
+
server.listen(0, "127.0.0.1", () => {
|
|
8659
|
+
server.off("error", reject);
|
|
8660
|
+
resolve();
|
|
8661
|
+
});
|
|
8662
|
+
});
|
|
8663
|
+
const address = server.address();
|
|
8664
|
+
if (!address || typeof address === "string")
|
|
8665
|
+
throw new Error("Could not start the Antigravity model adapter.");
|
|
8666
|
+
return {
|
|
8667
|
+
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
8668
|
+
close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
|
8669
|
+
};
|
|
8670
|
+
}
|
|
8671
|
+
|
|
8672
|
+
// src/commands/start.ts
|
|
8614
8673
|
function isGatewayModelAvailable(availableModels, model) {
|
|
8615
8674
|
return availableModels.includes(model);
|
|
8616
8675
|
}
|
|
@@ -8634,7 +8693,7 @@ function assertNoManagedCodexArgs(args) {
|
|
|
8634
8693
|
throw new Error(`Remove ${conflicting}; ClosedRouter manages the Codex profile and model. Put other arguments after --.`);
|
|
8635
8694
|
}
|
|
8636
8695
|
}
|
|
8637
|
-
function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir9(), executablePath) {
|
|
8696
|
+
function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir9(), executablePath, agyRouting) {
|
|
8638
8697
|
const baseUrl = normalizeServerUrl(serverUrl);
|
|
8639
8698
|
const env = { ...process.env };
|
|
8640
8699
|
const command = executablePath || tool;
|
|
@@ -8700,15 +8759,17 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
|
|
|
8700
8759
|
};
|
|
8701
8760
|
}
|
|
8702
8761
|
if (tool === "agy") {
|
|
8762
|
+
if (!agyRouting)
|
|
8763
|
+
throw new Error("Antigravity requires a local ClosedRouter model adapter.");
|
|
8703
8764
|
env.GEMINI_API_KEY = apiKey;
|
|
8704
|
-
env.GOOGLE_GEMINI_BASE_URL = baseUrl;
|
|
8765
|
+
env.GOOGLE_GEMINI_BASE_URL = agyRouting.baseUrl;
|
|
8705
8766
|
const conflicting = toolArgs.find((arg) => arg === "--model" || arg.startsWith("--model=") || arg === "--effort" || arg.startsWith("--effort="));
|
|
8706
8767
|
if (conflicting)
|
|
8707
8768
|
throw new Error(`Remove ${conflicting}; ClosedRouter manages the Antigravity model and effort.`);
|
|
8708
8769
|
return {
|
|
8709
8770
|
tool,
|
|
8710
8771
|
command,
|
|
8711
|
-
args: ["--model",
|
|
8772
|
+
args: ["--model", agyRouting.recognizedModel, "--effort", "high", ...toolArgs],
|
|
8712
8773
|
env
|
|
8713
8774
|
};
|
|
8714
8775
|
}
|
|
@@ -8763,7 +8824,7 @@ function modelId(model) {
|
|
|
8763
8824
|
}
|
|
8764
8825
|
function modelSupportsHarness(model, tool) {
|
|
8765
8826
|
const parameters = model.supported_parameters;
|
|
8766
|
-
return (
|
|
8827
|
+
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"));
|
|
8767
8828
|
}
|
|
8768
8829
|
function discoverAgyModels(command) {
|
|
8769
8830
|
try {
|
|
@@ -8773,6 +8834,15 @@ function discoverAgyModels(command) {
|
|
|
8773
8834
|
throw new Error("Could not list Antigravity CLI models. Install or update `agy`, then try `cr start agy` again.");
|
|
8774
8835
|
}
|
|
8775
8836
|
}
|
|
8837
|
+
function assertAgyGeminiMode(homeDir) {
|
|
8838
|
+
const settingsPath = join9(homeDir, ".gemini", "antigravity-cli", "settings.json");
|
|
8839
|
+
try {
|
|
8840
|
+
const settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
|
|
8841
|
+
if (settings?.modelProvider === "gemini")
|
|
8842
|
+
return;
|
|
8843
|
+
} catch {}
|
|
8844
|
+
throw new Error(`Set "modelProvider": "gemini" in ${settingsPath} before starting Antigravity through ClosedRouter.`);
|
|
8845
|
+
}
|
|
8776
8846
|
async function selectTool(tool) {
|
|
8777
8847
|
const cleaned = tool?.toLowerCase();
|
|
8778
8848
|
if (cleaned) {
|
|
@@ -8818,11 +8888,7 @@ async function startCommand(options = {}) {
|
|
|
8818
8888
|
const apiKey = await ensurePlatformKey(tool, client, options);
|
|
8819
8889
|
client.setApiKey(apiKey);
|
|
8820
8890
|
const models = await client.listGatewayModels({ preferApiKey: true });
|
|
8821
|
-
|
|
8822
|
-
if (tool === "agy") {
|
|
8823
|
-
const supportedModels = discoverAgyModels(options.executablePath || "agy");
|
|
8824
|
-
availableModels = availableModels.filter((id) => supportedModels.has(id));
|
|
8825
|
-
}
|
|
8891
|
+
const availableModels = models.filter((item) => modelSupportsHarness(item, tool)).map(modelId).filter(Boolean).sort();
|
|
8826
8892
|
let model = options.model?.trim();
|
|
8827
8893
|
if (!model) {
|
|
8828
8894
|
if (availableModels.length === 0) {
|
|
@@ -8842,12 +8908,24 @@ async function startCommand(options = {}) {
|
|
|
8842
8908
|
if (!isGatewayModelAvailable(availableModels, model)) {
|
|
8843
8909
|
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.`);
|
|
8844
8910
|
}
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8911
|
+
if (tool === "agy")
|
|
8912
|
+
assertAgyGeminiMode(homedir9());
|
|
8913
|
+
const supportedAgyModels = tool === "agy" ? discoverAgyModels(options.executablePath || "agy") : undefined;
|
|
8914
|
+
if (supportedAgyModels && supportedAgyModels.size === 0) {
|
|
8915
|
+
throw new Error("Antigravity did not report any Gemini API models. Update `agy` and select Gemini API key mode in its settings.");
|
|
8916
|
+
}
|
|
8917
|
+
const recognizedModel = supportedAgyModels ? supportedAgyModels.has("gemini-3.8-flash") ? "gemini-3.8-flash" : [...supportedAgyModels][0] : undefined;
|
|
8918
|
+
const agyProxy = tool === "agy" ? await startAgyModelProxy(serverUrl, apiKey, model) : undefined;
|
|
8919
|
+
try {
|
|
8920
|
+
const plan = buildLaunchPlan(tool, serverUrl, apiKey, model, options.toolArgs, homedir9(), options.executablePath, agyProxy && recognizedModel ? { baseUrl: agyProxy.baseUrl, recognizedModel } : undefined);
|
|
8921
|
+
if (plan.configFile)
|
|
8922
|
+
writeOwnedConfig(plan.configFile);
|
|
8923
|
+
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)}`);
|
|
8924
|
+
const exitCode = await runTool(plan);
|
|
8925
|
+
process.exitCode = exitCode;
|
|
8926
|
+
} finally {
|
|
8927
|
+
await agyProxy?.close();
|
|
8928
|
+
}
|
|
8851
8929
|
}
|
|
8852
8930
|
|
|
8853
8931
|
// src/commands/whoami.ts
|
|
@@ -8895,7 +8973,8 @@ process.on("warning", (warning) => {
|
|
|
8895
8973
|
console.warn(warning.name, warning.message);
|
|
8896
8974
|
});
|
|
8897
8975
|
var program2 = new Command;
|
|
8898
|
-
|
|
8976
|
+
var cliVersion = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
8977
|
+
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) => {
|
|
8899
8978
|
await onboardCommand({ server: options.server });
|
|
8900
8979
|
});
|
|
8901
8980
|
program2.command("onboard").description("Run the full interactive onboarding wizard").option("-s, --server <url>", "ClosedRouter server base URL").option("-a, --all", "Automatically import all discovered accounts").option("--shared", "Contribute accounts to the shared team pool").action(async (options) => {
|
package/package.json
CHANGED