cue-ai 0.9.2 → 0.9.3

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 (278) hide show
  1. package/CHANGELOG.md +4 -3
  2. package/README.md +148 -170
  3. package/bin/cue-learnings +30 -4
  4. package/bin/cue-review-progress +0 -0
  5. package/bin/cue-review-watch +0 -0
  6. package/dist/cue.js +4328 -3108
  7. package/package.json +1 -1
  8. package/plugins/cue/commands/cue-switch.md +1 -1
  9. package/plugins/cue/commands/cue.md +1 -1
  10. package/profiles/backend/profile.yaml +4 -0
  11. package/profiles/browser/profile.yaml +4 -0
  12. package/profiles/career/profile.yaml +2 -13
  13. package/profiles/commerce/profile.yaml +0 -2
  14. package/profiles/coolify/profile.yaml +0 -1
  15. package/profiles/core/profile.yaml +78 -11
  16. package/profiles/dash-merge-test/profile.yaml +6 -1
  17. package/profiles/designer/profile.yaml +9 -1
  18. package/profiles/dropshipping/profile.yaml +69 -0
  19. package/profiles/frontend/profile.yaml +4 -0
  20. package/profiles/google-ads/profile.yaml +34 -0
  21. package/profiles/google-analytics/profile.yaml +34 -0
  22. package/profiles/google-drive/profile.yaml +34 -0
  23. package/profiles/gstack/profile.yaml +117 -29
  24. package/profiles/marketing/profile.yaml +0 -1
  25. package/profiles/media/README.md +70 -0
  26. package/profiles/media/profile.yaml +104 -0
  27. package/profiles/nano-banana/profile.yaml +52 -0
  28. package/profiles/ops/profile.yaml +1 -2
  29. package/profiles/secops/profile.yaml +3 -0
  30. package/profiles/skill-writer/profile.yaml +15 -0
  31. package/profiles/video/profile.yaml +3 -0
  32. package/profiles/web-frontend-base/profile.yaml +6 -0
  33. package/profiles/webshop/profile.yaml +0 -1
  34. package/profiles/webshop-google/profile.yaml +1 -0
  35. package/profiles/x-growth-bot/profile.yaml +2 -0
  36. package/resources/icons/generate-icons.py +2 -128
  37. package/resources/mcps/configs/claude.sanitized.json +88 -20
  38. package/resources/mcps/configs/claude_runtime.sanitized.json +40 -1
  39. package/resources/mcps/configs/codex.sanitized.json +29 -0
  40. package/resources/skills/skills/career/job-hunter/LICENSE +21 -0
  41. package/resources/skills/skills/career/job-hunter/README.md +323 -0
  42. package/resources/skills/skills/career/job-hunter/SKILL.md +91 -0
  43. package/resources/skills/skills/career/job-hunter/agents/README.md +96 -0
  44. package/resources/skills/skills/career/job-hunter/agents/apply-assessment-prep.md +195 -0
  45. package/resources/skills/skills/career/job-hunter/agents/apply-ats-scan.md +155 -0
  46. package/resources/skills/skills/career/job-hunter/agents/apply-bias-audit.md +224 -0
  47. package/resources/skills/skills/career/job-hunter/agents/apply-cover-letter.md +69 -0
  48. package/resources/skills/skills/career/job-hunter/agents/apply-decode-jd.md +117 -0
  49. package/resources/skills/skills/career/job-hunter/agents/apply-fit-score.md +183 -0
  50. package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-audit.md +74 -0
  51. package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-scrape.md +255 -0
  52. package/resources/skills/skills/career/job-hunter/agents/apply-portfolio-brief.md +123 -0
  53. package/resources/skills/skills/career/job-hunter/agents/apply-reality-check.md +164 -0
  54. package/resources/skills/skills/career/job-hunter/agents/apply-reference-prep.md +150 -0
  55. package/resources/skills/skills/career/job-hunter/agents/apply-rejection-analysis.md +172 -0
  56. package/resources/skills/skills/career/job-hunter/agents/apply-resume.md +70 -0
  57. package/resources/skills/skills/career/job-hunter/agents/apply-skills-gap-filler.md +109 -0
  58. package/resources/skills/skills/career/job-hunter/agents/career-internal.md +94 -0
  59. package/resources/skills/skills/career/job-hunter/agents/career-linkedin-content.md +173 -0
  60. package/resources/skills/skills/career/job-hunter/agents/career-linkedin-scanner.md +262 -0
  61. package/resources/skills/skills/career/job-hunter/agents/career-network-message.md +108 -0
  62. package/resources/skills/skills/career/job-hunter/agents/career-promote.md +102 -0
  63. package/resources/skills/skills/career/job-hunter/agents/career-review.md +71 -0
  64. package/resources/skills/skills/career/job-hunter/agents/interview-debrief.md +117 -0
  65. package/resources/skills/skills/career/job-hunter/agents/interview-mock.md +171 -0
  66. package/resources/skills/skills/career/job-hunter/agents/interview-panel-decoder.md +152 -0
  67. package/resources/skills/skills/career/job-hunter/agents/interview-prep.md +184 -0
  68. package/resources/skills/skills/career/job-hunter/agents/interview-question-bank.md +133 -0
  69. package/resources/skills/skills/career/job-hunter/agents/interview-research.md +148 -0
  70. package/resources/skills/skills/career/job-hunter/agents/offer-compare.md +117 -0
  71. package/resources/skills/skills/career/job-hunter/agents/offer-counteroffer.md +144 -0
  72. package/resources/skills/skills/career/job-hunter/agents/offer-deadline-manager.md +148 -0
  73. package/resources/skills/skills/career/job-hunter/agents/offer-negotiate.md +126 -0
  74. package/resources/skills/skills/career/job-hunter/agents/offer-schedule.md +99 -0
  75. package/resources/skills/skills/career/job-hunter/agents/offer-thankyou.md +80 -0
  76. package/resources/skills/skills/career/job-hunter/agents/search-company-research.md +146 -0
  77. package/resources/skills/skills/career/job-hunter/agents/search-follow-up.md +129 -0
  78. package/resources/skills/skills/career/job-hunter/agents/search-ghost-job-detector.md +152 -0
  79. package/resources/skills/skills/career/job-hunter/agents/search-inbox-scan.md +193 -0
  80. package/resources/skills/skills/career/job-hunter/agents/search-interview-scorecard.md +164 -0
  81. package/resources/skills/skills/career/job-hunter/agents/search-jobs.md +149 -0
  82. package/resources/skills/skills/career/job-hunter/agents/search-momentum-check.md +194 -0
  83. package/resources/skills/skills/career/job-hunter/agents/search-outreach.md +85 -0
  84. package/resources/skills/skills/career/job-hunter/agents/search-referral-finder.md +124 -0
  85. package/resources/skills/skills/career/job-hunter/agents/search-salary.md +96 -0
  86. package/resources/skills/skills/career/job-hunter/agents/search-send-email.md +109 -0
  87. package/resources/skills/skills/career/job-hunter/agents/search-tracker-update.md +127 -0
  88. package/resources/skills/skills/career/job-hunter/inputs/README.md +26 -0
  89. package/resources/skills/skills/career/job-hunter/inputs/apply-linkedin-url.txt +8 -0
  90. package/resources/skills/skills/career/job-hunter/inputs/interview-context.md +24 -0
  91. package/resources/skills/skills/career/job-hunter/inputs/job-description.md +20 -0
  92. package/resources/skills/skills/career/job-hunter/inputs/job-search-criteria.md +36 -0
  93. package/resources/skills/skills/career/job-hunter/inputs/my-linkedin.md +24 -0
  94. package/resources/skills/skills/career/job-hunter/inputs/my-resume.md +28 -0
  95. package/resources/skills/skills/career/job-hunter/inputs/search-outreach-target.md +24 -0
  96. package/resources/skills/skills/career/job-hunter/rules/README.md +37 -0
  97. package/resources/skills/skills/career/job-hunter/rules/writing-rules.md +81 -0
  98. package/resources/skills/skills/design/banana/SKILL.md +375 -0
  99. package/resources/skills/skills/design/banana/references/cost-tracking.md +47 -0
  100. package/resources/skills/skills/design/banana/references/gemini-models.md +236 -0
  101. package/resources/skills/skills/design/banana/references/mcp-tools.md +145 -0
  102. package/resources/skills/skills/design/banana/references/post-processing.md +192 -0
  103. package/resources/skills/skills/design/banana/references/presets.md +69 -0
  104. package/resources/skills/skills/design/banana/references/prompt-engineering.md +481 -0
  105. package/resources/skills/skills/design/banana/scripts/batch.py +97 -0
  106. package/resources/skills/skills/design/banana/scripts/cost_tracker.py +191 -0
  107. package/resources/skills/skills/design/banana/scripts/edit.py +159 -0
  108. package/resources/skills/skills/design/banana/scripts/generate.py +168 -0
  109. package/resources/skills/skills/design/banana/scripts/presets.py +154 -0
  110. package/resources/skills/skills/design/banana/scripts/setup_mcp.py +151 -0
  111. package/resources/skills/skills/design/banana/scripts/validate_setup.py +133 -0
  112. package/resources/skills/skills/gstack/ship/SKILL.md +13 -0
  113. package/resources/skills/skills/media/3d-logo-animation/SKILL.md +59 -0
  114. package/resources/skills/skills/media/action-figure-generator/SKILL.md +48 -0
  115. package/resources/skills/skills/media/ad-creative/SKILL.md +79 -0
  116. package/resources/skills/skills/media/ai-clipping/SKILL.md +194 -0
  117. package/resources/skills/skills/media/ai-clipping/scripts/run-ai-clipping.sh +200 -0
  118. package/resources/skills/skills/media/ai-fight-scene/SKILL.md +132 -0
  119. package/resources/skills/skills/media/amazon-product-listing/SKILL.md +68 -0
  120. package/resources/skills/skills/media/animal-video-generator/SKILL.md +59 -0
  121. package/resources/skills/skills/media/award-ceremony-video/SKILL.md +87 -0
  122. package/resources/skills/skills/media/blog-header/SKILL.md +61 -0
  123. package/resources/skills/skills/media/brand-kit/SKILL.md +72 -0
  124. package/resources/skills/skills/media/brochures/SKILL.md +65 -0
  125. package/resources/skills/skills/media/cartoon-dance-animation/SKILL.md +62 -0
  126. package/resources/skills/skills/media/character-story-video/SKILL.md +84 -0
  127. package/resources/skills/skills/media/chibi-collage-effect/SKILL.md +63 -0
  128. package/resources/skills/skills/media/cinema-director/SKILL.md +93 -0
  129. package/resources/skills/skills/media/cinema-director/scripts/generate-film.sh +78 -0
  130. package/resources/skills/skills/media/color-analysis-board/SKILL.md +71 -0
  131. package/resources/skills/skills/media/core-edit/SKILL.md +48 -0
  132. package/resources/skills/skills/media/core-edit/edit-image.sh +54 -0
  133. package/resources/skills/skills/media/core-edit/enhance-image.sh +191 -0
  134. package/resources/skills/skills/media/core-edit/lipsync.sh +144 -0
  135. package/resources/skills/skills/media/core-edit/video-effects.sh +193 -0
  136. package/resources/skills/skills/media/core-media/SKILL.md +49 -0
  137. package/resources/skills/skills/media/core-media/create-music.sh +169 -0
  138. package/resources/skills/skills/media/core-media/generate-image.sh +161 -0
  139. package/resources/skills/skills/media/core-media/generate-video.sh +137 -0
  140. package/resources/skills/skills/media/core-media/image-to-video.sh +228 -0
  141. package/resources/skills/skills/media/core-media/schema_data.json +18708 -0
  142. package/resources/skills/skills/media/core-media/upload.sh +41 -0
  143. package/resources/skills/skills/media/core-platform/SKILL.md +41 -0
  144. package/resources/skills/skills/media/core-platform/check-result.sh +37 -0
  145. package/resources/skills/skills/media/core-platform/setup.sh +31 -0
  146. package/resources/skills/skills/media/couple-grid-creator/SKILL.md +47 -0
  147. package/resources/skills/skills/media/design-guide/SKILL.md +73 -0
  148. package/resources/skills/skills/media/drone-style-video/SKILL.md +61 -0
  149. package/resources/skills/skills/media/fashion-try-on/SKILL.md +61 -0
  150. package/resources/skills/skills/media/floor-plan-rendering/SKILL.md +56 -0
  151. package/resources/skills/skills/media/freeze-effect-video/SKILL.md +100 -0
  152. package/resources/skills/skills/media/giant-product-showcase/SKILL.md +61 -0
  153. package/resources/skills/skills/media/instagram-post/SKILL.md +58 -0
  154. package/resources/skills/skills/media/interior-design/SKILL.md +61 -0
  155. package/resources/skills/skills/media/interior-design-visualizer/SKILL.md +57 -0
  156. package/resources/skills/skills/media/jewelry-product-video/SKILL.md +61 -0
  157. package/resources/skills/skills/media/kdenlive/SKILL.md +106 -0
  158. package/resources/skills/skills/media/kdenlive/scripts/assemble.sh +57 -0
  159. package/resources/skills/skills/media/kdenlive/scripts/common.sh +30 -0
  160. package/resources/skills/skills/media/kdenlive/scripts/inspect.sh +19 -0
  161. package/resources/skills/skills/media/kdenlive/scripts/reframe.sh +22 -0
  162. package/resources/skills/skills/media/kdenlive/scripts/render.sh +16 -0
  163. package/resources/skills/skills/media/kdenlive/scripts/title-card.sh +25 -0
  164. package/resources/skills/skills/media/keyboard-art-maker/SKILL.md +44 -0
  165. package/resources/skills/skills/media/logo-branding/SKILL.md +70 -0
  166. package/resources/skills/skills/media/logo-creator/SKILL.md +80 -0
  167. package/resources/skills/skills/media/logo-creator/scripts/create-logo.sh +38 -0
  168. package/resources/skills/skills/media/logo-generator/SKILL.md +56 -0
  169. package/resources/skills/skills/media/multi-angle-reshoot/SKILL.md +70 -0
  170. package/resources/skills/skills/media/multi-angle-shots/SKILL.md +73 -0
  171. package/resources/skills/skills/media/music-video/SKILL.md +61 -0
  172. package/resources/skills/skills/media/nano-banana/SKILL.md +80 -0
  173. package/resources/skills/skills/media/nano-banana/scripts/generate-nano-art.sh +54 -0
  174. package/resources/skills/skills/media/one-shot-video/SKILL.md +56 -0
  175. package/resources/skills/skills/media/photo-pack-generator/SKILL.md +205 -0
  176. package/resources/skills/skills/media/photo-pack-generator/scripts/generate-pack.sh +241 -0
  177. package/resources/skills/skills/media/product-ad-cinematic/SKILL.md +78 -0
  178. package/resources/skills/skills/media/product-campaign/SKILL.md +76 -0
  179. package/resources/skills/skills/media/product-showcase-video/SKILL.md +60 -0
  180. package/resources/skills/skills/media/product-video-ad-maker/SKILL.md +59 -0
  181. package/resources/skills/skills/media/rednote-cover/SKILL.md +57 -0
  182. package/resources/skills/skills/media/seedance-2/SKILL.md +632 -0
  183. package/resources/skills/skills/media/seedance-2/scripts/generate-seedance.sh +701 -0
  184. package/resources/skills/skills/media/selfie-with-celebrities/SKILL.md +64 -0
  185. package/resources/skills/skills/media/social-media-video/SKILL.md +277 -0
  186. package/resources/skills/skills/media/social-media-video/scripts/run-social-video.sh +316 -0
  187. package/resources/skills/skills/media/social-pack/SKILL.md +58 -0
  188. package/resources/skills/skills/media/storyboard/SKILL.md +57 -0
  189. package/resources/skills/skills/media/storyboard-to-cooking-video/SKILL.md +143 -0
  190. package/resources/skills/skills/media/talking-baby-video/SKILL.md +57 -0
  191. package/resources/skills/skills/media/ugc-ads-workflow/SKILL.md +70 -0
  192. package/resources/skills/skills/media/ugc-lifestyle-try-on/SKILL.md +65 -0
  193. package/resources/skills/skills/media/ugc-video-factory/SKILL.md +134 -0
  194. package/resources/skills/skills/media/ui-design/SKILL.md +81 -0
  195. package/resources/skills/skills/media/ui-design/scripts/generate-mockup.sh +49 -0
  196. package/resources/skills/skills/media/url-to-design/SKILL.md +61 -0
  197. package/resources/skills/skills/media/workflow/SKILL.md +197 -0
  198. package/resources/skills/skills/media/workflow/scripts/discover-workflow.sh +18 -0
  199. package/resources/skills/skills/media/workflow/scripts/generate-workflow.sh +33 -0
  200. package/resources/skills/skills/media/workflow/scripts/interactive-run.sh +16 -0
  201. package/resources/skills/skills/media/workflow/scripts/list-workflows.sh +20 -0
  202. package/resources/skills/skills/media/workflow/scripts/run-workflow.sh +34 -0
  203. package/resources/skills/skills/media/youtube-shorts/SKILL.md +173 -0
  204. package/resources/skills/skills/media/youtube-shorts/scripts/run-youtube-shorts.sh +141 -0
  205. package/resources/skills/skills/media/youtube-thumbnail/SKILL.md +66 -0
  206. package/resources/skills/skills/meta/cue-developer/references/architecture.md +2 -2
  207. package/resources/skills/skills/meta/cue-usage/SKILL.md +1 -1
  208. package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +2 -2
  209. package/resources/skills/skills/meta/profile-optimizer/SKILL.md +1 -1
  210. package/resources/skills/skills/meta/profile-suggest/SKILL.md +7 -7
  211. package/resources/skills/skills/meta/profile-summon/SKILL.md +159 -0
  212. package/resources/skills/skills/meta/profile-summon/evals/evals.json +53 -0
  213. package/resources/skills/skills/meta/save-profile/SKILL.md +1 -1
  214. package/resources/skills/skills/meta/skill-reviewer/SKILL.md +3 -0
  215. package/resources/skills/skills/meta/skill-reviewer/references/tdd-for-skills.md +55 -0
  216. package/resources/skills/skills/research/find-skills/SKILL.md +1 -1
  217. package/resources/skills/skills/review/code-review-deep/SKILL.md +20 -0
  218. package/resources/skills/skills/security/trivy-scan/SKILL.md +139 -0
  219. package/resources/skills/skills/security/trivy-scan/scripts/ensure-trivy.sh +21 -0
  220. package/resources/skills/skills/tools/ccusage/SKILL.md +142 -0
  221. package/src/commands/_index.ts +8 -0
  222. package/src/commands/ai.ts +2 -2
  223. package/src/commands/auto-detect.test.ts +74 -0
  224. package/src/commands/auto-detect.ts +9 -7
  225. package/src/commands/cli.test.ts +20 -4
  226. package/src/commands/cli.ts +36 -20
  227. package/src/commands/create-profile.ts +2 -2
  228. package/src/commands/debug.ts +2 -2
  229. package/src/commands/discover.ts +14 -4
  230. package/src/commands/export-docker.ts +1 -1
  231. package/src/commands/features-batch1.test.ts +1 -1
  232. package/src/commands/gates.ts +1 -1
  233. package/src/commands/import-profile.ts +1 -1
  234. package/src/commands/init.ts +15 -11
  235. package/src/commands/install.test.ts +192 -0
  236. package/src/commands/install.ts +610 -0
  237. package/src/commands/launch-handoff.e2e.test.ts +33 -1
  238. package/src/commands/launch.e2e.test.ts +15 -10
  239. package/src/commands/launch.ts +73 -116
  240. package/src/commands/materialize.ts +2 -2
  241. package/src/commands/prune.ts +1 -1
  242. package/src/commands/security-audit.ts +1 -1
  243. package/src/commands/shell.ts +7 -7
  244. package/src/commands/skill-report.ts +1 -1
  245. package/src/commands/skills.ts +3 -3
  246. package/src/commands/snapshot.ts +2 -2
  247. package/src/commands/summon.test.ts +116 -0
  248. package/src/commands/summon.ts +338 -0
  249. package/src/commands/trigger-gaps.ts +1 -1
  250. package/src/commands/use.ts +47 -3
  251. package/src/commands/watch-live.ts +5 -5
  252. package/src/commands/watch.ts +8 -8
  253. package/src/index.ts +2 -0
  254. package/src/lib/active-sessions.test.ts +3 -3
  255. package/src/lib/active-sessions.ts +4 -4
  256. package/src/lib/auto-detect.test.ts +172 -8
  257. package/src/lib/auto-detect.ts +191 -136
  258. package/src/lib/codex-persona-parity.test.ts +58 -0
  259. package/src/lib/companion-detect.test.ts +43 -1
  260. package/src/lib/companion-detect.ts +35 -0
  261. package/src/lib/credentials-sync.test.ts +121 -1
  262. package/src/lib/credentials-sync.ts +95 -1
  263. package/src/lib/cwd-resolver.test.ts +8 -8
  264. package/src/lib/cwd-resolver.ts +2 -2
  265. package/src/lib/dashboard-merge.test.ts +9 -4
  266. package/src/lib/dashboard-server.ts +1 -1
  267. package/src/lib/picker.test.ts +1 -1
  268. package/src/lib/picker.ts +5 -5
  269. package/src/lib/profile-merge.test.ts +8 -0
  270. package/src/lib/profile-names.test.ts +3 -3
  271. package/src/lib/runtime-install.ts +166 -0
  272. package/src/lib/runtime-materializer.test.ts +137 -0
  273. package/src/lib/runtime-materializer.ts +105 -2
  274. package/src/lib/skill-router.test.ts +38 -0
  275. package/src/lib/skill-router.ts +65 -4
  276. package/profiles/eu-tender-research/README.md +0 -48
  277. package/profiles/eu-tender-research/logo.png +0 -0
  278. package/profiles/eu-tender-research/profile.yaml +0 -108
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """Banana Claude -- 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="Banana Claude 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,159 @@
1
+ #!/usr/bin/env python3
2
+ """Banana Claude -- 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 time
18
+ import urllib.request
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+
22
+ DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
23
+ OUTPUT_DIR = Path.home() / "Documents" / "nanobanana_generated"
24
+ API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
25
+
26
+
27
+ def edit_image(image_path, prompt, model, api_key):
28
+ """Call Gemini API to edit an image."""
29
+ image_path = Path(image_path).resolve()
30
+ if not image_path.exists():
31
+ print(json.dumps({"error": True, "message": f"Image not found: {image_path}"}))
32
+ sys.exit(1)
33
+
34
+ # Read and encode image
35
+ with open(image_path, "rb") as f:
36
+ image_b64 = base64.b64encode(f.read()).decode("utf-8")
37
+
38
+ # Determine MIME type
39
+ suffix = image_path.suffix.lower()
40
+ mime_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
41
+ ".webp": "image/webp", ".gif": "image/gif"}
42
+ mime_type = mime_types.get(suffix, "image/png")
43
+
44
+ url = f"{API_BASE}/{model}:generateContent?key={api_key}"
45
+
46
+ body = {
47
+ "contents": [
48
+ {
49
+ "parts": [
50
+ {"text": prompt},
51
+ {"inlineData": {"mimeType": mime_type, "data": image_b64}},
52
+ ]
53
+ }
54
+ ],
55
+ "generationConfig": {
56
+ "responseModalities": ["TEXT", "IMAGE"],
57
+ },
58
+ }
59
+
60
+ data = json.dumps(body).encode("utf-8")
61
+ req = urllib.request.Request(
62
+ url,
63
+ data=data,
64
+ headers={"Content-Type": "application/json"},
65
+ method="POST",
66
+ )
67
+
68
+ max_retries = 3
69
+ result = None
70
+ for attempt in range(max_retries):
71
+ try:
72
+ with urllib.request.urlopen(req, timeout=120) as resp:
73
+ result = json.loads(resp.read().decode("utf-8"))
74
+ break # Success
75
+ except urllib.error.HTTPError as e:
76
+ error_body = e.read().decode("utf-8") if e.fp else ""
77
+ if e.code == 429 and attempt < max_retries - 1:
78
+ wait = 2 ** (attempt + 1)
79
+ print(json.dumps({"retry": True, "attempt": attempt + 1, "wait_seconds": wait, "reason": "rate_limited"}), file=sys.stderr)
80
+ time.sleep(wait)
81
+ req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
82
+ continue
83
+ if e.code == 400 and "FAILED_PRECONDITION" in error_body:
84
+ print(json.dumps({"error": True, "status": 400, "message": "Billing not enabled. Enable billing at https://aistudio.google.com/apikey"}))
85
+ sys.exit(1)
86
+ print(json.dumps({"error": True, "status": e.code, "message": error_body}))
87
+ sys.exit(1)
88
+ except urllib.error.URLError as e:
89
+ print(json.dumps({"error": True, "message": str(e.reason)}))
90
+ sys.exit(1)
91
+
92
+ if result is None:
93
+ print(json.dumps({"error": True, "message": "Max retries exceeded"}))
94
+ sys.exit(1)
95
+
96
+ # Extract image from response
97
+ candidates = result.get("candidates", [])
98
+ if not candidates:
99
+ finish_reason = result.get("promptFeedback", {}).get("blockReason", "UNKNOWN")
100
+ print(json.dumps({"error": True, "message": f"No candidates returned. Reason: {finish_reason}"}))
101
+ sys.exit(1)
102
+
103
+ parts = candidates[0].get("content", {}).get("parts", [])
104
+ image_data = None
105
+ text_response = ""
106
+
107
+ for part in parts:
108
+ if "inlineData" in part:
109
+ image_data = part["inlineData"]["data"]
110
+ elif "text" in part:
111
+ text_response = part["text"]
112
+
113
+ if not image_data:
114
+ finish_reason = candidates[0].get("finishReason", "UNKNOWN")
115
+ print(json.dumps({"error": True, "message": f"No image in response. finishReason: {finish_reason}"}))
116
+ sys.exit(1)
117
+
118
+ # Save image
119
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
120
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
121
+ filename = f"banana_edit_{timestamp}.png"
122
+ output_path = (OUTPUT_DIR / filename).resolve()
123
+
124
+ with open(output_path, "wb") as f:
125
+ f.write(base64.b64decode(image_data))
126
+
127
+ return {
128
+ "path": str(output_path),
129
+ "model": model,
130
+ "source": str(image_path),
131
+ "text": text_response,
132
+ }
133
+
134
+
135
+ def main():
136
+ parser = argparse.ArgumentParser(description="Edit images via Gemini REST API")
137
+ parser.add_argument("--image", required=True, help="Path to input image")
138
+ parser.add_argument("--prompt", required=True, help="Edit instruction")
139
+ parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model ID (default: {DEFAULT_MODEL})")
140
+ parser.add_argument("--api-key", default=None, help="Google AI API key (or set GOOGLE_AI_API_KEY env)")
141
+
142
+ args = parser.parse_args()
143
+
144
+ api_key = args.api_key or os.environ.get("GOOGLE_AI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
145
+ if not api_key:
146
+ print(json.dumps({"error": True, "message": "No API key. Set GOOGLE_AI_API_KEY env or pass --api-key"}))
147
+ sys.exit(1)
148
+
149
+ result = edit_image(
150
+ image_path=args.image,
151
+ prompt=args.prompt,
152
+ model=args.model,
153
+ api_key=api_key,
154
+ )
155
+ print(json.dumps(result, indent=2))
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """Banana Claude -- Direct API Fallback: Image Generation
3
+
4
+ Generate images via Gemini REST API when MCP is unavailable.
5
+ Uses only Python stdlib (no pip dependencies).
6
+
7
+ Usage:
8
+ generate.py --prompt "a cat in space" [--aspect-ratio 16:9] [--resolution 1K]
9
+ [--model MODEL] [--api-key KEY] [--thinking LEVEL] [--image-only]
10
+ """
11
+
12
+ import argparse
13
+ import base64
14
+ import json
15
+ import os
16
+ import sys
17
+ import time
18
+ import urllib.request
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+
22
+ DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
23
+ DEFAULT_RESOLUTION = "2K" # Must be uppercase -- lowercase values are silently rejected by the API
24
+ DEFAULT_RATIO = "1:1"
25
+ OUTPUT_DIR = Path.home() / "Documents" / "nanobanana_generated"
26
+ API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
27
+
28
+ VALID_RATIOS = {"1:1", "16:9", "9:16", "4:3", "3:4", "2:3", "3:2",
29
+ "4:5", "5:4", "1:4", "4:1", "1:8", "8:1", "21:9"}
30
+ VALID_RESOLUTIONS = {"512", "1K", "2K", "4K"}
31
+
32
+
33
+ def generate_image(prompt, model, aspect_ratio, resolution, api_key,
34
+ thinking_level=None, image_only=False):
35
+ """Call Gemini API to generate an image."""
36
+ url = f"{API_BASE}/{model}:generateContent?key={api_key}"
37
+
38
+ modalities = ["IMAGE"] if image_only else ["TEXT", "IMAGE"]
39
+ body = {
40
+ "contents": [{"parts": [{"text": prompt}]}],
41
+ "generationConfig": {
42
+ "responseModalities": modalities,
43
+ "imageConfig": {
44
+ "aspectRatio": aspect_ratio,
45
+ "imageSize": resolution,
46
+ },
47
+ },
48
+ }
49
+
50
+ if thinking_level:
51
+ body["generationConfig"]["thinkingConfig"] = {"thinkingLevel": thinking_level}
52
+
53
+ data = json.dumps(body).encode("utf-8")
54
+ req = urllib.request.Request(
55
+ url,
56
+ data=data,
57
+ headers={"Content-Type": "application/json"},
58
+ method="POST",
59
+ )
60
+
61
+ max_retries = 3
62
+ result = None
63
+ for attempt in range(max_retries):
64
+ try:
65
+ with urllib.request.urlopen(req, timeout=120) as resp:
66
+ result = json.loads(resp.read().decode("utf-8"))
67
+ break # Success
68
+ except urllib.error.HTTPError as e:
69
+ error_body = e.read().decode("utf-8") if e.fp else ""
70
+ if e.code == 429 and attempt < max_retries - 1:
71
+ wait = 2 ** (attempt + 1)
72
+ print(json.dumps({"retry": True, "attempt": attempt + 1, "wait_seconds": wait, "reason": "rate_limited"}), file=sys.stderr)
73
+ time.sleep(wait)
74
+ # Rebuild request for retry
75
+ req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
76
+ continue
77
+ if e.code == 400 and "FAILED_PRECONDITION" in error_body:
78
+ print(json.dumps({"error": True, "status": 400, "message": "Billing not enabled. Enable billing at https://aistudio.google.com/apikey"}))
79
+ sys.exit(1)
80
+ print(json.dumps({"error": True, "status": e.code, "message": error_body}))
81
+ sys.exit(1)
82
+ except urllib.error.URLError as e:
83
+ print(json.dumps({"error": True, "message": str(e.reason)}))
84
+ sys.exit(1)
85
+
86
+ if result is None:
87
+ print(json.dumps({"error": True, "message": "Max retries exceeded"}))
88
+ sys.exit(1)
89
+
90
+ # Extract image from response
91
+ candidates = result.get("candidates", [])
92
+ if not candidates:
93
+ finish_reason = result.get("promptFeedback", {}).get("blockReason", "UNKNOWN")
94
+ print(json.dumps({"error": True, "message": f"No candidates returned. Reason: {finish_reason}"}))
95
+ sys.exit(1)
96
+
97
+ parts = candidates[0].get("content", {}).get("parts", [])
98
+ image_data = None
99
+ text_response = ""
100
+
101
+ for part in parts:
102
+ if "inlineData" in part:
103
+ image_data = part["inlineData"]["data"]
104
+ elif "text" in part:
105
+ text_response = part["text"]
106
+
107
+ if not image_data:
108
+ finish_reason = candidates[0].get("finishReason", "UNKNOWN")
109
+ print(json.dumps({"error": True, "message": f"No image in response. finishReason: {finish_reason}"}))
110
+ sys.exit(1)
111
+
112
+ # Save image
113
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
114
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
115
+ filename = f"banana_{timestamp}.png"
116
+ output_path = (OUTPUT_DIR / filename).resolve()
117
+
118
+ with open(output_path, "wb") as f:
119
+ f.write(base64.b64decode(image_data))
120
+
121
+ return {
122
+ "path": str(output_path),
123
+ "model": model,
124
+ "aspect_ratio": aspect_ratio,
125
+ "resolution": resolution,
126
+ "text": text_response,
127
+ }
128
+
129
+
130
+ def main():
131
+ parser = argparse.ArgumentParser(description="Generate images via Gemini REST API")
132
+ parser.add_argument("--prompt", required=True, help="Image generation prompt")
133
+ parser.add_argument("--aspect-ratio", default=DEFAULT_RATIO, help=f"Aspect ratio (default: {DEFAULT_RATIO})")
134
+ parser.add_argument("--resolution", default=DEFAULT_RESOLUTION, help=f"Resolution: 512, 1K, 2K, 4K (default: {DEFAULT_RESOLUTION})")
135
+ parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model ID (default: {DEFAULT_MODEL})")
136
+ parser.add_argument("--api-key", default=None, help="Google AI API key (or set GOOGLE_AI_API_KEY env)")
137
+ parser.add_argument("--thinking", default=None, choices=["minimal", "low", "medium", "high"], help="Thinking level")
138
+ parser.add_argument("--image-only", action="store_true", help="Return image only (no text)")
139
+
140
+ args = parser.parse_args()
141
+
142
+ if args.aspect_ratio not in VALID_RATIOS:
143
+ print(json.dumps({"error": True, "message": f"Invalid aspect ratio '{args.aspect_ratio}'. Valid: {sorted(VALID_RATIOS)}"}))
144
+ sys.exit(1)
145
+
146
+ if args.resolution not in VALID_RESOLUTIONS:
147
+ print(json.dumps({"error": True, "message": f"Invalid resolution '{args.resolution}'. Valid: {sorted(VALID_RESOLUTIONS)}"}))
148
+ sys.exit(1)
149
+
150
+ api_key = args.api_key or os.environ.get("GOOGLE_AI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
151
+ if not api_key:
152
+ print(json.dumps({"error": True, "message": "No API key. Set GOOGLE_AI_API_KEY env or pass --api-key"}))
153
+ sys.exit(1)
154
+
155
+ result = generate_image(
156
+ prompt=args.prompt,
157
+ model=args.model,
158
+ aspect_ratio=args.aspect_ratio,
159
+ resolution=args.resolution,
160
+ api_key=api_key,
161
+ thinking_level=args.thinking,
162
+ image_only=args.image_only,
163
+ )
164
+ print(json.dumps(result, indent=2))
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """Banana Claude -- Brand/Style Presets
3
+
4
+ Manage reusable brand and style presets for consistent image generation.
5
+
6
+ Usage:
7
+ presets.py list
8
+ presets.py show NAME
9
+ presets.py create NAME --colors "#hex,#hex" --style "..." [options]
10
+ presets.py delete NAME --confirm
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ PRESETS_DIR = Path.home() / ".banana" / "presets"
20
+
21
+
22
+ def _ensure_dir():
23
+ """Ensure presets directory exists."""
24
+ PRESETS_DIR.mkdir(parents=True, exist_ok=True)
25
+
26
+
27
+ def _sanitize_name(name):
28
+ """Sanitize preset name to prevent path traversal."""
29
+ # Strip path separators and keep only safe characters
30
+ safe = re.sub(r'[^a-zA-Z0-9_\-]', '', name)
31
+ if not safe:
32
+ print("Error: Preset name must contain only letters, numbers, hyphens, and underscores.", file=sys.stderr)
33
+ sys.exit(1)
34
+ return safe
35
+
36
+
37
+ def _preset_path(name):
38
+ """Get path for a preset file."""
39
+ safe_name = _sanitize_name(name)
40
+ return PRESETS_DIR / f"{safe_name}.json"
41
+
42
+
43
+ def _load_preset(name):
44
+ """Load a preset by name."""
45
+ path = _preset_path(name)
46
+ if not path.exists():
47
+ print(f"Error: Preset '{name}' not found.", file=sys.stderr)
48
+ sys.exit(1)
49
+ with open(path, "r") as f:
50
+ return json.load(f)
51
+
52
+
53
+ def cmd_list(args):
54
+ """List available presets."""
55
+ _ensure_dir()
56
+ presets = sorted(PRESETS_DIR.glob("*.json"))
57
+ if not presets:
58
+ print("No presets found. Create one with: presets.py create NAME --style \"...\"")
59
+ return
60
+ print(f"Available presets ({len(presets)}):\n")
61
+ for p in presets:
62
+ try:
63
+ with open(p, "r") as f:
64
+ data = json.load(f)
65
+ desc = data.get("description", "No description")
66
+ print(f" {p.stem:20s} -- {desc}")
67
+ except (json.JSONDecodeError, KeyError):
68
+ print(f" {p.stem:20s} -- (invalid preset file)")
69
+
70
+
71
+ def cmd_show(args):
72
+ """Show full preset details."""
73
+ preset = _load_preset(args.name)
74
+ print(json.dumps(preset, indent=2))
75
+
76
+
77
+ def cmd_create(args):
78
+ """Create a new preset."""
79
+ _ensure_dir()
80
+ path = _preset_path(args.name)
81
+ if path.exists():
82
+ print(f"Error: Preset '{args.name}' already exists. Use a different name.", file=sys.stderr)
83
+ sys.exit(1)
84
+
85
+ colors = [c.strip() for c in args.colors.split(",")] if args.colors else []
86
+
87
+ preset = {
88
+ "name": args.name,
89
+ "description": args.description or f"Custom preset: {args.name}",
90
+ "colors": colors,
91
+ "style": args.style or "",
92
+ "typography": args.typography or "",
93
+ "lighting": args.lighting or "",
94
+ "mood": args.mood or "",
95
+ "default_ratio": args.ratio or "16:9",
96
+ "default_resolution": args.resolution or "2K",
97
+ }
98
+
99
+ with open(path, "w") as f:
100
+ json.dump(preset, f, indent=2)
101
+
102
+ print(f"Preset '{args.name}' created at {path}")
103
+ print(json.dumps(preset, indent=2))
104
+
105
+
106
+ def cmd_delete(args):
107
+ """Delete a preset."""
108
+ if not args.confirm:
109
+ print("Error: Pass --confirm to delete the preset.", file=sys.stderr)
110
+ sys.exit(1)
111
+ path = _preset_path(args.name)
112
+ if not path.exists():
113
+ print(f"Error: Preset '{args.name}' not found.", file=sys.stderr)
114
+ sys.exit(1)
115
+ path.unlink()
116
+ print(f"Preset '{args.name}' deleted.")
117
+
118
+
119
+ def main():
120
+ parser = argparse.ArgumentParser(description="Banana Claude Brand/Style Presets")
121
+ sub = parser.add_subparsers(dest="command", required=True)
122
+
123
+ # list
124
+ sub.add_parser("list", help="List available presets")
125
+
126
+ # show
127
+ p_show = sub.add_parser("show", help="Show preset details")
128
+ p_show.add_argument("name", help="Preset name")
129
+
130
+ # create
131
+ p_create = sub.add_parser("create", help="Create a new preset")
132
+ p_create.add_argument("name", help="Preset name (e.g., tech-saas, luxury-brand)")
133
+ p_create.add_argument("--colors", default="", help="Comma-separated hex colors")
134
+ p_create.add_argument("--style", default="", help="Visual style description")
135
+ p_create.add_argument("--typography", default="", help="Typography description")
136
+ p_create.add_argument("--lighting", default="", help="Lighting description")
137
+ p_create.add_argument("--mood", default="", help="Mood/emotion description")
138
+ p_create.add_argument("--description", default="", help="Brief preset description")
139
+ p_create.add_argument("--ratio", default="16:9", help="Default aspect ratio")
140
+ p_create.add_argument("--resolution", default="2K", help="Default resolution")
141
+ p_create.add_argument("--force", action="store_true", help="Overwrite existing preset")
142
+
143
+ # delete
144
+ p_delete = sub.add_parser("delete", help="Delete a preset")
145
+ p_delete.add_argument("name", help="Preset name")
146
+ p_delete.add_argument("--confirm", action="store_true", help="Confirm deletion")
147
+
148
+ args = parser.parse_args()
149
+ cmds = {"list": cmd_list, "show": cmd_show, "create": cmd_create, "delete": cmd_delete}
150
+ cmds[args.command](args)
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()