github-router 0.3.250 → 0.3.256
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attribution-settings-jqkXfeVr.js +2347 -0
- package/dist/attribution-settings-jqkXfeVr.js.map +1 -0
- package/dist/auth-CP4q2CjO.js +44 -0
- package/dist/auth-CP4q2CjO.js.map +1 -0
- package/dist/browser-ext/background.js +406 -30
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/check-usage-6PB5IUFb.js +43 -0
- package/dist/check-usage-6PB5IUFb.js.map +1 -0
- package/dist/claude-Cpy7b9gd.js +593 -0
- package/dist/claude-Cpy7b9gd.js.map +1 -0
- package/dist/client-CQMfroGV.js +413 -0
- package/dist/client-CQMfroGV.js.map +1 -0
- package/dist/codex-BJK_QVsI.js +85 -0
- package/dist/codex-BJK_QVsI.js.map +1 -0
- package/dist/debug-CjmE9sgG.js +81 -0
- package/dist/debug-CjmE9sgG.js.map +1 -0
- package/dist/engine-DXn5XmFp.js +2 -0
- package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
- package/dist/exec-DTO2oin0.js.map +1 -0
- package/dist/file-log-reporter-IFQcL5IK.js +158 -0
- package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
- package/dist/gate-discovery-Bm1nxHSU.js +635 -0
- package/dist/gate-discovery-Bm1nxHSU.js.map +1 -0
- package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
- package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
- package/dist/hook-mcp-client-DBvm9608.js +136 -0
- package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
- package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
- package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
- package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
- package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
- package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
- package/dist/internal-plan-review-ipnT3bhj.js +114 -0
- package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
- package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
- package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
- package/dist/internal-session-bind-DwK2iGW0.js +133 -0
- package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
- package/dist/internal-stop-hook-BaBkqJyg.js +212 -0
- package/dist/internal-stop-hook-BaBkqJyg.js.map +1 -0
- package/dist/internal-stop-review-AUcFssrW.js +122 -0
- package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
- package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
- package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
- package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
- package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
- package/dist/lifecycle-Byw5s1G8.js +134 -0
- package/dist/lifecycle-Byw5s1G8.js.map +1 -0
- package/dist/lifecycle-D80CaWie.js +2 -0
- package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
- package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
- package/dist/lifecycle-DzqCqDlM.js +2 -0
- package/dist/main.js +43 -12904
- package/dist/main.js.map +1 -1
- package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
- package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
- package/dist/models-B6UnGyRo.js +158 -0
- package/dist/models-B6UnGyRo.js.map +1 -0
- package/dist/orchestration-hRU4TdWr.js +39 -0
- package/dist/orchestration-hRU4TdWr.js.map +1 -0
- package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
- package/dist/paths-C1cLZ7Ga.js.map +1 -0
- package/dist/paths-CwKwc2pi.js +2 -0
- package/dist/{peer-mcp-personas-BvommiSI.js → peer-mcp-personas-BPl6_V2A.js} +288 -2445
- package/dist/peer-mcp-personas-BPl6_V2A.js.map +1 -0
- package/dist/plan-review-hook-C8x0uKPf.js +159 -0
- package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
- package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
- package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
- package/dist/provision-DueN4M8G.js +656 -0
- package/dist/provision-DueN4M8G.js.map +1 -0
- package/dist/serve-GN-flJmF.js +1511 -0
- package/dist/serve-GN-flJmF.js.map +1 -0
- package/dist/server-setup-BJL-X7w6.js +4776 -0
- package/dist/server-setup-BJL-X7w6.js.map +1 -0
- package/dist/start-CE0k-weK.js +129 -0
- package/dist/start-CE0k-weK.js.map +1 -0
- package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
- package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
- package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
- package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
- package/dist/token-R2ocFC3M.js +1054 -0
- package/dist/token-R2ocFC3M.js.map +1 -0
- package/dist/version-_Q1WpsQp.js +32 -0
- package/dist/version-_Q1WpsQp.js.map +1 -0
- package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
- package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
- package/package.json +1 -1
- package/dist/engine-dOYoMQis.js +0 -2
- package/dist/lifecycle-0rXrwjWk.js +0 -2
- package/dist/lifecycle-DHZsVN8j.js.map +0 -1
- package/dist/lifecycle-hzPwXc3I.js +0 -2
- package/dist/paths-BjTMI_xK.js.map +0 -1
- package/dist/paths-D3XgmTcQ.js +0 -2
- package/dist/peer-mcp-personas-BvommiSI.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
|