Files
sparkvault/server/idea_generator.js
T

710 lines
28 KiB
JavaScript

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()
};
}