@corbat-tech/coco 2.11.0 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2135 -2065
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +990 -919
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1027,12 +1027,16 @@ var init_anthropic = __esm({
|
|
|
1027
1027
|
);
|
|
1028
1028
|
const streamTimeout = this.config.timeout ?? 12e4;
|
|
1029
1029
|
let lastActivityTime = Date.now();
|
|
1030
|
-
const
|
|
1030
|
+
const timeoutController = new AbortController();
|
|
1031
|
+
const timeoutInterval = setInterval(() => {
|
|
1031
1032
|
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
1032
|
-
|
|
1033
|
+
clearInterval(timeoutInterval);
|
|
1034
|
+
timeoutController.abort();
|
|
1033
1035
|
}
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
+
}, 5e3);
|
|
1037
|
+
timeoutController.signal.addEventListener("abort", () => stream.controller.abort(), {
|
|
1038
|
+
once: true
|
|
1039
|
+
});
|
|
1036
1040
|
try {
|
|
1037
1041
|
let streamStopReason;
|
|
1038
1042
|
for await (const event of stream) {
|
|
@@ -1053,6 +1057,9 @@ var init_anthropic = __esm({
|
|
|
1053
1057
|
} finally {
|
|
1054
1058
|
clearInterval(timeoutInterval);
|
|
1055
1059
|
}
|
|
1060
|
+
if (timeoutController.signal.aborted) {
|
|
1061
|
+
throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
|
|
1062
|
+
}
|
|
1056
1063
|
} catch (error) {
|
|
1057
1064
|
throw this.handleError(error);
|
|
1058
1065
|
}
|
|
@@ -1079,12 +1086,16 @@ var init_anthropic = __esm({
|
|
|
1079
1086
|
let currentToolInputJson = "";
|
|
1080
1087
|
const streamTimeout = this.config.timeout ?? 12e4;
|
|
1081
1088
|
let lastActivityTime = Date.now();
|
|
1082
|
-
const
|
|
1089
|
+
const timeoutController = new AbortController();
|
|
1090
|
+
const timeoutInterval = setInterval(() => {
|
|
1083
1091
|
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
1084
|
-
|
|
1092
|
+
clearInterval(timeoutInterval);
|
|
1093
|
+
timeoutController.abort();
|
|
1085
1094
|
}
|
|
1086
|
-
};
|
|
1087
|
-
|
|
1095
|
+
}, 5e3);
|
|
1096
|
+
timeoutController.signal.addEventListener("abort", () => stream.controller.abort(), {
|
|
1097
|
+
once: true
|
|
1098
|
+
});
|
|
1088
1099
|
try {
|
|
1089
1100
|
let streamStopReason;
|
|
1090
1101
|
for await (const event of stream) {
|
|
@@ -1169,6 +1180,9 @@ var init_anthropic = __esm({
|
|
|
1169
1180
|
} finally {
|
|
1170
1181
|
clearInterval(timeoutInterval);
|
|
1171
1182
|
}
|
|
1183
|
+
if (timeoutController.signal.aborted) {
|
|
1184
|
+
throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
|
|
1185
|
+
}
|
|
1172
1186
|
} catch (error) {
|
|
1173
1187
|
throw this.handleError(error);
|
|
1174
1188
|
}
|
|
@@ -1380,6 +1394,9 @@ var init_anthropic = __esm({
|
|
|
1380
1394
|
};
|
|
1381
1395
|
}
|
|
1382
1396
|
});
|
|
1397
|
+
function needsResponsesApi(model) {
|
|
1398
|
+
return model.includes("codex") || model.startsWith("gpt-5") || model.startsWith("o4-") || model.startsWith("o3-");
|
|
1399
|
+
}
|
|
1383
1400
|
function createOpenAIProvider(config) {
|
|
1384
1401
|
const provider = new OpenAIProvider();
|
|
1385
1402
|
if (config) {
|
|
@@ -1580,9 +1597,12 @@ var init_openai = __esm({
|
|
|
1580
1597
|
*/
|
|
1581
1598
|
async chat(messages, options) {
|
|
1582
1599
|
this.ensureInitialized();
|
|
1600
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1601
|
+
if (needsResponsesApi(model)) {
|
|
1602
|
+
return this.chatViaResponses(messages, options);
|
|
1603
|
+
}
|
|
1583
1604
|
return withRetry(async () => {
|
|
1584
1605
|
try {
|
|
1585
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1586
1606
|
const supportsTemp = this.supportsTemperature(model);
|
|
1587
1607
|
const response = await this.client.chat.completions.create({
|
|
1588
1608
|
model,
|
|
@@ -1614,9 +1634,12 @@ var init_openai = __esm({
|
|
|
1614
1634
|
*/
|
|
1615
1635
|
async chatWithTools(messages, options) {
|
|
1616
1636
|
this.ensureInitialized();
|
|
1637
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1638
|
+
if (needsResponsesApi(model)) {
|
|
1639
|
+
return this.chatWithToolsViaResponses(messages, options);
|
|
1640
|
+
}
|
|
1617
1641
|
return withRetry(async () => {
|
|
1618
1642
|
try {
|
|
1619
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1620
1643
|
const supportsTemp = this.supportsTemperature(model);
|
|
1621
1644
|
const extraBody = this.getExtraBody(model);
|
|
1622
1645
|
const requestParams = {
|
|
@@ -1658,8 +1681,12 @@ var init_openai = __esm({
|
|
|
1658
1681
|
*/
|
|
1659
1682
|
async *stream(messages, options) {
|
|
1660
1683
|
this.ensureInitialized();
|
|
1684
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1685
|
+
if (needsResponsesApi(model)) {
|
|
1686
|
+
yield* this.streamViaResponses(messages, options);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1661
1689
|
try {
|
|
1662
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1663
1690
|
const supportsTemp = this.supportsTemperature(model);
|
|
1664
1691
|
const stream = await this.client.chat.completions.create({
|
|
1665
1692
|
model,
|
|
@@ -1689,8 +1716,12 @@ var init_openai = __esm({
|
|
|
1689
1716
|
*/
|
|
1690
1717
|
async *streamWithTools(messages, options) {
|
|
1691
1718
|
this.ensureInitialized();
|
|
1719
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1720
|
+
if (needsResponsesApi(model)) {
|
|
1721
|
+
yield* this.streamWithToolsViaResponses(messages, options);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1692
1724
|
try {
|
|
1693
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
1694
1725
|
const supportsTemp = this.supportsTemperature(model);
|
|
1695
1726
|
const extraBody = this.getExtraBody(model);
|
|
1696
1727
|
const requestParams = {
|
|
@@ -1713,12 +1744,16 @@ var init_openai = __esm({
|
|
|
1713
1744
|
const toolCallBuilders = /* @__PURE__ */ new Map();
|
|
1714
1745
|
const streamTimeout = this.config.timeout ?? 12e4;
|
|
1715
1746
|
let lastActivityTime = Date.now();
|
|
1716
|
-
const
|
|
1747
|
+
const timeoutController = new AbortController();
|
|
1748
|
+
const timeoutInterval = setInterval(() => {
|
|
1717
1749
|
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
1718
|
-
|
|
1750
|
+
clearInterval(timeoutInterval);
|
|
1751
|
+
timeoutController.abort();
|
|
1719
1752
|
}
|
|
1720
|
-
};
|
|
1721
|
-
|
|
1753
|
+
}, 5e3);
|
|
1754
|
+
timeoutController.signal.addEventListener("abort", () => stream.controller.abort(), {
|
|
1755
|
+
once: true
|
|
1756
|
+
});
|
|
1722
1757
|
const providerName = this.name;
|
|
1723
1758
|
const parseArguments = (builder) => {
|
|
1724
1759
|
let input = {};
|
|
@@ -1822,6 +1857,9 @@ var init_openai = __esm({
|
|
|
1822
1857
|
} finally {
|
|
1823
1858
|
clearInterval(timeoutInterval);
|
|
1824
1859
|
}
|
|
1860
|
+
if (timeoutController.signal.aborted) {
|
|
1861
|
+
throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
|
|
1862
|
+
}
|
|
1825
1863
|
} catch (error) {
|
|
1826
1864
|
throw this.handleError(error);
|
|
1827
1865
|
}
|
|
@@ -2154,623 +2192,463 @@ var init_openai = __esm({
|
|
|
2154
2192
|
cause: error instanceof Error ? error : void 0
|
|
2155
2193
|
});
|
|
2156
2194
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
if (contentType2.includes("text/html") || error.includes("<!DOCTYPE") || error.includes("<html")) {
|
|
2190
|
-
throw new Error(
|
|
2191
|
-
"OAuth request blocked (possibly by Cloudflare).\n This can happen due to network restrictions or rate limiting.\n Please try:\n 1. Use an API key instead (recommended)\n 2. Wait a few minutes and try again\n 3. Try from a different network"
|
|
2192
|
-
);
|
|
2193
|
-
}
|
|
2194
|
-
throw new Error(`Failed to request device code: ${error}`);
|
|
2195
|
-
}
|
|
2196
|
-
const contentType = response.headers.get("content-type") || "";
|
|
2197
|
-
if (!contentType.includes("application/json")) {
|
|
2198
|
-
const text13 = await response.text();
|
|
2199
|
-
if (text13.includes("<!DOCTYPE") || text13.includes("<html")) {
|
|
2200
|
-
throw new Error(
|
|
2201
|
-
"OAuth service returned HTML instead of JSON.\n The service may be temporarily unavailable.\n Please use an API key instead, or try again later."
|
|
2202
|
-
);
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
const data = await response.json();
|
|
2206
|
-
return {
|
|
2207
|
-
deviceCode: data.device_code,
|
|
2208
|
-
userCode: data.user_code,
|
|
2209
|
-
verificationUri: data.verification_uri || config.verificationUri || "",
|
|
2210
|
-
verificationUriComplete: data.verification_uri_complete,
|
|
2211
|
-
expiresIn: data.expires_in,
|
|
2212
|
-
interval: data.interval || 5
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
async function pollForToken(provider, deviceCode, interval, expiresIn, onPoll) {
|
|
2216
|
-
const config = OAUTH_CONFIGS[provider];
|
|
2217
|
-
if (!config) {
|
|
2218
|
-
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
2219
|
-
}
|
|
2220
|
-
const startTime = Date.now();
|
|
2221
|
-
const expiresAt = startTime + expiresIn * 1e3;
|
|
2222
|
-
while (Date.now() < expiresAt) {
|
|
2223
|
-
await new Promise((resolve4) => setTimeout(resolve4, interval * 1e3));
|
|
2224
|
-
if (onPoll) onPoll();
|
|
2225
|
-
const body = new URLSearchParams({
|
|
2226
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
2227
|
-
client_id: config.clientId,
|
|
2228
|
-
device_code: deviceCode
|
|
2229
|
-
});
|
|
2230
|
-
const response = await fetch(config.tokenEndpoint, {
|
|
2231
|
-
method: "POST",
|
|
2232
|
-
headers: {
|
|
2233
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
2234
|
-
},
|
|
2235
|
-
body: body.toString()
|
|
2236
|
-
});
|
|
2237
|
-
const data = await response.json();
|
|
2238
|
-
if (data.access_token) {
|
|
2239
|
-
return {
|
|
2240
|
-
accessToken: data.access_token,
|
|
2241
|
-
refreshToken: data.refresh_token,
|
|
2242
|
-
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
|
|
2243
|
-
tokenType: data.token_type || "Bearer"
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
if (data.error === "authorization_pending") {
|
|
2247
|
-
continue;
|
|
2248
|
-
} else if (data.error === "slow_down") {
|
|
2249
|
-
interval += 5;
|
|
2250
|
-
continue;
|
|
2251
|
-
} else if (data.error === "expired_token") {
|
|
2252
|
-
throw new Error("Device code expired. Please try again.");
|
|
2253
|
-
} else if (data.error === "access_denied") {
|
|
2254
|
-
throw new Error("Access denied by user.");
|
|
2255
|
-
} else if (data.error) {
|
|
2256
|
-
throw new Error(data.error_description || data.error);
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
throw new Error("Authentication timed out. Please try again.");
|
|
2260
|
-
}
|
|
2261
|
-
async function refreshAccessToken(provider, refreshToken) {
|
|
2262
|
-
const config = OAUTH_CONFIGS[provider];
|
|
2263
|
-
if (!config) {
|
|
2264
|
-
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
2265
|
-
}
|
|
2266
|
-
const body = new URLSearchParams({
|
|
2267
|
-
grant_type: "refresh_token",
|
|
2268
|
-
client_id: config.clientId,
|
|
2269
|
-
refresh_token: refreshToken
|
|
2270
|
-
});
|
|
2271
|
-
const response = await fetch(config.tokenEndpoint, {
|
|
2272
|
-
method: "POST",
|
|
2273
|
-
headers: {
|
|
2274
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
2275
|
-
},
|
|
2276
|
-
body: body.toString()
|
|
2277
|
-
});
|
|
2278
|
-
if (!response.ok) {
|
|
2279
|
-
const error = await response.text();
|
|
2280
|
-
throw new Error(`Token refresh failed: ${error}`);
|
|
2281
|
-
}
|
|
2282
|
-
const data = await response.json();
|
|
2283
|
-
return {
|
|
2284
|
-
accessToken: data.access_token,
|
|
2285
|
-
refreshToken: data.refresh_token || refreshToken,
|
|
2286
|
-
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
|
|
2287
|
-
tokenType: data.token_type
|
|
2288
|
-
};
|
|
2289
|
-
}
|
|
2290
|
-
function getTokenStoragePath(provider) {
|
|
2291
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2292
|
-
return path36.join(home, ".coco", "tokens", `${provider}.json`);
|
|
2293
|
-
}
|
|
2294
|
-
async function saveTokens(provider, tokens) {
|
|
2295
|
-
const filePath = getTokenStoragePath(provider);
|
|
2296
|
-
const dir = path36.dirname(filePath);
|
|
2297
|
-
await fs34.mkdir(dir, { recursive: true, mode: 448 });
|
|
2298
|
-
await fs34.writeFile(filePath, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
2299
|
-
}
|
|
2300
|
-
async function loadTokens(provider) {
|
|
2301
|
-
const filePath = getTokenStoragePath(provider);
|
|
2302
|
-
try {
|
|
2303
|
-
const content = await fs34.readFile(filePath, "utf-8");
|
|
2304
|
-
return JSON.parse(content);
|
|
2305
|
-
} catch {
|
|
2306
|
-
return null;
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
async function deleteTokens(provider) {
|
|
2310
|
-
const filePath = getTokenStoragePath(provider);
|
|
2311
|
-
try {
|
|
2312
|
-
await fs34.unlink(filePath);
|
|
2313
|
-
} catch {
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
function isTokenExpired(tokens) {
|
|
2317
|
-
if (!tokens.expiresAt) return false;
|
|
2318
|
-
return Date.now() >= tokens.expiresAt - 5 * 60 * 1e3;
|
|
2319
|
-
}
|
|
2320
|
-
async function getValidAccessToken(provider) {
|
|
2321
|
-
const config = OAUTH_CONFIGS[provider];
|
|
2322
|
-
if (!config) return null;
|
|
2323
|
-
const tokens = await loadTokens(provider);
|
|
2324
|
-
if (!tokens) return null;
|
|
2325
|
-
if (isTokenExpired(tokens)) {
|
|
2326
|
-
if (tokens.refreshToken) {
|
|
2327
|
-
try {
|
|
2328
|
-
const newTokens = await refreshAccessToken(provider, tokens.refreshToken);
|
|
2329
|
-
await saveTokens(provider, newTokens);
|
|
2330
|
-
return { accessToken: newTokens.accessToken, isNew: true };
|
|
2331
|
-
} catch {
|
|
2332
|
-
await deleteTokens(provider);
|
|
2333
|
-
return null;
|
|
2195
|
+
// --- Responses API support (GPT-5+, Codex, o3, o4 models) ---
|
|
2196
|
+
/**
|
|
2197
|
+
* Simple chat via Responses API (no tools)
|
|
2198
|
+
*/
|
|
2199
|
+
async chatViaResponses(messages, options) {
|
|
2200
|
+
this.ensureInitialized();
|
|
2201
|
+
return withRetry(async () => {
|
|
2202
|
+
try {
|
|
2203
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
2204
|
+
const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
|
|
2205
|
+
const response = await this.client.responses.create({
|
|
2206
|
+
model,
|
|
2207
|
+
input,
|
|
2208
|
+
instructions: instructions ?? void 0,
|
|
2209
|
+
max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
2210
|
+
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
2211
|
+
store: false
|
|
2212
|
+
});
|
|
2213
|
+
return {
|
|
2214
|
+
id: response.id,
|
|
2215
|
+
content: response.output_text ?? "",
|
|
2216
|
+
stopReason: response.status === "completed" ? "end_turn" : "max_tokens",
|
|
2217
|
+
usage: {
|
|
2218
|
+
inputTokens: response.usage?.input_tokens ?? 0,
|
|
2219
|
+
outputTokens: response.usage?.output_tokens ?? 0
|
|
2220
|
+
},
|
|
2221
|
+
model: String(response.model)
|
|
2222
|
+
};
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
throw this.handleError(error);
|
|
2225
|
+
}
|
|
2226
|
+
}, this.retryConfig);
|
|
2334
2227
|
}
|
|
2335
|
-
}
|
|
2336
|
-
await deleteTokens(provider);
|
|
2337
|
-
return null;
|
|
2338
|
-
}
|
|
2339
|
-
return { accessToken: tokens.accessToken, isNew: false };
|
|
2340
|
-
}
|
|
2341
|
-
function buildAuthorizationUrl(provider, redirectUri, codeChallenge, state) {
|
|
2342
|
-
const config = OAUTH_CONFIGS[provider];
|
|
2343
|
-
if (!config) {
|
|
2344
|
-
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
2345
|
-
}
|
|
2346
|
-
const params = new URLSearchParams({
|
|
2347
|
-
response_type: "code",
|
|
2348
|
-
client_id: config.clientId,
|
|
2349
|
-
redirect_uri: redirectUri,
|
|
2350
|
-
scope: config.scopes.join(" "),
|
|
2351
|
-
code_challenge: codeChallenge,
|
|
2352
|
-
code_challenge_method: "S256",
|
|
2353
|
-
state
|
|
2354
|
-
});
|
|
2355
|
-
if (config.extraAuthParams) {
|
|
2356
|
-
for (const [key, value] of Object.entries(config.extraAuthParams)) {
|
|
2357
|
-
params.set(key, value);
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
return `${config.authorizationEndpoint}?${params.toString()}`;
|
|
2361
|
-
}
|
|
2362
|
-
async function exchangeCodeForTokens(provider, code, codeVerifier, redirectUri) {
|
|
2363
|
-
const config = OAUTH_CONFIGS[provider];
|
|
2364
|
-
if (!config) {
|
|
2365
|
-
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
2366
|
-
}
|
|
2367
|
-
const body = new URLSearchParams({
|
|
2368
|
-
grant_type: "authorization_code",
|
|
2369
|
-
client_id: config.clientId,
|
|
2370
|
-
code,
|
|
2371
|
-
code_verifier: codeVerifier,
|
|
2372
|
-
redirect_uri: redirectUri
|
|
2373
|
-
});
|
|
2374
|
-
const response = await fetch(config.tokenEndpoint, {
|
|
2375
|
-
method: "POST",
|
|
2376
|
-
headers: {
|
|
2377
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
2378
|
-
Accept: "application/json"
|
|
2379
|
-
},
|
|
2380
|
-
body: body.toString()
|
|
2381
|
-
});
|
|
2382
|
-
if (!response.ok) {
|
|
2383
|
-
const error = await response.text();
|
|
2384
|
-
throw new Error(`Token exchange failed: ${error}`);
|
|
2385
|
-
}
|
|
2386
|
-
const data = await response.json();
|
|
2387
|
-
return {
|
|
2388
|
-
accessToken: data.access_token,
|
|
2389
|
-
refreshToken: data.refresh_token,
|
|
2390
|
-
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
|
|
2391
|
-
tokenType: data.token_type || "Bearer"
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
var OAUTH_CONFIGS;
|
|
2395
|
-
var init_oauth = __esm({
|
|
2396
|
-
"src/auth/oauth.ts"() {
|
|
2397
|
-
OAUTH_CONFIGS = {
|
|
2398
2228
|
/**
|
|
2399
|
-
*
|
|
2400
|
-
* Uses the official Codex client ID (same as OpenCode, Codex CLI, etc.)
|
|
2229
|
+
* Chat with tools via Responses API
|
|
2401
2230
|
*/
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
});
|
|
2450
|
-
function escapeHtml(unsafe) {
|
|
2451
|
-
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2452
|
-
}
|
|
2453
|
-
async function createCallbackServer(expectedState, timeout = 5 * 60 * 1e3, port = OAUTH_CALLBACK_PORT) {
|
|
2454
|
-
let resolveResult;
|
|
2455
|
-
let rejectResult;
|
|
2456
|
-
const resultPromise = new Promise((resolve4, reject) => {
|
|
2457
|
-
resolveResult = resolve4;
|
|
2458
|
-
rejectResult = reject;
|
|
2459
|
-
});
|
|
2460
|
-
const server = http.createServer((req, res) => {
|
|
2461
|
-
console.log(` [OAuth] ${req.method} ${req.url?.split("?")[0]}`);
|
|
2462
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2463
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2464
|
-
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
2465
|
-
if (req.method === "OPTIONS") {
|
|
2466
|
-
res.writeHead(204);
|
|
2467
|
-
res.end();
|
|
2468
|
-
return;
|
|
2469
|
-
}
|
|
2470
|
-
if (!req.url?.startsWith("/auth/callback")) {
|
|
2471
|
-
res.writeHead(404);
|
|
2472
|
-
res.end("Not Found");
|
|
2473
|
-
return;
|
|
2474
|
-
}
|
|
2475
|
-
try {
|
|
2476
|
-
const url = new URL$1(req.url, `http://localhost`);
|
|
2477
|
-
const code = url.searchParams.get("code");
|
|
2478
|
-
const state = url.searchParams.get("state");
|
|
2479
|
-
const error = url.searchParams.get("error");
|
|
2480
|
-
const errorDescription = url.searchParams.get("error_description");
|
|
2481
|
-
if (error) {
|
|
2482
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2483
|
-
res.end(ERROR_HTML(errorDescription || error));
|
|
2484
|
-
server.close();
|
|
2485
|
-
rejectResult(new Error(errorDescription || error));
|
|
2486
|
-
return;
|
|
2231
|
+
async chatWithToolsViaResponses(messages, options) {
|
|
2232
|
+
this.ensureInitialized();
|
|
2233
|
+
return withRetry(async () => {
|
|
2234
|
+
try {
|
|
2235
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
2236
|
+
const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
|
|
2237
|
+
const tools = this.convertToolsForResponses(options.tools);
|
|
2238
|
+
const response = await this.client.responses.create({
|
|
2239
|
+
model,
|
|
2240
|
+
input,
|
|
2241
|
+
instructions: instructions ?? void 0,
|
|
2242
|
+
tools,
|
|
2243
|
+
max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
2244
|
+
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
2245
|
+
store: false
|
|
2246
|
+
});
|
|
2247
|
+
let content = "";
|
|
2248
|
+
const toolCalls = [];
|
|
2249
|
+
for (const item of response.output) {
|
|
2250
|
+
if (item.type === "message") {
|
|
2251
|
+
for (const part of item.content) {
|
|
2252
|
+
if (part.type === "output_text") {
|
|
2253
|
+
content += part.text;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
} else if (item.type === "function_call") {
|
|
2257
|
+
toolCalls.push({
|
|
2258
|
+
id: item.call_id,
|
|
2259
|
+
name: item.name,
|
|
2260
|
+
input: this.parseResponsesArguments(item.arguments)
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return {
|
|
2265
|
+
id: response.id,
|
|
2266
|
+
content,
|
|
2267
|
+
stopReason: toolCalls.length > 0 ? "tool_use" : "end_turn",
|
|
2268
|
+
usage: {
|
|
2269
|
+
inputTokens: response.usage?.input_tokens ?? 0,
|
|
2270
|
+
outputTokens: response.usage?.output_tokens ?? 0
|
|
2271
|
+
},
|
|
2272
|
+
model: String(response.model),
|
|
2273
|
+
toolCalls
|
|
2274
|
+
};
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
throw this.handleError(error);
|
|
2277
|
+
}
|
|
2278
|
+
}, this.retryConfig);
|
|
2487
2279
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2280
|
+
/**
|
|
2281
|
+
* Stream via Responses API (no tools)
|
|
2282
|
+
*/
|
|
2283
|
+
async *streamViaResponses(messages, options) {
|
|
2284
|
+
this.ensureInitialized();
|
|
2285
|
+
try {
|
|
2286
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
2287
|
+
const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
|
|
2288
|
+
const stream = await this.client.responses.create({
|
|
2289
|
+
model,
|
|
2290
|
+
input,
|
|
2291
|
+
instructions: instructions ?? void 0,
|
|
2292
|
+
max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
2293
|
+
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
2294
|
+
store: false,
|
|
2295
|
+
stream: true
|
|
2296
|
+
});
|
|
2297
|
+
const streamTimeout = this.config.timeout ?? 12e4;
|
|
2298
|
+
let lastActivityTime = Date.now();
|
|
2299
|
+
const timeoutController = new AbortController();
|
|
2300
|
+
const timeoutInterval = setInterval(() => {
|
|
2301
|
+
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
2302
|
+
clearInterval(timeoutInterval);
|
|
2303
|
+
timeoutController.abort();
|
|
2304
|
+
}
|
|
2305
|
+
}, 5e3);
|
|
2306
|
+
timeoutController.signal.addEventListener(
|
|
2307
|
+
"abort",
|
|
2308
|
+
() => stream.controller?.abort(),
|
|
2309
|
+
{ once: true }
|
|
2310
|
+
);
|
|
2311
|
+
try {
|
|
2312
|
+
for await (const event of stream) {
|
|
2313
|
+
lastActivityTime = Date.now();
|
|
2314
|
+
if (event.type === "response.output_text.delta") {
|
|
2315
|
+
yield { type: "text", text: event.delta };
|
|
2316
|
+
} else if (event.type === "response.completed") {
|
|
2317
|
+
yield { type: "done", stopReason: "end_turn" };
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
} finally {
|
|
2321
|
+
clearInterval(timeoutInterval);
|
|
2322
|
+
}
|
|
2323
|
+
if (timeoutController.signal.aborted) {
|
|
2324
|
+
throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
|
|
2325
|
+
}
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
throw this.handleError(error);
|
|
2328
|
+
}
|
|
2494
2329
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2330
|
+
/**
|
|
2331
|
+
* Stream with tools via Responses API
|
|
2332
|
+
*
|
|
2333
|
+
* IMPORTANT: fnCallBuilders is keyed by output item ID (fc.id), NOT by
|
|
2334
|
+
* call_id. The streaming events (function_call_arguments.delta/done) use
|
|
2335
|
+
* item_id which references the output item's id field, not call_id.
|
|
2336
|
+
*/
|
|
2337
|
+
async *streamWithToolsViaResponses(messages, options) {
|
|
2338
|
+
this.ensureInitialized();
|
|
2339
|
+
try {
|
|
2340
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
|
|
2341
|
+
const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
|
|
2342
|
+
const tools = options.tools.length > 0 ? this.convertToolsForResponses(options.tools) : void 0;
|
|
2343
|
+
const requestParams = {
|
|
2344
|
+
model,
|
|
2345
|
+
input,
|
|
2346
|
+
instructions: instructions ?? void 0,
|
|
2347
|
+
max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
2348
|
+
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
2349
|
+
store: false,
|
|
2350
|
+
stream: true
|
|
2351
|
+
};
|
|
2352
|
+
if (tools) {
|
|
2353
|
+
requestParams.tools = tools;
|
|
2354
|
+
}
|
|
2355
|
+
const stream = await this.client.responses.create(
|
|
2356
|
+
requestParams
|
|
2357
|
+
);
|
|
2358
|
+
const fnCallBuilders = /* @__PURE__ */ new Map();
|
|
2359
|
+
const streamTimeout = this.config.timeout ?? 12e4;
|
|
2360
|
+
let lastActivityTime = Date.now();
|
|
2361
|
+
const timeoutController = new AbortController();
|
|
2362
|
+
const timeoutInterval = setInterval(() => {
|
|
2363
|
+
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
2364
|
+
clearInterval(timeoutInterval);
|
|
2365
|
+
timeoutController.abort();
|
|
2366
|
+
}
|
|
2367
|
+
}, 5e3);
|
|
2368
|
+
timeoutController.signal.addEventListener(
|
|
2369
|
+
"abort",
|
|
2370
|
+
() => stream.controller?.abort(),
|
|
2371
|
+
{ once: true }
|
|
2372
|
+
);
|
|
2373
|
+
try {
|
|
2374
|
+
for await (const event of stream) {
|
|
2375
|
+
lastActivityTime = Date.now();
|
|
2376
|
+
switch (event.type) {
|
|
2377
|
+
case "response.output_text.delta":
|
|
2378
|
+
yield { type: "text", text: event.delta };
|
|
2379
|
+
break;
|
|
2380
|
+
case "response.output_item.added":
|
|
2381
|
+
if (event.item.type === "function_call") {
|
|
2382
|
+
const fc = event.item;
|
|
2383
|
+
const itemKey = fc.id ?? fc.call_id;
|
|
2384
|
+
fnCallBuilders.set(itemKey, {
|
|
2385
|
+
callId: fc.call_id,
|
|
2386
|
+
name: fc.name,
|
|
2387
|
+
arguments: ""
|
|
2388
|
+
});
|
|
2389
|
+
yield {
|
|
2390
|
+
type: "tool_use_start",
|
|
2391
|
+
toolCall: { id: fc.call_id, name: fc.name }
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
break;
|
|
2395
|
+
case "response.function_call_arguments.delta":
|
|
2396
|
+
{
|
|
2397
|
+
const builder = fnCallBuilders.get(event.item_id);
|
|
2398
|
+
if (builder) {
|
|
2399
|
+
builder.arguments += event.delta;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
break;
|
|
2403
|
+
case "response.function_call_arguments.done":
|
|
2404
|
+
{
|
|
2405
|
+
const builder = fnCallBuilders.get(event.item_id);
|
|
2406
|
+
if (builder) {
|
|
2407
|
+
yield {
|
|
2408
|
+
type: "tool_use_end",
|
|
2409
|
+
toolCall: {
|
|
2410
|
+
id: builder.callId,
|
|
2411
|
+
name: builder.name,
|
|
2412
|
+
input: this.parseResponsesArguments(event.arguments)
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
fnCallBuilders.delete(event.item_id);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
break;
|
|
2419
|
+
case "response.completed":
|
|
2420
|
+
{
|
|
2421
|
+
for (const [, builder] of fnCallBuilders) {
|
|
2422
|
+
yield {
|
|
2423
|
+
type: "tool_use_end",
|
|
2424
|
+
toolCall: {
|
|
2425
|
+
id: builder.callId,
|
|
2426
|
+
name: builder.name,
|
|
2427
|
+
input: this.parseResponsesArguments(builder.arguments)
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
fnCallBuilders.clear();
|
|
2432
|
+
const hasToolCalls = event.response.output.some(
|
|
2433
|
+
(i) => i.type === "function_call"
|
|
2434
|
+
);
|
|
2435
|
+
yield {
|
|
2436
|
+
type: "done",
|
|
2437
|
+
stopReason: hasToolCalls ? "tool_use" : "end_turn"
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
} finally {
|
|
2444
|
+
clearInterval(timeoutInterval);
|
|
2445
|
+
}
|
|
2446
|
+
if (timeoutController.signal.aborted) {
|
|
2447
|
+
throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
|
|
2448
|
+
}
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
throw this.handleError(error);
|
|
2451
|
+
}
|
|
2501
2452
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2453
|
+
// --- Responses API conversion helpers ---
|
|
2454
|
+
/**
|
|
2455
|
+
* Convert internal messages to Responses API input format.
|
|
2456
|
+
*
|
|
2457
|
+
* The Responses API uses a flat array of input items instead of the
|
|
2458
|
+
* chat completions messages array.
|
|
2459
|
+
*/
|
|
2460
|
+
convertToResponsesInput(messages, systemPrompt) {
|
|
2461
|
+
const input = [];
|
|
2462
|
+
let instructions = null;
|
|
2463
|
+
if (systemPrompt) {
|
|
2464
|
+
instructions = systemPrompt;
|
|
2465
|
+
}
|
|
2466
|
+
for (const msg of messages) {
|
|
2467
|
+
if (msg.role === "system") {
|
|
2468
|
+
instructions = (instructions ? instructions + "\n\n" : "") + this.contentToString(msg.content);
|
|
2469
|
+
} else if (msg.role === "user") {
|
|
2470
|
+
if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
|
|
2471
|
+
for (const block of msg.content) {
|
|
2472
|
+
if (block.type === "tool_result") {
|
|
2473
|
+
const tr = block;
|
|
2474
|
+
input.push({
|
|
2475
|
+
type: "function_call_output",
|
|
2476
|
+
call_id: tr.tool_use_id,
|
|
2477
|
+
output: tr.content
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
} else if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "image")) {
|
|
2482
|
+
const parts = [];
|
|
2483
|
+
for (const block of msg.content) {
|
|
2484
|
+
if (block.type === "text") {
|
|
2485
|
+
parts.push({ type: "input_text", text: block.text });
|
|
2486
|
+
} else if (block.type === "image") {
|
|
2487
|
+
const imgBlock = block;
|
|
2488
|
+
parts.push({
|
|
2489
|
+
type: "input_image",
|
|
2490
|
+
image_url: `data:${imgBlock.source.media_type};base64,${imgBlock.source.data}`,
|
|
2491
|
+
detail: "auto"
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
input.push({
|
|
2496
|
+
role: "user",
|
|
2497
|
+
content: parts
|
|
2498
|
+
});
|
|
2499
|
+
} else {
|
|
2500
|
+
input.push({
|
|
2501
|
+
role: "user",
|
|
2502
|
+
content: this.contentToString(msg.content)
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
} else if (msg.role === "assistant") {
|
|
2506
|
+
if (typeof msg.content === "string") {
|
|
2507
|
+
input.push({ role: "assistant", content: msg.content });
|
|
2508
|
+
} else if (Array.isArray(msg.content)) {
|
|
2509
|
+
const textParts = [];
|
|
2510
|
+
for (const block of msg.content) {
|
|
2511
|
+
if (block.type === "text") {
|
|
2512
|
+
textParts.push(block.text);
|
|
2513
|
+
} else if (block.type === "tool_use") {
|
|
2514
|
+
if (textParts.length > 0) {
|
|
2515
|
+
input.push({ role: "assistant", content: textParts.join("") });
|
|
2516
|
+
textParts.length = 0;
|
|
2517
|
+
}
|
|
2518
|
+
input.push({
|
|
2519
|
+
type: "function_call",
|
|
2520
|
+
call_id: block.id,
|
|
2521
|
+
name: block.name,
|
|
2522
|
+
arguments: JSON.stringify(block.input)
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
if (textParts.length > 0) {
|
|
2527
|
+
input.push({ role: "assistant", content: textParts.join("") });
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2524
2530
|
}
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
reject(err);
|
|
2531
|
+
}
|
|
2532
|
+
return { input, instructions };
|
|
2528
2533
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2534
|
+
/**
|
|
2535
|
+
* Convert tool definitions to Responses API FunctionTool format
|
|
2536
|
+
*/
|
|
2537
|
+
convertToolsForResponses(tools) {
|
|
2538
|
+
return tools.map((tool) => ({
|
|
2539
|
+
type: "function",
|
|
2540
|
+
name: tool.name,
|
|
2541
|
+
description: tool.description ?? void 0,
|
|
2542
|
+
parameters: tool.input_schema ?? null,
|
|
2543
|
+
strict: false
|
|
2544
|
+
}));
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Parse tool call arguments with jsonrepair fallback (Responses API)
|
|
2548
|
+
*/
|
|
2549
|
+
parseResponsesArguments(args) {
|
|
2550
|
+
try {
|
|
2551
|
+
return args ? JSON.parse(args) : {};
|
|
2552
|
+
} catch {
|
|
2553
|
+
try {
|
|
2554
|
+
if (args) {
|
|
2555
|
+
const repaired = jsonrepair(args);
|
|
2556
|
+
return JSON.parse(repaired);
|
|
2557
|
+
}
|
|
2558
|
+
} catch {
|
|
2559
|
+
console.error(`[${this.name}] Cannot parse tool arguments: ${args.slice(0, 200)}`);
|
|
2560
|
+
}
|
|
2561
|
+
return {};
|
|
2562
|
+
}
|
|
2538
2563
|
}
|
|
2539
|
-
});
|
|
2540
|
-
});
|
|
2541
|
-
const timeoutId = setTimeout(() => {
|
|
2542
|
-
server.close();
|
|
2543
|
-
rejectResult(new Error("Authentication timed out. Please try again."));
|
|
2544
|
-
}, timeout);
|
|
2545
|
-
server.on("close", () => {
|
|
2546
|
-
clearTimeout(timeoutId);
|
|
2547
|
-
});
|
|
2548
|
-
return { port: actualPort, resultPromise };
|
|
2549
|
-
}
|
|
2550
|
-
var OAUTH_CALLBACK_PORT, SUCCESS_HTML, ERROR_HTML;
|
|
2551
|
-
var init_callback_server = __esm({
|
|
2552
|
-
"src/auth/callback-server.ts"() {
|
|
2553
|
-
OAUTH_CALLBACK_PORT = 1455;
|
|
2554
|
-
SUCCESS_HTML = `
|
|
2555
|
-
<!DOCTYPE html>
|
|
2556
|
-
<html>
|
|
2557
|
-
<head>
|
|
2558
|
-
<meta charset="utf-8">
|
|
2559
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2560
|
-
<title>Authentication Successful</title>
|
|
2561
|
-
<style>
|
|
2562
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2563
|
-
body {
|
|
2564
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
2565
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2566
|
-
min-height: 100vh;
|
|
2567
|
-
display: flex;
|
|
2568
|
-
align-items: center;
|
|
2569
|
-
justify-content: center;
|
|
2570
|
-
}
|
|
2571
|
-
.container {
|
|
2572
|
-
background: white;
|
|
2573
|
-
border-radius: 16px;
|
|
2574
|
-
padding: 48px;
|
|
2575
|
-
text-align: center;
|
|
2576
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
2577
|
-
max-width: 400px;
|
|
2578
|
-
}
|
|
2579
|
-
.checkmark {
|
|
2580
|
-
width: 80px;
|
|
2581
|
-
height: 80px;
|
|
2582
|
-
margin: 0 auto 24px;
|
|
2583
|
-
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
2584
|
-
border-radius: 50%;
|
|
2585
|
-
display: flex;
|
|
2586
|
-
align-items: center;
|
|
2587
|
-
justify-content: center;
|
|
2588
|
-
}
|
|
2589
|
-
.checkmark svg {
|
|
2590
|
-
width: 40px;
|
|
2591
|
-
height: 40px;
|
|
2592
|
-
stroke: white;
|
|
2593
|
-
stroke-width: 3;
|
|
2594
|
-
fill: none;
|
|
2595
|
-
}
|
|
2596
|
-
h1 {
|
|
2597
|
-
font-size: 24px;
|
|
2598
|
-
font-weight: 600;
|
|
2599
|
-
color: #1f2937;
|
|
2600
|
-
margin-bottom: 12px;
|
|
2601
|
-
}
|
|
2602
|
-
p {
|
|
2603
|
-
color: #6b7280;
|
|
2604
|
-
font-size: 16px;
|
|
2605
|
-
line-height: 1.5;
|
|
2606
|
-
}
|
|
2607
|
-
.brand {
|
|
2608
|
-
margin-top: 24px;
|
|
2609
|
-
padding-top: 24px;
|
|
2610
|
-
border-top: 1px solid #e5e7eb;
|
|
2611
|
-
color: #9ca3af;
|
|
2612
|
-
font-size: 14px;
|
|
2613
|
-
}
|
|
2614
|
-
.brand strong {
|
|
2615
|
-
color: #667eea;
|
|
2616
|
-
}
|
|
2617
|
-
</style>
|
|
2618
|
-
</head>
|
|
2619
|
-
<body>
|
|
2620
|
-
<div class="container">
|
|
2621
|
-
<div class="checkmark">
|
|
2622
|
-
<svg viewBox="0 0 24 24">
|
|
2623
|
-
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
2624
|
-
</svg>
|
|
2625
|
-
</div>
|
|
2626
|
-
<h1>Authentication Successful!</h1>
|
|
2627
|
-
<p>You can close this window and return to your terminal.</p>
|
|
2628
|
-
<div class="brand">
|
|
2629
|
-
Powered by <strong>Corbat-Coco</strong>
|
|
2630
|
-
</div>
|
|
2631
|
-
</div>
|
|
2632
|
-
<script>
|
|
2633
|
-
// Auto-close after 3 seconds
|
|
2634
|
-
setTimeout(() => window.close(), 3000);
|
|
2635
|
-
</script>
|
|
2636
|
-
</body>
|
|
2637
|
-
</html>
|
|
2638
|
-
`;
|
|
2639
|
-
ERROR_HTML = (error) => {
|
|
2640
|
-
const safeError = escapeHtml(error);
|
|
2641
|
-
return `
|
|
2642
|
-
<!DOCTYPE html>
|
|
2643
|
-
<html>
|
|
2644
|
-
<head>
|
|
2645
|
-
<meta charset="utf-8">
|
|
2646
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2647
|
-
<title>Authentication Failed</title>
|
|
2648
|
-
<style>
|
|
2649
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2650
|
-
body {
|
|
2651
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
2652
|
-
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
2653
|
-
min-height: 100vh;
|
|
2654
|
-
display: flex;
|
|
2655
|
-
align-items: center;
|
|
2656
|
-
justify-content: center;
|
|
2657
|
-
}
|
|
2658
|
-
.container {
|
|
2659
|
-
background: white;
|
|
2660
|
-
border-radius: 16px;
|
|
2661
|
-
padding: 48px;
|
|
2662
|
-
text-align: center;
|
|
2663
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
2664
|
-
max-width: 400px;
|
|
2665
|
-
}
|
|
2666
|
-
.icon {
|
|
2667
|
-
width: 80px;
|
|
2668
|
-
height: 80px;
|
|
2669
|
-
margin: 0 auto 24px;
|
|
2670
|
-
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
2671
|
-
border-radius: 50%;
|
|
2672
|
-
display: flex;
|
|
2673
|
-
align-items: center;
|
|
2674
|
-
justify-content: center;
|
|
2675
|
-
}
|
|
2676
|
-
.icon svg {
|
|
2677
|
-
width: 40px;
|
|
2678
|
-
height: 40px;
|
|
2679
|
-
stroke: white;
|
|
2680
|
-
stroke-width: 3;
|
|
2681
|
-
fill: none;
|
|
2682
|
-
}
|
|
2683
|
-
h1 {
|
|
2684
|
-
font-size: 24px;
|
|
2685
|
-
font-weight: 600;
|
|
2686
|
-
color: #1f2937;
|
|
2687
|
-
margin-bottom: 12px;
|
|
2688
|
-
}
|
|
2689
|
-
p {
|
|
2690
|
-
color: #6b7280;
|
|
2691
|
-
font-size: 16px;
|
|
2692
|
-
line-height: 1.5;
|
|
2693
|
-
}
|
|
2694
|
-
.error {
|
|
2695
|
-
margin-top: 16px;
|
|
2696
|
-
padding: 12px;
|
|
2697
|
-
background: #fef2f2;
|
|
2698
|
-
border-radius: 8px;
|
|
2699
|
-
color: #dc2626;
|
|
2700
|
-
font-family: monospace;
|
|
2701
|
-
font-size: 14px;
|
|
2702
|
-
}
|
|
2703
|
-
</style>
|
|
2704
|
-
</head>
|
|
2705
|
-
<body>
|
|
2706
|
-
<div class="container">
|
|
2707
|
-
<div class="icon">
|
|
2708
|
-
<svg viewBox="0 0 24 24">
|
|
2709
|
-
<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
2710
|
-
</svg>
|
|
2711
|
-
</div>
|
|
2712
|
-
<h1>Authentication Failed</h1>
|
|
2713
|
-
<p>Something went wrong. Please try again.</p>
|
|
2714
|
-
<div class="error">${safeError}</div>
|
|
2715
|
-
</div>
|
|
2716
|
-
</body>
|
|
2717
|
-
</html>
|
|
2718
|
-
`;
|
|
2719
2564
|
};
|
|
2720
2565
|
}
|
|
2721
2566
|
});
|
|
2722
|
-
function
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
} catch {
|
|
2727
|
-
return false;
|
|
2567
|
+
async function requestDeviceCode(provider) {
|
|
2568
|
+
const config = OAUTH_CONFIGS[provider];
|
|
2569
|
+
if (!config) {
|
|
2570
|
+
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
2728
2571
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
isWSL = detectWSL();
|
|
2572
|
+
if (!config.deviceAuthEndpoint) {
|
|
2573
|
+
throw new Error(
|
|
2574
|
+
`Device code flow not supported for provider: ${provider}. Use browser OAuth instead.`
|
|
2575
|
+
);
|
|
2734
2576
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2577
|
+
const body = new URLSearchParams({
|
|
2578
|
+
client_id: config.clientId,
|
|
2579
|
+
scope: config.scopes.join(" ")
|
|
2580
|
+
});
|
|
2581
|
+
if (provider === "openai") {
|
|
2582
|
+
body.set("audience", "https://api.openai.com/v1");
|
|
2583
|
+
}
|
|
2584
|
+
const response = await fetch(config.deviceAuthEndpoint, {
|
|
2738
2585
|
method: "POST",
|
|
2739
2586
|
headers: {
|
|
2740
|
-
"Content-Type": "application/
|
|
2587
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2588
|
+
"User-Agent": "Corbat-Coco CLI",
|
|
2741
2589
|
Accept: "application/json"
|
|
2742
2590
|
},
|
|
2743
|
-
body:
|
|
2744
|
-
client_id: COPILOT_CLIENT_ID,
|
|
2745
|
-
scope: "read:user"
|
|
2746
|
-
})
|
|
2591
|
+
body: body.toString()
|
|
2747
2592
|
});
|
|
2748
2593
|
if (!response.ok) {
|
|
2594
|
+
const contentType2 = response.headers.get("content-type") || "";
|
|
2749
2595
|
const error = await response.text();
|
|
2750
|
-
|
|
2596
|
+
if (contentType2.includes("text/html") || error.includes("<!DOCTYPE") || error.includes("<html")) {
|
|
2597
|
+
throw new Error(
|
|
2598
|
+
"OAuth request blocked (possibly by Cloudflare).\n This can happen due to network restrictions or rate limiting.\n Please try:\n 1. Use an API key instead (recommended)\n 2. Wait a few minutes and try again\n 3. Try from a different network"
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
throw new Error(`Failed to request device code: ${error}`);
|
|
2602
|
+
}
|
|
2603
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2604
|
+
if (!contentType.includes("application/json")) {
|
|
2605
|
+
const text13 = await response.text();
|
|
2606
|
+
if (text13.includes("<!DOCTYPE") || text13.includes("<html")) {
|
|
2607
|
+
throw new Error(
|
|
2608
|
+
"OAuth service returned HTML instead of JSON.\n The service may be temporarily unavailable.\n Please use an API key instead, or try again later."
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
const data = await response.json();
|
|
2613
|
+
return {
|
|
2614
|
+
deviceCode: data.device_code,
|
|
2615
|
+
userCode: data.user_code,
|
|
2616
|
+
verificationUri: data.verification_uri || config.verificationUri || "",
|
|
2617
|
+
verificationUriComplete: data.verification_uri_complete,
|
|
2618
|
+
expiresIn: data.expires_in,
|
|
2619
|
+
interval: data.interval || 5
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
async function pollForToken(provider, deviceCode, interval, expiresIn, onPoll) {
|
|
2623
|
+
const config = OAUTH_CONFIGS[provider];
|
|
2624
|
+
if (!config) {
|
|
2625
|
+
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
2751
2626
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
async function pollGitHubForToken(deviceCode, interval, expiresIn, onPoll) {
|
|
2755
|
-
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
2627
|
+
const startTime = Date.now();
|
|
2628
|
+
const expiresAt = startTime + expiresIn * 1e3;
|
|
2756
2629
|
while (Date.now() < expiresAt) {
|
|
2757
2630
|
await new Promise((resolve4) => setTimeout(resolve4, interval * 1e3));
|
|
2758
2631
|
if (onPoll) onPoll();
|
|
2759
|
-
const
|
|
2632
|
+
const body = new URLSearchParams({
|
|
2633
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
2634
|
+
client_id: config.clientId,
|
|
2635
|
+
device_code: deviceCode
|
|
2636
|
+
});
|
|
2637
|
+
const response = await fetch(config.tokenEndpoint, {
|
|
2760
2638
|
method: "POST",
|
|
2761
2639
|
headers: {
|
|
2762
|
-
"Content-Type": "application/
|
|
2763
|
-
Accept: "application/json"
|
|
2640
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2764
2641
|
},
|
|
2765
|
-
body:
|
|
2766
|
-
client_id: COPILOT_CLIENT_ID,
|
|
2767
|
-
device_code: deviceCode,
|
|
2768
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
2769
|
-
})
|
|
2642
|
+
body: body.toString()
|
|
2770
2643
|
});
|
|
2771
2644
|
const data = await response.json();
|
|
2772
2645
|
if (data.access_token) {
|
|
2773
|
-
return
|
|
2646
|
+
return {
|
|
2647
|
+
accessToken: data.access_token,
|
|
2648
|
+
refreshToken: data.refresh_token,
|
|
2649
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
|
|
2650
|
+
tokenType: data.token_type || "Bearer"
|
|
2651
|
+
};
|
|
2774
2652
|
}
|
|
2775
2653
|
if (data.error === "authorization_pending") {
|
|
2776
2654
|
continue;
|
|
@@ -2787,1625 +2665,1817 @@ async function pollGitHubForToken(deviceCode, interval, expiresIn, onPoll) {
|
|
|
2787
2665
|
}
|
|
2788
2666
|
throw new Error("Authentication timed out. Please try again.");
|
|
2789
2667
|
}
|
|
2790
|
-
async function
|
|
2791
|
-
const
|
|
2792
|
-
|
|
2668
|
+
async function refreshAccessToken(provider, refreshToken) {
|
|
2669
|
+
const config = OAUTH_CONFIGS[provider];
|
|
2670
|
+
if (!config) {
|
|
2671
|
+
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
2672
|
+
}
|
|
2673
|
+
const body = new URLSearchParams({
|
|
2674
|
+
grant_type: "refresh_token",
|
|
2675
|
+
client_id: config.clientId,
|
|
2676
|
+
refresh_token: refreshToken
|
|
2677
|
+
});
|
|
2678
|
+
const response = await fetch(config.tokenEndpoint, {
|
|
2679
|
+
method: "POST",
|
|
2793
2680
|
headers: {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
}
|
|
2681
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2682
|
+
},
|
|
2683
|
+
body: body.toString()
|
|
2798
2684
|
});
|
|
2799
2685
|
if (!response.ok) {
|
|
2800
2686
|
const error = await response.text();
|
|
2801
|
-
|
|
2802
|
-
throw new CopilotAuthError(
|
|
2803
|
-
"GitHub token is invalid or expired. Please re-authenticate with /provider copilot.",
|
|
2804
|
-
true
|
|
2805
|
-
);
|
|
2806
|
-
}
|
|
2807
|
-
if (response.status === 403) {
|
|
2808
|
-
throw new CopilotAuthError(
|
|
2809
|
-
"GitHub Copilot is not enabled for this account.\n Please ensure you have an active Copilot subscription:\n https://github.com/settings/copilot",
|
|
2810
|
-
true
|
|
2811
|
-
);
|
|
2812
|
-
}
|
|
2813
|
-
throw new Error(`Copilot token exchange failed: ${response.status} - ${error}`);
|
|
2814
|
-
}
|
|
2815
|
-
return await response.json();
|
|
2816
|
-
}
|
|
2817
|
-
function getCopilotBaseUrl(accountType) {
|
|
2818
|
-
if (accountType && accountType in COPILOT_BASE_URLS) {
|
|
2819
|
-
return COPILOT_BASE_URLS[accountType];
|
|
2687
|
+
throw new Error(`Token refresh failed: ${error}`);
|
|
2820
2688
|
}
|
|
2821
|
-
|
|
2689
|
+
const data = await response.json();
|
|
2690
|
+
return {
|
|
2691
|
+
accessToken: data.access_token,
|
|
2692
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
2693
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
|
|
2694
|
+
tokenType: data.token_type
|
|
2695
|
+
};
|
|
2822
2696
|
}
|
|
2823
|
-
function
|
|
2697
|
+
function getTokenStoragePath(provider) {
|
|
2824
2698
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2825
|
-
return path36.join(home, ".coco", "tokens",
|
|
2699
|
+
return path36.join(home, ".coco", "tokens", `${provider}.json`);
|
|
2826
2700
|
}
|
|
2827
|
-
async function
|
|
2828
|
-
const filePath =
|
|
2701
|
+
async function saveTokens(provider, tokens) {
|
|
2702
|
+
const filePath = getTokenStoragePath(provider);
|
|
2829
2703
|
const dir = path36.dirname(filePath);
|
|
2830
|
-
await fs34.mkdir(dir, { recursive: true, mode: 448 });
|
|
2831
|
-
await fs34.writeFile(filePath, JSON.stringify(
|
|
2832
|
-
}
|
|
2833
|
-
async function
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
const
|
|
2837
|
-
return
|
|
2838
|
-
} catch {
|
|
2839
|
-
return null;
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
async function deleteCopilotCredentials() {
|
|
2843
|
-
try {
|
|
2844
|
-
await fs34.unlink(getCopilotCredentialsPath());
|
|
2845
|
-
} catch {
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
function isCopilotTokenExpired(creds) {
|
|
2849
|
-
if (!creds.copilotToken || !creds.copilotTokenExpiresAt) return true;
|
|
2850
|
-
return Date.now() >= creds.copilotTokenExpiresAt - REFRESH_BUFFER_MS;
|
|
2851
|
-
}
|
|
2852
|
-
async function getValidCopilotToken() {
|
|
2853
|
-
const creds = await loadCopilotCredentials();
|
|
2854
|
-
if (!creds) return null;
|
|
2855
|
-
const envToken = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
|
|
2856
|
-
const githubToken = envToken || creds.githubToken;
|
|
2857
|
-
if (!isCopilotTokenExpired(creds) && creds.copilotToken) {
|
|
2858
|
-
return {
|
|
2859
|
-
token: creds.copilotToken,
|
|
2860
|
-
baseUrl: getCopilotBaseUrl(creds.accountType),
|
|
2861
|
-
isNew: false
|
|
2862
|
-
};
|
|
2863
|
-
}
|
|
2864
|
-
try {
|
|
2865
|
-
const copilotToken = await exchangeForCopilotToken(githubToken);
|
|
2866
|
-
const updatedCreds = {
|
|
2867
|
-
...creds,
|
|
2868
|
-
githubToken: creds.githubToken,
|
|
2869
|
-
copilotToken: copilotToken.token,
|
|
2870
|
-
copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
|
|
2871
|
-
accountType: copilotToken.annotations?.copilot_plan ?? creds.accountType
|
|
2872
|
-
};
|
|
2873
|
-
await saveCopilotCredentials(updatedCreds);
|
|
2874
|
-
return {
|
|
2875
|
-
token: copilotToken.token,
|
|
2876
|
-
baseUrl: getCopilotBaseUrl(updatedCreds.accountType),
|
|
2877
|
-
isNew: true
|
|
2878
|
-
};
|
|
2879
|
-
} catch (error) {
|
|
2880
|
-
if (error instanceof CopilotAuthError && error.permanent) {
|
|
2881
|
-
await deleteCopilotCredentials();
|
|
2882
|
-
return null;
|
|
2883
|
-
}
|
|
2884
|
-
throw error;
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
var COPILOT_CLIENT_ID, GITHUB_DEVICE_CODE_URL, GITHUB_TOKEN_URL, COPILOT_TOKEN_URL, COPILOT_BASE_URLS, DEFAULT_COPILOT_BASE_URL, REFRESH_BUFFER_MS, CopilotAuthError, CopilotCredentialsSchema;
|
|
2888
|
-
var init_copilot = __esm({
|
|
2889
|
-
"src/auth/copilot.ts"() {
|
|
2890
|
-
COPILOT_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
2891
|
-
GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
2892
|
-
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
2893
|
-
COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
|
|
2894
|
-
COPILOT_BASE_URLS = {
|
|
2895
|
-
individual: "https://api.githubcopilot.com",
|
|
2896
|
-
business: "https://api.business.githubcopilot.com",
|
|
2897
|
-
enterprise: "https://api.enterprise.githubcopilot.com"
|
|
2898
|
-
};
|
|
2899
|
-
DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
2900
|
-
REFRESH_BUFFER_MS = 6e4;
|
|
2901
|
-
CopilotAuthError = class extends Error {
|
|
2902
|
-
constructor(message, permanent) {
|
|
2903
|
-
super(message);
|
|
2904
|
-
this.permanent = permanent;
|
|
2905
|
-
this.name = "CopilotAuthError";
|
|
2906
|
-
}
|
|
2907
|
-
};
|
|
2908
|
-
CopilotCredentialsSchema = z.object({
|
|
2909
|
-
githubToken: z.string().min(1),
|
|
2910
|
-
copilotToken: z.string().optional(),
|
|
2911
|
-
copilotTokenExpiresAt: z.number().optional(),
|
|
2912
|
-
accountType: z.string().optional()
|
|
2913
|
-
});
|
|
2914
|
-
}
|
|
2915
|
-
});
|
|
2916
|
-
function getOAuthProviderName(provider) {
|
|
2917
|
-
if (provider === "codex") return "openai";
|
|
2918
|
-
return provider;
|
|
2919
|
-
}
|
|
2920
|
-
function getProviderDisplayInfo(provider) {
|
|
2921
|
-
const oauthProvider = getOAuthProviderName(provider);
|
|
2922
|
-
switch (oauthProvider) {
|
|
2923
|
-
case "openai":
|
|
2924
|
-
return {
|
|
2925
|
-
name: "OpenAI",
|
|
2926
|
-
emoji: "\u{1F7E2}",
|
|
2927
|
-
authDescription: "Sign in with your ChatGPT account",
|
|
2928
|
-
apiKeyUrl: "https://platform.openai.com/api-keys"
|
|
2929
|
-
};
|
|
2930
|
-
case "copilot":
|
|
2931
|
-
return {
|
|
2932
|
-
name: "GitHub Copilot",
|
|
2933
|
-
emoji: "\u{1F419}",
|
|
2934
|
-
authDescription: "Sign in with your GitHub account",
|
|
2935
|
-
apiKeyUrl: "https://github.com/settings/copilot"
|
|
2936
|
-
};
|
|
2937
|
-
default:
|
|
2938
|
-
return {
|
|
2939
|
-
name: provider,
|
|
2940
|
-
emoji: "\u{1F510}",
|
|
2941
|
-
authDescription: "Sign in with your account",
|
|
2942
|
-
apiKeyUrl: ""
|
|
2943
|
-
};
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
function supportsOAuth(provider) {
|
|
2947
|
-
if (provider === "copilot") return true;
|
|
2948
|
-
const oauthProvider = getOAuthProviderName(provider);
|
|
2949
|
-
return oauthProvider in OAUTH_CONFIGS;
|
|
2950
|
-
}
|
|
2951
|
-
async function isOAuthConfigured(provider) {
|
|
2952
|
-
if (provider === "copilot") {
|
|
2953
|
-
const creds = await loadCopilotCredentials();
|
|
2954
|
-
return creds !== null;
|
|
2955
|
-
}
|
|
2956
|
-
const oauthProvider = getOAuthProviderName(provider);
|
|
2957
|
-
const tokens = await loadTokens(oauthProvider);
|
|
2958
|
-
return tokens !== null;
|
|
2959
|
-
}
|
|
2960
|
-
function printAuthUrl(url) {
|
|
2961
|
-
try {
|
|
2962
|
-
const parsed = new URL(url);
|
|
2963
|
-
const maskedParams = new URLSearchParams(parsed.searchParams);
|
|
2964
|
-
if (maskedParams.has("client_id")) {
|
|
2965
|
-
const clientId = maskedParams.get("client_id");
|
|
2966
|
-
maskedParams.set("client_id", clientId.slice(0, 8) + "...");
|
|
2967
|
-
}
|
|
2968
|
-
parsed.search = maskedParams.toString();
|
|
2969
|
-
console.log(chalk2.cyan(` ${parsed.toString()}`));
|
|
2970
|
-
} catch {
|
|
2971
|
-
console.log(chalk2.cyan(" [invalid URL]"));
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
async function openBrowser(url) {
|
|
2975
|
-
let sanitizedUrl;
|
|
2976
|
-
try {
|
|
2977
|
-
const parsed = new URL(url);
|
|
2978
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
2979
|
-
return false;
|
|
2980
|
-
}
|
|
2981
|
-
sanitizedUrl = parsed.toString();
|
|
2982
|
-
} catch {
|
|
2983
|
-
return false;
|
|
2984
|
-
}
|
|
2985
|
-
const platform = process.platform;
|
|
2986
|
-
try {
|
|
2987
|
-
if (platform === "darwin") {
|
|
2988
|
-
await execFileAsync("open", [sanitizedUrl]);
|
|
2989
|
-
} else if (platform === "win32") {
|
|
2990
|
-
await execFileAsync("rundll32", ["url.dll,FileProtocolHandler", sanitizedUrl]);
|
|
2991
|
-
} else if (isWSL) {
|
|
2992
|
-
await execFileAsync("cmd.exe", ["/c", "start", "", sanitizedUrl]);
|
|
2993
|
-
} else {
|
|
2994
|
-
await execFileAsync("xdg-open", [sanitizedUrl]);
|
|
2995
|
-
}
|
|
2996
|
-
return true;
|
|
2704
|
+
await fs34.mkdir(dir, { recursive: true, mode: 448 });
|
|
2705
|
+
await fs34.writeFile(filePath, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
2706
|
+
}
|
|
2707
|
+
async function loadTokens(provider) {
|
|
2708
|
+
const filePath = getTokenStoragePath(provider);
|
|
2709
|
+
try {
|
|
2710
|
+
const content = await fs34.readFile(filePath, "utf-8");
|
|
2711
|
+
return JSON.parse(content);
|
|
2997
2712
|
} catch {
|
|
2998
|
-
return
|
|
2713
|
+
return null;
|
|
2999
2714
|
}
|
|
3000
2715
|
}
|
|
3001
|
-
async function
|
|
3002
|
-
|
|
2716
|
+
async function deleteTokens(provider) {
|
|
2717
|
+
const filePath = getTokenStoragePath(provider);
|
|
3003
2718
|
try {
|
|
3004
|
-
|
|
3005
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
3006
|
-
return false;
|
|
3007
|
-
}
|
|
3008
|
-
sanitizedUrl = parsed.toString();
|
|
2719
|
+
await fs34.unlink(filePath);
|
|
3009
2720
|
} catch {
|
|
3010
|
-
return false;
|
|
3011
|
-
}
|
|
3012
|
-
const platform = process.platform;
|
|
3013
|
-
const commands2 = [];
|
|
3014
|
-
if (platform === "darwin") {
|
|
3015
|
-
commands2.push(
|
|
3016
|
-
{ cmd: "open", args: [sanitizedUrl] },
|
|
3017
|
-
{ cmd: "open", args: ["-a", "Safari", sanitizedUrl] },
|
|
3018
|
-
{ cmd: "open", args: ["-a", "Google Chrome", sanitizedUrl] }
|
|
3019
|
-
);
|
|
3020
|
-
} else if (platform === "win32") {
|
|
3021
|
-
commands2.push({
|
|
3022
|
-
cmd: "rundll32",
|
|
3023
|
-
args: ["url.dll,FileProtocolHandler", sanitizedUrl]
|
|
3024
|
-
});
|
|
3025
|
-
} else if (isWSL) {
|
|
3026
|
-
commands2.push(
|
|
3027
|
-
{ cmd: "cmd.exe", args: ["/c", "start", "", sanitizedUrl] },
|
|
3028
|
-
{ cmd: "powershell.exe", args: ["-Command", `Start-Process '${sanitizedUrl}'`] },
|
|
3029
|
-
{ cmd: "wslview", args: [sanitizedUrl] }
|
|
3030
|
-
);
|
|
3031
|
-
} else {
|
|
3032
|
-
commands2.push(
|
|
3033
|
-
{ cmd: "xdg-open", args: [sanitizedUrl] },
|
|
3034
|
-
{ cmd: "sensible-browser", args: [sanitizedUrl] },
|
|
3035
|
-
{ cmd: "x-www-browser", args: [sanitizedUrl] },
|
|
3036
|
-
{ cmd: "gnome-open", args: [sanitizedUrl] },
|
|
3037
|
-
{ cmd: "firefox", args: [sanitizedUrl] },
|
|
3038
|
-
{ cmd: "chromium-browser", args: [sanitizedUrl] },
|
|
3039
|
-
{ cmd: "google-chrome", args: [sanitizedUrl] }
|
|
3040
|
-
);
|
|
3041
2721
|
}
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
2722
|
+
}
|
|
2723
|
+
function isTokenExpired(tokens) {
|
|
2724
|
+
if (!tokens.expiresAt) return false;
|
|
2725
|
+
return Date.now() >= tokens.expiresAt - 5 * 60 * 1e3;
|
|
2726
|
+
}
|
|
2727
|
+
async function getValidAccessToken(provider) {
|
|
2728
|
+
const config = OAUTH_CONFIGS[provider];
|
|
2729
|
+
if (!config) return null;
|
|
2730
|
+
const tokens = await loadTokens(provider);
|
|
2731
|
+
if (!tokens) return null;
|
|
2732
|
+
if (isTokenExpired(tokens)) {
|
|
2733
|
+
if (tokens.refreshToken) {
|
|
2734
|
+
try {
|
|
2735
|
+
const newTokens = await refreshAccessToken(provider, tokens.refreshToken);
|
|
2736
|
+
await saveTokens(provider, newTokens);
|
|
2737
|
+
return { accessToken: newTokens.accessToken, isNew: true };
|
|
2738
|
+
} catch {
|
|
2739
|
+
await deleteTokens(provider);
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
3048
2742
|
}
|
|
2743
|
+
await deleteTokens(provider);
|
|
2744
|
+
return null;
|
|
3049
2745
|
}
|
|
3050
|
-
return false;
|
|
2746
|
+
return { accessToken: tokens.accessToken, isNew: false };
|
|
3051
2747
|
}
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
2748
|
+
function buildAuthorizationUrl(provider, redirectUri, codeChallenge, state) {
|
|
2749
|
+
const config = OAUTH_CONFIGS[provider];
|
|
2750
|
+
if (!config) {
|
|
2751
|
+
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
3055
2752
|
}
|
|
3056
|
-
const
|
|
3057
|
-
|
|
2753
|
+
const params = new URLSearchParams({
|
|
2754
|
+
response_type: "code",
|
|
2755
|
+
client_id: config.clientId,
|
|
2756
|
+
redirect_uri: redirectUri,
|
|
2757
|
+
scope: config.scopes.join(" "),
|
|
2758
|
+
code_challenge: codeChallenge,
|
|
2759
|
+
code_challenge_method: "S256",
|
|
2760
|
+
state
|
|
2761
|
+
});
|
|
2762
|
+
if (config.extraAuthParams) {
|
|
2763
|
+
for (const [key, value] of Object.entries(config.extraAuthParams)) {
|
|
2764
|
+
params.set(key, value);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
return `${config.authorizationEndpoint}?${params.toString()}`;
|
|
2768
|
+
}
|
|
2769
|
+
async function exchangeCodeForTokens(provider, code, codeVerifier, redirectUri) {
|
|
2770
|
+
const config = OAUTH_CONFIGS[provider];
|
|
3058
2771
|
if (!config) {
|
|
3059
|
-
|
|
3060
|
-
return null;
|
|
2772
|
+
throw new Error(`OAuth not supported for provider: ${provider}`);
|
|
3061
2773
|
}
|
|
3062
|
-
const
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
{
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
hint: `${displayInfo.authDescription} (recommended)`
|
|
2774
|
+
const body = new URLSearchParams({
|
|
2775
|
+
grant_type: "authorization_code",
|
|
2776
|
+
client_id: config.clientId,
|
|
2777
|
+
code,
|
|
2778
|
+
code_verifier: codeVerifier,
|
|
2779
|
+
redirect_uri: redirectUri
|
|
2780
|
+
});
|
|
2781
|
+
const response = await fetch(config.tokenEndpoint, {
|
|
2782
|
+
method: "POST",
|
|
2783
|
+
headers: {
|
|
2784
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2785
|
+
Accept: "application/json"
|
|
3075
2786
|
},
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
2787
|
+
body: body.toString()
|
|
2788
|
+
});
|
|
2789
|
+
if (!response.ok) {
|
|
2790
|
+
const error = await response.text();
|
|
2791
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
2792
|
+
}
|
|
2793
|
+
const data = await response.json();
|
|
2794
|
+
return {
|
|
2795
|
+
accessToken: data.access_token,
|
|
2796
|
+
refreshToken: data.refresh_token,
|
|
2797
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
|
|
2798
|
+
tokenType: data.token_type || "Bearer"
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
var OAUTH_CONFIGS;
|
|
2802
|
+
var init_oauth = __esm({
|
|
2803
|
+
"src/auth/oauth.ts"() {
|
|
2804
|
+
OAUTH_CONFIGS = {
|
|
2805
|
+
/**
|
|
2806
|
+
* OpenAI OAuth (ChatGPT Plus/Pro subscriptions)
|
|
2807
|
+
* Uses the official Codex client ID (same as OpenCode, Codex CLI, etc.)
|
|
2808
|
+
*/
|
|
2809
|
+
openai: {
|
|
2810
|
+
provider: "openai",
|
|
2811
|
+
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
2812
|
+
authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
|
|
2813
|
+
tokenEndpoint: "https://auth.openai.com/oauth/token",
|
|
2814
|
+
deviceAuthEndpoint: "https://auth.openai.com/oauth/device/code",
|
|
2815
|
+
verificationUri: "https://chatgpt.com/codex/device",
|
|
2816
|
+
scopes: ["openid", "profile", "email", "offline_access"],
|
|
2817
|
+
extraAuthParams: {
|
|
2818
|
+
id_token_add_organizations: "true",
|
|
2819
|
+
codex_cli_simplified_flow: "true",
|
|
2820
|
+
originator: "opencode"
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
// NOTE: Gemini OAuth removed - Google's client ID is restricted to official apps
|
|
2824
|
+
// Use API Key (https://aistudio.google.com/apikey) or gcloud ADC instead
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
});
|
|
2828
|
+
function generateCodeVerifier(length = 64) {
|
|
2829
|
+
const randomBytes2 = crypto.randomBytes(length);
|
|
2830
|
+
return base64UrlEncode(randomBytes2);
|
|
2831
|
+
}
|
|
2832
|
+
function generateCodeChallenge(codeVerifier) {
|
|
2833
|
+
const hash = crypto.createHash("sha256").update(codeVerifier).digest();
|
|
2834
|
+
return base64UrlEncode(hash);
|
|
2835
|
+
}
|
|
2836
|
+
function generateState(length = 32) {
|
|
2837
|
+
const randomBytes2 = crypto.randomBytes(length);
|
|
2838
|
+
return base64UrlEncode(randomBytes2);
|
|
2839
|
+
}
|
|
2840
|
+
function base64UrlEncode(buffer) {
|
|
2841
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
2842
|
+
}
|
|
2843
|
+
function generatePKCECredentials() {
|
|
2844
|
+
const codeVerifier = generateCodeVerifier();
|
|
2845
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
2846
|
+
const state = generateState();
|
|
2847
|
+
return {
|
|
2848
|
+
codeVerifier,
|
|
2849
|
+
codeChallenge,
|
|
2850
|
+
state
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
var init_pkce = __esm({
|
|
2854
|
+
"src/auth/pkce.ts"() {
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
function escapeHtml(unsafe) {
|
|
2858
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2859
|
+
}
|
|
2860
|
+
async function createCallbackServer(expectedState, timeout = 5 * 60 * 1e3, port = OAUTH_CALLBACK_PORT) {
|
|
2861
|
+
let resolveResult;
|
|
2862
|
+
let rejectResult;
|
|
2863
|
+
const resultPromise = new Promise((resolve4, reject) => {
|
|
2864
|
+
resolveResult = resolve4;
|
|
2865
|
+
rejectResult = reject;
|
|
2866
|
+
});
|
|
2867
|
+
const server = http.createServer((req, res) => {
|
|
2868
|
+
console.log(` [OAuth] ${req.method} ${req.url?.split("?")[0]}`);
|
|
2869
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2870
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2871
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
2872
|
+
if (req.method === "OPTIONS") {
|
|
2873
|
+
res.writeHead(204);
|
|
2874
|
+
res.end();
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
if (!req.url?.startsWith("/auth/callback")) {
|
|
2878
|
+
res.writeHead(404);
|
|
2879
|
+
res.end("Not Found");
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
try {
|
|
2883
|
+
const url = new URL$1(req.url, `http://localhost`);
|
|
2884
|
+
const code = url.searchParams.get("code");
|
|
2885
|
+
const state = url.searchParams.get("state");
|
|
2886
|
+
const error = url.searchParams.get("error");
|
|
2887
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
2888
|
+
if (error) {
|
|
2889
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2890
|
+
res.end(ERROR_HTML(errorDescription || error));
|
|
2891
|
+
server.close();
|
|
2892
|
+
rejectResult(new Error(errorDescription || error));
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (!code || !state) {
|
|
2896
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2897
|
+
res.end(ERROR_HTML("Missing authorization code or state"));
|
|
2898
|
+
server.close();
|
|
2899
|
+
rejectResult(new Error("Missing authorization code or state"));
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
if (state !== expectedState) {
|
|
2903
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2904
|
+
res.end(ERROR_HTML("State mismatch - possible CSRF attack"));
|
|
2905
|
+
server.close();
|
|
2906
|
+
rejectResult(new Error("State mismatch - possible CSRF attack"));
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2910
|
+
res.end(SUCCESS_HTML);
|
|
2911
|
+
server.close();
|
|
2912
|
+
resolveResult({ code, state });
|
|
2913
|
+
} catch (err) {
|
|
2914
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2915
|
+
res.end(ERROR_HTML(String(err)));
|
|
2916
|
+
server.close();
|
|
2917
|
+
rejectResult(err instanceof Error ? err : new Error(String(err)));
|
|
3080
2918
|
}
|
|
3081
|
-
];
|
|
3082
|
-
const authMethod = await p26.select({
|
|
3083
|
-
message: "Choose authentication method:",
|
|
3084
|
-
options: authOptions
|
|
3085
2919
|
});
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
return runBrowserOAuthFlow(provider);
|
|
3089
|
-
} else {
|
|
3090
|
-
return runApiKeyFlow(provider);
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
async function isPortAvailable(port) {
|
|
3094
|
-
const net = await import('net');
|
|
3095
|
-
return new Promise((resolve4) => {
|
|
3096
|
-
const server = net.createServer();
|
|
3097
|
-
server.once("error", (err) => {
|
|
2920
|
+
const actualPort = await new Promise((resolve4, reject) => {
|
|
2921
|
+
const errorHandler = (err) => {
|
|
3098
2922
|
if (err.code === "EADDRINUSE") {
|
|
3099
|
-
|
|
2923
|
+
console.log(` Port ${port} is in use, trying alternative port...`);
|
|
2924
|
+
server.removeListener("error", errorHandler);
|
|
2925
|
+
server.listen(0, () => {
|
|
2926
|
+
const address = server.address();
|
|
2927
|
+
if (typeof address === "object" && address) {
|
|
2928
|
+
resolve4(address.port);
|
|
2929
|
+
} else {
|
|
2930
|
+
reject(new Error("Failed to get server port"));
|
|
2931
|
+
}
|
|
2932
|
+
});
|
|
3100
2933
|
} else {
|
|
3101
|
-
|
|
2934
|
+
reject(err);
|
|
2935
|
+
}
|
|
2936
|
+
};
|
|
2937
|
+
server.on("error", errorHandler);
|
|
2938
|
+
server.listen(port, () => {
|
|
2939
|
+
server.removeListener("error", errorHandler);
|
|
2940
|
+
const address = server.address();
|
|
2941
|
+
if (typeof address === "object" && address) {
|
|
2942
|
+
resolve4(address.port);
|
|
2943
|
+
} else {
|
|
2944
|
+
reject(new Error("Failed to get server port"));
|
|
3102
2945
|
}
|
|
3103
2946
|
});
|
|
3104
|
-
server.once("listening", () => {
|
|
3105
|
-
server.close();
|
|
3106
|
-
resolve4({ available: true });
|
|
3107
|
-
});
|
|
3108
|
-
server.listen(port, "127.0.0.1");
|
|
3109
2947
|
});
|
|
2948
|
+
const timeoutId = setTimeout(() => {
|
|
2949
|
+
server.close();
|
|
2950
|
+
rejectResult(new Error("Authentication timed out. Please try again."));
|
|
2951
|
+
}, timeout);
|
|
2952
|
+
server.on("close", () => {
|
|
2953
|
+
clearTimeout(timeoutId);
|
|
2954
|
+
});
|
|
2955
|
+
return { port: actualPort, resultPromise };
|
|
3110
2956
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
` ${displayInfo.name} OAuth requires port ${requiredPort}, which is currently occupied.`
|
|
3132
|
-
)
|
|
3133
|
-
);
|
|
3134
|
-
console.log(chalk2.dim(" This usually means OpenCode or another coding tool is running."));
|
|
3135
|
-
console.log();
|
|
3136
|
-
console.log(chalk2.cyan(" To fix this:"));
|
|
3137
|
-
console.log(chalk2.dim(" 1. Close OpenCode/Codex CLI (if running)"));
|
|
3138
|
-
console.log(
|
|
3139
|
-
chalk2.dim(" 2. Or use an API key instead (recommended if using multiple tools)")
|
|
3140
|
-
);
|
|
3141
|
-
console.log();
|
|
3142
|
-
const fallbackOptions = [
|
|
3143
|
-
{
|
|
3144
|
-
value: "api_key",
|
|
3145
|
-
label: "\u{1F4CB} Use API key instead",
|
|
3146
|
-
hint: `Get from ${displayInfo.apiKeyUrl}`
|
|
3147
|
-
},
|
|
3148
|
-
{
|
|
3149
|
-
value: "retry",
|
|
3150
|
-
label: "\u{1F504} Retry (after closing other tools)",
|
|
3151
|
-
hint: "Check port again"
|
|
3152
|
-
}
|
|
3153
|
-
];
|
|
3154
|
-
if (config?.deviceAuthEndpoint) {
|
|
3155
|
-
fallbackOptions.push({
|
|
3156
|
-
value: "device_code",
|
|
3157
|
-
label: "\u{1F511} Try device code flow",
|
|
3158
|
-
hint: "May be blocked by Cloudflare"
|
|
3159
|
-
});
|
|
3160
|
-
}
|
|
3161
|
-
fallbackOptions.push({
|
|
3162
|
-
value: "cancel",
|
|
3163
|
-
label: "\u274C Cancel",
|
|
3164
|
-
hint: ""
|
|
3165
|
-
});
|
|
3166
|
-
const fallback = await p26.select({
|
|
3167
|
-
message: "What would you like to do?",
|
|
3168
|
-
options: fallbackOptions
|
|
3169
|
-
});
|
|
3170
|
-
if (p26.isCancel(fallback) || fallback === "cancel") return null;
|
|
3171
|
-
if (fallback === "api_key") {
|
|
3172
|
-
return runApiKeyFlow(provider);
|
|
3173
|
-
} else if (fallback === "device_code") {
|
|
3174
|
-
return runDeviceCodeFlow(provider);
|
|
3175
|
-
} else if (fallback === "retry") {
|
|
3176
|
-
return runBrowserOAuthFlow(provider);
|
|
3177
|
-
}
|
|
3178
|
-
return null;
|
|
2957
|
+
var OAUTH_CALLBACK_PORT, SUCCESS_HTML, ERROR_HTML;
|
|
2958
|
+
var init_callback_server = __esm({
|
|
2959
|
+
"src/auth/callback-server.ts"() {
|
|
2960
|
+
OAUTH_CALLBACK_PORT = 1455;
|
|
2961
|
+
SUCCESS_HTML = `
|
|
2962
|
+
<!DOCTYPE html>
|
|
2963
|
+
<html>
|
|
2964
|
+
<head>
|
|
2965
|
+
<meta charset="utf-8">
|
|
2966
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2967
|
+
<title>Authentication Successful</title>
|
|
2968
|
+
<style>
|
|
2969
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2970
|
+
body {
|
|
2971
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
2972
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2973
|
+
min-height: 100vh;
|
|
2974
|
+
display: flex;
|
|
2975
|
+
align-items: center;
|
|
2976
|
+
justify-content: center;
|
|
3179
2977
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
2978
|
+
.container {
|
|
2979
|
+
background: white;
|
|
2980
|
+
border-radius: 16px;
|
|
2981
|
+
padding: 48px;
|
|
2982
|
+
text-align: center;
|
|
2983
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
2984
|
+
max-width: 400px;
|
|
2985
|
+
}
|
|
2986
|
+
.checkmark {
|
|
2987
|
+
width: 80px;
|
|
2988
|
+
height: 80px;
|
|
2989
|
+
margin: 0 auto 24px;
|
|
2990
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
2991
|
+
border-radius: 50%;
|
|
2992
|
+
display: flex;
|
|
2993
|
+
align-items: center;
|
|
2994
|
+
justify-content: center;
|
|
2995
|
+
}
|
|
2996
|
+
.checkmark svg {
|
|
2997
|
+
width: 40px;
|
|
2998
|
+
height: 40px;
|
|
2999
|
+
stroke: white;
|
|
3000
|
+
stroke-width: 3;
|
|
3001
|
+
fill: none;
|
|
3002
|
+
}
|
|
3003
|
+
h1 {
|
|
3004
|
+
font-size: 24px;
|
|
3005
|
+
font-weight: 600;
|
|
3006
|
+
color: #1f2937;
|
|
3007
|
+
margin-bottom: 12px;
|
|
3008
|
+
}
|
|
3009
|
+
p {
|
|
3010
|
+
color: #6b7280;
|
|
3011
|
+
font-size: 16px;
|
|
3012
|
+
line-height: 1.5;
|
|
3013
|
+
}
|
|
3014
|
+
.brand {
|
|
3015
|
+
margin-top: 24px;
|
|
3016
|
+
padding-top: 24px;
|
|
3017
|
+
border-top: 1px solid #e5e7eb;
|
|
3018
|
+
color: #9ca3af;
|
|
3019
|
+
font-size: 14px;
|
|
3020
|
+
}
|
|
3021
|
+
.brand strong {
|
|
3022
|
+
color: #667eea;
|
|
3023
|
+
}
|
|
3024
|
+
</style>
|
|
3025
|
+
</head>
|
|
3026
|
+
<body>
|
|
3027
|
+
<div class="container">
|
|
3028
|
+
<div class="checkmark">
|
|
3029
|
+
<svg viewBox="0 0 24 24">
|
|
3030
|
+
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3031
|
+
</svg>
|
|
3032
|
+
</div>
|
|
3033
|
+
<h1>Authentication Successful!</h1>
|
|
3034
|
+
<p>You can close this window and return to your terminal.</p>
|
|
3035
|
+
<div class="brand">
|
|
3036
|
+
Powered by <strong>Corbat-Coco</strong>
|
|
3037
|
+
</div>
|
|
3038
|
+
</div>
|
|
3039
|
+
<script>
|
|
3040
|
+
// Auto-close after 3 seconds
|
|
3041
|
+
setTimeout(() => window.close(), 3000);
|
|
3042
|
+
</script>
|
|
3043
|
+
</body>
|
|
3044
|
+
</html>
|
|
3045
|
+
`;
|
|
3046
|
+
ERROR_HTML = (error) => {
|
|
3047
|
+
const safeError = escapeHtml(error);
|
|
3048
|
+
return `
|
|
3049
|
+
<!DOCTYPE html>
|
|
3050
|
+
<html>
|
|
3051
|
+
<head>
|
|
3052
|
+
<meta charset="utf-8">
|
|
3053
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3054
|
+
<title>Authentication Failed</title>
|
|
3055
|
+
<style>
|
|
3056
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3057
|
+
body {
|
|
3058
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
3059
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
3060
|
+
min-height: 100vh;
|
|
3061
|
+
display: flex;
|
|
3062
|
+
align-items: center;
|
|
3063
|
+
justify-content: center;
|
|
3233
3064
|
}
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
callbackResult.code,
|
|
3242
|
-
pkce.codeVerifier,
|
|
3243
|
-
redirectUri
|
|
3244
|
-
);
|
|
3245
|
-
await saveTokens(oauthProvider, tokens);
|
|
3246
|
-
console.log(chalk2.green("\n \u2705 Authentication complete!\n"));
|
|
3247
|
-
if (oauthProvider === "openai") {
|
|
3248
|
-
console.log(chalk2.dim(" Your ChatGPT Plus/Pro subscription is now linked."));
|
|
3065
|
+
.container {
|
|
3066
|
+
background: white;
|
|
3067
|
+
border-radius: 16px;
|
|
3068
|
+
padding: 48px;
|
|
3069
|
+
text-align: center;
|
|
3070
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
3071
|
+
max-width: 400px;
|
|
3249
3072
|
}
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
const fallbackOptions = [];
|
|
3260
|
-
if (config?.deviceAuthEndpoint) {
|
|
3261
|
-
fallbackOptions.push({
|
|
3262
|
-
value: "device_code",
|
|
3263
|
-
label: "\u{1F511} Try device code flow",
|
|
3264
|
-
hint: "Enter code manually in browser"
|
|
3265
|
-
});
|
|
3073
|
+
.icon {
|
|
3074
|
+
width: 80px;
|
|
3075
|
+
height: 80px;
|
|
3076
|
+
margin: 0 auto 24px;
|
|
3077
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
3078
|
+
border-radius: 50%;
|
|
3079
|
+
display: flex;
|
|
3080
|
+
align-items: center;
|
|
3081
|
+
justify-content: center;
|
|
3266
3082
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
value: "cancel",
|
|
3274
|
-
label: "\u274C Cancel",
|
|
3275
|
-
hint: ""
|
|
3276
|
-
});
|
|
3277
|
-
const fallback = await p26.select({
|
|
3278
|
-
message: "What would you like to do?",
|
|
3279
|
-
options: fallbackOptions
|
|
3280
|
-
});
|
|
3281
|
-
if (p26.isCancel(fallback) || fallback === "cancel") return null;
|
|
3282
|
-
if (fallback === "device_code") {
|
|
3283
|
-
return runDeviceCodeFlow(provider);
|
|
3284
|
-
} else {
|
|
3285
|
-
return runApiKeyFlow(provider);
|
|
3083
|
+
.icon svg {
|
|
3084
|
+
width: 40px;
|
|
3085
|
+
height: 40px;
|
|
3086
|
+
stroke: white;
|
|
3087
|
+
stroke-width: 3;
|
|
3088
|
+
fill: none;
|
|
3286
3089
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
console.log();
|
|
3293
|
-
console.log(chalk2.dim(` Requesting device code from ${displayInfo.name}...`));
|
|
3294
|
-
try {
|
|
3295
|
-
const deviceCode = await requestDeviceCode(oauthProvider);
|
|
3296
|
-
console.log();
|
|
3297
|
-
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3298
|
-
console.log(
|
|
3299
|
-
chalk2.magenta(" \u2502 ") + chalk2.bold.white("Enter this code in your browser:") + chalk2.magenta(" \u2502")
|
|
3300
|
-
);
|
|
3301
|
-
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3302
|
-
console.log(
|
|
3303
|
-
chalk2.magenta(" \u2502 ") + chalk2.bold.cyan.bgBlack(` ${deviceCode.userCode} `) + chalk2.magenta(" \u2502")
|
|
3304
|
-
);
|
|
3305
|
-
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3306
|
-
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3307
|
-
console.log();
|
|
3308
|
-
const verificationUrl = deviceCode.verificationUriComplete || deviceCode.verificationUri;
|
|
3309
|
-
console.log(chalk2.cyan(` \u2192 ${verificationUrl}`));
|
|
3310
|
-
console.log();
|
|
3311
|
-
const openIt = await p26.confirm({
|
|
3312
|
-
message: "Open browser to sign in?",
|
|
3313
|
-
initialValue: true
|
|
3314
|
-
});
|
|
3315
|
-
if (p26.isCancel(openIt)) return null;
|
|
3316
|
-
if (openIt) {
|
|
3317
|
-
const opened = await openBrowser(verificationUrl);
|
|
3318
|
-
if (opened) {
|
|
3319
|
-
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3320
|
-
} else {
|
|
3321
|
-
const fallbackOpened = await openBrowserFallback(verificationUrl);
|
|
3322
|
-
if (fallbackOpened) {
|
|
3323
|
-
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3324
|
-
} else {
|
|
3325
|
-
console.log(chalk2.dim(" Copy the URL above and paste it in your browser"));
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3090
|
+
h1 {
|
|
3091
|
+
font-size: 24px;
|
|
3092
|
+
font-weight: 600;
|
|
3093
|
+
color: #1f2937;
|
|
3094
|
+
margin-bottom: 12px;
|
|
3328
3095
|
}
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
const tokens = await pollForToken(
|
|
3334
|
-
oauthProvider,
|
|
3335
|
-
deviceCode.deviceCode,
|
|
3336
|
-
deviceCode.interval,
|
|
3337
|
-
deviceCode.expiresIn,
|
|
3338
|
-
() => {
|
|
3339
|
-
pollCount++;
|
|
3340
|
-
const dots = ".".repeat(pollCount % 3 + 1);
|
|
3341
|
-
spinner18.message(`Waiting for you to sign in${dots}`);
|
|
3342
|
-
}
|
|
3343
|
-
);
|
|
3344
|
-
spinner18.stop(chalk2.green("\u2713 Signed in successfully!"));
|
|
3345
|
-
await saveTokens(oauthProvider, tokens);
|
|
3346
|
-
console.log(chalk2.green("\n \u2705 Authentication complete!\n"));
|
|
3347
|
-
if (oauthProvider === "openai") {
|
|
3348
|
-
console.log(chalk2.dim(" Your ChatGPT Plus/Pro subscription is now linked."));
|
|
3349
|
-
} else {
|
|
3350
|
-
console.log(chalk2.dim(` Your ${displayInfo.name} account is now linked.`));
|
|
3096
|
+
p {
|
|
3097
|
+
color: #6b7280;
|
|
3098
|
+
font-size: 16px;
|
|
3099
|
+
line-height: 1.5;
|
|
3351
3100
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
console.log();
|
|
3361
|
-
const useFallback = await p26.confirm({
|
|
3362
|
-
message: "Use API key instead?",
|
|
3363
|
-
initialValue: true
|
|
3364
|
-
});
|
|
3365
|
-
if (p26.isCancel(useFallback) || !useFallback) return null;
|
|
3366
|
-
return runApiKeyFlow(provider);
|
|
3101
|
+
.error {
|
|
3102
|
+
margin-top: 16px;
|
|
3103
|
+
padding: 12px;
|
|
3104
|
+
background: #fef2f2;
|
|
3105
|
+
border-radius: 8px;
|
|
3106
|
+
color: #dc2626;
|
|
3107
|
+
font-family: monospace;
|
|
3108
|
+
font-size: 14px;
|
|
3367
3109
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3110
|
+
</style>
|
|
3111
|
+
</head>
|
|
3112
|
+
<body>
|
|
3113
|
+
<div class="container">
|
|
3114
|
+
<div class="icon">
|
|
3115
|
+
<svg viewBox="0 0 24 24">
|
|
3116
|
+
<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3117
|
+
</svg>
|
|
3118
|
+
</div>
|
|
3119
|
+
<h1>Authentication Failed</h1>
|
|
3120
|
+
<p>Something went wrong. Please try again.</p>
|
|
3121
|
+
<div class="error">${safeError}</div>
|
|
3122
|
+
</div>
|
|
3123
|
+
</body>
|
|
3124
|
+
</html>
|
|
3125
|
+
`;
|
|
3126
|
+
};
|
|
3376
3127
|
}
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
const keyPrefix = oauthProvider === "openai" ? "sk-" : oauthProvider === "gemini" ? "AI" : "";
|
|
3381
|
-
const keyPrefixHint = keyPrefix ? ` (starts with '${keyPrefix}')` : "";
|
|
3382
|
-
console.log();
|
|
3383
|
-
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3384
|
-
console.log(
|
|
3385
|
-
chalk2.magenta(" \u2502 ") + chalk2.bold.white(`\u{1F511} Get your ${displayInfo.name} API key:`.padEnd(47)) + chalk2.magenta("\u2502")
|
|
3386
|
-
);
|
|
3387
|
-
console.log(chalk2.magenta(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
|
|
3388
|
-
console.log(
|
|
3389
|
-
chalk2.magenta(" \u2502 ") + chalk2.dim("1. Sign in with your account") + chalk2.magenta(" \u2502")
|
|
3390
|
-
);
|
|
3391
|
-
console.log(
|
|
3392
|
-
chalk2.magenta(" \u2502 ") + chalk2.dim("2. Create a new API key") + chalk2.magenta(" \u2502")
|
|
3393
|
-
);
|
|
3394
|
-
console.log(
|
|
3395
|
-
chalk2.magenta(" \u2502 ") + chalk2.dim("3. Copy and paste it here") + chalk2.magenta(" \u2502")
|
|
3396
|
-
);
|
|
3397
|
-
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3398
|
-
console.log();
|
|
3128
|
+
});
|
|
3129
|
+
function detectWSL() {
|
|
3130
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) return true;
|
|
3399
3131
|
try {
|
|
3400
|
-
|
|
3401
|
-
parsedUrl.search = "";
|
|
3402
|
-
console.log(chalk2.cyan(` \u2192 ${parsedUrl.toString()}`));
|
|
3132
|
+
return /microsoft/i.test(readFileSync("/proc/version", "utf-8"));
|
|
3403
3133
|
} catch {
|
|
3404
|
-
|
|
3134
|
+
return false;
|
|
3405
3135
|
}
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
if (p26.isCancel(openIt)) return null;
|
|
3412
|
-
if (openIt) {
|
|
3413
|
-
const opened = await openBrowser(apiKeysUrl);
|
|
3414
|
-
if (opened) {
|
|
3415
|
-
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3416
|
-
} else {
|
|
3417
|
-
const fallbackOpened = await openBrowserFallback(apiKeysUrl);
|
|
3418
|
-
if (fallbackOpened) {
|
|
3419
|
-
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3420
|
-
} else {
|
|
3421
|
-
console.log(chalk2.dim(" Copy the URL above and paste it in your browser"));
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3136
|
+
}
|
|
3137
|
+
var isWSL;
|
|
3138
|
+
var init_platform = __esm({
|
|
3139
|
+
"src/utils/platform.ts"() {
|
|
3140
|
+
isWSL = detectWSL();
|
|
3424
3141
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
}
|
|
3142
|
+
});
|
|
3143
|
+
async function requestGitHubDeviceCode() {
|
|
3144
|
+
const response = await fetch(GITHUB_DEVICE_CODE_URL, {
|
|
3145
|
+
method: "POST",
|
|
3146
|
+
headers: {
|
|
3147
|
+
"Content-Type": "application/json",
|
|
3148
|
+
Accept: "application/json"
|
|
3149
|
+
},
|
|
3150
|
+
body: JSON.stringify({
|
|
3151
|
+
client_id: COPILOT_CLIENT_ID,
|
|
3152
|
+
scope: "read:user"
|
|
3153
|
+
})
|
|
3437
3154
|
});
|
|
3438
|
-
if (
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
await saveTokens(oauthProvider, tokens);
|
|
3444
|
-
console.log(chalk2.green("\n \u2705 API key saved!\n"));
|
|
3445
|
-
return { tokens, accessToken: apiKey };
|
|
3155
|
+
if (!response.ok) {
|
|
3156
|
+
const error = await response.text();
|
|
3157
|
+
throw new Error(`GitHub device code request failed: ${response.status} - ${error}`);
|
|
3158
|
+
}
|
|
3159
|
+
return await response.json();
|
|
3446
3160
|
}
|
|
3447
|
-
async function
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
console.log(
|
|
3464
|
-
chalk2.magenta(" \u2502 ") + chalk2.bold.white("Enter this code in your browser:") + chalk2.magenta(" \u2502")
|
|
3465
|
-
);
|
|
3466
|
-
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3467
|
-
console.log(
|
|
3468
|
-
chalk2.magenta(" \u2502 ") + chalk2.bold.cyan.bgBlack(` ${deviceCode.user_code} `) + chalk2.magenta(" \u2502")
|
|
3469
|
-
);
|
|
3470
|
-
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3471
|
-
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3472
|
-
console.log();
|
|
3473
|
-
console.log(chalk2.cyan(` \u2192 ${deviceCode.verification_uri}`));
|
|
3474
|
-
console.log();
|
|
3475
|
-
const openIt = await p26.confirm({
|
|
3476
|
-
message: "Open browser to sign in?",
|
|
3477
|
-
initialValue: true
|
|
3161
|
+
async function pollGitHubForToken(deviceCode, interval, expiresIn, onPoll) {
|
|
3162
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
3163
|
+
while (Date.now() < expiresAt) {
|
|
3164
|
+
await new Promise((resolve4) => setTimeout(resolve4, interval * 1e3));
|
|
3165
|
+
if (onPoll) onPoll();
|
|
3166
|
+
const response = await fetch(GITHUB_TOKEN_URL, {
|
|
3167
|
+
method: "POST",
|
|
3168
|
+
headers: {
|
|
3169
|
+
"Content-Type": "application/json",
|
|
3170
|
+
Accept: "application/json"
|
|
3171
|
+
},
|
|
3172
|
+
body: JSON.stringify({
|
|
3173
|
+
client_id: COPILOT_CLIENT_ID,
|
|
3174
|
+
device_code: deviceCode,
|
|
3175
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
3176
|
+
})
|
|
3478
3177
|
});
|
|
3479
|
-
|
|
3480
|
-
if (
|
|
3481
|
-
|
|
3482
|
-
if (opened) {
|
|
3483
|
-
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3484
|
-
} else {
|
|
3485
|
-
const fallbackOpened = await openBrowserFallback(deviceCode.verification_uri);
|
|
3486
|
-
if (fallbackOpened) {
|
|
3487
|
-
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3488
|
-
} else {
|
|
3489
|
-
console.log(chalk2.dim(" Copy the URL above and paste it in your browser"));
|
|
3490
|
-
}
|
|
3491
|
-
}
|
|
3178
|
+
const data = await response.json();
|
|
3179
|
+
if (data.access_token) {
|
|
3180
|
+
return data.access_token;
|
|
3492
3181
|
}
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
(
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
spinner18.message(`Waiting for you to sign in on GitHub${dots}`);
|
|
3505
|
-
}
|
|
3506
|
-
);
|
|
3507
|
-
spinner18.stop(chalk2.green("\u2713 GitHub authentication successful!"));
|
|
3508
|
-
console.log(chalk2.dim(" Exchanging token for Copilot access..."));
|
|
3509
|
-
const copilotToken = await exchangeForCopilotToken(githubToken);
|
|
3510
|
-
const creds = {
|
|
3511
|
-
githubToken,
|
|
3512
|
-
copilotToken: copilotToken.token,
|
|
3513
|
-
copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
|
|
3514
|
-
accountType: copilotToken.annotations?.copilot_plan
|
|
3515
|
-
};
|
|
3516
|
-
await saveCopilotCredentials(creds);
|
|
3517
|
-
const planType = creds.accountType ?? "individual";
|
|
3518
|
-
console.log(chalk2.green("\n \u2705 GitHub Copilot authenticated!\n"));
|
|
3519
|
-
console.log(chalk2.dim(` Plan: ${planType}`));
|
|
3520
|
-
console.log(chalk2.dim(" Credentials stored in ~/.coco/tokens/copilot.json\n"));
|
|
3521
|
-
const tokens = {
|
|
3522
|
-
accessToken: copilotToken.token,
|
|
3523
|
-
tokenType: "Bearer",
|
|
3524
|
-
expiresAt: copilotToken.expires_at * 1e3
|
|
3525
|
-
};
|
|
3526
|
-
return { tokens, accessToken: copilotToken.token };
|
|
3527
|
-
} catch (error) {
|
|
3528
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3529
|
-
console.log();
|
|
3530
|
-
if (errorMsg.includes("403") || errorMsg.includes("not enabled")) {
|
|
3531
|
-
console.log(chalk2.red(" \u2717 GitHub Copilot is not enabled for this account."));
|
|
3532
|
-
console.log(chalk2.dim(" Please ensure you have an active Copilot subscription:"));
|
|
3533
|
-
console.log(chalk2.cyan(" \u2192 https://github.com/settings/copilot"));
|
|
3534
|
-
} else if (errorMsg.includes("expired") || errorMsg.includes("timed out")) {
|
|
3535
|
-
console.log(chalk2.yellow(" \u26A0 Authentication timed out. Please try again."));
|
|
3536
|
-
} else if (errorMsg.includes("denied")) {
|
|
3537
|
-
console.log(chalk2.yellow(" \u26A0 Access was denied."));
|
|
3538
|
-
} else {
|
|
3539
|
-
const category = errorMsg.includes("network") || errorMsg.includes("fetch") ? "Network error" : "Authentication error";
|
|
3540
|
-
console.log(chalk2.red(` \u2717 ${category}`));
|
|
3182
|
+
if (data.error === "authorization_pending") {
|
|
3183
|
+
continue;
|
|
3184
|
+
} else if (data.error === "slow_down") {
|
|
3185
|
+
interval += 5;
|
|
3186
|
+
continue;
|
|
3187
|
+
} else if (data.error === "expired_token") {
|
|
3188
|
+
throw new Error("Device code expired. Please try again.");
|
|
3189
|
+
} else if (data.error === "access_denied") {
|
|
3190
|
+
throw new Error("Access denied by user.");
|
|
3191
|
+
} else if (data.error) {
|
|
3192
|
+
throw new Error(data.error_description || data.error);
|
|
3541
3193
|
}
|
|
3542
|
-
console.log();
|
|
3543
|
-
return null;
|
|
3544
3194
|
}
|
|
3195
|
+
throw new Error("Authentication timed out. Please try again.");
|
|
3545
3196
|
}
|
|
3546
|
-
async function
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3197
|
+
async function exchangeForCopilotToken(githubToken) {
|
|
3198
|
+
const response = await fetch(COPILOT_TOKEN_URL, {
|
|
3199
|
+
method: "GET",
|
|
3200
|
+
headers: {
|
|
3201
|
+
Authorization: `token ${githubToken}`,
|
|
3202
|
+
Accept: "application/json",
|
|
3203
|
+
"User-Agent": "Corbat-Coco/1.0"
|
|
3551
3204
|
}
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3205
|
+
});
|
|
3206
|
+
if (!response.ok) {
|
|
3207
|
+
const error = await response.text();
|
|
3208
|
+
if (response.status === 401) {
|
|
3209
|
+
throw new CopilotAuthError(
|
|
3210
|
+
"GitHub token is invalid or expired. Please re-authenticate with /provider copilot.",
|
|
3211
|
+
true
|
|
3212
|
+
);
|
|
3555
3213
|
}
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
const flowResult = await runOAuthFlow(provider);
|
|
3564
|
-
if (flowResult) {
|
|
3565
|
-
return { accessToken: flowResult.accessToken };
|
|
3214
|
+
if (response.status === 403) {
|
|
3215
|
+
throw new CopilotAuthError(
|
|
3216
|
+
"GitHub Copilot is not enabled for this account.\n Please ensure you have an active Copilot subscription:\n https://github.com/settings/copilot",
|
|
3217
|
+
true
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
throw new Error(`Copilot token exchange failed: ${response.status} - ${error}`);
|
|
3566
3221
|
}
|
|
3567
|
-
return
|
|
3222
|
+
return await response.json();
|
|
3568
3223
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
init_oauth();
|
|
3573
|
-
init_pkce();
|
|
3574
|
-
init_callback_server();
|
|
3575
|
-
init_platform();
|
|
3576
|
-
init_copilot();
|
|
3577
|
-
execFileAsync = promisify(execFile);
|
|
3578
|
-
}
|
|
3579
|
-
});
|
|
3580
|
-
function getADCPath() {
|
|
3581
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3582
|
-
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
3583
|
-
return process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
3224
|
+
function getCopilotBaseUrl(accountType) {
|
|
3225
|
+
if (accountType && accountType in COPILOT_BASE_URLS) {
|
|
3226
|
+
return COPILOT_BASE_URLS[accountType];
|
|
3584
3227
|
}
|
|
3585
|
-
return
|
|
3228
|
+
return DEFAULT_COPILOT_BASE_URL;
|
|
3229
|
+
}
|
|
3230
|
+
function getCopilotCredentialsPath() {
|
|
3231
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3232
|
+
return path36.join(home, ".coco", "tokens", "copilot.json");
|
|
3586
3233
|
}
|
|
3587
|
-
async function
|
|
3234
|
+
async function saveCopilotCredentials(creds) {
|
|
3235
|
+
const filePath = getCopilotCredentialsPath();
|
|
3236
|
+
const dir = path36.dirname(filePath);
|
|
3237
|
+
await fs34.mkdir(dir, { recursive: true, mode: 448 });
|
|
3238
|
+
await fs34.writeFile(filePath, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
3239
|
+
}
|
|
3240
|
+
async function loadCopilotCredentials() {
|
|
3588
3241
|
try {
|
|
3589
|
-
await
|
|
3590
|
-
|
|
3242
|
+
const content = await fs34.readFile(getCopilotCredentialsPath(), "utf-8");
|
|
3243
|
+
const parsed = CopilotCredentialsSchema.safeParse(JSON.parse(content));
|
|
3244
|
+
return parsed.success ? parsed.data : null;
|
|
3591
3245
|
} catch {
|
|
3592
|
-
return
|
|
3246
|
+
return null;
|
|
3593
3247
|
}
|
|
3594
3248
|
}
|
|
3595
|
-
async function
|
|
3596
|
-
const adcPath = getADCPath();
|
|
3249
|
+
async function deleteCopilotCredentials() {
|
|
3597
3250
|
try {
|
|
3598
|
-
await fs34.
|
|
3599
|
-
return true;
|
|
3251
|
+
await fs34.unlink(getCopilotCredentialsPath());
|
|
3600
3252
|
} catch {
|
|
3601
|
-
return false;
|
|
3602
3253
|
}
|
|
3603
3254
|
}
|
|
3604
|
-
|
|
3255
|
+
function isCopilotTokenExpired(creds) {
|
|
3256
|
+
if (!creds.copilotToken || !creds.copilotTokenExpiresAt) return true;
|
|
3257
|
+
return Date.now() >= creds.copilotTokenExpiresAt - REFRESH_BUFFER_MS;
|
|
3258
|
+
}
|
|
3259
|
+
async function getValidCopilotToken() {
|
|
3260
|
+
const creds = await loadCopilotCredentials();
|
|
3261
|
+
if (!creds) return null;
|
|
3262
|
+
const envToken = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
|
|
3263
|
+
const githubToken = envToken || creds.githubToken;
|
|
3264
|
+
if (!isCopilotTokenExpired(creds) && creds.copilotToken) {
|
|
3265
|
+
return {
|
|
3266
|
+
token: creds.copilotToken,
|
|
3267
|
+
baseUrl: getCopilotBaseUrl(creds.accountType),
|
|
3268
|
+
isNew: false
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3605
3271
|
try {
|
|
3606
|
-
const
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3272
|
+
const copilotToken = await exchangeForCopilotToken(githubToken);
|
|
3273
|
+
const updatedCreds = {
|
|
3274
|
+
...creds,
|
|
3275
|
+
githubToken: creds.githubToken,
|
|
3276
|
+
copilotToken: copilotToken.token,
|
|
3277
|
+
copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
|
|
3278
|
+
accountType: copilotToken.annotations?.copilot_plan ?? creds.accountType
|
|
3279
|
+
};
|
|
3280
|
+
await saveCopilotCredentials(updatedCreds);
|
|
3612
3281
|
return {
|
|
3613
|
-
|
|
3614
|
-
|
|
3282
|
+
token: copilotToken.token,
|
|
3283
|
+
baseUrl: getCopilotBaseUrl(updatedCreds.accountType),
|
|
3284
|
+
isNew: true
|
|
3615
3285
|
};
|
|
3616
3286
|
} catch (error) {
|
|
3617
|
-
|
|
3618
|
-
|
|
3287
|
+
if (error instanceof CopilotAuthError && error.permanent) {
|
|
3288
|
+
await deleteCopilotCredentials();
|
|
3619
3289
|
return null;
|
|
3620
3290
|
}
|
|
3621
|
-
|
|
3291
|
+
throw error;
|
|
3622
3292
|
}
|
|
3623
3293
|
}
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3294
|
+
var COPILOT_CLIENT_ID, GITHUB_DEVICE_CODE_URL, GITHUB_TOKEN_URL, COPILOT_TOKEN_URL, COPILOT_BASE_URLS, DEFAULT_COPILOT_BASE_URL, REFRESH_BUFFER_MS, CopilotAuthError, CopilotCredentialsSchema;
|
|
3295
|
+
var init_copilot = __esm({
|
|
3296
|
+
"src/auth/copilot.ts"() {
|
|
3297
|
+
COPILOT_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
3298
|
+
GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
3299
|
+
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
3300
|
+
COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
|
|
3301
|
+
COPILOT_BASE_URLS = {
|
|
3302
|
+
individual: "https://api.githubcopilot.com",
|
|
3303
|
+
business: "https://api.business.githubcopilot.com",
|
|
3304
|
+
enterprise: "https://api.enterprise.githubcopilot.com"
|
|
3305
|
+
};
|
|
3306
|
+
DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
3307
|
+
REFRESH_BUFFER_MS = 6e4;
|
|
3308
|
+
CopilotAuthError = class extends Error {
|
|
3309
|
+
constructor(message, permanent) {
|
|
3310
|
+
super(message);
|
|
3311
|
+
this.permanent = permanent;
|
|
3312
|
+
this.name = "CopilotAuthError";
|
|
3313
|
+
}
|
|
3314
|
+
};
|
|
3315
|
+
CopilotCredentialsSchema = z.object({
|
|
3316
|
+
githubToken: z.string().min(1),
|
|
3317
|
+
copilotToken: z.string().optional(),
|
|
3318
|
+
copilotTokenExpiresAt: z.number().optional(),
|
|
3319
|
+
accountType: z.string().optional()
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
3323
|
+
function getOAuthProviderName(provider) {
|
|
3324
|
+
if (provider === "codex") return "openai";
|
|
3325
|
+
return provider;
|
|
3629
3326
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3327
|
+
function getProviderDisplayInfo(provider) {
|
|
3328
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3329
|
+
switch (oauthProvider) {
|
|
3330
|
+
case "openai":
|
|
3331
|
+
return {
|
|
3332
|
+
name: "OpenAI",
|
|
3333
|
+
emoji: "\u{1F7E2}",
|
|
3334
|
+
authDescription: "Sign in with your ChatGPT account",
|
|
3335
|
+
apiKeyUrl: "https://platform.openai.com/api-keys"
|
|
3336
|
+
};
|
|
3337
|
+
case "copilot":
|
|
3338
|
+
return {
|
|
3339
|
+
name: "GitHub Copilot",
|
|
3340
|
+
emoji: "\u{1F419}",
|
|
3341
|
+
authDescription: "Sign in with your GitHub account",
|
|
3342
|
+
apiKeyUrl: "https://github.com/settings/copilot"
|
|
3343
|
+
};
|
|
3344
|
+
default:
|
|
3345
|
+
return {
|
|
3346
|
+
name: provider,
|
|
3347
|
+
emoji: "\u{1F510}",
|
|
3348
|
+
authDescription: "Sign in with your account",
|
|
3349
|
+
apiKeyUrl: ""
|
|
3350
|
+
};
|
|
3633
3351
|
}
|
|
3634
|
-
cachedToken = await getADCAccessToken();
|
|
3635
|
-
return cachedToken;
|
|
3636
3352
|
}
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3353
|
+
function supportsOAuth(provider) {
|
|
3354
|
+
if (provider === "copilot") return true;
|
|
3355
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3356
|
+
return oauthProvider in OAUTH_CONFIGS;
|
|
3357
|
+
}
|
|
3358
|
+
async function isOAuthConfigured(provider) {
|
|
3359
|
+
if (provider === "copilot") {
|
|
3360
|
+
const creds = await loadCopilotCredentials();
|
|
3361
|
+
return creds !== null;
|
|
3642
3362
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3363
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3364
|
+
const tokens = await loadTokens(oauthProvider);
|
|
3365
|
+
return tokens !== null;
|
|
3366
|
+
}
|
|
3367
|
+
function printAuthUrl(url) {
|
|
3368
|
+
try {
|
|
3369
|
+
const parsed = new URL(url);
|
|
3370
|
+
const maskedParams = new URLSearchParams(parsed.searchParams);
|
|
3371
|
+
if (maskedParams.has("client_id")) {
|
|
3372
|
+
const clientId = maskedParams.get("client_id");
|
|
3373
|
+
maskedParams.set("client_id", clientId.slice(0, 8) + "...");
|
|
3374
|
+
}
|
|
3375
|
+
parsed.search = maskedParams.toString();
|
|
3376
|
+
console.log(chalk2.cyan(` ${parsed.toString()}`));
|
|
3377
|
+
} catch {
|
|
3378
|
+
console.log(chalk2.cyan(" [invalid URL]"));
|
|
3654
3379
|
}
|
|
3655
|
-
}
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
function parseJwtClaims(token) {
|
|
3659
|
-
const parts = token.split(".");
|
|
3660
|
-
if (parts.length !== 3 || !parts[1]) return void 0;
|
|
3380
|
+
}
|
|
3381
|
+
async function openBrowser(url) {
|
|
3382
|
+
let sanitizedUrl;
|
|
3661
3383
|
try {
|
|
3662
|
-
|
|
3384
|
+
const parsed = new URL(url);
|
|
3385
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
3386
|
+
return false;
|
|
3387
|
+
}
|
|
3388
|
+
sanitizedUrl = parsed.toString();
|
|
3663
3389
|
} catch {
|
|
3664
|
-
return
|
|
3390
|
+
return false;
|
|
3391
|
+
}
|
|
3392
|
+
const platform = process.platform;
|
|
3393
|
+
try {
|
|
3394
|
+
if (platform === "darwin") {
|
|
3395
|
+
await execFileAsync("open", [sanitizedUrl]);
|
|
3396
|
+
} else if (platform === "win32") {
|
|
3397
|
+
await execFileAsync("rundll32", ["url.dll,FileProtocolHandler", sanitizedUrl]);
|
|
3398
|
+
} else if (isWSL) {
|
|
3399
|
+
await execFileAsync("cmd.exe", ["/c", "start", "", sanitizedUrl]);
|
|
3400
|
+
} else {
|
|
3401
|
+
await execFileAsync("xdg-open", [sanitizedUrl]);
|
|
3402
|
+
}
|
|
3403
|
+
return true;
|
|
3404
|
+
} catch {
|
|
3405
|
+
return false;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async function openBrowserFallback(url) {
|
|
3409
|
+
let sanitizedUrl;
|
|
3410
|
+
try {
|
|
3411
|
+
const parsed = new URL(url);
|
|
3412
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
3413
|
+
return false;
|
|
3414
|
+
}
|
|
3415
|
+
sanitizedUrl = parsed.toString();
|
|
3416
|
+
} catch {
|
|
3417
|
+
return false;
|
|
3418
|
+
}
|
|
3419
|
+
const platform = process.platform;
|
|
3420
|
+
const commands2 = [];
|
|
3421
|
+
if (platform === "darwin") {
|
|
3422
|
+
commands2.push(
|
|
3423
|
+
{ cmd: "open", args: [sanitizedUrl] },
|
|
3424
|
+
{ cmd: "open", args: ["-a", "Safari", sanitizedUrl] },
|
|
3425
|
+
{ cmd: "open", args: ["-a", "Google Chrome", sanitizedUrl] }
|
|
3426
|
+
);
|
|
3427
|
+
} else if (platform === "win32") {
|
|
3428
|
+
commands2.push({
|
|
3429
|
+
cmd: "rundll32",
|
|
3430
|
+
args: ["url.dll,FileProtocolHandler", sanitizedUrl]
|
|
3431
|
+
});
|
|
3432
|
+
} else if (isWSL) {
|
|
3433
|
+
commands2.push(
|
|
3434
|
+
{ cmd: "cmd.exe", args: ["/c", "start", "", sanitizedUrl] },
|
|
3435
|
+
{ cmd: "powershell.exe", args: ["-Command", `Start-Process '${sanitizedUrl}'`] },
|
|
3436
|
+
{ cmd: "wslview", args: [sanitizedUrl] }
|
|
3437
|
+
);
|
|
3438
|
+
} else {
|
|
3439
|
+
commands2.push(
|
|
3440
|
+
{ cmd: "xdg-open", args: [sanitizedUrl] },
|
|
3441
|
+
{ cmd: "sensible-browser", args: [sanitizedUrl] },
|
|
3442
|
+
{ cmd: "x-www-browser", args: [sanitizedUrl] },
|
|
3443
|
+
{ cmd: "gnome-open", args: [sanitizedUrl] },
|
|
3444
|
+
{ cmd: "firefox", args: [sanitizedUrl] },
|
|
3445
|
+
{ cmd: "chromium-browser", args: [sanitizedUrl] },
|
|
3446
|
+
{ cmd: "google-chrome", args: [sanitizedUrl] }
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
for (const { cmd, args } of commands2) {
|
|
3450
|
+
try {
|
|
3451
|
+
await execFileAsync(cmd, args);
|
|
3452
|
+
return true;
|
|
3453
|
+
} catch {
|
|
3454
|
+
continue;
|
|
3455
|
+
}
|
|
3665
3456
|
}
|
|
3457
|
+
return false;
|
|
3666
3458
|
}
|
|
3667
|
-
function
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3459
|
+
async function runOAuthFlow(provider) {
|
|
3460
|
+
if (provider === "copilot") {
|
|
3461
|
+
return runCopilotDeviceFlow();
|
|
3462
|
+
}
|
|
3463
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3464
|
+
const config = OAUTH_CONFIGS[oauthProvider];
|
|
3465
|
+
if (!config) {
|
|
3466
|
+
p26.log.error(`OAuth not supported for provider: ${provider}`);
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
const displayInfo = getProviderDisplayInfo(provider);
|
|
3470
|
+
console.log();
|
|
3471
|
+
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3472
|
+
console.log(
|
|
3473
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.white(`${displayInfo.emoji} ${displayInfo.name} Authentication`.padEnd(47)) + chalk2.magenta("\u2502")
|
|
3474
|
+
);
|
|
3475
|
+
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3476
|
+
console.log();
|
|
3477
|
+
const authOptions = [
|
|
3478
|
+
{
|
|
3479
|
+
value: "browser",
|
|
3480
|
+
label: "\u{1F310} Sign in with browser",
|
|
3481
|
+
hint: `${displayInfo.authDescription} (recommended)`
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
value: "api_key",
|
|
3485
|
+
label: "\u{1F4CB} Paste API key manually",
|
|
3486
|
+
hint: `Get from ${displayInfo.apiKeyUrl}`
|
|
3487
|
+
}
|
|
3488
|
+
];
|
|
3489
|
+
const authMethod = await p26.select({
|
|
3490
|
+
message: "Choose authentication method:",
|
|
3491
|
+
options: authOptions
|
|
3492
|
+
});
|
|
3493
|
+
if (p26.isCancel(authMethod)) return null;
|
|
3494
|
+
if (authMethod === "browser") {
|
|
3495
|
+
return runBrowserOAuthFlow(provider);
|
|
3496
|
+
} else {
|
|
3497
|
+
return runApiKeyFlow(provider);
|
|
3678
3498
|
}
|
|
3679
|
-
return provider;
|
|
3680
3499
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
"gpt-5.2-codex": 2e5,
|
|
3691
|
-
"gpt-5-codex": 2e5,
|
|
3692
|
-
"gpt-5.1-codex": 2e5,
|
|
3693
|
-
"gpt-5": 2e5,
|
|
3694
|
-
"gpt-5.2": 2e5,
|
|
3695
|
-
"gpt-5.1": 2e5
|
|
3696
|
-
};
|
|
3697
|
-
CodexProvider = class {
|
|
3698
|
-
id = "codex";
|
|
3699
|
-
name = "OpenAI Codex (ChatGPT Plus/Pro)";
|
|
3700
|
-
config = {};
|
|
3701
|
-
accessToken = null;
|
|
3702
|
-
accountId;
|
|
3703
|
-
/**
|
|
3704
|
-
* Initialize the provider with OAuth tokens
|
|
3705
|
-
*/
|
|
3706
|
-
async initialize(config) {
|
|
3707
|
-
this.config = config;
|
|
3708
|
-
const tokenResult = await getValidAccessToken("openai");
|
|
3709
|
-
if (tokenResult) {
|
|
3710
|
-
this.accessToken = tokenResult.accessToken;
|
|
3711
|
-
this.accountId = extractAccountId(tokenResult.accessToken);
|
|
3712
|
-
} else if (config.apiKey) {
|
|
3713
|
-
this.accessToken = config.apiKey;
|
|
3714
|
-
this.accountId = extractAccountId(config.apiKey);
|
|
3715
|
-
}
|
|
3716
|
-
if (!this.accessToken) {
|
|
3717
|
-
throw new ProviderError(
|
|
3718
|
-
"No OAuth token found. Please run authentication first with: coco --provider openai",
|
|
3719
|
-
{ provider: this.id }
|
|
3720
|
-
);
|
|
3721
|
-
}
|
|
3500
|
+
async function isPortAvailable(port) {
|
|
3501
|
+
const net = await import('net');
|
|
3502
|
+
return new Promise((resolve4) => {
|
|
3503
|
+
const server = net.createServer();
|
|
3504
|
+
server.once("error", (err) => {
|
|
3505
|
+
if (err.code === "EADDRINUSE") {
|
|
3506
|
+
resolve4({ available: false, processName: "another process" });
|
|
3507
|
+
} else {
|
|
3508
|
+
resolve4({ available: false });
|
|
3722
3509
|
}
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3510
|
+
});
|
|
3511
|
+
server.once("listening", () => {
|
|
3512
|
+
server.close();
|
|
3513
|
+
resolve4({ available: true });
|
|
3514
|
+
});
|
|
3515
|
+
server.listen(port, "127.0.0.1");
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
function getRequiredPort(provider) {
|
|
3519
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3520
|
+
if (oauthProvider === "openai") return 1455;
|
|
3521
|
+
return void 0;
|
|
3522
|
+
}
|
|
3523
|
+
async function runBrowserOAuthFlow(provider) {
|
|
3524
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3525
|
+
const displayInfo = getProviderDisplayInfo(provider);
|
|
3526
|
+
const config = OAUTH_CONFIGS[oauthProvider];
|
|
3527
|
+
const requiredPort = getRequiredPort(provider);
|
|
3528
|
+
if (requiredPort) {
|
|
3529
|
+
console.log();
|
|
3530
|
+
console.log(chalk2.dim(" Checking port availability..."));
|
|
3531
|
+
const portCheck = await isPortAvailable(requiredPort);
|
|
3532
|
+
if (!portCheck.available) {
|
|
3533
|
+
console.log();
|
|
3534
|
+
console.log(chalk2.yellow(` \u26A0 Port ${requiredPort} is already in use`));
|
|
3535
|
+
console.log();
|
|
3536
|
+
console.log(
|
|
3537
|
+
chalk2.dim(
|
|
3538
|
+
` ${displayInfo.name} OAuth requires port ${requiredPort}, which is currently occupied.`
|
|
3539
|
+
)
|
|
3540
|
+
);
|
|
3541
|
+
console.log(chalk2.dim(" This usually means OpenCode or another coding tool is running."));
|
|
3542
|
+
console.log();
|
|
3543
|
+
console.log(chalk2.cyan(" To fix this:"));
|
|
3544
|
+
console.log(chalk2.dim(" 1. Close OpenCode/Codex CLI (if running)"));
|
|
3545
|
+
console.log(
|
|
3546
|
+
chalk2.dim(" 2. Or use an API key instead (recommended if using multiple tools)")
|
|
3547
|
+
);
|
|
3548
|
+
console.log();
|
|
3549
|
+
const fallbackOptions = [
|
|
3550
|
+
{
|
|
3551
|
+
value: "api_key",
|
|
3552
|
+
label: "\u{1F4CB} Use API key instead",
|
|
3553
|
+
hint: `Get from ${displayInfo.apiKeyUrl}`
|
|
3554
|
+
},
|
|
3555
|
+
{
|
|
3556
|
+
value: "retry",
|
|
3557
|
+
label: "\u{1F504} Retry (after closing other tools)",
|
|
3558
|
+
hint: "Check port again"
|
|
3731
3559
|
}
|
|
3560
|
+
];
|
|
3561
|
+
if (config?.deviceAuthEndpoint) {
|
|
3562
|
+
fallbackOptions.push({
|
|
3563
|
+
value: "device_code",
|
|
3564
|
+
label: "\u{1F511} Try device code flow",
|
|
3565
|
+
hint: "May be blocked by Cloudflare"
|
|
3566
|
+
});
|
|
3732
3567
|
}
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
*/
|
|
3750
|
-
async isAvailable() {
|
|
3751
|
-
try {
|
|
3752
|
-
const tokenResult = await getValidAccessToken("openai");
|
|
3753
|
-
return tokenResult !== null;
|
|
3754
|
-
} catch {
|
|
3755
|
-
return false;
|
|
3756
|
-
}
|
|
3568
|
+
fallbackOptions.push({
|
|
3569
|
+
value: "cancel",
|
|
3570
|
+
label: "\u274C Cancel",
|
|
3571
|
+
hint: ""
|
|
3572
|
+
});
|
|
3573
|
+
const fallback = await p26.select({
|
|
3574
|
+
message: "What would you like to do?",
|
|
3575
|
+
options: fallbackOptions
|
|
3576
|
+
});
|
|
3577
|
+
if (p26.isCancel(fallback) || fallback === "cancel") return null;
|
|
3578
|
+
if (fallback === "api_key") {
|
|
3579
|
+
return runApiKeyFlow(provider);
|
|
3580
|
+
} else if (fallback === "device_code") {
|
|
3581
|
+
return runDeviceCodeFlow(provider);
|
|
3582
|
+
} else if (fallback === "retry") {
|
|
3583
|
+
return runBrowserOAuthFlow(provider);
|
|
3757
3584
|
}
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3585
|
+
return null;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
console.log(chalk2.dim(" Starting authentication server..."));
|
|
3589
|
+
try {
|
|
3590
|
+
const pkce = generatePKCECredentials();
|
|
3591
|
+
const { port, resultPromise } = await createCallbackServer(pkce.state);
|
|
3592
|
+
const redirectUri = `http://localhost:${port}/auth/callback`;
|
|
3593
|
+
const authUrl = buildAuthorizationUrl(
|
|
3594
|
+
oauthProvider,
|
|
3595
|
+
redirectUri,
|
|
3596
|
+
pkce.codeChallenge,
|
|
3597
|
+
pkce.state
|
|
3598
|
+
);
|
|
3599
|
+
console.log(chalk2.green(` \u2713 Server ready on port ${port}`));
|
|
3600
|
+
console.log();
|
|
3601
|
+
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3602
|
+
console.log(
|
|
3603
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.white(`${displayInfo.authDescription}`.padEnd(47)) + chalk2.magenta("\u2502")
|
|
3604
|
+
);
|
|
3605
|
+
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3606
|
+
console.log(
|
|
3607
|
+
chalk2.magenta(" \u2502 ") + chalk2.dim("A browser window will open for you to sign in.") + chalk2.magenta(" \u2502")
|
|
3608
|
+
);
|
|
3609
|
+
console.log(
|
|
3610
|
+
chalk2.magenta(" \u2502 ") + chalk2.dim("After signing in, you'll be redirected back.") + chalk2.magenta(" \u2502")
|
|
3611
|
+
);
|
|
3612
|
+
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3613
|
+
console.log();
|
|
3614
|
+
const openIt = await p26.confirm({
|
|
3615
|
+
message: "Open browser to sign in?",
|
|
3616
|
+
initialValue: true
|
|
3617
|
+
});
|
|
3618
|
+
if (p26.isCancel(openIt)) return null;
|
|
3619
|
+
if (openIt) {
|
|
3620
|
+
const opened = await openBrowser(authUrl);
|
|
3621
|
+
if (opened) {
|
|
3622
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3623
|
+
} else {
|
|
3624
|
+
const fallbackOpened = await openBrowserFallback(authUrl);
|
|
3625
|
+
if (fallbackOpened) {
|
|
3626
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3627
|
+
} else {
|
|
3628
|
+
console.log(chalk2.dim(" Could not open browser automatically."));
|
|
3629
|
+
console.log(chalk2.dim(" Please open this URL manually:"));
|
|
3630
|
+
console.log();
|
|
3631
|
+
printAuthUrl(authUrl);
|
|
3632
|
+
console.log();
|
|
3781
3633
|
}
|
|
3782
|
-
return response;
|
|
3783
3634
|
}
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3635
|
+
} else {
|
|
3636
|
+
console.log(chalk2.dim(" Please open this URL in your browser:"));
|
|
3637
|
+
console.log();
|
|
3638
|
+
printAuthUrl(authUrl);
|
|
3639
|
+
console.log();
|
|
3640
|
+
}
|
|
3641
|
+
const spinner18 = p26.spinner();
|
|
3642
|
+
spinner18.start("Waiting for you to sign in...");
|
|
3643
|
+
const callbackResult = await resultPromise;
|
|
3644
|
+
spinner18.stop(chalk2.green("\u2713 Authentication received!"));
|
|
3645
|
+
console.log(chalk2.dim(" Exchanging code for tokens..."));
|
|
3646
|
+
const tokens = await exchangeCodeForTokens(
|
|
3647
|
+
oauthProvider,
|
|
3648
|
+
callbackResult.code,
|
|
3649
|
+
pkce.codeVerifier,
|
|
3650
|
+
redirectUri
|
|
3651
|
+
);
|
|
3652
|
+
await saveTokens(oauthProvider, tokens);
|
|
3653
|
+
console.log(chalk2.green("\n \u2705 Authentication complete!\n"));
|
|
3654
|
+
if (oauthProvider === "openai") {
|
|
3655
|
+
console.log(chalk2.dim(" Your ChatGPT Plus/Pro subscription is now linked."));
|
|
3656
|
+
}
|
|
3657
|
+
console.log(chalk2.dim(" Tokens are securely stored in ~/.coco/tokens/\n"));
|
|
3658
|
+
return { tokens, accessToken: tokens.accessToken };
|
|
3659
|
+
} catch (error) {
|
|
3660
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3661
|
+
console.log();
|
|
3662
|
+
console.log(chalk2.yellow(" \u26A0 Browser authentication failed"));
|
|
3663
|
+
const errorCategory = errorMsg.includes("timeout") || errorMsg.includes("Timeout") ? "Request timed out" : errorMsg.includes("network") || errorMsg.includes("ECONNREFUSED") || errorMsg.includes("fetch") ? "Network error" : errorMsg.includes("401") || errorMsg.includes("403") ? "Authorization denied" : errorMsg.includes("invalid_grant") || errorMsg.includes("invalid_client") ? "Invalid credentials" : "Authentication error (see debug logs for details)";
|
|
3664
|
+
console.log(chalk2.dim(` Error: ${errorCategory}`));
|
|
3665
|
+
console.log();
|
|
3666
|
+
const fallbackOptions = [];
|
|
3667
|
+
if (config?.deviceAuthEndpoint) {
|
|
3668
|
+
fallbackOptions.push({
|
|
3669
|
+
value: "device_code",
|
|
3670
|
+
label: "\u{1F511} Try device code flow",
|
|
3671
|
+
hint: "Enter code manually in browser"
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
fallbackOptions.push({
|
|
3675
|
+
value: "api_key",
|
|
3676
|
+
label: "\u{1F4CB} Use API key instead",
|
|
3677
|
+
hint: `Get from ${displayInfo.apiKeyUrl}`
|
|
3678
|
+
});
|
|
3679
|
+
fallbackOptions.push({
|
|
3680
|
+
value: "cancel",
|
|
3681
|
+
label: "\u274C Cancel",
|
|
3682
|
+
hint: ""
|
|
3683
|
+
});
|
|
3684
|
+
const fallback = await p26.select({
|
|
3685
|
+
message: "What would you like to do?",
|
|
3686
|
+
options: fallbackOptions
|
|
3687
|
+
});
|
|
3688
|
+
if (p26.isCancel(fallback) || fallback === "cancel") return null;
|
|
3689
|
+
if (fallback === "device_code") {
|
|
3690
|
+
return runDeviceCodeFlow(provider);
|
|
3691
|
+
} else {
|
|
3692
|
+
return runApiKeyFlow(provider);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
async function runDeviceCodeFlow(provider) {
|
|
3697
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3698
|
+
const displayInfo = getProviderDisplayInfo(provider);
|
|
3699
|
+
console.log();
|
|
3700
|
+
console.log(chalk2.dim(` Requesting device code from ${displayInfo.name}...`));
|
|
3701
|
+
try {
|
|
3702
|
+
const deviceCode = await requestDeviceCode(oauthProvider);
|
|
3703
|
+
console.log();
|
|
3704
|
+
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3705
|
+
console.log(
|
|
3706
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.white("Enter this code in your browser:") + chalk2.magenta(" \u2502")
|
|
3707
|
+
);
|
|
3708
|
+
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3709
|
+
console.log(
|
|
3710
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.cyan.bgBlack(` ${deviceCode.userCode} `) + chalk2.magenta(" \u2502")
|
|
3711
|
+
);
|
|
3712
|
+
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3713
|
+
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3714
|
+
console.log();
|
|
3715
|
+
const verificationUrl = deviceCode.verificationUriComplete || deviceCode.verificationUri;
|
|
3716
|
+
console.log(chalk2.cyan(` \u2192 ${verificationUrl}`));
|
|
3717
|
+
console.log();
|
|
3718
|
+
const openIt = await p26.confirm({
|
|
3719
|
+
message: "Open browser to sign in?",
|
|
3720
|
+
initialValue: true
|
|
3721
|
+
});
|
|
3722
|
+
if (p26.isCancel(openIt)) return null;
|
|
3723
|
+
if (openIt) {
|
|
3724
|
+
const opened = await openBrowser(verificationUrl);
|
|
3725
|
+
if (opened) {
|
|
3726
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3727
|
+
} else {
|
|
3728
|
+
const fallbackOpened = await openBrowserFallback(verificationUrl);
|
|
3729
|
+
if (fallbackOpened) {
|
|
3730
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3731
|
+
} else {
|
|
3732
|
+
console.log(chalk2.dim(" Copy the URL above and paste it in your browser"));
|
|
3797
3733
|
}
|
|
3798
|
-
return "";
|
|
3799
3734
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
const text13 = this.extractTextContent(msg);
|
|
3815
|
-
const role = msg.role === "system" ? "developer" : msg.role;
|
|
3816
|
-
const contentType = msg.role === "assistant" ? "output_text" : "input_text";
|
|
3817
|
-
return {
|
|
3818
|
-
type: "message",
|
|
3819
|
-
role,
|
|
3820
|
-
content: [{ type: contentType, text: text13 }]
|
|
3821
|
-
};
|
|
3822
|
-
});
|
|
3735
|
+
}
|
|
3736
|
+
console.log();
|
|
3737
|
+
const spinner18 = p26.spinner();
|
|
3738
|
+
spinner18.start("Waiting for you to sign in...");
|
|
3739
|
+
let pollCount = 0;
|
|
3740
|
+
const tokens = await pollForToken(
|
|
3741
|
+
oauthProvider,
|
|
3742
|
+
deviceCode.deviceCode,
|
|
3743
|
+
deviceCode.interval,
|
|
3744
|
+
deviceCode.expiresIn,
|
|
3745
|
+
() => {
|
|
3746
|
+
pollCount++;
|
|
3747
|
+
const dots = ".".repeat(pollCount % 3 + 1);
|
|
3748
|
+
spinner18.message(`Waiting for you to sign in${dots}`);
|
|
3823
3749
|
}
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
outputTokens
|
|
3904
|
-
}
|
|
3905
|
-
};
|
|
3750
|
+
);
|
|
3751
|
+
spinner18.stop(chalk2.green("\u2713 Signed in successfully!"));
|
|
3752
|
+
await saveTokens(oauthProvider, tokens);
|
|
3753
|
+
console.log(chalk2.green("\n \u2705 Authentication complete!\n"));
|
|
3754
|
+
if (oauthProvider === "openai") {
|
|
3755
|
+
console.log(chalk2.dim(" Your ChatGPT Plus/Pro subscription is now linked."));
|
|
3756
|
+
} else {
|
|
3757
|
+
console.log(chalk2.dim(` Your ${displayInfo.name} account is now linked.`));
|
|
3758
|
+
}
|
|
3759
|
+
console.log(chalk2.dim(" Tokens are securely stored in ~/.coco/tokens/\n"));
|
|
3760
|
+
return { tokens, accessToken: tokens.accessToken };
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3763
|
+
if (errorMsg.includes("Cloudflare") || errorMsg.includes("blocked") || errorMsg.includes("HTML instead of JSON") || errorMsg.includes("not supported")) {
|
|
3764
|
+
console.log();
|
|
3765
|
+
console.log(chalk2.yellow(" \u26A0 Device code flow unavailable"));
|
|
3766
|
+
console.log(chalk2.dim(" This can happen due to network restrictions."));
|
|
3767
|
+
console.log();
|
|
3768
|
+
const useFallback = await p26.confirm({
|
|
3769
|
+
message: "Use API key instead?",
|
|
3770
|
+
initialValue: true
|
|
3771
|
+
});
|
|
3772
|
+
if (p26.isCancel(useFallback) || !useFallback) return null;
|
|
3773
|
+
return runApiKeyFlow(provider);
|
|
3774
|
+
}
|
|
3775
|
+
const deviceErrorCategory = errorMsg.includes("timeout") || errorMsg.includes("expired") ? "Device code expired" : errorMsg.includes("denied") || errorMsg.includes("access_denied") ? "Access denied by user" : "Unexpected error during device code authentication";
|
|
3776
|
+
p26.log.error(chalk2.red(` Authentication failed: ${deviceErrorCategory}`));
|
|
3777
|
+
return null;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
async function runApiKeyFlow(provider) {
|
|
3781
|
+
if (provider === "copilot") {
|
|
3782
|
+
throw new Error("runApiKeyFlow called with copilot \u2014 use runCopilotDeviceFlow() instead");
|
|
3783
|
+
}
|
|
3784
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3785
|
+
const displayInfo = getProviderDisplayInfo(provider);
|
|
3786
|
+
const apiKeysUrl = displayInfo.apiKeyUrl;
|
|
3787
|
+
const keyPrefix = oauthProvider === "openai" ? "sk-" : oauthProvider === "gemini" ? "AI" : "";
|
|
3788
|
+
const keyPrefixHint = keyPrefix ? ` (starts with '${keyPrefix}')` : "";
|
|
3789
|
+
console.log();
|
|
3790
|
+
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3791
|
+
console.log(
|
|
3792
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.white(`\u{1F511} Get your ${displayInfo.name} API key:`.padEnd(47)) + chalk2.magenta("\u2502")
|
|
3793
|
+
);
|
|
3794
|
+
console.log(chalk2.magenta(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
|
|
3795
|
+
console.log(
|
|
3796
|
+
chalk2.magenta(" \u2502 ") + chalk2.dim("1. Sign in with your account") + chalk2.magenta(" \u2502")
|
|
3797
|
+
);
|
|
3798
|
+
console.log(
|
|
3799
|
+
chalk2.magenta(" \u2502 ") + chalk2.dim("2. Create a new API key") + chalk2.magenta(" \u2502")
|
|
3800
|
+
);
|
|
3801
|
+
console.log(
|
|
3802
|
+
chalk2.magenta(" \u2502 ") + chalk2.dim("3. Copy and paste it here") + chalk2.magenta(" \u2502")
|
|
3803
|
+
);
|
|
3804
|
+
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3805
|
+
console.log();
|
|
3806
|
+
try {
|
|
3807
|
+
const parsedUrl = new URL(apiKeysUrl);
|
|
3808
|
+
parsedUrl.search = "";
|
|
3809
|
+
console.log(chalk2.cyan(` \u2192 ${parsedUrl.toString()}`));
|
|
3810
|
+
} catch {
|
|
3811
|
+
console.log(chalk2.cyan(" \u2192 [provider API keys page]"));
|
|
3812
|
+
}
|
|
3813
|
+
console.log();
|
|
3814
|
+
const openIt = await p26.confirm({
|
|
3815
|
+
message: "Open browser to get API key?",
|
|
3816
|
+
initialValue: true
|
|
3817
|
+
});
|
|
3818
|
+
if (p26.isCancel(openIt)) return null;
|
|
3819
|
+
if (openIt) {
|
|
3820
|
+
const opened = await openBrowser(apiKeysUrl);
|
|
3821
|
+
if (opened) {
|
|
3822
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3823
|
+
} else {
|
|
3824
|
+
const fallbackOpened = await openBrowserFallback(apiKeysUrl);
|
|
3825
|
+
if (fallbackOpened) {
|
|
3826
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3827
|
+
} else {
|
|
3828
|
+
console.log(chalk2.dim(" Copy the URL above and paste it in your browser"));
|
|
3906
3829
|
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
return
|
|
3915
|
-
...response,
|
|
3916
|
-
toolCalls: []
|
|
3917
|
-
// Tools not yet supported in Codex provider
|
|
3918
|
-
};
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
console.log();
|
|
3833
|
+
const apiKey = await p26.password({
|
|
3834
|
+
message: `Paste your ${displayInfo.name} API key${keyPrefixHint}:`,
|
|
3835
|
+
validate: (value) => {
|
|
3836
|
+
if (!value || value.length < 10) {
|
|
3837
|
+
return "Please enter a valid API key";
|
|
3919
3838
|
}
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3839
|
+
if (keyPrefix && !value.startsWith(keyPrefix)) {
|
|
3840
|
+
return `${displayInfo.name} API keys typically start with '${keyPrefix}'`;
|
|
3841
|
+
}
|
|
3842
|
+
return;
|
|
3843
|
+
}
|
|
3844
|
+
});
|
|
3845
|
+
if (p26.isCancel(apiKey)) return null;
|
|
3846
|
+
const tokens = {
|
|
3847
|
+
accessToken: apiKey,
|
|
3848
|
+
tokenType: "Bearer"
|
|
3849
|
+
};
|
|
3850
|
+
await saveTokens(oauthProvider, tokens);
|
|
3851
|
+
console.log(chalk2.green("\n \u2705 API key saved!\n"));
|
|
3852
|
+
return { tokens, accessToken: apiKey };
|
|
3853
|
+
}
|
|
3854
|
+
async function runCopilotDeviceFlow() {
|
|
3855
|
+
console.log();
|
|
3856
|
+
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3857
|
+
console.log(
|
|
3858
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.white("\u{1F419} GitHub Copilot Authentication".padEnd(47)) + chalk2.magenta("\u2502")
|
|
3859
|
+
);
|
|
3860
|
+
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3861
|
+
console.log();
|
|
3862
|
+
console.log(chalk2.dim(" Requires an active GitHub Copilot subscription."));
|
|
3863
|
+
console.log(chalk2.dim(" https://github.com/settings/copilot"));
|
|
3864
|
+
console.log();
|
|
3865
|
+
try {
|
|
3866
|
+
console.log(chalk2.dim(" Requesting device code from GitHub..."));
|
|
3867
|
+
const deviceCode = await requestGitHubDeviceCode();
|
|
3868
|
+
console.log();
|
|
3869
|
+
console.log(chalk2.magenta(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
3870
|
+
console.log(
|
|
3871
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.white("Enter this code in your browser:") + chalk2.magenta(" \u2502")
|
|
3872
|
+
);
|
|
3873
|
+
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3874
|
+
console.log(
|
|
3875
|
+
chalk2.magenta(" \u2502 ") + chalk2.bold.cyan.bgBlack(` ${deviceCode.user_code} `) + chalk2.magenta(" \u2502")
|
|
3876
|
+
);
|
|
3877
|
+
console.log(chalk2.magenta(" \u2502 \u2502"));
|
|
3878
|
+
console.log(chalk2.magenta(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
3879
|
+
console.log();
|
|
3880
|
+
console.log(chalk2.cyan(` \u2192 ${deviceCode.verification_uri}`));
|
|
3881
|
+
console.log();
|
|
3882
|
+
const openIt = await p26.confirm({
|
|
3883
|
+
message: "Open browser to sign in?",
|
|
3884
|
+
initialValue: true
|
|
3885
|
+
});
|
|
3886
|
+
if (p26.isCancel(openIt)) return null;
|
|
3887
|
+
if (openIt) {
|
|
3888
|
+
const opened = await openBrowser(deviceCode.verification_uri);
|
|
3889
|
+
if (opened) {
|
|
3890
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3891
|
+
} else {
|
|
3892
|
+
const fallbackOpened = await openBrowserFallback(deviceCode.verification_uri);
|
|
3893
|
+
if (fallbackOpened) {
|
|
3894
|
+
console.log(chalk2.green(" \u2713 Browser opened"));
|
|
3895
|
+
} else {
|
|
3896
|
+
console.log(chalk2.dim(" Copy the URL above and paste it in your browser"));
|
|
3937
3897
|
}
|
|
3938
|
-
yield { type: "done", stopReason: response.stopReason };
|
|
3939
3898
|
}
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3899
|
+
}
|
|
3900
|
+
console.log();
|
|
3901
|
+
const spinner18 = p26.spinner();
|
|
3902
|
+
spinner18.start("Waiting for you to sign in on GitHub...");
|
|
3903
|
+
let pollCount = 0;
|
|
3904
|
+
const githubToken = await pollGitHubForToken(
|
|
3905
|
+
deviceCode.device_code,
|
|
3906
|
+
deviceCode.interval,
|
|
3907
|
+
deviceCode.expires_in,
|
|
3908
|
+
() => {
|
|
3909
|
+
pollCount++;
|
|
3910
|
+
const dots = ".".repeat(pollCount % 3 + 1);
|
|
3911
|
+
spinner18.message(`Waiting for you to sign in on GitHub${dots}`);
|
|
3947
3912
|
}
|
|
3913
|
+
);
|
|
3914
|
+
spinner18.stop(chalk2.green("\u2713 GitHub authentication successful!"));
|
|
3915
|
+
console.log(chalk2.dim(" Exchanging token for Copilot access..."));
|
|
3916
|
+
const copilotToken = await exchangeForCopilotToken(githubToken);
|
|
3917
|
+
const creds = {
|
|
3918
|
+
githubToken,
|
|
3919
|
+
copilotToken: copilotToken.token,
|
|
3920
|
+
copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
|
|
3921
|
+
accountType: copilotToken.annotations?.copilot_plan
|
|
3922
|
+
};
|
|
3923
|
+
await saveCopilotCredentials(creds);
|
|
3924
|
+
const planType = creds.accountType ?? "individual";
|
|
3925
|
+
console.log(chalk2.green("\n \u2705 GitHub Copilot authenticated!\n"));
|
|
3926
|
+
console.log(chalk2.dim(` Plan: ${planType}`));
|
|
3927
|
+
console.log(chalk2.dim(" Credentials stored in ~/.coco/tokens/copilot.json\n"));
|
|
3928
|
+
const tokens = {
|
|
3929
|
+
accessToken: copilotToken.token,
|
|
3930
|
+
tokenType: "Bearer",
|
|
3931
|
+
expiresAt: copilotToken.expires_at * 1e3
|
|
3932
|
+
};
|
|
3933
|
+
return { tokens, accessToken: copilotToken.token };
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3936
|
+
console.log();
|
|
3937
|
+
if (errorMsg.includes("403") || errorMsg.includes("not enabled")) {
|
|
3938
|
+
console.log(chalk2.red(" \u2717 GitHub Copilot is not enabled for this account."));
|
|
3939
|
+
console.log(chalk2.dim(" Please ensure you have an active Copilot subscription:"));
|
|
3940
|
+
console.log(chalk2.cyan(" \u2192 https://github.com/settings/copilot"));
|
|
3941
|
+
} else if (errorMsg.includes("expired") || errorMsg.includes("timed out")) {
|
|
3942
|
+
console.log(chalk2.yellow(" \u26A0 Authentication timed out. Please try again."));
|
|
3943
|
+
} else if (errorMsg.includes("denied")) {
|
|
3944
|
+
console.log(chalk2.yellow(" \u26A0 Access was denied."));
|
|
3945
|
+
} else {
|
|
3946
|
+
const category = errorMsg.includes("network") || errorMsg.includes("fetch") ? "Network error" : "Authentication error";
|
|
3947
|
+
console.log(chalk2.red(` \u2717 ${category}`));
|
|
3948
|
+
}
|
|
3949
|
+
console.log();
|
|
3950
|
+
return null;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
async function getOrRefreshOAuthToken(provider) {
|
|
3954
|
+
if (provider === "copilot") {
|
|
3955
|
+
const tokenResult = await getValidCopilotToken();
|
|
3956
|
+
if (tokenResult) {
|
|
3957
|
+
return { accessToken: tokenResult.token };
|
|
3958
|
+
}
|
|
3959
|
+
const flowResult2 = await runOAuthFlow(provider);
|
|
3960
|
+
if (flowResult2) {
|
|
3961
|
+
return { accessToken: flowResult2.accessToken };
|
|
3962
|
+
}
|
|
3963
|
+
return null;
|
|
3964
|
+
}
|
|
3965
|
+
const oauthProvider = getOAuthProviderName(provider);
|
|
3966
|
+
const result = await getValidAccessToken(oauthProvider);
|
|
3967
|
+
if (result) {
|
|
3968
|
+
return { accessToken: result.accessToken };
|
|
3969
|
+
}
|
|
3970
|
+
const flowResult = await runOAuthFlow(provider);
|
|
3971
|
+
if (flowResult) {
|
|
3972
|
+
return { accessToken: flowResult.accessToken };
|
|
3973
|
+
}
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
var execFileAsync;
|
|
3977
|
+
var init_flow = __esm({
|
|
3978
|
+
"src/auth/flow.ts"() {
|
|
3979
|
+
init_oauth();
|
|
3980
|
+
init_pkce();
|
|
3981
|
+
init_callback_server();
|
|
3982
|
+
init_platform();
|
|
3983
|
+
init_copilot();
|
|
3984
|
+
execFileAsync = promisify(execFile);
|
|
3985
|
+
}
|
|
3986
|
+
});
|
|
3987
|
+
function getADCPath() {
|
|
3988
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3989
|
+
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
3990
|
+
return process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
3991
|
+
}
|
|
3992
|
+
return path36.join(home, ".config", "gcloud", "application_default_credentials.json");
|
|
3993
|
+
}
|
|
3994
|
+
async function isGcloudInstalled() {
|
|
3995
|
+
try {
|
|
3996
|
+
await execAsync("gcloud --version");
|
|
3997
|
+
return true;
|
|
3998
|
+
} catch {
|
|
3999
|
+
return false;
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
async function hasADCCredentials() {
|
|
4003
|
+
const adcPath = getADCPath();
|
|
4004
|
+
try {
|
|
4005
|
+
await fs34.access(adcPath);
|
|
4006
|
+
return true;
|
|
4007
|
+
} catch {
|
|
4008
|
+
return false;
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
async function getADCAccessToken() {
|
|
4012
|
+
try {
|
|
4013
|
+
const { stdout } = await execAsync("gcloud auth application-default print-access-token", {
|
|
4014
|
+
timeout: 1e4
|
|
4015
|
+
});
|
|
4016
|
+
const accessToken = stdout.trim();
|
|
4017
|
+
if (!accessToken) return null;
|
|
4018
|
+
const expiresAt = Date.now() + 55 * 60 * 1e3;
|
|
4019
|
+
return {
|
|
4020
|
+
accessToken,
|
|
4021
|
+
expiresAt
|
|
3948
4022
|
};
|
|
4023
|
+
} catch (error) {
|
|
4024
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4025
|
+
if (message.includes("not logged in") || message.includes("no application default credentials")) {
|
|
4026
|
+
return null;
|
|
4027
|
+
}
|
|
4028
|
+
return null;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
async function isADCConfigured() {
|
|
4032
|
+
const hasCredentials = await hasADCCredentials();
|
|
4033
|
+
if (!hasCredentials) return false;
|
|
4034
|
+
const token = await getADCAccessToken();
|
|
4035
|
+
return token !== null;
|
|
4036
|
+
}
|
|
4037
|
+
async function getCachedADCToken() {
|
|
4038
|
+
if (cachedToken && cachedToken.expiresAt && Date.now() < cachedToken.expiresAt) {
|
|
4039
|
+
return cachedToken;
|
|
4040
|
+
}
|
|
4041
|
+
cachedToken = await getADCAccessToken();
|
|
4042
|
+
return cachedToken;
|
|
4043
|
+
}
|
|
4044
|
+
var execAsync, cachedToken;
|
|
4045
|
+
var init_gcloud = __esm({
|
|
4046
|
+
"src/auth/gcloud.ts"() {
|
|
4047
|
+
execAsync = promisify(exec);
|
|
4048
|
+
cachedToken = null;
|
|
3949
4049
|
}
|
|
3950
4050
|
});
|
|
3951
|
-
|
|
3952
|
-
|
|
4051
|
+
|
|
4052
|
+
// src/auth/index.ts
|
|
4053
|
+
var init_auth = __esm({
|
|
4054
|
+
"src/auth/index.ts"() {
|
|
4055
|
+
init_oauth();
|
|
4056
|
+
init_pkce();
|
|
4057
|
+
init_callback_server();
|
|
4058
|
+
init_flow();
|
|
4059
|
+
init_copilot();
|
|
4060
|
+
init_gcloud();
|
|
4061
|
+
}
|
|
4062
|
+
});
|
|
4063
|
+
|
|
4064
|
+
// src/providers/codex.ts
|
|
4065
|
+
function parseJwtClaims(token) {
|
|
4066
|
+
const parts = token.split(".");
|
|
4067
|
+
if (parts.length !== 3 || !parts[1]) return void 0;
|
|
4068
|
+
try {
|
|
4069
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
4070
|
+
} catch {
|
|
4071
|
+
return void 0;
|
|
4072
|
+
}
|
|
3953
4073
|
}
|
|
3954
|
-
function
|
|
3955
|
-
|
|
4074
|
+
function extractAccountId(accessToken) {
|
|
4075
|
+
const claims = parseJwtClaims(accessToken);
|
|
4076
|
+
if (!claims) return void 0;
|
|
4077
|
+
const auth = claims["https://api.openai.com/auth"];
|
|
4078
|
+
return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
|
|
3956
4079
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
4080
|
+
function createCodexProvider(config) {
|
|
4081
|
+
const provider = new CodexProvider();
|
|
4082
|
+
if (config) {
|
|
4083
|
+
provider.initialize(config).catch(() => {
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
return provider;
|
|
4087
|
+
}
|
|
4088
|
+
var CODEX_API_ENDPOINT, DEFAULT_MODEL3, CONTEXT_WINDOWS3, CodexProvider;
|
|
4089
|
+
var init_codex = __esm({
|
|
4090
|
+
"src/providers/codex.ts"() {
|
|
3960
4091
|
init_errors();
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
"
|
|
3967
|
-
"
|
|
3968
|
-
"
|
|
3969
|
-
"
|
|
3970
|
-
"
|
|
3971
|
-
|
|
3972
|
-
"gpt-4.1": 1048576,
|
|
3973
|
-
// OpenAI models — /responses API (Codex/GPT-5+)
|
|
3974
|
-
"gpt-5.3-codex": 4e5,
|
|
3975
|
-
"gpt-5.2-codex": 4e5,
|
|
3976
|
-
"gpt-5.1-codex-max": 4e5,
|
|
3977
|
-
"gpt-5.2": 4e5,
|
|
3978
|
-
"gpt-5.1": 4e5,
|
|
3979
|
-
// Google models
|
|
3980
|
-
"gemini-3.1-pro-preview": 1e6,
|
|
3981
|
-
"gemini-3-flash-preview": 1e6,
|
|
3982
|
-
"gemini-2.5-pro": 1048576
|
|
3983
|
-
};
|
|
3984
|
-
DEFAULT_MODEL4 = "claude-sonnet-4.6";
|
|
3985
|
-
COPILOT_HEADERS = {
|
|
3986
|
-
"Copilot-Integration-Id": "vscode-chat",
|
|
3987
|
-
"Editor-Version": "vscode/1.99.0",
|
|
3988
|
-
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
3989
|
-
"X-GitHub-Api-Version": "2025-04-01"
|
|
4092
|
+
init_auth();
|
|
4093
|
+
CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
|
4094
|
+
DEFAULT_MODEL3 = "gpt-5.3-codex";
|
|
4095
|
+
CONTEXT_WINDOWS3 = {
|
|
4096
|
+
"gpt-5.3-codex": 2e5,
|
|
4097
|
+
"gpt-5.2-codex": 2e5,
|
|
4098
|
+
"gpt-5-codex": 2e5,
|
|
4099
|
+
"gpt-5.1-codex": 2e5,
|
|
4100
|
+
"gpt-5": 2e5,
|
|
4101
|
+
"gpt-5.2": 2e5,
|
|
4102
|
+
"gpt-5.1": 2e5
|
|
3990
4103
|
};
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
super("copilot", "GitHub Copilot");
|
|
3998
|
-
}
|
|
4104
|
+
CodexProvider = class {
|
|
4105
|
+
id = "codex";
|
|
4106
|
+
name = "OpenAI Codex (ChatGPT Plus/Pro)";
|
|
4107
|
+
config = {};
|
|
4108
|
+
accessToken = null;
|
|
4109
|
+
accountId;
|
|
3999
4110
|
/**
|
|
4000
|
-
* Initialize the provider with
|
|
4001
|
-
*
|
|
4002
|
-
* Gets a valid Copilot API token (from cache or by refreshing),
|
|
4003
|
-
* then creates an OpenAI client configured for the Copilot endpoint.
|
|
4111
|
+
* Initialize the provider with OAuth tokens
|
|
4004
4112
|
*/
|
|
4005
4113
|
async initialize(config) {
|
|
4006
|
-
this.config =
|
|
4007
|
-
|
|
4008
|
-
model: config.model ?? DEFAULT_MODEL4
|
|
4009
|
-
};
|
|
4010
|
-
const tokenResult = await getValidCopilotToken();
|
|
4114
|
+
this.config = config;
|
|
4115
|
+
const tokenResult = await getValidAccessToken("openai");
|
|
4011
4116
|
if (tokenResult) {
|
|
4012
|
-
this.
|
|
4013
|
-
this.
|
|
4117
|
+
this.accessToken = tokenResult.accessToken;
|
|
4118
|
+
this.accountId = extractAccountId(tokenResult.accessToken);
|
|
4014
4119
|
} else if (config.apiKey) {
|
|
4015
|
-
this.
|
|
4120
|
+
this.accessToken = config.apiKey;
|
|
4121
|
+
this.accountId = extractAccountId(config.apiKey);
|
|
4016
4122
|
}
|
|
4017
|
-
if (!this.
|
|
4123
|
+
if (!this.accessToken) {
|
|
4018
4124
|
throw new ProviderError(
|
|
4019
|
-
"No
|
|
4125
|
+
"No OAuth token found. Please run authentication first with: coco --provider openai",
|
|
4020
4126
|
{ provider: this.id }
|
|
4021
4127
|
);
|
|
4022
4128
|
}
|
|
4023
|
-
this.createCopilotClient();
|
|
4024
|
-
}
|
|
4025
|
-
/**
|
|
4026
|
-
* Create the OpenAI client configured for Copilot API
|
|
4027
|
-
*/
|
|
4028
|
-
createCopilotClient() {
|
|
4029
|
-
this.client = new OpenAI({
|
|
4030
|
-
apiKey: this.currentToken,
|
|
4031
|
-
baseURL: this.config.baseUrl ?? this.baseUrl,
|
|
4032
|
-
timeout: this.config.timeout ?? 12e4,
|
|
4033
|
-
defaultHeaders: COPILOT_HEADERS
|
|
4034
|
-
});
|
|
4035
4129
|
}
|
|
4036
4130
|
/**
|
|
4037
|
-
*
|
|
4038
|
-
*
|
|
4039
|
-
* Uses a mutex so concurrent callers share a single in-flight token
|
|
4040
|
-
* exchange. The slot is cleared inside the IIFE's finally block,
|
|
4041
|
-
* which runs after all awaiting callers have resumed.
|
|
4131
|
+
* Ensure provider is initialized
|
|
4042
4132
|
*/
|
|
4043
|
-
|
|
4044
|
-
if (!this.
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
if (tokenResult && tokenResult.isNew) {
|
|
4049
|
-
this.currentToken = tokenResult.token;
|
|
4050
|
-
this.baseUrl = tokenResult.baseUrl;
|
|
4051
|
-
this.createCopilotClient();
|
|
4052
|
-
}
|
|
4053
|
-
} finally {
|
|
4054
|
-
this.refreshPromise = null;
|
|
4055
|
-
}
|
|
4056
|
-
})();
|
|
4057
|
-
}
|
|
4058
|
-
await this.refreshPromise;
|
|
4059
|
-
}
|
|
4060
|
-
// --- Override public methods to add token refresh + Responses API routing ---
|
|
4061
|
-
async chat(messages, options) {
|
|
4062
|
-
await this.refreshTokenIfNeeded();
|
|
4063
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
|
|
4064
|
-
if (needsResponsesApi(model)) {
|
|
4065
|
-
return this.chatViaResponses(messages, options);
|
|
4066
|
-
}
|
|
4067
|
-
return super.chat(messages, options);
|
|
4068
|
-
}
|
|
4069
|
-
async chatWithTools(messages, options) {
|
|
4070
|
-
await this.refreshTokenIfNeeded();
|
|
4071
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
|
|
4072
|
-
if (needsResponsesApi(model)) {
|
|
4073
|
-
return this.chatWithToolsViaResponses(messages, options);
|
|
4074
|
-
}
|
|
4075
|
-
return super.chatWithTools(messages, options);
|
|
4076
|
-
}
|
|
4077
|
-
// Note: Token is refreshed before the stream starts but NOT mid-stream.
|
|
4078
|
-
// Copilot tokens last ~25 min. Very long streams may get a 401 mid-stream
|
|
4079
|
-
// which surfaces as a ProviderError. The retry layer handles re-attempts.
|
|
4080
|
-
async *stream(messages, options) {
|
|
4081
|
-
await this.refreshTokenIfNeeded();
|
|
4082
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
|
|
4083
|
-
if (needsResponsesApi(model)) {
|
|
4084
|
-
yield* this.streamViaResponses(messages, options);
|
|
4085
|
-
return;
|
|
4086
|
-
}
|
|
4087
|
-
yield* super.stream(messages, options);
|
|
4088
|
-
}
|
|
4089
|
-
async *streamWithTools(messages, options) {
|
|
4090
|
-
await this.refreshTokenIfNeeded();
|
|
4091
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
|
|
4092
|
-
if (needsResponsesApi(model)) {
|
|
4093
|
-
yield* this.streamWithToolsViaResponses(messages, options);
|
|
4094
|
-
return;
|
|
4133
|
+
ensureInitialized() {
|
|
4134
|
+
if (!this.accessToken) {
|
|
4135
|
+
throw new ProviderError("Provider not initialized", {
|
|
4136
|
+
provider: this.id
|
|
4137
|
+
});
|
|
4095
4138
|
}
|
|
4096
|
-
yield* super.streamWithTools(messages, options);
|
|
4097
4139
|
}
|
|
4098
|
-
// --- Responses API implementations ---
|
|
4099
4140
|
/**
|
|
4100
|
-
*
|
|
4141
|
+
* Get context window size for a model
|
|
4101
4142
|
*/
|
|
4102
|
-
|
|
4103
|
-
this.
|
|
4104
|
-
return
|
|
4105
|
-
try {
|
|
4106
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
|
|
4107
|
-
const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
|
|
4108
|
-
const response = await this.client.responses.create({
|
|
4109
|
-
model,
|
|
4110
|
-
input,
|
|
4111
|
-
instructions: instructions ?? void 0,
|
|
4112
|
-
max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
4113
|
-
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
4114
|
-
store: false
|
|
4115
|
-
});
|
|
4116
|
-
return {
|
|
4117
|
-
id: response.id,
|
|
4118
|
-
content: response.output_text ?? "",
|
|
4119
|
-
stopReason: response.status === "completed" ? "end_turn" : "max_tokens",
|
|
4120
|
-
usage: {
|
|
4121
|
-
inputTokens: response.usage?.input_tokens ?? 0,
|
|
4122
|
-
outputTokens: response.usage?.output_tokens ?? 0
|
|
4123
|
-
},
|
|
4124
|
-
model: String(response.model)
|
|
4125
|
-
};
|
|
4126
|
-
} catch (error) {
|
|
4127
|
-
throw this.handleError(error);
|
|
4128
|
-
}
|
|
4129
|
-
}, DEFAULT_RETRY_CONFIG);
|
|
4143
|
+
getContextWindow(model) {
|
|
4144
|
+
const m = model ?? this.config.model ?? DEFAULT_MODEL3;
|
|
4145
|
+
return CONTEXT_WINDOWS3[m] ?? 128e3;
|
|
4130
4146
|
}
|
|
4131
4147
|
/**
|
|
4132
|
-
*
|
|
4148
|
+
* Count tokens in text (approximate)
|
|
4149
|
+
* Uses GPT-4 approximation: ~4 chars per token
|
|
4133
4150
|
*/
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
return withRetry(async () => {
|
|
4137
|
-
try {
|
|
4138
|
-
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
|
|
4139
|
-
const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
|
|
4140
|
-
const tools = this.convertToolsForResponses(options.tools);
|
|
4141
|
-
const response = await this.client.responses.create({
|
|
4142
|
-
model,
|
|
4143
|
-
input,
|
|
4144
|
-
instructions: instructions ?? void 0,
|
|
4145
|
-
tools,
|
|
4146
|
-
max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
4147
|
-
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
4148
|
-
store: false
|
|
4149
|
-
});
|
|
4150
|
-
let content = "";
|
|
4151
|
-
const toolCalls = [];
|
|
4152
|
-
for (const item of response.output) {
|
|
4153
|
-
if (item.type === "message") {
|
|
4154
|
-
for (const part of item.content) {
|
|
4155
|
-
if (part.type === "output_text") {
|
|
4156
|
-
content += part.text;
|
|
4157
|
-
}
|
|
4158
|
-
}
|
|
4159
|
-
} else if (item.type === "function_call") {
|
|
4160
|
-
toolCalls.push({
|
|
4161
|
-
id: item.call_id,
|
|
4162
|
-
name: item.name,
|
|
4163
|
-
input: this.parseToolArguments(item.arguments)
|
|
4164
|
-
});
|
|
4165
|
-
}
|
|
4166
|
-
}
|
|
4167
|
-
return {
|
|
4168
|
-
id: response.id,
|
|
4169
|
-
content,
|
|
4170
|
-
stopReason: response.status === "completed" ? "end_turn" : "tool_use",
|
|
4171
|
-
usage: {
|
|
4172
|
-
inputTokens: response.usage?.input_tokens ?? 0,
|
|
4173
|
-
outputTokens: response.usage?.output_tokens ?? 0
|
|
4174
|
-
},
|
|
4175
|
-
model: String(response.model),
|
|
4176
|
-
toolCalls
|
|
4177
|
-
};
|
|
4178
|
-
} catch (error) {
|
|
4179
|
-
throw this.handleError(error);
|
|
4180
|
-
}
|
|
4181
|
-
}, DEFAULT_RETRY_CONFIG);
|
|
4151
|
+
countTokens(text13) {
|
|
4152
|
+
return Math.ceil(text13.length / 4);
|
|
4182
4153
|
}
|
|
4183
4154
|
/**
|
|
4184
|
-
*
|
|
4155
|
+
* Check if provider is available (has valid OAuth tokens)
|
|
4185
4156
|
*/
|
|
4186
|
-
async
|
|
4187
|
-
this.ensureInitialized();
|
|
4157
|
+
async isAvailable() {
|
|
4188
4158
|
try {
|
|
4189
|
-
const
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
input,
|
|
4194
|
-
instructions: instructions ?? void 0,
|
|
4195
|
-
max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
4196
|
-
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
4197
|
-
store: false,
|
|
4198
|
-
stream: true
|
|
4199
|
-
});
|
|
4200
|
-
for await (const event of stream) {
|
|
4201
|
-
if (event.type === "response.output_text.delta") {
|
|
4202
|
-
yield { type: "text", text: event.delta };
|
|
4203
|
-
} else if (event.type === "response.completed") {
|
|
4204
|
-
yield { type: "done", stopReason: "end_turn" };
|
|
4205
|
-
}
|
|
4206
|
-
}
|
|
4207
|
-
} catch (error) {
|
|
4208
|
-
throw this.handleError(error);
|
|
4159
|
+
const tokenResult = await getValidAccessToken("openai");
|
|
4160
|
+
return tokenResult !== null;
|
|
4161
|
+
} catch {
|
|
4162
|
+
return false;
|
|
4209
4163
|
}
|
|
4210
4164
|
}
|
|
4211
4165
|
/**
|
|
4212
|
-
*
|
|
4166
|
+
* Make a request to the Codex API
|
|
4213
4167
|
*/
|
|
4214
|
-
async
|
|
4168
|
+
async makeRequest(body) {
|
|
4215
4169
|
this.ensureInitialized();
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4170
|
+
const headers = {
|
|
4171
|
+
"Content-Type": "application/json",
|
|
4172
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
4173
|
+
};
|
|
4174
|
+
if (this.accountId) {
|
|
4175
|
+
headers["ChatGPT-Account-Id"] = this.accountId;
|
|
4176
|
+
}
|
|
4177
|
+
const response = await fetch(CODEX_API_ENDPOINT, {
|
|
4178
|
+
method: "POST",
|
|
4179
|
+
headers,
|
|
4180
|
+
body: JSON.stringify(body)
|
|
4181
|
+
});
|
|
4182
|
+
if (!response.ok) {
|
|
4183
|
+
const errorText = await response.text();
|
|
4184
|
+
throw new ProviderError(`Codex API error: ${response.status} - ${errorText}`, {
|
|
4185
|
+
provider: this.id,
|
|
4186
|
+
statusCode: response.status
|
|
4229
4187
|
});
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4188
|
+
}
|
|
4189
|
+
return response;
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Extract text content from a message
|
|
4193
|
+
*/
|
|
4194
|
+
extractTextContent(msg) {
|
|
4195
|
+
if (typeof msg.content === "string") {
|
|
4196
|
+
return msg.content;
|
|
4197
|
+
}
|
|
4198
|
+
if (Array.isArray(msg.content)) {
|
|
4199
|
+
return msg.content.map((part) => {
|
|
4200
|
+
if (part.type === "text") return part.text;
|
|
4201
|
+
if (part.type === "tool_result") return `Tool result: ${JSON.stringify(part.content)}`;
|
|
4202
|
+
return "";
|
|
4203
|
+
}).join("\n");
|
|
4204
|
+
}
|
|
4205
|
+
return "";
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Convert messages to Codex Responses API format
|
|
4209
|
+
* Codex uses a different format than Chat Completions:
|
|
4210
|
+
* {
|
|
4211
|
+
* "input": [
|
|
4212
|
+
* { "type": "message", "role": "developer|user", "content": [{ "type": "input_text", "text": "..." }] },
|
|
4213
|
+
* { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] }
|
|
4214
|
+
* ]
|
|
4215
|
+
* }
|
|
4216
|
+
*
|
|
4217
|
+
* IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
|
|
4218
|
+
*/
|
|
4219
|
+
convertMessagesToResponsesFormat(messages) {
|
|
4220
|
+
return messages.map((msg) => {
|
|
4221
|
+
const text13 = this.extractTextContent(msg);
|
|
4222
|
+
const role = msg.role === "system" ? "developer" : msg.role;
|
|
4223
|
+
const contentType = msg.role === "assistant" ? "output_text" : "input_text";
|
|
4224
|
+
return {
|
|
4225
|
+
type: "message",
|
|
4226
|
+
role,
|
|
4227
|
+
content: [{ type: contentType, text: text13 }]
|
|
4228
|
+
};
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
/**
|
|
4232
|
+
* Send a chat message using Codex Responses API format
|
|
4233
|
+
*/
|
|
4234
|
+
async chat(messages, options) {
|
|
4235
|
+
const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
|
|
4236
|
+
const systemMsg = messages.find((m) => m.role === "system");
|
|
4237
|
+
const instructions = systemMsg ? this.extractTextContent(systemMsg) : "You are a helpful coding assistant.";
|
|
4238
|
+
const inputMessages = messages.filter((m) => m.role !== "system").map((msg) => this.convertMessagesToResponsesFormat([msg])[0]);
|
|
4239
|
+
const body = {
|
|
4240
|
+
model,
|
|
4241
|
+
instructions,
|
|
4242
|
+
input: inputMessages,
|
|
4243
|
+
tools: [],
|
|
4244
|
+
store: false,
|
|
4245
|
+
stream: true
|
|
4246
|
+
// Codex API requires streaming
|
|
4247
|
+
};
|
|
4248
|
+
const response = await this.makeRequest(body);
|
|
4249
|
+
if (!response.body) {
|
|
4250
|
+
throw new ProviderError("No response body from Codex API", {
|
|
4251
|
+
provider: this.id
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
const reader = response.body.getReader();
|
|
4255
|
+
const decoder = new TextDecoder();
|
|
4256
|
+
let buffer = "";
|
|
4257
|
+
let content = "";
|
|
4258
|
+
let responseId = `codex-${Date.now()}`;
|
|
4259
|
+
let inputTokens = 0;
|
|
4260
|
+
let outputTokens = 0;
|
|
4261
|
+
let status = "completed";
|
|
4262
|
+
try {
|
|
4263
|
+
while (true) {
|
|
4264
|
+
const { done, value } = await reader.read();
|
|
4265
|
+
if (done) break;
|
|
4266
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4267
|
+
const lines = buffer.split("\n");
|
|
4268
|
+
buffer = lines.pop() ?? "";
|
|
4269
|
+
for (const line of lines) {
|
|
4270
|
+
if (line.startsWith("data: ")) {
|
|
4271
|
+
const data = line.slice(6).trim();
|
|
4272
|
+
if (!data || data === "[DONE]") continue;
|
|
4273
|
+
try {
|
|
4274
|
+
const parsed = JSON.parse(data);
|
|
4275
|
+
if (parsed.id) {
|
|
4276
|
+
responseId = parsed.id;
|
|
4271
4277
|
}
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
input: this.parseToolArguments(builder.arguments)
|
|
4283
|
-
}
|
|
4284
|
-
};
|
|
4278
|
+
if (parsed.type === "response.output_text.delta" && parsed.delta) {
|
|
4279
|
+
content += parsed.delta;
|
|
4280
|
+
} else if (parsed.type === "response.completed" && parsed.response) {
|
|
4281
|
+
if (parsed.response.usage) {
|
|
4282
|
+
inputTokens = parsed.response.usage.input_tokens ?? 0;
|
|
4283
|
+
outputTokens = parsed.response.usage.output_tokens ?? 0;
|
|
4284
|
+
}
|
|
4285
|
+
status = parsed.response.status ?? "completed";
|
|
4286
|
+
} else if (parsed.type === "response.output_text.done" && parsed.text) {
|
|
4287
|
+
content = parsed.text;
|
|
4285
4288
|
}
|
|
4286
|
-
|
|
4287
|
-
const hasToolCalls = event.response.output.some((i) => i.type === "function_call");
|
|
4288
|
-
yield {
|
|
4289
|
-
type: "done",
|
|
4290
|
-
stopReason: hasToolCalls ? "tool_use" : "end_turn"
|
|
4291
|
-
};
|
|
4289
|
+
} catch {
|
|
4292
4290
|
}
|
|
4293
|
-
|
|
4291
|
+
}
|
|
4294
4292
|
}
|
|
4295
4293
|
}
|
|
4296
|
-
}
|
|
4297
|
-
|
|
4294
|
+
} finally {
|
|
4295
|
+
reader.releaseLock();
|
|
4296
|
+
}
|
|
4297
|
+
if (!content) {
|
|
4298
|
+
throw new ProviderError("No response content from Codex API", {
|
|
4299
|
+
provider: this.id
|
|
4300
|
+
});
|
|
4298
4301
|
}
|
|
4302
|
+
const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
|
|
4303
|
+
return {
|
|
4304
|
+
id: responseId,
|
|
4305
|
+
content,
|
|
4306
|
+
stopReason,
|
|
4307
|
+
model,
|
|
4308
|
+
usage: {
|
|
4309
|
+
inputTokens,
|
|
4310
|
+
outputTokens
|
|
4311
|
+
}
|
|
4312
|
+
};
|
|
4299
4313
|
}
|
|
4300
|
-
// --- Responses API helpers ---
|
|
4301
4314
|
/**
|
|
4302
|
-
*
|
|
4303
|
-
*
|
|
4304
|
-
*
|
|
4305
|
-
* function_call, function_call_output) instead of the chat completions
|
|
4306
|
-
* messages array.
|
|
4315
|
+
* Send a chat message with tool use
|
|
4316
|
+
* Note: Codex Responses API tool support is complex; for now we delegate to chat()
|
|
4317
|
+
* and return empty toolCalls. Full tool support can be added later.
|
|
4307
4318
|
*/
|
|
4308
|
-
|
|
4309
|
-
const
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
role: "user",
|
|
4332
|
-
content: this.contentToStr(msg.content)
|
|
4333
|
-
});
|
|
4334
|
-
}
|
|
4335
|
-
} else if (msg.role === "assistant") {
|
|
4336
|
-
if (typeof msg.content === "string") {
|
|
4337
|
-
input.push({
|
|
4338
|
-
role: "assistant",
|
|
4339
|
-
content: msg.content
|
|
4340
|
-
});
|
|
4341
|
-
} else if (Array.isArray(msg.content)) {
|
|
4342
|
-
const textParts = [];
|
|
4343
|
-
for (const block of msg.content) {
|
|
4344
|
-
if (block.type === "text") {
|
|
4345
|
-
textParts.push(block.text);
|
|
4346
|
-
} else if (block.type === "tool_use") {
|
|
4347
|
-
if (textParts.length > 0) {
|
|
4348
|
-
input.push({
|
|
4349
|
-
role: "assistant",
|
|
4350
|
-
content: textParts.join("")
|
|
4351
|
-
});
|
|
4352
|
-
textParts.length = 0;
|
|
4353
|
-
}
|
|
4354
|
-
input.push({
|
|
4355
|
-
type: "function_call",
|
|
4356
|
-
call_id: block.id,
|
|
4357
|
-
name: block.name,
|
|
4358
|
-
arguments: JSON.stringify(block.input)
|
|
4359
|
-
});
|
|
4360
|
-
}
|
|
4361
|
-
}
|
|
4362
|
-
if (textParts.length > 0) {
|
|
4363
|
-
input.push({
|
|
4364
|
-
role: "assistant",
|
|
4365
|
-
content: textParts.join("")
|
|
4366
|
-
});
|
|
4367
|
-
}
|
|
4319
|
+
async chatWithTools(messages, options) {
|
|
4320
|
+
const response = await this.chat(messages, options);
|
|
4321
|
+
return {
|
|
4322
|
+
...response,
|
|
4323
|
+
toolCalls: []
|
|
4324
|
+
// Tools not yet supported in Codex provider
|
|
4325
|
+
};
|
|
4326
|
+
}
|
|
4327
|
+
/**
|
|
4328
|
+
* Stream a chat response
|
|
4329
|
+
* Note: True streaming with Codex Responses API is complex.
|
|
4330
|
+
* For now, we make a non-streaming call and simulate streaming by emitting chunks.
|
|
4331
|
+
*/
|
|
4332
|
+
async *stream(messages, options) {
|
|
4333
|
+
const response = await this.chat(messages, options);
|
|
4334
|
+
if (response.content) {
|
|
4335
|
+
const content = response.content;
|
|
4336
|
+
const chunkSize = 20;
|
|
4337
|
+
for (let i = 0; i < content.length; i += chunkSize) {
|
|
4338
|
+
const chunk = content.slice(i, i + chunkSize);
|
|
4339
|
+
yield { type: "text", text: chunk };
|
|
4340
|
+
if (i + chunkSize < content.length) {
|
|
4341
|
+
await new Promise((resolve4) => setTimeout(resolve4, 5));
|
|
4368
4342
|
}
|
|
4369
4343
|
}
|
|
4370
4344
|
}
|
|
4371
|
-
|
|
4345
|
+
yield { type: "done", stopReason: response.stopReason };
|
|
4372
4346
|
}
|
|
4373
4347
|
/**
|
|
4374
|
-
*
|
|
4348
|
+
* Stream a chat response with tool use
|
|
4349
|
+
* Note: Tools and true streaming with Codex Responses API are not yet implemented.
|
|
4350
|
+
* For now, we delegate to stream() which uses non-streaming under the hood.
|
|
4375
4351
|
*/
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4352
|
+
async *streamWithTools(messages, options) {
|
|
4353
|
+
yield* this.stream(messages, options);
|
|
4354
|
+
}
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
});
|
|
4358
|
+
function createCopilotProvider() {
|
|
4359
|
+
return new CopilotProvider();
|
|
4360
|
+
}
|
|
4361
|
+
var CONTEXT_WINDOWS4, DEFAULT_MODEL4, COPILOT_HEADERS, CopilotProvider;
|
|
4362
|
+
var init_copilot2 = __esm({
|
|
4363
|
+
"src/providers/copilot.ts"() {
|
|
4364
|
+
init_errors();
|
|
4365
|
+
init_openai();
|
|
4366
|
+
init_copilot();
|
|
4367
|
+
CONTEXT_WINDOWS4 = {
|
|
4368
|
+
// Claude models
|
|
4369
|
+
"claude-sonnet-4.6": 2e5,
|
|
4370
|
+
"claude-opus-4.6": 2e5,
|
|
4371
|
+
"claude-sonnet-4.5": 2e5,
|
|
4372
|
+
"claude-opus-4.5": 2e5,
|
|
4373
|
+
"claude-haiku-4.5": 2e5,
|
|
4374
|
+
// OpenAI models — chat/completions
|
|
4375
|
+
"gpt-4.1": 1048576,
|
|
4376
|
+
// OpenAI models — /responses API (Codex/GPT-5+)
|
|
4377
|
+
"gpt-5.3-codex": 4e5,
|
|
4378
|
+
"gpt-5.2-codex": 4e5,
|
|
4379
|
+
"gpt-5.1-codex-max": 4e5,
|
|
4380
|
+
"gpt-5.2": 4e5,
|
|
4381
|
+
"gpt-5.1": 4e5,
|
|
4382
|
+
// Google models
|
|
4383
|
+
"gemini-3.1-pro-preview": 1e6,
|
|
4384
|
+
"gemini-3-flash-preview": 1e6,
|
|
4385
|
+
"gemini-2.5-pro": 1048576
|
|
4386
|
+
};
|
|
4387
|
+
DEFAULT_MODEL4 = "claude-sonnet-4.6";
|
|
4388
|
+
COPILOT_HEADERS = {
|
|
4389
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
4390
|
+
"Editor-Version": "vscode/1.99.0",
|
|
4391
|
+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
4392
|
+
"X-GitHub-Api-Version": "2025-04-01"
|
|
4393
|
+
};
|
|
4394
|
+
CopilotProvider = class extends OpenAIProvider {
|
|
4395
|
+
baseUrl = "https://api.githubcopilot.com";
|
|
4396
|
+
currentToken = null;
|
|
4397
|
+
/** In-flight refresh promise to prevent concurrent token exchanges */
|
|
4398
|
+
refreshPromise = null;
|
|
4399
|
+
constructor() {
|
|
4400
|
+
super("copilot", "GitHub Copilot");
|
|
4384
4401
|
}
|
|
4385
4402
|
/**
|
|
4386
|
-
*
|
|
4403
|
+
* Initialize the provider with Copilot credentials.
|
|
4404
|
+
*
|
|
4405
|
+
* Gets a valid Copilot API token (from cache or by refreshing),
|
|
4406
|
+
* then creates an OpenAI client configured for the Copilot endpoint.
|
|
4387
4407
|
*/
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4408
|
+
async initialize(config) {
|
|
4409
|
+
this.config = {
|
|
4410
|
+
...config,
|
|
4411
|
+
model: config.model ?? DEFAULT_MODEL4
|
|
4412
|
+
};
|
|
4413
|
+
const tokenResult = await getValidCopilotToken();
|
|
4414
|
+
if (tokenResult) {
|
|
4415
|
+
this.currentToken = tokenResult.token;
|
|
4416
|
+
this.baseUrl = tokenResult.baseUrl;
|
|
4417
|
+
} else if (config.apiKey) {
|
|
4418
|
+
this.currentToken = config.apiKey;
|
|
4419
|
+
}
|
|
4420
|
+
if (!this.currentToken) {
|
|
4421
|
+
throw new ProviderError(
|
|
4422
|
+
"No Copilot token found. Please authenticate with: coco --provider copilot",
|
|
4423
|
+
{ provider: this.id }
|
|
4424
|
+
);
|
|
4401
4425
|
}
|
|
4426
|
+
this.createCopilotClient();
|
|
4402
4427
|
}
|
|
4403
4428
|
/**
|
|
4404
|
-
*
|
|
4429
|
+
* Create the OpenAI client configured for Copilot API
|
|
4430
|
+
*/
|
|
4431
|
+
createCopilotClient() {
|
|
4432
|
+
this.client = new OpenAI({
|
|
4433
|
+
apiKey: this.currentToken,
|
|
4434
|
+
baseURL: this.config.baseUrl ?? this.baseUrl,
|
|
4435
|
+
timeout: this.config.timeout ?? 12e4,
|
|
4436
|
+
defaultHeaders: COPILOT_HEADERS
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
/**
|
|
4440
|
+
* Refresh the Copilot token if expired.
|
|
4441
|
+
*
|
|
4442
|
+
* Uses a mutex so concurrent callers share a single in-flight token
|
|
4443
|
+
* exchange. The slot is cleared inside the IIFE's finally block,
|
|
4444
|
+
* which runs after all awaiting callers have resumed.
|
|
4405
4445
|
*/
|
|
4406
|
-
|
|
4407
|
-
if (
|
|
4408
|
-
|
|
4446
|
+
async refreshTokenIfNeeded() {
|
|
4447
|
+
if (!this.refreshPromise) {
|
|
4448
|
+
this.refreshPromise = (async () => {
|
|
4449
|
+
try {
|
|
4450
|
+
const tokenResult = await getValidCopilotToken();
|
|
4451
|
+
if (tokenResult && tokenResult.isNew) {
|
|
4452
|
+
this.currentToken = tokenResult.token;
|
|
4453
|
+
this.baseUrl = tokenResult.baseUrl;
|
|
4454
|
+
this.createCopilotClient();
|
|
4455
|
+
}
|
|
4456
|
+
} finally {
|
|
4457
|
+
this.refreshPromise = null;
|
|
4458
|
+
}
|
|
4459
|
+
})();
|
|
4460
|
+
}
|
|
4461
|
+
await this.refreshPromise;
|
|
4462
|
+
}
|
|
4463
|
+
// --- Override public methods to add token refresh ---
|
|
4464
|
+
async chat(messages, options) {
|
|
4465
|
+
await this.refreshTokenIfNeeded();
|
|
4466
|
+
return super.chat(messages, options);
|
|
4467
|
+
}
|
|
4468
|
+
async chatWithTools(messages, options) {
|
|
4469
|
+
await this.refreshTokenIfNeeded();
|
|
4470
|
+
return super.chatWithTools(messages, options);
|
|
4471
|
+
}
|
|
4472
|
+
async *stream(messages, options) {
|
|
4473
|
+
await this.refreshTokenIfNeeded();
|
|
4474
|
+
yield* super.stream(messages, options);
|
|
4475
|
+
}
|
|
4476
|
+
async *streamWithTools(messages, options) {
|
|
4477
|
+
await this.refreshTokenIfNeeded();
|
|
4478
|
+
yield* super.streamWithTools(messages, options);
|
|
4409
4479
|
}
|
|
4410
4480
|
// --- Override metadata methods ---
|
|
4411
4481
|
/**
|