github-router 0.3.273 → 0.3.274

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.
Files changed (36) hide show
  1. package/dist/{attribution-settings-4nEQq_tn.js → attribution-settings-Cqh_A9yx.js} +3 -3
  2. package/dist/{attribution-settings-4nEQq_tn.js.map → attribution-settings-Cqh_A9yx.js.map} +1 -1
  3. package/dist/{auth-3so8RT91.js → auth-Q1tfmfCT.js} +2 -2
  4. package/dist/{auth-3so8RT91.js.map → auth-Q1tfmfCT.js.map} +1 -1
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/{check-usage-goXyc3qS.js → check-usage-CHLz4tU_.js} +3 -3
  7. package/dist/{check-usage-goXyc3qS.js.map → check-usage-CHLz4tU_.js.map} +1 -1
  8. package/dist/{claude-BNZD-bQN.js → claude-CqFkkQGo.js} +7 -7
  9. package/dist/{claude-BNZD-bQN.js.map → claude-CqFkkQGo.js.map} +1 -1
  10. package/dist/{codex-C_MA9uy5.js → codex-gGkDJHZJ.js} +5 -5
  11. package/dist/{codex-C_MA9uy5.js.map → codex-gGkDJHZJ.js.map} +1 -1
  12. package/dist/engine-BM2mIXsc.js +2 -0
  13. package/dist/{gate-discovery-DvA4xObh.js → gate-discovery-e114xla0.js} +2 -2
  14. package/dist/{gate-discovery-DvA4xObh.js.map → gate-discovery-e114xla0.js.map} +1 -1
  15. package/dist/{get-copilot-usage-nZXbvSi8.js → get-copilot-usage-BRN18uMJ.js} +2 -2
  16. package/dist/{get-copilot-usage-nZXbvSi8.js.map → get-copilot-usage-BRN18uMJ.js.map} +1 -1
  17. package/dist/{internal-stop-hook-WVgnwt5U.js → internal-stop-hook-BLPxEImO.js} +2 -2
  18. package/dist/{internal-stop-hook-WVgnwt5U.js.map → internal-stop-hook-BLPxEImO.js.map} +1 -1
  19. package/dist/main.js +8 -8
  20. package/dist/{models-BmGGH6pP.js → models-d0gkuYGy.js} +2 -2
  21. package/dist/{models-BmGGH6pP.js.map → models-d0gkuYGy.js.map} +1 -1
  22. package/dist/{peer-mcp-personas-DAZU1OfN.js → peer-mcp-personas-pzk1zcDa.js} +2 -2
  23. package/dist/{peer-mcp-personas-DAZU1OfN.js.map → peer-mcp-personas-pzk1zcDa.js.map} +1 -1
  24. package/dist/{provision-C3UIA1Tf.js → provision-BIUvGrMS.js} +2 -2
  25. package/dist/{provision-C3UIA1Tf.js.map → provision-BIUvGrMS.js.map} +1 -1
  26. package/dist/{serve-DPgawWWq.js → serve-BYbEyMoF.js} +6 -6
  27. package/dist/{serve-DPgawWWq.js.map → serve-BYbEyMoF.js.map} +1 -1
  28. package/dist/{server-setup-I5KYkuxE.js → server-setup-CNKsKERG.js} +6 -5
  29. package/dist/{server-setup-I5KYkuxE.js.map → server-setup-CNKsKERG.js.map} +1 -1
  30. package/dist/{start-CWWqjjwm.js → start-Cy_fEtEw.js} +3 -3
  31. package/dist/{start-CWWqjjwm.js.map → start-Cy_fEtEw.js.map} +1 -1
  32. package/dist/{token-Bz7iN8US.js → token-Bz6HoH2H.js} +365 -46
  33. package/dist/token-Bz6HoH2H.js.map +1 -0
  34. package/package.json +1 -1
  35. package/dist/engine-CHN8j6BV.js +0 -2
  36. package/dist/token-Bz7iN8US.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { $ as browserToolsEnabled, Ct as provisionAndIndexColbert, Et as warmTreeSitterPool, St as colbertDegradedWarning, bt as provisionBrowserAssets } from "./peer-mcp-personas-DAZU1OfN.js";
2
- import { a as sharedServerArgs, i as setupAndServe, n as getCodexEnvVars, o as startKeepAwake, r as parseSharedArgs, t as getClaudeCodeEnvVars, u as runSelfUpdate } from "./server-setup-I5KYkuxE.js";
1
+ import { $ as browserToolsEnabled, Ct as provisionAndIndexColbert, Et as warmTreeSitterPool, St as colbertDegradedWarning, bt as provisionBrowserAssets } from "./peer-mcp-personas-pzk1zcDa.js";
2
+ import { a as sharedServerArgs, i as setupAndServe, n as getCodexEnvVars, o as startKeepAwake, r as parseSharedArgs, t as getClaudeCodeEnvVars, u as runSelfUpdate } from "./server-setup-CNKsKERG.js";
3
3
  import { defineCommand } from "citty";
4
4
  import consola from "consola";
5
5
  import process$1 from "node:process";
@@ -129,4 +129,4 @@ const start = defineCommand({
129
129
  //#endregion
130
130
  export { start };
131
131
 
132
- //# sourceMappingURL=start-CWWqjjwm.js.map
132
+ //# sourceMappingURL=start-Cy_fEtEw.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"start-CWWqjjwm.js","names":["process"],"sources":["../src/lib/shell.ts","../src/start.ts"],"sourcesContent":["import process from \"node:process\"\n\ntype ShellName = \"bash\" | \"zsh\" | \"fish\" | \"powershell\" | \"cmd\" | \"sh\"\ntype EnvVars = Record<string, string | undefined>\n\nfunction getShell(): ShellName {\n const { platform, env } = process\n\n if (platform === \"win32\") {\n // Git Bash / MSYS2 / Cygwin set SHELL even on Windows\n if (env.SHELL) {\n if (env.SHELL.endsWith(\"zsh\")) return \"zsh\"\n if (env.SHELL.endsWith(\"fish\")) return \"fish\"\n if (env.SHELL.endsWith(\"bash\")) return \"bash\"\n return \"sh\"\n }\n\n // Windows PowerShell 5.x sets this\n if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return \"powershell\"\n\n // PowerShell (both 5.x and 7+/pwsh) adds user-scoped module paths\n // at runtime. The system-level PSModulePath in CMD lacks these paths.\n if (env.PSModulePath) {\n const lower = env.PSModulePath.toLowerCase()\n if (\n lower.includes(\"documents\\\\powershell\")\n || lower.includes(\"documents\\\\windowspowershell\")\n ) {\n return \"powershell\"\n }\n }\n\n return \"cmd\"\n }\n\n const shellPath = env.SHELL\n if (shellPath) {\n if (shellPath.endsWith(\"zsh\")) return \"zsh\"\n if (shellPath.endsWith(\"fish\")) return \"fish\"\n if (shellPath.endsWith(\"bash\")) return \"bash\"\n }\n\n return \"sh\"\n}\n\nfunction quotePosixValue(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`\n}\n\nfunction quotePowerShellValue(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Generates a copy-pasteable script to set multiple environment variables\n * and run a subsequent command.\n * @param {EnvVars} envVars - An object of environment variables to set.\n * @param {string} commandToRun - The command to run after setting the variables.\n * @returns {string} The formatted script string.\n */\nexport function generateEnvScript(\n envVars: EnvVars,\n commandToRun: string = \"\",\n): string {\n const shell = getShell()\n const filteredEnvVars = Object.entries(envVars).filter(\n ([, value]) => value !== undefined,\n ) as Array<[string, string]>\n\n let commandBlock: string\n\n switch (shell) {\n case \"powershell\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)\n .join(\"; \")\n break\n }\n case \"cmd\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set \"${key}=${value}\"`)\n .join(\" & \")\n break\n }\n case \"fish\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`)\n .join(\"; \")\n break\n }\n default: {\n // bash, zsh, sh\n const assignments = filteredEnvVars\n .map(([key, value]) => `${key}=${quotePosixValue(value)}`)\n .join(\" \")\n commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : \"\"\n break\n }\n }\n\n if (commandBlock && commandToRun) {\n const separator =\n shell === \"cmd\" ? \" & \" : shell === \"powershell\" ? \"; \" : \" && \"\n return `${commandBlock}${separator}${commandToRun}`\n }\n\n return commandBlock || commandToRun\n}\n","#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport clipboard from \"clipboardy\"\nimport consola from \"consola\"\n\nimport { generateEnvScript } from \"./lib/shell\"\nimport { DEFAULT_CODEX_MODEL, DEFAULT_PORT } from \"./lib/port\"\nimport { provisionBrowserAssets } from \"./lib/browser-mcp/provision\"\nimport { browserToolsEnabled } from \"./lib/mcp-capabilities\"\nimport { colbertDegradedWarning, provisionAndIndexColbert } from \"./lib/colbert\"\nimport { startKeepAwake } from \"./lib/keep-awake\"\nimport { warmTreeSitterPool } from \"./lib/tree-sitter-pool/pool\"\nimport {\n getClaudeCodeEnvVars,\n getCodexEnvVars,\n parseSharedArgs,\n setupAndServe,\n sharedServerArgs,\n} from \"./lib/server-setup\"\nimport { runSelfUpdate } from \"./lib/self-update\"\n\nfunction printAndCopyCommand(command: string, label: string): void {\n consola.box(`${label}\\n\\n${command}`)\n try {\n clipboard.writeSync(command)\n consola.success(`Copied ${label} command to clipboard!`)\n } catch {\n consola.warn(\"Failed to copy to clipboard. Copy the command above manually.\")\n }\n}\n\nfunction generateClaudeCodeCommand(serverUrl: string, model?: string) {\n const envVars = getClaudeCodeEnvVars(serverUrl, model)\n const command = generateEnvScript(envVars, \"claude --dangerously-skip-permissions\")\n printAndCopyCommand(command, \"Claude Code\")\n}\n\nfunction generateCodexCommand(serverUrl: string, model?: string) {\n const codexModel = model ?? DEFAULT_CODEX_MODEL\n const envVars = getCodexEnvVars(serverUrl)\n const command = generateEnvScript(envVars, `codex -m ${codexModel}`)\n printAndCopyCommand(command, \"Codex CLI\")\n}\n\nexport const start = defineCommand({\n meta: {\n name: \"start\",\n description: \"Start the github-router server\",\n },\n args: {\n ...sharedServerArgs,\n cc: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Claude Code with Copilot API config\",\n },\n cx: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Codex CLI with Copilot API config\",\n },\n model: {\n alias: \"m\",\n type: \"string\",\n description: \"Override the default model (used with --cc or --cx)\",\n },\n },\n async run({ args }) {\n const parsed = parseSharedArgs(args as unknown as Record<string, unknown>)\n\n const { serverUrl } = await setupAndServe({\n ...parsed,\n port: parsed.port ?? DEFAULT_PORT,\n silent: false,\n })\n\n // Best-effort self-update (detached, applies next launch). Runs\n // after the server is listening so the bounded probe can't delay it.\n void runSelfUpdate({ selfUpdate: args[\"self-update\"] !== false })\n\n // Best-effort ColBERT semantic-search provision + background index of\n // the launch cwd (if a git repo). ON by default; never blocks launch,\n // never throws. Opt out with GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1.\n void provisionAndIndexColbert()\n\n // Surface a terminally-failed semantic index to the HUMAN (see the note\n // in claude.ts). Fire-and-forget; lexical search still works.\n void colbertDegradedWarning()\n .then((warning) => {\n if (warning) process.stderr.write(`${warning}\\n`)\n })\n .catch(() => {})\n\n // Ready one tree-sitter worker in the background so the first code search\n // avoids WASM/grammar initialization. One worker keeps launch CPU bounded;\n // demand grows the pool lazily. Opt out with GH_ROUTER_DISABLE_TS_POOL_WARMUP=1.\n void warmTreeSitterPool()\n\n // Best-effort: keep the machine awake while the proxy serves (win32\n // default-on; opt out with GH_ROUTER_DISABLE_KEEP_AWAKE=1). Holds a\n // SetThreadExecutionState assertion via a persistent PowerShell\n // helper. Self-registers its own SIGINT/SIGTERM/exit reaper — load-\n // bearing here, because `start` has no launchChild/onShutdown.\n startKeepAwake()\n\n // Best-effort: materialize the browser extension + bridge into the\n // stable app-dir and stamp the running version, so a one-time \"Load\n // unpacked\" survives npx/bunx upgrades. Gated on --browse; never\n // blocks launch, never throws.\n if (browserToolsEnabled()) {\n void provisionBrowserAssets().catch((err) =>\n consola.debug(\"Browser extension provisioning failed:\", err),\n )\n }\n\n if (args.cc) generateClaudeCodeCommand(serverUrl, args.model)\n if (args.cx) generateCodexCommand(serverUrl, args.model)\n\n consola.box(\n `🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`,\n )\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"start-Cy_fEtEw.js","names":["process"],"sources":["../src/lib/shell.ts","../src/start.ts"],"sourcesContent":["import process from \"node:process\"\n\ntype ShellName = \"bash\" | \"zsh\" | \"fish\" | \"powershell\" | \"cmd\" | \"sh\"\ntype EnvVars = Record<string, string | undefined>\n\nfunction getShell(): ShellName {\n const { platform, env } = process\n\n if (platform === \"win32\") {\n // Git Bash / MSYS2 / Cygwin set SHELL even on Windows\n if (env.SHELL) {\n if (env.SHELL.endsWith(\"zsh\")) return \"zsh\"\n if (env.SHELL.endsWith(\"fish\")) return \"fish\"\n if (env.SHELL.endsWith(\"bash\")) return \"bash\"\n return \"sh\"\n }\n\n // Windows PowerShell 5.x sets this\n if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return \"powershell\"\n\n // PowerShell (both 5.x and 7+/pwsh) adds user-scoped module paths\n // at runtime. The system-level PSModulePath in CMD lacks these paths.\n if (env.PSModulePath) {\n const lower = env.PSModulePath.toLowerCase()\n if (\n lower.includes(\"documents\\\\powershell\")\n || lower.includes(\"documents\\\\windowspowershell\")\n ) {\n return \"powershell\"\n }\n }\n\n return \"cmd\"\n }\n\n const shellPath = env.SHELL\n if (shellPath) {\n if (shellPath.endsWith(\"zsh\")) return \"zsh\"\n if (shellPath.endsWith(\"fish\")) return \"fish\"\n if (shellPath.endsWith(\"bash\")) return \"bash\"\n }\n\n return \"sh\"\n}\n\nfunction quotePosixValue(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`\n}\n\nfunction quotePowerShellValue(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Generates a copy-pasteable script to set multiple environment variables\n * and run a subsequent command.\n * @param {EnvVars} envVars - An object of environment variables to set.\n * @param {string} commandToRun - The command to run after setting the variables.\n * @returns {string} The formatted script string.\n */\nexport function generateEnvScript(\n envVars: EnvVars,\n commandToRun: string = \"\",\n): string {\n const shell = getShell()\n const filteredEnvVars = Object.entries(envVars).filter(\n ([, value]) => value !== undefined,\n ) as Array<[string, string]>\n\n let commandBlock: string\n\n switch (shell) {\n case \"powershell\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)\n .join(\"; \")\n break\n }\n case \"cmd\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set \"${key}=${value}\"`)\n .join(\" & \")\n break\n }\n case \"fish\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`)\n .join(\"; \")\n break\n }\n default: {\n // bash, zsh, sh\n const assignments = filteredEnvVars\n .map(([key, value]) => `${key}=${quotePosixValue(value)}`)\n .join(\" \")\n commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : \"\"\n break\n }\n }\n\n if (commandBlock && commandToRun) {\n const separator =\n shell === \"cmd\" ? \" & \" : shell === \"powershell\" ? \"; \" : \" && \"\n return `${commandBlock}${separator}${commandToRun}`\n }\n\n return commandBlock || commandToRun\n}\n","#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport clipboard from \"clipboardy\"\nimport consola from \"consola\"\n\nimport { generateEnvScript } from \"./lib/shell\"\nimport { DEFAULT_CODEX_MODEL, DEFAULT_PORT } from \"./lib/port\"\nimport { provisionBrowserAssets } from \"./lib/browser-mcp/provision\"\nimport { browserToolsEnabled } from \"./lib/mcp-capabilities\"\nimport { colbertDegradedWarning, provisionAndIndexColbert } from \"./lib/colbert\"\nimport { startKeepAwake } from \"./lib/keep-awake\"\nimport { warmTreeSitterPool } from \"./lib/tree-sitter-pool/pool\"\nimport {\n getClaudeCodeEnvVars,\n getCodexEnvVars,\n parseSharedArgs,\n setupAndServe,\n sharedServerArgs,\n} from \"./lib/server-setup\"\nimport { runSelfUpdate } from \"./lib/self-update\"\n\nfunction printAndCopyCommand(command: string, label: string): void {\n consola.box(`${label}\\n\\n${command}`)\n try {\n clipboard.writeSync(command)\n consola.success(`Copied ${label} command to clipboard!`)\n } catch {\n consola.warn(\"Failed to copy to clipboard. Copy the command above manually.\")\n }\n}\n\nfunction generateClaudeCodeCommand(serverUrl: string, model?: string) {\n const envVars = getClaudeCodeEnvVars(serverUrl, model)\n const command = generateEnvScript(envVars, \"claude --dangerously-skip-permissions\")\n printAndCopyCommand(command, \"Claude Code\")\n}\n\nfunction generateCodexCommand(serverUrl: string, model?: string) {\n const codexModel = model ?? DEFAULT_CODEX_MODEL\n const envVars = getCodexEnvVars(serverUrl)\n const command = generateEnvScript(envVars, `codex -m ${codexModel}`)\n printAndCopyCommand(command, \"Codex CLI\")\n}\n\nexport const start = defineCommand({\n meta: {\n name: \"start\",\n description: \"Start the github-router server\",\n },\n args: {\n ...sharedServerArgs,\n cc: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Claude Code with Copilot API config\",\n },\n cx: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Codex CLI with Copilot API config\",\n },\n model: {\n alias: \"m\",\n type: \"string\",\n description: \"Override the default model (used with --cc or --cx)\",\n },\n },\n async run({ args }) {\n const parsed = parseSharedArgs(args as unknown as Record<string, unknown>)\n\n const { serverUrl } = await setupAndServe({\n ...parsed,\n port: parsed.port ?? DEFAULT_PORT,\n silent: false,\n })\n\n // Best-effort self-update (detached, applies next launch). Runs\n // after the server is listening so the bounded probe can't delay it.\n void runSelfUpdate({ selfUpdate: args[\"self-update\"] !== false })\n\n // Best-effort ColBERT semantic-search provision + background index of\n // the launch cwd (if a git repo). ON by default; never blocks launch,\n // never throws. Opt out with GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1.\n void provisionAndIndexColbert()\n\n // Surface a terminally-failed semantic index to the HUMAN (see the note\n // in claude.ts). Fire-and-forget; lexical search still works.\n void colbertDegradedWarning()\n .then((warning) => {\n if (warning) process.stderr.write(`${warning}\\n`)\n })\n .catch(() => {})\n\n // Ready one tree-sitter worker in the background so the first code search\n // avoids WASM/grammar initialization. One worker keeps launch CPU bounded;\n // demand grows the pool lazily. Opt out with GH_ROUTER_DISABLE_TS_POOL_WARMUP=1.\n void warmTreeSitterPool()\n\n // Best-effort: keep the machine awake while the proxy serves (win32\n // default-on; opt out with GH_ROUTER_DISABLE_KEEP_AWAKE=1). Holds a\n // SetThreadExecutionState assertion via a persistent PowerShell\n // helper. Self-registers its own SIGINT/SIGTERM/exit reaper — load-\n // bearing here, because `start` has no launchChild/onShutdown.\n startKeepAwake()\n\n // Best-effort: materialize the browser extension + bridge into the\n // stable app-dir and stamp the running version, so a one-time \"Load\n // unpacked\" survives npx/bunx upgrades. Gated on --browse; never\n // blocks launch, never throws.\n if (browserToolsEnabled()) {\n void provisionBrowserAssets().catch((err) =>\n consola.debug(\"Browser extension provisioning failed:\", err),\n )\n }\n\n if (args.cc) generateClaudeCodeCommand(serverUrl, args.model)\n if (args.cx) generateCodexCommand(serverUrl, args.model)\n\n consola.box(\n `🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`,\n )\n },\n})\n"],"mappings":"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"}
@@ -1,12 +1,13 @@
1
1
  import { t as PATHS } from "./paths-Bg1_DWhi.js";
2
2
  import consola from "consola";
3
3
  import path from "node:path";
4
- import { randomBytes, randomUUID } from "node:crypto";
4
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import fs from "node:fs/promises";
6
6
  import process$1 from "node:process";
7
7
  //#region src/lib/state.ts
8
8
  const state = {
9
9
  accountType: "enterprise",
10
+ copilotTokenGeneration: 0,
10
11
  manualApprove: false,
11
12
  rateLimitWait: false,
12
13
  showToken: false,
@@ -631,9 +632,113 @@ function isAllowedCopilotHost(rawUrl) {
631
632
  if (parsed.protocol !== "https:") return false;
632
633
  return COPILOT_HOST_ALLOWLIST.includes(parsed.hostname);
633
634
  }
634
- const getCopilotToken = async () => {
635
- const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(state) }), { label: "/copilot_internal/v2/token" });
636
- if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
635
+ /**
636
+ * Short, non-reversible identifier for a credential, safe to log.
637
+ *
638
+ * During the 2026-08-08 incident the logs could not answer the one question
639
+ * that mattered: was the process holding a stale credential, or was the
640
+ * credential on disk itself dead? The operator re-authenticated at 13:53 and
641
+ * the proxy kept failing, and nothing recorded whether those were the same
642
+ * secret. A truncated SHA-256 makes two credentials comparable across log
643
+ * lines without ever printing one. 8 hex chars is ample to distinguish the
644
+ * handful of credentials a machine sees, and far too short to attack.
645
+ */
646
+ function credentialFingerprint(token) {
647
+ if (!token) return "none";
648
+ return createHash("sha256").update(token).digest("hex").slice(0, 8);
649
+ }
650
+ /**
651
+ * A failed `/copilot_internal/v2/token` exchange, carrying enough to diagnose
652
+ * it after the fact.
653
+ *
654
+ * The predecessor threw `HTTPError("Failed to get Copilot token", response)`
655
+ * and every catch logged only `.message`, so status and body were dropped —
656
+ * which is why the incident is unattributable.
657
+ */
658
+ var CopilotTokenExchangeError = class extends HTTPError {
659
+ kind;
660
+ status;
661
+ /** Fingerprint of the credential that was rejected, never the credential. */
662
+ credential;
663
+ constructor(opts) {
664
+ super(`Failed to get Copilot token: HTTP ${opts.status} (${opts.kind}, credential ${opts.credential})${opts.detail ? ` — ${opts.detail}` : ""}`, opts.response);
665
+ this.kind = opts.kind;
666
+ this.status = opts.status;
667
+ this.credential = opts.credential;
668
+ }
669
+ };
670
+ /** Upper bound on how much of an error body we quote into a log line. */
671
+ const ERROR_BODY_SNIPPET_CHARS = 300;
672
+ /**
673
+ * `notification_id` values that mean "the credential is valid but the
674
+ * entitlement is not". These need a different remedy than a bad credential:
675
+ * telling someone whose subscription lapsed to re-authenticate sends them
676
+ * around a loop that cannot help.
677
+ */
678
+ const ENTITLEMENT_NOTIFICATIONS = /* @__PURE__ */ new Set([
679
+ "access_revoked",
680
+ "subscription_ended",
681
+ "no_copilot_access"
682
+ ]);
683
+ /**
684
+ * Classify a non-OK exchange response.
685
+ *
686
+ * Deliberately asymmetric: only a 401, or a 403 that NAMES an entitlement
687
+ * problem, is treated as terminal. A bare 403 stays transient because GitHub
688
+ * returns 403 for primary rate limits (`x-ratelimit-remaining: 0`) and
689
+ * secondary abuse limits — telling a user with a perfectly good credential to
690
+ * re-authenticate because they were briefly rate-limited would be a worse
691
+ * failure than the one being fixed. The costs are not symmetric: calling a
692
+ * terminal failure transient wastes some retries, while calling a transient
693
+ * failure terminal kills a working session.
694
+ */
695
+ function classifyExchangeFailure(status, body) {
696
+ if (status === 401) return "credential_rejected";
697
+ if (status === 403) {
698
+ let notificationId;
699
+ try {
700
+ const parsed = JSON.parse(body);
701
+ notificationId = parsed.notification_id ?? parsed.error?.notification_id;
702
+ } catch {}
703
+ if (typeof notificationId === "string" && ENTITLEMENT_NOTIFICATIONS.has(notificationId)) return "entitlement_lapsed";
704
+ }
705
+ return "transient";
706
+ }
707
+ /**
708
+ * Exchange the GitHub credential for a short-lived Copilot token.
709
+ *
710
+ * `credentialOverride` lets a caller try a candidate credential WITHOUT
711
+ * publishing it to the process-global `state` first. That matters because
712
+ * `state` is shared by every concurrent request: installing an unvalidated
713
+ * credential globally, even briefly, would make unrelated in-flight requests
714
+ * send it and 401 while a perfectly good credential was still in hand.
715
+ */
716
+ const getCopilotToken = async (credentialOverride) => {
717
+ const credential = credentialFingerprint(credentialOverride ?? state.githubToken);
718
+ const authState = credentialOverride === void 0 ? state : {
719
+ ...state,
720
+ githubToken: credentialOverride
721
+ };
722
+ const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(authState) }), { label: "/copilot_internal/v2/token" });
723
+ if (!response.ok) {
724
+ let body;
725
+ try {
726
+ body = await response.text();
727
+ } catch {
728
+ body = "(could not read error body)";
729
+ }
730
+ throw new CopilotTokenExchangeError({
731
+ kind: classifyExchangeFailure(response.status, body),
732
+ status: response.status,
733
+ credential,
734
+ detail: body.slice(0, ERROR_BODY_SNIPPET_CHARS).replaceAll(/\s+/g, " ").trim(),
735
+ response: new Response(body, {
736
+ status: response.status,
737
+ statusText: response.statusText,
738
+ headers: response.headers
739
+ })
740
+ });
741
+ }
637
742
  const data = await response.json();
638
743
  if (data.endpoints?.api) if (isAllowedCopilotHost(data.endpoints.api)) state.copilotApiUrl = data.endpoints.api;
639
744
  else consola.warn(`Refusing to honor Copilot API endpoint "${data.endpoints.api}" from the token-exchange response — not in allowlist (${COPILOT_HOST_ALLOWLIST.join(", ")}). ` + (state.copilotApiUrl ? `Keeping existing override "${state.copilotApiUrl}".` : `Falling back to the default api.githubcopilot.com.`));
@@ -1164,6 +1269,43 @@ async function pollAccessToken(deviceCode, clientId = GITHUB_CLIENT_ID) {
1164
1269
  //#region src/lib/token.ts
1165
1270
  const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
1166
1271
  /**
1272
+ * Transient filesystem errors from a read that raced the temp+rename write.
1273
+ *
1274
+ * On Windows a read landing in that window fails with one of these even
1275
+ * though a perfectly good file exists microseconds later. Treating one as
1276
+ * fatal at startup would cost a launch for no reason.
1277
+ */
1278
+ const TRANSIENT_READ_CODES = /* @__PURE__ */ new Set([
1279
+ "EPERM",
1280
+ "EBUSY",
1281
+ "EACCES",
1282
+ "ENOENT"
1283
+ ]);
1284
+ function isTransientReadError(err) {
1285
+ const code = err?.code;
1286
+ return typeof code === "string" && TRANSIENT_READ_CODES.has(code);
1287
+ }
1288
+ /**
1289
+ * Read the credential file, retrying briefly through a racing rename.
1290
+ *
1291
+ * Same delays and the same reasoning as {@link RENAME_RETRY_DELAYS_MS} on the
1292
+ * write side: the contending handle closes almost immediately, so a short
1293
+ * bounded retry converts a spurious launch failure into a non-event. A
1294
+ * persistent failure still propagates — the caller must not believe a
1295
+ * credential was read when it was not.
1296
+ */
1297
+ async function readGithubTokenWithRetry() {
1298
+ let lastErr;
1299
+ for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt++) try {
1300
+ return await readGithubToken();
1301
+ } catch (err) {
1302
+ lastErr = err;
1303
+ if (!isTransientReadError(err)) throw err;
1304
+ if (attempt < RENAME_RETRY_DELAYS_MS.length) await new Promise((resolve) => setTimeout(resolve, RENAME_RETRY_DELAYS_MS[attempt]));
1305
+ }
1306
+ throw lastErr;
1307
+ }
1308
+ /**
1167
1309
  * Backoff for a transient Windows rename failure. On Windows `fs.rename` over
1168
1310
  * an existing destination transiently fails with EPERM / EBUSY / EACCES when
1169
1311
  * anything else holds the target open for a moment (antivirus, the search
@@ -1226,10 +1368,60 @@ const writeGithubToken = (token) => writeTokenFileAtomic(PATHS.GITHUB_TOKEN_PATH
1226
1368
  const readGithubAgentToken = () => fs.readFile(PATHS.GITHUB_AGENT_TOKEN_PATH, "utf8");
1227
1369
  const writeGithubAgentToken = (token) => writeTokenFileAtomic(PATHS.GITHUB_AGENT_TOKEN_PATH, token);
1228
1370
  /**
1371
+ * Safety margin subtracted from the derived refresh deadline, so a request
1372
+ * arriving just before expiry refreshes rather than racing it.
1373
+ */
1374
+ const REFRESH_SKEW_MS = 12e4;
1375
+ /**
1376
+ * Bounds on a `refresh_in` we are willing to turn into a timer delay. An
1377
+ * absurd or missing upstream value must not produce a zero-length timer (a
1378
+ * hot loop hammering GitHub) or an effectively infinite one (a token that is
1379
+ * never refreshed). 60s floor, 6h ceiling, 1500s default — the value GitHub
1380
+ * actually returns today.
1381
+ */
1382
+ const MIN_REFRESH_IN_S = 60;
1383
+ const MAX_REFRESH_IN_S = 21600;
1384
+ const DEFAULT_REFRESH_IN_S = 1500;
1385
+ /** Backoff schedule after a failed refresh, then hold at the last value. */
1386
+ const REFRESH_BACKOFF_MS = [
1387
+ 5e3,
1388
+ 15e3,
1389
+ 6e4,
1390
+ 3e5
1391
+ ];
1392
+ function clampRefreshIn(refreshIn) {
1393
+ if (typeof refreshIn !== "number" || !Number.isFinite(refreshIn)) return DEFAULT_REFRESH_IN_S;
1394
+ return Math.min(Math.max(refreshIn, MIN_REFRESH_IN_S), MAX_REFRESH_IN_S);
1395
+ }
1396
+ /**
1397
+ * Adopt an exchange result into `state`.
1398
+ *
1399
+ * The generation bump is what lets an in-flight request know a newer token
1400
+ * exists (see `State.copilotTokenGeneration`), so it happens on EVERY success
1401
+ * — including one that returns a byte-identical token, which upstream
1402
+ * demonstrably does.
1403
+ */
1404
+ /**
1405
+ * Floor on how far ahead the derived refresh deadline may be placed.
1406
+ *
1407
+ * The deadline is `now + refresh_in - skew`, and the skew (120s) is larger
1408
+ * than the `refresh_in` floor (60s) — so a short upstream `refresh_in` would
1409
+ * put the deadline in the PAST, the scheduler would floor its delay to 1s,
1410
+ * and the proxy would hammer the token endpoint at 1 Hz forever. The deadline
1411
+ * must always be in the future, whatever upstream says.
1412
+ */
1413
+ const MIN_REFRESH_LEAD_MS = 3e4;
1414
+ function commitCopilotToken(token, refreshIn) {
1415
+ state.copilotToken = token;
1416
+ state.copilotTokenGeneration += 1;
1417
+ const lead = Math.max(clampRefreshIn(refreshIn) * 1e3 - REFRESH_SKEW_MS, MIN_REFRESH_LEAD_MS);
1418
+ state.copilotTokenRefreshAt = Date.now() + lead;
1419
+ }
1420
+ /**
1229
1421
  * Fetch the Copilot token and keep it fresh in the background.
1230
1422
  *
1231
1423
  * Returns a disposer that stops the refresh loop. **Long-lived callers**
1232
- * (`start`/`claude`/`codex`, via `setupAndServe`) can ignore it — the interval
1424
+ * (`start`/`claude`/`codex`, via `setupAndServe`) can ignore it — the timer
1233
1425
  * is `unref()`d, so it never holds the event loop open on its own. The one-shot
1234
1426
  * `models` command calls it, so ownership of the timer is explicit rather than
1235
1427
  * implied by a runtime flag. (`check-usage` does not call this function at all;
@@ -1238,95 +1430,222 @@ const writeGithubAgentToken = (token) => writeTokenFileAtomic(PATHS.GITHUB_AGENT
1238
1430
  * Both halves are load-bearing, for different failure modes. Without the
1239
1431
  * `unref()`, `github-router models` printed its full correct output and then
1240
1432
  * hung forever: its success path just returns (only the failure branches call
1241
- * `process.exit`), and the un-unref'd interval pinned the event loop. Without
1433
+ * `process.exit`), and the un-unref'd timer pinned the event loop. Without
1242
1434
  * the disposer, that fix would depend on a property no caller can see, so the
1243
1435
  * next one-shot command would inherit the same trap.
1436
+ *
1437
+ * The schedule is a SELF-RE-ARMING `setTimeout`, not a fixed `setInterval`.
1438
+ * Three reasons: the delay tracks each response's own `refresh_in` instead of
1439
+ * the one captured at startup; a failure can back off (5s → 15s → 60s → 300s)
1440
+ * instead of waiting a full period during which the token is already dead;
1441
+ * and one timer cannot disagree with itself the way a period timer plus a
1442
+ * separate retry timer can.
1244
1443
  */
1245
1444
  const setupCopilotToken = async () => {
1246
1445
  const { token, refresh_in } = await getCopilotToken();
1247
- state.copilotToken = token;
1248
- consola.debug("GitHub Copilot Token fetched successfully!");
1446
+ commitCopilotToken(token, refresh_in);
1447
+ consola.debug(`GitHub Copilot Token fetched successfully! (credential ${credentialFingerprint(state.githubToken)})`);
1249
1448
  if (state.showToken) consola.info("Copilot token:", token);
1250
- const refreshInterval = Math.max((refresh_in - 60) * 1e3, 1e3);
1251
- const handle = setInterval(() => {
1252
- refreshCopilotToken("interval");
1253
- }, refreshInterval);
1254
- handle.unref?.();
1255
- let stopped = false;
1449
+ let handle;
1450
+ let disposed = false;
1451
+ let failures = 0;
1452
+ const arm = (delayMs) => {
1453
+ if (disposed) return;
1454
+ handle = setTimeout(() => {
1455
+ tick();
1456
+ }, delayMs);
1457
+ handle.unref?.();
1458
+ };
1459
+ const tick = async () => {
1460
+ const outcome = await refreshCopilotToken("interval");
1461
+ if (disposed) return;
1462
+ if (outcome === "refreshed") {
1463
+ failures = 0;
1464
+ arm(nextDelayFromState());
1465
+ return;
1466
+ }
1467
+ const delay = REFRESH_BACKOFF_MS[Math.min(failures, REFRESH_BACKOFF_MS.length - 1)];
1468
+ failures += 1;
1469
+ arm(delay);
1470
+ };
1471
+ arm(nextDelayFromState());
1256
1472
  return () => {
1257
- if (stopped) return;
1258
- stopped = true;
1259
- clearInterval(handle);
1473
+ if (disposed) return;
1474
+ disposed = true;
1475
+ if (handle) clearTimeout(handle);
1260
1476
  };
1261
1477
  };
1478
+ /**
1479
+ * Delay until the next scheduled refresh, from the deadline already stored on
1480
+ * `state`. Floored at 1s so a pathological deadline can never busy-loop.
1481
+ */
1482
+ function nextDelayFromState() {
1483
+ const deadline = state.copilotTokenRefreshAt;
1484
+ if (deadline === void 0) return DEFAULT_REFRESH_IN_S * 1e3;
1485
+ return Math.max(deadline - Date.now(), 1e3);
1486
+ }
1262
1487
  let inflightRefresh;
1263
1488
  let lastRefreshSuccess = 0;
1264
1489
  let lastRefreshFailure = 0;
1265
1490
  const REFRESH_SUCCESS_COOLDOWN_MS = 3e4;
1266
1491
  const REFRESH_FAILURE_COOLDOWN_MS = 5e3;
1492
+ /**
1493
+ * Read the credential from disk, returning it only if it is usable.
1494
+ *
1495
+ * Every failure mode here resolves to `undefined` (meaning "keep using what
1496
+ * is in memory") rather than throwing or returning a partial value. The file
1497
+ * is replaced by temp+rename (`writeTokenFileAtomic`), and on Windows a read
1498
+ * racing that rename fails with EPERM/EBUSY/EACCES/ENOENT. None of those mean
1499
+ * the in-memory credential went bad, so none of them may clear it — and a
1500
+ * filesystem error must never escape into the refresh loop and kill the timer.
1501
+ */
1502
+ async function readGithubTokenIfUsable() {
1503
+ if (state.githubTokenSource === "explicit") return void 0;
1504
+ try {
1505
+ const trimmed = (await readGithubToken()).trim();
1506
+ return trimmed.length > 0 ? trimmed : void 0;
1507
+ } catch (err) {
1508
+ consola.debug("Could not re-read GitHub credential from disk:", err);
1509
+ return;
1510
+ }
1511
+ }
1512
+ /**
1513
+ * Perform one exchange, preferring a newer credential from disk if there is one.
1514
+ *
1515
+ * The disk token is a CANDIDATE: it is passed to the exchange directly and
1516
+ * published to `state.githubToken` only after that exchange succeeds. It is
1517
+ * never installed globally on spec — `state` is shared by every concurrent
1518
+ * request, so an unvalidated credential visible there would be sent by
1519
+ * unrelated in-flight requests and 401 them while a working credential was
1520
+ * still in hand.
1521
+ */
1522
+ async function exchangeWithFreshestCredential() {
1523
+ const onDisk = await readGithubTokenIfUsable();
1524
+ const inMemory = state.githubToken;
1525
+ if (onDisk && onDisk !== inMemory) {
1526
+ consola.info(`GitHub credential on disk differs from the one in memory (${credentialFingerprint(inMemory)} → ${credentialFingerprint(onDisk)}); trying it.`);
1527
+ const result = await getCopilotToken(onDisk);
1528
+ state.githubToken = onDisk;
1529
+ state.githubTokenSource = "file";
1530
+ return result;
1531
+ }
1532
+ return getCopilotToken();
1533
+ }
1267
1534
  async function refreshCopilotToken(reason) {
1268
1535
  if (inflightRefresh) return inflightRefresh;
1269
- if (reason === "401-retry") {
1270
- const now = Date.now();
1271
- if (now - lastRefreshSuccess < REFRESH_SUCCESS_COOLDOWN_MS) {
1272
- consola.debug(`refreshCopilotToken(${reason}) skipped: prior success within ${REFRESH_SUCCESS_COOLDOWN_MS}ms`);
1273
- return;
1274
- }
1275
- if (now - lastRefreshFailure < REFRESH_FAILURE_COOLDOWN_MS) {
1276
- consola.debug(`refreshCopilotToken(${reason}) skipped: prior failure within ${REFRESH_FAILURE_COOLDOWN_MS}ms`);
1277
- return;
1278
- }
1279
- }
1280
- inflightRefresh = (async () => {
1281
- consola.debug(`Refreshing Copilot token (reason=${reason})`);
1536
+ const run = async () => {
1282
1537
  try {
1283
- const { token } = await getCopilotToken();
1284
- state.copilotToken = token;
1285
- lastRefreshSuccess = Date.now();
1286
- consola.debug("Copilot token refreshed");
1287
- if (state.showToken) consola.info("Refreshed Copilot token:", token);
1288
- } catch (error) {
1289
- lastRefreshFailure = Date.now();
1290
- consola.error(`Failed to refresh Copilot token (reason=${reason}):`, error);
1538
+ if (reason === "401-retry" || reason === "expiry") {
1539
+ const now = Date.now();
1540
+ const onDisk = await readGithubTokenIfUsable();
1541
+ if (!Boolean(onDisk && onDisk !== state.githubToken)) {
1542
+ if (now - lastRefreshSuccess < REFRESH_SUCCESS_COOLDOWN_MS) {
1543
+ consola.debug(`refreshCopilotToken(${reason}) skipped: prior success within ${REFRESH_SUCCESS_COOLDOWN_MS}ms`);
1544
+ return "skipped";
1545
+ }
1546
+ if (now - lastRefreshFailure < REFRESH_FAILURE_COOLDOWN_MS) {
1547
+ consola.debug(`refreshCopilotToken(${reason}) skipped: prior failure within ${REFRESH_FAILURE_COOLDOWN_MS}ms`);
1548
+ return "skipped";
1549
+ }
1550
+ }
1551
+ }
1552
+ consola.debug(`Refreshing Copilot token (reason=${reason})`);
1553
+ try {
1554
+ const { token, refresh_in } = await exchangeWithFreshestCredential();
1555
+ commitCopilotToken(token, refresh_in);
1556
+ lastRefreshSuccess = Date.now();
1557
+ consola.debug(`Copilot token refreshed (credential ${credentialFingerprint(state.githubToken)}, generation ${state.copilotTokenGeneration})`);
1558
+ if (state.showToken) consola.info("Refreshed Copilot token:", token);
1559
+ return "refreshed";
1560
+ } catch (error) {
1561
+ lastRefreshFailure = Date.now();
1562
+ consola.error(`Failed to refresh Copilot token (reason=${reason}):`, error instanceof Error ? error.message : error);
1563
+ if (error instanceof CopilotTokenExchangeError) {
1564
+ if (error.kind === "credential_rejected") return "credential_rejected";
1565
+ if (error.kind === "entitlement_lapsed") return "entitlement_lapsed";
1566
+ }
1567
+ return "transient_failure";
1568
+ }
1291
1569
  } finally {
1292
- inflightRefresh = void 0;
1570
+ if (inflightRefresh === attempt) inflightRefresh = void 0;
1293
1571
  }
1294
- })();
1295
- return inflightRefresh;
1572
+ };
1573
+ const attempt = run();
1574
+ inflightRefresh = attempt;
1575
+ return attempt;
1576
+ }
1577
+ /**
1578
+ * Refresh the Copilot token if its derived deadline has passed.
1579
+ *
1580
+ * Called on EVERY request. The common path is one `Date.now()` and one
1581
+ * integer compare — no I/O — against an upstream call that costs hundreds of
1582
+ * milliseconds, so the overhead is not measurable. Sampling (every Nth
1583
+ * request) was considered and rejected: a session that idles and then resumes
1584
+ * may issue only a handful of requests, so a sampled guard would fail to fire
1585
+ * in precisely the scenario it exists for. Actual refreshes do not get more
1586
+ * frequent, because `refreshCopilotToken` is single-flighted and the deadline
1587
+ * moves forward on success.
1588
+ */
1589
+ async function ensureFreshCopilotToken() {
1590
+ const deadline = state.copilotTokenRefreshAt;
1591
+ if (deadline === void 0 || Date.now() < deadline) return;
1592
+ await refreshCopilotToken("expiry");
1296
1593
  }
1297
1594
  /**
1298
1595
  * Try `request()`. If it returns a 401, refresh the Copilot token (subject
1299
1596
  * to the single-flight + refresh-storm-protection of `refreshCopilotToken`)
1300
- * and retry once. After one retry, propagate whatever the second attempt
1301
- * returned — the caller's existing 401-handling path is preserved.
1597
+ * and retry once but ONLY if the token actually moved on.
1598
+ *
1599
+ * The retry criterion is the generation counter, not the refresh's verdict
1600
+ * and not a token-string comparison. Both alternatives are wrong:
1601
+ *
1602
+ * - By verdict: requests A and B both hold token T0 and both 401. A
1603
+ * refreshes to T1. B's refresh call lands inside the 30s success cooldown
1604
+ * and reports "skipped", so a verdict-based rule would give up and fail B
1605
+ * with a 503 — even though T1 is sitting in state and would have worked.
1606
+ * - By string: upstream demonstrably returns a byte-identical token from
1607
+ * two consecutive successful exchanges, so an unchanged string does not
1608
+ * mean an unchanged credential state.
1609
+ *
1610
+ * Comparing generations answers the question the request actually has: is
1611
+ * what is in state now different from what I already tried? When it is not,
1612
+ * we skip a re-send that is guaranteed to 401 again.
1302
1613
  *
1303
1614
  * The `request` callback is responsible for capturing `state.copilotToken`
1304
1615
  * locally before any await; this helper does NOT re-build the request
1305
1616
  * itself, just re-invokes the callback after a refresh.
1306
1617
  */
1307
1618
  async function tryRefreshAndRetry(request, routePath) {
1619
+ await ensureFreshCopilotToken();
1620
+ const generationBefore = state.copilotTokenGeneration;
1308
1621
  const first = await request();
1309
1622
  if (first.status !== 401) return first;
1310
1623
  consola.warn(`${routePath}: upstream returned 401, attempting one token refresh + retry`);
1311
1624
  await refreshCopilotToken("401-retry");
1625
+ if (state.copilotTokenGeneration === generationBefore) {
1626
+ consola.debug(`${routePath}: no newer Copilot token available (generation ${generationBefore}); not retrying`);
1627
+ return first;
1628
+ }
1312
1629
  return request();
1313
1630
  }
1314
1631
  async function setupGitHubToken(options) {
1315
1632
  try {
1316
- const githubToken = await readGithubToken();
1633
+ const githubToken = await readGithubTokenWithRetry();
1317
1634
  if (githubToken && !options?.force) {
1318
1635
  state.githubToken = githubToken;
1636
+ state.githubTokenSource = "file";
1319
1637
  if (state.showToken) consola.info("GitHub token:", githubToken);
1320
1638
  await logUser();
1321
1639
  return;
1322
1640
  }
1323
- consola.info("Not logged in, getting new access token");
1641
+ consola.info(options?.force ? "Re-authenticating, getting new access token" : "Not logged in, getting new access token");
1324
1642
  const response = await getDeviceCode();
1325
1643
  consola.debug("Device code response:", response);
1326
1644
  consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
1327
1645
  const token = await pollAccessToken(response);
1328
1646
  await writeGithubToken(token);
1329
1647
  state.githubToken = token;
1648
+ state.githubTokenSource = "file";
1330
1649
  if (state.showToken) consola.info("GitHub token:", token);
1331
1650
  await logUser();
1332
1651
  } catch (error) {
@@ -1410,4 +1729,4 @@ async function warnIfAgentScopesInsufficient() {
1410
1729
  //#endregion
1411
1730
  export { state as A, GITHUB_GRAPHQL_URL as C, githubAgentGraphQLHeaders as D, copilotVersion as E, githubAgentHeaders as O, GITHUB_API_BASE_URL as S, copilotHeaders as T, forwardError as _, cacheCopilotVersion as a, sanitizeTransportText as b, filterBetaHeader as c, resolveModel as d, sleep as f, HTTPError as g, isAllowedCopilotHost as h, tryRefreshAndRetry as i, githubHeaders as k, isNullish as l, getGitHubUser as m, setupGitHubAgentToken as n, cacheModels as o, getModels as p, setupGitHubToken as r, cacheVSCodeVersion as s, setupCopilotToken as t, resolveCodexModel as u, classifyTransportError as v, copilotBaseUrl as w, withTransientRetry as x, fetchWithTransientRetry as y };
1412
1731
 
1413
- //# sourceMappingURL=token-Bz7iN8US.js.map
1732
+ //# sourceMappingURL=token-Bz6HoH2H.js.map