import type { APIRoute } from 'astro'; import { spawn } from 'child_process'; const SYSTEM_PROMPT = `You are a meditation guide. Generate one short, calming guidance message for a meditation session. Rules: - One sentence only, 8-20 words - Gentle, present-tense, second person ("you") - No questions, no exclamation marks - Match the phase: "start" = settling in, "middle" = sustained awareness, "end" = returning - Never repeat common meditation cliches verbatim - Vary your language — draw from mindfulness, somatic awareness, nature imagery, gratitude, breath, stillness - Output only the guidance sentence, nothing else — no quotes, no labels, no extra text`; const lastCall = new Map(); export const POST: APIRoute = async ({ request }) => { const body = await request.json() as { phase: string; progress: number; duration: number; streak: number; sessionCount: number; sessionId: string; }; // Rate limiting: 20s minimum between calls per session const now = Date.now(); const last = lastCall.get(body.sessionId) || 0; if (now - last < 20_000) { return new Response(JSON.stringify({ error: "rate limited" }), { status: 429, headers: { 'Content-Type': 'application/json' }, }); } lastCall.set(body.sessionId, now); // Prune stale entries if (lastCall.size > 1000) { const cutoff = now - 3_600_000; for (const [k, v] of lastCall) { if (v < cutoff) lastCall.delete(k); } } const contextParts = [`Phase: ${body.phase}`, `Progress: ${Math.round(body.progress * 100)}%`]; if (body.duration) contextParts.push(`Session length: ${body.duration} minutes`); if (body.streak > 1) contextParts.push(`User is on a ${body.streak}-day streak`); if (body.sessionCount > 1) contextParts.push(`This is their session #${body.sessionCount}`); const prompt = `${SYSTEM_PROMPT}\n\n${contextParts.join(". ")}.`; return new Promise((resolve) => { const proc = spawn('claude', ['-p', '--model', 'claude-haiku-4-5-20251001'], { stdio: ['pipe', 'pipe', 'pipe'], }); let output = ''; let stderr = ''; proc.stdout.on('data', (chunk: Buffer) => { output += chunk.toString(); }); proc.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); proc.on('close', (code) => { const text = output.trim().replace(/^["']|["']$/g, ''); if (code !== 0 || !text) { console.error('Meditation guidance error:', stderr || 'empty output'); resolve(new Response(JSON.stringify({ error: 'generation failed' }), { status: 500, headers: { 'Content-Type': 'application/json' }, })); return; } resolve(new Response(JSON.stringify({ guidance: text }), { headers: { 'Content-Type': 'application/json' }, })); }); proc.on('error', (err) => { console.error('Failed to spawn claude:', err); resolve(new Response(JSON.stringify({ error: 'generation failed' }), { status: 500, headers: { 'Content-Type': 'application/json' }, })); }); proc.stdin.write(prompt); proc.stdin.end(); }); };