import type { APIRoute } from "astro"; import { upsertThread, getThread } from "../../lib/thread-store"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; const ROOT = process.env.REPO_ROOT!; const LINEAR_API = "https://api.linear.app/graphql"; // Map web status → Linear workflow state name const STATUS_TO_LINEAR: Record = { replied: "Junwon to reply", processing: "Ace to report progress", skipped: "We are both waiting", no_reply: "Done", failed: "Blocked", }; async function getLinearApiKey(): Promise { const envFile = await readFile(join(ROOT, "palaces/manglasabang/secretariat/keychain/linear.env"), "utf-8"); const m = envFile.match(/LINEAR_API_KEY='([^']+)'/); if (!m) throw new Error("LINEAR_API_KEY not found"); return m[1]; } async function linearGql(apiKey: string, query: string, variables?: Record): Promise { const res = await fetch(LINEAR_API, { method: "POST", headers: { "Content-Type": "application/json", Authorization: apiKey }, body: JSON.stringify({ query, variables }), }); if (!res.ok) throw new Error(`Linear API ${res.status}`); const json = await res.json(); if (json.errors?.length) throw new Error(json.errors[0].message); return json.data; } async function updateLinearIssueStatus(issueIdentifier: string, linearStateName: string): Promise { const apiKey = await getLinearApiKey(); // Find the issue to get its team const issueData = await linearGql(apiKey, ` query($id: String!) { issue(id: $id) { id team { id } } } `, { id: issueIdentifier }); if (!issueData.issue) return; const teamId = issueData.issue.team.id; const issueId = issueData.issue.id; // Get workflow states for this team const statesData = await linearGql(apiKey, ` query($teamId: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } }) { nodes { id name } } } `, { teamId }); const targetState = statesData.workflowStates.nodes.find((s: any) => s.name === linearStateName); if (!targetState) { console.warn(`[update-thread] Linear state "${linearStateName}" not found for team ${teamId}`); return; } // Update the issue await linearGql(apiKey, ` mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } } `, { id: issueId, stateId: targetState.id }); } export const POST: APIRoute = async ({ request }) => { const headers = { "Content-Type": "application/json" }; try { const { threadId, status } = await request.json(); if (!threadId || !status) { return new Response(JSON.stringify({ error: "threadId, status required" }), { status: 400, headers }); } const allowed = ["processing", "replied", "no_reply", "failed", "skipped", "archived"]; if (!allowed.includes(status)) { return new Response(JSON.stringify({ error: "invalid status" }), { status: 400, headers }); } // Update Redis thread await upsertThread({ threadId, status, endedAt: status === "processing" ? null : Date.now(), ...(status === "processing" ? { processingAt: Date.now() } : {}) }); // Sync to Linear if thread has a linked issue const thread = await getThread(threadId); const linearIssue = thread?.linearIssue; if (linearIssue && STATUS_TO_LINEAR[status]) { updateLinearIssueStatus(linearIssue, STATUS_TO_LINEAR[status]).catch((err) => { console.error(`[update-thread] Linear sync failed for ${linearIssue}:`, err.message); }); } return new Response(JSON.stringify({ ok: true }), { headers }); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } };