github-router 0.3.249 → 0.3.255

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 (95) hide show
  1. package/dist/attribution-settings-CegtE0TY.js +2347 -0
  2. package/dist/attribution-settings-CegtE0TY.js.map +1 -0
  3. package/dist/auth-CP4q2CjO.js +44 -0
  4. package/dist/auth-CP4q2CjO.js.map +1 -0
  5. package/dist/browser-ext/background.js +8 -1
  6. package/dist/browser-ext/manifest.json +1 -1
  7. package/dist/check-usage-6PB5IUFb.js +43 -0
  8. package/dist/check-usage-6PB5IUFb.js.map +1 -0
  9. package/dist/claude-BiZxoNWq.js +593 -0
  10. package/dist/claude-BiZxoNWq.js.map +1 -0
  11. package/dist/client-CQMfroGV.js +413 -0
  12. package/dist/client-CQMfroGV.js.map +1 -0
  13. package/dist/codex-ST21X3WF.js +85 -0
  14. package/dist/codex-ST21X3WF.js.map +1 -0
  15. package/dist/debug-CjmE9sgG.js +81 -0
  16. package/dist/debug-CjmE9sgG.js.map +1 -0
  17. package/dist/engine-DNCX0Hfd.js +2 -0
  18. package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
  19. package/dist/exec-DTO2oin0.js.map +1 -0
  20. package/dist/file-log-reporter-IFQcL5IK.js +158 -0
  21. package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
  22. package/dist/gate-discovery-CepmbZnc.js +635 -0
  23. package/dist/gate-discovery-CepmbZnc.js.map +1 -0
  24. package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
  25. package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
  26. package/dist/hook-mcp-client-DBvm9608.js +136 -0
  27. package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
  28. package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
  29. package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
  30. package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
  31. package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
  32. package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
  33. package/dist/internal-plan-review-ipnT3bhj.js +114 -0
  34. package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
  35. package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
  36. package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
  37. package/dist/internal-session-bind-DwK2iGW0.js +133 -0
  38. package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
  39. package/dist/internal-stop-hook-CNPGJm63.js +212 -0
  40. package/dist/internal-stop-hook-CNPGJm63.js.map +1 -0
  41. package/dist/internal-stop-review-AUcFssrW.js +122 -0
  42. package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
  43. package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
  44. package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
  45. package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
  46. package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
  47. package/dist/lifecycle-Byw5s1G8.js +134 -0
  48. package/dist/lifecycle-Byw5s1G8.js.map +1 -0
  49. package/dist/lifecycle-D80CaWie.js +2 -0
  50. package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
  51. package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
  52. package/dist/lifecycle-DzqCqDlM.js +2 -0
  53. package/dist/main.js +43 -12884
  54. package/dist/main.js.map +1 -1
  55. package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
  56. package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
  57. package/dist/models-B6UnGyRo.js +158 -0
  58. package/dist/models-B6UnGyRo.js.map +1 -0
  59. package/dist/orchestration-hRU4TdWr.js +39 -0
  60. package/dist/orchestration-hRU4TdWr.js.map +1 -0
  61. package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
  62. package/dist/paths-C1cLZ7Ga.js.map +1 -0
  63. package/dist/paths-CwKwc2pi.js +2 -0
  64. package/dist/{peer-mcp-personas-DJzLpfDJ.js → peer-mcp-personas-BDet588n.js} +1006 -2488
  65. package/dist/peer-mcp-personas-BDet588n.js.map +1 -0
  66. package/dist/plan-review-hook-C8x0uKPf.js +159 -0
  67. package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
  68. package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
  69. package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
  70. package/dist/provision-DSkPa5hY.js +656 -0
  71. package/dist/provision-DSkPa5hY.js.map +1 -0
  72. package/dist/serve-sa_7c-yD.js +1511 -0
  73. package/dist/serve-sa_7c-yD.js.map +1 -0
  74. package/dist/server-setup-DcO9qvN3.js +4776 -0
  75. package/dist/server-setup-DcO9qvN3.js.map +1 -0
  76. package/dist/start-siGxuZhO.js +129 -0
  77. package/dist/start-siGxuZhO.js.map +1 -0
  78. package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
  79. package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
  80. package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
  81. package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
  82. package/dist/token-R2ocFC3M.js +1054 -0
  83. package/dist/token-R2ocFC3M.js.map +1 -0
  84. package/dist/version-_Q1WpsQp.js +32 -0
  85. package/dist/version-_Q1WpsQp.js.map +1 -0
  86. package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
  87. package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
  88. package/package.json +1 -1
  89. package/dist/engine-BuuHUb4t.js +0 -2
  90. package/dist/lifecycle-0rXrwjWk.js +0 -2
  91. package/dist/lifecycle-DHZsVN8j.js.map +0 -1
  92. package/dist/lifecycle-hzPwXc3I.js +0 -2
  93. package/dist/paths-BjTMI_xK.js.map +0 -1
  94. package/dist/paths-D3XgmTcQ.js +0 -2
  95. package/dist/peer-mcp-personas-DJzLpfDJ.js.map +0 -1
@@ -0,0 +1,1054 @@
1
+ import { t as PATHS } from "./paths-C1cLZ7Ga.js";
2
+ import consola from "consola";
3
+ import path from "node:path";
4
+ import { randomBytes, randomUUID } from "node:crypto";
5
+ import fs from "node:fs/promises";
6
+ //#region src/lib/state.ts
7
+ const state = {
8
+ accountType: "enterprise",
9
+ manualApprove: false,
10
+ rateLimitWait: false,
11
+ showToken: false,
12
+ extendedBetas: false,
13
+ browseEnabled: false,
14
+ fleetEnabled: false,
15
+ agentsEnabled: false,
16
+ powerBrowseEnabled: false,
17
+ humanlikeForce: "auto",
18
+ sessionId: randomUUID(),
19
+ machineId: randomBytes(32).toString("hex")
20
+ };
21
+ //#endregion
22
+ //#region src/lib/api-config.ts
23
+ const standardHeaders = () => ({
24
+ "content-type": "application/json",
25
+ accept: "application/json"
26
+ });
27
+ const DEFAULT_COPILOT_VERSION = "0.43.2026033101";
28
+ function copilotVersion(state) {
29
+ return state.copilotVersion ?? DEFAULT_COPILOT_VERSION;
30
+ }
31
+ const API_VERSION = "2026-01-09";
32
+ const copilotBaseUrl = (state) => state.copilotApiUrl ?? "https://api.githubcopilot.com";
33
+ const copilotHeaders = (state, vision = false, integrationId = "vscode-chat") => {
34
+ const version = copilotVersion(state);
35
+ const headers = {
36
+ Authorization: `Bearer ${state.copilotToken}`,
37
+ "content-type": standardHeaders()["content-type"],
38
+ "copilot-integration-id": integrationId,
39
+ "editor-version": `vscode/${state.vsCodeVersion}`,
40
+ "editor-plugin-version": `copilot-chat/${version}`,
41
+ "user-agent": `GitHubCopilotChat/${version}`,
42
+ "openai-intent": "conversation-panel",
43
+ "x-interaction-type": "conversation-panel",
44
+ "x-github-api-version": API_VERSION,
45
+ "x-request-id": randomUUID(),
46
+ "x-vscode-user-agent-library-version": "electron-fetch",
47
+ "VScode-SessionId": state.sessionId,
48
+ "VScode-MachineId": state.machineId
49
+ };
50
+ if (vision) headers["copilot-vision-request"] = "true";
51
+ return headers;
52
+ };
53
+ const GITHUB_API_BASE_URL = process.env.GITHUB_API_URL ?? "https://api.github.com";
54
+ const githubHeaders = (state) => ({
55
+ ...standardHeaders(),
56
+ authorization: `token ${state.githubToken}`,
57
+ "editor-version": `vscode/${state.vsCodeVersion}`,
58
+ "editor-plugin-version": `copilot-chat/${copilotVersion(state)}`,
59
+ "user-agent": `GitHubCopilotChat/${copilotVersion(state)}`,
60
+ "x-github-api-version": API_VERSION,
61
+ "x-vscode-user-agent-library-version": "electron-fetch"
62
+ });
63
+ const GITHUB_BASE_URL = "https://github.com";
64
+ const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
65
+ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
66
+ const GITHUB_AGENT_CLIENT_ID = "178c6fc778ccc68e1d6a";
67
+ const GITHUB_AGENT_SCOPES = [
68
+ "repo",
69
+ "workflow",
70
+ "read:org"
71
+ ].join(" ");
72
+ const GITHUB_GRAPHQL_URL = process.env.GITHUB_GRAPHQL_URL ?? "https://api.github.com/graphql";
73
+ const GITHUB_REST_API_VERSION = "2022-11-28";
74
+ const githubAgentHeaders = (state) => ({
75
+ ...standardHeaders(),
76
+ authorization: `token ${state.githubAgentToken}`,
77
+ "x-github-api-version": GITHUB_REST_API_VERSION,
78
+ "user-agent": "github-router-first-mate"
79
+ });
80
+ const githubAgentGraphQLHeaders = (state, features) => ({
81
+ ...githubAgentHeaders(state),
82
+ ...features ? { "GraphQL-Features": features } : {}
83
+ });
84
+ //#endregion
85
+ //#region src/lib/error.ts
86
+ var HTTPError = class extends Error {
87
+ response;
88
+ constructor(message, response) {
89
+ super(message);
90
+ this.response = response;
91
+ }
92
+ };
93
+ async function forwardError(c, error) {
94
+ consola.error(`Error occurred at ${c.req.path}:`, error);
95
+ if (error instanceof HTTPError) {
96
+ const errorText = await error.response.text().catch(() => "");
97
+ let errorJson;
98
+ try {
99
+ errorJson = JSON.parse(errorText);
100
+ } catch {
101
+ errorJson = void 0;
102
+ }
103
+ if (isContextOverflow(error.response.status, errorJson, errorText)) {
104
+ const upstream = resolveErrorMessage(errorJson, errorText);
105
+ consola.error("HTTP error (mapped to overflow):", errorJson ?? errorText);
106
+ return c.json({
107
+ type: "error",
108
+ error: {
109
+ type: "invalid_request_error",
110
+ message: `prompt is too long: ${upstream}`
111
+ }
112
+ }, 400);
113
+ }
114
+ const responseStatus = error.response.status === 401 ? 503 : error.response.status;
115
+ if (isAnthropicError(errorJson)) {
116
+ consola.error("HTTP error:", errorJson);
117
+ return c.json(errorJson, responseStatus);
118
+ }
119
+ const message = resolveErrorMessage(errorJson, errorText);
120
+ consola.error("HTTP error:", errorJson ?? errorText);
121
+ return c.json({
122
+ type: "error",
123
+ error: {
124
+ type: resolveErrorType(responseStatus),
125
+ message
126
+ }
127
+ }, responseStatus);
128
+ }
129
+ return c.json({
130
+ type: "error",
131
+ error: {
132
+ type: "api_error",
133
+ message: error instanceof Error ? error.message : String(error)
134
+ }
135
+ }, 500);
136
+ }
137
+ function resolveErrorMessage(errorJson, fallback) {
138
+ if (typeof errorJson !== "object" || errorJson === null) return fallback;
139
+ const errorRecord = errorJson;
140
+ if (errorRecord.message !== void 0) return String(errorRecord.message);
141
+ if (typeof errorRecord.error === "object" && errorRecord.error !== null) {
142
+ const nestedRecord = errorRecord.error;
143
+ if (nestedRecord.message !== void 0) return String(nestedRecord.message);
144
+ }
145
+ return fallback;
146
+ }
147
+ /**
148
+ * Check if a parsed JSON body is already in Anthropic error format:
149
+ * { type: "error", error: { type: "...", message: "..." } }
150
+ */
151
+ function isAnthropicError(json) {
152
+ if (typeof json !== "object" || json === null) return false;
153
+ const record = json;
154
+ if (record.type !== "error") return false;
155
+ if (typeof record.error !== "object" || record.error === null) return false;
156
+ const inner = record.error;
157
+ return typeof inner.type === "string" && typeof inner.message === "string";
158
+ }
159
+ const CONTEXT_OVERFLOW_SUBSTRINGS = [
160
+ "prompt is too long",
161
+ "context_length_exceeded",
162
+ "context length exceeded",
163
+ "input is too long",
164
+ "maximum context length",
165
+ "too many tokens"
166
+ ];
167
+ /**
168
+ * Detect upstream context-overflow errors so we can remap them to a 400
169
+ * "prompt is too long" shape that triggers Claude Code self-compaction.
170
+ *
171
+ * Always remaps 413 (treated as a hard payload-size signal regardless of
172
+ * body wording). Remaps 400 only when the error text contains one of the
173
+ * known overflow substrings — a regular 400 (e.g. "model not found") must
174
+ * NOT remap.
175
+ */
176
+ function isContextOverflow(status, errorJson, errorText) {
177
+ if (status === 413) return true;
178
+ if (status !== 400) return false;
179
+ const haystack = (errorText + " " + (typeof errorJson === "object" && errorJson !== null ? JSON.stringify(errorJson) : "")).toLowerCase();
180
+ return CONTEXT_OVERFLOW_SUBSTRINGS.some((s) => haystack.includes(s));
181
+ }
182
+ /**
183
+ * Map HTTP status to Anthropic error type.
184
+ *
185
+ * Note: a 401 from upstream is remapped to 503 in `forwardError` BEFORE
186
+ * this function is called (no-401 invariant — see comment there). The
187
+ * 401 → "authentication_error" mapping below is preserved for
188
+ * defensive coverage in case any code path calls `resolveErrorType`
189
+ * directly with an unsanitized status.
190
+ */
191
+ function resolveErrorType(status) {
192
+ if (status === 400) return "invalid_request_error";
193
+ if (status === 401) return "authentication_error";
194
+ if (status === 403) return "permission_error";
195
+ if (status === 404) return "not_found_error";
196
+ if (status === 429) return "rate_limit_error";
197
+ if (status === 503) return "overloaded_error";
198
+ if (status === 529) return "overloaded_error";
199
+ return "api_error";
200
+ }
201
+ //#endregion
202
+ //#region src/lib/upstream-retry.ts
203
+ /**
204
+ * Shared transient-failure retry for upstream calls (`src/lib/upstream-retry.ts`).
205
+ *
206
+ * Retries ONLY on transient conditions — never on success, never on a
207
+ * deterministic 4xx (400/401/403/404…), so a malformed request or auth
208
+ * failure fails fast instead of being hammered. 401 is intentionally NOT
209
+ * retried here: it stays with the existing token-refresh path so the
210
+ * 401→503 forwardError invariant is preserved.
211
+ *
212
+ * Retryable:
213
+ * - HTTP 429 + 5xx (500/502/503/504) — the "upstream is sick" class.
214
+ * - network errors (ECONNRESET / "fetch failed" / "terminated" / EPIPE …).
215
+ * - upstream TIMEOUT aborts: an AbortError thrown while the CALLER'S
216
+ * `signal` is NOT aborted (a user cancel aborts the caller's signal
217
+ * and is rethrown immediately, never retried).
218
+ *
219
+ * Exponential backoff + FULL jitter, capped, honoring `Retry-After`. The
220
+ * inter-attempt sleep is abortable so a user cancel during backoff
221
+ * returns promptly. Bounded `attempts` keep a single call from holding an
222
+ * inflight slot indefinitely (robust AND fast).
223
+ *
224
+ * Streaming note: the user-facing passthrough and the worker loop must
225
+ * only retry in the PRE-FIRST-BYTE window — a retry after bytes have
226
+ * streamed would duplicate output. Callers there pass a `doFetch` that
227
+ * has not yet handed its body to the consumer.
228
+ */
229
+ const DEFAULT_RETRY_STATUSES = [
230
+ 429,
231
+ 500,
232
+ 502,
233
+ 503,
234
+ 504
235
+ ];
236
+ function parseRetryAfter(headerValue) {
237
+ if (!headerValue) return void 0;
238
+ const secs = Number(headerValue);
239
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
240
+ const dateMs = Date.parse(headerValue);
241
+ if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
242
+ }
243
+ /**
244
+ * A thrown fetch error that is worth retrying — network resets, broken
245
+ * pipes, DNS hiccups, and timeout aborts. NOTE: the caller must already
246
+ * have ruled out a user cancel (caller signal aborted) before calling
247
+ * this, since a timeout abort and a user-cancel abort look identical.
248
+ */
249
+ function isTransientNetworkError(err) {
250
+ const e = err;
251
+ if (!e) return false;
252
+ if (e.name === "AbortError" || e.name === "TimeoutError") return true;
253
+ const msg = (e.message ?? "").toLowerCase();
254
+ if (msg.includes("terminated") || msg.includes("fetch failed") || msg.includes("network") || msg.includes("socket") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("econnrefused") || msg.includes("epipe") || msg.includes("enotfound")) return true;
255
+ const code = e.code ?? e.cause?.code;
256
+ return code !== void 0 && [
257
+ "ECONNRESET",
258
+ "ETIMEDOUT",
259
+ "ECONNREFUSED",
260
+ "EPIPE",
261
+ "ENOTFOUND"
262
+ ].includes(code);
263
+ }
264
+ /** Sleep that resolves early (does not reject) when `signal` aborts — the
265
+ * retry loop re-checks `signal.aborted` at the top and throws there. */
266
+ function abortableSleep(ms, signal) {
267
+ if (ms <= 0) return Promise.resolve();
268
+ return new Promise((resolve) => {
269
+ let settled = false;
270
+ const done = () => {
271
+ if (settled) return;
272
+ settled = true;
273
+ clearTimeout(timer);
274
+ signal?.removeEventListener("abort", done);
275
+ resolve();
276
+ };
277
+ const timer = setTimeout(done, ms);
278
+ if (signal) {
279
+ if (signal.aborted) {
280
+ done();
281
+ return;
282
+ }
283
+ signal.addEventListener("abort", done, { once: true });
284
+ }
285
+ });
286
+ }
287
+ /**
288
+ * Run `doFetch` with bounded transient-failure retries. `doFetch` is a
289
+ * thunk so each attempt issues a FRESH request (bodies can't be replayed
290
+ * from a consumed stream). Returns the final `Response` — which may still
291
+ * carry a retryable status if all attempts are exhausted (the caller
292
+ * handles that as it would a single-shot failure).
293
+ */
294
+ async function fetchWithTransientRetry(doFetch, opts = {}) {
295
+ const attempts = Math.max(1, opts.attempts ?? 3);
296
+ const retryStatuses = opts.retryStatuses ?? DEFAULT_RETRY_STATUSES;
297
+ const baseDelayMs = opts.baseDelayMs ?? 250;
298
+ const maxDelayMs = opts.maxDelayMs ?? 4e3;
299
+ const { signal, label } = opts;
300
+ for (let attempt = 1;; attempt++) {
301
+ if (signal?.aborted) throw new DOMException("This operation was aborted", "AbortError");
302
+ let res;
303
+ let caught;
304
+ try {
305
+ res = await doFetch(attempt);
306
+ } catch (err) {
307
+ caught = err;
308
+ }
309
+ if (res && !retryStatuses.includes(res.status)) return res;
310
+ if (caught !== void 0) {
311
+ if (signal?.aborted) throw caught;
312
+ if (!isTransientNetworkError(caught)) throw caught;
313
+ }
314
+ if (attempt >= attempts) {
315
+ if (res) return res;
316
+ throw caught;
317
+ }
318
+ const retryAfterMs = res ? parseRetryAfter(res.headers.get("retry-after")) : void 0;
319
+ if (res?.body) try {
320
+ await res.body.cancel();
321
+ } catch {}
322
+ const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
323
+ const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
324
+ if (label) {
325
+ const why = res ? `HTTP ${res.status}` : caught?.name ?? "error";
326
+ consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay}ms`);
327
+ }
328
+ await abortableSleep(delay, signal);
329
+ }
330
+ }
331
+ /** Extract an HTTP status from a thrown error (HTTPError carries
332
+ * `.response.status`; others may carry `.status`/`.statusCode`; last
333
+ * resort parses `"HTTP <code>"` from the message). */
334
+ function errorStatus(err) {
335
+ const e = err;
336
+ for (const v of [
337
+ e?.status,
338
+ e?.statusCode,
339
+ e?.response?.status
340
+ ]) if (typeof v === "number") return v;
341
+ const m = /\bHTTP (\d{3})\b/.exec(e?.message ?? "");
342
+ return m ? Number(m[1]) : void 0;
343
+ }
344
+ /**
345
+ * Generic transient-retry for a non-`Response`-returning call (e.g. the
346
+ * Copilot service clients, which throw `HTTPError` on non-OK and throw on
347
+ * network errors). Retries `fn()` when it throws a transient error — an
348
+ * `HTTPError`-like with a retryable status (429/5xx) OR a transient
349
+ * network/timeout error — using the same backoff + abort semantics as
350
+ * `fetchWithTransientRetry`. Never retries a deterministic 4xx (incl.
351
+ * 401), a non-transient throw, or a user cancel.
352
+ */
353
+ async function withTransientRetry(fn, opts = {}) {
354
+ const attempts = Math.max(1, opts.attempts ?? 3);
355
+ const retryStatuses = opts.retryStatuses ?? DEFAULT_RETRY_STATUSES;
356
+ const baseDelayMs = opts.baseDelayMs ?? 250;
357
+ const maxDelayMs = opts.maxDelayMs ?? 4e3;
358
+ const { signal, label } = opts;
359
+ for (let attempt = 1;; attempt++) {
360
+ if (signal?.aborted) throw new DOMException("This operation was aborted", "AbortError");
361
+ try {
362
+ return await fn(attempt);
363
+ } catch (err) {
364
+ if (signal?.aborted) throw err;
365
+ const status = errorStatus(err);
366
+ if (!(status !== void 0 && retryStatuses.includes(status) || isTransientNetworkError(err)) || attempt >= attempts) throw err;
367
+ const retryAfterMs = parseRetryAfter(err?.response?.headers?.get?.("retry-after") ?? null);
368
+ const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
369
+ const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
370
+ if (label) consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${status !== void 0 ? `HTTP ${status}` : err?.name ?? "error"}); retrying in ${delay}ms`);
371
+ await abortableSleep(delay, signal);
372
+ }
373
+ }
374
+ }
375
+ //#endregion
376
+ //#region src/services/github/get-copilot-token.ts
377
+ /**
378
+ * Allowlist of hosts the router will trust as the Copilot API base URL.
379
+ * Anything else returned in `endpoints.api` (e.g. via a tampered or
380
+ * misconfigured token-exchange response) is rejected — otherwise a
381
+ * malicious value would receive the long-lived GitHub PAT we send to
382
+ * `/mcp` for web search (see `src/services/copilot/web-search.ts`).
383
+ */
384
+ const COPILOT_HOST_ALLOWLIST = [
385
+ "api.githubcopilot.com",
386
+ "api.individual.githubcopilot.com",
387
+ "api.business.githubcopilot.com",
388
+ "api.enterprise.githubcopilot.com"
389
+ ];
390
+ /**
391
+ * True iff `rawUrl` is an HTTPS URL whose host is a trusted Copilot API host.
392
+ * Exported so every path that sends a long-lived token to a discovered
393
+ * Copilot host (web search, first-mate CAPI session logs) shares one allowlist.
394
+ */
395
+ function isAllowedCopilotHost(rawUrl) {
396
+ let parsed;
397
+ try {
398
+ parsed = new URL(rawUrl);
399
+ } catch {
400
+ return false;
401
+ }
402
+ if (parsed.protocol !== "https:") return false;
403
+ return COPILOT_HOST_ALLOWLIST.includes(parsed.hostname);
404
+ }
405
+ const getCopilotToken = async () => {
406
+ const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(state) }), { label: "/copilot_internal/v2/token" });
407
+ if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
408
+ const data = await response.json();
409
+ if (data.endpoints?.api) if (isAllowedCopilotHost(data.endpoints.api)) state.copilotApiUrl = data.endpoints.api;
410
+ 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.`));
411
+ return data;
412
+ };
413
+ //#endregion
414
+ //#region src/services/github/get-device-code.ts
415
+ const DEFAULT_DEVICE_APP = {
416
+ clientId: GITHUB_CLIENT_ID,
417
+ scope: GITHUB_APP_SCOPES
418
+ };
419
+ async function getDeviceCode(app = DEFAULT_DEVICE_APP) {
420
+ const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_BASE_URL}/login/device/code`, {
421
+ method: "POST",
422
+ headers: standardHeaders(),
423
+ body: JSON.stringify({
424
+ client_id: app.clientId,
425
+ scope: app.scope
426
+ })
427
+ }), { label: "/login/device/code" });
428
+ if (!response.ok) throw new HTTPError("Failed to get device code", response);
429
+ return await response.json();
430
+ }
431
+ //#endregion
432
+ //#region src/services/github/get-user.ts
433
+ async function getGitHubUser() {
434
+ const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/user`, { headers: {
435
+ authorization: `token ${state.githubToken}`,
436
+ ...standardHeaders()
437
+ } }), { label: "/user" });
438
+ if (!response.ok) throw new HTTPError("Failed to get GitHub user", response);
439
+ return await response.json();
440
+ }
441
+ //#endregion
442
+ //#region src/services/copilot/get-models.ts
443
+ const getModels = async () => {
444
+ const response = await fetchWithTransientRetry(() => tryRefreshAndRetry(() => fetch(`${copilotBaseUrl(state)}/models`, { headers: copilotHeaders(state) }), "/models"), { label: "/models" });
445
+ if (!response.ok) throw new HTTPError("Failed to get models", response);
446
+ return await response.json();
447
+ };
448
+ //#endregion
449
+ //#region src/services/get-copilot-version.ts
450
+ /**
451
+ * Last-resort floor when the lookup fails. A fallback must never become the
452
+ * cached steady state — see the note on `VSCODE_VERSION_FALLBACK` in
453
+ * `./get-vscode-version`.
454
+ */
455
+ const COPILOT_CHAT_VERSION_FALLBACK = "0.43.2026033101";
456
+ /**
457
+ * The lookup, reporting failure as `undefined` rather than as the fallback.
458
+ * See `getVSCodeVersionOrUndefined` for why the distinction matters to the
459
+ * version cache.
460
+ */
461
+ async function getCopilotChatVersionOrUndefined() {
462
+ const controller = new AbortController();
463
+ const timeout = setTimeout(() => {
464
+ controller.abort();
465
+ }, 5e3);
466
+ try {
467
+ const response = await fetch("https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery", {
468
+ method: "POST",
469
+ headers: {
470
+ "Content-Type": "application/json",
471
+ Accept: "application/json;api-version=7.1-preview.1"
472
+ },
473
+ body: JSON.stringify({
474
+ filters: [{ criteria: [{
475
+ filterType: 7,
476
+ value: "GitHub.copilot-chat"
477
+ }] }],
478
+ flags: 914
479
+ }),
480
+ signal: controller.signal
481
+ });
482
+ if (!response.ok) return void 0;
483
+ return (await response.json())?.results?.[0]?.extensions?.[0]?.versions?.[0]?.version;
484
+ } catch {
485
+ return;
486
+ } finally {
487
+ clearTimeout(timeout);
488
+ }
489
+ }
490
+ //#endregion
491
+ //#region src/services/get-vscode-version.ts
492
+ /**
493
+ * Last-resort floor when the lookup fails. A fallback must never become the
494
+ * cached steady state, or a transient outage would freeze the version we
495
+ * impersonate. See `src/lib/editor-version-cache.ts`.
496
+ */
497
+ const VSCODE_VERSION_FALLBACK = "1.104.3";
498
+ /**
499
+ * The lookup, reporting failure as `undefined` rather than as the fallback.
500
+ *
501
+ * The distinction is load-bearing for the cache: it must not infer "the fetch
502
+ * failed" from `value === FALLBACK`, because the live version legitimately
503
+ * equals the constant right after someone bumps `VSCODE_VERSION_FALLBACK` to
504
+ * the then-current release — and that reading would refuse to cache a perfectly
505
+ * good result, making every launch re-pay the ~1.5s network call forever.
506
+ */
507
+ async function getVSCodeVersionOrUndefined() {
508
+ const controller = new AbortController();
509
+ const timeout = setTimeout(() => {
510
+ controller.abort();
511
+ }, 5e3);
512
+ try {
513
+ const match = (await (await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", { signal: controller.signal })).text()).match(/pkgver=([0-9.]+)/);
514
+ return match ? match[1] : void 0;
515
+ } catch {
516
+ return;
517
+ } finally {
518
+ clearTimeout(timeout);
519
+ }
520
+ }
521
+ //#endregion
522
+ //#region src/lib/editor-version-cache.ts
523
+ /**
524
+ * Disk-backed TTL cache for the EDITOR IDENTITY versions the proxy sends
525
+ * upstream: the VS Code version (`editor-version`) and the Copilot Chat
526
+ * extension version (`editor-plugin-version`), see `src/lib/api-config.ts`.
527
+ *
528
+ * Why this exists: impersonating VS Code is non-negotiable — Copilot is
529
+ * addressed as if by the real editor, so those headers must carry real,
530
+ * current versions. But the two lookups are NETWORK calls to third parties
531
+ * (the Arch AUR PKGBUILD and the VS Marketplace), and they sat on the
532
+ * blocking launch path of EVERY invocation. The AUR fetch alone measured
533
+ * ~1.5-1.8s, which every `github-router` process paid — including the
534
+ * per-turn Claude Code hooks (`internal-prompt-submit`, `internal-stop-hook`,
535
+ * …), each of which is a fresh `node dist/main.js` spawn.
536
+ *
537
+ * The fix is a cache, NOT a removal. Both fetches are still made; they are
538
+ * just made at most once per `TTL_HOURS` instead of once (twice, before the
539
+ * duplicate top-level await in `get-vscode-version.ts` was dropped) per
540
+ * launch. A 12h TTL keeps the impersonated versions well inside the real
541
+ * editor's release cadence.
542
+ *
543
+ * Two invariants that keep impersonation honest:
544
+ *
545
+ * 1. A FALLBACK is NEVER cached. `getVSCodeVersion()` /
546
+ * `getCopilotChatVersion()` return a hardcoded constant when the network
547
+ * fails, and that constant must stay a last-resort floor for the current
548
+ * process — never the persisted steady state. Persisting it would let a
549
+ * transient outage silently freeze the advertised version for 12h, and
550
+ * (worse) survive restarts. `writeEntry` is therefore only reached on a
551
+ * value that differs from the fallback.
552
+ * 2. The timestamp is written only on SUCCESS. An offline machine retries
553
+ * on the next launch rather than pinning a stale entry, which is the
554
+ * opposite of `claude-version-check.ts`'s throttle (that one stamps
555
+ * after the probes regardless, so an offline box pays both timeouts
556
+ * hourly). Do not copy that detail here.
557
+ *
558
+ * The file lives beside `last-update-check` / `last-self-update-check` and is
559
+ * deliberately named distinctly so the three throttles never cross-suppress.
560
+ */
561
+ const TTL_HOURS = 12;
562
+ function cacheFilePath() {
563
+ return path.join(PATHS.APP_DIR, "last-editor-versions");
564
+ }
565
+ async function readCache() {
566
+ try {
567
+ const parsed = JSON.parse(await fs.readFile(cacheFilePath(), "utf8"));
568
+ if (!parsed || typeof parsed !== "object") return {};
569
+ return parsed;
570
+ } catch {
571
+ return {};
572
+ }
573
+ }
574
+ function isFresh(entry) {
575
+ if (!entry || typeof entry.value !== "string" || entry.value.length === 0) return false;
576
+ const last = new Date(entry.checkedAt).getTime();
577
+ if (Number.isNaN(last)) return false;
578
+ const ageHours = (Date.now() - last) / 1e3 / 3600;
579
+ return ageHours >= 0 && ageHours < TTL_HOURS;
580
+ }
581
+ /**
582
+ * Serializes the read-modify-write below.
583
+ *
584
+ * `setupAndServe` resolves both keys concurrently (`Promise.all`). Without
585
+ * this, both callers would `readCache()` into their OWN object, spend ~1.5s in
586
+ * their network fetch, and then write that stale object back — so whichever
587
+ * finished last would clobber the other's entry, and one of the two versions
588
+ * would re-fetch on every launch forever.
589
+ *
590
+ * The chain also re-reads immediately before mutating, so the write is against
591
+ * current disk state rather than the snapshot taken before the fetch.
592
+ */
593
+ let writeChain = Promise.resolve();
594
+ function serializeWrite(fn) {
595
+ const next = writeChain.then(fn, fn);
596
+ writeChain = next.catch(() => {});
597
+ return next;
598
+ }
599
+ /**
600
+ * Resolve `key` through the cache, calling `fetchFresh` only on a miss.
601
+ *
602
+ * `fetchFresh` signals failure by returning `undefined` — NOT by returning the
603
+ * fallback constant. Inferring failure from `value === fallback` would break
604
+ * exactly when the live version happens to equal the hardcoded constant, which
605
+ * is the normal state right after someone bumps that constant to the
606
+ * then-current version: the entry would never be written and every launch
607
+ * would pay the ~1.5s fetch forever, silently defeating the cache.
608
+ *
609
+ * `fallback` is returned when the lookup fails and no usable cached value
610
+ * exists. It is never persisted (invariant 1 above), so a transient outage
611
+ * cannot freeze the version we impersonate.
612
+ */
613
+ async function resolveEditorVersion(key, fetchFresh, fallback) {
614
+ const entry = (await readCache())[key];
615
+ const cachedValue = isFresh(entry) ? entry.value : void 0;
616
+ if (cachedValue !== void 0) return cachedValue;
617
+ const fetched = await fetchFresh();
618
+ if (fetched !== void 0 && fetched.length > 0) {
619
+ await serializeWrite(async () => {
620
+ const cache = await readCache();
621
+ cache[key] = {
622
+ value: fetched,
623
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
624
+ };
625
+ try {
626
+ await fs.mkdir(path.dirname(cacheFilePath()), { recursive: true });
627
+ await fs.writeFile(cacheFilePath(), JSON.stringify(cache), { mode: 384 });
628
+ } catch (err) {
629
+ consola.debug("Failed to write editor-version cache:", err);
630
+ }
631
+ });
632
+ return fetched;
633
+ }
634
+ if (typeof entry?.value === "string" && entry.value.length > 0) return entry.value;
635
+ return fallback;
636
+ }
637
+ //#endregion
638
+ //#region src/lib/utils.ts
639
+ const sleep = (ms) => new Promise((resolve) => {
640
+ setTimeout(resolve, ms);
641
+ });
642
+ const isNullish = (value) => value === null || value === void 0;
643
+ /**
644
+ * Beta prefixes VS Code Copilot Chat v0.43 actually sends.
645
+ * Default mode — makes proxy traffic indistinguishable from VS Code.
646
+ */
647
+ const VSCODE_BETA_PREFIXES = [
648
+ "interleaved-thinking-",
649
+ "context-management-",
650
+ "advanced-tool-use-"
651
+ ];
652
+ /**
653
+ * Extended beta prefixes for Claude CLI compatibility.
654
+ * Enabled via --extended-betas flag. Includes all betas confirmed
655
+ * to work with the Copilot API.
656
+ *
657
+ * Notably absent (Copilot 400s on these — verified live):
658
+ * context-1m-, skills-, files-api-, code-execution-, output-128k-,
659
+ * advisor-tool- (see EXPLICITLY_STRIPPED_BETA_PREFIXES below).
660
+ * 1M context is unlocked by selecting `claude-opus-4.7-1m-internal`
661
+ * as the model id, not via a beta header.
662
+ *
663
+ * Empirical verification (2026-05-11 against api.enterprise.githubcopilot.com):
664
+ * task-budgets-2026-03-13 → 200 ACCEPTED (cost-ceiling leverage)
665
+ * token-efficient-tools-2026-03-28 → 200 ACCEPTED (per-tool token saving)
666
+ * summarize-connector-text-2026-03-13 → 200 (Anthropic-internal feature flag,
667
+ * won't fire for non-ant users; allowlisted defensively for ant edge case)
668
+ * afk-mode-2026-01-31 → 200 (Anthropic-internal feature flag)
669
+ * cli-internal-2026-02-09 → 200 (USER_TYPE=ant only)
670
+ * oauth-2025-04-20 → 200 (Files-API path; Files-API itself
671
+ * is not supportable via Copilot, but the header alone is harmless)
672
+ * prompt-caching-scope-2026-01-05 → 200 even with body cache_control.scope
673
+ * stripped (already covered by `prompt-caching-` prefix above)
674
+ */
675
+ const EXTENDED_BETA_PREFIXES = [
676
+ ...VSCODE_BETA_PREFIXES,
677
+ "claude-code-",
678
+ "effort-",
679
+ "prompt-caching-",
680
+ "computer-use-",
681
+ "pdfs-",
682
+ "max-tokens-",
683
+ "token-counting-",
684
+ "compact-",
685
+ "structured-outputs-",
686
+ "fast-mode-",
687
+ "mcp-client-",
688
+ "mcp-servers-",
689
+ "redact-thinking-",
690
+ "web-search-",
691
+ "task-budgets-",
692
+ "token-efficient-tools-",
693
+ "summarize-connector-text-",
694
+ "afk-mode-",
695
+ "cli-internal-",
696
+ "oauth-"
697
+ ];
698
+ /**
699
+ * Beta prefixes the proxy explicitly STRIPS even from the extended
700
+ * allowlist (and even if a future leverage mode broadens the allowlist
701
+ * further). Defensive layer: today's allowlist-only filter would already
702
+ * drop these because they're not in any allowlist, but keeping an
703
+ * explicit deny-list catches future changes that broaden allow rules
704
+ * (e.g. a hypothetical pattern-based mode that lets `claude-*` through).
705
+ *
706
+ * Empirical (2026-05-11): Copilot returns HTTP 400
707
+ * `unsupported beta header(s): advisor-tool-2026-03-01`
708
+ * on every request that includes `advisor-tool-`. Stripping it is the
709
+ * difference between a working request (no ADVISOR semantics) and a
710
+ * fully-failed request. Document upstream limitation in CLAUDE.md.
711
+ */
712
+ const EXPLICITLY_STRIPPED_BETA_PREFIXES = ["advisor-tool-"];
713
+ /**
714
+ * Filter an `anthropic-beta` header value, keeping only beta flags
715
+ * in the active whitelist AND not in the explicit-strip list.
716
+ * Uses extended prefixes when --extended-betas is enabled, VS Code-only
717
+ * prefixes otherwise. Returns the filtered comma-separated string,
718
+ * or undefined if nothing remains.
719
+ */
720
+ function filterBetaHeader(value) {
721
+ const prefixes = state.extendedBetas ? EXTENDED_BETA_PREFIXES : VSCODE_BETA_PREFIXES;
722
+ return value.split(",").map((v) => v.trim()).filter((v) => v && prefixes.some((prefix) => v.startsWith(prefix)) && !EXPLICITLY_STRIPPED_BETA_PREFIXES.some((p) => v.startsWith(p))).join(",") || void 0;
723
+ }
724
+ /**
725
+ * Normalize a model ID for fuzzy comparison: lowercase, replace dots with
726
+ * dashes, insert dash at letter→digit boundaries, and collapse repeated
727
+ * dashes. E.g. "gpt5.3-codex" → "gpt-5-3-codex", "GPT-5.3-Codex" → "gpt-5-3-codex".
728
+ */
729
+ function normalizeModelId(id) {
730
+ return id.toLowerCase().replace(/\./g, "-").replace(/([a-z])(\d)/g, "$1-$2").replace(/-{2,}/g, "-");
731
+ }
732
+ /**
733
+ * Resolve a model name to the best available variant in the Copilot model list.
734
+ *
735
+ * Resolution cascade:
736
+ * 0. `[1m]` literal-bracket suffix: strip, delegate, warn if downgraded.
737
+ * Bracketed slug must never reach Copilot (400s on it). See cc-backup
738
+ * `src/utils/context.ts:35-40` for Claude Code's 1M unlock mechanism.
739
+ * 1. Exact match
740
+ * 2. Case-insensitive match
741
+ * 3. Family preference (opus→1m, codex→highest version)
742
+ * 4. Normalized match (dots→dashes, letter-digit boundaries)
743
+ * 5. Anthropic dated-slug retry: if the input matches `claude-...-YYYYMMDD`,
744
+ * strip the date and re-run the cascade once. Family-guarded so non-claude
745
+ * 8-digit suffixes can't be mis-stripped; runs after Steps 1-4 so explicit
746
+ * version pinning (a dated catalog id matched at Step 1) always wins.
747
+ * 6. Return as-is with a warning
748
+ */
749
+ /**
750
+ * `[1m]`-downgrade warnings we've already emitted this process. `resolveModel`
751
+ * is called many times per launch (env setup, model validation, the banner,
752
+ * and — under `serve` — on every model-picker `/models` poll), so the downgrade
753
+ * notice is deduped to once per model id to avoid log spam.
754
+ */
755
+ const warnedOneMDowngrade = /* @__PURE__ */ new Set();
756
+ function resolveModel(modelId) {
757
+ const models = state.models?.data;
758
+ if (!models) return modelId;
759
+ const oneMMatch = modelId.match(/^(.*)\[1m\]$/i);
760
+ if (oneMMatch) {
761
+ const stripped = oneMMatch[1];
762
+ const resolved = resolveModel(stripped);
763
+ if (!(/-1m(?:$|-)/.test(resolved) || (models.find((m) => m.id === resolved)?.capabilities?.limits?.max_context_window_tokens ?? 0) >= 1e6) && !warnedOneMDowngrade.has(modelId)) {
764
+ warnedOneMDowngrade.add(modelId);
765
+ consola.warn(`Model "${modelId}" requested 1M context but no 1M backend is in Copilot's catalog for this tier/family; downgrading upstream to "${resolved}" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`);
766
+ }
767
+ return resolved;
768
+ }
769
+ if (models.some((m) => m.id === modelId)) return modelId;
770
+ const lower = modelId.toLowerCase();
771
+ const ciMatch = models.find((m) => m.id.toLowerCase() === lower);
772
+ if (ciMatch) return ciMatch.id;
773
+ if (lower.includes("opus")) {
774
+ const oneMs = models.filter((m) => m.id.includes("opus") && /-1m(?:$|-)/.test(m.id));
775
+ const versionMatch = lower.match(/opus-(\d+)(?:[.-](\d+))?/);
776
+ const requestedVersion = versionMatch ? versionMatch[2] ? `${versionMatch[1]}.${versionMatch[2]}` : versionMatch[1] : void 0;
777
+ const oneM = (requestedVersion ? oneMs.find((m) => m.id.includes(`opus-${requestedVersion}-`)) : void 0) ?? (requestedVersion ? void 0 : oneMs[0]);
778
+ if (oneM) return oneM.id;
779
+ }
780
+ if (lower.includes("codex")) {
781
+ const codexModels = models.filter((m) => m.id.includes("codex") && !m.id.includes("mini"));
782
+ if (codexModels.length > 0) {
783
+ codexModels.sort((a, b) => b.id.localeCompare(a.id));
784
+ return codexModels[0].id;
785
+ }
786
+ }
787
+ const normalized = normalizeModelId(modelId);
788
+ const normMatch = models.find((m) => normalizeModelId(m.id) === normalized);
789
+ if (normMatch) return normMatch.id;
790
+ const dateStripped = modelId.replace(/^(claude-[\w.-]+)-20\d{6}$/i, "$1");
791
+ if (dateStripped !== modelId) {
792
+ const retried = resolveModel(dateStripped);
793
+ if (retried !== dateStripped || models.some((m) => m.id === dateStripped)) {
794
+ consola.info(`Resolved Anthropic dated slug "${modelId}" → "${retried}" (stripped -YYYYMMDD; pass an explicit catalog id to pin a snapshot)`);
795
+ return retried;
796
+ }
797
+ }
798
+ if (lower.startsWith("claude-")) {
799
+ const matchSonnet = /(?:^|-)sonnet(?:-|$)/.test(lower);
800
+ const matchHaiku = /(?:^|-)haiku(?:-|$)/.test(lower);
801
+ if (matchSonnet || matchHaiku) {
802
+ const family = matchSonnet ? "sonnet" : "haiku";
803
+ const familyMembers = models.filter((m) => new RegExp(`(?:^|-)${family}(?:-|$|\\.)`).test(m.id));
804
+ if (familyMembers.length > 0) {
805
+ familyMembers.sort((a, b) => b.id.localeCompare(a.id, void 0, { numeric: true }));
806
+ const best = familyMembers[0].id;
807
+ consola.info(`Model "${modelId}" not in Copilot catalog; falling back to highest available "${best}" (legacy ${family} slug). Pin a current catalog id to silence.`);
808
+ return best;
809
+ }
810
+ }
811
+ }
812
+ consola.warn(`Model "${modelId}" not found in Copilot model list. Available: ${models.map((m) => m.id).join(", ")}`);
813
+ return modelId;
814
+ }
815
+ /**
816
+ * Resolve a codex model ID, falling back to the best available codex model.
817
+ * Used by the codex subcommand for model selection.
818
+ */
819
+ function resolveCodexModel(modelId) {
820
+ const resolved = resolveModel(modelId);
821
+ const models = state.models?.data;
822
+ if (!models) return resolved;
823
+ if (models.some((m) => m.id === resolved)) return resolved;
824
+ const candidates = models.filter((m) => {
825
+ const endpoints = m.supported_endpoints ?? [];
826
+ if (m.id.includes("mini") || m.id.includes("nano")) return false;
827
+ return endpoints.length === 0 || endpoints.includes("/responses");
828
+ });
829
+ if (candidates.length > 0) {
830
+ candidates.sort((a, b) => {
831
+ const aCodex = a.id.includes("codex") ? 1 : 0;
832
+ const bCodex = b.id.includes("codex") ? 1 : 0;
833
+ if (aCodex !== bCodex) return bCodex - aCodex;
834
+ return b.id.localeCompare(a.id);
835
+ });
836
+ const best = candidates[0].id;
837
+ consola.warn(`Model "${modelId}" not available, using "${best}" instead`);
838
+ return best;
839
+ }
840
+ return resolved;
841
+ }
842
+ async function cacheModels() {
843
+ const models = await getModels();
844
+ state.models = models;
845
+ }
846
+ const cacheVSCodeVersion = async () => {
847
+ const response = await resolveEditorVersion("vscode", getVSCodeVersionOrUndefined, VSCODE_VERSION_FALLBACK);
848
+ state.vsCodeVersion = response;
849
+ consola.info(`Using VSCode version: ${response}`);
850
+ };
851
+ const cacheCopilotVersion = async () => {
852
+ const version = await resolveEditorVersion("copilotChat", getCopilotChatVersionOrUndefined, COPILOT_CHAT_VERSION_FALLBACK);
853
+ state.copilotVersion = version;
854
+ consola.info(`Using Copilot Chat version: ${version}`);
855
+ };
856
+ //#endregion
857
+ //#region src/services/github/poll-access-token.ts
858
+ async function pollAccessToken(deviceCode, clientId = GITHUB_CLIENT_ID) {
859
+ const sleepDuration = (deviceCode.interval + 1) * 1e3;
860
+ consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
861
+ const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
862
+ while (Date.now() < expiresAt) {
863
+ const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
864
+ method: "POST",
865
+ headers: standardHeaders(),
866
+ body: JSON.stringify({
867
+ client_id: clientId,
868
+ device_code: deviceCode.device_code,
869
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
870
+ })
871
+ });
872
+ if (!response.ok) {
873
+ consola.error("Failed to poll access token:", await response.text());
874
+ if (Date.now() >= expiresAt) break;
875
+ await sleep(sleepDuration);
876
+ continue;
877
+ }
878
+ const json = await response.json();
879
+ consola.debug("Polling access token response:", json);
880
+ const { access_token } = json;
881
+ if (access_token) return access_token;
882
+ if (Date.now() >= expiresAt) break;
883
+ await sleep(sleepDuration);
884
+ }
885
+ throw new Error("Device code expired. Please run auth again.");
886
+ }
887
+ //#endregion
888
+ //#region src/lib/token.ts
889
+ const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
890
+ const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
891
+ const readGithubAgentToken = () => fs.readFile(PATHS.GITHUB_AGENT_TOKEN_PATH, "utf8");
892
+ const writeGithubAgentToken = (token) => fs.writeFile(PATHS.GITHUB_AGENT_TOKEN_PATH, token, { mode: 384 });
893
+ const setupCopilotToken = async () => {
894
+ const { token, refresh_in } = await getCopilotToken();
895
+ state.copilotToken = token;
896
+ consola.debug("GitHub Copilot Token fetched successfully!");
897
+ if (state.showToken) consola.info("Copilot token:", token);
898
+ const refreshInterval = Math.max((refresh_in - 60) * 1e3, 1e3);
899
+ setInterval(() => {
900
+ refreshCopilotToken("interval");
901
+ }, refreshInterval);
902
+ };
903
+ let inflightRefresh;
904
+ let lastRefreshSuccess = 0;
905
+ let lastRefreshFailure = 0;
906
+ const REFRESH_SUCCESS_COOLDOWN_MS = 3e4;
907
+ const REFRESH_FAILURE_COOLDOWN_MS = 5e3;
908
+ async function refreshCopilotToken(reason) {
909
+ if (inflightRefresh) return inflightRefresh;
910
+ if (reason === "401-retry") {
911
+ const now = Date.now();
912
+ if (now - lastRefreshSuccess < REFRESH_SUCCESS_COOLDOWN_MS) {
913
+ consola.debug(`refreshCopilotToken(${reason}) skipped: prior success within ${REFRESH_SUCCESS_COOLDOWN_MS}ms`);
914
+ return;
915
+ }
916
+ if (now - lastRefreshFailure < REFRESH_FAILURE_COOLDOWN_MS) {
917
+ consola.debug(`refreshCopilotToken(${reason}) skipped: prior failure within ${REFRESH_FAILURE_COOLDOWN_MS}ms`);
918
+ return;
919
+ }
920
+ }
921
+ inflightRefresh = (async () => {
922
+ consola.debug(`Refreshing Copilot token (reason=${reason})`);
923
+ try {
924
+ const { token } = await getCopilotToken();
925
+ state.copilotToken = token;
926
+ lastRefreshSuccess = Date.now();
927
+ consola.debug("Copilot token refreshed");
928
+ if (state.showToken) consola.info("Refreshed Copilot token:", token);
929
+ } catch (error) {
930
+ lastRefreshFailure = Date.now();
931
+ consola.error(`Failed to refresh Copilot token (reason=${reason}):`, error);
932
+ } finally {
933
+ inflightRefresh = void 0;
934
+ }
935
+ })();
936
+ return inflightRefresh;
937
+ }
938
+ /**
939
+ * Try `request()`. If it returns a 401, refresh the Copilot token (subject
940
+ * to the single-flight + refresh-storm-protection of `refreshCopilotToken`)
941
+ * and retry once. After one retry, propagate whatever the second attempt
942
+ * returned — the caller's existing 401-handling path is preserved.
943
+ *
944
+ * The `request` callback is responsible for capturing `state.copilotToken`
945
+ * locally before any await; this helper does NOT re-build the request
946
+ * itself, just re-invokes the callback after a refresh.
947
+ */
948
+ async function tryRefreshAndRetry(request, routePath) {
949
+ const first = await request();
950
+ if (first.status !== 401) return first;
951
+ consola.warn(`${routePath}: upstream returned 401, attempting one token refresh + retry`);
952
+ await refreshCopilotToken("401-retry");
953
+ return request();
954
+ }
955
+ async function setupGitHubToken(options) {
956
+ try {
957
+ const githubToken = await readGithubToken();
958
+ if (githubToken && !options?.force) {
959
+ state.githubToken = githubToken;
960
+ if (state.showToken) consola.info("GitHub token:", githubToken);
961
+ await logUser();
962
+ return;
963
+ }
964
+ consola.info("Not logged in, getting new access token");
965
+ const response = await getDeviceCode();
966
+ consola.debug("Device code response:", response);
967
+ consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
968
+ const token = await pollAccessToken(response);
969
+ await writeGithubToken(token);
970
+ state.githubToken = token;
971
+ if (state.showToken) consola.info("GitHub token:", token);
972
+ await logUser();
973
+ } catch (error) {
974
+ if (error instanceof HTTPError) {
975
+ consola.error("Failed to get GitHub token:", await error.response.json());
976
+ throw error;
977
+ }
978
+ consola.error("Failed to get GitHub token:", error);
979
+ throw error;
980
+ }
981
+ }
982
+ async function logUser() {
983
+ const user = await getGitHubUser();
984
+ consola.info(`Logged in as ${user.login}`);
985
+ }
986
+ /**
987
+ * Set up the SECOND, write-capable GitHub token used by the first-mate
988
+ * agent-orchestration surface (`--agents`). Mirrors `setupGitHubToken`
989
+ * but authenticates against the GitHub CLI's OAuth client
990
+ * (`GITHUB_AGENT_CLIENT_ID`) requesting `repo workflow read:org`, and
991
+ * stores the result apart at `PATHS.GITHUB_AGENT_TOKEN_PATH`. The Copilot
992
+ * App token (`state.githubToken`) is left completely untouched — this is
993
+ * a distinct identity for a distinct capability.
994
+ *
995
+ * Long-lived (device-flow user token) → no refresh loop; a later 401 is
996
+ * surfaced to the caller as a revoked grant to re-run the login. Called
997
+ * once from `setupAndServe` when `state.agentsEnabled` is true.
998
+ */
999
+ async function setupGitHubAgentToken(options) {
1000
+ try {
1001
+ const existing = (await readGithubAgentToken().catch(() => "")).trim();
1002
+ if (existing && !options?.force) {
1003
+ state.githubAgentToken = existing;
1004
+ if (state.showToken) consola.info("GitHub agent token:", existing);
1005
+ await warnIfAgentScopesInsufficient();
1006
+ return;
1007
+ }
1008
+ consola.info("Agent mode (--agents): a second GitHub login is required for a write-capable token (repo, workflow, read:org).");
1009
+ const response = await getDeviceCode({
1010
+ clientId: GITHUB_AGENT_CLIENT_ID,
1011
+ scope: GITHUB_AGENT_SCOPES
1012
+ });
1013
+ consola.debug("Agent device code response:", response);
1014
+ consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri} to authorize github-router's cloud-agent orchestration to act on your repositories.`);
1015
+ const token = await pollAccessToken(response, GITHUB_AGENT_CLIENT_ID);
1016
+ await writeGithubAgentToken(token);
1017
+ state.githubAgentToken = token;
1018
+ if (state.showToken) consola.info("GitHub agent token:", token);
1019
+ await warnIfAgentScopesInsufficient();
1020
+ } catch (error) {
1021
+ if (error instanceof HTTPError) {
1022
+ consola.error("Failed to get GitHub agent token:", await error.response.json());
1023
+ throw error;
1024
+ }
1025
+ consola.error("Failed to get GitHub agent token:", error);
1026
+ throw error;
1027
+ }
1028
+ }
1029
+ /**
1030
+ * Best-effort check that the agent token actually carries the scopes we
1031
+ * asked for. The GitHub CLI OAuth client is a classic OAuth App, so the
1032
+ * granted scopes are echoed in the `x-oauth-scopes` response header on
1033
+ * any authenticated call. Warn loudly (not fatal) if `repo`/`workflow`
1034
+ * are missing so the failure is diagnosable at login rather than at the
1035
+ * first write 403.
1036
+ */
1037
+ async function warnIfAgentScopesInsufficient() {
1038
+ try {
1039
+ const res = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: githubAgentHeaders(state) });
1040
+ if (res.status === 401) {
1041
+ consola.warn("GitHub agent token was rejected (401) — the grant may have been revoked. Re-run with --agents to log in again.");
1042
+ return;
1043
+ }
1044
+ const scopes = (res.headers.get("x-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1045
+ const missing = ["repo", "workflow"].filter((s) => !scopes.includes(s));
1046
+ if (missing.length > 0) consola.warn(`GitHub agent token is missing scope(s): ${missing.join(", ")}. The first-mate surface needs 'repo' + 'workflow' to create issues, assign cloud agents, and dispatch workflows. Re-run the agent login and grant the requested scopes.`);
1047
+ } catch (err) {
1048
+ consola.debug("Agent token scope check skipped:", err);
1049
+ }
1050
+ }
1051
+ //#endregion
1052
+ export { copilotHeaders as C, githubHeaders as D, githubAgentHeaders as E, state as O, copilotBaseUrl as S, githubAgentGraphQLHeaders as T, withTransientRetry as _, cacheCopilotVersion as a, GITHUB_API_BASE_URL as b, filterBetaHeader as c, resolveModel as d, sleep as f, fetchWithTransientRetry as g, isAllowedCopilotHost as h, tryRefreshAndRetry as i, 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, HTTPError as v, copilotVersion as w, GITHUB_GRAPHQL_URL as x, forwardError as y };
1053
+
1054
+ //# sourceMappingURL=token-R2ocFC3M.js.map