@corbat-tech/coco 2.27.4 → 2.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -13,10 +13,10 @@ import JSON5 from 'json5';
13
13
  import * as crypto from 'crypto';
14
14
  import { randomUUID, randomBytes, createHash } from 'crypto';
15
15
  import * as http from 'http';
16
- import * as p26 from '@clack/prompts';
17
- import chalk from 'chalk';
18
16
  import { execFile, execSync, spawn, execFileSync, exec } from 'child_process';
19
17
  import { promisify } from 'util';
18
+ import * as p26 from '@clack/prompts';
19
+ import chalk from 'chalk';
20
20
  import { Logger } from 'tslog';
21
21
  import Anthropic from '@anthropic-ai/sdk';
22
22
  import { jsonrepair } from 'jsonrepair';
@@ -1477,6 +1477,32 @@ async function exchangeForCopilotToken(githubToken) {
1477
1477
  }
1478
1478
  return await response.json();
1479
1479
  }
1480
+ async function getGitHubLogin(githubToken) {
1481
+ try {
1482
+ const response = await fetch("https://api.github.com/user", {
1483
+ method: "GET",
1484
+ headers: {
1485
+ Authorization: `token ${githubToken}`,
1486
+ Accept: "application/json",
1487
+ "User-Agent": "Corbat-Coco/1.0"
1488
+ }
1489
+ });
1490
+ if (!response.ok) return null;
1491
+ const data = await response.json();
1492
+ return data.login ?? null;
1493
+ } catch {
1494
+ return null;
1495
+ }
1496
+ }
1497
+ async function getGitHubCliToken() {
1498
+ try {
1499
+ const { stdout } = await execFileAsync("gh", ["auth", "token"], { timeout: 5e3 });
1500
+ const token = stdout.trim();
1501
+ return token.length > 0 ? token : null;
1502
+ } catch {
1503
+ return null;
1504
+ }
1505
+ }
1480
1506
  function getCopilotBaseUrl(accountType) {
1481
1507
  if (accountType && accountType in COPILOT_BASE_URLS) {
1482
1508
  return COPILOT_BASE_URLS[accountType];
@@ -1514,10 +1540,11 @@ function isCopilotTokenExpired(creds) {
1514
1540
  }
1515
1541
  async function getValidCopilotToken() {
1516
1542
  const creds = await loadCopilotCredentials();
1517
- if (!creds) return null;
1518
- const envToken = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
1519
- const githubToken = envToken || creds.githubToken;
1520
- if (!isCopilotTokenExpired(creds) && creds.copilotToken) {
1543
+ const envToken = process.env["COPILOT_GITHUB_TOKEN"] || process.env["GH_TOKEN"] || process.env["GITHUB_TOKEN"];
1544
+ const fallbackGhToken = await getGitHubCliToken();
1545
+ const githubToken = envToken || creds?.githubToken || fallbackGhToken;
1546
+ if (!githubToken) return null;
1547
+ if (creds && !isCopilotTokenExpired(creds) && creds.copilotToken) {
1521
1548
  return {
1522
1549
  token: creds.copilotToken,
1523
1550
  baseUrl: getCopilotBaseUrl(creds.accountType),
@@ -1527,11 +1554,11 @@ async function getValidCopilotToken() {
1527
1554
  try {
1528
1555
  const copilotToken = await exchangeForCopilotToken(githubToken);
1529
1556
  const updatedCreds = {
1530
- ...creds,
1531
- githubToken: creds.githubToken,
1557
+ ...creds ?? { githubToken },
1558
+ githubToken: creds?.githubToken ?? githubToken,
1532
1559
  copilotToken: copilotToken.token,
1533
1560
  copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
1534
- accountType: copilotToken.annotations?.copilot_plan ?? creds.accountType
1561
+ accountType: copilotToken.annotations?.copilot_plan ?? creds?.accountType
1535
1562
  };
1536
1563
  await saveCopilotCredentials(updatedCreds);
1537
1564
  return {
@@ -1547,7 +1574,7 @@ async function getValidCopilotToken() {
1547
1574
  throw error;
1548
1575
  }
1549
1576
  }
1550
- 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;
1577
+ var COPILOT_CLIENT_ID, GITHUB_DEVICE_CODE_URL, GITHUB_TOKEN_URL, COPILOT_TOKEN_URL, COPILOT_BASE_URLS, DEFAULT_COPILOT_BASE_URL, REFRESH_BUFFER_MS, execFileAsync, CopilotAuthError, CopilotCredentialsSchema;
1551
1578
  var init_copilot = __esm({
1552
1579
  "src/auth/copilot.ts"() {
1553
1580
  COPILOT_CLIENT_ID = "Iv1.b507a08c87ecfe98";
@@ -1561,6 +1588,7 @@ var init_copilot = __esm({
1561
1588
  };
1562
1589
  DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
1563
1590
  REFRESH_BUFFER_MS = 6e4;
1591
+ execFileAsync = promisify(execFile);
1564
1592
  CopilotAuthError = class extends Error {
1565
1593
  constructor(message, permanent) {
1566
1594
  super(message);
@@ -1648,13 +1676,13 @@ async function openBrowser(url) {
1648
1676
  const platform = process.platform;
1649
1677
  try {
1650
1678
  if (platform === "darwin") {
1651
- await execFileAsync("open", [sanitizedUrl]);
1679
+ await execFileAsync2("open", [sanitizedUrl]);
1652
1680
  } else if (platform === "win32") {
1653
- await execFileAsync("rundll32", ["url.dll,FileProtocolHandler", sanitizedUrl]);
1681
+ await execFileAsync2("rundll32", ["url.dll,FileProtocolHandler", sanitizedUrl]);
1654
1682
  } else if (isWSL) {
1655
- await execFileAsync("cmd.exe", ["/c", "start", "", sanitizedUrl]);
1683
+ await execFileAsync2("cmd.exe", ["/c", "start", "", sanitizedUrl]);
1656
1684
  } else {
1657
- await execFileAsync("xdg-open", [sanitizedUrl]);
1685
+ await execFileAsync2("xdg-open", [sanitizedUrl]);
1658
1686
  }
1659
1687
  return true;
1660
1688
  } catch {
@@ -1704,7 +1732,7 @@ async function openBrowserFallback(url) {
1704
1732
  }
1705
1733
  for (const { cmd, args } of commands2) {
1706
1734
  try {
1707
- await execFileAsync(cmd, args);
1735
+ await execFileAsync2(cmd, args);
1708
1736
  return true;
1709
1737
  } catch {
1710
1738
  continue;
@@ -2168,6 +2196,10 @@ async function runCopilotDeviceFlow() {
2168
2196
  }
2169
2197
  );
2170
2198
  spinner18.stop(chalk.green("\u2713 GitHub authentication successful!"));
2199
+ const githubLogin = await getGitHubLogin(githubToken);
2200
+ if (githubLogin) {
2201
+ console.log(chalk.dim(` Authenticated as: @${githubLogin}`));
2202
+ }
2171
2203
  console.log(chalk.dim(" Exchanging token for Copilot access..."));
2172
2204
  const copilotToken = await exchangeForCopilotToken(githubToken);
2173
2205
  const creds = {
@@ -2194,6 +2226,11 @@ async function runCopilotDeviceFlow() {
2194
2226
  console.log(chalk.red(" \u2717 GitHub Copilot is not enabled for this account."));
2195
2227
  console.log(chalk.dim(" Please ensure you have an active Copilot subscription:"));
2196
2228
  console.log(chalk.cyan(" \u2192 https://github.com/settings/copilot"));
2229
+ console.log(
2230
+ chalk.dim(
2231
+ " If this account is wrong, sign out of github.com in your browser and run /provider again."
2232
+ )
2233
+ );
2197
2234
  } else if (errorMsg.includes("expired") || errorMsg.includes("timed out")) {
2198
2235
  console.log(chalk.yellow(" \u26A0 Authentication timed out. Please try again."));
2199
2236
  } else if (errorMsg.includes("denied")) {
@@ -2229,7 +2266,7 @@ async function getOrRefreshOAuthToken(provider) {
2229
2266
  }
2230
2267
  return null;
2231
2268
  }
2232
- var execFileAsync;
2269
+ var execFileAsync2;
2233
2270
  var init_flow = __esm({
2234
2271
  "src/auth/flow.ts"() {
2235
2272
  init_oauth();
@@ -2237,7 +2274,7 @@ var init_flow = __esm({
2237
2274
  init_callback_server();
2238
2275
  init_platform();
2239
2276
  init_copilot();
2240
- execFileAsync = promisify(execFile);
2277
+ execFileAsync2 = promisify(execFile);
2241
2278
  }
2242
2279
  });
2243
2280
  function getADCPath() {
@@ -2340,6 +2377,22 @@ async function runGcloudADCLogin() {
2340
2377
  }
2341
2378
  }
2342
2379
  }
2380
+ async function runGcloudADCRevoke() {
2381
+ try {
2382
+ await execAsync(ADC_REVOKE_COMMAND, {
2383
+ timeout: 12e4
2384
+ });
2385
+ clearADCCache();
2386
+ return true;
2387
+ } catch (error) {
2388
+ const message = error instanceof Error ? error.message : String(error);
2389
+ if (message.includes("No credentialed accounts") || message.includes("There are no credentials") || message.includes("No active credentials")) {
2390
+ clearADCCache();
2391
+ return true;
2392
+ }
2393
+ return false;
2394
+ }
2395
+ }
2343
2396
  async function getGeminiADCKey() {
2344
2397
  const token = await getADCAccessToken();
2345
2398
  if (!token) return null;
@@ -2355,12 +2408,13 @@ async function getCachedADCToken() {
2355
2408
  function clearADCCache() {
2356
2409
  cachedToken = null;
2357
2410
  }
2358
- var execAsync, PRINT_ACCESS_TOKEN_COMMAND, ADC_LOGIN_COMMAND, GEMINI_OAUTH_SCOPES, cachedToken;
2411
+ var execAsync, PRINT_ACCESS_TOKEN_COMMAND, ADC_LOGIN_COMMAND, ADC_REVOKE_COMMAND, GEMINI_OAUTH_SCOPES, cachedToken;
2359
2412
  var init_gcloud = __esm({
2360
2413
  "src/auth/gcloud.ts"() {
2361
2414
  execAsync = promisify(exec);
2362
2415
  PRINT_ACCESS_TOKEN_COMMAND = "gcloud auth application-default print-access-token";
2363
2416
  ADC_LOGIN_COMMAND = "gcloud auth application-default login";
2417
+ ADC_REVOKE_COMMAND = "gcloud auth application-default revoke --quiet";
2364
2418
  GEMINI_OAUTH_SCOPES = [
2365
2419
  "https://www.googleapis.com/auth/cloud-platform",
2366
2420
  "https://www.googleapis.com/auth/generative-language.retriever"
@@ -2408,6 +2462,7 @@ __export(auth_exports, {
2408
2462
  requestDeviceCode: () => requestDeviceCode,
2409
2463
  requestGitHubDeviceCode: () => requestGitHubDeviceCode,
2410
2464
  runGcloudADCLogin: () => runGcloudADCLogin,
2465
+ runGcloudADCRevoke: () => runGcloudADCRevoke,
2411
2466
  runOAuthFlow: () => runOAuthFlow,
2412
2467
  saveCopilotCredentials: () => saveCopilotCredentials,
2413
2468
  saveTokens: () => saveTokens,
@@ -2585,7 +2640,7 @@ function getDefaultModel(provider) {
2585
2640
  case "anthropic":
2586
2641
  return process.env["ANTHROPIC_MODEL"] ?? "claude-opus-4-6";
2587
2642
  case "openai":
2588
- return process.env["OPENAI_MODEL"] ?? "gpt-5.4-codex";
2643
+ return process.env["OPENAI_MODEL"] ?? "gpt-5.3-codex";
2589
2644
  case "gemini":
2590
2645
  return process.env["GEMINI_MODEL"] ?? "gemini-3.1-pro-preview";
2591
2646
  case "vertex":
@@ -3773,7 +3828,7 @@ var init_openai = __esm({
3773
3828
  init_errors();
3774
3829
  init_retry();
3775
3830
  init_tool_call_normalizer();
3776
- DEFAULT_MODEL2 = "gpt-5.4-codex";
3831
+ DEFAULT_MODEL2 = "gpt-5.3-codex";
3777
3832
  CONTEXT_WINDOWS2 = {
3778
3833
  // OpenAI models
3779
3834
  "gpt-4o": 128e3,
@@ -4983,7 +5038,7 @@ var init_codex = __esm({
4983
5038
  init_retry();
4984
5039
  init_tool_call_normalizer();
4985
5040
  CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
4986
- DEFAULT_MODEL3 = "gpt-5.4-codex";
5041
+ DEFAULT_MODEL3 = "gpt-5.3-codex";
4987
5042
  CONTEXT_WINDOWS3 = {
4988
5043
  "gpt-5.4-codex": 2e5,
4989
5044
  "gpt-5.3-codex": 2e5,
@@ -5757,11 +5812,12 @@ function createGeminiProvider(config) {
5757
5812
  }
5758
5813
  return provider;
5759
5814
  }
5760
- var DEFAULT_MODEL5, CONTEXT_WINDOWS5, GeminiProvider;
5815
+ var DEFAULT_MODEL5, SKIP_THOUGHT_SIGNATURE_VALIDATOR, CONTEXT_WINDOWS5, GeminiProvider;
5761
5816
  var init_gemini = __esm({
5762
5817
  "src/providers/gemini.ts"() {
5763
5818
  init_errors();
5764
5819
  DEFAULT_MODEL5 = "gemini-3.1-pro-preview";
5820
+ SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
5765
5821
  CONTEXT_WINDOWS5 = {
5766
5822
  "gemini-3.1-pro-preview": 1e6,
5767
5823
  "gemini-3.1-flash-lite-preview": 1e6,
@@ -5854,30 +5910,29 @@ var init_gemini = __esm({
5854
5910
  if (text15) {
5855
5911
  yield { type: "text", text: text15 };
5856
5912
  }
5857
- const functionCalls = this.extractFunctionCalls(chunk);
5858
- for (const functionCall of functionCalls) {
5859
- const toolCallId = functionCall.id ?? `gemini_call_${++fallbackToolCounter}`;
5913
+ const toolCalls = this.extractToolCalls(chunk, { includeLegacyFunctionCalls: true });
5914
+ for (const toolCall of toolCalls) {
5915
+ const toolCallId = toolCall.id ?? `gemini_call_${++fallbackToolCounter}`;
5860
5916
  if (emittedToolIds.has(toolCallId)) continue;
5861
5917
  emittedToolIds.add(toolCallId);
5862
- const toolCall = {
5863
- id: toolCallId,
5864
- name: functionCall.name ?? "unknown_function",
5865
- input: functionCall.args ?? {}
5918
+ const normalizedToolCall = {
5919
+ ...toolCall,
5920
+ id: toolCallId
5866
5921
  };
5867
5922
  yield {
5868
5923
  type: "tool_use_start",
5869
5924
  toolCall: {
5870
- id: toolCall.id,
5871
- name: toolCall.name
5925
+ id: normalizedToolCall.id,
5926
+ name: normalizedToolCall.name
5872
5927
  }
5873
5928
  };
5874
5929
  yield {
5875
5930
  type: "tool_use_end",
5876
- toolCall
5931
+ toolCall: normalizedToolCall
5877
5932
  };
5878
5933
  }
5879
5934
  const finishReason = chunk.candidates?.[0]?.finishReason;
5880
- if (functionCalls.length > 0) {
5935
+ if (toolCalls.length > 0) {
5881
5936
  streamStopReason = "tool_use";
5882
5937
  } else if (finishReason) {
5883
5938
  streamStopReason = this.mapFinishReason(finishReason);
@@ -6001,13 +6056,18 @@ var init_gemini = __esm({
6001
6056
  });
6002
6057
  } else if (block.type === "tool_use") {
6003
6058
  const toolUse = block;
6004
- parts.push({
6005
- functionCall: {
6006
- id: toolUse.id,
6007
- name: toolUse.name,
6008
- args: toolUse.input
6009
- }
6010
- });
6059
+ const thoughtSignature = toolUse.geminiThoughtSignature ?? SKIP_THOUGHT_SIGNATURE_VALIDATOR;
6060
+ const functionCall = {
6061
+ id: toolUse.id,
6062
+ name: toolUse.name,
6063
+ args: toolUse.input
6064
+ };
6065
+ const part = {
6066
+ functionCall,
6067
+ thoughtSignature,
6068
+ thought_signature: thoughtSignature
6069
+ };
6070
+ parts.push(part);
6011
6071
  }
6012
6072
  }
6013
6073
  return parts.length > 0 ? parts : [{ text: "" }];
@@ -6031,13 +6091,31 @@ var init_gemini = __esm({
6031
6091
  allowedFunctionNames: [choice.name]
6032
6092
  };
6033
6093
  }
6034
- extractFunctionCalls(response) {
6035
- if (response.functionCalls && response.functionCalls.length > 0) {
6036
- return response.functionCalls;
6094
+ extractThoughtSignatureFromPart(part) {
6095
+ const withSignature = part;
6096
+ return withSignature.thoughtSignature ?? withSignature.thought_signature ?? withSignature.functionCall?.thoughtSignature ?? withSignature.functionCall?.thought_signature;
6097
+ }
6098
+ extractToolCalls(response, options) {
6099
+ const toolCallsFromParts = (response.candidates?.[0]?.content?.parts ?? []).filter((part) => !!part.functionCall).map((part, index) => ({
6100
+ id: part.functionCall.id ?? `gemini_call_${index + 1}`,
6101
+ name: part.functionCall.name ?? "unknown_function",
6102
+ input: part.functionCall.args ?? {},
6103
+ geminiThoughtSignature: this.extractThoughtSignatureFromPart(part)
6104
+ }));
6105
+ if (toolCallsFromParts.length > 0) {
6106
+ return toolCallsFromParts;
6037
6107
  }
6038
- const candidate = response.candidates?.[0];
6039
- const parts = candidate?.content?.parts ?? [];
6040
- return parts.filter((part) => !!part.functionCall).map((part) => part.functionCall).filter(Boolean);
6108
+ if (!options?.includeLegacyFunctionCalls || !response.functionCalls?.length) {
6109
+ return [];
6110
+ }
6111
+ return response.functionCalls.map((functionCall, index) => ({
6112
+ id: functionCall.id ?? `gemini_call_${index + 1}`,
6113
+ name: functionCall.name ?? "unknown_function",
6114
+ input: functionCall.args ?? {},
6115
+ geminiThoughtSignature: this.extractThoughtSignatureFromPart({
6116
+ functionCall
6117
+ })
6118
+ }));
6041
6119
  }
6042
6120
  parseResponse(response, model) {
6043
6121
  const usage = response.usageMetadata;
@@ -6054,11 +6132,7 @@ var init_gemini = __esm({
6054
6132
  }
6055
6133
  parseResponseWithTools(response, model) {
6056
6134
  const usage = response.usageMetadata;
6057
- const toolCalls = this.extractFunctionCalls(response).map((functionCall, index) => ({
6058
- id: functionCall.id ?? `gemini_call_${index + 1}`,
6059
- name: functionCall.name ?? "unknown_function",
6060
- input: functionCall.args ?? {}
6061
- }));
6135
+ const toolCalls = this.extractToolCalls(response, { includeLegacyFunctionCalls: true });
6062
6136
  return {
6063
6137
  id: `gemini-${Date.now()}`,
6064
6138
  content: response.text ?? "",
@@ -6106,6 +6180,24 @@ var init_gemini = __esm({
6106
6180
  });
6107
6181
 
6108
6182
  // src/providers/vertex.ts
6183
+ function extractSseEventData(rawEvent) {
6184
+ const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter(Boolean);
6185
+ if (dataLines.length === 0) {
6186
+ return null;
6187
+ }
6188
+ return dataLines.join("\n");
6189
+ }
6190
+ function getToolCallFingerprint(part) {
6191
+ const name = part.functionCall?.name ?? "unknown_function";
6192
+ let argsSerialized = "{}";
6193
+ try {
6194
+ argsSerialized = JSON.stringify(part.functionCall?.args ?? {});
6195
+ } catch {
6196
+ argsSerialized = "{}";
6197
+ }
6198
+ const thoughtSignature = part.thoughtSignature ?? part.thought_signature ?? part.functionCall?.thoughtSignature ?? part.functionCall?.thought_signature ?? "";
6199
+ return `${name}:${argsSerialized}:${thoughtSignature}`;
6200
+ }
6109
6201
  function createVertexProvider(config) {
6110
6202
  const provider = new VertexProvider();
6111
6203
  if (config) {
@@ -6114,7 +6206,7 @@ function createVertexProvider(config) {
6114
6206
  }
6115
6207
  return provider;
6116
6208
  }
6117
- var DEFAULT_MODEL6, DEFAULT_BASE_URL, DEFAULT_LOCATION, CONTEXT_WINDOWS6, VertexProvider;
6209
+ var DEFAULT_MODEL6, DEFAULT_BASE_URL, DEFAULT_LOCATION, CONTEXT_WINDOWS6, SKIP_THOUGHT_SIGNATURE_VALIDATOR2, VertexProvider;
6118
6210
  var init_vertex = __esm({
6119
6211
  "src/providers/vertex.ts"() {
6120
6212
  init_errors();
@@ -6124,12 +6216,15 @@ var init_vertex = __esm({
6124
6216
  DEFAULT_BASE_URL = "https://aiplatform.googleapis.com/v1";
6125
6217
  DEFAULT_LOCATION = "global";
6126
6218
  CONTEXT_WINDOWS6 = {
6219
+ "gemini-3-pro-preview": 1048576,
6220
+ "gemini-3-flash-preview": 1048576,
6127
6221
  "gemini-2.5-pro": 1048576,
6128
6222
  "gemini-2.5-flash": 1048576,
6129
6223
  "gemini-2.5-flash-lite": 1048576,
6130
6224
  "gemini-2.0-flash-001": 1048576,
6131
6225
  "gemini-2.0-flash-lite-001": 1048576
6132
6226
  };
6227
+ SKIP_THOUGHT_SIGNATURE_VALIDATOR2 = "skip_thought_signature_validator";
6133
6228
  VertexProvider = class {
6134
6229
  id = "vertex";
6135
6230
  name = "Google Vertex AI Gemini";
@@ -6205,6 +6300,7 @@ var init_vertex = __esm({
6205
6300
  );
6206
6301
  let stopReason = "end_turn";
6207
6302
  let streamToolCallCounter = 0;
6303
+ const emittedToolFingerprints = /* @__PURE__ */ new Set();
6208
6304
  for await (const chunk of stream) {
6209
6305
  const candidate = chunk.candidates?.[0];
6210
6306
  const parts = candidate?.content?.parts ?? [];
@@ -6213,13 +6309,20 @@ var init_vertex = __esm({
6213
6309
  yield { type: "text", text: part.text };
6214
6310
  }
6215
6311
  if (part.functionCall) {
6312
+ const fingerprint = getToolCallFingerprint(part);
6313
+ if (emittedToolFingerprints.has(fingerprint)) {
6314
+ continue;
6315
+ }
6316
+ emittedToolFingerprints.add(fingerprint);
6216
6317
  streamToolCallCounter++;
6318
+ const geminiThoughtSignature = part.thoughtSignature ?? part.thought_signature ?? part.functionCall.thoughtSignature ?? part.functionCall.thought_signature;
6217
6319
  yield {
6218
6320
  type: "tool_use_start",
6219
6321
  toolCall: {
6220
6322
  id: `vertex_call_${streamToolCallCounter}`,
6221
6323
  name: part.functionCall.name,
6222
- input: part.functionCall.args ?? {}
6324
+ input: part.functionCall.args ?? {},
6325
+ geminiThoughtSignature
6223
6326
  }
6224
6327
  };
6225
6328
  yield {
@@ -6227,7 +6330,8 @@ var init_vertex = __esm({
6227
6330
  toolCall: {
6228
6331
  id: `vertex_call_${streamToolCallCounter}`,
6229
6332
  name: part.functionCall.name,
6230
- input: part.functionCall.args ?? {}
6333
+ input: part.functionCall.args ?? {},
6334
+ geminiThoughtSignature
6231
6335
  }
6232
6336
  };
6233
6337
  }
@@ -6360,11 +6464,14 @@ var init_vertex = __esm({
6360
6464
  });
6361
6465
  } else if (block.type === "tool_use") {
6362
6466
  const toolUse = block;
6467
+ const thoughtSignature = toolUse.geminiThoughtSignature ?? SKIP_THOUGHT_SIGNATURE_VALIDATOR2;
6363
6468
  parts.push({
6364
6469
  functionCall: {
6365
6470
  name: toolUse.name,
6366
6471
  args: toolUse.input
6367
- }
6472
+ },
6473
+ thoughtSignature,
6474
+ thought_signature: thoughtSignature
6368
6475
  });
6369
6476
  }
6370
6477
  }
@@ -6456,22 +6563,27 @@ var init_vertex = __esm({
6456
6563
  if (done) break;
6457
6564
  buffer += decoder.decode(value, { stream: true });
6458
6565
  while (true) {
6459
- const eventBoundary = buffer.indexOf("\n\n");
6460
- if (eventBoundary === -1) break;
6461
- const rawEvent = buffer.slice(0, eventBoundary);
6462
- buffer = buffer.slice(eventBoundary + 2);
6463
- const dataLines = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter(Boolean);
6464
- for (const line of dataLines) {
6465
- if (line === "[DONE]") return;
6466
- yield JSON.parse(line);
6566
+ const boundaryMatch = /\r?\n\r?\n/.exec(buffer);
6567
+ if (!boundaryMatch || boundaryMatch.index === void 0) break;
6568
+ const rawEvent = buffer.slice(0, boundaryMatch.index);
6569
+ buffer = buffer.slice(boundaryMatch.index + boundaryMatch[0].length);
6570
+ const data = extractSseEventData(rawEvent);
6571
+ if (!data || data === "[DONE]") {
6572
+ if (data === "[DONE]") return;
6573
+ continue;
6574
+ }
6575
+ try {
6576
+ yield JSON.parse(data);
6577
+ } catch {
6578
+ continue;
6467
6579
  }
6468
6580
  }
6469
6581
  }
6470
- const trailing = buffer.trim();
6471
- if (trailing.startsWith("data:")) {
6472
- const line = trailing.slice(5).trim();
6473
- if (line && line !== "[DONE]") {
6474
- yield JSON.parse(line);
6582
+ const trailingData = extractSseEventData(buffer.trim());
6583
+ if (trailingData && trailingData !== "[DONE]") {
6584
+ try {
6585
+ yield JSON.parse(trailingData);
6586
+ } catch {
6475
6587
  }
6476
6588
  }
6477
6589
  }
@@ -6504,7 +6616,8 @@ var init_vertex = __esm({
6504
6616
  toolCalls.push({
6505
6617
  id: `vertex_call_${toolIndex}`,
6506
6618
  name: part.functionCall.name,
6507
- input: part.functionCall.args ?? {}
6619
+ input: part.functionCall.args ?? {},
6620
+ geminiThoughtSignature: part.thoughtSignature ?? part.thought_signature ?? part.functionCall.thoughtSignature ?? part.functionCall.thought_signature
6508
6621
  });
6509
6622
  }
6510
6623
  }
@@ -6609,6 +6722,7 @@ var init_pricing = __esm({
6609
6722
  "gpt-4o": { inputPerMillion: 2.5, outputPerMillion: 10, contextWindow: 128e3 },
6610
6723
  "gpt-4o-mini": { inputPerMillion: 0.15, outputPerMillion: 0.6, contextWindow: 128e3 },
6611
6724
  // Google Gemini models
6725
+ "gemini-3-pro-preview": { inputPerMillion: 1.25, outputPerMillion: 5, contextWindow: 1e6 },
6612
6726
  "gemini-3.1-pro-preview": { inputPerMillion: 1.25, outputPerMillion: 5, contextWindow: 1e6 },
6613
6727
  "gemini-3-flash-preview": {
6614
6728
  inputPerMillion: 0.15,
@@ -10446,17 +10560,55 @@ async function saveTrustSettings(settings) {
10446
10560
  console.warn(`[Trust] Failed to save trust settings: ${msg}`);
10447
10561
  }
10448
10562
  }
10449
- async function loadTrustedTools(projectPath) {
10563
+ function getProjectTrustSettingsFile(projectPath) {
10564
+ return path39__default.join(projectPath, PROJECT_TRUST_FILE_RELATIVE_PATH);
10565
+ }
10566
+ async function loadProjectTrustSettings(projectPath) {
10450
10567
  const settings = await loadTrustSettings();
10568
+ const legacyTrusted = settings.projectTrusted[projectPath] ?? [];
10569
+ const legacyDenied = settings.projectDenied[projectPath] ?? [];
10570
+ const defaultState = {
10571
+ trusted: legacyTrusted,
10572
+ denied: legacyDenied,
10573
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10574
+ };
10575
+ try {
10576
+ const content = await fs35__default.readFile(getProjectTrustSettingsFile(projectPath), "utf-8");
10577
+ const raw = JSON.parse(content);
10578
+ return {
10579
+ trusted: raw.trusted ?? defaultState.trusted,
10580
+ denied: raw.denied ?? defaultState.denied,
10581
+ updatedAt: raw.updatedAt ?? defaultState.updatedAt
10582
+ };
10583
+ } catch {
10584
+ return defaultState;
10585
+ }
10586
+ }
10587
+ async function saveProjectTrustSettings(projectPath, settings) {
10588
+ try {
10589
+ const filePath = getProjectTrustSettingsFile(projectPath);
10590
+ await fs35__default.mkdir(path39__default.dirname(filePath), { recursive: true });
10591
+ settings.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10592
+ await fs35__default.writeFile(filePath, JSON.stringify(settings, null, 2), "utf-8");
10593
+ } catch (error) {
10594
+ const msg = error instanceof Error ? error.message : String(error);
10595
+ console.warn(`[Trust] Failed to save project trust settings: ${msg}`);
10596
+ }
10597
+ }
10598
+ async function loadTrustedTools(projectPath) {
10599
+ const [settings, projectSettings] = await Promise.all([
10600
+ loadTrustSettings(),
10601
+ loadProjectTrustSettings(projectPath)
10602
+ ]);
10451
10603
  const trusted = /* @__PURE__ */ new Set();
10452
10604
  for (const tool of settings.globalTrusted) {
10453
10605
  trusted.add(tool);
10454
10606
  }
10455
- const projectTrusted = settings.projectTrusted[projectPath] ?? [];
10607
+ const projectTrusted = projectSettings.trusted;
10456
10608
  for (const tool of projectTrusted) {
10457
10609
  trusted.add(tool);
10458
10610
  }
10459
- const projectDenied = settings.projectDenied[projectPath] ?? [];
10611
+ const projectDenied = projectSettings.denied;
10460
10612
  for (const tool of projectDenied) {
10461
10613
  trusted.delete(tool);
10462
10614
  }
@@ -10469,61 +10621,54 @@ async function saveTrustedTool(toolName, projectPath, global = false) {
10469
10621
  settings.globalTrusted.push(toolName);
10470
10622
  }
10471
10623
  } else if (projectPath) {
10472
- if (!settings.projectTrusted[projectPath]) {
10473
- settings.projectTrusted[projectPath] = [];
10474
- }
10475
- const projectTrusted = settings.projectTrusted[projectPath];
10476
- if (projectTrusted && !projectTrusted.includes(toolName)) {
10477
- projectTrusted.push(toolName);
10624
+ const projectSettings = await loadProjectTrustSettings(projectPath);
10625
+ if (!projectSettings.trusted.includes(toolName)) {
10626
+ projectSettings.trusted.push(toolName);
10478
10627
  }
10628
+ projectSettings.denied = projectSettings.denied.filter((t) => t !== toolName);
10629
+ await saveProjectTrustSettings(projectPath, projectSettings);
10630
+ }
10631
+ if (global) {
10632
+ await saveTrustSettings(settings);
10479
10633
  }
10480
- await saveTrustSettings(settings);
10481
10634
  }
10482
10635
  async function removeTrustedTool(toolName, projectPath, global = false) {
10483
10636
  const settings = await loadTrustSettings();
10484
10637
  if (global) {
10485
10638
  settings.globalTrusted = settings.globalTrusted.filter((t) => t !== toolName);
10639
+ await saveTrustSettings(settings);
10486
10640
  } else {
10487
- const projectTrusted = settings.projectTrusted[projectPath];
10488
- if (projectTrusted) {
10489
- settings.projectTrusted[projectPath] = projectTrusted.filter((t) => t !== toolName);
10490
- }
10641
+ const projectSettings = await loadProjectTrustSettings(projectPath);
10642
+ projectSettings.trusted = projectSettings.trusted.filter((t) => t !== toolName);
10643
+ await saveProjectTrustSettings(projectPath, projectSettings);
10491
10644
  }
10492
- await saveTrustSettings(settings);
10493
10645
  }
10494
10646
  async function saveDeniedTool(toolName, projectPath) {
10495
- const settings = await loadTrustSettings();
10496
- if (!settings.projectDenied[projectPath]) {
10497
- settings.projectDenied[projectPath] = [];
10647
+ const projectSettings = await loadProjectTrustSettings(projectPath);
10648
+ if (!projectSettings.denied.includes(toolName)) {
10649
+ projectSettings.denied.push(toolName);
10498
10650
  }
10499
- const denied = settings.projectDenied[projectPath];
10500
- if (denied && !denied.includes(toolName)) {
10501
- denied.push(toolName);
10502
- }
10503
- const projectTrusted = settings.projectTrusted[projectPath];
10504
- if (projectTrusted) {
10505
- settings.projectTrusted[projectPath] = projectTrusted.filter((t) => t !== toolName);
10506
- }
10507
- await saveTrustSettings(settings);
10651
+ projectSettings.trusted = projectSettings.trusted.filter((t) => t !== toolName);
10652
+ await saveProjectTrustSettings(projectPath, projectSettings);
10508
10653
  }
10509
10654
  async function removeDeniedTool(toolName, projectPath) {
10510
- const settings = await loadTrustSettings();
10511
- const denied = settings.projectDenied[projectPath];
10512
- if (denied) {
10513
- settings.projectDenied[projectPath] = denied.filter((t) => t !== toolName);
10514
- }
10515
- await saveTrustSettings(settings);
10655
+ const projectSettings = await loadProjectTrustSettings(projectPath);
10656
+ projectSettings.denied = projectSettings.denied.filter((t) => t !== toolName);
10657
+ await saveProjectTrustSettings(projectPath, projectSettings);
10516
10658
  }
10517
10659
  async function getDeniedTools(projectPath) {
10518
- const settings = await loadTrustSettings();
10519
- return settings.projectDenied[projectPath] ?? [];
10660
+ const settings = await loadProjectTrustSettings(projectPath);
10661
+ return settings.denied;
10520
10662
  }
10521
10663
  async function getAllTrustedTools(projectPath) {
10522
- const settings = await loadTrustSettings();
10664
+ const [settings, projectSettings] = await Promise.all([
10665
+ loadTrustSettings(),
10666
+ loadProjectTrustSettings(projectPath)
10667
+ ]);
10523
10668
  return {
10524
10669
  global: settings.globalTrusted,
10525
- project: settings.projectTrusted[projectPath] ?? [],
10526
- denied: settings.projectDenied[projectPath] ?? []
10670
+ project: projectSettings.trusted,
10671
+ denied: projectSettings.denied
10527
10672
  };
10528
10673
  }
10529
10674
  async function initializeSessionTrust(session) {
@@ -10614,7 +10759,7 @@ function getSessionMemory(session) {
10614
10759
  async function reloadSessionMemory(session) {
10615
10760
  await initializeSessionMemory(session);
10616
10761
  }
10617
- var MAX_SKILL_INSTRUCTIONS_CHARS, TRUST_SETTINGS_DIR, TRUST_SETTINGS_FILE, CATEGORY_LABELS, COCO_SYSTEM_PROMPT, SHELL_METACHARACTERS, SAFE_COMMAND_VALIDATORS;
10762
+ var MAX_SKILL_INSTRUCTIONS_CHARS, TRUST_SETTINGS_DIR, TRUST_SETTINGS_FILE, PROJECT_TRUST_FILE_RELATIVE_PATH, CATEGORY_LABELS, COCO_SYSTEM_PROMPT, SHELL_METACHARACTERS, SAFE_COMMAND_VALIDATORS;
10618
10763
  var init_session = __esm({
10619
10764
  "src/cli/repl/session.ts"() {
10620
10765
  init_env();
@@ -10629,6 +10774,7 @@ var init_session = __esm({
10629
10774
  MAX_SKILL_INSTRUCTIONS_CHARS = 16e3;
10630
10775
  TRUST_SETTINGS_DIR = path39__default.dirname(CONFIG_PATHS.trustedTools);
10631
10776
  TRUST_SETTINGS_FILE = CONFIG_PATHS.trustedTools;
10777
+ PROJECT_TRUST_FILE_RELATIVE_PATH = path39__default.join(".coco", "trusted-tools.json");
10632
10778
  CATEGORY_LABELS = {
10633
10779
  mcp: "MCP Connected Services",
10634
10780
  file: "File Operations",
@@ -21620,7 +21766,7 @@ async function openBrowser2(url) {
21620
21766
  }
21621
21767
  for (const { cmd, args } of commands2) {
21622
21768
  try {
21623
- await execFileAsync2(cmd, args);
21769
+ await execFileAsync3(cmd, args);
21624
21770
  return true;
21625
21771
  } catch {
21626
21772
  continue;
@@ -21896,13 +22042,13 @@ async function authenticateMcpOAuth(params) {
21896
22042
  await persistToken(resource, token, { authorizationServer, clientId });
21897
22043
  return token.access_token;
21898
22044
  }
21899
- var execFileAsync2, TOKEN_STORE_PATH, OAUTH_TIMEOUT_MS, logger;
22045
+ var execFileAsync3, TOKEN_STORE_PATH, OAUTH_TIMEOUT_MS, logger;
21900
22046
  var init_oauth2 = __esm({
21901
22047
  "src/mcp/oauth.ts"() {
21902
22048
  init_callback_server();
21903
22049
  init_paths();
21904
22050
  init_logger();
21905
- execFileAsync2 = promisify(execFile);
22051
+ execFileAsync3 = promisify(execFile);
21906
22052
  TOKEN_STORE_PATH = path39__default.join(CONFIG_PATHS.tokens, "mcp-oauth.json");
21907
22053
  OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
21908
22054
  logger = getLogger();
@@ -22543,6 +22689,25 @@ var init_lifecycle = __esm({
22543
22689
  connections = /* @__PURE__ */ new Map();
22544
22690
  logger = getLogger();
22545
22691
  static STOP_TIMEOUT_MS = 5e3;
22692
+ /**
22693
+ * Run an async operation with a timeout, always clearing timer resources.
22694
+ */
22695
+ async runWithTimeout(operation, timeoutMs, timeoutMessage) {
22696
+ let timeoutId;
22697
+ const timeoutPromise = new Promise((_, reject) => {
22698
+ timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
22699
+ if (typeof timeoutId.unref === "function") {
22700
+ timeoutId.unref();
22701
+ }
22702
+ });
22703
+ try {
22704
+ return await Promise.race([operation, timeoutPromise]);
22705
+ } finally {
22706
+ if (timeoutId) {
22707
+ clearTimeout(timeoutId);
22708
+ }
22709
+ }
22710
+ }
22546
22711
  /**
22547
22712
  * Create transport for a server config
22548
22713
  */
@@ -22629,15 +22794,11 @@ var init_lifecycle = __esm({
22629
22794
  }
22630
22795
  this.logger.info(`Stopping MCP server: ${name}`);
22631
22796
  try {
22632
- await Promise.race([
22797
+ await this.runWithTimeout(
22633
22798
  connection.transport.disconnect(),
22634
- new Promise(
22635
- (_, reject) => setTimeout(
22636
- () => reject(new Error("MCP disconnect timeout")),
22637
- _MCPServerManager.STOP_TIMEOUT_MS
22638
- )
22639
- )
22640
- ]);
22799
+ _MCPServerManager.STOP_TIMEOUT_MS,
22800
+ "MCP disconnect timeout"
22801
+ );
22641
22802
  } catch (error) {
22642
22803
  this.logger.error(
22643
22804
  `Error disconnecting server '${name}': ${error instanceof Error ? error.message : String(error)}`
@@ -22674,12 +22835,11 @@ var init_lifecycle = __esm({
22674
22835
  }
22675
22836
  const startTime = performance.now();
22676
22837
  try {
22677
- const { tools } = await Promise.race([
22838
+ const { tools } = await this.runWithTimeout(
22678
22839
  connection.client.listTools(),
22679
- new Promise(
22680
- (_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 5e3)
22681
- )
22682
- ]);
22840
+ 5e3,
22841
+ "Health check timeout"
22842
+ );
22683
22843
  const latencyMs = performance.now() - startTime;
22684
22844
  connection.healthy = true;
22685
22845
  connection.toolCount = tools.length;
@@ -29123,20 +29283,13 @@ var PROVIDER_DEFINITIONS = {
29123
29283
  },
29124
29284
  // Updated: March 2026 — from platform.openai.com/docs/models
29125
29285
  models: [
29126
- {
29127
- id: "gpt-5.4-codex",
29128
- name: "GPT-5.4 Codex",
29129
- description: "Latest agentic coding model (Mar 2026)",
29130
- contextWindow: 4e5,
29131
- maxOutputTokens: 128e3,
29132
- recommended: true
29133
- },
29134
29286
  {
29135
29287
  id: "gpt-5.3-codex",
29136
29288
  name: "GPT-5.3 Codex",
29137
- description: "Previous agentic coding model (Feb 2026)",
29289
+ description: "Latest available agentic coding model",
29138
29290
  contextWindow: 4e5,
29139
- maxOutputTokens: 128e3
29291
+ maxOutputTokens: 128e3,
29292
+ recommended: true
29140
29293
  },
29141
29294
  {
29142
29295
  id: "gpt-5.2-codex",
@@ -29437,20 +29590,13 @@ var PROVIDER_DEFINITIONS = {
29437
29590
  vision: true
29438
29591
  },
29439
29592
  models: [
29440
- {
29441
- id: "gpt-5.4-codex",
29442
- name: "GPT-5.4 Codex",
29443
- description: "Latest coding model via ChatGPT subscription (Mar 2026)",
29444
- contextWindow: 2e5,
29445
- maxOutputTokens: 128e3,
29446
- recommended: true
29447
- },
29448
29593
  {
29449
29594
  id: "gpt-5.3-codex",
29450
29595
  name: "GPT-5.3 Codex",
29451
- description: "Previous coding model via ChatGPT subscription",
29596
+ description: "Latest available coding model via ChatGPT subscription",
29452
29597
  contextWindow: 2e5,
29453
- maxOutputTokens: 128e3
29598
+ maxOutputTokens: 128e3,
29599
+ recommended: true
29454
29600
  },
29455
29601
  {
29456
29602
  id: "gpt-5.2-codex",
@@ -29560,13 +29706,27 @@ var PROVIDER_DEFINITIONS = {
29560
29706
  },
29561
29707
  models: [
29562
29708
  {
29563
- id: "gemini-2.5-pro",
29564
- name: "Gemini 2.5 Pro",
29565
- description: "Recommended Vertex model for coding and complex reasoning",
29709
+ id: "gemini-3-pro-preview",
29710
+ name: "Gemini 3 Pro (Preview)",
29711
+ description: "Most capable Vertex Gemini 3 model (preview)",
29566
29712
  contextWindow: 1048576,
29567
29713
  maxOutputTokens: 65536,
29568
29714
  recommended: true
29569
29715
  },
29716
+ {
29717
+ id: "gemini-3-flash-preview",
29718
+ name: "Gemini 3 Flash (Preview)",
29719
+ description: "Fast Gemini 3 model on Vertex (preview)",
29720
+ contextWindow: 1048576,
29721
+ maxOutputTokens: 65536
29722
+ },
29723
+ {
29724
+ id: "gemini-2.5-pro",
29725
+ name: "Gemini 2.5 Pro",
29726
+ description: "Stable high-quality Vertex model for coding and complex reasoning",
29727
+ contextWindow: 1048576,
29728
+ maxOutputTokens: 65536
29729
+ },
29570
29730
  {
29571
29731
  id: "gemini-2.5-flash",
29572
29732
  name: "Gemini 2.5 Flash",
@@ -33847,10 +34007,13 @@ async function selectModelInteractively(models, currentModelId) {
33847
34007
  totalLines++;
33848
34008
  }
33849
34009
  const star = model.recommended ? " \u2B50" : "";
33850
- const ctx = model.contextWindow ? ` ${Math.round(model.contextWindow / 1e3)}K` : "";
34010
+ const multiplierMatch = model.description?.match(
34011
+ /\b(?:Premium|Free)\s*x([0-9]+(?:\.[0-9]+)?)\b/i
34012
+ );
34013
+ const metric = multiplierMatch ? ` x${multiplierMatch[1]}` : model.contextWindow ? ` ${Math.round(model.contextWindow / 1e3)}K` : "";
33851
34014
  const desc = model.description ? ` ${model.description}` : "";
33852
34015
  const hint = model.hint ? ` \u2192 ${model.hint}` : "";
33853
- const suffix = truncate2(`${star}${ctx}${desc}${hint}`, descWidth, "\u2026");
34016
+ const suffix = truncate2(`${star}${metric}${desc}${hint}`, descWidth, "\u2026");
33854
34017
  if (model.disabled) {
33855
34018
  let line2 = chalk.dim(" \u25CB ");
33856
34019
  line2 += chalk.dim(model.id.padEnd(30));
@@ -34173,7 +34336,10 @@ async function setupProviderWithAuth(provider) {
34173
34336
  authMethod = choice;
34174
34337
  }
34175
34338
  if (authMethod === "oauth") {
34339
+ const oauthSpinner = p26.spinner();
34340
+ oauthSpinner.start("Starting OAuth sign-in flow...");
34176
34341
  const result = await runOAuthFlow(provider.id);
34342
+ oauthSpinner.stop(result ? "OAuth sign-in completed" : "OAuth sign-in cancelled");
34177
34343
  if (!result) return null;
34178
34344
  if (provider.id === "copilot") {
34179
34345
  const model3 = await selectModel(provider);
@@ -34257,7 +34423,10 @@ async function setupGcloudADC(provider) {
34257
34423
  );
34258
34424
  console.log(chalk.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"));
34259
34425
  console.log();
34426
+ const gcloudCheckSpinner = p26.spinner();
34427
+ gcloudCheckSpinner.start("Checking gcloud CLI...");
34260
34428
  const gcloudInstalled = await isGcloudInstalled();
34429
+ gcloudCheckSpinner.stop(gcloudInstalled ? "gcloud CLI detected" : "gcloud CLI not detected");
34261
34430
  if (!gcloudInstalled) {
34262
34431
  p26.log.error("gcloud CLI is not installed");
34263
34432
  console.log(chalk.dim(" Install it from: https://cloud.google.com/sdk/docs/install"));
@@ -34288,10 +34457,50 @@ async function setupGcloudADC(provider) {
34288
34457
  location: vertexSettings2?.location
34289
34458
  };
34290
34459
  }
34460
+ const adcInspectSpinner = p26.spinner();
34461
+ adcInspectSpinner.start("Checking existing ADC credentials...");
34291
34462
  let adc = await inspectADC();
34463
+ adcInspectSpinner.stop(
34464
+ adc.status === "ok" && adc.token ? "ADC credentials found" : "No reusable ADC credentials found"
34465
+ );
34292
34466
  if (adc.status === "ok" && adc.token) {
34293
34467
  console.log(chalk.green(" \u2713 gcloud ADC is already configured!"));
34294
34468
  console.log();
34469
+ const adcChoice = await p26.select({
34470
+ message: "ADC session detected. What do you want to do?",
34471
+ options: [
34472
+ { value: "use", label: "Use current ADC session" },
34473
+ { value: "switch", label: "Switch Google account (revoke and re-login)" },
34474
+ { value: "cancel", label: "Cancel" }
34475
+ ]
34476
+ });
34477
+ if (p26.isCancel(adcChoice) || adcChoice === "cancel") return null;
34478
+ if (adcChoice === "switch") {
34479
+ const revokeSpinner = p26.spinner();
34480
+ revokeSpinner.start("Revoking current gcloud ADC session...");
34481
+ const revoked = await runGcloudADCRevoke();
34482
+ revokeSpinner.stop(
34483
+ revoked ? "Current ADC session revoked" : "Could not revoke current ADC session"
34484
+ );
34485
+ if (!revoked) {
34486
+ p26.log.error("Could not revoke gcloud ADC from Coco.");
34487
+ console.log(chalk.dim(" Try manually: gcloud auth application-default revoke"));
34488
+ console.log();
34489
+ return null;
34490
+ }
34491
+ const loginSpinner = p26.spinner();
34492
+ loginSpinner.start("Running `gcloud auth application-default login`...");
34493
+ const loginOk = await runGcloudADCLogin();
34494
+ loginSpinner.stop(loginOk ? "gcloud login flow completed" : "gcloud login flow failed");
34495
+ if (!loginOk) return null;
34496
+ const recheckSpinner = p26.spinner();
34497
+ recheckSpinner.start("Verifying ADC credentials after re-login...");
34498
+ adc = await inspectADC();
34499
+ recheckSpinner.stop(
34500
+ adc.status === "ok" && adc.token ? "ADC credentials verified" : "ADC verification failed after re-login"
34501
+ );
34502
+ if (!(adc.status === "ok" && adc.token)) return null;
34503
+ }
34295
34504
  p26.log.success("Authentication verified");
34296
34505
  const vertexSettings2 = provider.id === "vertex" ? await promptVertexSettings() : void 0;
34297
34506
  if (provider.id === "vertex" && !vertexSettings2) return null;
@@ -34318,9 +34527,17 @@ async function setupGcloudADC(provider) {
34318
34527
  if (p26.isCancel(runLoginNow)) return null;
34319
34528
  if (runLoginNow) {
34320
34529
  p26.log.step("Running `gcloud auth application-default login`...");
34530
+ const loginSpinner = p26.spinner();
34531
+ loginSpinner.start("Launching gcloud login flow (browser may open)...");
34321
34532
  const loginOk = await runGcloudADCLogin();
34533
+ loginSpinner.stop(loginOk ? "gcloud login flow completed" : "gcloud login flow failed");
34322
34534
  if (loginOk) {
34535
+ const recheckSpinner = p26.spinner();
34536
+ recheckSpinner.start("Verifying ADC credentials after login...");
34323
34537
  adc = await inspectADC();
34538
+ recheckSpinner.stop(
34539
+ adc.status === "ok" && adc.token ? "ADC credentials verified" : "ADC verification failed after login"
34540
+ );
34324
34541
  if (adc.status === "ok" && adc.token) {
34325
34542
  console.log(chalk.green(" \u2713 gcloud ADC is now configured."));
34326
34543
  console.log();
@@ -34382,6 +34599,19 @@ async function setupGcloudADC(provider) {
34382
34599
  async function promptVertexSettings() {
34383
34600
  const projectDefault = process.env["VERTEX_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCLOUD_PROJECT"] ?? "";
34384
34601
  const locationDefault = process.env["VERTEX_LOCATION"] ?? process.env["GOOGLE_CLOUD_LOCATION"] ?? "global";
34602
+ console.log(chalk.dim("\n Need help finding these values?"));
34603
+ console.log(chalk.cyan(" $ gcloud projects list"));
34604
+ console.log(chalk.cyan(" $ gcloud config set project <PROJECT_ID>"));
34605
+ console.log(chalk.cyan(" $ gcloud config get-value project"));
34606
+ console.log(chalk.cyan(" $ gcloud config get-value compute/region"));
34607
+ console.log(
34608
+ chalk.cyan(" $ gcloud config set compute/region <LOCATION> # e.g. global, europe-west1")
34609
+ );
34610
+ console.log(
34611
+ chalk.dim(
34612
+ " (If compute/region is unset, set it as above, then use that value for Vertex location)\n"
34613
+ )
34614
+ );
34385
34615
  const project = await p26.text({
34386
34616
  message: "Google Cloud project ID:",
34387
34617
  placeholder: projectDefault || "my-gcp-project",
@@ -35445,12 +35675,27 @@ async function switchProvider(initialProvider, session) {
35445
35675
  const apiKey = supportsApiKey ? process.env[newProvider.envVar] : void 0;
35446
35676
  const hasOAuth = supportsOAuth(newProvider.id) || newProvider.supportsOAuth;
35447
35677
  const hasGcloudADC = newProvider.supportsGcloudADC;
35678
+ let adcConnected = false;
35679
+ if (hasGcloudADC) {
35680
+ try {
35681
+ const adc = await inspectADC();
35682
+ adcConnected = adc.status === "ok" && adc.token !== null;
35683
+ } catch {
35684
+ adcConnected = false;
35685
+ }
35686
+ }
35448
35687
  const oauthProviderName = newProvider.id === "copilot" ? "copilot" : newProvider.id === "gemini" ? "gemini" : "openai";
35449
35688
  let oauthConnected = false;
35450
35689
  if (hasOAuth) {
35690
+ const oauthCheckSpinner = p26.spinner();
35691
+ oauthCheckSpinner.start("Checking existing OAuth session...");
35451
35692
  try {
35452
35693
  oauthConnected = await isOAuthConfigured(oauthProviderName);
35694
+ oauthCheckSpinner.stop(
35695
+ oauthConnected ? "Existing OAuth session found" : "No existing OAuth session"
35696
+ );
35453
35697
  } catch {
35698
+ oauthCheckSpinner.stop("Could not verify OAuth session");
35454
35699
  }
35455
35700
  }
35456
35701
  {
@@ -35486,8 +35731,8 @@ async function switchProvider(initialProvider, session) {
35486
35731
  if (hasGcloudADC) {
35487
35732
  authOptions.push({
35488
35733
  value: "gcloud",
35489
- label: "\u2601\uFE0F Use gcloud ADC",
35490
- hint: "Authenticate via gcloud CLI"
35734
+ label: adcConnected ? "\u2601\uFE0F Use gcloud ADC (configured \u2713)" : "\u2601\uFE0F Use gcloud ADC",
35735
+ hint: adcConnected ? "Reuse current ADC session or switch account" : "Authenticate via gcloud CLI"
35491
35736
  });
35492
35737
  }
35493
35738
  if (supportsApiKey) {
@@ -35505,7 +35750,7 @@ async function switchProvider(initialProvider, session) {
35505
35750
  });
35506
35751
  }
35507
35752
  }
35508
- if (oauthConnected || apiKey) {
35753
+ if (oauthConnected || apiKey || adcConnected) {
35509
35754
  authOptions.push({
35510
35755
  value: "remove",
35511
35756
  label: "\u{1F5D1}\uFE0F Remove saved credentials",
@@ -35517,159 +35762,103 @@ async function switchProvider(initialProvider, session) {
35517
35762
  label: "\u274C Cancel",
35518
35763
  hint: ""
35519
35764
  });
35765
+ let authChoice;
35520
35766
  if (authOptions.length > 2) {
35521
- const authChoice = await p26.select({
35767
+ const selected = await p26.select({
35522
35768
  message: `How would you like to authenticate with ${newProvider.name}?`,
35523
35769
  options: authOptions
35524
35770
  });
35525
- if (p26.isCancel(authChoice) || authChoice === "cancel") {
35771
+ if (p26.isCancel(selected) || selected === "cancel") {
35526
35772
  return false;
35527
35773
  }
35528
- if (authChoice === "oauth") {
35529
- const isCopilot = newProvider.id === "copilot";
35530
- const isGemini = newProvider.id === "gemini";
35531
- if (isCopilot) {
35532
- if (oauthConnected) {
35533
- console.log(chalk.dim(`
35774
+ authChoice = selected;
35775
+ } else {
35776
+ authChoice = authOptions.find((option) => option.value !== "cancel")?.value ?? "cancel";
35777
+ if (authChoice === "cancel") return false;
35778
+ }
35779
+ if (authChoice === "oauth") {
35780
+ const isCopilot = newProvider.id === "copilot";
35781
+ const isGemini = newProvider.id === "gemini";
35782
+ if (isCopilot) {
35783
+ if (oauthConnected) {
35784
+ console.log(chalk.dim(`
35534
35785
  Using existing GitHub session...`));
35535
- } else {
35536
- const result = await runOAuthFlow("copilot");
35537
- if (!result) return false;
35538
- }
35539
- selectedAuthMethod = "oauth";
35540
35786
  } else {
35541
- const tokenEnvVar = isGemini ? "GEMINI_OAUTH_TOKEN" : "OPENAI_CODEX_TOKEN";
35542
- if (oauthConnected) {
35543
- try {
35544
- const tokenResult = await getOrRefreshOAuthToken(oauthProviderName);
35545
- if (tokenResult) {
35546
- process.env[tokenEnvVar] = tokenResult.accessToken;
35547
- selectedAuthMethod = "oauth";
35548
- if (!isGemini) internalProviderId = "codex";
35549
- console.log(chalk.dim(`
35787
+ const oauthStartSpinner = p26.spinner();
35788
+ oauthStartSpinner.start("Starting GitHub OAuth sign-in...");
35789
+ const result = await runOAuthFlow("copilot");
35790
+ oauthStartSpinner.stop(
35791
+ result ? "GitHub OAuth sign-in completed" : "GitHub OAuth sign-in cancelled"
35792
+ );
35793
+ if (!result) return false;
35794
+ }
35795
+ selectedAuthMethod = "oauth";
35796
+ } else {
35797
+ const tokenEnvVar = isGemini ? "GEMINI_OAUTH_TOKEN" : "OPENAI_CODEX_TOKEN";
35798
+ if (oauthConnected) {
35799
+ const refreshSpinner = p26.spinner();
35800
+ refreshSpinner.start("Refreshing existing OAuth session...");
35801
+ try {
35802
+ const tokenResult = await getOrRefreshOAuthToken(oauthProviderName);
35803
+ if (tokenResult) {
35804
+ process.env[tokenEnvVar] = tokenResult.accessToken;
35805
+ selectedAuthMethod = "oauth";
35806
+ if (!isGemini) internalProviderId = "codex";
35807
+ refreshSpinner.stop("OAuth session ready");
35808
+ console.log(chalk.dim(`
35550
35809
  Using existing OAuth session...`));
35551
- } else {
35552
- const result = await runOAuthFlow(newProvider.id);
35553
- if (!result) return false;
35554
- process.env[tokenEnvVar] = result.accessToken;
35555
- selectedAuthMethod = "oauth";
35556
- if (!isGemini) internalProviderId = "codex";
35557
- }
35558
- } catch {
35810
+ } else {
35811
+ refreshSpinner.stop("OAuth refresh failed, re-authentication required");
35812
+ const oauthStartSpinner = p26.spinner();
35813
+ oauthStartSpinner.start("Starting OAuth sign-in...");
35559
35814
  const result = await runOAuthFlow(newProvider.id);
35815
+ oauthStartSpinner.stop(
35816
+ result ? "OAuth sign-in completed" : "OAuth sign-in cancelled"
35817
+ );
35560
35818
  if (!result) return false;
35561
35819
  process.env[tokenEnvVar] = result.accessToken;
35562
35820
  selectedAuthMethod = "oauth";
35563
35821
  if (!isGemini) internalProviderId = "codex";
35564
35822
  }
35565
- } else {
35823
+ } catch {
35824
+ refreshSpinner.stop("OAuth refresh failed, re-authentication required");
35825
+ const oauthStartSpinner = p26.spinner();
35826
+ oauthStartSpinner.start("Starting OAuth sign-in...");
35566
35827
  const result = await runOAuthFlow(newProvider.id);
35828
+ oauthStartSpinner.stop(result ? "OAuth sign-in completed" : "OAuth sign-in cancelled");
35567
35829
  if (!result) return false;
35568
35830
  process.env[tokenEnvVar] = result.accessToken;
35569
35831
  selectedAuthMethod = "oauth";
35570
35832
  if (!isGemini) internalProviderId = "codex";
35571
35833
  }
35572
- }
35573
- } else if (authChoice === "gcloud") {
35574
- const adcResult = await setupGcloudADCForProvider();
35575
- if (!adcResult) return false;
35576
- selectedAuthMethod = "gcloud";
35577
- if (newProvider.id === "vertex") {
35578
- const settings = await promptVertexSettings2({
35579
- project: session.config.provider.project,
35580
- location: session.config.provider.location
35581
- });
35582
- if (!settings) return false;
35583
- vertexSettings = settings;
35584
- }
35585
- } else if (authChoice === "apikey") {
35586
- if (apiKey) {
35587
- selectedAuthMethod = "apikey";
35588
- console.log(chalk.dim(`
35589
- Using existing API key...`));
35590
35834
  } else {
35591
- const key = await p26.password({
35592
- message: `Enter your ${newProvider.name} API key:`,
35593
- validate: (v) => !v || v.length < 10 ? "API key too short" : void 0
35594
- });
35595
- if (p26.isCancel(key)) {
35596
- return false;
35597
- }
35598
- process.env[newProvider.envVar] = key;
35599
- selectedAuthMethod = "apikey";
35600
- newApiKeyForSaving = key;
35601
- }
35602
- if (newProvider.id === "vertex") {
35603
- const settings = await promptVertexSettings2({
35604
- project: session.config.provider.project,
35605
- location: session.config.provider.location
35606
- });
35607
- if (!settings) return false;
35608
- vertexSettings = settings;
35609
- }
35610
- } else if (authChoice === "remove") {
35611
- const removeOptions = [];
35612
- if (oauthConnected) {
35613
- removeOptions.push({
35614
- value: "oauth",
35615
- label: "\u{1F510} Remove OAuth session"
35616
- });
35617
- }
35618
- if (apiKey) {
35619
- removeOptions.push({
35620
- value: "apikey",
35621
- label: "\u{1F511} Remove API key"
35622
- });
35623
- }
35624
- if (oauthConnected && apiKey) {
35625
- removeOptions.push({
35626
- value: "all",
35627
- label: "\u{1F5D1}\uFE0F Remove all credentials"
35628
- });
35835
+ const oauthStartSpinner = p26.spinner();
35836
+ oauthStartSpinner.start("Starting OAuth sign-in...");
35837
+ const result = await runOAuthFlow(newProvider.id);
35838
+ oauthStartSpinner.stop(result ? "OAuth sign-in completed" : "OAuth sign-in cancelled");
35839
+ if (!result) return false;
35840
+ process.env[tokenEnvVar] = result.accessToken;
35841
+ selectedAuthMethod = "oauth";
35842
+ if (!isGemini) internalProviderId = "codex";
35629
35843
  }
35630
- removeOptions.push({
35631
- value: "cancel",
35632
- label: "\u274C Cancel"
35633
- });
35634
- const removeChoice = await p26.select({
35635
- message: "What would you like to remove?",
35636
- options: removeOptions
35844
+ }
35845
+ } else if (authChoice === "gcloud") {
35846
+ const adcResult = await setupGcloudADCForProvider();
35847
+ if (!adcResult) return false;
35848
+ selectedAuthMethod = "gcloud";
35849
+ if (newProvider.id === "vertex") {
35850
+ const settings = await promptVertexSettings2({
35851
+ project: session.config.provider.project,
35852
+ location: session.config.provider.location
35637
35853
  });
35638
- if (p26.isCancel(removeChoice) || removeChoice === "cancel") {
35639
- return false;
35640
- }
35641
- if (removeChoice === "oauth" || removeChoice === "all") {
35642
- if (oauthProviderName === "copilot") {
35643
- await deleteCopilotCredentials();
35644
- } else {
35645
- await deleteTokens(oauthProviderName);
35646
- }
35647
- await clearAuthMethod(newProvider.id);
35648
- console.log(chalk.green("\u2713 OAuth session removed"));
35649
- }
35650
- if (removeChoice === "apikey" || removeChoice === "all") {
35651
- delete process.env[newProvider.envVar];
35652
- console.log(chalk.green("\u2713 API key removed from session"));
35653
- console.log(chalk.dim(` Note: If key is in ~/.coco/.env, remove it there too`));
35654
- }
35655
- console.log("");
35656
- return false;
35854
+ if (!settings) return false;
35855
+ vertexSettings = settings;
35657
35856
  }
35658
- } else {
35659
- console.log(chalk.yellow(`
35660
- ${newProvider.emoji} ${newProvider.name} is not configured.`));
35661
- if (hasGcloudADC && !supportsApiKey) {
35662
- const adcResult = await setupGcloudADCForProvider();
35663
- if (!adcResult) return false;
35664
- selectedAuthMethod = "gcloud";
35665
- if (newProvider.id === "vertex") {
35666
- const settings = await promptVertexSettings2({
35667
- project: session.config.provider.project,
35668
- location: session.config.provider.location
35669
- });
35670
- if (!settings) return false;
35671
- vertexSettings = settings;
35672
- }
35857
+ } else if (authChoice === "apikey") {
35858
+ if (apiKey) {
35859
+ selectedAuthMethod = "apikey";
35860
+ console.log(chalk.dim(`
35861
+ Using existing API key...`));
35673
35862
  } else {
35674
35863
  const key = await p26.password({
35675
35864
  message: `Enter your ${newProvider.name} API key:`,
@@ -35682,18 +35871,93 @@ ${newProvider.emoji} ${newProvider.name} is not configured.`));
35682
35871
  selectedAuthMethod = "apikey";
35683
35872
  newApiKeyForSaving = key;
35684
35873
  }
35874
+ if (newProvider.id === "vertex") {
35875
+ const settings = await promptVertexSettings2({
35876
+ project: session.config.provider.project,
35877
+ location: session.config.provider.location
35878
+ });
35879
+ if (!settings) return false;
35880
+ vertexSettings = settings;
35881
+ }
35882
+ } else if (authChoice === "remove") {
35883
+ const removeOptions = [];
35884
+ if (oauthConnected) {
35885
+ removeOptions.push({
35886
+ value: "oauth",
35887
+ label: "\u{1F510} Remove OAuth session"
35888
+ });
35889
+ }
35890
+ if (apiKey) {
35891
+ removeOptions.push({
35892
+ value: "apikey",
35893
+ label: "\u{1F511} Remove API key"
35894
+ });
35895
+ }
35896
+ if (adcConnected) {
35897
+ removeOptions.push({
35898
+ value: "gcloud",
35899
+ label: "\u2601\uFE0F Revoke gcloud ADC session"
35900
+ });
35901
+ }
35902
+ if (oauthConnected && apiKey || adcConnected && (oauthConnected || apiKey)) {
35903
+ removeOptions.push({
35904
+ value: "all",
35905
+ label: "\u{1F5D1}\uFE0F Remove all credentials"
35906
+ });
35907
+ }
35908
+ removeOptions.push({
35909
+ value: "cancel",
35910
+ label: "\u274C Cancel"
35911
+ });
35912
+ const removeChoice = await p26.select({
35913
+ message: "What would you like to remove?",
35914
+ options: removeOptions
35915
+ });
35916
+ if (p26.isCancel(removeChoice) || removeChoice === "cancel") {
35917
+ return false;
35918
+ }
35919
+ if (removeChoice === "oauth" || removeChoice === "all") {
35920
+ if (oauthProviderName === "copilot") {
35921
+ await deleteCopilotCredentials();
35922
+ } else {
35923
+ await deleteTokens(oauthProviderName);
35924
+ }
35925
+ await clearAuthMethod(newProvider.id);
35926
+ console.log(chalk.green("\u2713 OAuth session removed"));
35927
+ }
35928
+ if (removeChoice === "apikey" || removeChoice === "all") {
35929
+ delete process.env[newProvider.envVar];
35930
+ console.log(chalk.green("\u2713 API key removed from session"));
35931
+ console.log(chalk.dim(` Note: If key is in ~/.coco/.env, remove it there too`));
35932
+ }
35933
+ if (removeChoice === "gcloud" || removeChoice === "all") {
35934
+ const revokeSpinner = p26.spinner();
35935
+ revokeSpinner.start("Revoking gcloud ADC session...");
35936
+ const revoked = await runGcloudADCRevoke();
35937
+ revokeSpinner.stop(revoked ? "gcloud ADC session revoked" : "Failed to revoke gcloud ADC");
35938
+ if (!revoked) {
35939
+ console.log(chalk.yellow("\u26A0\uFE0F Could not revoke ADC from Coco."));
35940
+ console.log(chalk.dim(" Try manually: gcloud auth application-default revoke"));
35941
+ } else {
35942
+ console.log(chalk.green("\u2713 gcloud ADC session revoked"));
35943
+ }
35944
+ }
35945
+ console.log("");
35946
+ return false;
35685
35947
  }
35686
35948
  }
35687
35949
  const rememberedModel = await getLastUsedModel(newProvider.id);
35688
35950
  const recommendedModel = getRecommendedModel(newProvider.id);
35689
35951
  const newModel = rememberedModel || recommendedModel?.id || newProvider.models[0]?.id || "";
35952
+ const resolvedVertexProject = newProvider.id === "vertex" ? (vertexSettings?.project ?? session.config.provider.project ?? process.env["VERTEX_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCLOUD_PROJECT"] ?? "").trim() : void 0;
35953
+ const resolvedVertexLocation = newProvider.id === "vertex" ? (vertexSettings?.location ?? session.config.provider.location ?? process.env["VERTEX_LOCATION"] ?? process.env["GOOGLE_CLOUD_LOCATION"] ?? "global").trim() : void 0;
35690
35954
  const spinner18 = p26.spinner();
35691
35955
  spinner18.start(`Connecting to ${newProvider.name}...`);
35692
35956
  try {
35693
35957
  const testProvider = await createProvider(internalProviderId, {
35694
35958
  model: newModel,
35695
- project: vertexSettings?.project,
35696
- location: vertexSettings?.location
35959
+ project: resolvedVertexProject,
35960
+ location: resolvedVertexLocation
35697
35961
  });
35698
35962
  const available = await testProvider.isAvailable();
35699
35963
  if (!available) {
@@ -35707,8 +35971,14 @@ ${newProvider.emoji} ${newProvider.name} is not configured.`));
35707
35971
  session.config.provider.type = userFacingProviderId;
35708
35972
  session.config.provider.model = newModel;
35709
35973
  if (userFacingProviderId === "vertex") {
35710
- session.config.provider.project = vertexSettings?.project;
35711
- session.config.provider.location = vertexSettings?.location;
35974
+ session.config.provider.project = resolvedVertexProject;
35975
+ session.config.provider.location = resolvedVertexLocation;
35976
+ if (resolvedVertexProject) {
35977
+ process.env["VERTEX_PROJECT"] = resolvedVertexProject;
35978
+ }
35979
+ if (resolvedVertexLocation) {
35980
+ process.env["VERTEX_LOCATION"] = resolvedVertexLocation;
35981
+ }
35712
35982
  } else {
35713
35983
  delete session.config.provider.project;
35714
35984
  delete session.config.provider.location;
@@ -35718,13 +35988,13 @@ ${newProvider.emoji} ${newProvider.name} is not configured.`));
35718
35988
  type: userFacingProviderId,
35719
35989
  model: newModel,
35720
35990
  apiKey: newApiKeyForSaving,
35721
- project: vertexSettings?.project,
35722
- location: vertexSettings?.location
35991
+ project: resolvedVertexProject,
35992
+ location: resolvedVertexLocation
35723
35993
  });
35724
35994
  } else {
35725
35995
  await saveProviderPreference(userFacingProviderId, newModel, {
35726
- project: vertexSettings?.project,
35727
- location: vertexSettings?.location
35996
+ project: resolvedVertexProject,
35997
+ location: resolvedVertexLocation
35728
35998
  });
35729
35999
  }
35730
36000
  console.log(chalk.green(`
@@ -35747,16 +36017,53 @@ ${newProvider.emoji} ${newProvider.name} is not configured.`));
35747
36017
  return false;
35748
36018
  }
35749
36019
  async function setupGcloudADCForProvider(_provider) {
36020
+ const gcloudCheckSpinner = p26.spinner();
36021
+ gcloudCheckSpinner.start("Checking gcloud CLI...");
35750
36022
  const gcloudInstalled = await isGcloudInstalled();
36023
+ gcloudCheckSpinner.stop(gcloudInstalled ? "gcloud CLI detected" : "gcloud CLI not detected");
35751
36024
  if (!gcloudInstalled) {
35752
36025
  p26.log.error("gcloud CLI is not installed");
35753
36026
  console.log(chalk.dim(" Install it from: https://cloud.google.com/sdk/docs/install\n"));
35754
36027
  return false;
35755
36028
  }
36029
+ const adcInspectSpinner = p26.spinner();
36030
+ adcInspectSpinner.start("Checking existing ADC credentials...");
35756
36031
  const adc = await inspectADC();
36032
+ adcInspectSpinner.stop(
36033
+ adc.status === "ok" && adc.token ? "ADC credentials found" : "No reusable ADC credentials found"
36034
+ );
35757
36035
  if (adc.status === "ok" && adc.token) {
35758
36036
  console.log(chalk.green(" \u2713 gcloud ADC is already configured!\n"));
35759
- return true;
36037
+ const adcChoice = await p26.select({
36038
+ message: "ADC session detected. What do you want to do?",
36039
+ options: [
36040
+ {
36041
+ value: "use",
36042
+ label: "Use current ADC session"
36043
+ },
36044
+ {
36045
+ value: "switch",
36046
+ label: "Switch Google account (revoke and re-login)"
36047
+ },
36048
+ {
36049
+ value: "cancel",
36050
+ label: "Cancel"
36051
+ }
36052
+ ]
36053
+ });
36054
+ if (p26.isCancel(adcChoice) || adcChoice === "cancel") return false;
36055
+ if (adcChoice === "use") return true;
36056
+ const revokeSpinner = p26.spinner();
36057
+ revokeSpinner.start("Revoking current gcloud ADC session...");
36058
+ const revoked = await runGcloudADCRevoke();
36059
+ revokeSpinner.stop(
36060
+ revoked ? "Current ADC session revoked" : "Could not revoke current ADC session"
36061
+ );
36062
+ if (!revoked) {
36063
+ console.log(chalk.yellow("\u26A0\uFE0F Could not revoke ADC from Coco."));
36064
+ console.log(chalk.dim(" Try manually: gcloud auth application-default revoke\n"));
36065
+ return false;
36066
+ }
35760
36067
  }
35761
36068
  console.log(chalk.yellow("\n No reusable gcloud ADC session was found for Coco."));
35762
36069
  console.log();
@@ -35769,9 +36076,17 @@ async function setupGcloudADCForProvider(_provider) {
35769
36076
  if (p26.isCancel(runLoginNow)) return false;
35770
36077
  if (runLoginNow) {
35771
36078
  p26.log.step("Running `gcloud auth application-default login`...");
36079
+ const loginSpinner = p26.spinner();
36080
+ loginSpinner.start("Launching gcloud login flow (browser may open)...");
35772
36081
  const loginOk = await runGcloudADCLogin();
36082
+ loginSpinner.stop(loginOk ? "gcloud login flow completed" : "gcloud login flow failed");
35773
36083
  if (loginOk) {
36084
+ const recheckSpinner = p26.spinner();
36085
+ recheckSpinner.start("Verifying ADC credentials after login...");
35774
36086
  const refreshed = await inspectADC();
36087
+ recheckSpinner.stop(
36088
+ refreshed.status === "ok" && refreshed.token ? "ADC credentials verified" : "ADC verification failed after login"
36089
+ );
35775
36090
  if (refreshed.status === "ok" && refreshed.token) {
35776
36091
  console.log(chalk.green(" \u2713 gcloud ADC is now configured.\n"));
35777
36092
  return true;
@@ -35794,6 +36109,19 @@ async function setupGcloudADCForProvider(_provider) {
35794
36109
  async function promptVertexSettings2(defaults) {
35795
36110
  const projectDefault = defaults?.project ?? process.env["VERTEX_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCLOUD_PROJECT"] ?? "";
35796
36111
  const locationDefault = defaults?.location ?? process.env["VERTEX_LOCATION"] ?? process.env["GOOGLE_CLOUD_LOCATION"] ?? "global";
36112
+ console.log(chalk.dim("\n Need help finding these values?"));
36113
+ console.log(chalk.cyan(" $ gcloud projects list"));
36114
+ console.log(chalk.cyan(" $ gcloud config set project <PROJECT_ID>"));
36115
+ console.log(chalk.cyan(" $ gcloud config get-value project"));
36116
+ console.log(chalk.cyan(" $ gcloud config get-value compute/region"));
36117
+ console.log(
36118
+ chalk.cyan(" $ gcloud config set compute/region <LOCATION> # e.g. global, europe-west1")
36119
+ );
36120
+ console.log(
36121
+ chalk.dim(
36122
+ " (If compute/region is unset, set it as above, then use that value for Vertex location)\n"
36123
+ )
36124
+ );
35797
36125
  const project = await p26.text({
35798
36126
  message: "Google Cloud project ID:",
35799
36127
  placeholder: projectDefault || "my-gcp-project",
@@ -39044,10 +39372,8 @@ async function revokePath(dirPath, _session) {
39044
39372
 
39045
39373
  // src/cli/repl/commands/permissions.ts
39046
39374
  init_session();
39047
- init_paths();
39048
39375
 
39049
39376
  // src/cli/repl/recommended-permissions.ts
39050
- init_paths();
39051
39377
  init_session();
39052
39378
  var RECOMMENDED_GLOBAL = [
39053
39379
  // ── Coco native tools (read-only) ──
@@ -39383,100 +39709,94 @@ var RECOMMENDED_DENY = [
39383
39709
  "bash:eval",
39384
39710
  "bash:source"
39385
39711
  ];
39386
- function getProjectPreferenceKey(projectPath) {
39387
- return path39__default.resolve(projectPath);
39712
+ function getProjectPermissionStatePath(projectPath) {
39713
+ return path39__default.join(projectPath, ".coco", "recommended-permissions.json");
39388
39714
  }
39389
- async function loadPermissionPreferences() {
39715
+ async function resolvePermissionScopePath(projectPath) {
39716
+ let resolved = path39__default.resolve(projectPath);
39390
39717
  try {
39391
- const content = await fs35__default.readFile(CONFIG_PATHS.config, "utf-8");
39392
- const config = JSON.parse(content);
39393
- return {
39394
- recommendedAllowlistApplied: config.recommendedAllowlistApplied,
39395
- recommendedAllowlistDismissed: config.recommendedAllowlistDismissed,
39396
- recommendedAllowlistPrompted: config.recommendedAllowlistPrompted,
39397
- recommendedAllowlistPromptedProjects: config.recommendedAllowlistPromptedProjects,
39398
- recommendedAllowlistAppliedProjects: config.recommendedAllowlistAppliedProjects,
39399
- recommendedAllowlistDismissedProjects: config.recommendedAllowlistDismissedProjects
39400
- };
39718
+ resolved = await fs35__default.realpath(resolved);
39401
39719
  } catch {
39402
- return {};
39403
39720
  }
39404
- }
39405
- async function savePermissionPreference(key, value) {
39406
- try {
39407
- let config = {};
39721
+ let current = resolved;
39722
+ while (true) {
39408
39723
  try {
39409
- const content = await fs35__default.readFile(CONFIG_PATHS.config, "utf-8");
39410
- config = JSON.parse(content);
39724
+ await fs35__default.access(path39__default.join(current, ".git"));
39725
+ return current;
39411
39726
  } catch {
39412
39727
  }
39413
- config[key] = value;
39414
- await fs35__default.mkdir(path39__default.dirname(CONFIG_PATHS.config), { recursive: true });
39415
- await fs35__default.writeFile(CONFIG_PATHS.config, JSON.stringify(config, null, 2), "utf-8");
39416
- } catch {
39417
- }
39418
- }
39419
- function isRecommendedAllowlistAppliedForProject(prefs, projectPath) {
39420
- const projectKey = getProjectPreferenceKey(projectPath);
39421
- if (prefs.recommendedAllowlistAppliedProjects?.[projectKey] === true) {
39422
- return true;
39423
- }
39424
- if (prefs.recommendedAllowlistApplied === true && !prefs.recommendedAllowlistAppliedProjects) {
39425
- return true;
39728
+ const parent = path39__default.dirname(current);
39729
+ if (parent === current) break;
39730
+ current = parent;
39426
39731
  }
39427
- return false;
39732
+ return resolved;
39428
39733
  }
39429
- function isRecommendedAllowlistDismissedForProject(prefs, projectPath) {
39430
- const projectKey = getProjectPreferenceKey(projectPath);
39431
- if (prefs.recommendedAllowlistDismissedProjects?.[projectKey] === true) {
39432
- return true;
39433
- }
39434
- if (prefs.recommendedAllowlistDismissed === true && !prefs.recommendedAllowlistDismissedProjects) {
39435
- return true;
39734
+ async function loadProjectPermissionState(projectPath) {
39735
+ const defaultState = {
39736
+ applied: false,
39737
+ dismissed: false,
39738
+ prompted: false,
39739
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
39740
+ };
39741
+ try {
39742
+ const content = await fs35__default.readFile(getProjectPermissionStatePath(projectPath), "utf-8");
39743
+ const parsed = JSON.parse(content);
39744
+ return {
39745
+ applied: parsed.applied ?? defaultState.applied,
39746
+ dismissed: parsed.dismissed ?? defaultState.dismissed,
39747
+ prompted: parsed.prompted ?? defaultState.prompted,
39748
+ updatedAt: parsed.updatedAt ?? defaultState.updatedAt
39749
+ };
39750
+ } catch {
39751
+ return defaultState;
39436
39752
  }
39437
- return false;
39438
39753
  }
39439
- async function saveProjectPermissionPreference(key, projectPath, value) {
39754
+ async function saveProjectPermissionState(projectPath, update) {
39440
39755
  try {
39441
- let config = {};
39442
- try {
39443
- const content = await fs35__default.readFile(CONFIG_PATHS.config, "utf-8");
39444
- config = JSON.parse(content);
39445
- } catch {
39446
- }
39447
- const projectKey = getProjectPreferenceKey(projectPath);
39448
- const currentMap = config[key] ?? {};
39449
- config[key] = {
39450
- ...currentMap,
39451
- [projectKey]: value
39756
+ const current = await loadProjectPermissionState(projectPath);
39757
+ const next = {
39758
+ ...current,
39759
+ ...update,
39760
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
39452
39761
  };
39453
- await fs35__default.mkdir(path39__default.dirname(CONFIG_PATHS.config), { recursive: true });
39454
- await fs35__default.writeFile(CONFIG_PATHS.config, JSON.stringify(config, null, 2), "utf-8");
39762
+ const filePath = getProjectPermissionStatePath(projectPath);
39763
+ await fs35__default.mkdir(path39__default.dirname(filePath), { recursive: true });
39764
+ await fs35__default.writeFile(filePath, JSON.stringify(next, null, 2), "utf-8");
39455
39765
  } catch {
39456
39766
  }
39457
39767
  }
39768
+ async function getProjectPermissionState(projectPath) {
39769
+ const scopePath = await resolvePermissionScopePath(projectPath);
39770
+ return loadProjectPermissionState(scopePath);
39771
+ }
39772
+ async function saveProjectPermissionPreference(key, projectPath, value) {
39773
+ const scopePath = await resolvePermissionScopePath(projectPath);
39774
+ if (key === "recommendedAllowlistAppliedProjects") {
39775
+ await saveProjectPermissionState(scopePath, { applied: value });
39776
+ return;
39777
+ }
39778
+ await saveProjectPermissionState(scopePath, { dismissed: value });
39779
+ }
39458
39780
  async function shouldShowPermissionSuggestion(projectPath = process.cwd()) {
39459
- const prefs = await loadPermissionPreferences();
39460
- if (isRecommendedAllowlistDismissedForProject(prefs, projectPath)) {
39781
+ const state = await getProjectPermissionState(projectPath);
39782
+ if (state.dismissed) {
39461
39783
  return false;
39462
39784
  }
39463
- if (isRecommendedAllowlistAppliedForProject(prefs, projectPath)) {
39785
+ if (state.applied) {
39464
39786
  return false;
39465
39787
  }
39466
39788
  return true;
39467
39789
  }
39468
39790
  async function applyRecommendedPermissions(projectPath = process.cwd()) {
39791
+ const scopePath = await resolvePermissionScopePath(projectPath);
39469
39792
  for (const tool of [...RECOMMENDED_GLOBAL, ...RECOMMENDED_PROJECT]) {
39470
39793
  await saveTrustedTool(tool, projectPath, false);
39471
39794
  }
39472
- await saveProjectPermissionPreference("recommendedAllowlistAppliedProjects", projectPath, true);
39473
- await saveProjectPermissionPreference(
39474
- "recommendedAllowlistDismissedProjects",
39475
- projectPath,
39476
- false
39477
- );
39795
+ await saveProjectPermissionPreference("recommendedAllowlistAppliedProjects", scopePath, true);
39796
+ await saveProjectPermissionPreference("recommendedAllowlistDismissedProjects", scopePath, false);
39478
39797
  }
39479
39798
  async function showPermissionSuggestion(projectPath = process.cwd()) {
39799
+ const scopePath = await resolvePermissionScopePath(projectPath);
39480
39800
  console.log();
39481
39801
  console.log(chalk.magenta.bold(" \u{1F4CB} Recommended Permissions"));
39482
39802
  console.log();
@@ -39487,7 +39807,7 @@ async function showPermissionSuggestion(projectPath = process.cwd()) {
39487
39807
  );
39488
39808
  console.log(chalk.dim(" \u2022 Deny: sudo, git push, docker push, inline code exec, DNS exfil..."));
39489
39809
  console.log();
39490
- console.log(chalk.dim(" Stored in ~/.coco/trusted-tools.json \u2014 edit manually or let"));
39810
+ console.log(chalk.dim(" Stored in .coco/trusted-tools.json \u2014 edit manually or let"));
39491
39811
  console.log(chalk.dim(" Coco manage it when you approve actions from the prompt."));
39492
39812
  console.log(chalk.dim(" Note: applying here affects only the current project."));
39493
39813
  console.log();
@@ -39508,11 +39828,7 @@ async function showPermissionSuggestion(projectPath = process.cwd()) {
39508
39828
  return;
39509
39829
  }
39510
39830
  if (action === "dismiss") {
39511
- await saveProjectPermissionPreference(
39512
- "recommendedAllowlistDismissedProjects",
39513
- projectPath,
39514
- true
39515
- );
39831
+ await saveProjectPermissionPreference("recommendedAllowlistDismissedProjects", scopePath, true);
39516
39832
  console.log(chalk.dim(" Won't show again. Use /permissions to apply later."));
39517
39833
  return;
39518
39834
  }
@@ -39612,12 +39928,12 @@ var permissionsCommand = {
39612
39928
  };
39613
39929
  async function showStatus(session) {
39614
39930
  const tools = await getAllTrustedTools(session.projectPath);
39615
- const prefs = await loadPermissionPreferences();
39931
+ const permissionState = await getProjectPermissionState(session.projectPath);
39616
39932
  console.log();
39617
39933
  console.log(chalk.magenta.bold(" \u{1F510} Tool Permissions"));
39618
39934
  console.log();
39619
39935
  const allowCount = RECOMMENDED_GLOBAL.length + RECOMMENDED_PROJECT.length;
39620
- if (isRecommendedAllowlistAppliedForProject(prefs, session.projectPath)) {
39936
+ if (permissionState.applied) {
39621
39937
  console.log(
39622
39938
  chalk.green(" \u2713 Recommended allowlist applied") + chalk.dim(` (${allowCount} allow, ${RECOMMENDED_DENY.length} deny)`)
39623
39939
  );
@@ -39698,25 +40014,26 @@ async function resetPermissions(session) {
39698
40014
  return;
39699
40015
  }
39700
40016
  session.trustedTools.clear();
39701
- const emptySettings = {
39702
- globalTrusted: [],
39703
- projectTrusted: {},
39704
- projectDenied: {},
40017
+ const emptyProjectSettings = {
40018
+ trusted: [],
40019
+ denied: [],
39705
40020
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
39706
40021
  };
39707
40022
  try {
39708
- await fs35__default.writeFile(CONFIG_PATHS.trustedTools, JSON.stringify(emptySettings, null, 2), "utf-8");
40023
+ const projectTrustPath = `${session.projectPath}/.coco/trusted-tools.json`;
40024
+ await fs35__default.mkdir(`${session.projectPath}/.coco`, { recursive: true });
40025
+ await fs35__default.writeFile(projectTrustPath, JSON.stringify(emptyProjectSettings, null, 2), "utf-8");
39709
40026
  } catch {
39710
40027
  }
39711
- await savePermissionPreference("recommendedAllowlistApplied", false);
40028
+ const permissionScopePath = session.projectPath;
39712
40029
  await saveProjectPermissionPreference(
39713
40030
  "recommendedAllowlistAppliedProjects",
39714
- session.projectPath,
40031
+ permissionScopePath,
39715
40032
  false
39716
40033
  );
39717
40034
  await saveProjectPermissionPreference(
39718
40035
  "recommendedAllowlistDismissedProjects",
39719
- session.projectPath,
40036
+ permissionScopePath,
39720
40037
  false
39721
40038
  );
39722
40039
  console.log(chalk.green(" \u2713 All tool permissions reset."));
@@ -43258,7 +43575,8 @@ var AgentManager = class extends EventEmitter {
43258
43575
  type: "tool_use",
43259
43576
  id: tc.id,
43260
43577
  name: tc.name,
43261
- input: tc.input
43578
+ input: tc.input,
43579
+ geminiThoughtSignature: tc.geminiThoughtSignature
43262
43580
  }));
43263
43581
  const assistantContent = response.content ? [{ type: "text", text: response.content }, ...toolUses] : toolUses;
43264
43582
  messages.push({ role: "assistant", content: assistantContent });
@@ -49171,7 +49489,7 @@ function buildProgressBar(pct) {
49171
49489
 
49172
49490
  // src/cli/repl/worktree/manager.ts
49173
49491
  init_logger();
49174
- var execFileAsync3 = promisify(execFile);
49492
+ var execFileAsync4 = promisify(execFile);
49175
49493
  var WORKTREES_DIR = ".worktrees";
49176
49494
  var WorktreeManager = class {
49177
49495
  worktrees = /* @__PURE__ */ new Map();
@@ -49395,7 +49713,7 @@ var WorktreeManager = class {
49395
49713
  try {
49396
49714
  await this.git(["push", "-u", "origin", worktree.branch]);
49397
49715
  const title = options.message ?? `Agent: ${worktree.name}`;
49398
- const { stdout } = await execFileAsync3(
49716
+ const { stdout } = await execFileAsync4(
49399
49717
  "gh",
49400
49718
  [
49401
49719
  "pr",
@@ -49421,10 +49739,10 @@ var WorktreeManager = class {
49421
49739
  }
49422
49740
  // ── Helpers ──────────────────────────────────────────────────────
49423
49741
  async git(args) {
49424
- return execFileAsync3("git", args, { cwd: this.projectRoot });
49742
+ return execFileAsync4("git", args, { cwd: this.projectRoot });
49425
49743
  }
49426
49744
  async gitIn(cwd, args) {
49427
- return execFileAsync3("git", args, { cwd });
49745
+ return execFileAsync4("git", args, { cwd });
49428
49746
  }
49429
49747
  async countChangedFiles(branch) {
49430
49748
  try {
@@ -53054,7 +53372,8 @@ ${tail}`;
53054
53372
  toolCallBuilders.set(id, {
53055
53373
  id,
53056
53374
  name: toolName,
53057
- input: {}
53375
+ input: {},
53376
+ geminiThoughtSignature: chunk.toolCall.geminiThoughtSignature
53058
53377
  });
53059
53378
  if (toolName) {
53060
53379
  options.onToolPreparing?.(toolName);
@@ -53067,14 +53386,16 @@ ${tail}`;
53067
53386
  const finalToolCall = {
53068
53387
  id: builder.id,
53069
53388
  name: chunk.toolCall.name ?? builder.name,
53070
- input: chunk.toolCall.input ?? builder.input
53389
+ input: chunk.toolCall.input ?? builder.input,
53390
+ geminiThoughtSignature: chunk.toolCall.geminiThoughtSignature ?? builder.geminiThoughtSignature
53071
53391
  };
53072
53392
  collectedToolCalls.push(finalToolCall);
53073
53393
  } else if (chunk.toolCall.id && chunk.toolCall.name) {
53074
53394
  collectedToolCalls.push({
53075
53395
  id: chunk.toolCall.id,
53076
53396
  name: chunk.toolCall.name,
53077
- input: chunk.toolCall.input ?? {}
53397
+ input: chunk.toolCall.input ?? {},
53398
+ geminiThoughtSignature: chunk.toolCall.geminiThoughtSignature
53078
53399
  });
53079
53400
  }
53080
53401
  }
@@ -53173,9 +53494,25 @@ ${tail}`;
53173
53494
  break;
53174
53495
  }
53175
53496
  noToolRecoveryAttempts = 0;
53497
+ const dedupedToolCalls = [];
53498
+ const seenToolCallFingerprints = /* @__PURE__ */ new Set();
53499
+ for (const toolCall of collectedToolCalls) {
53500
+ let inputSerialized = "{}";
53501
+ try {
53502
+ inputSerialized = JSON.stringify(toolCall.input ?? {});
53503
+ } catch {
53504
+ inputSerialized = "{}";
53505
+ }
53506
+ const fingerprint = `${toolCall.name}:${inputSerialized}`;
53507
+ if (seenToolCallFingerprints.has(fingerprint)) {
53508
+ continue;
53509
+ }
53510
+ seenToolCallFingerprints.add(fingerprint);
53511
+ dedupedToolCalls.push(toolCall);
53512
+ }
53176
53513
  const response = {
53177
53514
  content: responseContent,
53178
- toolCalls: collectedToolCalls
53515
+ toolCalls: dedupedToolCalls
53179
53516
  };
53180
53517
  const toolResults = [];
53181
53518
  const toolUses = [];
@@ -53331,7 +53668,8 @@ ${tail}`;
53331
53668
  type: "tool_use",
53332
53669
  id: toolCall.id,
53333
53670
  name: toolCall.name,
53334
- input: toolCall.input
53671
+ input: toolCall.input,
53672
+ geminiThoughtSignature: toolCall.geminiThoughtSignature
53335
53673
  });
53336
53674
  const declineReason = declinedTools.get(toolCall.id);
53337
53675
  if (declineReason) {
@@ -54115,11 +54453,15 @@ async function startRepl(options = {}) {
54115
54453
  }
54116
54454
  session.config = configured;
54117
54455
  const internalProviderId = getInternalProviderId(session.config.provider.type);
54456
+ const initialVertexProject = session.config.provider.project ?? process.env["VERTEX_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCLOUD_PROJECT"];
54457
+ const initialVertexLocation = session.config.provider.location ?? process.env["VERTEX_LOCATION"] ?? process.env["GOOGLE_CLOUD_LOCATION"];
54118
54458
  let provider;
54119
54459
  try {
54120
54460
  provider = await createProvider(internalProviderId, {
54121
54461
  model: session.config.provider.model || void 0,
54122
- maxTokens: session.config.provider.maxTokens
54462
+ maxTokens: session.config.provider.maxTokens,
54463
+ project: initialVertexProject,
54464
+ location: initialVertexLocation
54123
54465
  });
54124
54466
  } catch (error) {
54125
54467
  p26.log.error(
@@ -54504,7 +54846,9 @@ async function startRepl(options = {}) {
54504
54846
  const newInternalId = getInternalProviderId(session.config.provider.type);
54505
54847
  provider = await createProvider(newInternalId, {
54506
54848
  model: session.config.provider.model || void 0,
54507
- maxTokens: session.config.provider.maxTokens
54849
+ maxTokens: session.config.provider.maxTokens,
54850
+ project: session.config.provider.project ?? process.env["VERTEX_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCLOUD_PROJECT"],
54851
+ location: session.config.provider.location ?? process.env["VERTEX_LOCATION"] ?? process.env["GOOGLE_CLOUD_LOCATION"]
54508
54852
  });
54509
54853
  setAgentProvider(provider);
54510
54854
  initializeContextManager(session, provider);