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