github-router 0.3.7 → 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 () => {
@@ -671,7 +615,19 @@ function logRequest(info, model, startTime) {
671
615
  const elapsed = Date.now() - startTime;
672
616
  const duration = elapsed >= 1e3 ? `${(elapsed / 1e3).toFixed(1)}s` : `${elapsed}ms`;
673
617
  parts.push(info.streaming ? `${duration} stream` : duration);
674
- consola.info(parts.join(" "));
618
+ const line = parts.join(" ");
619
+ if (detectCapabilityMismatch(info, model)) consola.error(`[MISMATCH] ${line}`);
620
+ else consola.info(line);
621
+ }
622
+ /**
623
+ * Detect when the API rejects a request for token/context reasons
624
+ * that contradict what the /models endpoint reported.
625
+ */
626
+ function detectCapabilityMismatch(info, model) {
627
+ if (!info.errorBody || !model) return false;
628
+ if (!info.status || info.status < 400) return false;
629
+ const err = info.errorBody.toLowerCase();
630
+ return err.includes("token") || err.includes("context") || err.includes("too long") || err.includes("max_tokens") || err.includes("prompt is too long");
675
631
  }
676
632
 
677
633
  //#endregion
@@ -872,13 +828,13 @@ const getTokenCount = async (payload, model) => {
872
828
 
873
829
  //#endregion
874
830
  //#region src/services/copilot/create-chat-completions.ts
875
- const createChatCompletions = async (payload) => {
831
+ const createChatCompletions = async (payload, modelHeaders) => {
876
832
  if (!state.copilotToken) throw new Error("Copilot token not found");
877
833
  const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x$1) => x$1.type === "image_url"));
878
834
  const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
879
835
  const headers = {
880
836
  ...copilotHeaders(state, enableVision),
881
- "Openai-Organization": "github-copilot",
837
+ ...modelHeaders,
882
838
  "X-Initiator": isAgentCall ? "agent" : "user"
883
839
  };
884
840
  const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
@@ -997,7 +953,20 @@ async function handleCompletion$1(c) {
997
953
  };
998
954
  if (debugEnabled) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
999
955
  }
1000
- const response = await createChatCompletions(payload);
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(() => "");
959
+ logRequest({
960
+ method: "POST",
961
+ path: c.req.path,
962
+ model: originalModel,
963
+ resolvedModel,
964
+ status: error.response.status,
965
+ errorBody
966
+ }, selectedModel, startTime);
967
+ }
968
+ throw error;
969
+ });
1001
970
  const isStreaming = !isNonStreaming$1(response);
1002
971
  const outputTokens = !isStreaming ? response.usage?.completion_tokens : void 0;
1003
972
  logRequest({
@@ -1125,6 +1094,8 @@ function buildHeaders(extraHeaders) {
1125
1094
  return {
1126
1095
  ...copilotHeaders(state),
1127
1096
  accept: "application/json",
1097
+ "openai-intent": "conversation-agent",
1098
+ "x-interaction-type": "conversation-agent",
1128
1099
  "X-Initiator": "agent",
1129
1100
  "anthropic-version": "2023-06-01",
1130
1101
  "X-Interaction-Id": randomUUID(),
@@ -1233,10 +1204,13 @@ async function handleCountTokens(c) {
1233
1204
  const filtered = filterBetaHeader(anthropicBeta);
1234
1205
  if (filtered) extraHeaders["anthropic-beta"] = filtered;
1235
1206
  }
1236
- const response = await countTokens(finalBody, extraHeaders);
1237
- const responseBody = await response.json();
1238
1207
  const modelId = resolvedModel ?? originalModel;
1239
1208
  const selectedModel = state.models?.data.find((m) => m.id === modelId);
1209
+ const response = await countTokens(finalBody, {
1210
+ ...selectedModel?.requestHeaders,
1211
+ ...extraHeaders
1212
+ });
1213
+ const responseBody = await response.json();
1240
1214
  logRequest({
1241
1215
  method: "POST",
1242
1216
  path: c.req.path,
@@ -1387,7 +1361,27 @@ async function handleCompletion(c) {
1387
1361
  const betaHeaders = extractBetaHeaders(c);
1388
1362
  const { body: resolvedBody, originalModel, resolvedModel } = resolveModelInBody(await processWebSearch(rawBody));
1389
1363
  const selectedModel = state.models?.data.find((m) => m.id === (resolvedModel ?? originalModel));
1390
- const response = await createMessages(resolvedBody, betaHeaders);
1364
+ const effectiveBetas = applyDefaultBetas(betaHeaders, resolvedModel ?? originalModel);
1365
+ let response;
1366
+ try {
1367
+ response = await createMessages(resolvedBody, {
1368
+ ...selectedModel?.requestHeaders,
1369
+ ...effectiveBetas
1370
+ });
1371
+ } catch (error) {
1372
+ if (error instanceof HTTPError) {
1373
+ const errorBody = await error.response.clone().text().catch(() => "");
1374
+ logRequest({
1375
+ method: "POST",
1376
+ path: c.req.path,
1377
+ model: originalModel,
1378
+ resolvedModel,
1379
+ status: error.response.status,
1380
+ errorBody
1381
+ }, selectedModel, startTime);
1382
+ }
1383
+ throw error;
1384
+ }
1391
1385
  if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
1392
1386
  logRequest({
1393
1387
  method: "POST",
@@ -1449,6 +1443,19 @@ function resolveModelInBody(rawBody) {
1449
1443
  resolvedModel: resolved
1450
1444
  };
1451
1445
  }
1446
+ /**
1447
+ * Apply default anthropic-beta values for Claude models when the client
1448
+ * (e.g. curl) sends no beta headers. Claude CLI sends its own betas,
1449
+ * so this only fires as a safety net for bare clients.
1450
+ */
1451
+ function applyDefaultBetas(betaHeaders, modelId) {
1452
+ if (betaHeaders["anthropic-beta"]) return betaHeaders;
1453
+ if (!modelId || !modelId.startsWith("claude-")) return betaHeaders;
1454
+ return {
1455
+ ...betaHeaders,
1456
+ "anthropic-beta": ["interleaved-thinking-2025-05-14", "token-counting-2024-11-01"].join(",")
1457
+ };
1458
+ }
1452
1459
 
1453
1460
  //#endregion
1454
1461
  //#region src/routes/messages/route.ts
@@ -1495,12 +1502,13 @@ modelRoutes.get("/", async (c) => {
1495
1502
 
1496
1503
  //#endregion
1497
1504
  //#region src/services/copilot/create-responses.ts
1498
- const createResponses = async (payload) => {
1505
+ const createResponses = async (payload, modelHeaders) => {
1499
1506
  if (!state.copilotToken) throw new Error("Copilot token not found");
1500
1507
  const enableVision = detectVision(payload.input);
1501
1508
  const isAgentCall = detectAgentCall(payload.input);
1502
1509
  const headers = {
1503
1510
  ...copilotHeaders(state, enableVision),
1511
+ ...modelHeaders,
1504
1512
  "X-Initiator": isAgentCall ? "agent" : "user"
1505
1513
  };
1506
1514
  const filteredPayload = filterUnsupportedTools(payload);
@@ -1577,7 +1585,20 @@ async function handleResponses(c) {
1577
1585
  payload.max_output_tokens = selectedModel?.capabilities.limits.max_output_tokens;
1578
1586
  if (debugEnabled) consola.debug("Set max_output_tokens to:", JSON.stringify(payload.max_output_tokens));
1579
1587
  }
1580
- const response = await createResponses(payload);
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(() => "");
1591
+ logRequest({
1592
+ method: "POST",
1593
+ path: c.req.path,
1594
+ model: originalModel,
1595
+ resolvedModel,
1596
+ status: error.response.status,
1597
+ errorBody
1598
+ }, selectedModel, startTime);
1599
+ }
1600
+ throw error;
1601
+ });
1581
1602
  const isStreaming = !isNonStreaming(response);
1582
1603
  logRequest({
1583
1604
  method: "POST",
@@ -1710,84 +1731,9 @@ server.route("/v1/search", searchRoutes);
1710
1731
  server.route("/v1/messages", messageRoutes);
1711
1732
 
1712
1733
  //#endregion
1713
- //#region src/start.ts
1714
- const allowedAccountTypes = new Set([
1715
- "individual",
1716
- "business",
1717
- "enterprise"
1718
- ]);
1719
- function printAndCopyCommand(command, label) {
1720
- consola.box(`${label}\n\n${command}`);
1721
- try {
1722
- clipboard.writeSync(command);
1723
- consola.success(`Copied ${label} command to clipboard!`);
1724
- } catch {
1725
- consola.warn("Failed to copy to clipboard. Copy the command above manually.");
1726
- }
1727
- }
1728
- function filterModelsByEndpoint(models, endpoint) {
1729
- const filtered = models.filter((model) => {
1730
- const endpoints = model.supported_endpoints;
1731
- if (!endpoints || endpoints.length === 0) return true;
1732
- return endpoints.some((entry) => {
1733
- return entry.replace(/^\/?v1\//, "").replace(/^\//, "") === endpoint;
1734
- });
1735
- });
1736
- return filtered.length > 0 ? filtered : models;
1737
- }
1738
- async function generateClaudeCodeCommand(serverUrl) {
1739
- invariant(state.models, "Models should be loaded by now");
1740
- const claudeModels = state.models.data.filter((model) => model.id.toLowerCase().startsWith("claude"));
1741
- if (claudeModels.length === 0) {
1742
- consola.error("No Claude models available from Copilot API");
1743
- return;
1744
- }
1745
- const mainModel = claudeModels.find((m) => m.id.includes("opus")) ?? claudeModels.find((m) => m.id.includes("sonnet")) ?? claudeModels[0];
1746
- const smallModel = claudeModels.find((m) => m.id.includes("haiku")) ?? claudeModels.find((m) => m.id.includes("sonnet")) ?? claudeModels[0];
1747
- let selectedModel = mainModel.id;
1748
- let selectedSmallModel = smallModel.id;
1749
- if (claudeModels.length > 1) {
1750
- consola.info(`Using ${mainModel.id} as main model and ${smallModel.id} as small model`);
1751
- if (await consola.prompt("Override model selection?", {
1752
- type: "confirm",
1753
- initial: false
1754
- })) {
1755
- selectedModel = await consola.prompt("Select a main model for Claude Code", {
1756
- type: "select",
1757
- options: claudeModels.map((model) => model.id)
1758
- });
1759
- selectedSmallModel = await consola.prompt("Select a small/fast model for Claude Code", {
1760
- type: "select",
1761
- options: claudeModels.map((model) => model.id)
1762
- });
1763
- }
1764
- }
1765
- printAndCopyCommand(generateEnvScript({
1766
- ANTHROPIC_BASE_URL: serverUrl,
1767
- ANTHROPIC_AUTH_TOKEN: "dummy",
1768
- ANTHROPIC_MODEL: selectedModel,
1769
- ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
1770
- ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel,
1771
- ANTHROPIC_DEFAULT_HAIKU_MODEL: selectedSmallModel,
1772
- DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
1773
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
1774
- }, "claude --dangerously-skip-permissions"), "Claude Code");
1775
- }
1776
- async function generateCodexCommand(serverUrl) {
1777
- invariant(state.models, "Models should be loaded by now");
1778
- const supportedModels = filterModelsByEndpoint(state.models.data, "responses");
1779
- const defaultCodexModel = supportedModels.find((model) => model.id === "gpt5.2-codex");
1780
- const selectedModel = defaultCodexModel ? defaultCodexModel.id : await consola.prompt("Select a model to use with Codex CLI", {
1781
- type: "select",
1782
- options: supportedModels.map((model) => model.id)
1783
- });
1784
- const quotedModel = JSON.stringify(selectedModel);
1785
- printAndCopyCommand(generateEnvScript({
1786
- OPENAI_BASE_URL: `${serverUrl}/v1`,
1787
- OPENAI_API_KEY: "dummy"
1788
- }, `codex -m ${quotedModel}`), "Codex CLI");
1789
- }
1790
- async function runServer(options) {
1734
+ //#region src/lib/server-setup.ts
1735
+ const MAX_PORT_RETRIES = 10;
1736
+ async function setupAndServe(options) {
1791
1737
  if (options.proxyEnv) initProxyFromEnv();
1792
1738
  if (options.verbose) {
1793
1739
  consola.level = 5;
@@ -1808,115 +1754,443 @@ async function runServer(options) {
1808
1754
  await setupCopilotToken();
1809
1755
  await cacheModels();
1810
1756
  consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
1811
- const serverUrl = `http://localhost:${options.port}`;
1812
- if (options.claudeCode) await generateClaudeCodeCommand(serverUrl);
1813
- if (options.codex) await generateCodexCommand(serverUrl);
1814
- consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);
1815
- serve({
1757
+ const serveOptions = {
1816
1758
  fetch: server.fetch,
1817
1759
  hostname: "127.0.0.1",
1760
+ silent: options.silent
1761
+ };
1762
+ let srvxServer;
1763
+ if (options.port !== void 0) srvxServer = serve({
1764
+ ...serveOptions,
1818
1765
  port: options.port
1819
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
+ };
1820
1792
  }
1821
- 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({
1822
1902
  meta: {
1823
- name: "start",
1824
- description: "Start the github-router server"
1903
+ name: "claude",
1904
+ description: "Start the proxy server and launch Claude Code"
1825
1905
  },
1826
1906
  args: {
1827
- port: {
1828
- alias: "p",
1829
- type: "string",
1830
- default: "8787",
1831
- description: "Port to listen on"
1832
- },
1833
- verbose: {
1834
- alias: "v",
1835
- type: "boolean",
1836
- default: false,
1837
- description: "Enable verbose logging"
1838
- },
1839
- "account-type": {
1840
- alias: "a",
1907
+ ...sharedServerArgs,
1908
+ model: {
1909
+ alias: "m",
1841
1910
  type: "string",
1842
- default: "enterprise",
1843
- description: "Account type to use (individual, business, enterprise)"
1844
- },
1845
- manual: {
1846
- type: "boolean",
1847
- default: false,
1848
- description: "Enable manual request approval"
1849
- },
1850
- "rate-limit": {
1851
- alias: "r",
1852
- type: "string",
1853
- description: "Rate limit in seconds between requests"
1854
- },
1855
- wait: {
1856
- alias: "w",
1857
- type: "boolean",
1858
- default: false,
1859
- description: "Wait instead of error when rate limit is hit. Has no effect if rate limit is not set"
1860
- },
1861
- "github-token": {
1862
- 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",
1863
1957
  type: "string",
1864
- 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
1865
2029
  },
1866
- "claude-code": {
1867
- 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: {
1868
2157
  type: "boolean",
1869
2158
  default: false,
1870
2159
  description: "Generate a command to launch Claude Code with Copilot API config"
1871
2160
  },
1872
- codex: {
2161
+ cx: {
1873
2162
  type: "boolean",
1874
2163
  default: false,
1875
2164
  description: "Generate a command to launch Codex CLI with Copilot API config"
1876
2165
  },
1877
- "show-token": {
1878
- type: "boolean",
1879
- default: false,
1880
- description: "Show GitHub and Copilot tokens on fetch and refresh"
1881
- },
1882
- "proxy-env": {
1883
- type: "boolean",
1884
- default: false,
1885
- 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)"
1886
2170
  }
1887
2171
  },
1888
- run({ args }) {
1889
- const rateLimitRaw = args["rate-limit"];
1890
- let rateLimit;
1891
- if (rateLimitRaw !== void 0) {
1892
- rateLimit = Number.parseInt(rateLimitRaw, 10);
1893
- if (Number.isNaN(rateLimit) || rateLimit <= 0) throw new Error("Invalid rate limit. Must be a positive integer.");
1894
- }
1895
- const port = Number.parseInt(args.port, 10);
1896
- if (Number.isNaN(port) || port <= 0 || port > 65535) throw new Error("Invalid port. Must be between 1 and 65535.");
1897
- const accountType = args["account-type"];
1898
- if (!allowedAccountTypes.has(accountType)) throw new Error("Invalid account type. Must be individual, business, or enterprise.");
1899
- const rateLimitWait = args.wait && rateLimit !== void 0;
1900
- if (args.wait && rateLimit === void 0) consola.warn("Rate limit wait ignored because no rate limit was set.");
1901
- const githubToken = args["github-token"] ?? process.env.GH_TOKEN;
1902
- return runServer({
1903
- port,
1904
- verbose: args.verbose,
1905
- accountType,
1906
- manual: args.manual,
1907
- rateLimit,
1908
- rateLimitWait,
1909
- githubToken,
1910
- claudeCode: args["claude-code"],
1911
- codex: args.codex,
1912
- showToken: args["show-token"],
1913
- 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
1914
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`);
1915
2182
  }
1916
2183
  });
1917
2184
 
1918
2185
  //#endregion
1919
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
+ });
1920
2194
  await runMain(defineCommand({
1921
2195
  meta: {
1922
2196
  name: "github-router",
@@ -1925,6 +2199,8 @@ await runMain(defineCommand({
1925
2199
  subCommands: {
1926
2200
  auth,
1927
2201
  start,
2202
+ claude,
2203
+ codex,
1928
2204
  "check-usage": checkUsage,
1929
2205
  debug
1930
2206
  }