@nick3/copilot-api 1.0.3
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/LICENSE +21 -0
- package/README.md +599 -0
- package/dist/accounts-manager-CxuKJ4qv.js +1326 -0
- package/dist/accounts-manager-CxuKJ4qv.js.map +1 -0
- package/dist/admin/assets/index-CFHE7_Zc.js +55 -0
- package/dist/admin/assets/index-DwLAH2FE.css +1 -0
- package/dist/admin/index.html +27 -0
- package/dist/admin/vite.svg +1 -0
- package/dist/main.js +746 -0
- package/dist/main.js.map +1 -0
- package/dist/server-D05YP0C0.js +5513 -0
- package/dist/server-D05YP0C0.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1326 @@
|
|
|
1
|
+
import consola from "consola";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import fs$1 from "node:fs";
|
|
8
|
+
|
|
9
|
+
//#region src/lib/paths.ts
|
|
10
|
+
const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
|
|
11
|
+
const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
|
|
12
|
+
const CONFIG_PATH = path.join(APP_DIR, "config.json");
|
|
13
|
+
const MODELS_PATH = path.join(APP_DIR, "models.json");
|
|
14
|
+
const TOKENS_DIR = path.join(APP_DIR, "tokens");
|
|
15
|
+
const ACCOUNTS_REGISTRY_PATH = path.join(APP_DIR, "accounts-registry.json");
|
|
16
|
+
const PATHS = {
|
|
17
|
+
APP_DIR,
|
|
18
|
+
GITHUB_TOKEN_PATH,
|
|
19
|
+
CONFIG_PATH,
|
|
20
|
+
MODELS_PATH,
|
|
21
|
+
TOKENS_DIR,
|
|
22
|
+
ACCOUNTS_REGISTRY_PATH
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Get the token file path for a specific account.
|
|
26
|
+
* @param id - The account ID (GitHub login)
|
|
27
|
+
* @returns The absolute path to the account's token file
|
|
28
|
+
*/
|
|
29
|
+
function accountTokenPath(id) {
|
|
30
|
+
return path.join(TOKENS_DIR, `github_${id}`);
|
|
31
|
+
}
|
|
32
|
+
async function ensurePaths() {
|
|
33
|
+
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
34
|
+
await fs.mkdir(PATHS.TOKENS_DIR, { recursive: true });
|
|
35
|
+
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
|
|
36
|
+
await ensureFile(PATHS.CONFIG_PATH);
|
|
37
|
+
}
|
|
38
|
+
async function ensureFile(filePath) {
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(filePath, fs.constants.W_OK);
|
|
41
|
+
} catch {
|
|
42
|
+
await fs.writeFile(filePath, "");
|
|
43
|
+
await fs.chmod(filePath, 384);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/lib/accounts-registry.ts
|
|
49
|
+
/**
|
|
50
|
+
* Validate account ID (GitHub login).
|
|
51
|
+
* Rules:
|
|
52
|
+
* - Only alphanumeric characters or single hyphens
|
|
53
|
+
* - 1-39 chars
|
|
54
|
+
* - Cannot begin or end with a hyphen
|
|
55
|
+
* - No consecutive hyphens
|
|
56
|
+
*/
|
|
57
|
+
function validateAccountId(id) {
|
|
58
|
+
if (id.length === 0 || id.length > 39) return false;
|
|
59
|
+
if (!/^[a-z0-9-]+$/i.test(id)) return false;
|
|
60
|
+
if (id.startsWith("-") || id.endsWith("-")) return false;
|
|
61
|
+
if (id.includes("--")) return false;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const accountMetaSchema = z.object({
|
|
65
|
+
id: z.string().refine(validateAccountId, { message: "Invalid account id. Expected a GitHub login (1-39 chars, alphanumeric or single hyphens, no leading/trailing hyphen, no consecutive hyphens)." }),
|
|
66
|
+
accountType: z.enum([
|
|
67
|
+
"individual",
|
|
68
|
+
"business",
|
|
69
|
+
"enterprise"
|
|
70
|
+
]),
|
|
71
|
+
addedAt: z.number()
|
|
72
|
+
});
|
|
73
|
+
const accountRegistrySchema = z.object({
|
|
74
|
+
version: z.literal(1),
|
|
75
|
+
accounts: z.array(accountMetaSchema)
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* Create an empty registry with the current schema version.
|
|
79
|
+
*/
|
|
80
|
+
function createEmptyRegistry() {
|
|
81
|
+
return {
|
|
82
|
+
version: 1,
|
|
83
|
+
accounts: []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Load the accounts registry from disk.
|
|
88
|
+
* Returns an empty registry if the file doesn't exist.
|
|
89
|
+
*/
|
|
90
|
+
async function loadRegistry() {
|
|
91
|
+
try {
|
|
92
|
+
const content = await fs.readFile(PATHS.ACCOUNTS_REGISTRY_PATH, "utf8");
|
|
93
|
+
if (!content.trim()) return createEmptyRegistry();
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(content);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new Error(`Invalid accounts registry JSON at ${PATHS.ACCOUNTS_REGISTRY_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
|
+
}
|
|
100
|
+
const result = accountRegistrySchema.safeParse(parsed);
|
|
101
|
+
if (!result.success) {
|
|
102
|
+
const issues = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
103
|
+
throw new Error(`Invalid accounts registry at ${PATHS.ACCOUNTS_REGISTRY_PATH}: ${issues}`);
|
|
104
|
+
}
|
|
105
|
+
const registry = result.data;
|
|
106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
107
|
+
for (const account of registry.accounts) {
|
|
108
|
+
if (seen.has(account.id)) throw new Error(`Invalid accounts registry at ${PATHS.ACCOUNTS_REGISTRY_PATH}: duplicate account id "${account.id}"`);
|
|
109
|
+
seen.add(account.id);
|
|
110
|
+
}
|
|
111
|
+
return registry;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === "ENOENT") return createEmptyRegistry();
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Save the accounts registry to disk with secure permissions.
|
|
119
|
+
*/
|
|
120
|
+
async function saveRegistry(registry) {
|
|
121
|
+
const content = JSON.stringify(registry, null, 2);
|
|
122
|
+
await fs.writeFile(PATHS.ACCOUNTS_REGISTRY_PATH, content, { mode: 384 });
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Add an account to the registry.
|
|
126
|
+
* The account is appended to the end of the list (lowest priority).
|
|
127
|
+
*/
|
|
128
|
+
async function addAccountToRegistry(meta) {
|
|
129
|
+
if (!validateAccountId(meta.id)) throw new Error(`Invalid account ID: ${meta.id}`);
|
|
130
|
+
const registry = await loadRegistry();
|
|
131
|
+
if (registry.accounts.some((a) => a.id === meta.id)) throw new Error(`Account already exists: ${meta.id}`);
|
|
132
|
+
registry.accounts.push(meta);
|
|
133
|
+
await saveRegistry(registry);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Remove an account from the registry by ID or index (1-based).
|
|
137
|
+
* Returns the removed account metadata.
|
|
138
|
+
*/
|
|
139
|
+
async function removeAccountFromRegistry(idOrIndex) {
|
|
140
|
+
const registry = await loadRegistry();
|
|
141
|
+
let index;
|
|
142
|
+
if (typeof idOrIndex === "number") {
|
|
143
|
+
index = idOrIndex - 1;
|
|
144
|
+
if (index < 0 || index >= registry.accounts.length) throw new Error(`Invalid account index: ${idOrIndex}`);
|
|
145
|
+
} else {
|
|
146
|
+
index = registry.accounts.findIndex((a) => a.id === idOrIndex);
|
|
147
|
+
if (index === -1) throw new Error(`Account not found: ${idOrIndex}`);
|
|
148
|
+
}
|
|
149
|
+
const [removed] = registry.accounts.splice(index, 1);
|
|
150
|
+
await saveRegistry(registry);
|
|
151
|
+
return removed;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* List all accounts from the registry.
|
|
155
|
+
*/
|
|
156
|
+
async function listAccountsFromRegistry() {
|
|
157
|
+
return (await loadRegistry()).accounts;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Load the GitHub token for a specific account.
|
|
161
|
+
* Returns null if the token file doesn't exist.
|
|
162
|
+
*/
|
|
163
|
+
async function loadAccountToken(id) {
|
|
164
|
+
if (!validateAccountId(id)) throw new Error(`Invalid account ID: ${id}`);
|
|
165
|
+
try {
|
|
166
|
+
const tokenPath = accountTokenPath(id);
|
|
167
|
+
return (await fs.readFile(tokenPath, "utf8")).trim() || null;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.code === "ENOENT") return null;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Save the GitHub token for a specific account with secure permissions.
|
|
175
|
+
*/
|
|
176
|
+
async function saveAccountToken(id, token) {
|
|
177
|
+
if (!validateAccountId(id)) throw new Error(`Invalid account ID: ${id}`);
|
|
178
|
+
const tokenPath = accountTokenPath(id);
|
|
179
|
+
await fs.writeFile(tokenPath, token, { mode: 384 });
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Remove the GitHub token file for a specific account.
|
|
183
|
+
*/
|
|
184
|
+
async function removeAccountToken(id) {
|
|
185
|
+
if (!validateAccountId(id)) throw new Error(`Invalid account ID: ${id}`);
|
|
186
|
+
const tokenPath = accountTokenPath(id);
|
|
187
|
+
try {
|
|
188
|
+
await fs.unlink(tokenPath);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error.code !== "ENOENT") throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check if the legacy github_token file exists.
|
|
195
|
+
*/
|
|
196
|
+
async function hasLegacyToken() {
|
|
197
|
+
try {
|
|
198
|
+
return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim().length > 0;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Read the legacy github_token file.
|
|
205
|
+
* Returns null if the file doesn't exist or is empty.
|
|
206
|
+
*/
|
|
207
|
+
async function readLegacyToken() {
|
|
208
|
+
try {
|
|
209
|
+
return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim() || null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if the registry file exists and has accounts.
|
|
216
|
+
*/
|
|
217
|
+
async function hasRegistry() {
|
|
218
|
+
return (await loadRegistry()).accounts.length > 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/lib/state.ts
|
|
223
|
+
const state = {
|
|
224
|
+
accountType: "individual",
|
|
225
|
+
manualApprove: false,
|
|
226
|
+
rateLimitWait: false,
|
|
227
|
+
showToken: false,
|
|
228
|
+
verbose: false
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Create an AccountContext from the current global state.
|
|
232
|
+
* This is a compatibility layer for transitioning to multi-account support.
|
|
233
|
+
* @throws Error if githubToken is not set in state
|
|
234
|
+
*/
|
|
235
|
+
function accountFromState() {
|
|
236
|
+
if (!state.githubToken) throw new Error("GitHub token not set in state");
|
|
237
|
+
return {
|
|
238
|
+
githubToken: state.githubToken,
|
|
239
|
+
copilotToken: state.copilotToken,
|
|
240
|
+
accountType: state.accountType,
|
|
241
|
+
vsCodeVersion: state.vsCodeVersion
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/lib/api-config.ts
|
|
247
|
+
const standardHeaders = () => ({
|
|
248
|
+
"content-type": "application/json",
|
|
249
|
+
accept: "application/json"
|
|
250
|
+
});
|
|
251
|
+
const COPILOT_VERSION = "0.35.0";
|
|
252
|
+
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
253
|
+
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
254
|
+
const API_VERSION = "2025-10-01";
|
|
255
|
+
const copilotBaseUrl = (account) => account.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${account.accountType}.githubcopilot.com`;
|
|
256
|
+
const copilotHeaders = (account, vision = false, requestId) => {
|
|
257
|
+
const resolvedRequestId = requestId ?? randomUUID();
|
|
258
|
+
const headers = {
|
|
259
|
+
Authorization: `Bearer ${account.copilotToken}`,
|
|
260
|
+
"content-type": standardHeaders()["content-type"],
|
|
261
|
+
"copilot-integration-id": "vscode-chat",
|
|
262
|
+
"editor-version": `vscode/${account.vsCodeVersion}`,
|
|
263
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
264
|
+
"user-agent": USER_AGENT,
|
|
265
|
+
"openai-intent": "conversation-agent",
|
|
266
|
+
"x-github-api-version": API_VERSION,
|
|
267
|
+
"x-request-id": resolvedRequestId,
|
|
268
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
269
|
+
};
|
|
270
|
+
if (vision) headers["copilot-vision-request"] = "true";
|
|
271
|
+
return headers;
|
|
272
|
+
};
|
|
273
|
+
const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
274
|
+
const githubHeaders = (account) => ({
|
|
275
|
+
...standardHeaders(),
|
|
276
|
+
authorization: `token ${account.githubToken}`,
|
|
277
|
+
"editor-version": `vscode/${account.vsCodeVersion}`,
|
|
278
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
279
|
+
"user-agent": USER_AGENT,
|
|
280
|
+
"x-github-api-version": API_VERSION,
|
|
281
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
282
|
+
});
|
|
283
|
+
const GITHUB_BASE_URL = "https://github.com";
|
|
284
|
+
const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
285
|
+
const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/lib/error.ts
|
|
289
|
+
var HTTPError = class extends Error {
|
|
290
|
+
response;
|
|
291
|
+
constructor(message, response) {
|
|
292
|
+
super(message);
|
|
293
|
+
this.response = response;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
async function forwardError(c, error) {
|
|
297
|
+
consola.error("Error occurred:", error);
|
|
298
|
+
if (error instanceof HTTPError) {
|
|
299
|
+
const errorText = await error.response.text();
|
|
300
|
+
let errorJson;
|
|
301
|
+
try {
|
|
302
|
+
errorJson = JSON.parse(errorText);
|
|
303
|
+
} catch {
|
|
304
|
+
errorJson = errorText;
|
|
305
|
+
}
|
|
306
|
+
consola.error("HTTP error:", errorJson);
|
|
307
|
+
return c.json({ error: {
|
|
308
|
+
message: errorText,
|
|
309
|
+
type: "error"
|
|
310
|
+
} }, error.response.status);
|
|
311
|
+
}
|
|
312
|
+
return c.json({ error: {
|
|
313
|
+
message: error.message,
|
|
314
|
+
type: "error"
|
|
315
|
+
} }, 500);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/services/github/get-copilot-usage.ts
|
|
320
|
+
const getCopilotUsage = async (account) => {
|
|
321
|
+
const ctx = account ?? accountFromState();
|
|
322
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(ctx) });
|
|
323
|
+
if (!response.ok) throw new HTTPError("Failed to get Copilot usage", response);
|
|
324
|
+
return await response.json();
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/services/github/get-user.ts
|
|
329
|
+
async function getGitHubUser(account) {
|
|
330
|
+
const token = account?.githubToken ?? state.githubToken;
|
|
331
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: {
|
|
332
|
+
authorization: `token ${token}`,
|
|
333
|
+
...standardHeaders()
|
|
334
|
+
} });
|
|
335
|
+
if (!response.ok) throw new HTTPError("Failed to get GitHub user", response);
|
|
336
|
+
return await response.json();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/services/copilot/get-models.ts
|
|
341
|
+
const getModels = async (account) => {
|
|
342
|
+
const ctx = account ?? accountFromState();
|
|
343
|
+
const response = await fetch(`${copilotBaseUrl(ctx)}/models`, { headers: copilotHeaders(ctx) });
|
|
344
|
+
if (!response.ok) throw new HTTPError("Failed to get models", response);
|
|
345
|
+
const models = await response.json();
|
|
346
|
+
try {
|
|
347
|
+
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
348
|
+
await fs.writeFile(PATHS.MODELS_PATH, `${JSON.stringify(models, null, 2)}\n`, {
|
|
349
|
+
encoding: "utf8",
|
|
350
|
+
mode: 384
|
|
351
|
+
});
|
|
352
|
+
} catch {}
|
|
353
|
+
return models;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/services/get-vscode-version.ts
|
|
358
|
+
const FALLBACK = "1.107.0";
|
|
359
|
+
async function getVSCodeVersion() {
|
|
360
|
+
const controller = new AbortController();
|
|
361
|
+
const timeout = setTimeout(() => {
|
|
362
|
+
controller.abort();
|
|
363
|
+
}, 5e3);
|
|
364
|
+
try {
|
|
365
|
+
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.]+)/);
|
|
366
|
+
if (match) return match[1];
|
|
367
|
+
return FALLBACK;
|
|
368
|
+
} catch {
|
|
369
|
+
return FALLBACK;
|
|
370
|
+
} finally {
|
|
371
|
+
clearTimeout(timeout);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await getVSCodeVersion();
|
|
375
|
+
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/lib/utils.ts
|
|
378
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
379
|
+
setTimeout(resolve, ms);
|
|
380
|
+
});
|
|
381
|
+
const isNullish = (value) => value === null || value === void 0;
|
|
382
|
+
const cacheVSCodeVersion = async () => {
|
|
383
|
+
const response = await getVSCodeVersion();
|
|
384
|
+
state.vsCodeVersion = response;
|
|
385
|
+
consola.info(`Using VSCode version: ${response}`);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/services/github/get-copilot-token.ts
|
|
390
|
+
const getCopilotToken = async (account) => {
|
|
391
|
+
const ctx = account ?? accountFromState();
|
|
392
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(ctx) });
|
|
393
|
+
if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
|
|
394
|
+
return await response.json();
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/lib/config.ts
|
|
399
|
+
const gpt5ExplorationPrompt = `## Exploration and reading files
|
|
400
|
+
- **Think first.** Before any tool call, decide ALL files/resources you will need.
|
|
401
|
+
- **Batch everything.** If you need multiple files (even from different places), read them together.
|
|
402
|
+
- **multi_tool_use.parallel** Use multi_tool_use.parallel to parallelize tool calls and only this.
|
|
403
|
+
- **Only make sequential calls if you truly cannot know the next file without seeing a result first.**
|
|
404
|
+
- **Workflow:** (a) plan all needed reads → (b) issue one parallel batch → (c) analyze results → (d) repeat if new, unpredictable reads arise.`;
|
|
405
|
+
const defaultConfig = {
|
|
406
|
+
extraPrompts: {
|
|
407
|
+
"gpt-5-mini": gpt5ExplorationPrompt,
|
|
408
|
+
"gpt-5.1-codex-max": gpt5ExplorationPrompt
|
|
409
|
+
},
|
|
410
|
+
smallModel: "gpt-5-mini",
|
|
411
|
+
freeModelLoadBalancing: true,
|
|
412
|
+
modelReasoningEfforts: { "gpt-5-mini": "low" },
|
|
413
|
+
allowOriginalModelNamesForAliases: false,
|
|
414
|
+
useFunctionApplyPatch: true,
|
|
415
|
+
compactUseSmallModel: true
|
|
416
|
+
};
|
|
417
|
+
let cachedConfig = null;
|
|
418
|
+
function ensureConfigFile() {
|
|
419
|
+
try {
|
|
420
|
+
fs$1.accessSync(PATHS.CONFIG_PATH, fs$1.constants.R_OK);
|
|
421
|
+
return;
|
|
422
|
+
} catch {}
|
|
423
|
+
try {
|
|
424
|
+
fs$1.mkdirSync(PATHS.APP_DIR, { recursive: true });
|
|
425
|
+
fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf8");
|
|
426
|
+
try {
|
|
427
|
+
fs$1.chmodSync(PATHS.CONFIG_PATH, 384);
|
|
428
|
+
} catch {}
|
|
429
|
+
} catch {}
|
|
430
|
+
}
|
|
431
|
+
function readConfigFromDisk() {
|
|
432
|
+
ensureConfigFile();
|
|
433
|
+
try {
|
|
434
|
+
const raw = fs$1.readFileSync(PATHS.CONFIG_PATH, "utf8");
|
|
435
|
+
if (!raw.trim()) {
|
|
436
|
+
fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf8");
|
|
437
|
+
return defaultConfig;
|
|
438
|
+
}
|
|
439
|
+
return JSON.parse(raw);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
consola.error("Failed to read config file, using default config", error);
|
|
442
|
+
return defaultConfig;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function mergeDefaultExtraPrompts(config) {
|
|
446
|
+
const extraPrompts = config.extraPrompts ?? {};
|
|
447
|
+
const defaultExtraPrompts = defaultConfig.extraPrompts ?? {};
|
|
448
|
+
if (Object.keys(defaultExtraPrompts).filter((model) => !Object.hasOwn(extraPrompts, model)).length === 0) return {
|
|
449
|
+
mergedConfig: config,
|
|
450
|
+
changed: false
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
mergedConfig: {
|
|
454
|
+
...config,
|
|
455
|
+
extraPrompts: {
|
|
456
|
+
...defaultExtraPrompts,
|
|
457
|
+
...extraPrompts
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
changed: true
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function mergeDefaultFreeModelLoadBalancing(config) {
|
|
464
|
+
if (typeof config.freeModelLoadBalancing === "boolean") return {
|
|
465
|
+
mergedConfig: config,
|
|
466
|
+
changed: false
|
|
467
|
+
};
|
|
468
|
+
return {
|
|
469
|
+
mergedConfig: {
|
|
470
|
+
...config,
|
|
471
|
+
freeModelLoadBalancing: defaultConfig.freeModelLoadBalancing ?? true
|
|
472
|
+
},
|
|
473
|
+
changed: true
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function applyConfigMerges(config, mergeFns) {
|
|
477
|
+
return mergeFns.reduce((acc, mergeFn) => {
|
|
478
|
+
const result = mergeFn(acc.mergedConfig);
|
|
479
|
+
return {
|
|
480
|
+
mergedConfig: result.mergedConfig,
|
|
481
|
+
changed: acc.changed || result.changed
|
|
482
|
+
};
|
|
483
|
+
}, {
|
|
484
|
+
mergedConfig: config,
|
|
485
|
+
changed: false
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
function mergeConfigWithDefaults() {
|
|
489
|
+
const config = readConfigFromDisk();
|
|
490
|
+
const { mergedConfig, changed } = applyConfigMerges(config, [mergeDefaultExtraPrompts, mergeDefaultFreeModelLoadBalancing]);
|
|
491
|
+
if (changed) try {
|
|
492
|
+
fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(mergedConfig, null, 2)}\n`, "utf8");
|
|
493
|
+
} catch (writeError) {
|
|
494
|
+
consola.warn("Failed to write merged config defaults", writeError);
|
|
495
|
+
}
|
|
496
|
+
cachedConfig = mergedConfig;
|
|
497
|
+
return mergedConfig;
|
|
498
|
+
}
|
|
499
|
+
function getConfig() {
|
|
500
|
+
cachedConfig ??= readConfigFromDisk();
|
|
501
|
+
return cachedConfig;
|
|
502
|
+
}
|
|
503
|
+
function normalizeAliasKey(value) {
|
|
504
|
+
const trimmed = value.trim().toLowerCase();
|
|
505
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
506
|
+
}
|
|
507
|
+
function normalizeAliasTarget(value) {
|
|
508
|
+
const trimmed = value.trim();
|
|
509
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
510
|
+
}
|
|
511
|
+
function normalizeAliasSpec(value) {
|
|
512
|
+
if (typeof value === "string") {
|
|
513
|
+
const normalizedTarget$1 = normalizeAliasTarget(value);
|
|
514
|
+
return normalizedTarget$1 ? { target: normalizedTarget$1 } : null;
|
|
515
|
+
}
|
|
516
|
+
if (!value || typeof value !== "object") return null;
|
|
517
|
+
const targetValue = value.target;
|
|
518
|
+
if (typeof targetValue !== "string") return null;
|
|
519
|
+
const normalizedTarget = normalizeAliasTarget(targetValue);
|
|
520
|
+
if (!normalizedTarget) return null;
|
|
521
|
+
const allowOriginalValue = value.allowOriginal;
|
|
522
|
+
return {
|
|
523
|
+
target: normalizedTarget,
|
|
524
|
+
allowOriginal: typeof allowOriginalValue === "boolean" ? allowOriginalValue : void 0
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function getModelAliasesInfo() {
|
|
528
|
+
const raw = getConfig().modelAliases ?? {};
|
|
529
|
+
const normalized = {};
|
|
530
|
+
for (const [alias, rawSpec] of Object.entries(raw)) {
|
|
531
|
+
const normalizedAlias = normalizeAliasKey(alias);
|
|
532
|
+
const normalizedSpec = normalizeAliasSpec(rawSpec);
|
|
533
|
+
if (!normalizedAlias || !normalizedSpec) continue;
|
|
534
|
+
if (!Object.hasOwn(normalized, normalizedAlias)) normalized[normalizedAlias] = normalizedSpec;
|
|
535
|
+
}
|
|
536
|
+
return normalized;
|
|
537
|
+
}
|
|
538
|
+
function getModelAliases() {
|
|
539
|
+
const info = getModelAliasesInfo();
|
|
540
|
+
const normalized = {};
|
|
541
|
+
for (const [alias, spec] of Object.entries(info)) normalized[alias] = spec.target;
|
|
542
|
+
return normalized;
|
|
543
|
+
}
|
|
544
|
+
function resolveModelAlias(modelId) {
|
|
545
|
+
const normalized = normalizeAliasKey(modelId);
|
|
546
|
+
if (!normalized) return modelId;
|
|
547
|
+
return getModelAliases()[normalized] ?? modelId;
|
|
548
|
+
}
|
|
549
|
+
function isOriginalModelNameAllowedForAliases() {
|
|
550
|
+
return getConfig().allowOriginalModelNamesForAliases ?? false;
|
|
551
|
+
}
|
|
552
|
+
function getAliasTargetSet() {
|
|
553
|
+
const aliases = getModelAliasesInfo();
|
|
554
|
+
const allowOriginalDefault = isOriginalModelNameAllowedForAliases();
|
|
555
|
+
const targetAllowMap = /* @__PURE__ */ new Map();
|
|
556
|
+
for (const { target, allowOriginal } of Object.values(aliases)) {
|
|
557
|
+
const normalizedTarget = target.toLowerCase();
|
|
558
|
+
const effectiveAllow = allowOriginal ?? allowOriginalDefault;
|
|
559
|
+
const currentAllow = targetAllowMap.get(normalizedTarget);
|
|
560
|
+
if (currentAllow === true) continue;
|
|
561
|
+
if (effectiveAllow) targetAllowMap.set(normalizedTarget, true);
|
|
562
|
+
else if (currentAllow === void 0) targetAllowMap.set(normalizedTarget, false);
|
|
563
|
+
}
|
|
564
|
+
const blockedTargets = /* @__PURE__ */ new Set();
|
|
565
|
+
for (const [target, allowed] of targetAllowMap.entries()) if (!allowed) blockedTargets.add(target);
|
|
566
|
+
return blockedTargets;
|
|
567
|
+
}
|
|
568
|
+
function isOriginalModelNameAllowedForTarget(modelId) {
|
|
569
|
+
const normalized = normalizeAliasKey(modelId);
|
|
570
|
+
if (!normalized) return true;
|
|
571
|
+
return !getAliasTargetSet().has(normalized);
|
|
572
|
+
}
|
|
573
|
+
function getPreferredAliasForTarget(modelId) {
|
|
574
|
+
const aliases = getModelAliases();
|
|
575
|
+
return getAliasKeysForTarget(modelId, aliases)[0] ?? null;
|
|
576
|
+
}
|
|
577
|
+
function getAliasKeysForTarget(target, aliases) {
|
|
578
|
+
const normalizedTarget = target.toLowerCase();
|
|
579
|
+
return Object.entries(aliases).filter(([, model]) => model.toLowerCase() === normalizedTarget).map(([alias]) => alias).sort();
|
|
580
|
+
}
|
|
581
|
+
function getAliasFallbackValue(record, modelId, aliases) {
|
|
582
|
+
if (!record) return void 0;
|
|
583
|
+
const aliasKeys = getAliasKeysForTarget(modelId, aliases);
|
|
584
|
+
if (aliasKeys.length === 0) return void 0;
|
|
585
|
+
const recordByAlias = /* @__PURE__ */ new Map();
|
|
586
|
+
for (const [key, value] of Object.entries(record)) {
|
|
587
|
+
const normalized = normalizeAliasKey(key);
|
|
588
|
+
if (normalized) recordByAlias.set(normalized, value);
|
|
589
|
+
}
|
|
590
|
+
for (const alias of aliasKeys) {
|
|
591
|
+
const value = recordByAlias.get(alias);
|
|
592
|
+
if (value !== void 0) return value;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function getExtraPromptForModel(model) {
|
|
596
|
+
const config = getConfig();
|
|
597
|
+
const direct = config.extraPrompts?.[model];
|
|
598
|
+
if (direct !== void 0) return direct;
|
|
599
|
+
const aliases = getModelAliases();
|
|
600
|
+
return getAliasFallbackValue(config.extraPrompts, model, aliases) ?? "";
|
|
601
|
+
}
|
|
602
|
+
function getSmallModel() {
|
|
603
|
+
const model = getConfig().smallModel ?? "gpt-5-mini";
|
|
604
|
+
if (isOriginalModelNameAllowedForTarget(model)) return model;
|
|
605
|
+
return getPreferredAliasForTarget(model) ?? model;
|
|
606
|
+
}
|
|
607
|
+
function isFreeModelLoadBalancingEnabled() {
|
|
608
|
+
return getConfig().freeModelLoadBalancing ?? true;
|
|
609
|
+
}
|
|
610
|
+
function getReasoningEffortForModel(model) {
|
|
611
|
+
const config = getConfig();
|
|
612
|
+
const direct = config.modelReasoningEfforts?.[model];
|
|
613
|
+
if (direct !== void 0) return direct;
|
|
614
|
+
const aliases = getModelAliases();
|
|
615
|
+
return getAliasFallbackValue(config.modelReasoningEfforts, model, aliases) ?? "high";
|
|
616
|
+
}
|
|
617
|
+
function isForceAgentEnabled() {
|
|
618
|
+
return getConfig().forceAgent ?? false;
|
|
619
|
+
}
|
|
620
|
+
function shouldCompactUseSmallModel() {
|
|
621
|
+
return getConfig().compactUseSmallModel ?? true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/lib/accounts-manager-auth.ts
|
|
626
|
+
const takeAuthSnapshot = (account) => ({
|
|
627
|
+
githubToken: account.githubToken,
|
|
628
|
+
accountType: account.accountType
|
|
629
|
+
});
|
|
630
|
+
const isAuthSnapshotCurrent = (account, snapshot) => account.githubToken === snapshot.githubToken && account.accountType === snapshot.accountType;
|
|
631
|
+
const isSameAuthSnapshot = (a, b) => {
|
|
632
|
+
if (!a) return false;
|
|
633
|
+
return a.githubToken === b.githubToken && a.accountType === b.accountType;
|
|
634
|
+
};
|
|
635
|
+
const toAccountContextFromSnapshot = (account, snapshot, copilotToken) => ({
|
|
636
|
+
githubToken: snapshot.githubToken,
|
|
637
|
+
copilotToken,
|
|
638
|
+
accountType: snapshot.accountType,
|
|
639
|
+
vsCodeVersion: account.vsCodeVersion
|
|
640
|
+
});
|
|
641
|
+
const applyCopilotTokenIfCurrent = (account, snapshot, copilotToken) => {
|
|
642
|
+
if (!isAuthSnapshotCurrent(account, snapshot)) return false;
|
|
643
|
+
account.copilotToken = copilotToken;
|
|
644
|
+
return true;
|
|
645
|
+
};
|
|
646
|
+
const applyModelsIfCurrent = (account, snapshot, models) => {
|
|
647
|
+
if (!isAuthSnapshotCurrent(account, snapshot)) return false;
|
|
648
|
+
account.models = models;
|
|
649
|
+
return true;
|
|
650
|
+
};
|
|
651
|
+
const applyTokenRefreshSuccessIfCurrent = (account, snapshot, token) => {
|
|
652
|
+
if (!isAuthSnapshotCurrent(account, snapshot)) return false;
|
|
653
|
+
account.copilotToken = token;
|
|
654
|
+
account.failed = false;
|
|
655
|
+
account.failureReason = void 0;
|
|
656
|
+
return true;
|
|
657
|
+
};
|
|
658
|
+
const applyTokenRefreshFailureIfCurrent = (account, snapshot, error) => {
|
|
659
|
+
if (!isAuthSnapshotCurrent(account, snapshot)) return false;
|
|
660
|
+
account.failed = true;
|
|
661
|
+
account.failureReason = String(error);
|
|
662
|
+
return true;
|
|
663
|
+
};
|
|
664
|
+
const applyQuotaRefreshSuccessIfCurrent = (account, snapshot, premium) => {
|
|
665
|
+
if (!isAuthSnapshotCurrent(account, snapshot)) return false;
|
|
666
|
+
account.premiumEntitlement = premium.entitlement;
|
|
667
|
+
account.premiumRemaining = premium.remaining;
|
|
668
|
+
account.unlimited = premium.unlimited;
|
|
669
|
+
account.lastQuotaFetch = Date.now();
|
|
670
|
+
account.failed = false;
|
|
671
|
+
account.failureReason = void 0;
|
|
672
|
+
return true;
|
|
673
|
+
};
|
|
674
|
+
const setAccountFailedState = (account, reason) => {
|
|
675
|
+
account.failed = true;
|
|
676
|
+
account.failureReason = reason;
|
|
677
|
+
consola.warn(`Account ${account.id} marked as failed: ${reason}`);
|
|
678
|
+
};
|
|
679
|
+
const applyUnauthorizedIfCurrent = (account, snapshot, reason) => {
|
|
680
|
+
if (!isAuthSnapshotCurrent(account, snapshot)) return false;
|
|
681
|
+
setAccountFailedState(account, reason);
|
|
682
|
+
return true;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/lib/accounts-manager-quota.ts
|
|
687
|
+
const getCostUnits = (model) => {
|
|
688
|
+
const billing = model.billing;
|
|
689
|
+
if (!billing) return 0;
|
|
690
|
+
if (billing.is_premium !== true) return 0;
|
|
691
|
+
const multiplier = billing.multiplier;
|
|
692
|
+
if (typeof multiplier !== "number" || !Number.isFinite(multiplier) || multiplier <= 0) return 1;
|
|
693
|
+
return multiplier;
|
|
694
|
+
};
|
|
695
|
+
const getEffectivePremiumRemaining = (account) => {
|
|
696
|
+
if (account.premiumRemaining === void 0) return;
|
|
697
|
+
const reserved = account.premiumReserved ?? 0;
|
|
698
|
+
return account.premiumRemaining - reserved;
|
|
699
|
+
};
|
|
700
|
+
const reservePremiumUnits = (account, units) => {
|
|
701
|
+
if (units <= 0) return;
|
|
702
|
+
const id = Symbol("quotaReservation");
|
|
703
|
+
if (!account.premiumReservations) account.premiumReservations = /* @__PURE__ */ new Map();
|
|
704
|
+
account.premiumReservations.set(id, units);
|
|
705
|
+
account.premiumReserved = (account.premiumReserved ?? 0) + units;
|
|
706
|
+
return { id };
|
|
707
|
+
};
|
|
708
|
+
const releasePremiumReservation = (account, reservation) => {
|
|
709
|
+
if (!reservation) return;
|
|
710
|
+
const reservations = account.premiumReservations;
|
|
711
|
+
if (!reservations) return;
|
|
712
|
+
const reservedUnits = reservations.get(reservation.id);
|
|
713
|
+
if (reservedUnits === void 0) return;
|
|
714
|
+
reservations.delete(reservation.id);
|
|
715
|
+
const nextReserved = (account.premiumReserved ?? 0) - reservedUnits;
|
|
716
|
+
account.premiumReserved = Math.max(0, nextReserved);
|
|
717
|
+
if (reservations.size === 0) account.premiumReservations = void 0;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/lib/accounts-manager.ts
|
|
722
|
+
/** Quota cache TTL in milliseconds (45 seconds) for pre-request selection. */
|
|
723
|
+
const QUOTA_CACHE_TTL = 45 * 1e3;
|
|
724
|
+
/** Debounce delay for registry reload in milliseconds */
|
|
725
|
+
const RELOAD_DEBOUNCE_MS = 500;
|
|
726
|
+
/** Registry watcher restart initial delay in milliseconds */
|
|
727
|
+
const WATCHER_RESTART_INITIAL_DELAY_MS = 1e3;
|
|
728
|
+
/** Registry watcher restart max delay in milliseconds */
|
|
729
|
+
const WATCHER_RESTART_MAX_DELAY_MS = 60 * 1e3;
|
|
730
|
+
/** Manages multiple GitHub Copilot accounts at runtime. */
|
|
731
|
+
var AccountsManager = class {
|
|
732
|
+
accounts = /* @__PURE__ */ new Map();
|
|
733
|
+
accountOrder = [];
|
|
734
|
+
temporaryAccount;
|
|
735
|
+
vsCodeVersion;
|
|
736
|
+
freeModelCursor = 0;
|
|
737
|
+
freeModelLoadBalancingEnabled = true;
|
|
738
|
+
quotaRefreshSnapshotByAccount = /* @__PURE__ */ new WeakMap();
|
|
739
|
+
tokenRefreshEnabledAccounts = /* @__PURE__ */ new WeakSet();
|
|
740
|
+
registryWatcher;
|
|
741
|
+
reloadDebounceTimer;
|
|
742
|
+
registryWatcherRestartTimer;
|
|
743
|
+
registryWatcherRestartDelayMs = WATCHER_RESTART_INITIAL_DELAY_MS;
|
|
744
|
+
isReloading = false;
|
|
745
|
+
/** Initialize accounts manager (load registry, migrate legacy token). */
|
|
746
|
+
async initialize(vsCodeVersion) {
|
|
747
|
+
this.vsCodeVersion = vsCodeVersion;
|
|
748
|
+
const hasReg = await hasRegistry();
|
|
749
|
+
const hasLegacy = await hasLegacyToken();
|
|
750
|
+
if (!hasReg && hasLegacy) await this.migrateLegacyToken();
|
|
751
|
+
const accountMetas = await listAccountsFromRegistry();
|
|
752
|
+
for (const meta of accountMetas) {
|
|
753
|
+
const token = await loadAccountToken(meta.id);
|
|
754
|
+
if (!token) {
|
|
755
|
+
consola.warn(`No token found for account ${meta.id}, skipping`);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const runtime = {
|
|
759
|
+
...meta,
|
|
760
|
+
githubToken: token,
|
|
761
|
+
vsCodeVersion: this.vsCodeVersion
|
|
762
|
+
};
|
|
763
|
+
this.accounts.set(meta.id, runtime);
|
|
764
|
+
this.accountOrder.push(meta.id);
|
|
765
|
+
}
|
|
766
|
+
for (const account of this.accounts.values()) try {
|
|
767
|
+
await this.initializeAccount(account);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
consola.error(`Failed to initialize account ${account.id}:`, error);
|
|
770
|
+
account.failed = true;
|
|
771
|
+
account.failureReason = String(error);
|
|
772
|
+
}
|
|
773
|
+
consola.info(`Loaded ${this.accounts.size} account(s)`);
|
|
774
|
+
this.startRegistryWatcher();
|
|
775
|
+
}
|
|
776
|
+
setFreeModelLoadBalancingEnabled(enabled) {
|
|
777
|
+
this.freeModelLoadBalancingEnabled = enabled;
|
|
778
|
+
}
|
|
779
|
+
computeTokenRefreshDelayMs(refreshInSeconds) {
|
|
780
|
+
return Math.max((refreshInSeconds - 60) * 1e3, 1e3);
|
|
781
|
+
}
|
|
782
|
+
shouldContinueTokenRefresh(account, snapshot) {
|
|
783
|
+
return this.tokenRefreshEnabledAccounts.has(account) && isAuthSnapshotCurrent(account, snapshot);
|
|
784
|
+
}
|
|
785
|
+
async runTokenRefreshTick(account, snapshot, refreshInSeconds) {
|
|
786
|
+
if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
|
|
787
|
+
try {
|
|
788
|
+
const ctx = toAccountContextFromSnapshot(account, snapshot);
|
|
789
|
+
const { token, refresh_in } = await getCopilotToken(ctx);
|
|
790
|
+
if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
|
|
791
|
+
if (!applyTokenRefreshSuccessIfCurrent(account, snapshot, token)) return;
|
|
792
|
+
consola.debug(`Refreshed token for account ${account.id}`);
|
|
793
|
+
if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
|
|
794
|
+
this.startTokenRefresh(account, refresh_in);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
consola.error(`Failed to refresh token for ${account.id}:`, error);
|
|
797
|
+
if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
|
|
798
|
+
applyTokenRefreshFailureIfCurrent(account, snapshot, error);
|
|
799
|
+
if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
|
|
800
|
+
this.startTokenRefresh(account, refreshInSeconds);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
finalizeQuotaRefreshPromise(account, promise) {
|
|
804
|
+
if (account.quotaRefreshPromise !== promise) return;
|
|
805
|
+
account.isRefreshingQuota = false;
|
|
806
|
+
account.quotaRefreshPromise = void 0;
|
|
807
|
+
this.quotaRefreshSnapshotByAccount.delete(account);
|
|
808
|
+
}
|
|
809
|
+
/** Initialize a single account. */
|
|
810
|
+
async initializeAccount(account) {
|
|
811
|
+
const snapshot = takeAuthSnapshot(account);
|
|
812
|
+
try {
|
|
813
|
+
const tokenCtx = toAccountContextFromSnapshot(account, snapshot);
|
|
814
|
+
const { token, refresh_in } = await getCopilotToken(tokenCtx);
|
|
815
|
+
if (!applyCopilotTokenIfCurrent(account, snapshot, token)) return;
|
|
816
|
+
this.startTokenRefresh(account, refresh_in);
|
|
817
|
+
const modelsCtx = toAccountContextFromSnapshot(account, snapshot, token);
|
|
818
|
+
const models = await getModels(modelsCtx);
|
|
819
|
+
if (!applyModelsIfCurrent(account, snapshot, models)) return;
|
|
820
|
+
await this.refreshQuota(account);
|
|
821
|
+
consola.debug(`Account ${account.id} initialized`);
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (!isAuthSnapshotCurrent(account, snapshot)) return;
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/** Migrate legacy github_token to the new multi-account system. */
|
|
828
|
+
async migrateLegacyToken() {
|
|
829
|
+
const token = await readLegacyToken();
|
|
830
|
+
if (!token) return;
|
|
831
|
+
try {
|
|
832
|
+
const id = (await getGitHubUser({
|
|
833
|
+
githubToken: token,
|
|
834
|
+
accountType: "individual"
|
|
835
|
+
})).login;
|
|
836
|
+
await saveAccountToken(id, token);
|
|
837
|
+
await addAccountToRegistry({
|
|
838
|
+
id,
|
|
839
|
+
accountType: "individual",
|
|
840
|
+
addedAt: Date.now()
|
|
841
|
+
});
|
|
842
|
+
consola.info(`Migrated legacy token to account: ${id}`);
|
|
843
|
+
} catch (error) {
|
|
844
|
+
consola.error("Failed to migrate legacy token:", error);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/** Start token refresh timer for an account. */
|
|
848
|
+
startTokenRefresh(account, refreshInSeconds) {
|
|
849
|
+
this.stopTokenRefresh(account);
|
|
850
|
+
this.tokenRefreshEnabledAccounts.add(account);
|
|
851
|
+
const snapshot = takeAuthSnapshot(account);
|
|
852
|
+
const delayMs = this.computeTokenRefreshDelayMs(refreshInSeconds);
|
|
853
|
+
account.refreshTimer = setTimeout(() => {
|
|
854
|
+
this.runTokenRefreshTick(account, snapshot, refreshInSeconds);
|
|
855
|
+
}, delayMs);
|
|
856
|
+
}
|
|
857
|
+
/** Stop token refresh timer for an account. */
|
|
858
|
+
stopTokenRefresh(account) {
|
|
859
|
+
this.tokenRefreshEnabledAccounts.delete(account);
|
|
860
|
+
if (account.refreshTimer) {
|
|
861
|
+
clearTimeout(account.refreshTimer);
|
|
862
|
+
account.refreshTimer = void 0;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
/** Stop all token refresh timers. */
|
|
866
|
+
stopAllTokenRefresh() {
|
|
867
|
+
for (const account of this.accounts.values()) this.stopTokenRefresh(account);
|
|
868
|
+
if (this.temporaryAccount) this.stopTokenRefresh(this.temporaryAccount);
|
|
869
|
+
}
|
|
870
|
+
/** Refresh quota information for an account. */
|
|
871
|
+
async refreshQuota(account) {
|
|
872
|
+
const snapshot = takeAuthSnapshot(account);
|
|
873
|
+
if (account.quotaRefreshPromise) {
|
|
874
|
+
const existingSnapshot = this.quotaRefreshSnapshotByAccount.get(account);
|
|
875
|
+
if (isSameAuthSnapshot(existingSnapshot, snapshot)) {
|
|
876
|
+
await account.quotaRefreshPromise;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
account.isRefreshingQuota = true;
|
|
881
|
+
const ctx = toAccountContextFromSnapshot(account, snapshot);
|
|
882
|
+
const promise = (async () => {
|
|
883
|
+
try {
|
|
884
|
+
const premium = (await getCopilotUsage(ctx)).quota_snapshots.premium_interactions;
|
|
885
|
+
applyQuotaRefreshSuccessIfCurrent(account, snapshot, premium);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (error instanceof HTTPError && error.response.status === 401) {
|
|
888
|
+
applyUnauthorizedIfCurrent(account, snapshot, "Unauthorized (401)");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
consola.error(`Failed to refresh quota for ${account.id}:`, error);
|
|
892
|
+
}
|
|
893
|
+
})();
|
|
894
|
+
account.quotaRefreshPromise = promise;
|
|
895
|
+
this.quotaRefreshSnapshotByAccount.set(account, snapshot);
|
|
896
|
+
promise.finally(() => {
|
|
897
|
+
this.finalizeQuotaRefreshPromise(account, promise);
|
|
898
|
+
});
|
|
899
|
+
await promise;
|
|
900
|
+
}
|
|
901
|
+
/** Check if quota cache is expired. */
|
|
902
|
+
isQuotaCacheExpired(account) {
|
|
903
|
+
if (!account.lastQuotaFetch) return true;
|
|
904
|
+
return Date.now() - account.lastQuotaFetch > QUOTA_CACHE_TTL;
|
|
905
|
+
}
|
|
906
|
+
isAccountFailed(account) {
|
|
907
|
+
return account.failed === true;
|
|
908
|
+
}
|
|
909
|
+
isModelSupportedForEndpoint(model, endpoint) {
|
|
910
|
+
if (endpoint === "/responses") return model.supported_endpoints?.includes(endpoint) ?? false;
|
|
911
|
+
const supported = model.supported_endpoints;
|
|
912
|
+
if (!supported) return true;
|
|
913
|
+
return supported.includes(endpoint);
|
|
914
|
+
}
|
|
915
|
+
pickSupportedCandidate(account, candidates) {
|
|
916
|
+
const models = account.models?.data;
|
|
917
|
+
if (!models) return null;
|
|
918
|
+
for (const candidate of candidates) {
|
|
919
|
+
const model = models.find((m) => m.id === candidate.modelId);
|
|
920
|
+
if (!model) continue;
|
|
921
|
+
if (!this.isModelSupportedForEndpoint(model, candidate.endpoint)) continue;
|
|
922
|
+
return {
|
|
923
|
+
candidate,
|
|
924
|
+
model
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
selectFreeAccountForRequest(orderedAccounts, candidates) {
|
|
930
|
+
const count = orderedAccounts.length;
|
|
931
|
+
const start = this.freeModelCursor % count;
|
|
932
|
+
let supportedCandidateFound = false;
|
|
933
|
+
for (let i = 0; i < count; i++) {
|
|
934
|
+
const idx = (start + i) % count;
|
|
935
|
+
const account = orderedAccounts[idx];
|
|
936
|
+
if (this.isAccountFailed(account)) continue;
|
|
937
|
+
const supported = this.pickSupportedCandidate(account, candidates);
|
|
938
|
+
if (!supported) continue;
|
|
939
|
+
supportedCandidateFound = true;
|
|
940
|
+
const { candidate, model } = supported;
|
|
941
|
+
const costUnits = getCostUnits(model);
|
|
942
|
+
if (costUnits > 0) continue;
|
|
943
|
+
this.freeModelCursor = (idx + 1) % count;
|
|
944
|
+
return {
|
|
945
|
+
ok: true,
|
|
946
|
+
account,
|
|
947
|
+
selectedModel: model,
|
|
948
|
+
endpoint: candidate.endpoint,
|
|
949
|
+
costUnits
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
if (!supportedCandidateFound) return {
|
|
953
|
+
ok: false,
|
|
954
|
+
reason: "MODEL_NOT_SUPPORTED"
|
|
955
|
+
};
|
|
956
|
+
return {
|
|
957
|
+
ok: false,
|
|
958
|
+
reason: "NO_QUOTA"
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
async selectAccountForCandidates(orderedAccounts, candidates) {
|
|
962
|
+
if (orderedAccounts.length === 0) return {
|
|
963
|
+
ok: false,
|
|
964
|
+
reason: "NO_ACCOUNTS"
|
|
965
|
+
};
|
|
966
|
+
let supportedCandidateFound = false;
|
|
967
|
+
for (const account of orderedAccounts) {
|
|
968
|
+
if (this.isAccountFailed(account)) continue;
|
|
969
|
+
const supported = this.pickSupportedCandidate(account, candidates);
|
|
970
|
+
if (!supported) continue;
|
|
971
|
+
supportedCandidateFound = true;
|
|
972
|
+
const { candidate, model } = supported;
|
|
973
|
+
const costUnits = getCostUnits(model);
|
|
974
|
+
if (costUnits <= 0) {
|
|
975
|
+
if (this.freeModelLoadBalancingEnabled) return this.selectFreeAccountForRequest(orderedAccounts, candidates);
|
|
976
|
+
return {
|
|
977
|
+
ok: true,
|
|
978
|
+
account,
|
|
979
|
+
selectedModel: model,
|
|
980
|
+
endpoint: candidate.endpoint,
|
|
981
|
+
costUnits
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
if (!account.unlimited && this.isQuotaCacheExpired(account)) await this.refreshQuota(account);
|
|
985
|
+
if (this.isAccountFailed(account)) continue;
|
|
986
|
+
if (account.unlimited) return {
|
|
987
|
+
ok: true,
|
|
988
|
+
account,
|
|
989
|
+
selectedModel: model,
|
|
990
|
+
endpoint: candidate.endpoint,
|
|
991
|
+
costUnits
|
|
992
|
+
};
|
|
993
|
+
const effectiveRemaining = getEffectivePremiumRemaining(account);
|
|
994
|
+
if (effectiveRemaining !== void 0 && effectiveRemaining < costUnits) continue;
|
|
995
|
+
const reservation = reservePremiumUnits(account, costUnits);
|
|
996
|
+
return {
|
|
997
|
+
ok: true,
|
|
998
|
+
account,
|
|
999
|
+
selectedModel: model,
|
|
1000
|
+
endpoint: candidate.endpoint,
|
|
1001
|
+
costUnits,
|
|
1002
|
+
reservation
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (!supportedCandidateFound) return {
|
|
1006
|
+
ok: false,
|
|
1007
|
+
reason: "MODEL_NOT_SUPPORTED"
|
|
1008
|
+
};
|
|
1009
|
+
return {
|
|
1010
|
+
ok: false,
|
|
1011
|
+
reason: "NO_QUOTA"
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Select an available account for a specific request (model + endpoint).
|
|
1016
|
+
* Uses reservation to avoid oversubscribing premium quota under concurrency.
|
|
1017
|
+
*/
|
|
1018
|
+
async selectAccountForRequest(candidates) {
|
|
1019
|
+
if (candidates.length === 0) throw new Error("selectAccountForRequest requires at least one candidate");
|
|
1020
|
+
const orderedAccounts = [...this.temporaryAccount ? [this.temporaryAccount] : [], ...this.accountOrder.map((id) => this.accounts.get(id)).filter((account) => account !== void 0)];
|
|
1021
|
+
const primary = await this.selectAccountForCandidates(orderedAccounts, candidates);
|
|
1022
|
+
if (primary.ok || primary.reason !== "MODEL_NOT_SUPPORTED") return primary;
|
|
1023
|
+
const aliasCandidates = candidates.map((candidate) => {
|
|
1024
|
+
const modelId = resolveModelAlias(candidate.modelId);
|
|
1025
|
+
if (modelId === candidate.modelId) return candidate;
|
|
1026
|
+
return {
|
|
1027
|
+
...candidate,
|
|
1028
|
+
modelId
|
|
1029
|
+
};
|
|
1030
|
+
});
|
|
1031
|
+
if (!aliasCandidates.some((candidate, index) => candidate.modelId !== candidates[index].modelId)) return primary;
|
|
1032
|
+
return this.selectAccountForCandidates(orderedAccounts, aliasCandidates);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Finalize quota after a request completes.
|
|
1036
|
+
* This releases any in-flight reservation and refreshes the actual quota from the API.
|
|
1037
|
+
*/
|
|
1038
|
+
async finalizeQuota(account, reservation) {
|
|
1039
|
+
releasePremiumReservation(account, reservation);
|
|
1040
|
+
try {
|
|
1041
|
+
await this.refreshQuota(account);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
consola.debug(`Failed to finalize quota for ${account.id}:`, error);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Mark an account as failed.
|
|
1048
|
+
*/
|
|
1049
|
+
markAccountFailed(id, reason) {
|
|
1050
|
+
const account = this.accounts.get(id);
|
|
1051
|
+
if (account) {
|
|
1052
|
+
setAccountFailedState(account, reason);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (this.temporaryAccount && this.temporaryAccount.id === id) setAccountFailedState(this.temporaryAccount, reason);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Get status of all accounts.
|
|
1059
|
+
*/
|
|
1060
|
+
getAccountStatus() {
|
|
1061
|
+
const statuses = [];
|
|
1062
|
+
if (this.temporaryAccount) statuses.push({
|
|
1063
|
+
id: "(temporary)",
|
|
1064
|
+
entitlement: this.temporaryAccount.premiumEntitlement,
|
|
1065
|
+
remaining: this.temporaryAccount.premiumRemaining,
|
|
1066
|
+
unlimited: this.temporaryAccount.unlimited,
|
|
1067
|
+
failed: this.temporaryAccount.failed,
|
|
1068
|
+
failureReason: this.temporaryAccount.failureReason
|
|
1069
|
+
});
|
|
1070
|
+
for (const id of this.accountOrder) {
|
|
1071
|
+
const account = this.accounts.get(id);
|
|
1072
|
+
if (account) statuses.push({
|
|
1073
|
+
id: account.id,
|
|
1074
|
+
entitlement: account.premiumEntitlement,
|
|
1075
|
+
remaining: account.premiumRemaining,
|
|
1076
|
+
unlimited: account.unlimited,
|
|
1077
|
+
failed: account.failed,
|
|
1078
|
+
failureReason: account.failureReason
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
return statuses;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Set a temporary account from a GitHub token (--github-token).
|
|
1085
|
+
* This account takes priority over registered accounts.
|
|
1086
|
+
*/
|
|
1087
|
+
async setTemporaryAccount(githubToken, accountType) {
|
|
1088
|
+
const runtime = {
|
|
1089
|
+
id: "(temporary)",
|
|
1090
|
+
accountType,
|
|
1091
|
+
addedAt: Date.now(),
|
|
1092
|
+
githubToken,
|
|
1093
|
+
vsCodeVersion: this.vsCodeVersion
|
|
1094
|
+
};
|
|
1095
|
+
try {
|
|
1096
|
+
await this.initializeAccount(runtime);
|
|
1097
|
+
this.temporaryAccount = runtime;
|
|
1098
|
+
consola.info("Temporary account initialized");
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
consola.error("Failed to initialize temporary account:", error);
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Check if any accounts are available.
|
|
1106
|
+
*/
|
|
1107
|
+
hasAccounts() {
|
|
1108
|
+
return this.accounts.size > 0 || this.temporaryAccount !== void 0;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Get the first available account's models.
|
|
1112
|
+
* Used for caching models in legacy compatibility mode.
|
|
1113
|
+
*/
|
|
1114
|
+
getFirstAccountModels() {
|
|
1115
|
+
if (this.temporaryAccount?.models) return this.temporaryAccount.models;
|
|
1116
|
+
for (const id of this.accountOrder) {
|
|
1117
|
+
const account = this.accounts.get(id);
|
|
1118
|
+
if (account?.models) return account.models;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Get account context by index.
|
|
1123
|
+
* Index 0 is the temporary account (if exists), otherwise the first registered account.
|
|
1124
|
+
* Returns null if index is out of bounds.
|
|
1125
|
+
*/
|
|
1126
|
+
getAccountContextByIndex(index) {
|
|
1127
|
+
const allAccounts = [];
|
|
1128
|
+
if (this.temporaryAccount) allAccounts.push(this.temporaryAccount);
|
|
1129
|
+
for (const id of this.accountOrder) {
|
|
1130
|
+
const account = this.accounts.get(id);
|
|
1131
|
+
if (account) allAccounts.push(account);
|
|
1132
|
+
}
|
|
1133
|
+
if (index < 0 || index >= allAccounts.length) return null;
|
|
1134
|
+
return this.toAccountContext(allAccounts[index]);
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Get the total number of accounts (including temporary).
|
|
1138
|
+
*/
|
|
1139
|
+
getAccountCount() {
|
|
1140
|
+
return (this.temporaryAccount ? 1 : 0) + this.accountOrder.length;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Convert AccountRuntime to AccountContext for service calls.
|
|
1144
|
+
*/
|
|
1145
|
+
toAccountContext(account) {
|
|
1146
|
+
return {
|
|
1147
|
+
githubToken: account.githubToken,
|
|
1148
|
+
copilotToken: account.copilotToken,
|
|
1149
|
+
accountType: account.accountType,
|
|
1150
|
+
vsCodeVersion: account.vsCodeVersion
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Start watching the registry file for changes.
|
|
1155
|
+
* Enables hot reload of accounts when the file is modified.
|
|
1156
|
+
*/
|
|
1157
|
+
startRegistryWatcher() {
|
|
1158
|
+
this.stopRegistryWatcher();
|
|
1159
|
+
try {
|
|
1160
|
+
this.registryWatcher = fs$1.watch(PATHS.ACCOUNTS_REGISTRY_PATH, (eventType) => {
|
|
1161
|
+
if (eventType === "change") this.scheduleReload();
|
|
1162
|
+
});
|
|
1163
|
+
this.registryWatcherRestartDelayMs = WATCHER_RESTART_INITIAL_DELAY_MS;
|
|
1164
|
+
if (this.registryWatcherRestartTimer) {
|
|
1165
|
+
clearTimeout(this.registryWatcherRestartTimer);
|
|
1166
|
+
this.registryWatcherRestartTimer = void 0;
|
|
1167
|
+
}
|
|
1168
|
+
this.registryWatcher.on("error", (error) => {
|
|
1169
|
+
consola.debug("Registry watcher error:", error);
|
|
1170
|
+
const delayMs = this.registryWatcherRestartDelayMs;
|
|
1171
|
+
this.registryWatcherRestartDelayMs = Math.min(this.registryWatcherRestartDelayMs * 2, WATCHER_RESTART_MAX_DELAY_MS);
|
|
1172
|
+
this.stopRegistryWatcher();
|
|
1173
|
+
this.registryWatcherRestartTimer = setTimeout(() => {
|
|
1174
|
+
this.registryWatcherRestartTimer = void 0;
|
|
1175
|
+
this.startRegistryWatcher();
|
|
1176
|
+
}, delayMs);
|
|
1177
|
+
consola.debug(`Restarting registry watcher in ${delayMs}ms`);
|
|
1178
|
+
});
|
|
1179
|
+
consola.debug("Started registry file watcher");
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
consola.warn("Failed to start registry watcher:", error);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Schedule a registry reload with debouncing.
|
|
1186
|
+
*/
|
|
1187
|
+
scheduleReload() {
|
|
1188
|
+
if (this.reloadDebounceTimer) clearTimeout(this.reloadDebounceTimer);
|
|
1189
|
+
this.reloadDebounceTimer = setTimeout(() => {
|
|
1190
|
+
this.reloadRegistry();
|
|
1191
|
+
}, RELOAD_DEBOUNCE_MS);
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Reload the registry and perform incremental updates.
|
|
1195
|
+
* Adds new accounts, removes deleted ones, and reinitializes existing accounts
|
|
1196
|
+
* when token/accountType changes.
|
|
1197
|
+
*/
|
|
1198
|
+
async reloadRegistry() {
|
|
1199
|
+
if (this.isReloading) return;
|
|
1200
|
+
this.isReloading = true;
|
|
1201
|
+
try {
|
|
1202
|
+
const newMetas = await listAccountsFromRegistry();
|
|
1203
|
+
const newIds = new Set(newMetas.map((m) => m.id));
|
|
1204
|
+
const currentIds = new Set(this.accountOrder);
|
|
1205
|
+
const added = [];
|
|
1206
|
+
const removed = [];
|
|
1207
|
+
const updated = [];
|
|
1208
|
+
this.removeDeletedAccounts(currentIds, newIds, removed);
|
|
1209
|
+
for (const meta of newMetas) if (!currentIds.has(meta.id)) await this.addNewAccount(meta, added);
|
|
1210
|
+
await this.reinitializeUpdatedAccounts(newMetas, currentIds, updated);
|
|
1211
|
+
this.accountOrder = newMetas.map((m) => m.id).filter((id) => this.accounts.has(id));
|
|
1212
|
+
this.freeModelCursor = 0;
|
|
1213
|
+
this.logRegistryReloadChanges(added, removed, updated);
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
consola.error("Failed to reload registry:", error);
|
|
1216
|
+
this.shutdown();
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
} finally {
|
|
1219
|
+
this.isReloading = false;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
removeDeletedAccounts(currentIds, newIds, removed) {
|
|
1223
|
+
for (const id of currentIds) if (!newIds.has(id)) {
|
|
1224
|
+
const account = this.accounts.get(id);
|
|
1225
|
+
if (!account) continue;
|
|
1226
|
+
this.stopTokenRefresh(account);
|
|
1227
|
+
this.accounts.delete(id);
|
|
1228
|
+
removed.push(id);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async reinitializeUpdatedAccounts(newMetas, currentIds, updated) {
|
|
1232
|
+
for (const meta of newMetas) {
|
|
1233
|
+
if (!currentIds.has(meta.id)) continue;
|
|
1234
|
+
const account = this.accounts.get(meta.id);
|
|
1235
|
+
if (!account) continue;
|
|
1236
|
+
const token = await loadAccountToken(meta.id);
|
|
1237
|
+
if (!token) {
|
|
1238
|
+
consola.warn(`No token found for account ${meta.id}, skipping update`);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
const accountTypeChanged = account.accountType !== meta.accountType;
|
|
1242
|
+
const tokenChanged = account.githubToken !== token;
|
|
1243
|
+
const addedAtChanged = account.addedAt !== meta.addedAt;
|
|
1244
|
+
if (accountTypeChanged) account.accountType = meta.accountType;
|
|
1245
|
+
if (addedAtChanged) account.addedAt = meta.addedAt;
|
|
1246
|
+
if (tokenChanged) account.githubToken = token;
|
|
1247
|
+
if (!accountTypeChanged && !tokenChanged) continue;
|
|
1248
|
+
try {
|
|
1249
|
+
await this.initializeAccount(account);
|
|
1250
|
+
updated.push(meta.id);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
consola.error(`Failed to reinitialize account ${meta.id} after update:`, error);
|
|
1253
|
+
account.failed = true;
|
|
1254
|
+
account.failureReason = String(error);
|
|
1255
|
+
updated.push(`${meta.id} (failed)`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
logRegistryReloadChanges(added, removed, updated) {
|
|
1260
|
+
if (added.length === 0 && removed.length === 0 && updated.length === 0) return;
|
|
1261
|
+
const changes = [];
|
|
1262
|
+
if (added.length > 0) changes.push(`added: ${added.join(", ")}`);
|
|
1263
|
+
if (removed.length > 0) changes.push(`removed: ${removed.join(", ")}`);
|
|
1264
|
+
if (updated.length > 0) changes.push(`updated: ${updated.join(", ")}`);
|
|
1265
|
+
consola.info(`Registry reloaded (${changes.join("; ")}). Total: ${this.accounts.size} account(s)`);
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Helper to add a new account during reload.
|
|
1269
|
+
*/
|
|
1270
|
+
async addNewAccount(meta, added) {
|
|
1271
|
+
const token = await loadAccountToken(meta.id);
|
|
1272
|
+
if (!token) {
|
|
1273
|
+
consola.warn(`No token found for new account ${meta.id}, skipping`);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const runtime = {
|
|
1277
|
+
...meta,
|
|
1278
|
+
githubToken: token,
|
|
1279
|
+
vsCodeVersion: this.vsCodeVersion
|
|
1280
|
+
};
|
|
1281
|
+
try {
|
|
1282
|
+
await this.initializeAccount(runtime);
|
|
1283
|
+
this.accounts.set(meta.id, runtime);
|
|
1284
|
+
added.push(meta.id);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
consola.error(`Failed to initialize new account ${meta.id}:`, error);
|
|
1287
|
+
runtime.failed = true;
|
|
1288
|
+
runtime.failureReason = String(error);
|
|
1289
|
+
this.accounts.set(meta.id, runtime);
|
|
1290
|
+
added.push(`${meta.id} (failed)`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Stop the registry file watcher.
|
|
1295
|
+
*/
|
|
1296
|
+
stopRegistryWatcher() {
|
|
1297
|
+
if (this.reloadDebounceTimer) {
|
|
1298
|
+
clearTimeout(this.reloadDebounceTimer);
|
|
1299
|
+
this.reloadDebounceTimer = void 0;
|
|
1300
|
+
}
|
|
1301
|
+
if (this.registryWatcherRestartTimer) {
|
|
1302
|
+
clearTimeout(this.registryWatcherRestartTimer);
|
|
1303
|
+
this.registryWatcherRestartTimer = void 0;
|
|
1304
|
+
}
|
|
1305
|
+
if (this.registryWatcher) {
|
|
1306
|
+
this.registryWatcher.close();
|
|
1307
|
+
this.registryWatcher = void 0;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Shutdown the manager and clean up resources.
|
|
1312
|
+
*/
|
|
1313
|
+
shutdown() {
|
|
1314
|
+
this.stopRegistryWatcher();
|
|
1315
|
+
this.stopAllTokenRefresh();
|
|
1316
|
+
this.accounts.clear();
|
|
1317
|
+
this.accountOrder = [];
|
|
1318
|
+
this.temporaryAccount = void 0;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
/** Singleton instance of AccountsManager */
|
|
1322
|
+
const accountsManager = new AccountsManager();
|
|
1323
|
+
|
|
1324
|
+
//#endregion
|
|
1325
|
+
export { GITHUB_APP_SCOPES, GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, accountFromState, accountsManager, addAccountToRegistry, cacheVSCodeVersion, copilotBaseUrl, copilotHeaders, ensurePaths, forwardError, getAliasTargetSet, getConfig, getCopilotUsage, getExtraPromptForModel, getGitHubUser, getModelAliases, getReasoningEffortForModel, getSmallModel, isForceAgentEnabled, isFreeModelLoadBalancingEnabled, isNullish, listAccountsFromRegistry, loadAccountToken, mergeConfigWithDefaults, removeAccountFromRegistry, removeAccountToken, saveAccountToken, saveRegistry, shouldCompactUseSmallModel, sleep, standardHeaders, state };
|
|
1326
|
+
//# sourceMappingURL=accounts-manager-CxuKJ4qv.js.map
|