@closedrouter/cli 1.0.1 → 1.1.0

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
@@ -45,8 +45,8 @@ cr
45
45
 
46
46
  ### `closedrouter` / `closedrouter onboard`
47
47
  Launches the full interactive onboarding wizard:
48
- 1. Connects to your ClosedRouter server (default: `http://localhost:8000`)
49
- 2. Authenticates via email/password, new account registration, or session token
48
+ 1. Connects to the hosted ClosedRouter server (use `--server` for a self-hosted install)
49
+ 2. Opens your browser to sign in or create an account and approve CLI access
50
50
  3. Scans your computer for local AI subscriptions and API keys
51
51
  4. Presents an interactive multi-select checklist of discovered accounts
52
52
  5. Configures sharing policies (Shared Pool vs Private)
@@ -116,10 +116,12 @@ closedrouter keys create "MacBook Pro M3"
116
116
  ```
117
117
 
118
118
  ### `closedrouter login` / `closedrouter logout`
119
- Manage your authentication session. Session tokens are stored securely in `~/.closedrouter/config.json`.
119
+ `cr login` opens the web app. Sign in or create an account, then approve CLI access. The CLI receives a separate 14-day session through a one-time local callback. Credentials are stored in `~/.closedrouter/config.json` with owner-only permissions. Use `--no-browser` for terminal email, signup, or token login.
120
120
  ```bash
121
- closedrouter login --server http://localhost:8000
122
- closedrouter logout
121
+ cr login
122
+ cr login --server http://localhost:8000 # local self-hosted install
123
+ cr login --no-browser # terminal fallback
124
+ cr logout
123
125
  ```
124
126
 
125
127
  ### `cr start` / `cr launch`
@@ -0,0 +1,7 @@
1
+ /** Browser sign-in using a loopback callback and PKCE. */
2
+ import type { AuthResponse } from "./client";
3
+ export declare function browserLogin(serverUrl: string, options?: {
4
+ open?: boolean;
5
+ timeoutMs?: number;
6
+ }): Promise<AuthResponse>;
7
+ //# sourceMappingURL=browser-login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-login.d.ts","sourceRoot":"","sources":["../src/browser-login.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAM1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAc7C,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAuEjI"}
@@ -3,5 +3,6 @@
3
3
  */
4
4
  export declare function loginCommand(options: {
5
5
  server?: string;
6
+ browser?: boolean;
6
7
  }): Promise<void>;
7
8
  //# sourceMappingURL=login.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF9E"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuGjG"}
@@ -1 +1 @@
1
- {"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../../src/commands/onboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyVjH"}
1
+ {"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../../src/commands/onboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuWjH"}
@@ -1 +1 @@
1
- {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAC7E,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;AA6BD,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,CA2FZ;AA6FD,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCnF"}
1
+ {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAC7E,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;AA6BD,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,CA4FZ;AA6FD,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCnF"}
package/dist/index.js CHANGED
@@ -6224,7 +6224,7 @@ class ClosedRouterClient {
6224
6224
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6225
6225
  import { homedir } from "node:os";
6226
6226
  import { join } from "node:path";
6227
- var DEFAULT_SERVER_URL = "http://localhost:8000";
6227
+ var DEFAULT_SERVER_URL = "https://closedrouter.213.183.59.186.sslip.io";
6228
6228
  function getConfigDir() {
6229
6229
  return process.env.CLOSEDROUTER_CONFIG_DIR || join(homedir(), ".closedrouter");
6230
6230
  }
@@ -8006,6 +8006,100 @@ Create one via:
8006
8006
 
8007
8007
  // src/commands/login.ts
8008
8008
  var import_picocolors7 = __toESM(require_picocolors(), 1);
8009
+
8010
+ // src/browser-login.ts
8011
+ import { createHash, randomBytes } from "node:crypto";
8012
+ import { spawn as spawn2 } from "node:child_process";
8013
+ import { createServer } from "node:http";
8014
+ var encode = (bytes) => bytes.toString("base64url");
8015
+ function openBrowser(url) {
8016
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
8017
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
8018
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
8019
+ child.on("error", () => {
8020
+ console.log("Could not open your browser automatically. Open the URL above manually.");
8021
+ });
8022
+ child.unref();
8023
+ }
8024
+ async function browserLogin(serverUrl, options = {}) {
8025
+ const baseUrl = serverUrl.replace(/\/+$/, "");
8026
+ const configResponse = await fetch(`${baseUrl}/api/v1/cli-auth/config`, { signal: AbortSignal.timeout(1e4) });
8027
+ if (!configResponse.ok)
8028
+ throw new Error(`Browser login is unavailable (HTTP ${configResponse.status}).`);
8029
+ const { browser_url: browserUrl } = await configResponse.json();
8030
+ if (!browserUrl || !/^https?:\/\//.test(browserUrl))
8031
+ throw new Error("Server returned an invalid browser URL.");
8032
+ const verifier = encode(randomBytes(32));
8033
+ const challenge = encode(createHash("sha256").update(verifier).digest());
8034
+ const state = encode(randomBytes(32));
8035
+ let timer;
8036
+ let settled = false;
8037
+ let resolveResult;
8038
+ let rejectResult;
8039
+ const result = new Promise((resolve, reject) => {
8040
+ resolveResult = resolve;
8041
+ rejectResult = reject;
8042
+ });
8043
+ const server = createServer(async (request, response) => {
8044
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
8045
+ response.setHeader("Cache-Control", "no-store");
8046
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
8047
+ response.setHeader("Referrer-Policy", "no-referrer");
8048
+ if (request.method !== "GET" || url.pathname !== "/callback" || url.searchParams.get("state") !== state || !url.searchParams.get("code") || settled) {
8049
+ response.writeHead(400).end("Invalid CLI login callback.");
8050
+ return;
8051
+ }
8052
+ settled = true;
8053
+ try {
8054
+ const exchange = await fetch(`${baseUrl}/api/v1/cli-auth/exchange`, {
8055
+ method: "POST",
8056
+ headers: { "Content-Type": "application/json" },
8057
+ body: JSON.stringify({ code: url.searchParams.get("code"), code_verifier: verifier }),
8058
+ signal: AbortSignal.timeout(1e4)
8059
+ });
8060
+ if (!exchange.ok) {
8061
+ const body = await exchange.json().catch(() => ({}));
8062
+ throw new Error(body.error?.message || `Login exchange failed (HTTP ${exchange.status}).`);
8063
+ }
8064
+ const credentials = await exchange.json();
8065
+ if (!credentials.token || !credentials.user?.email)
8066
+ throw new Error("Server returned incomplete credentials.");
8067
+ response.writeHead(200).end("<script>history.replaceState(null, '', '/');</script><h1>ClosedRouter CLI connected</h1><p>You can return to your terminal.</p>");
8068
+ resolveResult(credentials);
8069
+ } catch (error) {
8070
+ response.writeHead(400).end("CLI login failed. Return to your terminal and try again.");
8071
+ rejectResult(error instanceof Error ? error : new Error(String(error)));
8072
+ }
8073
+ });
8074
+ server.on("error", rejectResult);
8075
+ try {
8076
+ await new Promise((resolve, reject) => {
8077
+ server.listen(0, "127.0.0.1", resolve);
8078
+ server.once("error", reject);
8079
+ });
8080
+ const port = server.address().port;
8081
+ const url = new URL("/cli-login", browserUrl);
8082
+ url.searchParams.set("callback", `http://127.0.0.1:${port}/callback`);
8083
+ url.searchParams.set("state", state);
8084
+ url.searchParams.set("challenge", challenge);
8085
+ console.log(`Open this URL to sign in:
8086
+ ${url.toString()}
8087
+ `);
8088
+ if (options.open !== false)
8089
+ openBrowser(url.toString());
8090
+ timer = setTimeout(() => {
8091
+ if (!settled)
8092
+ rejectResult(new Error("Browser login timed out. Run cr login again."));
8093
+ }, options.timeoutMs ?? 300000);
8094
+ return await result;
8095
+ } finally {
8096
+ if (timer)
8097
+ clearTimeout(timer);
8098
+ server.close();
8099
+ }
8100
+ }
8101
+
8102
+ // src/commands/login.ts
8009
8103
  async function loginCommand(options) {
8010
8104
  Ie(import_picocolors7.default.bgCyan(import_picocolors7.default.black(" ClosedRouter Login ")));
8011
8105
  const config = loadConfig();
@@ -8019,6 +8113,22 @@ async function loginCommand(options) {
8019
8113
  process.exit(1);
8020
8114
  }
8021
8115
  s.stop(import_picocolors7.default.green(`Connected to ${serverUrl}`));
8116
+ if (options.browser !== false) {
8117
+ try {
8118
+ const res = await browserLogin(serverUrl);
8119
+ client.setSessionToken(res.token);
8120
+ const verified = await client.getSession();
8121
+ if (!verified)
8122
+ throw new Error("The browser session could not be verified.");
8123
+ saveConfig({ serverUrl, sessionToken: res.token, user: verified });
8124
+ Se(import_picocolors7.default.green(`Logged in as ${verified.email}.`));
8125
+ return;
8126
+ } catch (error) {
8127
+ Me(error instanceof Error ? error.message : String(error), "Browser login failed");
8128
+ process.exitCode = 1;
8129
+ return;
8130
+ }
8131
+ }
8022
8132
  const method = await ve({
8023
8133
  message: "Select login method:",
8024
8134
  options: [
@@ -8155,6 +8265,7 @@ Or start the backend on port 8000.`, "Server Unreachable");
8155
8265
  const authMethod = await ve({
8156
8266
  message: "How would you like to authenticate with ClosedRouter?",
8157
8267
  options: [
8268
+ { value: "browser", label: "Sign in with browser" },
8158
8269
  { value: "login", label: "Sign in with email and password" },
8159
8270
  { value: "signup", label: "Create a new account" },
8160
8271
  { value: "token", label: "Paste existing session token or API key" }
@@ -8164,7 +8275,21 @@ Or start the backend on port 8000.`, "Server Unreachable");
8164
8275
  xe("Onboarding cancelled.");
8165
8276
  process.exit(0);
8166
8277
  }
8167
- if (authMethod === "signup") {
8278
+ if (authMethod === "browser") {
8279
+ try {
8280
+ const authRes = await browserLogin(serverUrl);
8281
+ client.setSessionToken(authRes.token);
8282
+ const verified = await client.getSession();
8283
+ if (!verified)
8284
+ throw new Error("The browser session could not be verified.");
8285
+ saveConfig({ serverUrl, sessionToken: authRes.token, user: verified });
8286
+ Me(`Logged in as ${verified.email}.`, "Browser Login");
8287
+ } catch (error) {
8288
+ Me(error instanceof Error ? error.message : String(error), "Browser Login Failed");
8289
+ process.exitCode = 1;
8290
+ return;
8291
+ }
8292
+ } else if (authMethod === "signup") {
8168
8293
  const name = await he({
8169
8294
  message: "Your name:",
8170
8295
  placeholder: "Alex Smith"
@@ -8481,7 +8606,7 @@ to scan and import local subscriptions!`, "Pool Empty");
8481
8606
 
8482
8607
  // src/commands/start.ts
8483
8608
  var import_picocolors12 = __toESM(require_picocolors(), 1);
8484
- import { spawn as spawn2 } from "node:child_process";
8609
+ import { spawn as spawn3 } from "node:child_process";
8485
8610
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
8486
8611
  import { homedir as homedir9 } from "node:os";
8487
8612
  import { dirname, join as join9, resolve } from "node:path";
@@ -8540,6 +8665,7 @@ function buildLaunchPlan(tool, serverUrl, apiKey, model, toolArgs = [], homeDir
8540
8665
  `[model_providers.${profileName}]`,
8541
8666
  `name = ${tomlString("ClosedRouter")}`,
8542
8667
  `base_url = ${tomlString(`${baseUrl}/v1/`)}`,
8668
+ 'env_key = "OPENAI_API_KEY"',
8543
8669
  'wire_api = "responses"',
8544
8670
  ""
8545
8671
  ];
@@ -8653,7 +8779,7 @@ async function ensureClaudeBridge(client, serverUrl, key) {
8653
8779
  const script = process.argv[1];
8654
8780
  if (!script)
8655
8781
  throw new Error("No Claude subscription gateway is online. Start one with `cr gateway`.");
8656
- const child = spawn2(process.execPath, [resolve(script), "gateway", "--server", serverUrl, "--key-stdin"], {
8782
+ const child = spawn3(process.execPath, [resolve(script), "gateway", "--server", serverUrl, "--key-stdin"], {
8657
8783
  detached: true,
8658
8784
  env: process.env,
8659
8785
  stdio: ["pipe", "ignore", "ignore"]
@@ -8674,7 +8800,7 @@ async function ensureClaudeBridge(client, serverUrl, key) {
8674
8800
  }
8675
8801
  async function runTool(plan) {
8676
8802
  return await new Promise((resolveExit, rejectExit) => {
8677
- const child = spawn2(plan.command, plan.args, { env: plan.env, stdio: "inherit" });
8803
+ const child = spawn3(plan.command, plan.args, { env: plan.env, stdio: "inherit" });
8678
8804
  child.once("error", rejectExit);
8679
8805
  child.once("exit", (code) => resolveExit(code ?? 1));
8680
8806
  });
@@ -8762,7 +8888,7 @@ process.on("warning", (warning) => {
8762
8888
  console.warn(warning.name, warning.message);
8763
8889
  });
8764
8890
  var program2 = new Command;
8765
- program2.name("closedrouter").description("ClosedRouter CLI: Import local AI subscriptions & API keys into your shared token pool").version("1.0.1").option("-s, --server <url>", "ClosedRouter server base URL (default: http://localhost:8000)").action(async (options) => {
8891
+ program2.name("closedrouter").description("ClosedRouter CLI: Import local AI subscriptions & API keys into your shared token pool").version("1.1.0").option("-s, --server <url>", "ClosedRouter server base URL (default: http://localhost:8000)").action(async (options) => {
8766
8892
  await onboardCommand({ server: options.server });
8767
8893
  });
8768
8894
  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) => {
@@ -8785,8 +8911,8 @@ program2.command("import").description("Scan and import local accounts into Clos
8785
8911
  server: options.server || program2.opts().server
8786
8912
  });
8787
8913
  });
8788
- program2.command("login").description("Log in to ClosedRouter and store session credentials").option("-s, --server <url>", "ClosedRouter server base URL").action(async (options) => {
8789
- await loginCommand({ server: options.server || program2.opts().server });
8914
+ program2.command("login").description("Sign in through your browser and store CLI credentials").option("-s, --server <url>", "ClosedRouter server base URL").option("--no-browser", "Use email, signup, or token login in the terminal").action(async (options) => {
8915
+ await loginCommand({ server: options.server || program2.opts().server, browser: options.browser });
8790
8916
  });
8791
8917
  program2.command("logout").description("Clear stored session credentials").action(() => {
8792
8918
  logoutCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closedrouter/cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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": {