github-router 0.3.8 → 0.3.9

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/dist/main.js CHANGED
@@ -5,16 +5,15 @@ import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { randomBytes, randomUUID } from "node:crypto";
8
- import clipboard from "clipboardy";
8
+ import process$1 from "node:process";
9
9
  import { serve } from "srvx";
10
- import invariant from "tiny-invariant";
11
10
  import { getProxyForUrl } from "proxy-from-env";
12
11
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
13
- import process$1 from "node:process";
14
12
  import { Hono } from "hono";
15
13
  import { cors } from "hono/cors";
16
14
  import { streamSSE } from "hono/streaming";
17
15
  import { events } from "fetch-event-stream";
16
+ import clipboard from "clipboardy";
18
17
 
19
18
  //#region src/lib/paths.ts
20
19
  const APP_DIR = path.join(os.homedir(), ".local", "share", "github-router");
@@ -103,7 +102,7 @@ var HTTPError = class extends Error {
103
102
  async function forwardError(c, error) {
104
103
  consola.error("Error occurred:", error);
105
104
  if (error instanceof HTTPError) {
106
- const errorText = await error.response.text();
105
+ const errorText = await error.response.text().catch(() => "");
107
106
  let errorJson;
108
107
  try {
109
108
  errorJson = JSON.parse(errorText);
@@ -414,78 +413,86 @@ const checkUsage = defineCommand({
414
413
  });
415
414
 
416
415
  //#endregion
417
- //#region src/debug.ts
418
- async function getPackageVersion() {
419
- try {
420
- const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
421
- return JSON.parse(await fs.readFile(packageJsonPath)).version;
422
- } catch {
423
- return "unknown";
424
- }
416
+ //#region src/lib/port.ts
417
+ const DEFAULT_PORT = 8787;
418
+ const DEFAULT_CODEX_MODEL = "gpt5.3-codex";
419
+ const PORT_RANGE_MIN = 11e3;
420
+ const PORT_RANGE_MAX = 65535;
421
+ /** Generate a random port number in the range [11000, 65535]. */
422
+ function generateRandomPort() {
423
+ return Math.floor(Math.random() * (PORT_RANGE_MAX - PORT_RANGE_MIN + 1)) + PORT_RANGE_MIN;
425
424
  }
426
- function getRuntimeInfo() {
427
- const isBun = typeof Bun !== "undefined";
425
+
426
+ //#endregion
427
+ //#region src/lib/launch.ts
428
+ function buildLaunchCommand(target) {
428
429
  return {
429
- name: isBun ? "bun" : "node",
430
- version: isBun ? Bun.version : process.version.slice(1),
431
- platform: os.platform(),
432
- arch: os.arch()
430
+ cmd: target.kind === "claude-code" ? [
431
+ "claude",
432
+ "--dangerously-skip-permissions",
433
+ ...target.extraArgs
434
+ ] : [
435
+ "codex",
436
+ "-m",
437
+ target.model ?? DEFAULT_CODEX_MODEL,
438
+ ...target.extraArgs
439
+ ],
440
+ env: {
441
+ ...process$1.env,
442
+ ...target.envVars
443
+ }
433
444
  };
434
445
  }
435
- async function checkTokenExists() {
446
+ function launchChild(target, server$1) {
447
+ const { cmd, env } = buildLaunchCommand(target);
448
+ const executable = cmd[0];
449
+ if (!Bun.which(executable)) {
450
+ consola.error(`"${executable}" not found on PATH. Install it first, then try again.`);
451
+ process$1.exit(1);
452
+ }
453
+ let child;
436
454
  try {
437
- if (!(await fs.stat(PATHS.GITHUB_TOKEN_PATH)).isFile()) return false;
438
- return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim().length > 0;
439
- } catch {
440
- return false;
455
+ child = Bun.spawn({
456
+ cmd,
457
+ env,
458
+ stdin: "inherit",
459
+ stdout: "inherit",
460
+ stderr: "inherit"
461
+ });
462
+ } catch (error) {
463
+ consola.error(`Failed to launch ${executable}:`, error instanceof Error ? error.message : String(error));
464
+ server$1.close(true).catch(() => {});
465
+ process$1.exit(1);
466
+ }
467
+ let cleaned = false;
468
+ let exiting = false;
469
+ async function cleanup() {
470
+ if (cleaned) return;
471
+ cleaned = true;
472
+ try {
473
+ child.kill();
474
+ } catch {}
475
+ const timeout = setTimeout(() => process$1.exit(1), 5e3);
476
+ try {
477
+ await server$1.close(true);
478
+ } catch {}
479
+ clearTimeout(timeout);
441
480
  }
442
- }
443
- async function getDebugInfo() {
444
- const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
445
- return {
446
- version,
447
- runtime: getRuntimeInfo(),
448
- paths: {
449
- APP_DIR: PATHS.APP_DIR,
450
- GITHUB_TOKEN_PATH: PATHS.GITHUB_TOKEN_PATH
451
- },
452
- tokenExists
481
+ function exit(code) {
482
+ if (exiting) return;
483
+ exiting = true;
484
+ process$1.exit(code);
485
+ }
486
+ const onSignal = () => {
487
+ cleanup().then(() => exit(130)).catch(() => exit(1));
453
488
  };
489
+ process$1.on("SIGINT", onSignal);
490
+ process$1.on("SIGTERM", onSignal);
491
+ child.exited.then(async (exitCode) => {
492
+ await cleanup();
493
+ exit(exitCode ?? 0);
494
+ }).catch(() => exit(1));
454
495
  }
455
- function printDebugInfoPlain(info) {
456
- consola.info(`github-router debug
457
-
458
- Version: ${info.version}
459
- Runtime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})
460
-
461
- Paths:
462
- - APP_DIR: ${info.paths.APP_DIR}
463
- - GITHUB_TOKEN_PATH: ${info.paths.GITHUB_TOKEN_PATH}
464
-
465
- Token exists: ${info.tokenExists ? "Yes" : "No"}`);
466
- }
467
- function printDebugInfoJson(info) {
468
- console.log(JSON.stringify(info, null, 2));
469
- }
470
- async function runDebug(options) {
471
- const debugInfo = await getDebugInfo();
472
- if (options.json) printDebugInfoJson(debugInfo);
473
- else printDebugInfoPlain(debugInfo);
474
- }
475
- const debug = defineCommand({
476
- meta: {
477
- name: "debug",
478
- description: "Print debug information about the application"
479
- },
480
- args: { json: {
481
- type: "boolean",
482
- default: false,
483
- description: "Output debug information as JSON"
484
- } },
485
- run({ args }) {
486
- return runDebug({ json: args.json });
487
- }
488
- });
489
496
 
490
497
  //#endregion
491
498
  //#region src/lib/proxy.ts
@@ -533,69 +540,6 @@ function initProxyFromEnv() {
533
540
  }
534
541
  }
535
542
 
536
- //#endregion
537
- //#region src/lib/shell.ts
538
- function getShell() {
539
- const { platform, env } = process$1;
540
- if (platform === "win32") {
541
- if (env.SHELL) {
542
- if (env.SHELL.endsWith("zsh")) return "zsh";
543
- if (env.SHELL.endsWith("fish")) return "fish";
544
- if (env.SHELL.endsWith("bash")) return "bash";
545
- return "sh";
546
- }
547
- if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return "powershell";
548
- if (env.PSModulePath) {
549
- const lower = env.PSModulePath.toLowerCase();
550
- if (lower.includes("documents\\powershell") || lower.includes("documents\\windowspowershell")) return "powershell";
551
- }
552
- return "cmd";
553
- }
554
- const shellPath = env.SHELL;
555
- if (shellPath) {
556
- if (shellPath.endsWith("zsh")) return "zsh";
557
- if (shellPath.endsWith("fish")) return "fish";
558
- if (shellPath.endsWith("bash")) return "bash";
559
- }
560
- return "sh";
561
- }
562
- function quotePosixValue(value) {
563
- return `'${value.replace(/'/g, "'\\''")}'`;
564
- }
565
- function quotePowerShellValue(value) {
566
- return `'${value.replace(/'/g, "''")}'`;
567
- }
568
- /**
569
- * Generates a copy-pasteable script to set multiple environment variables
570
- * and run a subsequent command.
571
- * @param {EnvVars} envVars - An object of environment variables to set.
572
- * @param {string} commandToRun - The command to run after setting the variables.
573
- * @returns {string} The formatted script string.
574
- */
575
- function generateEnvScript(envVars, commandToRun = "") {
576
- const shell = getShell();
577
- const filteredEnvVars = Object.entries(envVars).filter(([, value]) => value !== void 0);
578
- let commandBlock;
579
- switch (shell) {
580
- case "powershell":
581
- commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`).join("; ");
582
- break;
583
- case "cmd":
584
- commandBlock = filteredEnvVars.map(([key, value]) => `set "${key}=${value}"`).join(" & ");
585
- break;
586
- case "fish":
587
- commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`).join("; ");
588
- break;
589
- default: {
590
- const assignments = filteredEnvVars.map(([key, value]) => `${key}=${quotePosixValue(value)}`).join(" ");
591
- commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
592
- break;
593
- }
594
- }
595
- if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : shell === "powershell" ? "; " : " && "}${commandToRun}`;
596
- return commandBlock || commandToRun;
597
- }
598
-
599
543
  //#endregion
600
544
  //#region src/lib/approval.ts
601
545
  const awaitApproval = async () => {
@@ -1009,8 +953,9 @@ async function handleCompletion$1(c) {
1009
953
  };
1010
954
  if (debugEnabled) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
1011
955
  }
1012
- const response = await createChatCompletions(payload, selectedModel?.requestHeaders).catch((error) => {
1013
- if (error instanceof HTTPError) error.response.clone().text().then((errorBody) => {
956
+ const response = await createChatCompletions(payload, selectedModel?.requestHeaders).catch(async (error) => {
957
+ if (error instanceof HTTPError) {
958
+ const errorBody = await error.response.clone().text().catch(() => "");
1014
959
  logRequest({
1015
960
  method: "POST",
1016
961
  path: c.req.path,
@@ -1019,7 +964,7 @@ async function handleCompletion$1(c) {
1019
964
  status: error.response.status,
1020
965
  errorBody
1021
966
  }, selectedModel, startTime);
1022
- }).catch(() => {});
967
+ }
1023
968
  throw error;
1024
969
  });
1025
970
  const isStreaming = !isNonStreaming$1(response);
@@ -1640,8 +1585,9 @@ async function handleResponses(c) {
1640
1585
  payload.max_output_tokens = selectedModel?.capabilities.limits.max_output_tokens;
1641
1586
  if (debugEnabled) consola.debug("Set max_output_tokens to:", JSON.stringify(payload.max_output_tokens));
1642
1587
  }
1643
- const response = await createResponses(payload, selectedModel?.requestHeaders).catch((error) => {
1644
- if (error instanceof HTTPError) error.response.clone().text().then((errorBody) => {
1588
+ const response = await createResponses(payload, selectedModel?.requestHeaders).catch(async (error) => {
1589
+ if (error instanceof HTTPError) {
1590
+ const errorBody = await error.response.clone().text().catch(() => "");
1645
1591
  logRequest({
1646
1592
  method: "POST",
1647
1593
  path: c.req.path,
@@ -1650,7 +1596,7 @@ async function handleResponses(c) {
1650
1596
  status: error.response.status,
1651
1597
  errorBody
1652
1598
  }, selectedModel, startTime);
1653
- }).catch(() => {});
1599
+ }
1654
1600
  throw error;
1655
1601
  });
1656
1602
  const isStreaming = !isNonStreaming(response);
@@ -1785,84 +1731,9 @@ server.route("/v1/search", searchRoutes);
1785
1731
  server.route("/v1/messages", messageRoutes);
1786
1732
 
1787
1733
  //#endregion
1788
- //#region src/start.ts
1789
- const allowedAccountTypes = new Set([
1790
- "individual",
1791
- "business",
1792
- "enterprise"
1793
- ]);
1794
- function printAndCopyCommand(command, label) {
1795
- consola.box(`${label}\n\n${command}`);
1796
- try {
1797
- clipboard.writeSync(command);
1798
- consola.success(`Copied ${label} command to clipboard!`);
1799
- } catch {
1800
- consola.warn("Failed to copy to clipboard. Copy the command above manually.");
1801
- }
1802
- }
1803
- function filterModelsByEndpoint(models, endpoint) {
1804
- const filtered = models.filter((model) => {
1805
- const endpoints = model.supported_endpoints;
1806
- if (!endpoints || endpoints.length === 0) return true;
1807
- return endpoints.some((entry) => {
1808
- return entry.replace(/^\/?v1\//, "").replace(/^\//, "") === endpoint;
1809
- });
1810
- });
1811
- return filtered.length > 0 ? filtered : models;
1812
- }
1813
- async function generateClaudeCodeCommand(serverUrl) {
1814
- invariant(state.models, "Models should be loaded by now");
1815
- const claudeModels = state.models.data.filter((model) => model.id.toLowerCase().startsWith("claude"));
1816
- if (claudeModels.length === 0) {
1817
- consola.error("No Claude models available from Copilot API");
1818
- return;
1819
- }
1820
- const mainModel = claudeModels.find((m) => m.id.includes("opus")) ?? claudeModels.find((m) => m.id.includes("sonnet")) ?? claudeModels[0];
1821
- const smallModel = claudeModels.find((m) => m.id.includes("haiku")) ?? claudeModels.find((m) => m.id.includes("sonnet")) ?? claudeModels[0];
1822
- let selectedModel = mainModel.id;
1823
- let selectedSmallModel = smallModel.id;
1824
- if (claudeModels.length > 1) {
1825
- consola.info(`Using ${mainModel.id} as main model and ${smallModel.id} as small model`);
1826
- if (await consola.prompt("Override model selection?", {
1827
- type: "confirm",
1828
- initial: false
1829
- })) {
1830
- selectedModel = await consola.prompt("Select a main model for Claude Code", {
1831
- type: "select",
1832
- options: claudeModels.map((model) => model.id)
1833
- });
1834
- selectedSmallModel = await consola.prompt("Select a small/fast model for Claude Code", {
1835
- type: "select",
1836
- options: claudeModels.map((model) => model.id)
1837
- });
1838
- }
1839
- }
1840
- printAndCopyCommand(generateEnvScript({
1841
- ANTHROPIC_BASE_URL: serverUrl,
1842
- ANTHROPIC_AUTH_TOKEN: "dummy",
1843
- ANTHROPIC_MODEL: selectedModel,
1844
- ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
1845
- ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel,
1846
- ANTHROPIC_DEFAULT_HAIKU_MODEL: selectedSmallModel,
1847
- DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
1848
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
1849
- }, "claude --dangerously-skip-permissions"), "Claude Code");
1850
- }
1851
- async function generateCodexCommand(serverUrl) {
1852
- invariant(state.models, "Models should be loaded by now");
1853
- const supportedModels = filterModelsByEndpoint(state.models.data, "responses");
1854
- const defaultCodexModel = supportedModels.find((model) => model.id === "gpt5.2-codex");
1855
- const selectedModel = defaultCodexModel ? defaultCodexModel.id : await consola.prompt("Select a model to use with Codex CLI", {
1856
- type: "select",
1857
- options: supportedModels.map((model) => model.id)
1858
- });
1859
- const quotedModel = JSON.stringify(selectedModel);
1860
- printAndCopyCommand(generateEnvScript({
1861
- OPENAI_BASE_URL: `${serverUrl}/v1`,
1862
- OPENAI_API_KEY: "dummy"
1863
- }, `codex -m ${quotedModel}`), "Codex CLI");
1864
- }
1865
- async function runServer(options) {
1734
+ //#region src/lib/server-setup.ts
1735
+ const MAX_PORT_RETRIES = 10;
1736
+ async function setupAndServe(options) {
1866
1737
  if (options.proxyEnv) initProxyFromEnv();
1867
1738
  if (options.verbose) {
1868
1739
  consola.level = 5;
@@ -1883,115 +1754,443 @@ async function runServer(options) {
1883
1754
  await setupCopilotToken();
1884
1755
  await cacheModels();
1885
1756
  consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
1886
- const serverUrl = `http://localhost:${options.port}`;
1887
- if (options.claudeCode) await generateClaudeCodeCommand(serverUrl);
1888
- if (options.codex) await generateCodexCommand(serverUrl);
1889
- consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);
1890
- serve({
1757
+ const serveOptions = {
1891
1758
  fetch: server.fetch,
1892
1759
  hostname: "127.0.0.1",
1760
+ silent: options.silent
1761
+ };
1762
+ let srvxServer;
1763
+ if (options.port !== void 0) srvxServer = serve({
1764
+ ...serveOptions,
1893
1765
  port: options.port
1894
1766
  });
1767
+ else {
1768
+ let lastError;
1769
+ for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
1770
+ const candidatePort = generateRandomPort();
1771
+ try {
1772
+ srvxServer = serve({
1773
+ ...serveOptions,
1774
+ port: candidatePort
1775
+ });
1776
+ break;
1777
+ } catch (error) {
1778
+ lastError = error;
1779
+ if (!(error instanceof Error && (error.message.includes("EADDRINUSE") || error.message.includes("address already in use") || "code" in error && error.code === "EADDRINUSE"))) throw error;
1780
+ consola.debug(`Port ${candidatePort} in use, trying another...`);
1781
+ }
1782
+ }
1783
+ if (srvxServer === void 0) throw new Error(`Failed to find an available port after ${MAX_PORT_RETRIES} attempts. Specify a port with --port or free some ports. Last error: ${lastError}`);
1784
+ }
1785
+ const url = srvxServer.url;
1786
+ if (!url) throw new Error("Server started but URL is not available");
1787
+ const serverUrl = url.replace(/\/$/, "");
1788
+ return {
1789
+ server: srvxServer,
1790
+ serverUrl
1791
+ };
1895
1792
  }
1896
- const start = defineCommand({
1793
+ /** Shared CLI arg definitions for all server commands. */
1794
+ const sharedServerArgs = {
1795
+ port: {
1796
+ alias: "p",
1797
+ type: "string",
1798
+ description: "Port to listen on"
1799
+ },
1800
+ verbose: {
1801
+ alias: "v",
1802
+ type: "boolean",
1803
+ default: false,
1804
+ description: "Enable verbose logging"
1805
+ },
1806
+ "account-type": {
1807
+ alias: "a",
1808
+ type: "string",
1809
+ default: "enterprise",
1810
+ description: "Account type to use (individual, business, enterprise)"
1811
+ },
1812
+ manual: {
1813
+ type: "boolean",
1814
+ default: false,
1815
+ description: "Enable manual request approval"
1816
+ },
1817
+ "rate-limit": {
1818
+ alias: "r",
1819
+ type: "string",
1820
+ description: "Rate limit in seconds between requests"
1821
+ },
1822
+ wait: {
1823
+ alias: "w",
1824
+ type: "boolean",
1825
+ default: false,
1826
+ description: "Wait instead of error when rate limit is hit. Has no effect if rate limit is not set"
1827
+ },
1828
+ "github-token": {
1829
+ alias: "g",
1830
+ type: "string",
1831
+ description: "Provide GitHub token directly (must be generated using the `auth` subcommand)"
1832
+ },
1833
+ "show-token": {
1834
+ type: "boolean",
1835
+ default: false,
1836
+ description: "Show GitHub and Copilot tokens on fetch and refresh"
1837
+ },
1838
+ "proxy-env": {
1839
+ type: "boolean",
1840
+ default: false,
1841
+ description: "Initialize proxy from environment variables"
1842
+ }
1843
+ };
1844
+ const allowedAccountTypes = new Set([
1845
+ "individual",
1846
+ "business",
1847
+ "enterprise"
1848
+ ]);
1849
+ /** Parse shared server args into ServerSetupOptions fields. */
1850
+ function parseSharedArgs(args) {
1851
+ const portRaw = args.port;
1852
+ let port;
1853
+ if (portRaw !== void 0) {
1854
+ port = Number.parseInt(portRaw, 10);
1855
+ if (Number.isNaN(port) || port <= 0 || port > 65535) throw new Error("Invalid port. Must be between 1 and 65535.");
1856
+ }
1857
+ const accountType = args["account-type"] ?? "enterprise";
1858
+ if (!allowedAccountTypes.has(accountType)) throw new Error("Invalid account type. Must be individual, business, or enterprise.");
1859
+ const rateLimitRaw = args["rate-limit"];
1860
+ let rateLimit;
1861
+ if (rateLimitRaw !== void 0) {
1862
+ rateLimit = Number.parseInt(rateLimitRaw, 10);
1863
+ if (Number.isNaN(rateLimit) || rateLimit <= 0) throw new Error("Invalid rate limit. Must be a positive integer.");
1864
+ }
1865
+ const rateLimitWait = args.wait && rateLimit !== void 0;
1866
+ if (args.wait && rateLimit === void 0) consola.warn("Rate limit wait ignored because no rate limit was set.");
1867
+ const githubToken = args["github-token"] ?? process.env.GH_TOKEN;
1868
+ return {
1869
+ port,
1870
+ verbose: args.verbose,
1871
+ accountType,
1872
+ manual: args.manual,
1873
+ rateLimit,
1874
+ rateLimitWait,
1875
+ githubToken,
1876
+ showToken: args["show-token"],
1877
+ proxyEnv: args["proxy-env"]
1878
+ };
1879
+ }
1880
+ /** Build environment variables for Claude Code. */
1881
+ function getClaudeCodeEnvVars(serverUrl, model) {
1882
+ const vars = {
1883
+ ANTHROPIC_BASE_URL: serverUrl,
1884
+ ANTHROPIC_AUTH_TOKEN: "dummy",
1885
+ DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
1886
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
1887
+ };
1888
+ if (model) vars.ANTHROPIC_MODEL = model;
1889
+ return vars;
1890
+ }
1891
+ /** Build environment variables for Codex CLI. */
1892
+ function getCodexEnvVars(serverUrl) {
1893
+ return {
1894
+ OPENAI_BASE_URL: `${serverUrl}/v1`,
1895
+ OPENAI_API_KEY: "dummy"
1896
+ };
1897
+ }
1898
+
1899
+ //#endregion
1900
+ //#region src/claude.ts
1901
+ const claude = defineCommand({
1897
1902
  meta: {
1898
- name: "start",
1899
- description: "Start the github-router server"
1903
+ name: "claude",
1904
+ description: "Start the proxy server and launch Claude Code"
1900
1905
  },
1901
1906
  args: {
1902
- port: {
1903
- alias: "p",
1904
- type: "string",
1905
- default: "8787",
1906
- description: "Port to listen on"
1907
- },
1908
- verbose: {
1909
- alias: "v",
1910
- type: "boolean",
1911
- default: false,
1912
- description: "Enable verbose logging"
1913
- },
1914
- "account-type": {
1915
- alias: "a",
1916
- type: "string",
1917
- default: "enterprise",
1918
- description: "Account type to use (individual, business, enterprise)"
1919
- },
1920
- manual: {
1921
- type: "boolean",
1922
- default: false,
1923
- description: "Enable manual request approval"
1924
- },
1925
- "rate-limit": {
1926
- alias: "r",
1907
+ ...sharedServerArgs,
1908
+ model: {
1909
+ alias: "m",
1927
1910
  type: "string",
1928
- description: "Rate limit in seconds between requests"
1929
- },
1930
- wait: {
1931
- alias: "w",
1932
- type: "boolean",
1933
- default: false,
1934
- description: "Wait instead of error when rate limit is hit. Has no effect if rate limit is not set"
1935
- },
1936
- "github-token": {
1937
- alias: "g",
1911
+ description: "Override the default model for Claude Code"
1912
+ }
1913
+ },
1914
+ async run({ args }) {
1915
+ if (!process$1.stdout.isTTY) {
1916
+ consola.error("The claude subcommand requires a TTY (interactive terminal).");
1917
+ process$1.exit(1);
1918
+ }
1919
+ const parsed = parseSharedArgs(args);
1920
+ let server$1;
1921
+ let serverUrl;
1922
+ try {
1923
+ const result = await setupAndServe({
1924
+ ...parsed,
1925
+ port: parsed.port,
1926
+ silent: true
1927
+ });
1928
+ server$1 = result.server;
1929
+ serverUrl = result.serverUrl;
1930
+ await server$1.ready();
1931
+ } catch (error) {
1932
+ consola.error("Failed to start server:", error instanceof Error ? error.message : error);
1933
+ process$1.exit(1);
1934
+ }
1935
+ consola.success(`Server ready on ${serverUrl}, launching Claude Code...`);
1936
+ consola.level = 1;
1937
+ launchChild({
1938
+ kind: "claude-code",
1939
+ envVars: getClaudeCodeEnvVars(serverUrl, args.model),
1940
+ extraArgs: args._ ?? [],
1941
+ model: args.model
1942
+ }, server$1);
1943
+ }
1944
+ });
1945
+
1946
+ //#endregion
1947
+ //#region src/codex.ts
1948
+ const codex = defineCommand({
1949
+ meta: {
1950
+ name: "codex",
1951
+ description: "Start the proxy server and launch Codex CLI"
1952
+ },
1953
+ args: {
1954
+ ...sharedServerArgs,
1955
+ model: {
1956
+ alias: "m",
1938
1957
  type: "string",
1939
- description: "Provide GitHub token directly (must be generated using the `auth` subcommand)"
1958
+ description: "Override the default model for Codex CLI"
1959
+ }
1960
+ },
1961
+ async run({ args }) {
1962
+ if (!process$1.stdout.isTTY) {
1963
+ consola.error("The codex subcommand requires a TTY (interactive terminal).");
1964
+ process$1.exit(1);
1965
+ }
1966
+ const parsed = parseSharedArgs(args);
1967
+ let server$1;
1968
+ let serverUrl;
1969
+ try {
1970
+ const result = await setupAndServe({
1971
+ ...parsed,
1972
+ port: parsed.port,
1973
+ silent: true
1974
+ });
1975
+ server$1 = result.server;
1976
+ serverUrl = result.serverUrl;
1977
+ await server$1.ready();
1978
+ } catch (error) {
1979
+ consola.error("Failed to start server:", error instanceof Error ? error.message : error);
1980
+ process$1.exit(1);
1981
+ }
1982
+ const codexModel = args.model ?? DEFAULT_CODEX_MODEL;
1983
+ consola.success(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...`);
1984
+ consola.level = 1;
1985
+ launchChild({
1986
+ kind: "codex",
1987
+ envVars: getCodexEnvVars(serverUrl),
1988
+ extraArgs: args._ ?? [],
1989
+ model: args.model
1990
+ }, server$1);
1991
+ }
1992
+ });
1993
+
1994
+ //#endregion
1995
+ //#region src/debug.ts
1996
+ async function getPackageVersion() {
1997
+ try {
1998
+ const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
1999
+ return JSON.parse(await fs.readFile(packageJsonPath)).version;
2000
+ } catch {
2001
+ return "unknown";
2002
+ }
2003
+ }
2004
+ function getRuntimeInfo() {
2005
+ const isBun = typeof Bun !== "undefined";
2006
+ return {
2007
+ name: isBun ? "bun" : "node",
2008
+ version: isBun ? Bun.version : process.version.slice(1),
2009
+ platform: os.platform(),
2010
+ arch: os.arch()
2011
+ };
2012
+ }
2013
+ async function checkTokenExists() {
2014
+ try {
2015
+ if (!(await fs.stat(PATHS.GITHUB_TOKEN_PATH)).isFile()) return false;
2016
+ return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim().length > 0;
2017
+ } catch {
2018
+ return false;
2019
+ }
2020
+ }
2021
+ async function getDebugInfo() {
2022
+ const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
2023
+ return {
2024
+ version,
2025
+ runtime: getRuntimeInfo(),
2026
+ paths: {
2027
+ APP_DIR: PATHS.APP_DIR,
2028
+ GITHUB_TOKEN_PATH: PATHS.GITHUB_TOKEN_PATH
1940
2029
  },
1941
- "claude-code": {
1942
- alias: "c",
2030
+ tokenExists
2031
+ };
2032
+ }
2033
+ function printDebugInfoPlain(info) {
2034
+ consola.info(`github-router debug
2035
+
2036
+ Version: ${info.version}
2037
+ Runtime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})
2038
+
2039
+ Paths:
2040
+ - APP_DIR: ${info.paths.APP_DIR}
2041
+ - GITHUB_TOKEN_PATH: ${info.paths.GITHUB_TOKEN_PATH}
2042
+
2043
+ Token exists: ${info.tokenExists ? "Yes" : "No"}`);
2044
+ }
2045
+ function printDebugInfoJson(info) {
2046
+ console.log(JSON.stringify(info, null, 2));
2047
+ }
2048
+ async function runDebug(options) {
2049
+ const debugInfo = await getDebugInfo();
2050
+ if (options.json) printDebugInfoJson(debugInfo);
2051
+ else printDebugInfoPlain(debugInfo);
2052
+ }
2053
+ const debug = defineCommand({
2054
+ meta: {
2055
+ name: "debug",
2056
+ description: "Print debug information about the application"
2057
+ },
2058
+ args: { json: {
2059
+ type: "boolean",
2060
+ default: false,
2061
+ description: "Output debug information as JSON"
2062
+ } },
2063
+ run({ args }) {
2064
+ return runDebug({ json: args.json });
2065
+ }
2066
+ });
2067
+
2068
+ //#endregion
2069
+ //#region src/lib/shell.ts
2070
+ function getShell() {
2071
+ const { platform, env } = process$1;
2072
+ if (platform === "win32") {
2073
+ if (env.SHELL) {
2074
+ if (env.SHELL.endsWith("zsh")) return "zsh";
2075
+ if (env.SHELL.endsWith("fish")) return "fish";
2076
+ if (env.SHELL.endsWith("bash")) return "bash";
2077
+ return "sh";
2078
+ }
2079
+ if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return "powershell";
2080
+ if (env.PSModulePath) {
2081
+ const lower = env.PSModulePath.toLowerCase();
2082
+ if (lower.includes("documents\\powershell") || lower.includes("documents\\windowspowershell")) return "powershell";
2083
+ }
2084
+ return "cmd";
2085
+ }
2086
+ const shellPath = env.SHELL;
2087
+ if (shellPath) {
2088
+ if (shellPath.endsWith("zsh")) return "zsh";
2089
+ if (shellPath.endsWith("fish")) return "fish";
2090
+ if (shellPath.endsWith("bash")) return "bash";
2091
+ }
2092
+ return "sh";
2093
+ }
2094
+ function quotePosixValue(value) {
2095
+ return `'${value.replace(/'/g, "'\\''")}'`;
2096
+ }
2097
+ function quotePowerShellValue(value) {
2098
+ return `'${value.replace(/'/g, "''")}'`;
2099
+ }
2100
+ /**
2101
+ * Generates a copy-pasteable script to set multiple environment variables
2102
+ * and run a subsequent command.
2103
+ * @param {EnvVars} envVars - An object of environment variables to set.
2104
+ * @param {string} commandToRun - The command to run after setting the variables.
2105
+ * @returns {string} The formatted script string.
2106
+ */
2107
+ function generateEnvScript(envVars, commandToRun = "") {
2108
+ const shell = getShell();
2109
+ const filteredEnvVars = Object.entries(envVars).filter(([, value]) => value !== void 0);
2110
+ let commandBlock;
2111
+ switch (shell) {
2112
+ case "powershell":
2113
+ commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`).join("; ");
2114
+ break;
2115
+ case "cmd":
2116
+ commandBlock = filteredEnvVars.map(([key, value]) => `set "${key}=${value}"`).join(" & ");
2117
+ break;
2118
+ case "fish":
2119
+ commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`).join("; ");
2120
+ break;
2121
+ default: {
2122
+ const assignments = filteredEnvVars.map(([key, value]) => `${key}=${quotePosixValue(value)}`).join(" ");
2123
+ commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
2124
+ break;
2125
+ }
2126
+ }
2127
+ if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : shell === "powershell" ? "; " : " && "}${commandToRun}`;
2128
+ return commandBlock || commandToRun;
2129
+ }
2130
+
2131
+ //#endregion
2132
+ //#region src/start.ts
2133
+ function printAndCopyCommand(command, label) {
2134
+ consola.box(`${label}\n\n${command}`);
2135
+ try {
2136
+ clipboard.writeSync(command);
2137
+ consola.success(`Copied ${label} command to clipboard!`);
2138
+ } catch {
2139
+ consola.warn("Failed to copy to clipboard. Copy the command above manually.");
2140
+ }
2141
+ }
2142
+ function generateClaudeCodeCommand(serverUrl, model) {
2143
+ printAndCopyCommand(generateEnvScript(getClaudeCodeEnvVars(serverUrl, model), "claude --dangerously-skip-permissions"), "Claude Code");
2144
+ }
2145
+ function generateCodexCommand(serverUrl, model) {
2146
+ const codexModel = model ?? DEFAULT_CODEX_MODEL;
2147
+ printAndCopyCommand(generateEnvScript(getCodexEnvVars(serverUrl), `codex -m ${codexModel}`), "Codex CLI");
2148
+ }
2149
+ const start = defineCommand({
2150
+ meta: {
2151
+ name: "start",
2152
+ description: "Start the github-router server"
2153
+ },
2154
+ args: {
2155
+ ...sharedServerArgs,
2156
+ cc: {
1943
2157
  type: "boolean",
1944
2158
  default: false,
1945
2159
  description: "Generate a command to launch Claude Code with Copilot API config"
1946
2160
  },
1947
- codex: {
2161
+ cx: {
1948
2162
  type: "boolean",
1949
2163
  default: false,
1950
2164
  description: "Generate a command to launch Codex CLI with Copilot API config"
1951
2165
  },
1952
- "show-token": {
1953
- type: "boolean",
1954
- default: false,
1955
- description: "Show GitHub and Copilot tokens on fetch and refresh"
1956
- },
1957
- "proxy-env": {
1958
- type: "boolean",
1959
- default: false,
1960
- description: "Initialize proxy from environment variables"
2166
+ model: {
2167
+ alias: "m",
2168
+ type: "string",
2169
+ description: "Override the default model (used with --cc or --cx)"
1961
2170
  }
1962
2171
  },
1963
- run({ args }) {
1964
- const rateLimitRaw = args["rate-limit"];
1965
- let rateLimit;
1966
- if (rateLimitRaw !== void 0) {
1967
- rateLimit = Number.parseInt(rateLimitRaw, 10);
1968
- if (Number.isNaN(rateLimit) || rateLimit <= 0) throw new Error("Invalid rate limit. Must be a positive integer.");
1969
- }
1970
- const port = Number.parseInt(args.port, 10);
1971
- if (Number.isNaN(port) || port <= 0 || port > 65535) throw new Error("Invalid port. Must be between 1 and 65535.");
1972
- const accountType = args["account-type"];
1973
- if (!allowedAccountTypes.has(accountType)) throw new Error("Invalid account type. Must be individual, business, or enterprise.");
1974
- const rateLimitWait = args.wait && rateLimit !== void 0;
1975
- if (args.wait && rateLimit === void 0) consola.warn("Rate limit wait ignored because no rate limit was set.");
1976
- const githubToken = args["github-token"] ?? process.env.GH_TOKEN;
1977
- return runServer({
1978
- port,
1979
- verbose: args.verbose,
1980
- accountType,
1981
- manual: args.manual,
1982
- rateLimit,
1983
- rateLimitWait,
1984
- githubToken,
1985
- claudeCode: args["claude-code"],
1986
- codex: args.codex,
1987
- showToken: args["show-token"],
1988
- proxyEnv: args["proxy-env"]
2172
+ async run({ args }) {
2173
+ const parsed = parseSharedArgs(args);
2174
+ const { serverUrl } = await setupAndServe({
2175
+ ...parsed,
2176
+ port: parsed.port ?? DEFAULT_PORT,
2177
+ silent: false
1989
2178
  });
2179
+ if (args.cc) generateClaudeCodeCommand(serverUrl, args.model);
2180
+ if (args.cx) generateCodexCommand(serverUrl, args.model);
2181
+ consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);
1990
2182
  }
1991
2183
  });
1992
2184
 
1993
2185
  //#endregion
1994
2186
  //#region src/main.ts
2187
+ process.on("unhandledRejection", (error) => {
2188
+ consola.error("Unhandled rejection:", error);
2189
+ });
2190
+ process.on("uncaughtException", (error) => {
2191
+ consola.error("Uncaught exception:", error);
2192
+ process.exit(1);
2193
+ });
1995
2194
  await runMain(defineCommand({
1996
2195
  meta: {
1997
2196
  name: "github-router",
@@ -2000,6 +2199,8 @@ await runMain(defineCommand({
2000
2199
  subCommands: {
2001
2200
  auth,
2002
2201
  start,
2202
+ claude,
2203
+ codex,
2003
2204
  "check-usage": checkUsage,
2004
2205
  debug
2005
2206
  }