antigravity-seo-kit 2.0.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.

Potentially problematic release.


This version of antigravity-seo-kit might be problematic. Click here for more details.

Files changed (135) hide show
  1. package/.agent/agent.md +96 -0
  2. package/.agent/skills/seo/SKILL.md +153 -0
  3. package/.agent/skills/seo/references/cwv-thresholds.md +108 -0
  4. package/.agent/skills/seo/references/eeat-framework.md +214 -0
  5. package/.agent/skills/seo/references/local-schema-types.md +230 -0
  6. package/.agent/skills/seo/references/local-seo-signals.md +218 -0
  7. package/.agent/skills/seo/references/maps-api-endpoints.md +160 -0
  8. package/.agent/skills/seo/references/maps-free-apis.md +176 -0
  9. package/.agent/skills/seo/references/maps-gbp-checklist.md +150 -0
  10. package/.agent/skills/seo/references/maps-geo-grid.md +154 -0
  11. package/.agent/skills/seo/references/quality-gates.md +155 -0
  12. package/.agent/skills/seo/references/schema-types.md +118 -0
  13. package/.agent/skills/seo/schema/templates.json +213 -0
  14. package/.agent/skills/seo/scripts/analyze_visual.py +217 -0
  15. package/.agent/skills/seo/scripts/capture_screenshot.py +181 -0
  16. package/.agent/skills/seo/scripts/fetch_page.py +196 -0
  17. package/.agent/skills/seo/scripts/parse_html.py +201 -0
  18. package/.agent/skills/seo-audit/SKILL.md +278 -0
  19. package/.agent/skills/seo-competitor-pages/SKILL.md +212 -0
  20. package/.agent/skills/seo-content/SKILL.md +230 -0
  21. package/.agent/skills/seo-dataforseo/SKILL.md +418 -0
  22. package/.agent/skills/seo-geo/SKILL.md +305 -0
  23. package/.agent/skills/seo-google/SKILL.md +405 -0
  24. package/.agent/skills/seo-google/assets/templates/cwv-audit-report.md +48 -0
  25. package/.agent/skills/seo-google/assets/templates/gsc-performance-report.md +44 -0
  26. package/.agent/skills/seo-google/assets/templates/indexation-status-report.md +43 -0
  27. package/.agent/skills/seo-google/references/auth-setup.md +154 -0
  28. package/.agent/skills/seo-google/references/ga4-data-api.md +184 -0
  29. package/.agent/skills/seo-google/references/indexing-api.md +107 -0
  30. package/.agent/skills/seo-google/references/keyword-planner-api.md +66 -0
  31. package/.agent/skills/seo-google/references/nlp-api.md +55 -0
  32. package/.agent/skills/seo-google/references/pagespeed-crux-api.md +204 -0
  33. package/.agent/skills/seo-google/references/rate-limits-quotas.md +75 -0
  34. package/.agent/skills/seo-google/references/search-console-api.md +156 -0
  35. package/.agent/skills/seo-google/references/supplementary-apis.md +99 -0
  36. package/.agent/skills/seo-google/references/youtube-api.md +49 -0
  37. package/.agent/skills/seo-google/scripts/crux_history.py +321 -0
  38. package/.agent/skills/seo-google/scripts/ga4_report.py +478 -0
  39. package/.agent/skills/seo-google/scripts/google_auth.py +795 -0
  40. package/.agent/skills/seo-google/scripts/google_report.py +2273 -0
  41. package/.agent/skills/seo-google/scripts/gsc_inspect.py +340 -0
  42. package/.agent/skills/seo-google/scripts/gsc_query.py +378 -0
  43. package/.agent/skills/seo-google/scripts/indexing_notify.py +313 -0
  44. package/.agent/skills/seo-google/scripts/keyword_planner.py +297 -0
  45. package/.agent/skills/seo-google/scripts/nlp_analyze.py +309 -0
  46. package/.agent/skills/seo-google/scripts/pagespeed_check.py +649 -0
  47. package/.agent/skills/seo-google/scripts/youtube_search.py +355 -0
  48. package/.agent/skills/seo-hreflang/SKILL.md +192 -0
  49. package/.agent/skills/seo-image-gen/SKILL.md +211 -0
  50. package/.agent/skills/seo-image-gen/references/cost-tracking.md +47 -0
  51. package/.agent/skills/seo-image-gen/references/gemini-models.md +200 -0
  52. package/.agent/skills/seo-image-gen/references/mcp-tools.md +115 -0
  53. package/.agent/skills/seo-image-gen/references/post-processing.md +192 -0
  54. package/.agent/skills/seo-image-gen/references/presets.md +69 -0
  55. package/.agent/skills/seo-image-gen/references/prompt-engineering.md +411 -0
  56. package/.agent/skills/seo-image-gen/references/seo-image-presets.md +137 -0
  57. package/.agent/skills/seo-image-gen/scripts/batch.py +97 -0
  58. package/.agent/skills/seo-image-gen/scripts/cost_tracker.py +191 -0
  59. package/.agent/skills/seo-image-gen/scripts/edit.py +141 -0
  60. package/.agent/skills/seo-image-gen/scripts/generate.py +149 -0
  61. package/.agent/skills/seo-image-gen/scripts/presets.py +153 -0
  62. package/.agent/skills/seo-image-gen/scripts/setup_mcp.py +151 -0
  63. package/.agent/skills/seo-image-gen/scripts/validate_setup.py +133 -0
  64. package/.agent/skills/seo-images/SKILL.md +176 -0
  65. package/.agent/skills/seo-local/SKILL.md +381 -0
  66. package/.agent/skills/seo-maps/SKILL.md +328 -0
  67. package/.agent/skills/seo-page/SKILL.md +86 -0
  68. package/.agent/skills/seo-plan/SKILL.md +118 -0
  69. package/.agent/skills/seo-plan/assets/agency.md +175 -0
  70. package/.agent/skills/seo-plan/assets/ecommerce.md +167 -0
  71. package/.agent/skills/seo-plan/assets/generic.md +144 -0
  72. package/.agent/skills/seo-plan/assets/local-service.md +160 -0
  73. package/.agent/skills/seo-plan/assets/publisher.md +153 -0
  74. package/.agent/skills/seo-plan/assets/saas.md +135 -0
  75. package/.agent/skills/seo-programmatic/SKILL.md +171 -0
  76. package/.agent/skills/seo-schema/SKILL.md +223 -0
  77. package/.agent/skills/seo-sitemap/SKILL.md +180 -0
  78. package/.agent/skills/seo-technical/SKILL.md +211 -0
  79. package/.agent/workflows/seo-audit.md +17 -0
  80. package/.agent/workflows/seo-competitor-pages.md +12 -0
  81. package/.agent/workflows/seo-content.md +14 -0
  82. package/.agent/workflows/seo-geo.md +12 -0
  83. package/.agent/workflows/seo-google.md +12 -0
  84. package/.agent/workflows/seo-hreflang.md +12 -0
  85. package/.agent/workflows/seo-images.md +13 -0
  86. package/.agent/workflows/seo-local.md +12 -0
  87. package/.agent/workflows/seo-maps.md +11 -0
  88. package/.agent/workflows/seo-page.md +13 -0
  89. package/.agent/workflows/seo-plan.md +13 -0
  90. package/.agent/workflows/seo-programmatic.md +12 -0
  91. package/.agent/workflows/seo-schema.md +11 -0
  92. package/.agent/workflows/seo-sitemap.md +9 -0
  93. package/.agent/workflows/seo-technical.md +18 -0
  94. package/LICENSE +88 -0
  95. package/README.md +122 -0
  96. package/bin/cli.js +117 -0
  97. package/docs/ARCHITECTURE.md +218 -0
  98. package/docs/COMMANDS.md +184 -0
  99. package/docs/INSTALLATION.md +100 -0
  100. package/docs/MCP-INTEGRATION.md +153 -0
  101. package/docs/TROUBLESHOOTING.md +151 -0
  102. package/docs/superpowers/plans/2026-03-13-github-audit-fixes.md +511 -0
  103. package/extensions/banana/README.md +95 -0
  104. package/extensions/banana/docs/BANANA-SETUP.md +86 -0
  105. package/extensions/banana/install.sh +170 -0
  106. package/extensions/banana/references/cost-tracking.md +47 -0
  107. package/extensions/banana/references/gemini-models.md +200 -0
  108. package/extensions/banana/references/mcp-tools.md +115 -0
  109. package/extensions/banana/references/post-processing.md +192 -0
  110. package/extensions/banana/references/presets.md +69 -0
  111. package/extensions/banana/references/prompt-engineering.md +411 -0
  112. package/extensions/banana/references/seo-image-presets.md +137 -0
  113. package/extensions/banana/scripts/batch.py +97 -0
  114. package/extensions/banana/scripts/cost_tracker.py +191 -0
  115. package/extensions/banana/scripts/edit.py +141 -0
  116. package/extensions/banana/scripts/generate.py +149 -0
  117. package/extensions/banana/scripts/presets.py +153 -0
  118. package/extensions/banana/scripts/setup_mcp.py +151 -0
  119. package/extensions/banana/scripts/validate_setup.py +133 -0
  120. package/extensions/banana/uninstall.sh +43 -0
  121. package/extensions/dataforseo/README.md +169 -0
  122. package/extensions/dataforseo/docs/DATAFORSEO-SETUP.md +74 -0
  123. package/extensions/dataforseo/field-config.json +280 -0
  124. package/extensions/dataforseo/install.ps1 +110 -0
  125. package/extensions/dataforseo/install.sh +161 -0
  126. package/extensions/dataforseo/uninstall.ps1 +35 -0
  127. package/extensions/dataforseo/uninstall.sh +39 -0
  128. package/lib/api.js +190 -0
  129. package/lib/fingerprint.js +68 -0
  130. package/lib/installer.js +486 -0
  131. package/lib/utils.js +254 -0
  132. package/package.json +40 -0
  133. package/pyproject.toml +11 -0
  134. package/requirements-google.txt +15 -0
  135. package/requirements.txt +11 -0
@@ -0,0 +1,137 @@
1
+ # SEO Image Presets
2
+
3
+ Pre-configured presets for common SEO image use cases. These map to banana's
4
+ preset format (see `references/presets.md` for schema details).
5
+
6
+ ## Preset Templates
7
+
8
+ ### og-default:Standard OG/Social Preview
9
+
10
+ ```json
11
+ {
12
+ "name": "og-default",
13
+ "description": "Clean, professional OG image for social sharing",
14
+ "aspect_ratio": "16:9",
15
+ "resolution": "1K",
16
+ "domain_mode": "Product",
17
+ "style": {
18
+ "colors": ["#FFFFFF", "#F5F5F5"],
19
+ "mood": "Professional, clean, trustworthy",
20
+ "lighting": "Bright, even studio lighting with soft shadows",
21
+ "typography": "Modern sans-serif if text needed"
22
+ },
23
+ "post_processing": "magick output.png -resize 1200x630^ -gravity center -extent 1200x630 output-og.webp"
24
+ }
25
+ ```
26
+
27
+ ### blog-hero:Widescreen Blog Hero Image
28
+
29
+ ```json
30
+ {
31
+ "name": "blog-hero",
32
+ "description": "Dramatic widescreen hero for blog posts",
33
+ "aspect_ratio": "16:9",
34
+ "resolution": "2K",
35
+ "domain_mode": "Cinema",
36
+ "style": {
37
+ "colors": ["contextual"],
38
+ "mood": "Dramatic, atmospheric, editorial",
39
+ "lighting": "Golden hour or moody blue hour, directional",
40
+ "typography": "None:image only"
41
+ },
42
+ "post_processing": "magick output.png -quality 85 output-hero.webp"
43
+ }
44
+ ```
45
+
46
+ ### product-white:E-commerce Product Shot
47
+
48
+ ```json
49
+ {
50
+ "name": "product-white",
51
+ "description": "Clean white background product photography",
52
+ "aspect_ratio": "4:3",
53
+ "resolution": "2K",
54
+ "domain_mode": "Product",
55
+ "style": {
56
+ "colors": ["#FFFFFF"],
57
+ "mood": "Clean, professional, catalog-ready",
58
+ "lighting": "360-degree soft studio lighting, minimal shadows",
59
+ "typography": "None"
60
+ },
61
+ "prompt_suffix": "Studio product photography, clean white background, professional catalog shot, high resolution",
62
+ "post_processing": "magick output.png -fuzz 5% -transparent white output-transparent.png"
63
+ }
64
+ ```
65
+
66
+ ### social-square:Social Media Square
67
+
68
+ ```json
69
+ {
70
+ "name": "social-square",
71
+ "description": "1:1 square image for social media platforms",
72
+ "aspect_ratio": "1:1",
73
+ "resolution": "1K",
74
+ "domain_mode": "UI/Web",
75
+ "style": {
76
+ "colors": ["brand-contextual"],
77
+ "mood": "Engaging, scroll-stopping, platform-native",
78
+ "lighting": "Bright, even, high-contrast",
79
+ "typography": "Bold sans-serif if text needed, under 25 characters"
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### infographic-vertical:Data-Heavy Infographic
85
+
86
+ ```json
87
+ {
88
+ "name": "infographic-vertical",
89
+ "description": "Tall vertical infographic for data visualization",
90
+ "aspect_ratio": "2:3",
91
+ "resolution": "4K",
92
+ "domain_mode": "Infographic",
93
+ "style": {
94
+ "colors": ["brand-contextual", "data-visualization palette"],
95
+ "mood": "Informative, structured, authoritative",
96
+ "lighting": "Flat, even, no dramatic shadows",
97
+ "typography": "Clear hierarchy:headline, subheads, body, captions"
98
+ },
99
+ "notes": "Use thinking: high for better text rendering accuracy"
100
+ }
101
+ ```
102
+
103
+ ### favicon-mark:Favicon / App Icon
104
+
105
+ ```json
106
+ {
107
+ "name": "favicon-mark",
108
+ "description": "Minimal iconic mark for favicon or app icon",
109
+ "aspect_ratio": "1:1",
110
+ "resolution": "512",
111
+ "domain_mode": "Logo",
112
+ "style": {
113
+ "colors": ["2-3 brand colors max"],
114
+ "mood": "Minimal, recognizable, scalable",
115
+ "lighting": "Flat, no shadows",
116
+ "typography": "Single letter or symbol only"
117
+ },
118
+ "post_processing": "magick output.png -resize 32x32 favicon.ico && magick output.png -resize 180x180 apple-touch-icon.png"
119
+ }
120
+ ```
121
+
122
+ ## Creating Custom Presets
123
+
124
+ Users can create their own presets:
125
+ ```bash
126
+ python3 .agent/skills/seo-image-gen/scripts/presets.py create my-brand
127
+ ```
128
+
129
+ This creates `~/.banana/presets/my-brand.json` with the full schema.
130
+ Custom presets override SEO defaults when specified.
131
+
132
+ ## Preset Selection Logic
133
+
134
+ 1. If user specifies a use case command (og, hero, product), load the matching preset
135
+ 2. If user mentions a brand preset name, load from `~/.banana/presets/`
136
+ 3. Brand presets override SEO presets for colors, mood, and typography
137
+ 4. SEO presets always provide aspect ratio and resolution defaults
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """Claude Banana - CSV Batch Workflow
3
+
4
+ Parse a CSV file of image generation requests and output a structured plan.
5
+ Claude then executes each row via MCP.
6
+
7
+ Usage:
8
+ batch.py --csv path/to/file.csv
9
+
10
+ CSV columns:
11
+ prompt (required), ratio, resolution, model, preset (all optional)
12
+
13
+ Example CSV:
14
+ prompt,ratio,resolution
15
+ "coffee shop hero image",16:9,2K
16
+ "team photo placeholder",1:1,1K
17
+ "product shot on marble",4:3,2K
18
+ """
19
+
20
+ import argparse
21
+ import csv
22
+ import json
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ # Inline pricing for estimates
27
+ PRICING = {
28
+ "gemini-3.1-flash-image-preview": {"512": 0.020, "1K": 0.039, "2K": 0.078, "4K": 0.156},
29
+ "gemini-2.5-flash-image": {"512": 0.020, "1K": 0.039},
30
+ }
31
+ DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
32
+ DEFAULT_RESOLUTION = "1K"
33
+ DEFAULT_RATIO = "1:1"
34
+
35
+
36
+ def estimate_cost(model, resolution):
37
+ """Estimate cost for a single image."""
38
+ model_pricing = PRICING.get(model, PRICING[DEFAULT_MODEL])
39
+ return model_pricing.get(resolution, model_pricing.get("1K", 0.039))
40
+
41
+
42
+ def main():
43
+ parser = argparse.ArgumentParser(description="Parse CSV batch and output generation plan")
44
+ parser.add_argument("--csv", required=True, help="Path to CSV file")
45
+ args = parser.parse_args()
46
+
47
+ csv_path = Path(args.csv).resolve()
48
+ if not csv_path.exists():
49
+ print(json.dumps({"error": True, "message": f"CSV not found: {csv_path}"}))
50
+ sys.exit(1)
51
+
52
+ rows = []
53
+ errors = []
54
+
55
+ try:
56
+ with open(csv_path, "r", newline="") as f:
57
+ reader = csv.DictReader(f)
58
+ if not reader.fieldnames or "prompt" not in reader.fieldnames:
59
+ print(json.dumps({"error": True, "message": "CSV must have a 'prompt' column header"}))
60
+ sys.exit(1)
61
+ for i, row in enumerate(reader, start=2): # Line 2+ (1 is header)
62
+ prompt = row.get("prompt", "").strip()
63
+ if not prompt:
64
+ errors.append(f"Row {i}: missing prompt")
65
+ continue
66
+
67
+ rows.append({
68
+ "row": i,
69
+ "prompt": prompt,
70
+ "ratio": row.get("ratio", "").strip() or DEFAULT_RATIO,
71
+ "resolution": row.get("resolution", "").strip() or DEFAULT_RESOLUTION,
72
+ "model": row.get("model", "").strip() or DEFAULT_MODEL,
73
+ "preset": row.get("preset", "").strip() or None,
74
+ })
75
+ except (csv.Error, UnicodeDecodeError) as e:
76
+ print(json.dumps({"error": True, "message": f"Failed to parse CSV: {e}"}))
77
+ sys.exit(1)
78
+
79
+ if errors:
80
+ print("Validation errors:")
81
+ for e in errors:
82
+ print(f" - {e}")
83
+ if not rows:
84
+ sys.exit(1)
85
+ print()
86
+
87
+ # Cost estimate
88
+ total_cost = sum(estimate_cost(r["model"], r["resolution"]) for r in rows)
89
+
90
+ # Output structured JSON for Claude to consume
91
+ print(json.dumps({"rows": rows, "total_count": len(rows),
92
+ "estimated_cost": round(total_cost, 3),
93
+ "errors": errors}, indent=2))
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """Claude Banana - Cost Tracker
3
+
4
+ Track image generation costs, view summaries, and estimate batch costs.
5
+
6
+ Usage:
7
+ cost_tracker.py log --model MODEL --resolution RES --prompt "summary"
8
+ cost_tracker.py summary
9
+ cost_tracker.py today
10
+ cost_tracker.py estimate --model MODEL --resolution RES --count N
11
+ cost_tracker.py reset --confirm
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+
20
+ LEDGER_PATH = Path.home() / ".banana" / "costs.json"
21
+
22
+ # Cost per image in USD (approximate, based on ~1,290 output tokens)
23
+ PRICING = {
24
+ "gemini-3.1-flash-image-preview": {
25
+ "512": 0.020,
26
+ "1K": 0.039,
27
+ "2K": 0.078,
28
+ "4K": 0.156,
29
+ },
30
+ "gemini-2.5-flash-image": {
31
+ "512": 0.020,
32
+ "1K": 0.039,
33
+ },
34
+ }
35
+
36
+ # Batch API gets 50% discount
37
+ BATCH_DISCOUNT = 0.5
38
+
39
+
40
+ def _load_ledger():
41
+ """Load the cost ledger from disk."""
42
+ if not LEDGER_PATH.exists():
43
+ return {"total_cost": 0.0, "total_images": 0, "entries": [], "daily": {}}
44
+ with open(LEDGER_PATH, "r") as f:
45
+ return json.load(f)
46
+
47
+
48
+ def _save_ledger(ledger):
49
+ """Save the cost ledger to disk."""
50
+ LEDGER_PATH.parent.mkdir(parents=True, exist_ok=True)
51
+ with open(LEDGER_PATH, "w") as f:
52
+ json.dump(ledger, f, indent=2)
53
+
54
+
55
+ def _lookup_cost(model, resolution, batch=False):
56
+ """Look up cost for a model+resolution combination."""
57
+ model_pricing = PRICING.get(model)
58
+ if not model_pricing:
59
+ # Try partial match
60
+ for key in PRICING:
61
+ if key in model or model in key:
62
+ model_pricing = PRICING[key]
63
+ break
64
+ if not model_pricing:
65
+ print(f"Warning: Unknown model '{model}', using 3.1 Flash pricing", file=sys.stderr)
66
+ model_pricing = PRICING["gemini-3.1-flash-image-preview"]
67
+
68
+ valid_resolutions = {"512", "1K", "2K", "4K"}
69
+ if resolution not in valid_resolutions:
70
+ print(f"Warning: Unknown resolution '{resolution}', using 1K pricing", file=sys.stderr)
71
+ cost = model_pricing.get(resolution, model_pricing.get("1K", 0.039))
72
+ if batch:
73
+ cost *= BATCH_DISCOUNT
74
+ return cost
75
+
76
+
77
+ def cmd_log(args):
78
+ """Log a generation to the ledger."""
79
+ ledger = _load_ledger()
80
+ cost = _lookup_cost(args.model, args.resolution, getattr(args, "batch", False))
81
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
82
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
83
+
84
+ entry = {
85
+ "ts": now,
86
+ "model": args.model,
87
+ "res": args.resolution,
88
+ "cost": cost,
89
+ "prompt": args.prompt[:100],
90
+ }
91
+
92
+ ledger["entries"].append(entry)
93
+ ledger["total_cost"] = round(ledger["total_cost"] + cost, 4)
94
+ ledger["total_images"] += 1
95
+
96
+ if today not in ledger["daily"]:
97
+ ledger["daily"][today] = {"count": 0, "cost": 0.0}
98
+ ledger["daily"][today]["count"] += 1
99
+ ledger["daily"][today]["cost"] = round(ledger["daily"][today]["cost"] + cost, 4)
100
+
101
+ _save_ledger(ledger)
102
+ print(json.dumps({"logged": True, "cost": cost, "total_cost": ledger["total_cost"],
103
+ "total_images": ledger["total_images"]}))
104
+
105
+
106
+ def cmd_summary(args):
107
+ """Show cost summary."""
108
+ ledger = _load_ledger()
109
+ print(f"Total images: {ledger['total_images']}")
110
+ print(f"Total cost: ${ledger['total_cost']:.3f}")
111
+ print()
112
+
113
+ daily = ledger.get("daily", {})
114
+ if daily:
115
+ # Show last 7 days
116
+ sorted_days = sorted(daily.keys(), reverse=True)[:7]
117
+ print("Last 7 days:")
118
+ for day in sorted_days:
119
+ d = daily[day]
120
+ print(f" {day}: {d['count']} images, ${d['cost']:.3f}")
121
+ else:
122
+ print("No usage recorded yet.")
123
+
124
+
125
+ def cmd_today(args):
126
+ """Show today's usage."""
127
+ ledger = _load_ledger()
128
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
129
+ daily = ledger.get("daily", {}).get(today, {"count": 0, "cost": 0.0})
130
+ print(f"Today ({today}): {daily['count']} images, ${daily['cost']:.3f}")
131
+
132
+
133
+ def cmd_estimate(args):
134
+ """Estimate cost for a batch."""
135
+ cost_per = _lookup_cost(args.model, args.resolution, getattr(args, "batch", False))
136
+ total = round(cost_per * args.count, 3)
137
+ print(f"Model: {args.model}")
138
+ print(f"Resolution: {args.resolution}")
139
+ print(f"Count: {args.count}")
140
+ print(f"Cost/image: ${cost_per:.3f}")
141
+ print(f"Total est: ${total:.3f}")
142
+ if not getattr(args, "batch", False):
143
+ batch_total = round(cost_per * BATCH_DISCOUNT * args.count, 3)
144
+ print(f"Batch est: ${batch_total:.3f} (50% discount)")
145
+
146
+
147
+ def cmd_reset(args):
148
+ """Reset the ledger."""
149
+ if not args.confirm:
150
+ print("Error: Pass --confirm to reset the cost ledger.", file=sys.stderr)
151
+ sys.exit(1)
152
+ _save_ledger({"total_cost": 0.0, "total_images": 0, "entries": [], "daily": {}})
153
+ print("Cost ledger reset.")
154
+
155
+
156
+ def main():
157
+ parser = argparse.ArgumentParser(description="Claude Banana Cost Tracker")
158
+ sub = parser.add_subparsers(dest="command", required=True)
159
+
160
+ # log
161
+ p_log = sub.add_parser("log", help="Log a generation")
162
+ p_log.add_argument("--model", required=True, help="Model ID")
163
+ p_log.add_argument("--resolution", required=True, help="Resolution (512, 1K, 2K, 4K)")
164
+ p_log.add_argument("--prompt", required=True, help="Brief prompt description")
165
+ p_log.add_argument("--batch", action="store_true", help="Batch API (50%% discount)")
166
+
167
+ # summary
168
+ sub.add_parser("summary", help="Show cost summary")
169
+
170
+ # today
171
+ sub.add_parser("today", help="Show today's usage")
172
+
173
+ # estimate
174
+ p_est = sub.add_parser("estimate", help="Estimate batch cost")
175
+ p_est.add_argument("--model", required=True, help="Model ID")
176
+ p_est.add_argument("--resolution", required=True, help="Resolution (512, 1K, 2K, 4K)")
177
+ p_est.add_argument("--count", required=True, type=int, help="Number of images")
178
+ p_est.add_argument("--batch", action="store_true", help="Use batch pricing (50%% discount)")
179
+
180
+ # reset
181
+ p_reset = sub.add_parser("reset", help="Reset cost ledger")
182
+ p_reset.add_argument("--confirm", action="store_true", help="Confirm reset")
183
+
184
+ args = parser.parse_args()
185
+ cmds = {"log": cmd_log, "summary": cmd_summary, "today": cmd_today,
186
+ "estimate": cmd_estimate, "reset": cmd_reset}
187
+ cmds[args.command](args)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ """Claude Banana - Direct API Fallback: Image Editing
3
+
4
+ Edit images via Gemini REST API when MCP is unavailable.
5
+ Uses only Python stdlib (no pip dependencies).
6
+
7
+ Usage:
8
+ edit.py --image path/to/image.png --prompt "remove the background"
9
+ [--model MODEL] [--api-key KEY]
10
+ """
11
+
12
+ import argparse
13
+ import base64
14
+ import json
15
+ import os
16
+ import sys
17
+ import urllib.request
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+ DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
22
+ OUTPUT_DIR = Path.home() / "Documents" / "nanobanana_generated"
23
+ API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
24
+
25
+
26
+ def edit_image(image_path, prompt, model, api_key):
27
+ """Call Gemini API to edit an image."""
28
+ image_path = Path(image_path).resolve()
29
+ if not image_path.exists():
30
+ print(json.dumps({"error": True, "message": f"Image not found: {image_path}"}))
31
+ sys.exit(1)
32
+
33
+ # Read and encode image
34
+ with open(image_path, "rb") as f:
35
+ image_b64 = base64.b64encode(f.read()).decode("utf-8")
36
+
37
+ # Determine MIME type
38
+ suffix = image_path.suffix.lower()
39
+ mime_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
40
+ ".webp": "image/webp", ".gif": "image/gif"}
41
+ mime_type = mime_types.get(suffix, "image/png")
42
+
43
+ url = f"{API_BASE}/{model}:generateContent?key={api_key}"
44
+
45
+ body = {
46
+ "contents": [
47
+ {
48
+ "parts": [
49
+ {"text": prompt},
50
+ {"inlineData": {"mimeType": mime_type, "data": image_b64}},
51
+ ]
52
+ }
53
+ ],
54
+ "generationConfig": {
55
+ "responseModalities": ["TEXT", "IMAGE"],
56
+ },
57
+ }
58
+
59
+ data = json.dumps(body).encode("utf-8")
60
+ req = urllib.request.Request(
61
+ url,
62
+ data=data,
63
+ headers={"Content-Type": "application/json"},
64
+ method="POST",
65
+ )
66
+
67
+ try:
68
+ with urllib.request.urlopen(req, timeout=120) as resp:
69
+ result = json.loads(resp.read().decode("utf-8"))
70
+ except urllib.error.HTTPError as e:
71
+ error_body = e.read().decode("utf-8") if e.fp else ""
72
+ print(json.dumps({"error": True, "status": e.code, "message": error_body}))
73
+ sys.exit(1)
74
+ except urllib.error.URLError as e:
75
+ print(json.dumps({"error": True, "message": str(e.reason)}))
76
+ sys.exit(1)
77
+
78
+ # Extract image from response
79
+ candidates = result.get("candidates", [])
80
+ if not candidates:
81
+ finish_reason = result.get("promptFeedback", {}).get("blockReason", "UNKNOWN")
82
+ print(json.dumps({"error": True, "message": f"No candidates returned. Reason: {finish_reason}"}))
83
+ sys.exit(1)
84
+
85
+ parts = candidates[0].get("content", {}).get("parts", [])
86
+ image_data = None
87
+ text_response = ""
88
+
89
+ for part in parts:
90
+ if "inlineData" in part:
91
+ image_data = part["inlineData"]["data"]
92
+ elif "text" in part:
93
+ text_response = part["text"]
94
+
95
+ if not image_data:
96
+ finish_reason = candidates[0].get("finishReason", "UNKNOWN")
97
+ print(json.dumps({"error": True, "message": f"No image in response. finishReason: {finish_reason}"}))
98
+ sys.exit(1)
99
+
100
+ # Save image
101
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
102
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
103
+ filename = f"banana_edit_{timestamp}.png"
104
+ output_path = (OUTPUT_DIR / filename).resolve()
105
+
106
+ with open(output_path, "wb") as f:
107
+ f.write(base64.b64decode(image_data))
108
+
109
+ return {
110
+ "path": str(output_path),
111
+ "model": model,
112
+ "source": str(image_path),
113
+ "text": text_response,
114
+ }
115
+
116
+
117
+ def main():
118
+ parser = argparse.ArgumentParser(description="Edit images via Gemini REST API")
119
+ parser.add_argument("--image", required=True, help="Path to input image")
120
+ parser.add_argument("--prompt", required=True, help="Edit instruction")
121
+ parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model ID (default: {DEFAULT_MODEL})")
122
+ parser.add_argument("--api-key", default=None, help="Google AI API key (or set GOOGLE_AI_API_KEY env)")
123
+
124
+ args = parser.parse_args()
125
+
126
+ api_key = args.api_key or os.environ.get("GOOGLE_AI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
127
+ if not api_key:
128
+ print(json.dumps({"error": True, "message": "No API key. Set GOOGLE_AI_API_KEY env or pass --api-key"}))
129
+ sys.exit(1)
130
+
131
+ result = edit_image(
132
+ image_path=args.image,
133
+ prompt=args.prompt,
134
+ model=args.model,
135
+ api_key=api_key,
136
+ )
137
+ print(json.dumps(result, indent=2))
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()