pi-banana 2.0.8 → 2.1.2

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/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  ![pi-banana banner](https://fcskjxapefiqdclrvbtw.supabase.co/storage/v1/object/public/assets/pi-packages/pi-banana-banner.jpg)
4
4
 
5
- Generate and edit images directly inside [pi](https://github.com/badlogic/pi-mono) using Google's **Nano Banana 2** (`gemini-3.1-flash-image-preview`) and **Nano Banana Pro** (`gemini-3-pro-image-preview`).
5
+ Generate, edit, and analyze images directly inside [pi](https://github.com/badlogic/pi-mono) using Google's **Nano Banana 2** (`gemini-3.1-flash-image-preview`), **Nano Banana Pro** (`gemini-3-pro-image-preview`), and **Gemini Vision**.
6
6
 
7
- - **Inline preview** in Kitty / iTerm2 / WezTerm image shows up right under the tool call, no copy-pasting paths.
8
- - **Reference-image editing** — pass `referenceImage: "./logo.png"` and the model edits that picture instead of starting from scratch.
7
+ - **Image Generation & Editing** Create images from scratch or pass `referenceImages: ["./logo.png"]` to edit existing pictures.
8
+ - **Multimodal Vision** — Analyze, describe, or extract text from images using `gemini-3.1-flash-lite` (fast) or `gemini-3.1-pro-preview` (deep reasoning).
9
+ - **Inline preview** in Kitty / iTerm2 / WezTerm — generated images show up right under the tool call.
9
10
  - **Auto-save** to `./generated/` so the agent and you both have a real file to refer back to.
10
11
  - **One env var** — `GOOGLE_API_KEY`. Works with both AI Studio (`AIza…`) and Vertex AI Express (`AQ.…`) keys.
11
- - **Backend-aware** — Vertex publishes the `-preview` model id, AI Studio sometimes publishes the GA name; the extension transparently retries the other variant on 404 so it just works.
12
12
 
13
13
  ## Install
14
14
 
@@ -41,11 +41,15 @@ Just ask pi for what you want:
41
41
  >
42
42
  > *"Edit `./generated/logo-20260508.png` — make the background transparent"*
43
43
  >
44
- > *"Draw a pixel-art banana with sunglasses, 1:1, save it as `./assets/mascot.png`"*
44
+ > *"What color is the car in `./assets/photo.jpg`?"*
45
+ >
46
+ > *"Extract the text from these three receipts."*
47
+
48
+ The model calls `banana_image` or `banana_vision` automatically.
45
49
 
46
- The model calls `generate_image` automatically. The PNG is shown inline and written to disk.
50
+ ## Tools
47
51
 
48
- ## Tool: `banana_image`
52
+ ### 1. `banana_image`
49
53
 
50
54
  > Renamed from `generate_image` in v2.0.1 to avoid colliding with `@benvargas/pi-antigravity-image-gen`. Both extensions can now coexist in the same pi install.
51
55
 
@@ -55,9 +59,17 @@ The model calls `generate_image` automatically. The PNG is shown inline and writ
55
59
  | `aspectRatio` | enum | `1:1` | `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9` |
56
60
  | `imageSize` | enum | `1K` | `1K`, `2K`, `4K`. `4K` requires `quality=high`. |
57
61
  | `quality` | enum | `fast` | `fast` = Nano Banana 2 (~3–10 s, cheapest). `high` = Nano Banana Pro (slower, top quality). |
58
- | `referenceImage` | string | — | Optional path to a PNG/JPEG/WebP/GIF to edit instead of generating from scratch. |
62
+ | `referenceImages` | array | — | Optional array of paths (PNG/JPEG/WebP/GIF) to edit or use for composition instead of generating from scratch. |
59
63
  | `outputPath` | string | `./generated/<slug>-<ts>.png` | Optional output path. Parent dirs are created. |
60
64
 
65
+ ### 2. `banana_vision`
66
+
67
+ | Param | Type | Default | Description |
68
+ |---|---|---|---|
69
+ | `prompt` | string | — | Required. What you want to know about the image(s) (e.g. 'Describe this image', 'Extract the text'). |
70
+ | `imagePaths` | array | — | Required. Array of paths to existing images to analyze. |
71
+ | `quality` | enum | `fast` | `fast` = gemini-3.1-flash-lite (fast/cheap). `high` = gemini-3.1-pro-preview (slower, deep reasoning). |
72
+
61
73
  ## Configuration
62
74
 
63
75
  Two env vars adjust defaults without touching tool parameters:
@@ -406,6 +406,7 @@ export default function (pi: ExtensionAPI) {
406
406
  },
407
407
 
408
408
  renderResult(result, _options, theme) {
409
+ // existing renderResult code...
409
410
  const { details, content } = result;
410
411
  const container = new Container();
411
412
 
@@ -437,6 +438,7 @@ export default function (pi: ExtensionAPI) {
437
438
  aspectRatio,
438
439
  imageSize,
439
440
  outputPath,
441
+ referenceImages,
440
442
  } = details as any;
441
443
 
442
444
  container.addChild(new Spacer(1));
@@ -466,6 +468,163 @@ export default function (pi: ExtensionAPI) {
466
468
  if (aspectRatio) addSetting("Aspect", aspectRatio);
467
469
  if (imageSize) addSetting("Size", imageSize);
468
470
  if (outputPath) addSetting("Path", outputPath);
471
+ if (referenceImages && referenceImages.length > 0) {
472
+ addSetting("Refs", Array.isArray(referenceImages) ? referenceImages.join(", ") : referenceImages);
473
+ }
474
+
475
+ settingsBox.addChild(settingsContainer);
476
+ container.addChild(settingsBox);
477
+
478
+ return container;
479
+ },
480
+ });
481
+
482
+ pi.registerTool({
483
+ name: "banana_vision",
484
+ label: "Banana Vision",
485
+ description:
486
+ "Analyze, describe, or extract text from images using Google Gemini Vision " +
487
+ "(gemini-3.1-flash-lite for fast analysis or gemini-3.1-pro-preview for deep inspection).",
488
+ promptSnippet:
489
+ "Analyze and describe images using Google Gemini Vision.",
490
+ promptGuidelines: [
491
+ "Call banana_vision when the user asks you to look at, describe, analyze, or extract text from an existing image file.",
492
+ "Default quality 'fast' (flash-lite) is good for most simple descriptions and text extraction; use 'high' (pro) for complex reasoning.",
493
+ ],
494
+ parameters: Type.Object({
495
+ prompt: Type.String({
496
+ description: "What you want to know about the image(s) (e.g. 'Describe this image', 'Extract the text').",
497
+ }),
498
+ imagePaths: Type.Array(Type.String(), {
499
+ description:
500
+ "Provide an ARRAY OF STRINGS containing the paths to existing images " +
501
+ "to analyze (e.g., ['image1.png']). Relative paths resolve to current working directory.",
502
+ }),
503
+ quality: Type.Optional(
504
+ StringEnum(QUALITY, {
505
+ description:
506
+ "'fast' = gemini-3.1-flash-lite (default, fast/cheap). 'high' = gemini-3.1-pro-preview (slower, deep reasoning).",
507
+ default: DEFAULT_QUALITY,
508
+ }),
509
+ ),
510
+ }),
511
+
512
+ prepareArguments(args: any) {
513
+ if (args.imagePath !== undefined) {
514
+ args.imagePaths = Array.isArray(args.imagePath) ? args.imagePath : [args.imagePath];
515
+ delete args.imagePath;
516
+ }
517
+ return args;
518
+ },
519
+
520
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
521
+ const quality = params.quality ?? DEFAULT_QUALITY;
522
+ const model = quality === "fast" ? "gemini-3.1-flash-lite" : "gemini-3.1-pro-preview";
523
+ const cwd = ctx.cwd;
524
+
525
+ if (signal?.aborted) {
526
+ return { content: [{ type: "text", text: "Cancelled." }], details: {} };
527
+ }
528
+
529
+ const client = buildClient();
530
+ const parts: Array<{
531
+ text?: string;
532
+ inlineData?: { mimeType: string; data: string };
533
+ }> = [];
534
+
535
+ if (!params.imagePaths || params.imagePaths.length === 0) {
536
+ throw new Error("At least one image path must be provided in imagePaths.");
537
+ }
538
+
539
+ for (const imgPath of params.imagePaths) {
540
+ const ref = await loadReferenceImage(cwd, imgPath);
541
+ parts.push({ inlineData: ref });
542
+ }
543
+ parts.push({ text: params.prompt });
544
+
545
+ onUpdate?.({
546
+ content: [
547
+ {
548
+ type: "text",
549
+ text: `👁️ Analyzing image(s) with ${model}…`,
550
+ },
551
+ ],
552
+ details: { model, quality, imagePaths: params.imagePaths },
553
+ });
554
+
555
+ let response;
556
+ try {
557
+ response = await client.models.generateContent({
558
+ model,
559
+ contents: [{ role: "user", parts }],
560
+ config: { abortSignal: signal },
561
+ });
562
+ } catch (err: any) {
563
+ throw new Error(`Google vision API error: ${err?.message ?? String(err)}`);
564
+ }
565
+
566
+ const textOut = response.text || "";
567
+ if (!textOut) {
568
+ const candidate = response.candidates?.[0];
569
+ const reason = candidate?.finishReason ?? "unknown";
570
+ const safety = candidate?.safetyRatings
571
+ ?.filter((r: any) => r.blocked || r.probability === "HIGH")
572
+ .map((r: any) => r.category)
573
+ .join(", ");
574
+ throw new Error(
575
+ `No analysis returned (finishReason=${reason}` +
576
+ (safety ? `, blocked=${safety}` : "") +
577
+ `)`,
578
+ );
579
+ }
580
+
581
+ return {
582
+ content: [{ type: "text", text: textOut }],
583
+ details: {
584
+ prompt: params.prompt,
585
+ model,
586
+ quality,
587
+ imagePaths: params.imagePaths,
588
+ },
589
+ };
590
+ },
591
+
592
+ renderResult(result, _options, theme) {
593
+ const { details, content } = result;
594
+ const container = new Container();
595
+
596
+ const textPart = content.find((c: any) => c.type === "text");
597
+ const responseText = textPart && textPart.type === "text" ? textPart.text : "";
598
+
599
+ // Show the output text with a nice quote-like block or just plain text
600
+ container.addChild(new Text(theme.fg("text", responseText), 0, 0));
601
+
602
+ if (!details) return container;
603
+ const { prompt, model, quality, imagePaths } = details as any;
604
+
605
+ container.addChild(new Spacer(1));
606
+ const settingsBox = new Box(1, 1, (s) => theme.bg("customMessageBg", s));
607
+ const settingsContainer = new Container();
608
+
609
+ settingsContainer.addChild(
610
+ new Text(theme.fg("accent", theme.bold("👁️ VISION SETTINGS")), 0, 0)
611
+ );
612
+ settingsContainer.addChild(new Spacer(1));
613
+
614
+ const addSetting = (label: string, value: string) => {
615
+ settingsContainer.addChild(
616
+ new Text(
617
+ theme.fg("muted", label.padEnd(10)) + theme.fg("text", String(value)),
618
+ 0,
619
+ 0
620
+ )
621
+ );
622
+ };
623
+
624
+ if (prompt) addSetting("Prompt", prompt);
625
+ if (model) addSetting("Model", model);
626
+ if (quality) addSetting("Quality", quality);
627
+ if (imagePaths) addSetting("Images", Array.isArray(imagePaths) ? imagePaths.join(", ") : imagePaths);
469
628
 
470
629
  settingsBox.addChild(settingsContainer);
471
630
  container.addChild(settingsBox);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-banana",
3
- "version": "2.0.8",
4
- "description": "Generate and edit images in pi using Google Nano Banana 2 (gemini-3.1-flash-image) and Nano Banana Pro. Inline terminal preview, reference-image editing, auto-save.",
3
+ "version": "2.1.2",
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",
7
7
  "type": "module",