import { Hono } from "hono"; import { serveStatic } from "@hono/node-server/serve-static"; import { serve } from "@hono/node-server"; import Anthropic from "@anthropic-ai/sdk"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; const app = new Hono(); // Use Claude Code subscription credentials // Priority: ANTHROPIC_API_KEY env var > ~/.claude/.credentials.json function getApiKey(): string { if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY; try { const credsPath = join(homedir(), ".claude", ".credentials.json"); const creds = JSON.parse(readFileSync(credsPath, "utf-8")); const token = creds?.claudeAiOauth?.accessToken; if (token) return token; } catch {} throw new Error("No Anthropic credentials found. Install Claude Code and authenticate, or set ANTHROPIC_API_KEY."); } let apiKey = getApiKey(); let client = new Anthropic({ apiKey }); // Re-read credentials every 30 min in case Claude Code refreshed the token setInterval(() => { try { const fresh = getApiKey(); if (fresh !== apiKey) { apiKey = fresh; client = new Anthropic({ apiKey }); console.log("Refreshed Anthropic credentials from Claude Code"); } } catch {} }, 30 * 60_000); 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 clichés verbatim - Vary your language — draw from mindfulness, somatic awareness, nature imagery, gratitude, breath, stillness`; const lastCall = new Map(); app.post("/api/guidance", async (c) => { const body = await c.req.json<{ phase: string; progress: number; duration: number; streak: number; sessionCount: number; sessionId: string; }>(); const now = Date.now(); const last = lastCall.get(body.sessionId) || 0; if (now - last < 20_000) { return c.json({ error: "rate limited" }, 429); } lastCall.set(body.sessionId, now); 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}`); try { const msg = await client.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 60, system: SYSTEM_PROMPT, messages: [{ role: "user", content: contextParts.join(". ") + "." }], }); const text = msg.content[0].type === "text" ? msg.content[0].text.trim().replace(/^["']|["']$/g, "") : ""; return c.json({ text }); } catch (e) { console.error("Guidance API error:", e); return c.json({ error: "generation failed" }, 500); } }); app.use("/*", serveStatic({ root: "./app" })); const port = parseInt(process.env.PORT || "3000"); console.log(`Palace Meditate running on :${port}`); serve({ fetch: app.fetch, port });