@love-moon/ai-sdk 0.3.1 → 0.4.0
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/CHANGELOG.md +18 -0
- package/dist/built-in-backends.d.ts +1 -0
- package/dist/built-in-backends.js +6 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +103 -1
- package/dist/external-provider-registry.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/manager/account.d.ts +6 -0
- package/dist/manager/account.js +121 -0
- package/dist/manager/auth-parser.d.ts +27 -0
- package/dist/manager/auth-parser.js +54 -0
- package/dist/manager/config.d.ts +6 -0
- package/dist/manager/config.js +32 -0
- package/dist/manager/index.d.ts +12 -0
- package/dist/manager/index.js +11 -0
- package/dist/manager/install.d.ts +9 -0
- package/dist/manager/install.js +117 -0
- package/dist/manager/manager.d.ts +51 -0
- package/dist/manager/manager.js +105 -0
- package/dist/manager/network.d.ts +8 -0
- package/dist/manager/network.js +46 -0
- package/dist/manager/paths.d.ts +6 -0
- package/dist/manager/paths.js +16 -0
- package/dist/manager/quota/cache.d.ts +9 -0
- package/dist/manager/quota/cache.js +33 -0
- package/dist/manager/quota/claude.d.ts +19 -0
- package/dist/manager/quota/claude.js +193 -0
- package/dist/manager/quota/codex.d.ts +27 -0
- package/dist/manager/quota/codex.js +182 -0
- package/dist/manager/quota/copilot.d.ts +64 -0
- package/dist/manager/quota/copilot.js +718 -0
- package/dist/manager/quota/external.d.ts +29 -0
- package/dist/manager/quota/external.js +176 -0
- package/dist/manager/quota/headers.d.ts +5 -0
- package/dist/manager/quota/headers.js +29 -0
- package/dist/manager/quota/kimi.d.ts +24 -0
- package/dist/manager/quota/kimi.js +230 -0
- package/dist/manager/types.d.ts +166 -0
- package/dist/manager/types.js +1 -0
- package/dist/providers/chat-web-session.d.ts +218 -0
- package/dist/providers/chat-web-session.js +584 -0
- package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
- package/dist/providers/claude-agent-sdk-session.js +109 -1
- package/dist/providers/codex-app-server-session.d.ts +107 -0
- package/dist/providers/codex-app-server-session.js +479 -9
- package/dist/providers/copilot-sdk-session.d.ts +9 -1
- package/dist/providers/copilot-sdk-session.js +48 -0
- package/dist/resume/chat-web.d.ts +20 -0
- package/dist/resume/chat-web.js +44 -0
- package/dist/resume/index.js +2 -0
- package/dist/session-factory.d.ts +3 -1
- package/dist/session-factory.js +17 -4
- package/dist/shared.d.ts +159 -0
- package/dist/shared.js +111 -0
- package/dist/transports/codex-app-server-transport.d.ts +1 -0
- package/dist/transports/codex-app-server-transport.js +45 -1
- package/dist/worker.js +19 -5
- package/package.json +10 -3
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { cacheFile, fingerprintKey, isFresh, readCache, writeCache } from "./cache.js";
|
|
8
|
+
const DEFAULT_TTL = 60;
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
10
|
+
const STOP_TIMEOUT_MS = 5_000;
|
|
11
|
+
const SECURITY_TIMEOUT_MS = 5_000;
|
|
12
|
+
const GITHUB_TOKEN_ENV_KEYS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
13
|
+
const GITHUB_USER_AGENT = "conductor-ai-manager";
|
|
14
|
+
const GITHUB_API_VERSION = "2022-11-28";
|
|
15
|
+
const COPILOT_KEYCHAIN_SERVICE = "copilot-cli";
|
|
16
|
+
const COPILOT_USER_PATH = "/copilot_internal/user";
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
export async function getCopilotQuota(opts = {}) {
|
|
20
|
+
const ttl = opts.ttlSeconds ?? DEFAULT_TTL;
|
|
21
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
22
|
+
const deadlineStartedAt = Date.now();
|
|
23
|
+
const doFetch = opts.fetcher ?? fetch;
|
|
24
|
+
const preStartCacheKey = resolveExplicitAuthCacheKey(opts);
|
|
25
|
+
let file = preStartCacheKey ? cachePathForIdentity(preStartCacheKey, opts.cacheDir) : undefined;
|
|
26
|
+
let canWriteQuotaCache = Boolean(preStartCacheKey);
|
|
27
|
+
let authInfo;
|
|
28
|
+
if (!file) {
|
|
29
|
+
const rememberedCacheKey = await readRememberedAuthCacheKey(opts.cacheDir);
|
|
30
|
+
if (rememberedCacheKey) {
|
|
31
|
+
file = cachePathForIdentity(rememberedCacheKey, opts.cacheDir);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (file && !opts.forceRefresh && ttl > 0) {
|
|
35
|
+
const cached = await readCache(file);
|
|
36
|
+
if (isFresh(cached, ttl) && cached) {
|
|
37
|
+
return { ...cached.value, source: "cached" };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let client;
|
|
41
|
+
try {
|
|
42
|
+
const sdk = opts.sdkModule ?? (await import("@github/copilot-sdk"));
|
|
43
|
+
client = new sdk.CopilotClient(resolveClientOptions(opts));
|
|
44
|
+
const startTimeoutMs = remainingTimeoutMs(deadlineStartedAt, timeoutMs, "copilot quota request timed out");
|
|
45
|
+
await withTimeout(client.start(), startTimeoutMs, "copilot SDK start timed out");
|
|
46
|
+
const authTimeoutMs = remainingTimeoutMs(deadlineStartedAt, timeoutMs, "copilot quota request timed out");
|
|
47
|
+
const authStatus = await getClientAuthStatus(client, authTimeoutMs);
|
|
48
|
+
authInfo = await resolveCopilotAuthInfo(authStatus, opts, doFetch, safeRemainingTimeoutMs(deadlineStartedAt, timeoutMs));
|
|
49
|
+
const authCacheKey = preStartCacheKey ?? authStatusToCacheKey(authStatus);
|
|
50
|
+
file = authCacheKey ? cachePathForIdentity(authCacheKey, opts.cacheDir) : file;
|
|
51
|
+
canWriteQuotaCache = Boolean(authCacheKey);
|
|
52
|
+
if (!preStartCacheKey && authCacheKey) {
|
|
53
|
+
await writeRememberedAuthCacheKey(authCacheKey, opts.cacheDir);
|
|
54
|
+
}
|
|
55
|
+
else if (!preStartCacheKey && shouldClearRememberedAuthCacheKey(authStatus)) {
|
|
56
|
+
await clearRememberedAuthCacheKey(opts.cacheDir);
|
|
57
|
+
file = undefined;
|
|
58
|
+
}
|
|
59
|
+
if (file && !opts.forceRefresh && ttl > 0) {
|
|
60
|
+
const cached = await readCache(file);
|
|
61
|
+
if (isFresh(cached, ttl) && cached) {
|
|
62
|
+
return applyAuthInfo({ ...cached.value, source: "cached" }, authInfo);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const quotaTimeoutMs = remainingTimeoutMs(deadlineStartedAt, timeoutMs, "copilot quota request timed out");
|
|
66
|
+
const payload = await withTimeout(client.rpc.account.getQuota(), quotaTimeoutMs, "copilot quota request timed out");
|
|
67
|
+
let parsed = parseCopilotQuotaSnapshots(payload?.quotaSnapshots);
|
|
68
|
+
const userPayload = Object.keys(parsed.snapshots).length > 0
|
|
69
|
+
? undefined
|
|
70
|
+
: await fetchCopilotUserQuota(authInfo, opts, doFetch, safeRemainingTimeoutMs(deadlineStartedAt, timeoutMs));
|
|
71
|
+
if (userPayload) {
|
|
72
|
+
parsed = parseCopilotUserQuota(userPayload);
|
|
73
|
+
}
|
|
74
|
+
const fresh = applyAuthInfo({
|
|
75
|
+
tool: "copilot",
|
|
76
|
+
fetchedAt: Math.floor(Date.now() / 1000),
|
|
77
|
+
source: "fresh",
|
|
78
|
+
...parsed,
|
|
79
|
+
}, authInfo);
|
|
80
|
+
if (Object.keys(fresh.snapshots).length > 0) {
|
|
81
|
+
if (file && canWriteQuotaCache) {
|
|
82
|
+
await writeCache(file, fresh);
|
|
83
|
+
}
|
|
84
|
+
return fresh;
|
|
85
|
+
}
|
|
86
|
+
return await fallbackFromCache(file, ttl, userPayload ? "copilot account did not expose quota data" : "copilot SDK returned no quota snapshots", authInfo);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
authInfo ??= await resolveCopilotAuthInfo(null, opts, doFetch, safeRemainingTimeoutMs(deadlineStartedAt, timeoutMs));
|
|
90
|
+
return await fallbackFromCache(file, ttl, err?.message ?? String(err), authInfo);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await stopClient(client);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function resolveCopilotPlatformPackageName(platform = process.platform, arch = process.arch) {
|
|
97
|
+
if (!["darwin", "linux", "win32"].includes(platform)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (!["arm64", "x64"].includes(arch)) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return `@github/copilot-${platform}-${arch}`;
|
|
104
|
+
}
|
|
105
|
+
function resolvePackageFileFromSearchPaths(packageName, relativePath, resolvePackagePaths, existsSyncFn) {
|
|
106
|
+
const searchPaths = resolvePackagePaths(packageName) ?? [];
|
|
107
|
+
const packageParts = packageName.split("/");
|
|
108
|
+
for (const basePath of searchPaths) {
|
|
109
|
+
const candidate = join(basePath, ...packageParts, relativePath);
|
|
110
|
+
if (existsSyncFn(candidate)) {
|
|
111
|
+
return candidate;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
export function resolveBundledCopilotCliPath({ platform = process.platform, arch = process.arch, resolvePackage = (packageName) => require.resolve(packageName), resolvePackagePaths = (packageName) => require.resolve.paths(packageName) ?? [], existsSyncFn = existsSync, } = {}) {
|
|
117
|
+
const platformPackageName = resolveCopilotPlatformPackageName(platform, arch);
|
|
118
|
+
if (platformPackageName) {
|
|
119
|
+
try {
|
|
120
|
+
const platformExecutablePath = resolvePackage(platformPackageName);
|
|
121
|
+
if (platformExecutablePath && existsSyncFn(platformExecutablePath)) {
|
|
122
|
+
return platformExecutablePath;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Optional platform packages may be absent when optional dependencies are disabled.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return resolvePackageFileFromSearchPaths("@github/copilot", "npm-loader.js", resolvePackagePaths, existsSyncFn);
|
|
130
|
+
}
|
|
131
|
+
/** Exported for tests. */
|
|
132
|
+
export function parseCopilotQuotaSnapshots(rawSnapshots) {
|
|
133
|
+
const snapshots = {};
|
|
134
|
+
if (rawSnapshots && typeof rawSnapshots === "object") {
|
|
135
|
+
for (const [key, value] of Object.entries(rawSnapshots)) {
|
|
136
|
+
const snapshot = normalizeSnapshot(value);
|
|
137
|
+
if (snapshot)
|
|
138
|
+
snapshots[key] = snapshot;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return buildQuotaFromSnapshots(snapshots, rawSnapshots && typeof rawSnapshots === "object"
|
|
142
|
+
? rawSnapshots
|
|
143
|
+
: undefined);
|
|
144
|
+
}
|
|
145
|
+
/** Exported for tests. */
|
|
146
|
+
export function parseCopilotUserQuota(rawUser) {
|
|
147
|
+
const payload = rawUser && typeof rawUser === "object"
|
|
148
|
+
? rawUser
|
|
149
|
+
: undefined;
|
|
150
|
+
const snapshots = {};
|
|
151
|
+
const resetDate = resolveCopilotUserResetDate(payload);
|
|
152
|
+
const directSnapshots = payload?.quota_snapshots;
|
|
153
|
+
if (directSnapshots && typeof directSnapshots === "object") {
|
|
154
|
+
for (const [key, value] of Object.entries(directSnapshots)) {
|
|
155
|
+
const snapshot = normalizeUserSnapshot(value, resetDate);
|
|
156
|
+
if (snapshot)
|
|
157
|
+
snapshots[key] = snapshot;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (Object.keys(snapshots).length === 0) {
|
|
161
|
+
const monthly = payload?.monthly_quotas;
|
|
162
|
+
const remaining = payload?.limited_user_quotas;
|
|
163
|
+
if (monthly && typeof monthly === "object" && remaining && typeof remaining === "object") {
|
|
164
|
+
const keys = new Set([...Object.keys(monthly), ...Object.keys(remaining)]);
|
|
165
|
+
for (const key of keys) {
|
|
166
|
+
const snapshot = snapshotFromMonthlyQuota(monthly[key], remaining[key], resetDate);
|
|
167
|
+
if (snapshot)
|
|
168
|
+
snapshots[key] = snapshot;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return buildQuotaFromSnapshots(snapshots, payload && typeof payload === "object" ? payload : undefined);
|
|
173
|
+
}
|
|
174
|
+
function normalizeSnapshot(value) {
|
|
175
|
+
if (!value || typeof value !== "object")
|
|
176
|
+
return undefined;
|
|
177
|
+
const obj = value;
|
|
178
|
+
const entitlementRequests = toNumber(obj.entitlementRequests);
|
|
179
|
+
const usedRequests = toNumber(obj.usedRequests);
|
|
180
|
+
const remainingPercentage = toNumber(obj.remainingPercentage);
|
|
181
|
+
if (entitlementRequests === undefined ||
|
|
182
|
+
usedRequests === undefined ||
|
|
183
|
+
remainingPercentage === undefined) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
entitlementRequests,
|
|
188
|
+
usedRequests,
|
|
189
|
+
remainingPercentage,
|
|
190
|
+
overage: toNumber(obj.overage) ?? 0,
|
|
191
|
+
overageAllowedWithExhaustedQuota: typeof obj.overageAllowedWithExhaustedQuota === "boolean"
|
|
192
|
+
? obj.overageAllowedWithExhaustedQuota
|
|
193
|
+
: false,
|
|
194
|
+
resetDate: typeof obj.resetDate === "string" ? obj.resetDate : undefined,
|
|
195
|
+
isUnlimitedEntitlement: typeof obj.isUnlimitedEntitlement === "boolean"
|
|
196
|
+
? obj.isUnlimitedEntitlement
|
|
197
|
+
: entitlementRequests === -1,
|
|
198
|
+
usageAllowedWithExhaustedQuota: typeof obj.usageAllowedWithExhaustedQuota === "boolean"
|
|
199
|
+
? obj.usageAllowedWithExhaustedQuota
|
|
200
|
+
: undefined,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function normalizeUserSnapshot(value, resetDate) {
|
|
204
|
+
if (!value || typeof value !== "object")
|
|
205
|
+
return undefined;
|
|
206
|
+
const obj = value;
|
|
207
|
+
const unlimited = typeof obj.unlimited === "boolean" ? obj.unlimited : false;
|
|
208
|
+
const entitlement = toNumber(obj.entitlement);
|
|
209
|
+
const remaining = toNumber(obj.remaining) ?? toNumber(obj.quota_remaining);
|
|
210
|
+
const explicitPercent = toNumber(obj.percent_remaining);
|
|
211
|
+
const remainingPercentage = explicitPercent ??
|
|
212
|
+
(entitlement !== undefined &&
|
|
213
|
+
entitlement > 0 &&
|
|
214
|
+
remaining !== undefined
|
|
215
|
+
? round1((Math.max(0, Math.min(entitlement, remaining)) / entitlement) * 100)
|
|
216
|
+
: unlimited
|
|
217
|
+
? 100
|
|
218
|
+
: undefined);
|
|
219
|
+
let entitlementRequests;
|
|
220
|
+
let usedRequests;
|
|
221
|
+
if (unlimited) {
|
|
222
|
+
entitlementRequests = -1;
|
|
223
|
+
usedRequests = Math.max(0, toNumber(obj.overage_count) ?? 0);
|
|
224
|
+
}
|
|
225
|
+
else if (entitlement !== undefined) {
|
|
226
|
+
entitlementRequests = entitlement;
|
|
227
|
+
if (remaining !== undefined) {
|
|
228
|
+
usedRequests = Math.max(0, entitlement - Math.max(0, Math.min(entitlement, remaining)));
|
|
229
|
+
}
|
|
230
|
+
else if (remainingPercentage !== undefined) {
|
|
231
|
+
usedRequests = Math.max(0, entitlement - Math.round((entitlement * normalizePercent(remainingPercentage)) / 100));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (remainingPercentage === undefined ||
|
|
235
|
+
entitlementRequests === undefined ||
|
|
236
|
+
usedRequests === undefined) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
entitlementRequests,
|
|
241
|
+
usedRequests,
|
|
242
|
+
remainingPercentage,
|
|
243
|
+
overage: Math.max(0, toNumber(obj.overage_count) ?? 0),
|
|
244
|
+
overageAllowedWithExhaustedQuota: typeof obj.overage_permitted === "boolean" ? obj.overage_permitted : false,
|
|
245
|
+
resetDate,
|
|
246
|
+
isUnlimitedEntitlement: unlimited,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function snapshotFromMonthlyQuota(limitValue, remainingValue, resetDate) {
|
|
250
|
+
const entitlementRequests = toNumber(limitValue);
|
|
251
|
+
const remaining = toNumber(remainingValue);
|
|
252
|
+
if (entitlementRequests === undefined ||
|
|
253
|
+
remaining === undefined ||
|
|
254
|
+
entitlementRequests < 0 ||
|
|
255
|
+
remaining < 0) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
const clampedRemaining = Math.max(0, Math.min(entitlementRequests, remaining));
|
|
259
|
+
return {
|
|
260
|
+
entitlementRequests,
|
|
261
|
+
usedRequests: Math.max(0, entitlementRequests - clampedRemaining),
|
|
262
|
+
remainingPercentage: entitlementRequests === 0 ? 0 : round1((clampedRemaining / entitlementRequests) * 100),
|
|
263
|
+
overage: 0,
|
|
264
|
+
overageAllowedWithExhaustedQuota: false,
|
|
265
|
+
resetDate,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function resolveCopilotUserResetDate(payload) {
|
|
269
|
+
if (!payload)
|
|
270
|
+
return undefined;
|
|
271
|
+
return firstString(payload.quota_reset_date_utc, payload.quota_reset_date, payload.limited_user_reset_date);
|
|
272
|
+
}
|
|
273
|
+
function buildQuotaFromSnapshots(snapshots, raw) {
|
|
274
|
+
const windows = Object.fromEntries(Object.entries(snapshots).map(([key, snapshot]) => [key, windowFromSnapshot(snapshot)]));
|
|
275
|
+
return {
|
|
276
|
+
snapshots,
|
|
277
|
+
primary: windows.premium_interactions ??
|
|
278
|
+
windows.chat ??
|
|
279
|
+
windows.completions ??
|
|
280
|
+
Object.values(windows)[0],
|
|
281
|
+
chat: windows.chat,
|
|
282
|
+
completions: windows.completions,
|
|
283
|
+
premiumInteractions: windows.premium_interactions,
|
|
284
|
+
raw,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function windowFromSnapshot(snapshot) {
|
|
288
|
+
const remainingPercent = normalizePercent(snapshot.remainingPercentage);
|
|
289
|
+
const usedPercent = round1(Math.max(0, Math.min(100, 100 - remainingPercent)));
|
|
290
|
+
const unlimited = snapshot.isUnlimitedEntitlement === true || snapshot.entitlementRequests === -1;
|
|
291
|
+
const limit = unlimited ? undefined : snapshot.entitlementRequests;
|
|
292
|
+
const used = Math.max(0, snapshot.usedRequests);
|
|
293
|
+
const remaining = limit !== undefined ? Math.max(0, limit - used) : undefined;
|
|
294
|
+
const reset = parseResetDate(snapshot.resetDate);
|
|
295
|
+
return {
|
|
296
|
+
usedPercent,
|
|
297
|
+
remainingPercent,
|
|
298
|
+
...reset,
|
|
299
|
+
status: unlimited
|
|
300
|
+
? "unlimited"
|
|
301
|
+
: remainingPercent <= 0
|
|
302
|
+
? "exhausted"
|
|
303
|
+
: snapshot.overageAllowedWithExhaustedQuota
|
|
304
|
+
? "overage_allowed"
|
|
305
|
+
: "allowed",
|
|
306
|
+
limit,
|
|
307
|
+
used,
|
|
308
|
+
remaining,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function parseResetDate(value) {
|
|
312
|
+
if (!value)
|
|
313
|
+
return {};
|
|
314
|
+
const dateOnly = normalizeDateOnlyReset(value);
|
|
315
|
+
if (dateOnly) {
|
|
316
|
+
return { resetOnDate: dateOnly };
|
|
317
|
+
}
|
|
318
|
+
const t = Date.parse(value);
|
|
319
|
+
if (Number.isNaN(t))
|
|
320
|
+
return {};
|
|
321
|
+
return { resetAt: Math.floor(t / 1000) };
|
|
322
|
+
}
|
|
323
|
+
function normalizeDateOnlyReset(value) {
|
|
324
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
|
|
325
|
+
if (!match)
|
|
326
|
+
return undefined;
|
|
327
|
+
const year = Number(match[1]);
|
|
328
|
+
const month = Number(match[2]);
|
|
329
|
+
const day = Number(match[3]);
|
|
330
|
+
const normalized = new Date(Date.UTC(year, month - 1, day));
|
|
331
|
+
if (normalized.getUTCFullYear() !== year ||
|
|
332
|
+
normalized.getUTCMonth() !== month - 1 ||
|
|
333
|
+
normalized.getUTCDate() !== day) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
337
|
+
}
|
|
338
|
+
function normalizePercent(value) {
|
|
339
|
+
if (!Number.isFinite(value))
|
|
340
|
+
return 0;
|
|
341
|
+
const percent = value >= 0 && value <= 1 ? value * 100 : value;
|
|
342
|
+
return round1(Math.max(0, Math.min(100, percent)));
|
|
343
|
+
}
|
|
344
|
+
function toNumber(value) {
|
|
345
|
+
if (value === null || value === undefined)
|
|
346
|
+
return undefined;
|
|
347
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
348
|
+
return Number.isFinite(n) ? n : undefined;
|
|
349
|
+
}
|
|
350
|
+
function remainingTimeoutMs(startedAtMs, timeoutMs, message) {
|
|
351
|
+
const remaining = timeoutMs - (Date.now() - startedAtMs);
|
|
352
|
+
if (remaining <= 0) {
|
|
353
|
+
throw new Error(message);
|
|
354
|
+
}
|
|
355
|
+
return remaining;
|
|
356
|
+
}
|
|
357
|
+
function round1(n) {
|
|
358
|
+
return Math.round(n * 10) / 10;
|
|
359
|
+
}
|
|
360
|
+
function resolveExplicitAuthCacheKey(opts) {
|
|
361
|
+
const token = resolveExplicitGithubToken(opts);
|
|
362
|
+
return token ? `token:${token}` : undefined;
|
|
363
|
+
}
|
|
364
|
+
function resolveClientOptions(opts) {
|
|
365
|
+
const explicitGithubToken = resolveExplicitGithubToken(opts);
|
|
366
|
+
const baseEnv = opts.clientOptions?.env ?? process.env;
|
|
367
|
+
const options = {
|
|
368
|
+
logLevel: "none",
|
|
369
|
+
...opts.clientOptions,
|
|
370
|
+
env: explicitGithubToken ? baseEnv : withoutGitHubTokenEnv(baseEnv),
|
|
371
|
+
useLoggedInUser: explicitGithubToken
|
|
372
|
+
? opts.clientOptions?.useLoggedInUser
|
|
373
|
+
: opts.clientOptions?.useLoggedInUser ?? true,
|
|
374
|
+
...(opts.githubToken ? { githubToken: opts.githubToken } : {}),
|
|
375
|
+
};
|
|
376
|
+
if (options.cliPath === undefined &&
|
|
377
|
+
options.cliUrl === undefined &&
|
|
378
|
+
!hasExplicitCopilotCliPathEnv(options.env)) {
|
|
379
|
+
const bundledCliPath = resolveBundledCopilotCliPath();
|
|
380
|
+
if (bundledCliPath) {
|
|
381
|
+
options.cliPath = bundledCliPath;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return options;
|
|
385
|
+
}
|
|
386
|
+
function resolveExplicitGithubToken(opts) {
|
|
387
|
+
return opts.githubToken ?? opts.clientOptions?.githubToken;
|
|
388
|
+
}
|
|
389
|
+
function hasExplicitCopilotCliPathEnv(env) {
|
|
390
|
+
return Boolean(typeof env?.COPILOT_CLI_PATH === "string" && env.COPILOT_CLI_PATH.trim());
|
|
391
|
+
}
|
|
392
|
+
function withoutGitHubTokenEnv(env) {
|
|
393
|
+
const next = { ...env };
|
|
394
|
+
for (const key of GITHUB_TOKEN_ENV_KEYS) {
|
|
395
|
+
delete next[key];
|
|
396
|
+
}
|
|
397
|
+
return next;
|
|
398
|
+
}
|
|
399
|
+
function authStatusToCacheKey(status) {
|
|
400
|
+
if (!status?.isAuthenticated)
|
|
401
|
+
return undefined;
|
|
402
|
+
const host = resolveKnownGitHubHost(status.host) ?? "github.com";
|
|
403
|
+
const authType = status.authType || "unknown";
|
|
404
|
+
const login = typeof status.login === "string" ? status.login.trim() : "";
|
|
405
|
+
if (!login)
|
|
406
|
+
return undefined;
|
|
407
|
+
return `auth:${host}:${authType}:${login}`;
|
|
408
|
+
}
|
|
409
|
+
function shouldClearRememberedAuthCacheKey(status) {
|
|
410
|
+
if (!status)
|
|
411
|
+
return false;
|
|
412
|
+
if (!status.isAuthenticated)
|
|
413
|
+
return true;
|
|
414
|
+
return !(typeof status.login === "string" && status.login.trim());
|
|
415
|
+
}
|
|
416
|
+
function cachePathForIdentity(identity, dir) {
|
|
417
|
+
return cacheFile("copilot", fingerprintKey(["copilot", identity]), dir);
|
|
418
|
+
}
|
|
419
|
+
function authIndexPath(dir) {
|
|
420
|
+
return cacheFile("copilot", "local-auth", dir);
|
|
421
|
+
}
|
|
422
|
+
async function readRememberedAuthCacheKey(dir) {
|
|
423
|
+
const cached = await readCache(authIndexPath(dir));
|
|
424
|
+
const cacheKey = cached?.value?.cacheKey;
|
|
425
|
+
return typeof cacheKey === "string" && cacheKey.startsWith("auth:") ? cacheKey : undefined;
|
|
426
|
+
}
|
|
427
|
+
async function writeRememberedAuthCacheKey(cacheKey, dir) {
|
|
428
|
+
if (!cacheKey.startsWith("auth:"))
|
|
429
|
+
return;
|
|
430
|
+
await writeCache(authIndexPath(dir), { cacheKey });
|
|
431
|
+
}
|
|
432
|
+
async function clearRememberedAuthCacheKey(dir) {
|
|
433
|
+
await writeCache(authIndexPath(dir), { cacheKey: "" });
|
|
434
|
+
}
|
|
435
|
+
async function getClientAuthStatus(client, timeoutMs) {
|
|
436
|
+
if (typeof client.getAuthStatus !== "function")
|
|
437
|
+
return null;
|
|
438
|
+
try {
|
|
439
|
+
return await withTimeout(client.getAuthStatus(), timeoutMs, "copilot auth status timed out");
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function resolveCopilotAuthInfo(status, opts, doFetch, timeoutMs) {
|
|
446
|
+
const token = resolveGitHubLoginToken(opts)?.trim();
|
|
447
|
+
const host = resolveKnownGitHubHost(status?.host) ?? (token ? "github.com" : undefined);
|
|
448
|
+
const authType = status?.authType;
|
|
449
|
+
const login = typeof status?.login === "string" ? status.login.trim() : "";
|
|
450
|
+
if (login) {
|
|
451
|
+
return {
|
|
452
|
+
authType,
|
|
453
|
+
host,
|
|
454
|
+
login,
|
|
455
|
+
loginSource: "sdk",
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const fallbackLogin = await lookupGitHubTokenLogin(token, host, doFetch, timeoutMs);
|
|
459
|
+
if (!fallbackLogin) {
|
|
460
|
+
return authType || host ? { authType, host } : undefined;
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
authType: authType ?? "token",
|
|
464
|
+
host,
|
|
465
|
+
login: fallbackLogin,
|
|
466
|
+
loginSource: "github_token",
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function resolveKnownGitHubHost(rawHost) {
|
|
470
|
+
const host = typeof rawHost === "string" ? rawHost.trim() : "";
|
|
471
|
+
const envHost = typeof process.env.GH_HOST === "string" ? process.env.GH_HOST.trim() : "";
|
|
472
|
+
return host || envHost || undefined;
|
|
473
|
+
}
|
|
474
|
+
function resolveGitHubLoginToken(opts) {
|
|
475
|
+
return opts.githubToken ?? opts.clientOptions?.githubToken ?? process.env.GITHUB_TOKEN;
|
|
476
|
+
}
|
|
477
|
+
async function fetchCopilotUserQuota(authInfo, opts, doFetch, timeoutMs) {
|
|
478
|
+
if (!timeoutMs || timeoutMs <= 0)
|
|
479
|
+
return undefined;
|
|
480
|
+
const token = await resolveCopilotQuotaFetchToken(authInfo, opts);
|
|
481
|
+
if (!token)
|
|
482
|
+
return undefined;
|
|
483
|
+
const host = normalizeCopilotAuthHost(authInfo?.host) ?? "https://github.com";
|
|
484
|
+
const apiOrigin = toCopilotApiOrigin(host);
|
|
485
|
+
const controller = new AbortController();
|
|
486
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
487
|
+
try {
|
|
488
|
+
const response = await doFetch(new URL(COPILOT_USER_PATH, apiOrigin), {
|
|
489
|
+
method: "GET",
|
|
490
|
+
headers: {
|
|
491
|
+
Accept: "application/json",
|
|
492
|
+
Authorization: `Bearer ${token}`,
|
|
493
|
+
"User-Agent": GITHUB_USER_AGENT,
|
|
494
|
+
},
|
|
495
|
+
signal: controller.signal,
|
|
496
|
+
});
|
|
497
|
+
if (!response.ok)
|
|
498
|
+
return undefined;
|
|
499
|
+
const payload = await response.json().catch(() => null);
|
|
500
|
+
return payload && typeof payload === "object"
|
|
501
|
+
? payload
|
|
502
|
+
: undefined;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
clearTimeout(timer);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function resolveCopilotQuotaFetchToken(authInfo, opts) {
|
|
512
|
+
const explicitToken = resolveExplicitGithubToken(opts)?.trim();
|
|
513
|
+
if (explicitToken)
|
|
514
|
+
return explicitToken;
|
|
515
|
+
if (!authInfo?.login || authInfo.loginSource === "github_token")
|
|
516
|
+
return undefined;
|
|
517
|
+
return await resolveStoredCopilotToken(authInfo, opts);
|
|
518
|
+
}
|
|
519
|
+
async function resolveStoredCopilotToken(authInfo, opts) {
|
|
520
|
+
if (opts.storedTokenResolver) {
|
|
521
|
+
return trimToUndefined(await opts.storedTokenResolver(authInfo));
|
|
522
|
+
}
|
|
523
|
+
const host = normalizeCopilotAuthHost(authInfo.host) ?? "https://github.com";
|
|
524
|
+
const login = trimToUndefined(authInfo.login);
|
|
525
|
+
if (!login)
|
|
526
|
+
return undefined;
|
|
527
|
+
const env = opts.clientOptions?.env ?? process.env;
|
|
528
|
+
const fromConfig = await readCopilotTokenFromConfig(host, login, env);
|
|
529
|
+
if (fromConfig)
|
|
530
|
+
return fromConfig;
|
|
531
|
+
if (process.platform !== "darwin")
|
|
532
|
+
return undefined;
|
|
533
|
+
try {
|
|
534
|
+
const { stdout } = await execFileAsync("security", [
|
|
535
|
+
"find-generic-password",
|
|
536
|
+
"-s",
|
|
537
|
+
COPILOT_KEYCHAIN_SERVICE,
|
|
538
|
+
"-a",
|
|
539
|
+
`${host}:${login}`,
|
|
540
|
+
"-w",
|
|
541
|
+
], {
|
|
542
|
+
encoding: "utf8",
|
|
543
|
+
timeout: SECURITY_TIMEOUT_MS,
|
|
544
|
+
});
|
|
545
|
+
return trimToUndefined(stdout);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function readCopilotTokenFromConfig(host, login, env) {
|
|
552
|
+
const configPath = resolveCopilotConfigPath(env);
|
|
553
|
+
if (!configPath)
|
|
554
|
+
return undefined;
|
|
555
|
+
try {
|
|
556
|
+
const payload = JSON.parse(await readFile(configPath, "utf8"));
|
|
557
|
+
const rawTokens = payload.copilot_tokens ??
|
|
558
|
+
payload.copilotTokens;
|
|
559
|
+
if (!rawTokens || typeof rawTokens !== "object")
|
|
560
|
+
return undefined;
|
|
561
|
+
const tokenMap = rawTokens;
|
|
562
|
+
for (const key of tokenLookupKeys(host, login)) {
|
|
563
|
+
const token = trimToUndefined(tokenMap[key]);
|
|
564
|
+
if (token)
|
|
565
|
+
return token;
|
|
566
|
+
}
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function resolveCopilotConfigPath(env) {
|
|
574
|
+
const configDir = trimToUndefined(env.COPILOT_CONFIG_DIR);
|
|
575
|
+
if (configDir)
|
|
576
|
+
return join(configDir, "config.json");
|
|
577
|
+
const home = trimToUndefined(env.HOME) ?? trimToUndefined(env.USERPROFILE);
|
|
578
|
+
return home ? join(home, ".copilot", "config.json") : undefined;
|
|
579
|
+
}
|
|
580
|
+
function tokenLookupKeys(host, login) {
|
|
581
|
+
const withoutProtocol = host.replace(/^https?:\/\//, "");
|
|
582
|
+
return [`${host}:${login}`, `${withoutProtocol}:${login}`];
|
|
583
|
+
}
|
|
584
|
+
function normalizeCopilotAuthHost(rawHost) {
|
|
585
|
+
const host = trimToUndefined(rawHost);
|
|
586
|
+
if (!host)
|
|
587
|
+
return undefined;
|
|
588
|
+
try {
|
|
589
|
+
return new URL(host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`)
|
|
590
|
+
.origin;
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function toCopilotApiOrigin(host) {
|
|
597
|
+
const url = new URL(host);
|
|
598
|
+
if (!url.hostname.startsWith("api.")) {
|
|
599
|
+
url.hostname = `api.${url.hostname}`;
|
|
600
|
+
}
|
|
601
|
+
return url.origin;
|
|
602
|
+
}
|
|
603
|
+
async function lookupGitHubTokenLogin(token, host, doFetch, timeoutMs) {
|
|
604
|
+
if (!token || !timeoutMs || timeoutMs <= 0)
|
|
605
|
+
return undefined;
|
|
606
|
+
const controller = new AbortController();
|
|
607
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
608
|
+
try {
|
|
609
|
+
const response = await doFetch(githubUserUrl(host ?? "github.com"), {
|
|
610
|
+
method: "GET",
|
|
611
|
+
headers: {
|
|
612
|
+
Accept: "application/vnd.github+json",
|
|
613
|
+
Authorization: `Bearer ${token}`,
|
|
614
|
+
"User-Agent": GITHUB_USER_AGENT,
|
|
615
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
616
|
+
},
|
|
617
|
+
signal: controller.signal,
|
|
618
|
+
});
|
|
619
|
+
if (!response.ok)
|
|
620
|
+
return undefined;
|
|
621
|
+
const payload = await response.json().catch(() => null);
|
|
622
|
+
const login = typeof payload?.login === "string" ? payload.login.trim() : "";
|
|
623
|
+
return login || undefined;
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
finally {
|
|
629
|
+
clearTimeout(timer);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function githubUserUrl(host) {
|
|
633
|
+
return host === "github.com"
|
|
634
|
+
? "https://api.github.com/user"
|
|
635
|
+
: `https://${host}/api/v3/user`;
|
|
636
|
+
}
|
|
637
|
+
function trimToUndefined(value) {
|
|
638
|
+
if (typeof value !== "string")
|
|
639
|
+
return undefined;
|
|
640
|
+
const trimmed = value.trim();
|
|
641
|
+
return trimmed || undefined;
|
|
642
|
+
}
|
|
643
|
+
function firstString(...values) {
|
|
644
|
+
for (const value of values) {
|
|
645
|
+
const trimmed = trimToUndefined(value);
|
|
646
|
+
if (trimmed)
|
|
647
|
+
return trimmed;
|
|
648
|
+
}
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
function safeRemainingTimeoutMs(startedAtMs, timeoutMs) {
|
|
652
|
+
const remaining = timeoutMs - (Date.now() - startedAtMs);
|
|
653
|
+
return remaining > 0 ? remaining : undefined;
|
|
654
|
+
}
|
|
655
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
656
|
+
let timer;
|
|
657
|
+
try {
|
|
658
|
+
return await Promise.race([
|
|
659
|
+
promise,
|
|
660
|
+
new Promise((_, reject) => {
|
|
661
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
662
|
+
}),
|
|
663
|
+
]);
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
if (timer)
|
|
667
|
+
clearTimeout(timer);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async function stopClient(client) {
|
|
671
|
+
if (!client)
|
|
672
|
+
return;
|
|
673
|
+
try {
|
|
674
|
+
await withTimeout(client.stop(), STOP_TIMEOUT_MS, "copilot SDK stop timed out");
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
try {
|
|
678
|
+
await client.forceStop?.();
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
// ignore cleanup errors
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function fallbackFromCache(file, ttl, error, authInfo) {
|
|
686
|
+
if (!file) {
|
|
687
|
+
return applyAuthInfo(emptyQuota("unknown", error), authInfo);
|
|
688
|
+
}
|
|
689
|
+
const cached = await readCache(file);
|
|
690
|
+
if (cached) {
|
|
691
|
+
return applyAuthInfo({
|
|
692
|
+
...cached.value,
|
|
693
|
+
source: isFresh(cached, ttl) ? "cached" : "stale",
|
|
694
|
+
error,
|
|
695
|
+
}, authInfo);
|
|
696
|
+
}
|
|
697
|
+
return applyAuthInfo(emptyQuota("unknown", error), authInfo);
|
|
698
|
+
}
|
|
699
|
+
function emptyQuota(source, error) {
|
|
700
|
+
return {
|
|
701
|
+
tool: "copilot",
|
|
702
|
+
source,
|
|
703
|
+
error,
|
|
704
|
+
fetchedAt: Math.floor(Date.now() / 1000),
|
|
705
|
+
snapshots: {},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function applyAuthInfo(quota, authInfo) {
|
|
709
|
+
if (!authInfo)
|
|
710
|
+
return quota;
|
|
711
|
+
return {
|
|
712
|
+
...quota,
|
|
713
|
+
...(authInfo.authType ? { authType: authInfo.authType } : {}),
|
|
714
|
+
...(authInfo.host ? { host: authInfo.host } : {}),
|
|
715
|
+
...(authInfo.login ? { login: authInfo.login } : {}),
|
|
716
|
+
...(authInfo.loginSource ? { loginSource: authInfo.loginSource } : {}),
|
|
717
|
+
};
|
|
718
|
+
}
|