namango 0.3.0__tar.gz

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.
namango-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: namango
3
+ Version: 0.3.0
4
+ Summary: Namango — AI Stack Intelligence for Product Builders
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: httpx
File without changes
@@ -0,0 +1,3 @@
1
+ from namango.cli import main
2
+
3
+ main()
@@ -0,0 +1,898 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Namango — AI Stack Intelligence for Product Builders
4
+ =====================================================
5
+ Fetches Namango's curated catalog of real product-building tools,
6
+ selects the best stack for your product idea, asks OSS vs premium
7
+ preference, then generates a detailed stack blueprint + writes
8
+ CLAUDE.md, .env.example, and install.sh to ./namango-output/.
9
+
10
+ Usage:
11
+ namango init "build a customer support helpdesk for a Zomato-like app"
12
+ namango init # interactive prompt
13
+ namango init "my idea" --output ./my-project
14
+ namango init "my idea" --compare # show OSS vs Cloud side-by-side
15
+
16
+ GATEWAY_URL and API_KEY can also be set as env vars.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+ import json
23
+ import time
24
+ import re
25
+ import argparse
26
+ import textwrap
27
+ import shutil
28
+ from pathlib import Path
29
+ import httpx
30
+
31
+ # ── Config ───────────────────────────────────────────────────────────────────
32
+ DEFAULT_GATEWAY = os.getenv("GATEWAY_URL", "https://ai-gateway-backend-production.up.railway.app")
33
+ DEFAULT_KEY = os.getenv("API_KEY", "gw-bn1RrcLqzARb5mPpBMiCzB0ZbxhpCVasQ_5hz9aWIrE")
34
+ DEFAULT_MODEL = "meta-llama/llama-3.1-70b-instruct"
35
+
36
+ # ── ANSI ─────────────────────────────────────────────────────────────────────
37
+ R = "\033[0m"
38
+ BOLD = "\033[1m"
39
+ DIM = "\033[2m"
40
+ CYAN = "\033[36m"; BCYAN = "\033[96m"
41
+ GRN = "\033[32m"; BGRN = "\033[92m"
42
+ YLW = "\033[33m"; BYLW = "\033[93m"
43
+ MAG = "\033[35m"; BMAG = "\033[95m"
44
+ BLU = "\033[34m"; BBLU = "\033[94m"
45
+ RED = "\033[31m"
46
+ WHT = "\033[97m"
47
+
48
+ # ── Embedded Stack Catalog ────────────────────────────────────────────────────
49
+ # Curated real product-building tools organized by category.
50
+ # Used as fallback when the live /v1/stacks endpoint is unavailable.
51
+ # Keep in sync with backend/app/api/stacks.py STACK_CATALOG.
52
+ STACK_CATALOG: dict[str, list[dict]] = {
53
+ "web": [
54
+ {"slug": "fastapi", "name": "FastAPI", "description": "High-performance async Python web framework", "tier": "free", "monthly_cost_usd": 0, "category": "web"},
55
+ {"slug": "django", "name": "Django", "description": "Batteries-included Python web framework", "tier": "free", "monthly_cost_usd": 0, "category": "web"},
56
+ {"slug": "express", "name": "Express.js", "description": "Minimal, flexible Node.js web framework", "tier": "free", "monthly_cost_usd": 0, "category": "web"},
57
+ {"slug": "nextjs", "name": "Next.js", "description": "React framework with SSR and API routes", "tier": "free", "monthly_cost_usd": 0, "category": "web"},
58
+ {"slug": "flask", "name": "Flask", "description": "Lightweight Python web microframework", "tier": "free", "monthly_cost_usd": 0, "category": "web"},
59
+ ],
60
+ "database": [
61
+ {"slug": "postgresql", "name": "PostgreSQL", "description": "Reliable open-source relational database", "tier": "free", "monthly_cost_usd": 0, "category": "database"},
62
+ {"slug": "mongodb", "name": "MongoDB", "description": "Flexible NoSQL document database", "tier": "freemium", "monthly_cost_usd": 0, "category": "database"},
63
+ {"slug": "sqlite", "name": "SQLite", "description": "Zero-config embedded SQL, perfect for prototyping", "tier": "free", "monthly_cost_usd": 0, "category": "database"},
64
+ {"slug": "supabase", "name": "Supabase", "description": "Open-source Firebase with Postgres + Auth + Storage", "tier": "freemium", "monthly_cost_usd": 0, "category": "database"},
65
+ {"slug": "planetscale", "name": "PlanetScale", "description": "Serverless MySQL with branching for safe migrations", "tier": "freemium", "monthly_cost_usd": 0, "category": "database"},
66
+ ],
67
+ "cache": [
68
+ {"slug": "redis", "name": "Redis", "description": "In-memory cache, sessions, pub/sub messaging", "tier": "free", "monthly_cost_usd": 0, "category": "cache"},
69
+ {"slug": "upstash", "name": "Upstash Redis", "description": "Serverless Redis, pay-per-request pricing", "tier": "freemium", "monthly_cost_usd": 0, "category": "cache"},
70
+ ],
71
+ "queue": [
72
+ {"slug": "celery", "name": "Celery", "description": "Distributed task queue for Python with Redis/RabbitMQ", "tier": "free", "monthly_cost_usd": 0, "category": "queue"},
73
+ {"slug": "bullmq", "name": "BullMQ", "description": "Redis-backed job queue for Node.js", "tier": "free", "monthly_cost_usd": 0, "category": "queue"},
74
+ {"slug": "inngest", "name": "Inngest", "description": "Event-driven serverless background jobs", "tier": "freemium", "monthly_cost_usd": 0, "category": "queue"},
75
+ {"slug": "rabbitmq", "name": "RabbitMQ", "description": "Message broker for distributed systems", "tier": "free", "monthly_cost_usd": 0, "category": "queue"},
76
+ ],
77
+ "auth": [
78
+ {"slug": "jwt", "name": "JWT", "description": "Stateless token-based auth — zero infra", "tier": "free", "monthly_cost_usd": 0, "category": "auth"},
79
+ {"slug": "auth0", "name": "Auth0", "description": "Identity platform — OAuth, OIDC, SAML, MFA", "tier": "freemium", "monthly_cost_usd": 23, "category": "auth"},
80
+ {"slug": "supabase-auth", "name": "Supabase Auth", "description": "Open-source auth with social login and RLS", "tier": "freemium", "monthly_cost_usd": 0, "category": "auth"},
81
+ {"slug": "clerk", "name": "Clerk", "description": "Drop-in auth UI components for React/Next.js", "tier": "freemium", "monthly_cost_usd": 0, "category": "auth"},
82
+ {"slug": "nextauth", "name": "NextAuth.js", "description": "Auth for Next.js with 50+ OAuth providers", "tier": "free", "monthly_cost_usd": 0, "category": "auth"},
83
+ ],
84
+ "payments": [
85
+ {"slug": "stripe", "name": "Stripe", "description": "Payments, subscriptions, invoices, Connect", "tier": "paid", "monthly_cost_usd": 0, "category": "payments"},
86
+ {"slug": "razorpay", "name": "Razorpay", "description": "Indian payments — UPI, cards, net banking, EMI","tier": "paid", "monthly_cost_usd": 0, "category": "payments"},
87
+ {"slug": "paddle", "name": "Paddle", "description": "Merchant of record — handles global tax/VAT", "tier": "paid", "monthly_cost_usd": 0, "category": "payments"},
88
+ {"slug": "lemonsqueezy", "name": "Lemon Squeezy", "description": "Payments and subscriptions for indie SaaS", "tier": "paid", "monthly_cost_usd": 0, "category": "payments"},
89
+ ],
90
+ "email": [
91
+ {"slug": "sendgrid", "name": "SendGrid", "description": "Transactional email API, 100 emails/day free", "tier": "freemium", "monthly_cost_usd": 0, "category": "email"},
92
+ {"slug": "resend", "name": "Resend", "description": "Developer-first email with React Email templates", "tier": "freemium", "monthly_cost_usd": 0, "category": "email"},
93
+ {"slug": "postmark", "name": "Postmark", "description": "Best deliverability for transactional email", "tier": "paid", "monthly_cost_usd": 15, "category": "email"},
94
+ {"slug": "mailgun", "name": "Mailgun", "description": "Email API with advanced analytics and logging", "tier": "freemium", "monthly_cost_usd": 0, "category": "email"},
95
+ ],
96
+ "notifications": [
97
+ {"slug": "twilio", "name": "Twilio", "description": "SMS, WhatsApp, and voice notifications API", "tier": "paid", "monthly_cost_usd": 0, "category": "notifications"},
98
+ {"slug": "firebase-fcm", "name": "Firebase FCM", "description": "Free push notifications — iOS, Android, web", "tier": "freemium", "monthly_cost_usd": 0, "category": "notifications"},
99
+ {"slug": "onesignal", "name": "OneSignal", "description": "Push, email, SMS, in-app — all in one", "tier": "freemium", "monthly_cost_usd": 0, "category": "notifications"},
100
+ ],
101
+ "deploy": [
102
+ {"slug": "railway", "name": "Railway", "description": "Deploy anything with Git push, $5/mo hobby tier", "tier": "freemium", "monthly_cost_usd": 5, "category": "deploy"},
103
+ {"slug": "vercel", "name": "Vercel", "description": "Frontend + serverless, generous free tier", "tier": "freemium", "monthly_cost_usd": 0, "category": "deploy"},
104
+ {"slug": "render", "name": "Render", "description": "Full-stack cloud — web services, workers, cron, DB", "tier": "freemium", "monthly_cost_usd": 7, "category": "deploy"},
105
+ {"slug": "fly", "name": "Fly.io", "description": "Run Docker apps globally with edge deployment", "tier": "freemium", "monthly_cost_usd": 0, "category": "deploy"},
106
+ {"slug": "docker", "name": "Docker", "description": "Container packaging and local development runtime", "tier": "free", "monthly_cost_usd": 0, "category": "deploy"},
107
+ ],
108
+ "ai": [
109
+ {"slug": "openai", "name": "OpenAI API", "description": "GPT-4o, embeddings, vision, Whisper", "tier": "paid", "monthly_cost_usd": 20, "category": "ai"},
110
+ {"slug": "anthropic", "name": "Anthropic Claude", "description": "Claude 3.5 — reasoning, long context, code","tier": "paid", "monthly_cost_usd": 15, "category": "ai"},
111
+ {"slug": "groq", "name": "Groq", "description": "Ultra-fast inference — Llama3, Mixtral free","tier": "freemium", "monthly_cost_usd": 0, "category": "ai"},
112
+ {"slug": "ollama", "name": "Ollama", "description": "Run LLMs locally — Llama3, Mistral, Phi3", "tier": "free", "monthly_cost_usd": 0, "category": "ai"},
113
+ {"slug": "langchain", "name": "LangChain", "description": "LLM application framework and LCEL", "tier": "free", "monthly_cost_usd": 0, "category": "ai"},
114
+ {"slug": "pinecone", "name": "Pinecone", "description": "Managed vector DB for semantic search/RAG", "tier": "freemium", "monthly_cost_usd": 0, "category": "ai"},
115
+ ],
116
+ "storage": [
117
+ {"slug": "s3", "name": "AWS S3", "description": "Object storage for files, media, backups", "tier": "paid", "monthly_cost_usd": 2, "category": "storage"},
118
+ {"slug": "cloudinary", "name": "Cloudinary", "description": "Image/video transform, optimization, CDN", "tier": "freemium", "monthly_cost_usd": 0, "category": "storage"},
119
+ {"slug": "supabase-storage", "name": "Supabase Storage", "description": "S3-compatible storage with RLS access ctrl", "tier": "freemium", "monthly_cost_usd": 0, "category": "storage"},
120
+ {"slug": "r2", "name": "Cloudflare R2", "description": "Zero-egress S3-compatible object storage", "tier": "freemium", "monthly_cost_usd": 0, "category": "storage"},
121
+ ],
122
+ "monitoring": [
123
+ {"slug": "sentry", "name": "Sentry", "description": "Error tracking, performance monitoring, session replay", "tier": "freemium", "monthly_cost_usd": 0, "category": "monitoring"},
124
+ {"slug": "posthog", "name": "PostHog", "description": "Open-source analytics, feature flags, A/B tests", "tier": "freemium", "monthly_cost_usd": 0, "category": "monitoring"},
125
+ {"slug": "datadog", "name": "Datadog", "description": "Full-stack observability — metrics, APM, logs", "tier": "paid", "monthly_cost_usd": 31, "category": "monitoring"},
126
+ {"slug": "grafana", "name": "Grafana", "description": "Open-source metrics and log visualization dashboards", "tier": "freemium", "monthly_cost_usd": 0, "category": "monitoring"},
127
+ ],
128
+ "search": [
129
+ {"slug": "elasticsearch", "name": "Elasticsearch", "description": "Distributed full-text search and analytics", "tier": "freemium", "monthly_cost_usd": 0, "category": "search"},
130
+ {"slug": "algolia", "name": "Algolia", "description": "Search as a service — instant, typo-tolerant", "tier": "freemium", "monthly_cost_usd": 0, "category": "search"},
131
+ {"slug": "typesense", "name": "Typesense", "description": "Open-source instant search, self-hostable", "tier": "free", "monthly_cost_usd": 0, "category": "search"},
132
+ ],
133
+ "realtime": [
134
+ {"slug": "websockets", "name": "WebSockets", "description": "Native real-time bidirectional communication", "tier": "free", "monthly_cost_usd": 0, "category": "realtime"},
135
+ {"slug": "pusher", "name": "Pusher", "description": "Hosted WebSocket, 200k messages/day free", "tier": "freemium", "monthly_cost_usd": 0, "category": "realtime"},
136
+ {"slug": "socket-io", "name": "Socket.IO", "description": "WebSocket library with rooms and namespaces", "tier": "free", "monthly_cost_usd": 0, "category": "realtime"},
137
+ ],
138
+ }
139
+
140
+
141
+ # ── Terminal helpers ──────────────────────────────────────────────────────────
142
+
143
+ def _term_width() -> int:
144
+ return max(60, min(72, shutil.get_terminal_size(fallback=(80, 24)).columns - 4))
145
+
146
+
147
+ # ── Pipeline Renderer ─────────────────────────────────────────────────────────
148
+
149
+ def render_pipeline(steps_done: list[str], active_step: str | None, subtitle: str = "") -> str:
150
+ """Render the Namango 5-step stack intelligence pipeline."""
151
+ W = _term_width()
152
+
153
+ NODES = [
154
+ ("intent", "🧠 Intent Analyzer"),
155
+ ("catalog", "📦 Stack Catalog"),
156
+ ("selector", "🎯 Stack Selector"),
157
+ ("budget", "💰 Cost Advisor"),
158
+ ("blueprint", "🏗️ Blueprint Builder"),
159
+ ]
160
+
161
+ lines = []
162
+ lines.append(f"{CYAN}╔{'═' * (W-2)}╗{R}")
163
+ title = " NAMANGO — STACK INTELLIGENCE"
164
+ lines.append(f"{CYAN}║{BOLD}{title:^{W-2}}{R}{CYAN}║{R}")
165
+ if subtitle:
166
+ short = subtitle if len(subtitle) <= W - 6 else subtitle[:W-9] + "..."
167
+ lines.append(f"{CYAN}║{R} {DIM}{short}{R}")
168
+ lines.append(f"{CYAN}╠{'═' * (W-2)}╣{R}")
169
+
170
+ for i, (step_id, label) in enumerate(NODES):
171
+ is_done = step_id in steps_done
172
+ is_active = step_id == active_step
173
+
174
+ if is_active:
175
+ status = f"{BYLW}▶ RUNNING {R}"
176
+ color = BYLW
177
+ elif is_done:
178
+ status = f"{BGRN}✓ DONE {R}"
179
+ color = BGRN
180
+ else:
181
+ status = f"{DIM}○ PENDING {R}"
182
+ color = DIM
183
+
184
+ node_str = f"{color}{label:<26}{R}"
185
+ lines.append(f"{CYAN}║{R} {node_str} {status} {CYAN}║{R}")
186
+
187
+ if i < len(NODES) - 1:
188
+ arrow_color = GRN if step_id in steps_done else DIM
189
+ lines.append(f"{CYAN}║{R} {arrow_color}{'':4}↓{R}{'':50} {CYAN}║{R}")
190
+
191
+ lines.append(f"{CYAN}╚{'═' * (W-2)}╝{R}")
192
+ return "\n".join(lines)
193
+
194
+
195
+ # ── Catalog Functions ─────────────────────────────────────────────────────────
196
+
197
+ def fetch_stack_catalog(url: str, key: str) -> dict[str, list[dict]]:
198
+ """
199
+ Fetch the curated stack catalog from /v1/stacks.
200
+ Falls back to the embedded STACK_CATALOG on any error.
201
+
202
+ Returns a dict keyed by category (web, database, auth, ...) where
203
+ each value is a list of tool dicts with slug/name/description/tier/monthly_cost_usd.
204
+ """
205
+ try:
206
+ r = httpx.get(
207
+ f"{url}/v1/stacks",
208
+ headers={"X-API-Key": key},
209
+ timeout=5.0,
210
+ )
211
+ if r.status_code == 200:
212
+ data = r.json()
213
+ categories = data.get("categories")
214
+ if isinstance(categories, dict) and categories:
215
+ return categories
216
+ except Exception:
217
+ pass
218
+ return STACK_CATALOG
219
+
220
+
221
+ def _flatten_catalog(catalog: dict[str, list[dict]]) -> list[dict]:
222
+ """Flatten category → [tools] dict into a single list with category field."""
223
+ all_tools: list[dict] = []
224
+ for category, tools in catalog.items():
225
+ for t in tools:
226
+ all_tools.append({**t, "category": t.get("category") or category})
227
+ return all_tools
228
+
229
+
230
+ def select_tools(url: str, key: str, user_prompt: str, catalog: dict) -> list[dict]:
231
+ """
232
+ Ask the gateway LLM to select the best 5-8 tools from the stack catalog
233
+ for the user's product idea. Falls back to keyword scoring on failure.
234
+ """
235
+ all_tools = _flatten_catalog(catalog)
236
+
237
+ # Build prompt lines: send up to 35 tools, one per line, with category + tier
238
+ prompt_lines: list[str] = []
239
+ for t in all_tools[:35]:
240
+ cat = t.get("category", "?")
241
+ name = t.get("name", t.get("slug", "?"))
242
+ desc = (t.get("description") or "")[:65]
243
+ tier = t.get("tier", "free")
244
+ prompt_lines.append(f"[{cat}] {name}: {desc} (tier={tier})")
245
+
246
+ selection_prompt = (
247
+ f"You are a senior software architect helping a developer pick their tech stack.\n"
248
+ f"The developer wants to: {user_prompt}\n\n"
249
+ f"Available tools from the Namango catalog:\n"
250
+ + "\n".join(prompt_lines)
251
+ + "\n\nSelect 5-8 tools that form a complete, coherent stack for this product.\n"
252
+ f"Prefer tools that work well together. Include at minimum: one web framework, "
253
+ f"one database, and one deploy option.\n"
254
+ f"Reply with ONLY a JSON array, no markdown:\n"
255
+ f'[{{"name":"<exact name from catalog>", "category":"<category>", '
256
+ f'"tier":"free|freemium|paid", "reason":"one sentence why this tool fits"}}]'
257
+ )
258
+
259
+ # Build name → item lookup for enrichment
260
+ name_index: dict[str, dict] = {}
261
+ for t in all_tools:
262
+ name_key = (t.get("name") or "").lower()
263
+ slug_key = (t.get("slug") or "").lower()
264
+ if name_key:
265
+ name_index[name_key] = t
266
+ if slug_key:
267
+ name_index[slug_key] = t
268
+
269
+ def _lookup(name: str) -> dict:
270
+ k = name.lower()
271
+ if k in name_index:
272
+ return name_index[k]
273
+ for ck, cv in name_index.items():
274
+ if ck.startswith(k) or k.startswith(ck):
275
+ return cv
276
+ return {}
277
+
278
+ try:
279
+ resp = httpx.post(
280
+ f"{url}/v1/query",
281
+ json={"prompt": selection_prompt, "preferred_model": DEFAULT_MODEL},
282
+ headers={"X-API-Key": key, "Content-Type": "application/json"},
283
+ timeout=45.0,
284
+ )
285
+ if resp.status_code == 200:
286
+ text = resp.json().get("response", "").strip()
287
+ parsed = None
288
+ if text.startswith("["):
289
+ try:
290
+ parsed = json.loads(text)
291
+ except json.JSONDecodeError:
292
+ pass
293
+ if parsed is None:
294
+ m = re.search(r'\[[\s\S]*\]', text)
295
+ if m:
296
+ try:
297
+ parsed = json.loads(m.group(0))
298
+ except json.JSONDecodeError:
299
+ pass
300
+ if parsed and isinstance(parsed, list):
301
+ enriched: list[dict] = []
302
+ seen: set[str] = set()
303
+ for item in parsed:
304
+ name_val = item.get("name", "")
305
+ catalog_item = _lookup(name_val)
306
+ canonical_name = catalog_item.get("name") or name_val
307
+ if canonical_name.lower() in seen:
308
+ continue
309
+ seen.add(canonical_name.lower())
310
+ enriched.append({
311
+ "slug": catalog_item.get("slug") or name_val.lower().replace(" ", "-"),
312
+ "name": canonical_name,
313
+ "category": catalog_item.get("category") or item.get("category", "?"),
314
+ "tier": catalog_item.get("tier") or item.get("tier", "free"),
315
+ "monthly_cost_usd": catalog_item.get("monthly_cost_usd", 0),
316
+ "reason": item.get("reason", ""),
317
+ })
318
+ if enriched:
319
+ return enriched
320
+ except Exception:
321
+ pass
322
+
323
+ # Keyword fallback: score tools by description overlap with user prompt
324
+ words = set(re.sub(r'[^a-z\s]', '', user_prompt.lower()).split())
325
+ scored = sorted(
326
+ all_tools,
327
+ key=lambda t: len(words & set((t.get("description") or "").lower().split())),
328
+ reverse=True,
329
+ )
330
+ return [
331
+ {
332
+ "slug": t["slug"],
333
+ "name": t["name"],
334
+ "category": t.get("category", "?"),
335
+ "tier": t.get("tier", "free"),
336
+ "monthly_cost_usd": t.get("monthly_cost_usd", 0),
337
+ "reason": "matches your use case",
338
+ }
339
+ for t in scored[:6]
340
+ ]
341
+
342
+
343
+ def ask_budget(selected_tools: list[dict]) -> str:
344
+ """Show OSS vs Premium cost breakdown, ask user preference."""
345
+ free_tools = [t for t in selected_tools if t.get("tier") in ("free", "freemium")]
346
+ paid_tools = [t for t in selected_tools if t.get("tier") == "paid"]
347
+ cloud_cost = sum(t.get("monthly_cost_usd", 0) for t in selected_tools if t.get("tier") != "free")
348
+
349
+ W = _term_width()
350
+ print(f"\n {CYAN}{'─'*(W-2)}{R}")
351
+ print(f" {BOLD}Recommended Stack:{R}\n")
352
+
353
+ cat_groups: dict[str, list[dict]] = {}
354
+ for t in selected_tools:
355
+ cat = t.get("category", "other")
356
+ cat_groups.setdefault(cat, []).append(t)
357
+
358
+ for cat, tools in cat_groups.items():
359
+ names = ", ".join(
360
+ f"{BGRN}{t['name']}{R}" if t.get("tier") != "paid" else f"{BYLW}{t['name']}{R}"
361
+ for t in tools
362
+ )
363
+ print(f" {DIM}{cat:<14}{R} {names}")
364
+
365
+ print()
366
+
367
+ oss_cost = 0
368
+ premium_cost = cloud_cost if cloud_cost > 0 else 0
369
+ print(f" {BOLD}Estimated monthly cost:{R}")
370
+ print(f" {BGRN}A) Free / OSS stack {R} ${oss_cost}/mo {DIM}({', '.join(t['name'] for t in free_tools)}){R}")
371
+ if paid_tools:
372
+ print(f" {BYLW}B) Premium stack {R} ~${premium_cost}/mo {DIM}(adds {', '.join(t['name'] for t in paid_tools)}){R}")
373
+ else:
374
+ print(f" {BYLW}B) Premium stack {R} ~$15-50/mo {DIM}(swap to hosted/managed services){R}")
375
+ print()
376
+
377
+ try:
378
+ choice = input(f" {BOLD}Your choice (A/B):{R} ").strip().upper()
379
+ except (KeyboardInterrupt, EOFError):
380
+ print()
381
+ return "oss"
382
+
383
+ return "oss" if choice != "B" else "premium"
384
+
385
+
386
+ # ── Blueprint Generation ──────────────────────────────────────────────────────
387
+
388
+ def _build_blueprint_prompt(user_prompt: str, tools: list[dict], budget: str) -> str:
389
+ """Build the LLM prompt that generates a stack blueprint with CLAUDE.md."""
390
+ tool_list = "\n".join(
391
+ f"- {t['name']} ({t.get('category','?')}): {t.get('reason', 'core component')}"
392
+ for t in tools
393
+ )
394
+ tool_names = ", ".join(t["name"] for t in tools)
395
+ tier_label = "Free/Open Source" if budget == "oss" else "Premium/Production"
396
+
397
+ return (
398
+ f"You are a senior software architect at a top YC startup.\n"
399
+ f"Generate a detailed stack blueprint for building:\n\n"
400
+ f" {user_prompt}\n\n"
401
+ f"SELECTED STACK ({tier_label}):\n{tool_list}\n\n"
402
+ f"Produce output with EXACTLY these sections in order:\n\n"
403
+ f"## Architecture Diagram\n"
404
+ f"Draw an ASCII box diagram showing how {tool_names} connect.\n"
405
+ f"Show data flow with arrows (→). Group related services with boxes.\n"
406
+ f"Keep it 60 characters wide. Be specific to the domain (e.g., for food delivery:\n"
407
+ f"show Order Service, Rider Assignment, Customer Notifications).\n\n"
408
+ f"## Why This Stack\n"
409
+ f"For each selected tool, write exactly 1-2 sentences:\n"
410
+ f"what it does in THIS specific application, not in general.\n\n"
411
+ f"## Environment Variables\n"
412
+ f"List every environment variable needed. Format:\n"
413
+ f"- VARIABLE_NAME: what it stores and where to get it\n\n"
414
+ f"## Install\n"
415
+ f"```bash\n"
416
+ f"# One-line install command for all dependencies\n"
417
+ f"```\n\n"
418
+ f"## CLAUDE.md\n"
419
+ f"```markdown\n"
420
+ f"# <Project Name>\n\n"
421
+ f"## What This Project Does\n"
422
+ f"2 sentences describing the product — domain-specific, not generic.\n\n"
423
+ f"## Tech Stack\n"
424
+ f"For each tool: what it does in this project specifically.\n\n"
425
+ f"## Architecture\n"
426
+ f"How data flows between the services. Reference the actual domain entities.\n\n"
427
+ f"## Key Implementation Notes\n"
428
+ f"3-5 bullets on what to build first, patterns to follow, pitfalls to avoid.\n\n"
429
+ f"## Environment Variables\n"
430
+ f"Reference list of all env vars needed.\n"
431
+ f"```\n"
432
+ )
433
+
434
+
435
+ def _write_project_files(blueprint_text: str, tools: list[dict], output_dir: str) -> Path:
436
+ """
437
+ Parse the blueprint and write project starter files to output_dir/:
438
+ - CLAUDE.md (stack context for IDE like Cursor)
439
+ - .env.example
440
+ - install.sh
441
+ """
442
+ out = Path(output_dir)
443
+ out.mkdir(parents=True, exist_ok=True)
444
+
445
+ # ── CLAUDE.md ──────────────────────────────────────────────────────────
446
+ claude_match = re.search(
447
+ r'##\s*CLAUDE\.md\s*```(?:markdown)?\n([\s\S]*?)```',
448
+ blueprint_text,
449
+ re.IGNORECASE,
450
+ )
451
+ if claude_match:
452
+ claude_content = claude_match.group(1).strip()
453
+ else:
454
+ # Fallback: use the whole blueprint
455
+ claude_content = blueprint_text.strip()
456
+
457
+ (out / "CLAUDE.md").write_text(claude_content + "\n")
458
+
459
+ # ── .env.example ───────────────────────────────────────────────────────
460
+ env_match = re.search(
461
+ r'##\s*Environment Variables\s*\n([\s\S]*?)(?=\n##\s|\Z)',
462
+ blueprint_text,
463
+ )
464
+ env_lines: list[str] = []
465
+ if env_match:
466
+ for line in env_match.group(1).strip().split("\n"):
467
+ line = line.strip()
468
+ if not line:
469
+ continue
470
+ # Lines like: "- DATABASE_URL: PostgreSQL connection string"
471
+ m = re.match(r'^[-*]\s*([A-Z_][A-Z0-9_]*)\s*:\s*(.*)', line)
472
+ if m:
473
+ var_name, comment = m.group(1), m.group(2).strip()
474
+ if comment:
475
+ env_lines.append(f"# {comment}")
476
+ env_lines.append(f"{var_name}=")
477
+ env_lines.append("")
478
+ if not env_lines:
479
+ # Generic fallback from selected tools
480
+ known: dict[str, str] = {
481
+ "PostgreSQL": "DATABASE_URL=postgresql://user:password@localhost:5432/dbname",
482
+ "MongoDB": "MONGODB_URI=mongodb://localhost:27017/mydb",
483
+ "Redis": "REDIS_URL=redis://localhost:6379",
484
+ "Upstash Redis": "UPSTASH_REDIS_REST_URL=\nUPSTASH_REDIS_REST_TOKEN=",
485
+ "Stripe": "STRIPE_SECRET_KEY=sk_test_...\nSTRIPE_WEBHOOK_SECRET=whsec_...",
486
+ "Razorpay": "RAZORPAY_KEY_ID=\nRAZORPAY_KEY_SECRET=",
487
+ "SendGrid": "SENDGRID_API_KEY=SG.",
488
+ "Resend": "RESEND_API_KEY=re_",
489
+ "OpenAI API": "OPENAI_API_KEY=sk-",
490
+ "Anthropic Claude": "ANTHROPIC_API_KEY=sk-ant-",
491
+ "Auth0": "AUTH0_DOMAIN=\nAUTH0_CLIENT_ID=\nAUTH0_CLIENT_SECRET=",
492
+ "Clerk": "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=\nCLERK_SECRET_KEY=",
493
+ "Twilio": "TWILIO_ACCOUNT_SID=\nTWILIO_AUTH_TOKEN=\nTWILIO_PHONE_NUMBER=",
494
+ "Pinecone": "PINECONE_API_KEY=\nPINECONE_INDEX=",
495
+ }
496
+ env_lines.append("# Generated by namango init")
497
+ env_lines.append("")
498
+ for t in tools:
499
+ if t["name"] in known:
500
+ env_lines.append(f"# {t['name']}")
501
+ env_lines.extend(known[t["name"]].split("\n"))
502
+ env_lines.append("")
503
+
504
+ (out / ".env.example").write_text("\n".join(env_lines) + "\n")
505
+
506
+ # ── install.sh ─────────────────────────────────────────────────────────
507
+ install_match = re.search(
508
+ r'##\s*Install\s*\n```(?:bash|sh)?\n([\s\S]*?)```',
509
+ blueprint_text,
510
+ )
511
+ if install_match:
512
+ install_cmd = install_match.group(1).strip()
513
+ else:
514
+ # Generate pip install from Python tools
515
+ pip_slugs = {
516
+ "fastapi": "fastapi uvicorn[standard]",
517
+ "django": "django",
518
+ "flask": "flask",
519
+ "celery": "celery",
520
+ "redis": "redis",
521
+ "postgresql": "psycopg2-binary sqlalchemy",
522
+ "mongodb": "pymongo motor",
523
+ "langchain": "langchain langchain-openai",
524
+ "stripe": "stripe",
525
+ "sendgrid": "sendgrid",
526
+ "resend": "resend",
527
+ "pydantic": "pydantic",
528
+ "jwt": "pyjwt",
529
+ "sentry": "sentry-sdk",
530
+ }
531
+ pkgs = [pip_slugs[t["slug"]] for t in tools if t.get("slug") in pip_slugs]
532
+ install_cmd = f"pip install {' '.join(pkgs)}" if pkgs else "# Add your dependencies here"
533
+
534
+ (out / "install.sh").write_text(f"#!/bin/bash\n# Generated by namango init\nset -e\n\n{install_cmd}\n")
535
+
536
+ return out
537
+
538
+
539
+ # ── Main Pipeline ─────────────────────────────────────────────────────────────
540
+
541
+ def run_pipeline(
542
+ gateway_url: str,
543
+ api_key: str,
544
+ user_prompt: str,
545
+ output_dir: str = "namango-output",
546
+ ) -> None:
547
+ W = _term_width()
548
+ IS_TTY = sys.stdout.isatty()
549
+
550
+ title = user_prompt if len(user_prompt) <= 58 else user_prompt[:55] + "..."
551
+
552
+ # ── Header ─────────────────────────────────────────────────────────────
553
+ print()
554
+ print(f"{CYAN}╔{'═'*(W-2)}╗{R}")
555
+ print(f"{CYAN}║{BOLD}{' NAMANGO — AI STACK INTELLIGENCE':^{W-2}}{R}{CYAN}║{R}")
556
+ print(f"{CYAN}╠{'═'*(W-2)}╣{R}")
557
+ print(f"{CYAN}║{R} {BOLD}{title}{R}")
558
+ print(f"{CYAN}╚{'═'*(W-2)}╝{R}")
559
+ print()
560
+
561
+ steps_done: list[str] = []
562
+
563
+ arch = render_pipeline([], None)
564
+ print(arch)
565
+ arch_lines = len(arch.splitlines()) + 1
566
+
567
+ def redraw(active: str | None = None) -> None:
568
+ if IS_TTY:
569
+ sys.stdout.write(f"\033[{arch_lines}A")
570
+ sys.stdout.flush()
571
+ new_arch = render_pipeline(steps_done, active)
572
+ sys.stdout.write(new_arch + "\n")
573
+ sys.stdout.flush()
574
+
575
+ # ── Step 1: Intent Analysis ─────────────────────────────────────────────
576
+ redraw("intent")
577
+ time.sleep(0.4)
578
+ steps_done.append("intent")
579
+ redraw(None)
580
+ print(f"\n {BGRN}🧠 Intent:{R} {DIM}domain understood · fetching curated stack catalog{R}\n")
581
+
582
+ # ── Step 2: Stack Catalog Fetch ─────────────────────────────────────────
583
+ redraw("catalog")
584
+ sys.stdout.write(f" {BYLW}▶{R} Fetching Namango stack catalog...\n")
585
+ sys.stdout.flush()
586
+ catalog = fetch_stack_catalog(gateway_url, api_key)
587
+ all_tools_flat = _flatten_catalog(catalog)
588
+ total = len(all_tools_flat)
589
+ n_cats = len(catalog)
590
+ steps_done.append("catalog")
591
+ redraw(None)
592
+ print(f"\n {BGRN}📦 Catalog:{R} {BOLD}{total} tools{R} "
593
+ f"{DIM}across {n_cats} categories (web · db · auth · payments · deploy · ai...){R}\n")
594
+
595
+ # ── Step 3: Stack Selector ──────────────────────────────────────────────
596
+ redraw("selector")
597
+ sys.stdout.write(f" {BYLW}▶{R} Selecting optimal stack for your product...\n")
598
+ sys.stdout.flush()
599
+ selected = select_tools(gateway_url, api_key, user_prompt, catalog)
600
+ steps_done.append("selector")
601
+ redraw(None)
602
+
603
+ print(f"\n {BGRN}🎯 Selected stack:{R}\n")
604
+ for t in selected:
605
+ tier = t.get("tier", "free")
606
+ cat = t.get("category", "")
607
+ color = BGRN if tier != "paid" else BYLW
608
+ tier_badge = f"{BGRN}free{R}" if tier == "free" else (f"{BYLW}paid{R}" if tier == "paid" else f"{CYAN}freemium{R}")
609
+ print(f" {color} {t['name']:<20}{R} {DIM}{cat:<14}{R} [{tier_badge}] {DIM}{t.get('reason','')}{R}")
610
+ print()
611
+
612
+ # ── Step 4: Cost Advisor (interactive) ─────────────────────────────────
613
+ redraw("budget")
614
+ budget = ask_budget(selected)
615
+
616
+ if budget == "oss":
617
+ final_tools = [t for t in selected if t.get("tier", "free") != "paid"]
618
+ else:
619
+ final_tools = selected
620
+ if not final_tools:
621
+ final_tools = selected
622
+
623
+ steps_done.append("budget")
624
+ # Fresh pipeline box after interactive input — no cursor-up through prompt lines
625
+ print()
626
+ new_arch = render_pipeline(steps_done, None)
627
+ sys.stdout.write(new_arch + "\n")
628
+ sys.stdout.flush()
629
+ arch_lines = len(new_arch.splitlines()) + 1
630
+
631
+ stack_label = "Free / Open Source stack" if budget == "oss" else "Premium stack"
632
+ print(f"\n {BGRN}💰 Budget:{R} {BOLD}{stack_label}{R}\n")
633
+
634
+ # ── Step 5: Blueprint Builder (SSE streaming) ────────────────────────
635
+ arch_lines += 3
636
+ redraw("blueprint")
637
+ task = {
638
+ "title": title,
639
+ "prompt": _build_blueprint_prompt(user_prompt, final_tools, budget),
640
+ "model": DEFAULT_MODEL,
641
+ }
642
+ _stream_blueprint(gateway_url, api_key, task, final_tools, output_dir, steps_done, redraw)
643
+
644
+
645
+ # ── SSE Streaming (Step 5) ────────────────────────────────────────────────────
646
+
647
+ STEP_DETAIL_LINES: list[str] = []
648
+
649
+
650
+ def _record_step_detail(step: str, details: dict) -> None:
651
+ if step == "llm_routing":
652
+ model = details.get("model_id", details.get("llm", "?"))
653
+ STEP_DETAIL_LINES.append(f" {GRN}⚡ Router chose:{R} {CYAN}{model}{R}")
654
+ elif step == "generation":
655
+ in_t = details.get("input_tokens", 0)
656
+ out_t = details.get("output_tokens", 0)
657
+ lat = details.get("latency_ms", 0)
658
+ cost = details.get("cost_usd", 0.0)
659
+ STEP_DETAIL_LINES.append(
660
+ f" {GRN}✍️ Generated:{R} {BGRN}{in_t:,} in → {out_t:,} out{R}"
661
+ f" {DIM}{lat:,}ms ${cost:.4f}{R}"
662
+ )
663
+
664
+
665
+ def _stream_blueprint(
666
+ gateway_url: str,
667
+ api_key: str,
668
+ task: dict,
669
+ final_tools: list[dict],
670
+ output_dir: str,
671
+ steps_done: list[str],
672
+ redraw,
673
+ ) -> None:
674
+ W = _term_width()
675
+ full_response = ""
676
+ usage: dict = {}
677
+ start = time.time()
678
+
679
+ headers = {"Content-Type": "application/json", "X-API-Key": api_key}
680
+ body = {"prompt": task["prompt"], "preferred_model": task.get("model", DEFAULT_MODEL)}
681
+
682
+ sys.stdout.write(f" {BYLW}▶{R} Building your stack blueprint...\n\n")
683
+ sys.stdout.flush()
684
+
685
+ try:
686
+ with httpx.stream("POST", f"{gateway_url}/v1/query/stream",
687
+ json=body, headers=headers, timeout=180.0) as resp:
688
+ if resp.status_code != 200:
689
+ print(f"\n {RED}Error {resp.status_code}: {resp.read().decode()[:200]}{R}")
690
+ return
691
+
692
+ buffer = ""
693
+ for chunk in resp.iter_bytes():
694
+ buffer += chunk.decode("utf-8", errors="replace")
695
+ lines = buffer.split("\n")
696
+ buffer = lines.pop()
697
+
698
+ for line in lines:
699
+ if not line.startswith("data: "):
700
+ continue
701
+ raw = line[6:].strip()
702
+ if raw == "[DONE]":
703
+ break
704
+ try:
705
+ ev = json.loads(raw)
706
+ except json.JSONDecodeError:
707
+ continue
708
+
709
+ etype = ev.get("type")
710
+
711
+ if etype == "step_complete":
712
+ _record_step_detail(ev.get("step", ""), ev.get("details", {}))
713
+
714
+ elif etype == "token":
715
+ full_response += ev.get("text", "")
716
+
717
+ elif etype == "done":
718
+ usage = {k: ev.get(k, 0) for k in
719
+ ("input_tokens", "output_tokens", "cost_usd", "latency_ms")}
720
+ if ev.get("response"):
721
+ full_response = ev["response"]
722
+ steps_done.append("blueprint")
723
+ redraw(None)
724
+
725
+ elif etype == "error":
726
+ print(f"\n {RED}Gateway error: {ev.get('detail', '?')}{R}")
727
+ return
728
+
729
+ except httpx.ReadTimeout:
730
+ print(f"\n {RED}Timeout — gateway took too long{R}")
731
+ return
732
+ except Exception as e:
733
+ print(f"\n {RED}Connection error: {e}{R}")
734
+ return
735
+
736
+ elapsed = time.time() - start
737
+
738
+ # ── Print Blueprint ────────────────────────────────────────────────────
739
+ if full_response:
740
+ print()
741
+ print(f"{CYAN}{'═'*W}{R}")
742
+ print(f"{BOLD} 🗺️ STACK BLUEPRINT{R}")
743
+ print(f"{CYAN}{'─'*W}{R}")
744
+ print()
745
+ _pretty_print_response(full_response, W)
746
+
747
+ # ── Write Project Files ────────────────────────────────────────────────
748
+ if full_response:
749
+ out_path = _write_project_files(full_response, final_tools, output_dir)
750
+ print()
751
+ print(f"{CYAN}{'═'*W}{R}")
752
+ print(f"{BOLD} ✅ PROJECT FILES WRITTEN{R}")
753
+ print(f"{CYAN}{'─'*W}{R}")
754
+ print()
755
+ print(f" {BGRN} {out_path / 'CLAUDE.md'!s:<52}{R} {DIM}← paste into Cursor and say \"build this\"{R}")
756
+ print(f" {CYAN} {out_path / '.env.example'!s:<52}{R} {DIM}← fill in your API keys{R}")
757
+ print(f" {CYAN} {out_path / 'install.sh'!s:<52}{R} {DIM}← run to install dependencies{R}")
758
+
759
+ # ── Summary ───────────────────────────────────────────────────────────
760
+ print()
761
+ print(f"{CYAN}{'═'*W}{R}")
762
+ print(f"{BOLD} 📊 NAMANGO SUMMARY{R}")
763
+ print(f"{CYAN}{'─'*W}{R}")
764
+ print()
765
+
766
+ for line in STEP_DETAIL_LINES:
767
+ print(line)
768
+
769
+ if usage:
770
+ in_t = usage["input_tokens"]
771
+ out_t = usage["output_tokens"]
772
+ cost = usage["cost_usd"]
773
+ lat = usage["latency_ms"]
774
+ tps = out_t / (lat / 1000) if lat > 0 else 0
775
+ print()
776
+ print(f" {BOLD}Tokens {R}{BGRN}{in_t:,} in + {out_t:,} out = {in_t+out_t:,} total{R}")
777
+ print(f" {BOLD}Speed {R}{BGRN}{tps:.0f} tokens/sec{R} {DIM}({lat:,}ms gateway latency){R}")
778
+ print(f" {BOLD}Cost {R}{BGRN}${cost:.6f}{R} {DIM}(via Namango gateway){R}")
779
+ print(f" {BOLD}Wall {R}{elapsed:.1f}s end-to-end")
780
+ print()
781
+ # The value prop callout
782
+ avg_ide_cost = 0.08 # average IDE LLM stack discovery session
783
+ savings = max(0.0, avg_ide_cost - cost)
784
+ if savings > 0:
785
+ print(f" {BGRN}💡 Stack selected in {elapsed:.0f}s vs ~15min in your IDE{R}")
786
+ print(f" {BGRN} Saved ~${savings:.3f} in LLM discovery tokens{R}")
787
+
788
+ print()
789
+ print(f"{CYAN}{'─'*W}{R}")
790
+ print(f" {DIM}📈 Dashboard: https://frontend-five-theta-69.vercel.app{R}")
791
+ print(f"{CYAN}{'═'*W}{R}")
792
+ print()
793
+
794
+
795
+ # ── Response Printer ──────────────────────────────────────────────────────────
796
+
797
+ def _pretty_print_response(text: str, width: int = 72) -> None:
798
+ in_code = False
799
+ lang = ""
800
+ for line in text.split("\n"):
801
+ if line.startswith("```"):
802
+ if not in_code:
803
+ in_code = True
804
+ lang = line[3:].strip()
805
+ fence = f" {CYAN}┌─ {lang or 'code'} {'─'*(width-8-len(lang or 'code'))}┐{R}"
806
+ print(fence)
807
+ else:
808
+ in_code = False
809
+ print(f" {CYAN}└{'─'*(width-4)}┘{R}")
810
+ elif in_code:
811
+ hl = line
812
+ for kw in ("def ", "class ", "async ", "await ", "import ", "from ", "return "):
813
+ hl = hl.replace(kw, f"{MAG}{kw}{R}")
814
+ for kw in ("if ", "else:", "elif ", "for ", "while ", "try:", "except ", "with ", "raise "):
815
+ hl = hl.replace(kw, f"{BBLU}{kw}{R}")
816
+ print(f" {DIM}│{R} {hl}")
817
+ else:
818
+ if line.startswith("## "):
819
+ print(f"\n {BOLD}{BCYAN}{line[3:]}{R}")
820
+ elif line.startswith("# "):
821
+ print(f"\n {BOLD}{WHT}{line[2:]}{R}")
822
+ elif line.startswith("**") and line.endswith("**"):
823
+ print(f" {BOLD}{line[2:-2]}{R}")
824
+ elif line.startswith("- ") or line.startswith("* "):
825
+ print(f" {CYAN}•{R} {line[2:]}")
826
+ else:
827
+ if len(line) > width - 4:
828
+ for wrapped in textwrap.wrap(line, width - 4):
829
+ print(f" {wrapped}")
830
+ else:
831
+ print(f" {line}")
832
+
833
+
834
+ # ── CLI Entry Point ───────────────────────────────────────────────────────────
835
+
836
+ def main() -> None:
837
+ parser = argparse.ArgumentParser(
838
+ prog="namango",
839
+ description="Namango — AI Stack Intelligence for Product Builders",
840
+ formatter_class=argparse.RawDescriptionHelpFormatter,
841
+ epilog=textwrap.dedent("""\
842
+ Examples:
843
+ namango init "build a customer support helpdesk for a food delivery app"
844
+ namango init # interactive prompt
845
+ namango init "my idea" --output ./my-stack
846
+ """),
847
+ )
848
+
849
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
850
+
851
+ # ── namango init ────────────────────────────────────────────────────────
852
+ init_p = subparsers.add_parser(
853
+ "init",
854
+ help="Generate a stack blueprint for your product idea",
855
+ description="Analyze your product idea, select the best tech stack, and write CLAUDE.md + starter files.",
856
+ )
857
+ init_p.add_argument(
858
+ "prompt", nargs="?", default=None,
859
+ help="What to build — e.g. 'customer support helpdesk for a food delivery app'",
860
+ )
861
+ init_p.add_argument("--url", default=DEFAULT_GATEWAY, help="Gateway base URL")
862
+ init_p.add_argument("--key", default=DEFAULT_KEY, help="Gateway API key")
863
+ init_p.add_argument("--output", default="namango-output", metavar="DIR",
864
+ help="Output directory for CLAUDE.md and project files (default: ./namango-output)")
865
+
866
+ args = parser.parse_args()
867
+
868
+ # Support bare `namango "prompt"` as alias for `namango init "prompt"`
869
+ if args.command is None:
870
+ # Re-parse with init as default
871
+ args = init_p.parse_args(sys.argv[1:])
872
+ args.url = DEFAULT_GATEWAY
873
+ args.key = DEFAULT_KEY
874
+ args.output = "namango-output"
875
+
876
+ # Interactive prompt if not provided
877
+ prompt = getattr(args, "prompt", None)
878
+ if not prompt:
879
+ print(f"\n{CYAN} Namango — AI Stack Intelligence{R}")
880
+ print(f" {DIM}Describe the product you want to build. Be specific about the domain.{R}")
881
+ print(f" {DIM}Example: customer support helpdesk for a Zomato-like food delivery app{R}\n")
882
+ try:
883
+ prompt = input(f" {BOLD}What do you want to build?{R} ").strip()
884
+ except (KeyboardInterrupt, EOFError):
885
+ print()
886
+ return
887
+ if not prompt:
888
+ return
889
+
890
+ url = getattr(args, "url", DEFAULT_GATEWAY)
891
+ key = getattr(args, "key", DEFAULT_KEY)
892
+ output = getattr(args, "output", "namango-output")
893
+
894
+ run_pipeline(url, key, prompt, output)
895
+
896
+
897
+ if __name__ == "__main__":
898
+ main()
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: namango
3
+ Version: 0.3.0
4
+ Summary: Namango — AI Stack Intelligence for Product Builders
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: httpx
@@ -0,0 +1,10 @@
1
+ pyproject.toml
2
+ namango/__init__.py
3
+ namango/__main__.py
4
+ namango/cli.py
5
+ namango.egg-info/PKG-INFO
6
+ namango.egg-info/SOURCES.txt
7
+ namango.egg-info/dependency_links.txt
8
+ namango.egg-info/entry_points.txt
9
+ namango.egg-info/requires.txt
10
+ namango.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ namango = namango.cli:main
@@ -0,0 +1 @@
1
+ httpx
@@ -0,0 +1,3 @@
1
+ build
2
+ dist
3
+ namango
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "namango"
7
+ version = "0.3.0"
8
+ description = "Namango — AI Stack Intelligence for Product Builders"
9
+ requires-python = ">=3.8"
10
+ dependencies = ["httpx"]
11
+
12
+ [project.scripts]
13
+ namango = "namango.cli:main"
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+