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 +576 -300
- package/dist/main.js.map +1 -1
- package/package.json +1 -2
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
|
|
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/
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
427
|
-
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/lib/launch.ts
|
|
428
|
+
function buildLaunchCommand(target) {
|
|
428
429
|
return {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
1714
|
-
const
|
|
1715
|
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
1824
|
-
description: "Start the
|
|
1903
|
+
name: "claude",
|
|
1904
|
+
description: "Start the proxy server and launch Claude Code"
|
|
1825
1905
|
},
|
|
1826
1906
|
args: {
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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: "
|
|
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
|
-
|
|
1867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
description: "
|
|
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
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
}
|