@closedrouter/cli 1.1.3 → 1.1.4

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 gemini-3.8-flash
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`; its launcher uses `GEMINI_API_KEY` and `GOOGLE_GEMINI_BASE_URL`. The launchers do not change the tools' main configuration files.
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"}
@@ -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): LaunchPlan;
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;AAYH,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,GACtB,UAAU,CAgHZ;AAmBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAMzF;AAsDD,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCnF"}
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
@@ -8611,6 +8611,62 @@ import { execFileSync } from "node:child_process";
8611
8611
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
8612
8612
  import { homedir as homedir9 } from "node:os";
8613
8613
  import { dirname, join as join9 } from "node:path";
8614
+
8615
+ // src/commands/agy-proxy.ts
8616
+ import { createServer as createServer2 } from "node:http";
8617
+ import { request as httpRequest } from "node:http";
8618
+ import { request as httpsRequest } from "node:https";
8619
+ async function startAgyModelProxy(gatewayUrl, apiKey, model) {
8620
+ const upstream = new URL(gatewayUrl.replace(/\/+$/, "") + "/");
8621
+ if (!["https:", "http:"].includes(upstream.protocol)) {
8622
+ throw new Error("ClosedRouter server URL must use HTTP or HTTPS.");
8623
+ }
8624
+ const requestUpstream = upstream.protocol === "https:" ? httpsRequest : httpRequest;
8625
+ const server = createServer2((incoming, outgoing) => {
8626
+ if (incoming.headers["x-goog-api-key"] !== apiKey) {
8627
+ outgoing.writeHead(401).end();
8628
+ return;
8629
+ }
8630
+ const requestUrl = new URL(incoming.url || "/", "http://localhost");
8631
+ const match = /^\/v1beta\/models\/[^/:]+:(generateContent|streamGenerateContent)$/.exec(requestUrl.pathname);
8632
+ if (incoming.method !== "POST" || !match) {
8633
+ outgoing.writeHead(404).end();
8634
+ return;
8635
+ }
8636
+ const target = new URL(`v1beta/models/${encodeURIComponent(model)}:${match[1]}${requestUrl.search}`, upstream);
8637
+ const headers = { ...incoming.headers, host: target.host, "x-goog-api-key": apiKey };
8638
+ const upstreamRequest = requestUpstream(target, { method: "POST", headers }, (response) => {
8639
+ outgoing.writeHead(response.statusCode || 502, response.headers);
8640
+ response.pipe(outgoing);
8641
+ });
8642
+ upstreamRequest.on("error", () => {
8643
+ if (!outgoing.headersSent) {
8644
+ outgoing.writeHead(502, { "content-type": "application/json" });
8645
+ outgoing.end(JSON.stringify({ error: { message: "ClosedRouter gateway connection failed." } }));
8646
+ } else {
8647
+ outgoing.destroy();
8648
+ }
8649
+ });
8650
+ incoming.on("aborted", () => upstreamRequest.destroy());
8651
+ incoming.pipe(upstreamRequest);
8652
+ });
8653
+ await new Promise((resolve, reject) => {
8654
+ server.once("error", reject);
8655
+ server.listen(0, "127.0.0.1", () => {
8656
+ server.off("error", reject);
8657
+ resolve();
8658
+ });
8659
+ });
8660
+ const address = server.address();
8661
+ if (!address || typeof address === "string")
8662
+ throw new Error("Could not start the Antigravity model adapter.");
8663
+ return {
8664
+ baseUrl: `http://127.0.0.1:${address.port}`,
8665
+ close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
8666
+ };
8667
+ }
8668
+
8669
+ // src/commands/start.ts
8614
8670
  function isGatewayModelAvailable(availableModels, model) {
8615
8671
  return availableModels.includes(model);
8616
8672
  }
@@ -8634,7 +8690,7 @@ function assertNoManagedCodexArgs(args) {
8634
8690
  throw new Error(`Remove ${conflicting}; ClosedRouter manages the Codex profile and model. Put other arguments after --.`);
8635
8691
  }
8636
8692
  }
8637
- function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir9(), executablePath) {
8693
+ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir = homedir9(), executablePath, agyRouting) {
8638
8694
  const baseUrl = normalizeServerUrl(serverUrl);
8639
8695
  const env = { ...process.env };
8640
8696
  const command = executablePath || tool;
@@ -8700,15 +8756,17 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
8700
8756
  };
8701
8757
  }
8702
8758
  if (tool === "agy") {
8759
+ if (!agyRouting)
8760
+ throw new Error("Antigravity requires a local ClosedRouter model adapter.");
8703
8761
  env.GEMINI_API_KEY = apiKey;
8704
- env.GOOGLE_GEMINI_BASE_URL = baseUrl;
8762
+ env.GOOGLE_GEMINI_BASE_URL = agyRouting.baseUrl;
8705
8763
  const conflicting = toolArgs.find((arg) => arg === "--model" || arg.startsWith("--model=") || arg === "--effort" || arg.startsWith("--effort="));
8706
8764
  if (conflicting)
8707
8765
  throw new Error(`Remove ${conflicting}; ClosedRouter manages the Antigravity model and effort.`);
8708
8766
  return {
8709
8767
  tool,
8710
8768
  command,
8711
- args: ["--model", model, "--effort", "high", ...toolArgs],
8769
+ args: ["--model", agyRouting.recognizedModel, "--effort", "high", ...toolArgs],
8712
8770
  env
8713
8771
  };
8714
8772
  }
@@ -8763,7 +8821,7 @@ function modelId(model) {
8763
8821
  }
8764
8822
  function modelSupportsHarness(model, tool) {
8765
8823
  const parameters = model.supported_parameters;
8766
- return (tool !== "agy" || modelId(model).startsWith("gemini-")) && (model.status === undefined || model.status === "available") && (model.available_tool_account_count === undefined || model.available_tool_account_count > 0) && (!Array.isArray(parameters) || parameters.includes("tools"));
8824
+ 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
8825
  }
8768
8826
  function discoverAgyModels(command) {
8769
8827
  try {
@@ -8773,6 +8831,15 @@ function discoverAgyModels(command) {
8773
8831
  throw new Error("Could not list Antigravity CLI models. Install or update `agy`, then try `cr start agy` again.");
8774
8832
  }
8775
8833
  }
8834
+ function assertAgyGeminiMode(homeDir) {
8835
+ const settingsPath = join9(homeDir, ".gemini", "antigravity-cli", "settings.json");
8836
+ try {
8837
+ const settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
8838
+ if (settings?.modelProvider === "gemini")
8839
+ return;
8840
+ } catch {}
8841
+ throw new Error(`Set "modelProvider": "gemini" in ${settingsPath} before starting Antigravity through ClosedRouter.`);
8842
+ }
8776
8843
  async function selectTool(tool) {
8777
8844
  const cleaned = tool?.toLowerCase();
8778
8845
  if (cleaned) {
@@ -8818,11 +8885,7 @@ async function startCommand(options = {}) {
8818
8885
  const apiKey = await ensurePlatformKey(tool, client, options);
8819
8886
  client.setApiKey(apiKey);
8820
8887
  const models = await client.listGatewayModels({ preferApiKey: true });
8821
- let availableModels = models.filter((item) => modelSupportsHarness(item, tool)).map(modelId).filter(Boolean).sort();
8822
- if (tool === "agy") {
8823
- const supportedModels = discoverAgyModels(options.executablePath || "agy");
8824
- availableModels = availableModels.filter((id) => supportedModels.has(id));
8825
- }
8888
+ const availableModels = models.filter((item) => modelSupportsHarness(item, tool)).map(modelId).filter(Boolean).sort();
8826
8889
  let model = options.model?.trim();
8827
8890
  if (!model) {
8828
8891
  if (availableModels.length === 0) {
@@ -8842,12 +8905,24 @@ async function startCommand(options = {}) {
8842
8905
  if (!isGatewayModelAvailable(availableModels, model)) {
8843
8906
  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
8907
  }
8845
- const plan = buildLaunchPlan(tool, serverUrl, apiKey, model, options.toolArgs, homedir9(), options.executablePath);
8846
- if (plan.configFile)
8847
- writeOwnedConfig(plan.configFile);
8848
- 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)}`);
8849
- const exitCode = await runTool(plan);
8850
- process.exitCode = exitCode;
8908
+ if (tool === "agy")
8909
+ assertAgyGeminiMode(homedir9());
8910
+ const supportedAgyModels = tool === "agy" ? discoverAgyModels(options.executablePath || "agy") : undefined;
8911
+ if (supportedAgyModels && supportedAgyModels.size === 0) {
8912
+ throw new Error("Antigravity did not report any Gemini API models. Update `agy` and select Gemini API key mode in its settings.");
8913
+ }
8914
+ const recognizedModel = supportedAgyModels ? supportedAgyModels.has("gemini-3.8-flash") ? "gemini-3.8-flash" : [...supportedAgyModels][0] : undefined;
8915
+ const agyProxy = tool === "agy" ? await startAgyModelProxy(serverUrl, apiKey, model) : undefined;
8916
+ try {
8917
+ const plan = buildLaunchPlan(tool, serverUrl, apiKey, model, options.toolArgs, homedir9(), options.executablePath, agyProxy && recognizedModel ? { baseUrl: agyProxy.baseUrl, recognizedModel } : undefined);
8918
+ if (plan.configFile)
8919
+ writeOwnedConfig(plan.configFile);
8920
+ 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)}`);
8921
+ const exitCode = await runTool(plan);
8922
+ process.exitCode = exitCode;
8923
+ } finally {
8924
+ await agyProxy?.close();
8925
+ }
8851
8926
  }
8852
8927
 
8853
8928
  // src/commands/whoami.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closedrouter/cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "ClosedRouter CLI: Onboard and import local AI subscriptions and API keys into your shared token pool",
5
5
  "author": "ClosedRouter Contributors",
6
6
  "repository": {