import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { spawn } from "node:child_process"; import { GoogleGenAI } from "@google/genai"; import { drawCards } from "./whack_cards.js"; import { isCodexConfigured } from "./nano_banana.js"; export const DEFAULT_USER_BACKGROUND = `Creative technologist, homelabber, and maker (vloser.us). Interests & Background: - Vintage screenprint apparel, heavyweight crewneck tees (5-inch left-chest emblems & full-back prints), die-cut vinyl stickers, national park & alpine ski/mountain aesthetics, specialty espresso & craft brewing culture. - Self-hosted Docker homelab infrastructure (Traefik, Gitea, Homepage, SQLite, AI agents, local automation, media & art tools). - Tactile hardware & ambient displays (ESP32, E-Ink dashboards, home telemetry, vinyl/audio archives, photography & curated digital art galleries). - Lateral thinking, Roger von Oech's Creative Whack Pack, and playful weekend builds that ship fast.`; export const CURATED_IDEA_TEMPLATES = [ { title: "Alpine Telemetry Club — Heavyweight Left-Chest & Back Print Tee", elevator_pitch: "Vintage 1970s mountain weather-station tee featuring a 5-inch left-chest anemometer badge and a full-back topographic isobar map.", category: "creative", tags: ["tshirt", "merch", "screenprint", "alpine", "weather"], rating_excitement: 5, rating_impact: 4, rating_ease: 5, notes_markdown: "### Apparel & Print Concept\n- **Garment**: 6.5oz Heavyweight Vintage Cream (`#F5E8D3`) or Forest Pine (`#1E3F2B`) crewneck.\n- **Front Print**: 5-inch left-chest circular patch reading `HIGH ALTITUDE TELEMETRY DIVISION • EST. 1978`.\n- **Back Print**: 3-color spot screenprint showing elevation contour lines, barometric pressure curves, and a retro satellite dish icon.", checklist: [ "Mock up 5-inch left-chest emblem in Visual Mockup Studio", "Test Burnt Orange + Espresso Brown + Gold ink palette on Cream tee", "Export print-ready high-res PNG" ], pros: ["Bridges geeky homelab telemetry with classic outdoor heritage apparel", "Works great as a 3-color screenprint"], cons: ["Fine isobar contour lines need careful halftone/stroke weight"] }, { title: "Self-Hosted Homelab Rack Patch & Die-Cut Sticker Series", elevator_pitch: "Collectible merit-badge style vinyl stickers and embroidered patches for homelab milestones ('Zero Downtime Club', 'DNS Survivor', 'Docker Compose Alchemist').", category: "creative", tags: ["sticker", "merch", "homelab", "docker", "illustration"], rating_excitement: 5, rating_impact: 4, rating_ease: 5, notes_markdown: "### Series Concept\n- Design a set of 4 vintage scout/national-park style badges celebrating self-hosting and homelab triumphs:\n 1. **It Was DNS All Along** (owl holding a tangled ethernet cable)\n 2. **Container Ship Captain** (vintage tugboat carrying glowing Docker containers)\n 3. **3-2-1 Backup Ranger** (forest lookout tower with tape reels)\n- Great for laptop lids, server rack doors, or left-chest pocket tees.", checklist: [ "Generate 'It Was DNS All Along' die-cut sticker mockup", "Test white vinyl contour border on dark slate surface", "Try left-chest t-shirt variation" ], pros: ["Instant hit with self-hosting/homelab community", "Low unit cost to print die-cut vinyl runs"], cons: ["Need crisp bold typography that reads well at 3-inch sticker scale"] }, { title: "Sub-Zero Cold Brew & Vinyl Listening Society Crewneck", elevator_pitch: "Retro Japanese jazz-kissa (listening bar) inspired t-shirt combining a spinning 45rpm record adapter with an iced pour-over carafe.", category: "creative", tags: ["tshirt", "merch", "coffee", "vinyl", "retro"], rating_excitement: 4, rating_impact: 4, rating_ease: 5, notes_markdown: "### Design Notes\n- Inspired by Tokyo kissaten hi-fi coffee lounges.\n- **Colorway**: Midnight Navy (`#1B2A4A`) tee with cream and warm amber puff/plastisol ink.\n- **Graphic**: A geometric 45rpm vinyl spindle morphing into ripples of espresso in a ceramic mug.", checklist: [ "Generate Midnight Navy t-shirt mockup in Studio", "Experiment with vintage mono-spaced typography in Canvas Compositor" ], pros: ["Combines two tactile daily rituals (coffee + music)", "Minimalist 2-color screenprint"], cons: ["Dark garment requires opaque cream underbase"] }, { title: "Homelab Service Health Receipt Printer (Thermal Desk Totem)", elevator_pitch: "A retro POS thermal receipt printer on your desk that prints a physical morning 'Daily Dispatch' ticket with server stats, weather, and 1 random SparkVault idea + Whack Pack card.", category: "project", tags: ["hardware", "homelab", "thermal-printer", "automation", "esp32"], rating_excitement: 5, rating_impact: 5, rating_ease: 4, notes_markdown: "### Morning Ritual Hardware\n- Uses a $25 ESC/POS USB or Wi-Fi 58mm thermal receipt printer.\n- Every morning at 7:30 AM (or when a big arcade button is pressed), it queries `sparkvault.vloser.us/api/whack-cards/draw?mode=collider` and `vloser.us` container status.\n- Prints a crisp tear-off physical ticket with:\n - Today's Creative Whack Pack Card & prompt question\n - One stale Spark from your vault to think about over coffee\n - Homelab uptime summary", checklist: [ "Write Node/Python ESC/POS formatter script", "Hook up `/api/whack-cards/draw?mode=collider` endpoint", "Design dithered 1-bit header logo for the thermal roll" ], pros: ["Zero-ink thermal paper is pennies per roll", "Brings digital ideas into the physical world every morning"], cons: ["Thermal paper fades if left in direct sunlight"] }, { title: "AI Frame Curator — Dynamic Art & Mockup Rotator for Wall Display", elevator_pitch: "Self-hosted web frame mode that turns any tablet or wall TV into a curated gallery cycling through your best art, photography, and active SparkVault mockups with QR codes to vote/comment.", category: "project", tags: ["art-gallery", "homelab", "docker", "display", "ui"], rating_excitement: 4, rating_impact: 4, rating_ease: 4, notes_markdown: "### Architecture\n- Lightweight kiosk view with museum-style matte framing, subtle ambient color matching, and museum placard metadata in the corner.\n- Guests or family can scan a tiny corner QR code on the LAN to give a 🔥 vote or drop a note directly into SparkVault.", checklist: [ "Mock up the gallery kiosk UI in Mockup Studio (`app-ui` preset)", "Add museum placard typography overlay", "Deploy behind Traefik on `vloser.us`" ], pros: ["Showcases art & t-shirt mockups passively around the house", "Uses existing homelab stack"], cons: ["Needs wake-lock / auto-dimming schedule for night hours"] }, { title: "Midnight Maintenance Crew — Glow-in-the-Dark Homelab Tee", elevator_pitch: "Vintage utility-worker style t-shirt for late-night server tinkerers: 'MIDNIGHT MAINTENANCE CREW • VLOSER LABS • IF IT AIN'T BROKE, UPGRADE IT'.", category: "creative", tags: ["tshirt", "merch", "homelab", "screenprint", "vintage"], rating_excitement: 5, rating_impact: 4, rating_ease: 5, notes_markdown: "### Garment & Ink\n- **Garment**: Vintage Black (`#1C1B1A`) heavyweight tee.\n- **Graphic**: Retro mascot of a raccoon wearing a headlamp holding a wrench and a glowing fiber-optic patch cable.\n- **Placement**: 5-inch left-chest badge + oversized vintage back graphic.", checklist: [ "Generate raccoon mascot screenprint graphic in Mockup Studio", "Test both Left-Chest and Full-Front placements" ], pros: ["Relatable self-deprecating homelab humor", "Looks awesome on Vintage Black or Heather Grey"], cons: ["Multi-color mascot illustration benefits from DTG or simulated process screenprint"] } ]; function extractJsonObject(rawText) { if (!rawText) return null; const cleaned = String(rawText) .replace(/```json/gi, "") .replace(/```/g, "") .trim(); const match = cleaned.match(/\{[\s\S]*\}/); if (!match) return null; try { return JSON.parse(match[0]); } catch { return null; } } async function generateIdeaViaCodex(prompt) { const { stdout, exitCode } = await new Promise((resolve, reject) => { const child = spawn( "codex", [ "exec", "-m", process.env.CODEX_MODEL || "gpt-5.6-sol", "--skip-git-repo-check", "--json", prompt ], { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } } ); let out = ""; child.stdout.on("data", (d) => { out += d.toString(); }); child.on("error", reject); child.on("close", (code) => resolve({ stdout: out, exitCode: code })); }); let lastAgentText = ""; for (const line of stdout.split("\n")) { const trimmed = line.trim(); if (!trimmed.startsWith("{")) continue; try { const evt = JSON.parse(trimmed); if (evt.type === "item.completed" && evt.item?.type === "agent_message") { lastAgentText = evt.item.text; } } catch { // ignore } } const parsed = extractJsonObject(lastAgentText); if (!parsed || (!parsed.title && !Array.isArray(parsed.suggestions))) { throw new Error(`Codex exited (${exitCode}) without returning valid JSON.`); } return parsed; } async function generateIdeaViaGemini(apiKey, prompt) { const ai = new GoogleGenAI({ apiKey }); const interaction = await ai.interactions.create({ model: "gemini-3.1-flash-lite", input: prompt }); const parsed = extractJsonObject(interaction.output_text); if (!parsed || (!parsed.title && !Array.isArray(parsed.suggestions))) { throw new Error("Gemini completed without returning valid JSON."); } return parsed; } /** * Generates a fresh, personalized idea tailored to the user's background, * existing vault themes, and a randomly drawn Creative Whack Pack card. */ export async function generatePersonalizedRandomIdea({ userBackground = DEFAULT_USER_BACKGROUND, categoryFocus = "any", existingTitles = [], geminiApiKey = "" }) { const card = drawCards({ count: 1 })[0]; const categoryInstruction = categoryFocus && categoryFocus !== "any" ? `Focus specifically on the "${categoryFocus}" category (${ categoryFocus === "creative" ? "a graphic t-shirt, apparel, sticker, poster, or tactile merch design" : categoryFocus === "project" ? "a self-hosted app, homelab tool, hardware/E-Ink build, or creative utility" : categoryFocus }).` : "Pick either a standout graphic t-shirt/merch concept ('creative') OR a clever homelab/hardware/creative-tech project ('project')."; const prompt = [ "You are an imaginative creative partner generating a fresh, specific, high-craft idea for a maker's SparkVault.", `USER BACKGROUND & TASTES:\n${userBackground || DEFAULT_USER_BACKGROUND}`, `CREATIVE WHACK PACK CATALYST CARD DRAWN:\nCard #${card.number} (${card.suitLabel}): "${card.title}" — ${card.promptQuestion}`, existingTitles.length > 0 ? `EXISTING IDEAS IN VAULT (do NOT repeat these, invent something distinctly new):\n- ${existingTitles.slice(0, 20).join("\n- ")}` : "", categoryInstruction, " Do NOT run any shell commands or tools. Reply ONLY with a single raw JSON object (no markdown fences) matching this exact schema:", JSON.stringify({ title: "Catchy, specific idea title", elevator_pitch: "Compelling one-sentence pitch explaining what makes it awesome", category: "creative or project or research or spark", tags: ["tag1", "tag2", "tag3"], notes_markdown: "### Concept & Specs\n- Detailed markdown notes (if apparel: blank color, ink palette, 5-inch left-chest vs full-front; if project: stack, hardware/UI, and why it's fun)", checklist: ["Concrete step 1", "Concrete step 2", "Concrete step 3"], pros: ["Pro 1", "Pro 2"], cons: ["Constraint or risk 1"], whack_reflection: `How Card #${card.number} (${card.title}) shaped this idea` }) ] .filter(Boolean) .join("\n\n"); let rawIdea = null; let generatorUsed = "Curated Personalized Generator"; // 1. Try OpenAI Codex (if authenticated) if (isCodexConfigured()) { try { rawIdea = await generateIdeaViaCodex(prompt); generatorUsed = "OpenAI Codex (gpt-5.6-sol)"; } catch (err) { console.warn("Codex idea generation fallback:", err.message); } } // 2. Try Gemini 3.1 Flash-Lite (works on Free Tier!) if (!rawIdea && geminiApiKey) { try { rawIdea = await generateIdeaViaGemini(geminiApiKey, prompt); generatorUsed = "Gemini 3.1 Flash-Lite"; } catch (err) { console.warn("Gemini idea generation fallback:", err.message); } } // 3. Curated fallback remix if offline / no AI provider if (!rawIdea) { const unused = CURATED_IDEA_TEMPLATES.filter( (t) => !existingTitles.some((ex) => ex.toLowerCase() === t.title.toLowerCase()) ); const pool = unused.length > 0 ? unused : CURATED_IDEA_TEMPLATES; const chosen = pool[Math.floor(Math.random() * pool.length)]; rawIdea = { ...chosen, whack_reflection: `Sparked with Creative Whack Pack Card #${card.number} (${card.title}): ${card.promptQuestion}` }; } return { card, generatorUsed, ideaPayload: { title: rawIdea.title || "Untitled Spark", elevator_pitch: rawIdea.elevator_pitch || "", category: ["creative", "project", "spark", "research"].includes(rawIdea.category) ? rawIdea.category : "creative", status: "spark", rating_excitement: 4, rating_impact: 4, rating_ease: 4, inspired_by_card: card.number, tags: Array.isArray(rawIdea.tags) ? rawIdea.tags : ["ai-spark"], notes_markdown: rawIdea.notes_markdown || "", checklist: (Array.isArray(rawIdea.checklist) ? rawIdea.checklist : []).map((txt, idx) => ({ id: `chk-${Date.now()}-${idx}`, text: typeof txt === "string" ? txt : txt.text || "", done: false })), pros: Array.isArray(rawIdea.pros) ? rawIdea.pros : [], cons: Array.isArray(rawIdea.cons) ? rawIdea.cons : [], whack_history: [ { id: `whack-${Date.now()}`, cardNumber: card.number, cardTitle: card.title, suit: card.suit, suitLabel: card.suitLabel, promptQuestion: card.promptQuestion, reflection: rawIdea.whack_reflection || `Generated using Card #${card.number} (${card.title}) + personal background profile.`, createdAt: new Date().toISOString() } ] } }; } /** * Generates 6 creative name / title & slogan suggestions for a given idea * across distinct naming angles (Punchy Brand, Vintage Club/Badge, Clever/Witty, * Minimalist Editorial, Graphic Slogan, Technical/Maker). */ export async function generateNameSuggestions({ idea, userBackground = DEFAULT_USER_BACKGROUND, geminiApiKey = "" }) { const ideaTextBlob = [idea.title || "", idea.elevator_pitch || "", ...(idea.tags || [])].join(" "); const isTShirt = /\b(t-?shirt|tee\b|shirt|apparel|crewneck|garment|hoodie|merch|wearable)\b/i.test(ideaTextBlob); const angleGuidance = isTShirt ? "Generate 6 distinct, memorable name/title suggestions for this apparel/t-shirt idea across different naming angles (e.g., Vintage Club/Crest, Punchy Brand Name, Witty/Clever, Minimalist, Screenprint Headline + Sub-tagline, Outfitter Badge)." : "Generate 6 distinct, memorable name/title suggestions for this idea across different naming angles (e.g., Punchy Brand Name, Maker/Project Codename, Witty/Clever, Minimalist Editorial, Descriptive Product Name, Bold Hook). Do NOT frame it as a t-shirt or apparel slogan."; const prompt = [ "You are a sharp creative director, product namer, and brand strategist.", `IDEA TO NAME:\n- Current Title: ${idea.title}\n- Elevator Pitch: ${idea.elevator_pitch || ""}\n- Category: ${idea.category}\n- Tags: ${(idea.tags || []).join(", ")}\n- Notes: ${(idea.notes_markdown || "").slice(0, 600)}`, `USER BACKGROUND & TASTES:\n${userBackground || DEFAULT_USER_BACKGROUND}`, angleGuidance, "Do NOT run any shell commands or tools. Reply ONLY with a single raw JSON object (no markdown fences) matching this exact schema:", JSON.stringify({ suggestions: [ { name: "Suggested Idea Title or Brand Name", tagline: "Short punchy subtitle or one-line hook", style: isTShirt ? "Angle badge (e.g. Vintage Club, Punchy Brand, Witty, Minimalist, Tee Slogan, Outfitter)" : "Angle badge (e.g. Punchy Brand, Codename, Witty, Minimalist, Product Name, Bold Hook)" } ] }) ].join("\n\n"); let resultObj = null; let generatorUsed = "Smart Name Generator"; if (isCodexConfigured()) { try { resultObj = await generateIdeaViaCodex(prompt); generatorUsed = "OpenAI Codex (gpt-5.6-sol)"; } catch (err) { console.warn("Codex name suggestion fallback:", err.message); } } if (!resultObj && geminiApiKey) { try { resultObj = await generateIdeaViaGemini(geminiApiKey, prompt); generatorUsed = "Gemini 3.1 Flash-Lite"; } catch (err) { console.warn("Gemini name suggestion fallback:", err.message); } } if (!resultObj || !Array.isArray(resultObj.suggestions) || resultObj.suggestions.length === 0) { const coreWords = String(idea.title || "Spark") .replace(/—.*$/, "") .replace(/[^\w\s-]/g, "") .trim(); const shortCore = coreWords.split(/\s+/).slice(0, 3).join(" ") || "Alpine Spark"; resultObj = { suggestions: [ { name: `${shortCore} Society`, tagline: `EST. 1984 • HIGH ALTITUDE DIVISION`, style: "Vintage Club" }, { name: `The ${shortCore} Dept.`, tagline: `FIELD TESTED & SELF-HOSTED`, style: "Heritage Badge" }, { name: `${shortCore} — Heavyweight Edition`, tagline: `BUILT FOR LATE NIGHTS & EARLY SUMMITS`, style: "Apparel / Merch" }, { name: `Project ${shortCore.split(" ")[0] || "Pulse"}`, tagline: `ZERO FRICTION • LOCAL FIRST`, style: "Maker Codename" }, { name: `${shortCore} Co. Outpost`, tagline: `CRAFTED IN COLORADO • NO SHORTCUTS`, style: "Outfitter Crest" }, { name: ` Midnight ${shortCore} Club`, tagline: `IF IT AIN'T BROKE, UPGRADE IT`, style: "Witty / Homelab" } ] }; } return { generatorUsed, suggestions: resultObj.suggestions.slice(0, 6) }; } export const DEV_HANDOFF_TARGETS = { antigravity: { id: "antigravity", label: "🛸 Google Antigravity", protocol: "antigravity://", defaultModel: "gemini-3.8-flash", models: [ { id: "gemini-4-argon", label: "Gemini 4 Argon (Medium)" }, { id: "gemini-3.8-flash", label: "Gemini 3.8 Flash (Medium)" }, { id: "claude-opus-5.5", label: "Claude Opus 5.5 (Medium)" }, { id: "claude-sonnet-5.5", label: "Claude Sonnet 5.5 (Medium)" }, { id: "gpt-oss-120b", label: "GPT-OSS 120B (Medium)" } ] }, codex: { id: "codex", label: "🤖 OpenAI Codex", protocol: "codex://threads/new", defaultModel: "gpt-5.6-sol", models: [ { id: "gpt-5.6-sol", label: "GPT-5.6-Sol (Recommended — Active Codex Default)" }, { id: "gpt-5.3-codex", label: "GPT-5.3-Codex (Specialized Agentic Coding)" }, { id: "gpt-5.4", label: "GPT-5.4 (Flagship Reasoning & Implementation)" }, { id: "o3", label: "OpenAI o3 (Deep Architecture & Debugging)" }, { id: "codex-mini-latest", label: "Codex Mini (Fast Lightweight Execution)" } ] } }; export function slugifyIdeaTitle(title, id = "") { const base = String(title || "spark") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") .slice(0, 36); return base || `spark-${id || "app"}`; } export function buildDevelopmentSpecMarkdown({ idea, targetApp = "antigravity", model = "", includeHomelab = true, extraInstructions = "", publicBaseUrl = "https://sparkvault.vloser.us", userBackground = DEFAULT_USER_BACKGROUND }) { const targetMeta = DEV_HANDOFF_TARGETS[targetApp] || DEV_HANDOFF_TARGETS.antigravity; const chosenModel = model || targetMeta.defaultModel; const slug = slugifyIdeaTitle(idea.title, idea.id); const lines = [ `# 🚀 Development Handoff Specification: ${idea.title}`, ``, `- **Target Environment**: ${targetMeta.label}`, `- **Selected AI Model**: \`${chosenModel}\``, `- **Spark ID**: #${idea.id} (\`${slug}\`)`, `- **Category**: \`${idea.category}\` | **Current Stage**: \`${idea.status}\` | **Priority Score**: ${idea.composite_score}/100`, `- **Ratings**: 🔥 Excitement ${idea.rating_excitement}/5 • 🎯 Impact ${idea.rating_impact}/5 • ⚡ Ease ${idea.rating_ease}/5`, `- **Tags**: ${(idea.tags || []).map((t) => `#${t}`).join(", ") || "None"}`, `` ]; if (idea.elevator_pitch) { lines.push(`## 💡 Elevator Pitch`, idea.elevator_pitch, ``); } if (idea.notes_markdown) { lines.push(`## 📝 Detailed Specs, Brainstorm & Notes`, idea.notes_markdown, ``); } if (idea.checklist && idea.checklist.length > 0) { lines.push( `## ✅ Action Checklist & Requirements`, ...idea.checklist.map((c) => `- [${c.done ? "x" : " "}] ${c.text}`), `` ); } if ((idea.pros && idea.pros.length > 0) || (idea.cons && idea.cons.length > 0)) { lines.push(`## ⚖️ Pros, Value Props & Constraints`); if (idea.pros?.length) { lines.push(`### Why It Works (Pros)`, ...idea.pros.map((p) => `- ✅ ${p}`), ``); } if (idea.cons?.length) { lines.push(`### Risks & Constraints (Cons)`, ...idea.cons.map((c) => `- ⚠️ ${c}`), ``); } } if (idea.whack_history && idea.whack_history.length > 0) { lines.push( `## 🃏 Creative Direction & Lateral Twists`, ...idea.whack_history.map( (w) => `- **Card #${w.cardNumber} (${w.cardTitle})**: ${w.reflection}` ), `` ); } if (idea.mockups && idea.mockups.length > 0) { lines.push( `## 🖼️ Visual Mockups & Design Assets`, ...idea.mockups.map((m) => { const fullUrl = m.imageUrl?.startsWith("http") ? m.imageUrl : `${publicBaseUrl}${m.imageUrl}`; return `- **${m.title}** (\`${m.template || "mockup"}\`, generated via \`${m.modelUsed || "AI"}\`): ${fullUrl}${m.prompt ? `\n - Prompt: *${m.prompt}*` : ""}`; }), `` ); } if (idea.attachments && idea.attachments.length > 0) { lines.push( `## 📎 Attached Reference Notes & Images`, ...idea.attachments.map((a) => { const imgPart = a.imageUrl ? ` — Image: ${a.imageUrl.startsWith("http") ? a.imageUrl : `${publicBaseUrl}${a.imageUrl}`}` : ""; const srcPart = a.sourceUrl ? ` (Source: ${a.sourceUrl})` : ""; const txtPart = a.textContent ? `\n > ${a.textContent.replace(/\n/g, "\n > ")}` : ""; return `- **[${a.type.toUpperCase()}] ${a.title}**${imgPart}${srcPart}${txtPart}`; }), `` ); } if (idea.links && idea.links.length > 0) { lines.push( `## 🔗 Reference Links`, ...idea.links.map((l) => `- [${l.title || l.url}](${l.url})`), `` ); } if (userBackground) { lines.push(`## 👤 Creator Background & Preferences`, userBackground, ``); } if (includeHomelab) { lines.push( `## 🛠️ Target Architecture & Homelab Deployment Requirements`, `1. Build a clean, self-contained full-stack web application (or service) for **${idea.title}** (\`${slug}\`).`, `2. Create/push a Gitea repository at \`https://gitea.vloser.us/chrisf/${slug}\`.`, `3. Deploy via Docker Compose on \`vloser\` (\`/home/chrisf/docker/${slug}\`) with persistent storage under \`/home/chrisf/docker/appdata/${slug}\`.`, `4. Expose via Traefik dynamic config at \`https://${slug}.vloser.us\` and add an entry with a custom SVG icon to Homepage (\`/home/chrisf/docker/appdata/homepage/services.yaml\`).`, `5. Include mobile-responsive UI styling and automated verification tests.`, `` ); } if (extraInstructions && extraInstructions.trim()) { lines.push(`## 🎯 Additional Development Instructions`, extraInstructions.trim(), ``); } return lines.join("\n"); } export async function executeDevelopmentHandoff({ idea, targetApp = "antigravity", model = "", customPrompt = "", includeHomelab = true, extraInstructions = "", executeServerCodex = false, publicBaseUrl = "https://sparkvault.vloser.us", userBackground = DEFAULT_USER_BACKGROUND, workspacesRootDir }) { const targetMeta = DEV_HANDOFF_TARGETS[targetApp] || DEV_HANDOFF_TARGETS.antigravity; const chosenModel = model || targetMeta.defaultModel; const slug = slugifyIdeaTitle(idea.title, idea.id); const compiledSpec = customPrompt && customPrompt.trim() ? customPrompt.trim() : buildDevelopmentSpecMarkdown({ idea, targetApp, model: chosenModel, includeHomelab, extraInstructions, publicBaseUrl, userBackground }); let workspaceDir = ""; if (workspacesRootDir) { workspaceDir = path.join(workspacesRootDir, `${idea.id}-${slug}`); fs.mkdirSync(workspaceDir, { recursive: true }); fs.writeFileSync(path.join(workspaceDir, "SPEC.md"), compiledSpec, "utf8"); fs.writeFileSync( path.join(workspaceDir, "spark-context.json"), JSON.stringify({ idea, targetApp, model: chosenModel, createdAt: new Date().toISOString() }, null, 2), "utf8" ); } const specUrl = `${publicBaseUrl}/api/ideas/${idea.id}/spec.md?target=${encodeURIComponent(targetApp)}&model=${encodeURIComponent(chosenModel)}`; // Build OS Deep Link URL for the corresponding desktop application let deepLinkUrl = ""; let cliCommand = ""; if (targetApp === "codex") { // Keep URL prompt within safe Windows protocol command-line limits (~1600 chars) const compactPrompt = compiledSpec.length <= 1500 ? compiledSpec : `${compiledSpec.slice(0, 1350)}\n\n[Full specification copied to clipboard & hosted at: ${specUrl}]`; const codexUrl = new URL("codex://threads/new"); codexUrl.searchParams.set("mode", "codex"); codexUrl.searchParams.set("prompt", compactPrompt); deepLinkUrl = codexUrl.toString(); cliCommand = `codex -m ${chosenModel} "Read SPEC.md and implement ${idea.title}"`; } else { // Antigravity desktop deep link + CLI command deepLinkUrl = `antigravity://new?model=${encodeURIComponent(chosenModel)}&sparkId=${idea.id}`; cliCommand = `agy --model ${chosenModel}`; } let codexExecution = null; if (targetApp === "codex" && executeServerCodex && isCodexConfigured()) { try { const reportFile = workspaceDir ? path.join(workspaceDir, "CODEX_PLAN.md") : path.join(os.tmpdir(), `codex-plan-${idea.id}-${Date.now()}.md`); const args = [ "exec", "-m", chosenModel, "--skip-git-repo-check", "-o", reportFile, `You are an autonomous senior engineer. Review the following SparkVault project specification and produce a complete implementation blueprint, file structure, and starter code plan:\n\n${compiledSpec}` ]; if (workspaceDir) { args.splice(4, 0, "-C", workspaceDir); } await new Promise((resolve, reject) => { const child = spawn("codex", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } }); child.on("error", reject); child.on("close", () => resolve()); }); if (fs.existsSync(reportFile)) { const planText = fs.readFileSync(reportFile, "utf8").trim(); if (planText) { codexExecution = { ran: true, model: chosenModel, output: planText.slice(0, 6000) }; } } } catch (err) { codexExecution = { ran: false, error: err.message }; } } const modelObj = targetMeta.models.find((m) => m.id === chosenModel); const modelLabel = modelObj ? modelObj.label : chosenModel; return { targetApp, targetLabel: targetMeta.label, model: chosenModel, modelLabel, slug, specMarkdown: compiledSpec, specUrl, deepLinkUrl, cliCommand, workspaceDir, codexExecution, submittedAt: new Date().toISOString() }; }