#!/usr/bin/env bun /** * Compass Agent Server * Standalone Node.js server wrapping Anthropic Agent SDK */ import { config } from "./config" import { validateAuth } from "./auth" import { getOrCreateSession } from "./sessions" import { createAgentStream } from "./stream" interface ChatRequest { messages: Array<{ role: "user" | "assistant" content: string }> } /** * Handle POST /agent/chat */ async function handleChat(request: Request): Promise { // Validate auth const authResult = await validateAuth(request) if ("error" in authResult) { return new Response( JSON.stringify({ error: authResult.error }), { status: 401, headers: { "Content-Type": "application/json" } } ) } // Extract JWT token from Authorization header const authHeader = request.headers.get("authorization") const authToken = authHeader?.slice(7) || "" // Remove "Bearer " prefix // Extract headers const sessionId = request.headers.get("x-session-id") || crypto.randomUUID() const currentPage = request.headers.get("x-current-page") || "/" const timezone = request.headers.get("x-timezone") || "UTC" const model = request.headers.get("x-model") || "sonnet" // Get or create session getOrCreateSession(sessionId) // Parse body let body: ChatRequest try { body = await request.json() } catch { return new Response( JSON.stringify({ error: "Invalid JSON body" }), { status: 400, headers: { "Content-Type": "application/json" } } ) } if (!Array.isArray(body.messages) || body.messages.length === 0) { return new Response( JSON.stringify({ error: "messages array is required and cannot be empty" }), { status: 400, headers: { "Content-Type": "application/json" } } ) } // Create SSE stream const stream = await createAgentStream(body.messages, { auth: authResult, authToken, sessionId, currentPage, timezone, provider: authResult.provider, model, }) return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": request.headers.get("origin") || "*", "Access-Control-Allow-Credentials": "true", }, }) } /** * Handle GET /health */ function handleHealth(): Response { return new Response( JSON.stringify({ status: "ok", version: "0.1.0" }), { headers: { "Content-Type": "application/json" } } ) } /** * CORS preflight handler */ function handlePreflight(request: Request): Response { const origin = request.headers.get("origin") || "" const allowed = config.allowedOrigins.includes(origin) || config.allowedOrigins.includes("*") if (!allowed) { return new Response(null, { status: 403 }) } return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Authorization, Content-Type, x-session-id, x-current-page, x-timezone, x-model", "Access-Control-Allow-Credentials": "true", "Access-Control-Max-Age": "86400", }, }) } /** * Main request router */ async function handleRequest(request: Request): Promise { const url = new URL(request.url) // Handle CORS preflight if (request.method === "OPTIONS") { return handlePreflight(request) } // Route requests if (url.pathname === "/health" && request.method === "GET") { return handleHealth() } if (url.pathname === "/agent/chat" && request.method === "POST") { return handleChat(request) } return new Response( JSON.stringify({ error: "Not found" }), { status: 404, headers: { "Content-Type": "application/json" } } ) } /** * Start the server */ const server = Bun.serve({ port: config.port, async fetch(request) { return handleRequest(request) }, }) console.log(`Agent server running on http://localhost:${server.port}`) console.log(`Allowed origins: ${config.allowedOrigins.join(", ")}`)