@corbat-tech/coco 2.27.5 → 2.28.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
@@ -2377,6 +2377,22 @@ async function runGcloudADCLogin() {
2377
2377
  }
2378
2378
  }
2379
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
+ }
2380
2396
  async function getGeminiADCKey() {
2381
2397
  const token = await getADCAccessToken();
2382
2398
  if (!token) return null;
@@ -2392,12 +2408,13 @@ async function getCachedADCToken() {
2392
2408
  function clearADCCache() {
2393
2409
  cachedToken = null;
2394
2410
  }
2395
- 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;
2396
2412
  var init_gcloud = __esm({
2397
2413
  "src/auth/gcloud.ts"() {
2398
2414
  execAsync = promisify(exec);
2399
2415
  PRINT_ACCESS_TOKEN_COMMAND = "gcloud auth application-default print-access-token";
2400
2416
  ADC_LOGIN_COMMAND = "gcloud auth application-default login";
2417
+ ADC_REVOKE_COMMAND = "gcloud auth application-default revoke --quiet";
2401
2418
  GEMINI_OAUTH_SCOPES = [
2402
2419
  "https://www.googleapis.com/auth/cloud-platform",
2403
2420
  "https://www.googleapis.com/auth/generative-language.retriever"
@@ -2445,6 +2462,7 @@ __export(auth_exports, {
2445
2462
  requestDeviceCode: () => requestDeviceCode,
2446
2463
  requestGitHubDeviceCode: () => requestGitHubDeviceCode,
2447
2464
  runGcloudADCLogin: () => runGcloudADCLogin,
2465
+ runGcloudADCRevoke: () => runGcloudADCRevoke,
2448
2466
  runOAuthFlow: () => runOAuthFlow,
2449
2467
  saveCopilotCredentials: () => saveCopilotCredentials,
2450
2468
  saveTokens: () => saveTokens,
@@ -6169,6 +6187,17 @@ function extractSseEventData(rawEvent) {
6169
6187
  }
6170
6188
  return dataLines.join("\n");
6171
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
+ }
6172
6201
  function createVertexProvider(config) {
6173
6202
  const provider = new VertexProvider();
6174
6203
  if (config) {
@@ -6271,6 +6300,7 @@ var init_vertex = __esm({
6271
6300
  );
6272
6301
  let stopReason = "end_turn";
6273
6302
  let streamToolCallCounter = 0;
6303
+ const emittedToolFingerprints = /* @__PURE__ */ new Set();
6274
6304
  for await (const chunk of stream) {
6275
6305
  const candidate = chunk.candidates?.[0];
6276
6306
  const parts = candidate?.content?.parts ?? [];
@@ -6279,6 +6309,11 @@ var init_vertex = __esm({
6279
6309
  yield { type: "text", text: part.text };
6280
6310
  }
6281
6311
  if (part.functionCall) {
6312
+ const fingerprint = getToolCallFingerprint(part);
6313
+ if (emittedToolFingerprints.has(fingerprint)) {
6314
+ continue;
6315
+ }
6316
+ emittedToolFingerprints.add(fingerprint);
6282
6317
  streamToolCallCounter++;
6283
6318
  const geminiThoughtSignature = part.thoughtSignature ?? part.thought_signature ?? part.functionCall.thoughtSignature ?? part.functionCall.thought_signature;
6284
6319
  yield {
@@ -22654,6 +22689,25 @@ var init_lifecycle = __esm({
22654
22689
  connections = /* @__PURE__ */ new Map();
22655
22690
  logger = getLogger();
22656
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
+ }
22657
22711
  /**
22658
22712
  * Create transport for a server config
22659
22713
  */
@@ -22740,15 +22794,11 @@ var init_lifecycle = __esm({
22740
22794
  }
22741
22795
  this.logger.info(`Stopping MCP server: ${name}`);
22742
22796
  try {
22743
- await Promise.race([
22797
+ await this.runWithTimeout(
22744
22798
  connection.transport.disconnect(),
22745
- new Promise(
22746
- (_, reject) => setTimeout(
22747
- () => reject(new Error("MCP disconnect timeout")),
22748
- _MCPServerManager.STOP_TIMEOUT_MS
22749
- )
22750
- )
22751
- ]);
22799
+ _MCPServerManager.STOP_TIMEOUT_MS,
22800
+ "MCP disconnect timeout"
22801
+ );
22752
22802
  } catch (error) {
22753
22803
  this.logger.error(
22754
22804
  `Error disconnecting server '${name}': ${error instanceof Error ? error.message : String(error)}`
@@ -22785,12 +22835,11 @@ var init_lifecycle = __esm({
22785
22835
  }
22786
22836
  const startTime = performance.now();
22787
22837
  try {
22788
- const { tools } = await Promise.race([
22838
+ const { tools } = await this.runWithTimeout(
22789
22839
  connection.client.listTools(),
22790
- new Promise(
22791
- (_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 5e3)
22792
- )
22793
- ]);
22840
+ 5e3,
22841
+ "Health check timeout"
22842
+ );
22794
22843
  const latencyMs = performance.now() - startTime;
22795
22844
  connection.healthy = true;
22796
22845
  connection.toolCount = tools.length;
@@ -29661,8 +29710,7 @@ var PROVIDER_DEFINITIONS = {
29661
29710
  name: "Gemini 3 Pro (Preview)",
29662
29711
  description: "Most capable Vertex Gemini 3 model (preview)",
29663
29712
  contextWindow: 1048576,
29664
- maxOutputTokens: 65536,
29665
- recommended: true
29713
+ maxOutputTokens: 65536
29666
29714
  },
29667
29715
  {
29668
29716
  id: "gemini-3-flash-preview",
@@ -29676,7 +29724,8 @@ var PROVIDER_DEFINITIONS = {
29676
29724
  name: "Gemini 2.5 Pro",
29677
29725
  description: "Stable high-quality Vertex model for coding and complex reasoning",
29678
29726
  contextWindow: 1048576,
29679
- maxOutputTokens: 65536
29727
+ maxOutputTokens: 65536,
29728
+ recommended: true
29680
29729
  },
29681
29730
  {
29682
29731
  id: "gemini-2.5-flash",
@@ -34417,6 +34466,41 @@ async function setupGcloudADC(provider) {
34417
34466
  if (adc.status === "ok" && adc.token) {
34418
34467
  console.log(chalk.green(" \u2713 gcloud ADC is already configured!"));
34419
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
+ }
34420
34504
  p26.log.success("Authentication verified");
34421
34505
  const vertexSettings2 = provider.id === "vertex" ? await promptVertexSettings() : void 0;
34422
34506
  if (provider.id === "vertex" && !vertexSettings2) return null;
@@ -34515,6 +34599,19 @@ async function setupGcloudADC(provider) {
34515
34599
  async function promptVertexSettings() {
34516
34600
  const projectDefault = process.env["VERTEX_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCLOUD_PROJECT"] ?? "";
34517
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
+ );
34518
34615
  const project = await p26.text({
34519
34616
  message: "Google Cloud project ID:",
34520
34617
  placeholder: projectDefault || "my-gcp-project",
@@ -35578,6 +35675,15 @@ async function switchProvider(initialProvider, session) {
35578
35675
  const apiKey = supportsApiKey ? process.env[newProvider.envVar] : void 0;
35579
35676
  const hasOAuth = supportsOAuth(newProvider.id) || newProvider.supportsOAuth;
35580
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
+ }
35581
35687
  const oauthProviderName = newProvider.id === "copilot" ? "copilot" : newProvider.id === "gemini" ? "gemini" : "openai";
35582
35688
  let oauthConnected = false;
35583
35689
  if (hasOAuth) {
@@ -35625,8 +35731,8 @@ async function switchProvider(initialProvider, session) {
35625
35731
  if (hasGcloudADC) {
35626
35732
  authOptions.push({
35627
35733
  value: "gcloud",
35628
- label: "\u2601\uFE0F Use gcloud ADC",
35629
- 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"
35630
35736
  });
35631
35737
  }
35632
35738
  if (supportsApiKey) {
@@ -35644,7 +35750,7 @@ async function switchProvider(initialProvider, session) {
35644
35750
  });
35645
35751
  }
35646
35752
  }
35647
- if (oauthConnected || apiKey) {
35753
+ if (oauthConnected || apiKey || adcConnected) {
35648
35754
  authOptions.push({
35649
35755
  value: "remove",
35650
35756
  label: "\u{1F5D1}\uFE0F Remove saved credentials",
@@ -35787,7 +35893,13 @@ Using existing API key...`));
35787
35893
  label: "\u{1F511} Remove API key"
35788
35894
  });
35789
35895
  }
35790
- if (oauthConnected && apiKey) {
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)) {
35791
35903
  removeOptions.push({
35792
35904
  value: "all",
35793
35905
  label: "\u{1F5D1}\uFE0F Remove all credentials"
@@ -35818,13 +35930,25 @@ Using existing API key...`));
35818
35930
  console.log(chalk.green("\u2713 API key removed from session"));
35819
35931
  console.log(chalk.dim(` Note: If key is in ~/.coco/.env, remove it there too`));
35820
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
+ }
35821
35945
  console.log("");
35822
35946
  return false;
35823
35947
  }
35824
35948
  }
35825
35949
  const rememberedModel = await getLastUsedModel(newProvider.id);
35826
35950
  const recommendedModel = getRecommendedModel(newProvider.id);
35827
- const newModel = rememberedModel || recommendedModel?.id || newProvider.models[0]?.id || "";
35951
+ let newModel = rememberedModel || recommendedModel?.id || newProvider.models[0]?.id || "";
35828
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;
35829
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;
35830
35954
  const spinner18 = p26.spinner();
@@ -35835,7 +35959,31 @@ Using existing API key...`));
35835
35959
  project: resolvedVertexProject,
35836
35960
  location: resolvedVertexLocation
35837
35961
  });
35838
- const available = await testProvider.isAvailable();
35962
+ let available = await testProvider.isAvailable();
35963
+ if (!available && newProvider.id === "vertex") {
35964
+ const fallbackModels = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash-001"].filter(
35965
+ (modelId) => modelId !== newModel
35966
+ );
35967
+ for (const fallbackModel of fallbackModels) {
35968
+ const fallbackProvider = await createProvider(internalProviderId, {
35969
+ model: fallbackModel,
35970
+ project: resolvedVertexProject,
35971
+ location: resolvedVertexLocation
35972
+ });
35973
+ const fallbackAvailable = await fallbackProvider.isAvailable();
35974
+ if (fallbackAvailable) {
35975
+ newModel = fallbackModel;
35976
+ available = true;
35977
+ console.log(
35978
+ chalk.yellow(
35979
+ `
35980
+ \u26A0\uFE0F The selected Vertex model was not available. Using fallback model: ${fallbackModel}`
35981
+ )
35982
+ );
35983
+ break;
35984
+ }
35985
+ }
35986
+ }
35839
35987
  if (!available) {
35840
35988
  spinner18.stop(chalk.red("Connection failed"));
35841
35989
  console.log(chalk.red(`
@@ -35910,7 +36058,36 @@ async function setupGcloudADCForProvider(_provider) {
35910
36058
  );
35911
36059
  if (adc.status === "ok" && adc.token) {
35912
36060
  console.log(chalk.green(" \u2713 gcloud ADC is already configured!\n"));
35913
- return true;
36061
+ const adcChoice = await p26.select({
36062
+ message: "ADC session detected. What do you want to do?",
36063
+ options: [
36064
+ {
36065
+ value: "use",
36066
+ label: "Use current ADC session"
36067
+ },
36068
+ {
36069
+ value: "switch",
36070
+ label: "Switch Google account (revoke and re-login)"
36071
+ },
36072
+ {
36073
+ value: "cancel",
36074
+ label: "Cancel"
36075
+ }
36076
+ ]
36077
+ });
36078
+ if (p26.isCancel(adcChoice) || adcChoice === "cancel") return false;
36079
+ if (adcChoice === "use") return true;
36080
+ const revokeSpinner = p26.spinner();
36081
+ revokeSpinner.start("Revoking current gcloud ADC session...");
36082
+ const revoked = await runGcloudADCRevoke();
36083
+ revokeSpinner.stop(
36084
+ revoked ? "Current ADC session revoked" : "Could not revoke current ADC session"
36085
+ );
36086
+ if (!revoked) {
36087
+ console.log(chalk.yellow("\u26A0\uFE0F Could not revoke ADC from Coco."));
36088
+ console.log(chalk.dim(" Try manually: gcloud auth application-default revoke\n"));
36089
+ return false;
36090
+ }
35914
36091
  }
35915
36092
  console.log(chalk.yellow("\n No reusable gcloud ADC session was found for Coco."));
35916
36093
  console.log();
@@ -35956,6 +36133,19 @@ async function setupGcloudADCForProvider(_provider) {
35956
36133
  async function promptVertexSettings2(defaults) {
35957
36134
  const projectDefault = defaults?.project ?? process.env["VERTEX_PROJECT"] ?? process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCLOUD_PROJECT"] ?? "";
35958
36135
  const locationDefault = defaults?.location ?? process.env["VERTEX_LOCATION"] ?? process.env["GOOGLE_CLOUD_LOCATION"] ?? "global";
36136
+ console.log(chalk.dim("\n Need help finding these values?"));
36137
+ console.log(chalk.cyan(" $ gcloud projects list"));
36138
+ console.log(chalk.cyan(" $ gcloud config set project <PROJECT_ID>"));
36139
+ console.log(chalk.cyan(" $ gcloud config get-value project"));
36140
+ console.log(chalk.cyan(" $ gcloud config get-value compute/region"));
36141
+ console.log(
36142
+ chalk.cyan(" $ gcloud config set compute/region <LOCATION> # e.g. global, europe-west1")
36143
+ );
36144
+ console.log(
36145
+ chalk.dim(
36146
+ " (If compute/region is unset, set it as above, then use that value for Vertex location)\n"
36147
+ )
36148
+ );
35959
36149
  const project = await p26.text({
35960
36150
  message: "Google Cloud project ID:",
35961
36151
  placeholder: projectDefault || "my-gcp-project",
@@ -53130,6 +53320,30 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
53130
53320
  [... ${omitted.toLocaleString()} characters omitted \u2014 use read_file with offset/limit to retrieve more of '${toolName}' output ...]
53131
53321
  ${tail}`;
53132
53322
  }
53323
+ function stableSerialize(value) {
53324
+ if (value === null || typeof value !== "object") {
53325
+ try {
53326
+ return JSON.stringify(value);
53327
+ } catch {
53328
+ return "null";
53329
+ }
53330
+ }
53331
+ if (Array.isArray(value)) {
53332
+ return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
53333
+ }
53334
+ const objectValue = value;
53335
+ const keys = Object.keys(objectValue).sort();
53336
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableSerialize(objectValue[key])}`).join(",")}}`;
53337
+ }
53338
+ function getToolCallDedupeFingerprint(toolCall) {
53339
+ if (toolCall.name === "bash_exec") {
53340
+ const input = toolCall.input ?? {};
53341
+ const command = String(input.command ?? "").replace(/\s+/g, " ").trim();
53342
+ const cwd = String(input.cwd ?? "").replace(/\s+/g, " ").trim();
53343
+ return `bash_exec:${command}:cwd=${cwd}`;
53344
+ }
53345
+ return `${toolCall.name}:${stableSerialize(toolCall.input ?? {})}`;
53346
+ }
53133
53347
  function shouldRecoverNoToolTurn(stopReason, content) {
53134
53348
  const trimmed = content.trim();
53135
53349
  if (stopReason === "tool_use") {
@@ -53328,9 +53542,19 @@ ${tail}`;
53328
53542
  break;
53329
53543
  }
53330
53544
  noToolRecoveryAttempts = 0;
53545
+ const dedupedToolCalls = [];
53546
+ const seenToolCallFingerprints = /* @__PURE__ */ new Set();
53547
+ for (const toolCall of collectedToolCalls) {
53548
+ const fingerprint = getToolCallDedupeFingerprint(toolCall);
53549
+ if (seenToolCallFingerprints.has(fingerprint)) {
53550
+ continue;
53551
+ }
53552
+ seenToolCallFingerprints.add(fingerprint);
53553
+ dedupedToolCalls.push(toolCall);
53554
+ }
53331
53555
  const response = {
53332
53556
  content: responseContent,
53333
- toolCalls: collectedToolCalls
53557
+ toolCalls: dedupedToolCalls
53334
53558
  };
53335
53559
  const toolResults = [];
53336
53560
  const toolUses = [];