Replace Vercel AI SDK with Anthropic Claude Agent SDK. Add standalone agent server (packages/agent-server/) with MCP tools, JWT auth, and SSE streaming. Introduce bridge API routes (src/app/api/compass/) and custom SSE hooks (use-agent, use-compass-chat) replacing useChat. Remove provider.ts, tools.ts, system-prompt.ts, github-tools.ts, usage.ts, and old agent route.
156 lines
4.0 KiB
TypeScript
156 lines
4.0 KiB
TypeScript
#!/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<Response> {
|
|
// 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<Response> {
|
|
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(", ")}`)
|