@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 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 checkTimeout = () => {
1030
+ const timeoutController = new AbortController();
1031
+ const timeoutInterval = setInterval(() => {
1031
1032
  if (Date.now() - lastActivityTime > streamTimeout) {
1032
- throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1033
+ clearInterval(timeoutInterval);
1034
+ timeoutController.abort();
1033
1035
  }
1034
- };
1035
- const timeoutInterval = setInterval(checkTimeout, 5e3);
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 checkTimeout = () => {
1089
+ const timeoutController = new AbortController();
1090
+ const timeoutInterval = setInterval(() => {
1083
1091
  if (Date.now() - lastActivityTime > streamTimeout) {
1084
- throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1092
+ clearInterval(timeoutInterval);
1093
+ timeoutController.abort();
1085
1094
  }
1086
- };
1087
- const timeoutInterval = setInterval(checkTimeout, 5e3);
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.3-codex";
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.3-codex": 4e5,
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 checkTimeout = () => {
1748
+ const timeoutController = new AbortController();
1749
+ const timeoutInterval = setInterval(() => {
1717
1750
  if (Date.now() - lastActivityTime > streamTimeout) {
1718
- throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1751
+ clearInterval(timeoutInterval);
1752
+ timeoutController.abort();
1719
1753
  }
1720
- };
1721
- const timeoutInterval = setInterval(checkTimeout, 5e3);
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
- async function requestDeviceCode(provider) {
2161
- const config = OAUTH_CONFIGS[provider];
2162
- if (!config) {
2163
- throw new Error(`OAuth not supported for provider: ${provider}`);
2164
- }
2165
- if (!config.deviceAuthEndpoint) {
2166
- throw new Error(
2167
- `Device code flow not supported for provider: ${provider}. Use browser OAuth instead.`
2168
- );
2169
- }
2170
- const body = new URLSearchParams({
2171
- client_id: config.clientId,
2172
- scope: config.scopes.join(" ")
2173
- });
2174
- if (provider === "openai") {
2175
- body.set("audience", "https://api.openai.com/v1");
2176
- }
2177
- const response = await fetch(config.deviceAuthEndpoint, {
2178
- method: "POST",
2179
- headers: {
2180
- "Content-Type": "application/x-www-form-urlencoded",
2181
- "User-Agent": "Corbat-Coco CLI",
2182
- Accept: "application/json"
2183
- },
2184
- body: body.toString()
2185
- });
2186
- if (!response.ok) {
2187
- const contentType2 = response.headers.get("content-type") || "";
2188
- const error = await response.text();
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
- * OpenAI OAuth (ChatGPT Plus/Pro subscriptions)
2400
- * Uses the official Codex client ID (same as OpenCode, Codex CLI, etc.)
2230
+ * Chat with tools via Responses API
2401
2231
  */
2402
- openai: {
2403
- provider: "openai",
2404
- clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
2405
- authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
2406
- tokenEndpoint: "https://auth.openai.com/oauth/token",
2407
- deviceAuthEndpoint: "https://auth.openai.com/oauth/device/code",
2408
- verificationUri: "https://chatgpt.com/codex/device",
2409
- scopes: ["openid", "profile", "email", "offline_access"],
2410
- extraAuthParams: {
2411
- id_token_add_organizations: "true",
2412
- codex_cli_simplified_flow: "true",
2413
- originator: "opencode"
2414
- }
2415
- }
2416
- // NOTE: Gemini OAuth removed - Google's client ID is restricted to official apps
2417
- // Use API Key (https://aistudio.google.com/apikey) or gcloud ADC instead
2418
- };
2419
- }
2420
- });
2421
- function generateCodeVerifier(length = 64) {
2422
- const randomBytes2 = crypto.randomBytes(length);
2423
- return base64UrlEncode(randomBytes2);
2424
- }
2425
- function generateCodeChallenge(codeVerifier) {
2426
- const hash = crypto.createHash("sha256").update(codeVerifier).digest();
2427
- return base64UrlEncode(hash);
2428
- }
2429
- function generateState(length = 32) {
2430
- const randomBytes2 = crypto.randomBytes(length);
2431
- return base64UrlEncode(randomBytes2);
2432
- }
2433
- function base64UrlEncode(buffer) {
2434
- return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2435
- }
2436
- function generatePKCECredentials() {
2437
- const codeVerifier = generateCodeVerifier();
2438
- const codeChallenge = generateCodeChallenge(codeVerifier);
2439
- const state = generateState();
2440
- return {
2441
- codeVerifier,
2442
- codeChallenge,
2443
- state
2444
- };
2445
- }
2446
- var init_pkce = __esm({
2447
- "src/auth/pkce.ts"() {
2448
- }
2449
- });
2450
- function escapeHtml(unsafe) {
2451
- return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
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
- res.writeHead(200, { "Content-Type": "text/html" });
2503
- res.end(SUCCESS_HTML);
2504
- server.close();
2505
- resolveResult({ code, state });
2506
- } catch (err) {
2507
- res.writeHead(500, { "Content-Type": "text/html" });
2508
- res.end(ERROR_HTML(String(err)));
2509
- server.close();
2510
- rejectResult(err instanceof Error ? err : new Error(String(err)));
2511
- }
2512
- });
2513
- const actualPort = await new Promise((resolve4, reject) => {
2514
- const errorHandler = (err) => {
2515
- if (err.code === "EADDRINUSE") {
2516
- console.log(` Port ${port} is in use, trying alternative port...`);
2517
- server.removeListener("error", errorHandler);
2518
- server.listen(0, () => {
2519
- const address = server.address();
2520
- if (typeof address === "object" && address) {
2521
- resolve4(address.port);
2522
- } else {
2523
- reject(new Error("Failed to get server port"));
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
- } else {
2527
- reject(err);
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
- server.on("error", errorHandler);
2531
- server.listen(port, () => {
2532
- server.removeListener("error", errorHandler);
2533
- const address = server.address();
2534
- if (typeof address === "object" && address) {
2535
- resolve4(address.port);
2536
- } else {
2537
- reject(new Error("Failed to get server port"));
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
- const timeoutId = setTimeout(() => {
2542
- server.close();
2543
- rejectResult(new Error("Authentication timed out. Please try again."));
2544
- }, timeout);
2545
- server.on("close", () => {
2546
- clearTimeout(timeoutId);
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
- return { port: actualPort, resultPromise };
2549
- }
2550
- var OAUTH_CALLBACK_PORT, SUCCESS_HTML, ERROR_HTML;
2551
- var init_callback_server = __esm({
2552
- "src/auth/callback-server.ts"() {
2553
- OAUTH_CALLBACK_PORT = 1455;
2554
- SUCCESS_HTML = `
2555
- <!DOCTYPE html>
2556
- <html>
2557
- <head>
2558
- <meta charset="utf-8">
2559
- <meta name="viewport" content="width=device-width, initial-scale=1">
2560
- <title>Authentication Successful</title>
2561
- <style>
2562
- * { margin: 0; padding: 0; box-sizing: border-box; }
2563
- body {
2564
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
2565
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2566
- min-height: 100vh;
2567
- display: flex;
2568
- align-items: center;
2569
- justify-content: center;
2570
- }
2571
- .container {
2572
- background: white;
2573
- border-radius: 16px;
2574
- padding: 48px;
2575
- text-align: center;
2576
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
2577
- max-width: 400px;
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
- .checkmark {
2580
- width: 80px;
2581
- height: 80px;
2582
- margin: 0 auto 24px;
2583
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
2584
- border-radius: 50%;
2585
- display: flex;
2586
- align-items: center;
2587
- justify-content: center;
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
- var isWSL;
2731
- var init_platform = __esm({
2732
- "src/utils/platform.ts"() {
2733
- isWSL = detectWSL();
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
- return await response.json();
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 response = await fetch(GITHUB_TOKEN_URL, {
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/json",
2763
- Accept: "application/json"
2641
+ "Content-Type": "application/x-www-form-urlencoded"
2764
2642
  },
2765
- body: JSON.stringify({
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 data.access_token;
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 exchangeForCopilotToken(githubToken) {
2791
- const response = await fetch(COPILOT_TOKEN_URL, {
2792
- method: "GET",
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
- Authorization: `token ${githubToken}`,
2795
- Accept: "application/json",
2796
- "User-Agent": "Corbat-Coco/1.0"
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
- if (response.status === 401) {
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
- return DEFAULT_COPILOT_BASE_URL;
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 getCopilotCredentialsPath() {
2698
+ function getTokenStoragePath(provider) {
2824
2699
  const home = process.env.HOME || process.env.USERPROFILE || "";
2825
- return path36.join(home, ".coco", "tokens", "copilot.json");
2700
+ return path36.join(home, ".coco", "tokens", `${provider}.json`);
2826
2701
  }
2827
- async function saveCopilotCredentials(creds) {
2828
- const filePath = getCopilotCredentialsPath();
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(creds, null, 2), { mode: 384 });
2706
+ await fs34.writeFile(filePath, JSON.stringify(tokens, null, 2), { mode: 384 });
2832
2707
  }
2833
- async function loadCopilotCredentials() {
2708
+ async function loadTokens(provider) {
2709
+ const filePath = getTokenStoragePath(provider);
2834
2710
  try {
2835
- const content = await fs34.readFile(getCopilotCredentialsPath(), "utf-8");
2836
- const parsed = CopilotCredentialsSchema.safeParse(JSON.parse(content));
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 deleteCopilotCredentials() {
2717
+ async function deleteTokens(provider) {
2718
+ const filePath = getTokenStoragePath(provider);
2843
2719
  try {
2844
- await fs34.unlink(getCopilotCredentialsPath());
2720
+ await fs34.unlink(filePath);
2845
2721
  } catch {
2846
2722
  }
2847
2723
  }
2848
- function isCopilotTokenExpired(creds) {
2849
- if (!creds.copilotToken || !creds.copilotTokenExpiresAt) return true;
2850
- return Date.now() >= creds.copilotTokenExpiresAt - REFRESH_BUFFER_MS;
2724
+ function isTokenExpired(tokens) {
2725
+ if (!tokens.expiresAt) return false;
2726
+ return Date.now() >= tokens.expiresAt - 5 * 60 * 1e3;
2851
2727
  }
2852
- async function getValidCopilotToken() {
2853
- const creds = await loadCopilotCredentials();
2854
- if (!creds) return null;
2855
- const envToken = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
2856
- const githubToken = envToken || creds.githubToken;
2857
- if (!isCopilotTokenExpired(creds) && creds.copilotToken) {
2858
- return {
2859
- token: creds.copilotToken,
2860
- baseUrl: getCopilotBaseUrl(creds.accountType),
2861
- isNew: false
2862
- };
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
- try {
2865
- const copilotToken = await exchangeForCopilotToken(githubToken);
2866
- const updatedCreds = {
2867
- ...creds,
2868
- githubToken: creds.githubToken,
2869
- copilotToken: copilotToken.token,
2870
- copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
2871
- accountType: copilotToken.annotations?.copilot_plan ?? creds.accountType
2872
- };
2873
- await saveCopilotCredentials(updatedCreds);
2874
- return {
2875
- token: copilotToken.token,
2876
- baseUrl: getCopilotBaseUrl(updatedCreds.accountType),
2877
- isNew: true
2878
- };
2879
- } catch (error) {
2880
- if (error instanceof CopilotAuthError && error.permanent) {
2881
- await deleteCopilotCredentials();
2882
- return null;
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
- var COPILOT_CLIENT_ID, GITHUB_DEVICE_CODE_URL, GITHUB_TOKEN_URL, COPILOT_TOKEN_URL, COPILOT_BASE_URLS, DEFAULT_COPILOT_BASE_URL, REFRESH_BUFFER_MS, CopilotAuthError, CopilotCredentialsSchema;
2888
- var init_copilot = __esm({
2889
- "src/auth/copilot.ts"() {
2890
- COPILOT_CLIENT_ID = "Iv1.b507a08c87ecfe98";
2891
- GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
2892
- GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
2893
- COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
2894
- COPILOT_BASE_URLS = {
2895
- individual: "https://api.githubcopilot.com",
2896
- business: "https://api.business.githubcopilot.com",
2897
- enterprise: "https://api.enterprise.githubcopilot.com"
2898
- };
2899
- DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
2900
- REFRESH_BUFFER_MS = 6e4;
2901
- CopilotAuthError = class extends Error {
2902
- constructor(message, permanent) {
2903
- super(message);
2904
- this.permanent = permanent;
2905
- this.name = "CopilotAuthError";
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 getOAuthProviderName(provider) {
2917
- if (provider === "codex") return "openai";
2918
- return provider;
2829
+ function generateCodeVerifier(length = 64) {
2830
+ const randomBytes2 = crypto.randomBytes(length);
2831
+ return base64UrlEncode(randomBytes2);
2919
2832
  }
2920
- function getProviderDisplayInfo(provider) {
2921
- const oauthProvider = getOAuthProviderName(provider);
2922
- switch (oauthProvider) {
2923
- case "openai":
2924
- return {
2925
- name: "OpenAI",
2926
- emoji: "\u{1F7E2}",
2927
- authDescription: "Sign in with your ChatGPT account",
2928
- apiKeyUrl: "https://platform.openai.com/api-keys"
2929
- };
2930
- case "copilot":
2931
- return {
2932
- name: "GitHub Copilot",
2933
- emoji: "\u{1F419}",
2934
- authDescription: "Sign in with your GitHub account",
2935
- apiKeyUrl: "https://github.com/settings/copilot"
2936
- };
2937
- default:
2938
- return {
2939
- name: provider,
2940
- emoji: "\u{1F510}",
2941
- authDescription: "Sign in with your account",
2942
- apiKeyUrl: ""
2943
- };
2944
- }
2833
+ function generateCodeChallenge(codeVerifier) {
2834
+ const hash = crypto.createHash("sha256").update(codeVerifier).digest();
2835
+ return base64UrlEncode(hash);
2945
2836
  }
2946
- function supportsOAuth(provider) {
2947
- if (provider === "copilot") return true;
2948
- const oauthProvider = getOAuthProviderName(provider);
2949
- return oauthProvider in OAUTH_CONFIGS;
2837
+ function generateState(length = 32) {
2838
+ const randomBytes2 = crypto.randomBytes(length);
2839
+ return base64UrlEncode(randomBytes2);
2950
2840
  }
2951
- async function isOAuthConfigured(provider) {
2952
- if (provider === "copilot") {
2953
- const creds = await loadCopilotCredentials();
2954
- return creds !== null;
2955
- }
2956
- const oauthProvider = getOAuthProviderName(provider);
2957
- const tokens = await loadTokens(oauthProvider);
2958
- return tokens !== null;
2841
+ function base64UrlEncode(buffer) {
2842
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2959
2843
  }
2960
- function printAuthUrl(url) {
2961
- try {
2962
- const parsed = new URL(url);
2963
- const maskedParams = new URLSearchParams(parsed.searchParams);
2964
- if (maskedParams.has("client_id")) {
2965
- const clientId = maskedParams.get("client_id");
2966
- maskedParams.set("client_id", clientId.slice(0, 8) + "...");
2967
- }
2968
- parsed.search = maskedParams.toString();
2969
- console.log(chalk2.cyan(` ${parsed.toString()}`));
2970
- } catch {
2971
- console.log(chalk2.cyan(" [invalid URL]"));
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
2973
2860
  }
2974
- async function openBrowser(url) {
2975
- let sanitizedUrl;
2976
- try {
2977
- const parsed = new URL(url);
2978
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
2979
- return false;
2980
- }
2981
- sanitizedUrl = parsed.toString();
2982
- } catch {
2983
- return false;
2984
- }
2985
- const platform = process.platform;
2986
- try {
2987
- if (platform === "darwin") {
2988
- await execFileAsync("open", [sanitizedUrl]);
2989
- } else if (platform === "win32") {
2990
- await execFileAsync("rundll32", ["url.dll,FileProtocolHandler", sanitizedUrl]);
2991
- } else if (isWSL) {
2992
- await execFileAsync("cmd.exe", ["/c", "start", "", sanitizedUrl]);
2993
- } else {
2994
- await execFileAsync("xdg-open", [sanitizedUrl]);
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
- return true;
2997
- } catch {
2998
- return false;
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
- await execFileAsync(cmd, args);
3045
- return true;
3046
- } catch {
3047
- continue;
3048
- }
3049
- }
3050
- return false;
3051
- }
3052
- async function runOAuthFlow(provider) {
3053
- if (provider === "copilot") {
3054
- return runCopilotDeviceFlow();
3055
- }
3056
- const oauthProvider = getOAuthProviderName(provider);
3057
- const config = OAUTH_CONFIGS[oauthProvider];
3058
- if (!config) {
3059
- p26.log.error(`OAuth not supported for provider: ${provider}`);
3060
- return null;
3061
- }
3062
- const displayInfo = getProviderDisplayInfo(provider);
3063
- console.log();
3064
- 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"));
3065
- console.log(
3066
- chalk2.magenta(" \u2502 ") + chalk2.bold.white(`${displayInfo.emoji} ${displayInfo.name} Authentication`.padEnd(47)) + chalk2.magenta("\u2502")
3067
- );
3068
- 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"));
3069
- console.log();
3070
- const authOptions = [
3071
- {
3072
- value: "browser",
3073
- label: "\u{1F310} Sign in with browser",
3074
- hint: `${displayInfo.authDescription} (recommended)`
3075
- },
3076
- {
3077
- value: "api_key",
3078
- label: "\u{1F4CB} Paste API key manually",
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
- if (p26.isCancel(authMethod)) return null;
3087
- if (authMethod === "browser") {
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
- resolve4({ available: false, processName: "another process" });
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
- resolve4({ available: false });
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
- function getRequiredPort(provider) {
3112
- const oauthProvider = getOAuthProviderName(provider);
3113
- if (oauthProvider === "openai") return 1455;
3114
- return void 0;
3115
- }
3116
- async function runBrowserOAuthFlow(provider) {
3117
- const oauthProvider = getOAuthProviderName(provider);
3118
- const displayInfo = getProviderDisplayInfo(provider);
3119
- const config = OAUTH_CONFIGS[oauthProvider];
3120
- const requiredPort = getRequiredPort(provider);
3121
- if (requiredPort) {
3122
- console.log();
3123
- console.log(chalk2.dim(" Checking port availability..."));
3124
- const portCheck = await isPortAvailable(requiredPort);
3125
- if (!portCheck.available) {
3126
- console.log();
3127
- console.log(chalk2.yellow(` \u26A0 Port ${requiredPort} is already in use`));
3128
- console.log();
3129
- console.log(
3130
- chalk2.dim(
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
- console.log(chalk2.dim(" Starting authentication server..."));
3182
- try {
3183
- const pkce = generatePKCECredentials();
3184
- const { port, resultPromise } = await createCallbackServer(pkce.state);
3185
- const redirectUri = `http://localhost:${port}/auth/callback`;
3186
- const authUrl = buildAuthorizationUrl(
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
- const spinner18 = p26.spinner();
3235
- spinner18.start("Waiting for you to sign in...");
3236
- const callbackResult = await resultPromise;
3237
- spinner18.stop(chalk2.green("\u2713 Authentication received!"));
3238
- console.log(chalk2.dim(" Exchanging code for tokens..."));
3239
- const tokens = await exchangeCodeForTokens(
3240
- oauthProvider,
3241
- callbackResult.code,
3242
- pkce.codeVerifier,
3243
- redirectUri
3244
- );
3245
- await saveTokens(oauthProvider, tokens);
3246
- console.log(chalk2.green("\n \u2705 Authentication complete!\n"));
3247
- if (oauthProvider === "openai") {
3248
- console.log(chalk2.dim(" Your ChatGPT Plus/Pro subscription is now linked."));
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
- console.log(chalk2.dim(" Tokens are securely stored in ~/.coco/tokens/\n"));
3251
- return { tokens, accessToken: tokens.accessToken };
3252
- } catch (error) {
3253
- const errorMsg = error instanceof Error ? error.message : String(error);
3254
- console.log();
3255
- console.log(chalk2.yellow(" \u26A0 Browser authentication failed"));
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
- fallbackOptions.push({
3268
- value: "api_key",
3269
- label: "\u{1F4CB} Use API key instead",
3270
- hint: `Get from ${displayInfo.apiKeyUrl}`
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
- async function runDeviceCodeFlow(provider) {
3290
- const oauthProvider = getOAuthProviderName(provider);
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
- console.log();
3330
- const spinner18 = p26.spinner();
3331
- spinner18.start("Waiting for you to sign in...");
3332
- let pollCount = 0;
3333
- const tokens = await pollForToken(
3334
- oauthProvider,
3335
- deviceCode.deviceCode,
3336
- deviceCode.interval,
3337
- deviceCode.expiresIn,
3338
- () => {
3339
- pollCount++;
3340
- const dots = ".".repeat(pollCount % 3 + 1);
3341
- spinner18.message(`Waiting for you to sign in${dots}`);
3342
- }
3343
- );
3344
- spinner18.stop(chalk2.green("\u2713 Signed in successfully!"));
3345
- await saveTokens(oauthProvider, tokens);
3346
- console.log(chalk2.green("\n \u2705 Authentication complete!\n"));
3347
- if (oauthProvider === "openai") {
3348
- console.log(chalk2.dim(" Your ChatGPT Plus/Pro subscription is now linked."));
3349
- } else {
3350
- console.log(chalk2.dim(` Your ${displayInfo.name} account is now linked.`));
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
- console.log(chalk2.dim(" Tokens are securely stored in ~/.coco/tokens/\n"));
3353
- return { tokens, accessToken: tokens.accessToken };
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
- 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";
3369
- p26.log.error(chalk2.red(` Authentication failed: ${deviceErrorCategory}`));
3370
- return null;
3371
- }
3372
- }
3373
- async function runApiKeyFlow(provider) {
3374
- if (provider === "copilot") {
3375
- throw new Error("runApiKeyFlow called with copilot \u2014 use runCopilotDeviceFlow() instead");
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
- const oauthProvider = getOAuthProviderName(provider);
3378
- const displayInfo = getProviderDisplayInfo(provider);
3379
- const apiKeysUrl = displayInfo.apiKeyUrl;
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
- const parsedUrl = new URL(apiKeysUrl);
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
- console.log(chalk2.cyan(" \u2192 [provider API keys page]"));
3135
+ return false;
3405
3136
  }
3406
- console.log();
3407
- const openIt = await p26.confirm({
3408
- message: "Open browser to get API key?",
3409
- initialValue: true
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 (p26.isCancel(openIt)) return null;
3412
- if (openIt) {
3413
- const opened = await openBrowser(apiKeysUrl);
3414
- if (opened) {
3415
- console.log(chalk2.green(" \u2713 Browser opened"));
3416
- } else {
3417
- const fallbackOpened = await openBrowserFallback(apiKeysUrl);
3418
- if (fallbackOpened) {
3419
- console.log(chalk2.green(" \u2713 Browser opened"));
3420
- } else {
3421
- console.log(chalk2.dim(" Copy the URL above and paste it in your browser"));
3422
- }
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
- console.log();
3426
- const apiKey = await p26.password({
3427
- message: `Paste your ${displayInfo.name} API key${keyPrefixHint}:`,
3428
- validate: (value) => {
3429
- if (!value || value.length < 10) {
3430
- return "Please enter a valid API key";
3431
- }
3432
- if (keyPrefix && !value.startsWith(keyPrefix)) {
3433
- return `${displayInfo.name} API keys typically start with '${keyPrefix}'`;
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 (p26.isCancel(apiKey)) return null;
3439
- const tokens = {
3440
- accessToken: apiKey,
3441
- tokenType: "Bearer"
3442
- };
3443
- await saveTokens(oauthProvider, tokens);
3444
- console.log(chalk2.green("\n \u2705 API key saved!\n"));
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
- console.log();
3494
- const spinner18 = p26.spinner();
3495
- spinner18.start("Waiting for you to sign in on GitHub...");
3496
- let pollCount = 0;
3497
- const githubToken = await pollGitHubForToken(
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
- console.log();
3543
- return null;
3221
+ throw new Error(`Copilot token exchange failed: ${response.status} - ${error}`);
3544
3222
  }
3223
+ return await response.json();
3545
3224
  }
3546
- async function getOrRefreshOAuthToken(provider) {
3547
- if (provider === "copilot") {
3548
- const tokenResult = await getValidCopilotToken();
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 null;
3229
+ return DEFAULT_COPILOT_BASE_URL;
3568
3230
  }
3569
- var execFileAsync;
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
- if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
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 isGcloudInstalled() {
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 execAsync("gcloud --version");
3590
- return true;
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 false;
3247
+ return null;
3593
3248
  }
3594
3249
  }
3595
- async function hasADCCredentials() {
3596
- const adcPath = getADCPath();
3250
+ async function deleteCopilotCredentials() {
3597
3251
  try {
3598
- await fs34.access(adcPath);
3599
- return true;
3252
+ await fs34.unlink(getCopilotCredentialsPath());
3600
3253
  } catch {
3601
- return false;
3602
3254
  }
3603
3255
  }
3604
- async function getADCAccessToken() {
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 { stdout } = await execAsync("gcloud auth application-default print-access-token", {
3607
- timeout: 1e4
3608
- });
3609
- const accessToken = stdout.trim();
3610
- if (!accessToken) return null;
3611
- const expiresAt = Date.now() + 55 * 60 * 1e3;
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
- accessToken,
3614
- expiresAt
3283
+ token: copilotToken.token,
3284
+ baseUrl: getCopilotBaseUrl(updatedCreds.accountType),
3285
+ isNew: true
3615
3286
  };
3616
3287
  } catch (error) {
3617
- const message = error instanceof Error ? error.message : String(error);
3618
- if (message.includes("not logged in") || message.includes("no application default credentials")) {
3288
+ if (error instanceof CopilotAuthError && error.permanent) {
3289
+ await deleteCopilotCredentials();
3619
3290
  return null;
3620
3291
  }
3621
- return null;
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 execAsync, cachedToken;
3638
- var init_gcloud = __esm({
3639
- "src/auth/gcloud.ts"() {
3640
- execAsync = promisify(exec);
3641
- cachedToken = null;
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
- // src/auth/index.ts
3646
- var init_auth = __esm({
3647
- "src/auth/index.ts"() {
3648
- init_oauth();
3649
- init_pkce();
3650
- init_callback_server();
3651
- init_flow();
3652
- init_copilot();
3653
- init_gcloud();
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
- // src/providers/codex.ts
3658
- function parseJwtClaims(token) {
3659
- const parts = token.split(".");
3660
- if (parts.length !== 3 || !parts[1]) return void 0;
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
- return JSON.parse(Buffer.from(parts[1], "base64url").toString());
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
- return void 0;
3379
+ console.log(chalk2.cyan(" [invalid URL]"));
3665
3380
  }
3666
3381
  }
3667
- function extractAccountId(accessToken) {
3668
- const claims = parseJwtClaims(accessToken);
3669
- if (!claims) return void 0;
3670
- const auth = claims["https://api.openai.com/auth"];
3671
- return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
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 createCodexProvider(config) {
3674
- const provider = new CodexProvider();
3675
- if (config) {
3676
- provider.initialize(config).catch(() => {
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
- return provider;
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
- var CODEX_API_ENDPOINT, DEFAULT_MODEL3, CONTEXT_WINDOWS3, CodexProvider;
3682
- var init_codex = __esm({
3683
- "src/providers/codex.ts"() {
3684
- init_errors();
3685
- init_auth();
3686
- CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
3687
- DEFAULT_MODEL3 = "gpt-5.3-codex";
3688
- CONTEXT_WINDOWS3 = {
3689
- "gpt-5.3-codex": 2e5,
3690
- "gpt-5.2-codex": 2e5,
3691
- "gpt-5-codex": 2e5,
3692
- "gpt-5.1-codex": 2e5,
3693
- "gpt-5": 2e5,
3694
- "gpt-5.2": 2e5,
3695
- "gpt-5.1": 2e5
3696
- };
3697
- CodexProvider = class {
3698
- id = "codex";
3699
- name = "OpenAI Codex (ChatGPT Plus/Pro)";
3700
- config = {};
3701
- accessToken = null;
3702
- accountId;
3703
- /**
3704
- * Initialize the provider with OAuth tokens
3705
- */
3706
- async initialize(config) {
3707
- this.config = config;
3708
- const tokenResult = await getValidAccessToken("openai");
3709
- if (tokenResult) {
3710
- this.accessToken = tokenResult.accessToken;
3711
- this.accountId = extractAccountId(tokenResult.accessToken);
3712
- } else if (config.apiKey) {
3713
- this.accessToken = config.apiKey;
3714
- this.accountId = extractAccountId(config.apiKey);
3715
- }
3716
- if (!this.accessToken) {
3717
- throw new ProviderError(
3718
- "No OAuth token found. Please run authentication first with: coco --provider openai",
3719
- { provider: this.id }
3720
- );
3721
- }
3722
- }
3723
- /**
3724
- * Ensure provider is initialized
3725
- */
3726
- ensureInitialized() {
3727
- if (!this.accessToken) {
3728
- throw new ProviderError("Provider not initialized", {
3729
- provider: this.id
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
- * Get context window size for a model
3735
- */
3736
- getContextWindow(model) {
3737
- const m = model ?? this.config.model ?? DEFAULT_MODEL3;
3738
- return CONTEXT_WINDOWS3[m] ?? 128e3;
3739
- }
3740
- /**
3741
- * Count tokens in text (approximate)
3742
- * Uses GPT-4 approximation: ~4 chars per token
3743
- */
3744
- countTokens(text13) {
3745
- return Math.ceil(text13.length / 4);
3746
- }
3747
- /**
3748
- * Check if provider is available (has valid OAuth tokens)
3749
- */
3750
- async isAvailable() {
3751
- try {
3752
- const tokenResult = await getValidAccessToken("openai");
3753
- return tokenResult !== null;
3754
- } catch {
3755
- return false;
3756
- }
3757
- }
3758
- /**
3759
- * Make a request to the Codex API
3760
- */
3761
- async makeRequest(body) {
3762
- this.ensureInitialized();
3763
- const headers = {
3764
- "Content-Type": "application/json",
3765
- Authorization: `Bearer ${this.accessToken}`
3766
- };
3767
- if (this.accountId) {
3768
- headers["ChatGPT-Account-Id"] = this.accountId;
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
- const response = await fetch(CODEX_API_ENDPOINT, {
3771
- method: "POST",
3772
- headers,
3773
- body: JSON.stringify(body)
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
- * Extract text content from a message
3786
- */
3787
- extractTextContent(msg) {
3788
- if (typeof msg.content === "string") {
3789
- return msg.content;
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
- if (Array.isArray(msg.content)) {
3792
- return msg.content.map((part) => {
3793
- if (part.type === "text") return part.text;
3794
- if (part.type === "tool_result") return `Tool result: ${JSON.stringify(part.content)}`;
3795
- return "";
3796
- }).join("\n");
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
- * Convert messages to Codex Responses API format
3802
- * Codex uses a different format than Chat Completions:
3803
- * {
3804
- * "input": [
3805
- * { "type": "message", "role": "developer|user", "content": [{ "type": "input_text", "text": "..." }] },
3806
- * { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] }
3807
- * ]
3808
- * }
3809
- *
3810
- * IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
3811
- */
3812
- convertMessagesToResponsesFormat(messages) {
3813
- return messages.map((msg) => {
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
- * Send a chat message using Codex Responses API format
3826
- */
3827
- async chat(messages, options) {
3828
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
3829
- const systemMsg = messages.find((m) => m.role === "system");
3830
- const instructions = systemMsg ? this.extractTextContent(systemMsg) : "You are a helpful coding assistant.";
3831
- const inputMessages = messages.filter((m) => m.role !== "system").map((msg) => this.convertMessagesToResponsesFormat([msg])[0]);
3832
- const body = {
3833
- model,
3834
- instructions,
3835
- input: inputMessages,
3836
- tools: [],
3837
- store: false,
3838
- stream: true
3839
- // Codex API requires streaming
3840
- };
3841
- const response = await this.makeRequest(body);
3842
- if (!response.body) {
3843
- throw new ProviderError("No response body from Codex API", {
3844
- provider: this.id
3845
- });
3846
- }
3847
- const reader = response.body.getReader();
3848
- const decoder = new TextDecoder();
3849
- let buffer = "";
3850
- let content = "";
3851
- let responseId = `codex-${Date.now()}`;
3852
- let inputTokens = 0;
3853
- let outputTokens = 0;
3854
- let status = "completed";
3855
- try {
3856
- while (true) {
3857
- const { done, value } = await reader.read();
3858
- if (done) break;
3859
- buffer += decoder.decode(value, { stream: true });
3860
- const lines = buffer.split("\n");
3861
- buffer = lines.pop() ?? "";
3862
- for (const line of lines) {
3863
- if (line.startsWith("data: ")) {
3864
- const data = line.slice(6).trim();
3865
- if (!data || data === "[DONE]") continue;
3866
- try {
3867
- const parsed = JSON.parse(data);
3868
- if (parsed.id) {
3869
- responseId = parsed.id;
3870
- }
3871
- if (parsed.type === "response.output_text.delta" && parsed.delta) {
3872
- content += parsed.delta;
3873
- } else if (parsed.type === "response.completed" && parsed.response) {
3874
- if (parsed.response.usage) {
3875
- inputTokens = parsed.response.usage.input_tokens ?? 0;
3876
- outputTokens = parsed.response.usage.output_tokens ?? 0;
3877
- }
3878
- status = parsed.response.status ?? "completed";
3879
- } else if (parsed.type === "response.output_text.done" && parsed.text) {
3880
- content = parsed.text;
3881
- }
3882
- } catch {
3883
- }
3884
- }
3885
- }
3886
- }
3887
- } finally {
3888
- reader.releaseLock();
3889
- }
3890
- if (!content) {
3891
- throw new ProviderError("No response content from Codex API", {
3892
- provider: this.id
3893
- });
3894
- }
3895
- const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
3896
- return {
3897
- id: responseId,
3898
- content,
3899
- stopReason,
3900
- model,
3901
- usage: {
3902
- inputTokens,
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
- * Send a chat message with tool use
3909
- * Note: Codex Responses API tool support is complex; for now we delegate to chat()
3910
- * and return empty toolCalls. Full tool support can be added later.
3911
- */
3912
- async chatWithTools(messages, options) {
3913
- const response = await this.chat(messages, options);
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
- * Stream a chat response
3922
- * Note: True streaming with Codex Responses API is complex.
3923
- * For now, we make a non-streaming call and simulate streaming by emitting chunks.
3924
- */
3925
- async *stream(messages, options) {
3926
- const response = await this.chat(messages, options);
3927
- if (response.content) {
3928
- const content = response.content;
3929
- const chunkSize = 20;
3930
- for (let i = 0; i < content.length; i += chunkSize) {
3931
- const chunk = content.slice(i, i + chunkSize);
3932
- yield { type: "text", text: chunk };
3933
- if (i + chunkSize < content.length) {
3934
- await new Promise((resolve4) => setTimeout(resolve4, 5));
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
- * Stream a chat response with tool use
3942
- * Note: Tools and true streaming with Codex Responses API are not yet implemented.
3943
- * For now, we delegate to stream() which uses non-streaming under the hood.
3944
- */
3945
- async *streamWithTools(messages, options) {
3946
- yield* this.stream(messages, options);
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
- function needsResponsesApi(model) {
3952
- return model.includes("codex") || model.startsWith("gpt-5") || model.startsWith("o4-") || model.startsWith("o3-");
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 createCopilotProvider() {
3955
- return new CopilotProvider();
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
- var CONTEXT_WINDOWS4, DEFAULT_MODEL4, COPILOT_HEADERS, CopilotProvider;
3958
- var init_copilot2 = __esm({
3959
- "src/providers/copilot.ts"() {
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
- init_openai();
3962
- init_copilot();
3963
- init_retry();
3964
- CONTEXT_WINDOWS4 = {
3965
- // Claude models
3966
- "claude-sonnet-4.6": 2e5,
3967
- "claude-opus-4.6": 2e5,
3968
- "claude-sonnet-4.5": 2e5,
3969
- "claude-opus-4.5": 2e5,
3970
- "claude-haiku-4.5": 2e5,
3971
- // OpenAI models — chat/completions
3972
- "gpt-4.1": 1048576,
3973
- // OpenAI models — /responses API (Codex/GPT-5+)
3974
- "gpt-5.3-codex": 4e5,
3975
- "gpt-5.2-codex": 4e5,
3976
- "gpt-5.1-codex-max": 4e5,
3977
- "gpt-5.2": 4e5,
3978
- "gpt-5.1": 4e5,
3979
- // Google models
3980
- "gemini-3.1-pro-preview": 1e6,
3981
- "gemini-3-flash-preview": 1e6,
3982
- "gemini-2.5-pro": 1048576
3983
- };
3984
- DEFAULT_MODEL4 = "claude-sonnet-4.6";
3985
- COPILOT_HEADERS = {
3986
- "Copilot-Integration-Id": "vscode-chat",
3987
- "Editor-Version": "vscode/1.99.0",
3988
- "Editor-Plugin-Version": "copilot-chat/0.26.7",
3989
- "X-GitHub-Api-Version": "2025-04-01"
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
- CopilotProvider = class extends OpenAIProvider {
3992
- baseUrl = "https://api.githubcopilot.com";
3993
- currentToken = null;
3994
- /** In-flight refresh promise to prevent concurrent token exchanges */
3995
- refreshPromise = null;
3996
- constructor() {
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 Copilot credentials.
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
- ...config,
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.currentToken = tokenResult.token;
4013
- this.baseUrl = tokenResult.baseUrl;
4119
+ this.accessToken = tokenResult.accessToken;
4120
+ this.accountId = extractAccountId(tokenResult.accessToken);
4014
4121
  } else if (config.apiKey) {
4015
- this.currentToken = config.apiKey;
4122
+ this.accessToken = config.apiKey;
4123
+ this.accountId = extractAccountId(config.apiKey);
4016
4124
  }
4017
- if (!this.currentToken) {
4125
+ if (!this.accessToken) {
4018
4126
  throw new ProviderError(
4019
- "No Copilot token found. Please authenticate with: coco --provider copilot",
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
- * Simple chat via Responses API (no tools)
4101
- */
4102
- async chatViaResponses(messages, options) {
4103
- this.ensureInitialized();
4104
- return withRetry(async () => {
4105
- try {
4106
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
4107
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4108
- const response = await this.client.responses.create({
4109
- model,
4110
- input,
4111
- instructions: instructions ?? void 0,
4112
- max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
4113
- temperature: options?.temperature ?? this.config.temperature ?? 0,
4114
- store: false
4115
- });
4116
- return {
4117
- id: response.id,
4118
- content: response.output_text ?? "",
4119
- stopReason: response.status === "completed" ? "end_turn" : "max_tokens",
4120
- usage: {
4121
- inputTokens: response.usage?.input_tokens ?? 0,
4122
- outputTokens: response.usage?.output_tokens ?? 0
4123
- },
4124
- model: String(response.model)
4125
- };
4126
- } catch (error) {
4127
- throw this.handleError(error);
4128
- }
4129
- }, DEFAULT_RETRY_CONFIG);
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
- * Chat with tools via Responses API
4143
+ * Get context window size for a model
4133
4144
  */
4134
- async chatWithToolsViaResponses(messages, options) {
4135
- this.ensureInitialized();
4136
- return withRetry(async () => {
4137
- try {
4138
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
4139
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4140
- const tools = this.convertToolsForResponses(options.tools);
4141
- const response = await this.client.responses.create({
4142
- model,
4143
- input,
4144
- instructions: instructions ?? void 0,
4145
- tools,
4146
- max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
4147
- temperature: options?.temperature ?? this.config.temperature ?? 0,
4148
- store: false
4149
- });
4150
- let content = "";
4151
- const toolCalls = [];
4152
- for (const item of response.output) {
4153
- if (item.type === "message") {
4154
- for (const part of item.content) {
4155
- if (part.type === "output_text") {
4156
- content += part.text;
4157
- }
4158
- }
4159
- } else if (item.type === "function_call") {
4160
- toolCalls.push({
4161
- id: item.call_id,
4162
- name: item.name,
4163
- input: this.parseToolArguments(item.arguments)
4164
- });
4165
- }
4166
- }
4167
- return {
4168
- id: response.id,
4169
- content,
4170
- stopReason: response.status === "completed" ? "end_turn" : "tool_use",
4171
- usage: {
4172
- inputTokens: response.usage?.input_tokens ?? 0,
4173
- outputTokens: response.usage?.output_tokens ?? 0
4174
- },
4175
- model: String(response.model),
4176
- toolCalls
4177
- };
4178
- } catch (error) {
4179
- throw this.handleError(error);
4180
- }
4181
- }, DEFAULT_RETRY_CONFIG);
4145
+ getContextWindow(model) {
4146
+ const m = model ?? this.config.model ?? DEFAULT_MODEL3;
4147
+ return CONTEXT_WINDOWS3[m] ?? 128e3;
4182
4148
  }
4183
4149
  /**
4184
- * Stream via Responses API (no tools)
4150
+ * Count tokens in text (approximate)
4151
+ * Uses GPT-4 approximation: ~4 chars per token
4185
4152
  */
4186
- async *streamViaResponses(messages, options) {
4187
- this.ensureInitialized();
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 model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
4190
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4191
- const stream = await this.client.responses.create({
4192
- model,
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
- * Stream with tools via Responses API
4168
+ * Make a request to the Codex API
4213
4169
  */
4214
- async *streamWithToolsViaResponses(messages, options) {
4170
+ async makeRequest(body) {
4215
4171
  this.ensureInitialized();
4216
- try {
4217
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
4218
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4219
- const tools = this.convertToolsForResponses(options.tools);
4220
- const stream = await this.client.responses.create({
4221
- model,
4222
- input,
4223
- instructions: instructions ?? void 0,
4224
- tools,
4225
- max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
4226
- temperature: options?.temperature ?? this.config.temperature ?? 0,
4227
- store: false,
4228
- stream: true
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 our internal messages to Responses API input format.
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
- * The Responses API uses a flat array of input items (EasyInputMessage,
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
- convertToResponsesInput(messages, systemPrompt) {
4309
- const input = [];
4310
- let instructions = null;
4311
- if (systemPrompt) {
4312
- instructions = systemPrompt;
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
- for (const msg of messages) {
4315
- if (msg.role === "system") {
4316
- instructions = (instructions ? instructions + "\n\n" : "") + this.contentToStr(msg.content);
4317
- } else if (msg.role === "user") {
4318
- if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
4319
- for (const block of msg.content) {
4320
- if (block.type === "tool_result") {
4321
- const tr = block;
4322
- input.push({
4323
- type: "function_call_output",
4324
- call_id: tr.tool_use_id,
4325
- output: tr.content
4326
- });
4327
- }
4328
- }
4329
- } else {
4330
- input.push({
4331
- role: "user",
4332
- content: this.contentToStr(msg.content)
4333
- });
4334
- }
4335
- } else if (msg.role === "assistant") {
4336
- if (typeof msg.content === "string") {
4337
- input.push({
4338
- role: "assistant",
4339
- content: msg.content
4340
- });
4341
- } else if (Array.isArray(msg.content)) {
4342
- const textParts = [];
4343
- for (const block of msg.content) {
4344
- if (block.type === "text") {
4345
- textParts.push(block.text);
4346
- } else if (block.type === "tool_use") {
4347
- if (textParts.length > 0) {
4348
- input.push({
4349
- role: "assistant",
4350
- content: textParts.join("")
4351
- });
4352
- textParts.length = 0;
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
- input.push({
4355
- type: "function_call",
4356
- call_id: block.id,
4357
- name: block.name,
4358
- arguments: JSON.stringify(block.input)
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
- return { input, instructions };
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
- * Convert our tool definitions to Responses API FunctionTool format
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
- convertToolsForResponses(tools) {
4377
- return tools.map((tool) => ({
4378
- type: "function",
4379
- name: tool.name,
4380
- description: tool.description ?? void 0,
4381
- parameters: tool.input_schema ?? null,
4382
- strict: false
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
- * Parse tool call arguments with jsonrepair fallback
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
- parseToolArguments(args) {
4389
- try {
4390
- return args ? JSON.parse(args) : {};
4391
- } catch {
4392
- try {
4393
- if (args) {
4394
- const repaired = jsonrepair(args);
4395
- return JSON.parse(repaired);
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
- * Convert message content to string
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
- contentToStr(content) {
4407
- if (typeof content === "string") return content;
4408
- return content.filter((b) => b.type === "text").map((b) => b.text).join("");
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.3-codex";
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.3-codex";
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.3-codex";
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.3-codex",
25669
- name: "GPT-5.3 Codex",
25670
- description: "Latest agentic coding model (Feb 2026)",
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.3-codex",
25805
- name: "GPT-5.3 Codex",
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.3-codex",
25883
- name: "GPT-5.3 Codex",
25884
- description: "Latest coding model via ChatGPT subscription (Feb 2026)",
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.3-codex",
26306
- name: "GPT-5.3 Codex (via OR)",
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
  },