pi-banana 2.1.2 → 2.3.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.
Files changed (2) hide show
  1. package/extensions/index.ts +89 -30
  2. package/package.json +1 -1
@@ -9,9 +9,13 @@
9
9
  * pi-coding-agent renders them inline (Kitty / iTerm2). Also writes the file
10
10
  * to disk so the agent and the user always have a path to refer back to.
11
11
  *
12
- * Auth: GOOGLE_API_KEY in the environment.
13
- * - AI Studio keys (AIza...) hit the Gemini API.
14
- * - Vertex AI Express keys (AQ....) automatically switch to Vertex.
12
+ * Auth (checked in this order — first match wins):
13
+ * 1. pi modelRegistry auth.json / env vars / models.json
14
+ * (set via /login Google, or GEMINI_API_KEY env var)
15
+ * 2. Vertex AI ADC — gcloud auth application-default login,
16
+ * GOOGLE_APPLICATION_CREDENTIALS, or GOOGLE_CLOUD_PROJECT
17
+ * 3. GOOGLE_API_KEY env var (AIza… or AQ.…) — pi-banana backward compat
18
+ * Vertex AI Express keys (AQ.…) automatically switch to Vertex.
15
19
  */
16
20
 
17
21
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -25,6 +29,7 @@ import {
25
29
  } from "@earendil-works/pi-tui";
26
30
  import { GoogleGenAI } from "@google/genai";
27
31
  import { existsSync } from "node:fs";
32
+ import { homedir } from "node:os";
28
33
  import { mkdir, readFile, writeFile } from "node:fs/promises";
29
34
  import { extname, isAbsolute, resolve } from "node:path";
30
35
  import { Type } from "typebox";
@@ -76,19 +81,71 @@ const SUPPORTED_INPUT_MIME = new Set([
76
81
 
77
82
  // ─── Helpers ───────────────────────────────────────────────────────────────
78
83
 
79
- function buildClient(): GoogleGenAI {
80
- const apiKey = process.env.GOOGLE_API_KEY;
81
- if (!apiKey) {
82
- throw new Error(
83
- "GOOGLE_API_KEY not set. Get one at https://aistudio.google.com/apikey (AIza…) " +
84
- "or use a Vertex AI Express key (AQ.…) — both work.",
85
- );
84
+ // Well-known path for gcloud Application Default Credentials.
85
+ const ADC_CREDENTIALS_PATH = resolve(
86
+ homedir(),
87
+ ".config/gcloud/application_default_credentials.json",
88
+ );
89
+
90
+ /**
91
+ * Check whether Vertex AI Application Default Credentials are available.
92
+ * Returns true when any of these are configured:
93
+ * - GOOGLE_APPLICATION_CREDENTIALS points to an existing file
94
+ * - gcloud ADC file exists (~/.config/gcloud/application_default_credentials.json)
95
+ * - GOOGLE_CLOUD_PROJECT is set (implies GKE / Cloud Run / Compute Engine default SA)
96
+ */
97
+ function hasVertexADC(): boolean {
98
+ const gac = process.env.GOOGLE_APPLICATION_CREDENTIALS;
99
+ if (gac && existsSync(gac)) return true;
100
+ if (existsSync(ADC_CREDENTIALS_PATH)) return true;
101
+ if (process.env.GOOGLE_CLOUD_PROJECT) return true;
102
+ return false;
103
+ }
104
+
105
+ async function buildClient(
106
+ modelRegistry: ExtensionAPI["modelRegistry"] | undefined,
107
+ ): Promise<GoogleGenAI> {
108
+ // 1. Pi's model registry — the pi-native auth chain
109
+ // (auth.json → env vars → models.json fallback)
110
+ if (modelRegistry) {
111
+ try {
112
+ const apiKey = await modelRegistry.getApiKeyForProvider("google");
113
+ if (apiKey) {
114
+ return new GoogleGenAI({
115
+ apiKey,
116
+ ...(apiKey.startsWith("AQ.") ? { vertexai: true } : {}),
117
+ });
118
+ }
119
+ } catch {
120
+ // ignore — fall through
121
+ }
86
122
  }
87
- // AQ.* express keys authenticate against Vertex; AIza* against Gemini API.
88
- return new GoogleGenAI({
89
- apiKey,
90
- ...(apiKey.startsWith("AQ.") ? { vertexai: true } : {}),
91
- });
123
+
124
+ // 2. Vertex AI ADC (no API key needed — SDK picks up ADC automatically)
125
+ if (hasVertexADC()) {
126
+ return new GoogleGenAI({ vertexai: true });
127
+ }
128
+
129
+ // 3. GOOGLE_API_KEY env var — backward compat for pi-banana users
130
+ // (pi's standard GEMINI_API_KEY is already covered by modelRegistry above)
131
+ if (process.env.GOOGLE_API_KEY) {
132
+ const key = process.env.GOOGLE_API_KEY;
133
+ return new GoogleGenAI({
134
+ apiKey: key,
135
+ ...(key.startsWith("AQ.") ? { vertexai: true } : {}),
136
+ });
137
+ }
138
+
139
+ throw new Error(
140
+ "No Google credentials found. pi-banana looked in this order:\n" +
141
+ " 1. pi model registry (auth.json → env vars → models.json)\n" +
142
+ " Run /login → Google, or set GEMINI_API_KEY, or configure in models.json\n" +
143
+ " 2. Vertex AI ADC (gcloud auth application-default login,\n" +
144
+ " GOOGLE_APPLICATION_CREDENTIALS, or GOOGLE_CLOUD_PROJECT)\n" +
145
+ " 3. GOOGLE_API_KEY env var (AIza… or AQ.…)\n\n" +
146
+ "For Gemini API: get a key at https://aistudio.google.com/apikey\n" +
147
+ "For Vertex AI: gcloud auth application-default login",
148
+ );
92
149
  }
93
150
 
94
151
  function slugify(text: string): string {
@@ -193,7 +250,7 @@ export default function (pi: ExtensionAPI) {
193
250
  "inline in the terminal AND saved to disk so it can be re-used. " +
194
251
  "Pass `referenceImages` (array of paths) to iterate on existing pictures.",
195
252
  promptSnippet:
196
- "Generate or edit images with Google Nano Banana via the GOOGLE_API_KEY env var.",
253
+ "Generate or edit images with Google Nano Banana. Requires Google credentials (via /login, GOOGLE_API_KEY / GEMINI_API_KEY, or Vertex AI ADC).",
197
254
  promptGuidelines: [
198
255
  "Call banana_image when the user asks to create, draw, illustrate, or edit a picture.",
199
256
  "For tweaks like 'make the sky purple', pass the previous file path (or paths) as referenceImages to banana_image.",
@@ -269,7 +326,7 @@ export default function (pi: ExtensionAPI) {
269
326
  );
270
327
  }
271
328
 
272
- const client = buildClient();
329
+ const client = await buildClient(ctx.modelRegistry);
273
330
 
274
331
  // Build content parts — text + (optional) reference image.
275
332
  const parts: Array<{
@@ -385,12 +442,10 @@ export default function (pi: ExtensionAPI) {
385
442
  : `Generated → ${outPath}`;
386
443
 
387
444
  return {
388
- // Flat ImageContent blockpi-coding-agent auto-renders inline
389
- // in Kitty/iTerm2 terminals. Older terminals get the text fallback.
390
- content: [
391
- { type: "text", text: summary },
392
- { type: "image", data: base64, mimeType },
393
- ],
445
+ // Only text in content the base64 image goes into details
446
+ // so it does not bloat conversation history sent to the LLM.
447
+ // renderResult reads the image from details for inline display.
448
+ content: [{ type: "text", text: summary }],
394
449
  details: {
395
450
  prompt: params.prompt,
396
451
  model,
@@ -401,25 +456,29 @@ export default function (pi: ExtensionAPI) {
401
456
  outputPath: outPath,
402
457
  editing,
403
458
  referenceImages: params.referenceImages,
459
+ imageBase64: base64,
404
460
  },
405
461
  };
406
462
  },
407
463
 
408
464
  renderResult(result, _options, theme) {
409
- // existing renderResult code...
465
+ // Image is in details (not content) so base64 does not bloat
466
+ // conversation history sent to the LLM on subsequent turns.
410
467
  const { details, content } = result;
411
468
  const container = new Container();
412
469
 
413
470
  // 1. Summary
414
471
  const summaryPart = content.find((c: any) => c.type === "text");
415
472
  const summaryText = (summaryPart && summaryPart.type === "text") ? summaryPart.text : "";
416
- container.addChild(new Text(theme.fg("success", "✔ " + summaryText), 1, 0));
473
+ if (summaryText) {
474
+ container.addChild(new Text(theme.fg("success", "✔ " + summaryText), 1, 0));
475
+ }
417
476
 
418
- // 2. Image
419
- const imagePart = content.find((c: any) => c.type === "image");
420
- if (imagePart && imagePart.type === "image") {
477
+ // 2. Image from details
478
+ if (details && (details as any).imageBase64) {
479
+ container.addChild(new Spacer(1));
421
480
  container.addChild(
422
- new Image(imagePart.data, imagePart.mimeType, {
481
+ new Image((details as any).imageBase64, (details as any).mimeType ?? "image/png", {
423
482
  ...theme,
424
483
  fallbackColor: (s: string) => theme.fg("muted", s),
425
484
  }, {
@@ -526,7 +585,7 @@ export default function (pi: ExtensionAPI) {
526
585
  return { content: [{ type: "text", text: "Cancelled." }], details: {} };
527
586
  }
528
587
 
529
- const client = buildClient();
588
+ const client = await buildClient(ctx.modelRegistry);
530
589
  const parts: Array<{
531
590
  text?: string;
532
591
  inlineData?: { mimeType: string; data: string };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-banana",
3
- "version": "2.1.2",
3
+ "version": "2.3.0",
4
4
  "description": "Generate, edit, and analyze images in pi using Google Nano Banana (image gen) and Gemini Vision (analysis). Inline terminal preview, reference-image editing, auto-save.",
5
5
  "author": "Francesco Frapporti <effedue@gmail.com>",
6
6
  "license": "MIT",