@corbat-tech/coco 2.11.0 → 2.11.1

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