export const prerender = false; import type { APIRoute } from "astro"; import { spawn } from "child_process"; import * as readline from "readline"; import * as fs from "fs"; import * as path from "path"; import { logUsage, extractUsageFromResult } from "@palace/sdk/usage"; const REPO = process.env.REPO_ROOT || path.resolve("/Users/ace/palacering"); const DATA = path.resolve(REPO, "palaces", "manglasabang", "palaceappsdata", "palacediary"); const SESSION_DIR = path.join(REPO, "palacering", ".sessions"); const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; function dayOfWeek(date: string) { return DAYS[new Date(date + "T12:00:00").getDay()]; } function dayPath(date: string) { const [y, m, d] = date.split("-"); return path.resolve(DATA, y, m, `${d}.json`); } function readDay(date: string) { try { const raw = JSON.parse(fs.readFileSync(dayPath(date), "utf-8")); if (raw.moments && !raw.reviews) { return { date: raw.date, dayOfWeek: raw.dayOfWeek, reviews: raw.moments, plans: [] }; } return { date: raw.date, dayOfWeek: raw.dayOfWeek, reviews: raw.reviews || [], plans: raw.plans || [] }; } catch { return { date, dayOfWeek: dayOfWeek(date), reviews: [], plans: [] }; } } function saveDay(day: { date: string; dayOfWeek: string; reviews: any[]; plans: any[] }) { const fp = dayPath(day.date); fs.mkdirSync(path.dirname(fp), { recursive: true }); fs.writeFileSync(fp, JSON.stringify(day, null, 2) + "\n"); } function buildPrompt(date: string, message: string) { const day = readDay(date); const now = new Date(); const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; return `You are the Palace Diary assistant. The user is recording their day. Current time: ${currentTime}. Current date: ${date} (${day.dayOfWeek}). Today's diary so far: ${JSON.stringify(day, null, 2)} The user said: "${message}" Parse what they said and return a JSON object with the updates to apply. Extract: - reviews: things that happened (add to reviews list) - plans: new things they intend to do today (add to plans list) - done_plan_indices: indices (0-based) of existing plans that are now completed based on the new reviews or the message For reviews, infer the time from context — use the current time (${currentTime}) if they say "just now" or don't specify. Use 24h format for times. Respond with ONLY a JSON object, no markdown, no explanation: {"reviews": [{"time": "HH:MM", "what": "..."}], "plans": [{"what": "..."}], "done_plan_indices": []} All arrays can be empty. Keep "what" short and direct — one line, not a paragraph.`; } export const POST: APIRoute = async ({ request }) => { try { const { message, date } = await request.json(); if (!message || !date) { return new Response(JSON.stringify({ error: "message and date required" }), { status: 400 }); } if (!fs.existsSync(SESSION_DIR)) fs.mkdirSync(SESSION_DIR, { recursive: true }); const prompt = buildPrompt(date, message); const args = [ "-p", "--verbose", "--output-format", "stream-json", "--input-format", "stream-json", "--dangerously-skip-permissions", "--max-turns", "1", "--model", "claude-haiku-4-5-20251001", ]; const proc = spawn("claude", args, { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, CLAUDECODE: "", ACE_HAS_MEMORY: "1" }, }); const rl = readline.createInterface({ input: proc.stdout }); let reply = ""; let stderrData = ""; proc.stderr.on("data", (chunk: Buffer) => { stderrData += chunk.toString(); }); const result = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { proc.kill(); reject(new Error("timeout")); }, 60_000); rl.on("line", (line) => { if (!line.trim()) return; let msg: any; try { msg = JSON.parse(line); } catch { return; } if (msg.type === "assistant" && msg.message?.content) { for (const block of msg.message.content) { if (block.type === "text") reply += block.text; } } if (msg.type === "result") { clearTimeout(timeout); logUsage({ ...extractUsageFromResult(msg), channel: "palacediary", model: "haiku" }); resolve(reply.trim()); } }); proc.on("error", (err) => { clearTimeout(timeout); reject(err); }); proc.on("exit", (code) => { clearTimeout(timeout); if (reply) resolve(reply.trim()); else reject(new Error(`claude exited with code ${code}: ${stderrData.slice(0, 500)}`)); }); proc.stdin!.write( JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } }) + "\n" ); }); const match = result.match(/\{[\s\S]*\}/); if (!match) { return new Response(JSON.stringify({ error: "failed to parse response" }), { status: 500 }); } const parsed = JSON.parse(match[0]); const day = readDay(date); if (parsed.reviews?.length) { for (const r of parsed.reviews) { if (r.time && r.what) day.reviews.push({ time: r.time, what: r.what }); } day.reviews.sort((a: any, b: any) => a.time.localeCompare(b.time)); } if (parsed.plans?.length) { for (const p of parsed.plans) { if (p.what) day.plans.push({ what: p.what }); } } if (parsed.done_plan_indices?.length) { for (const i of parsed.done_plan_indices) { if (typeof i === "number" && day.plans[i]) day.plans[i].done = true; } } saveDay(day); const changed = (parsed.reviews?.length || 0) + (parsed.plans?.length || 0) + (parsed.done_plan_indices?.length || 0); return new Response(JSON.stringify({ ...day, _message: changed ? undefined : "Nothing new to add — already captured." }), { headers: { "Content-Type": "application/json" }, }); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500 }); } };