import path from "node:path"; import Fastify from "fastify"; import fastifyStatic from "@fastify/static"; import { BossEngine } from "./engine.js"; const engine = new BossEngine(); const app = Fastify({ logger: process.env.BOSS_DEBUG === "1" }); app.setErrorHandler((error, request, reply) => { const message = typeof error === "object" && error !== null && "message" in error ? String(error.message) : "Internal Server Error"; const normalized = message.toLowerCase(); if (normalized.includes("not found")) { return reply.status(404).send({ error: "Not Found", message }); } if ( normalized.includes("is not assigned to worker") || normalized.includes("is not currently executing") || normalized.includes("does not accept worker updates") ) { return reply.status(409).send({ error: "Conflict", message }); } if ( normalized.includes("required") || normalized.includes("archived") ) { return reply.status(400).send({ error: "Bad Request", message }); } request.log.error(error); return reply.status(500).send({ error: "Internal Server Error", message }); }); await app.register(fastifyStatic, { root: path.resolve(process.cwd(), "public"), prefix: "/", }); function ok() { return { ok: true }; } app.get("/api/health", async () => ({ status: "ok", sessions: engine.getState().sessions.length, workers: engine.getState().workers.length, })); app.get("/api/bootstrap", async () => engine.bootstrap()); app.get("/api/events", async (request) => { const query = request.query as { limit?: string }; const limit = Number(query.limit ?? 100); return engine.listEvents(Number.isFinite(limit) ? limit : 100); }); app.get("/api/sessions", async () => engine.listSessions()); app.post("/api/sessions", async (request) => { const body = (request.body ?? {}) as { title?: string }; return engine.createSession(body.title); }); app.post("/api/sessions/:sessionId/archive", async (request) => { const params = request.params as { sessionId: string }; return engine.archiveSession(params.sessionId); }); app.get("/api/sessions/:sessionId", async (request) => { const params = request.params as { sessionId: string }; return engine.getSession(params.sessionId); }); app.get("/api/tasks/:taskId", async (request) => { const params = request.params as { taskId: string }; return engine.getTask(params.taskId); }); app.post("/api/sessions/:sessionId/messages", async (request) => { const params = request.params as { sessionId: string }; const body = (request.body ?? {}) as { content?: string; channel?: string }; return engine.addMessage(params.sessionId, body.content ?? "", body.channel ?? "web"); }); app.get("/api/events/stream", async (_request, reply) => { reply.raw.setHeader("Content-Type", "text/event-stream"); reply.raw.setHeader("Cache-Control", "no-cache"); reply.raw.setHeader("Connection", "keep-alive"); reply.raw.flushHeaders?.(); const unsubscribe = engine.events.subscribe((event) => { reply.raw.write(`data: ${JSON.stringify(event)}\n\n`); }); const interval = setInterval(() => { reply.raw.write(": ping\n\n"); }, 15_000); reply.raw.on("close", () => { clearInterval(interval); unsubscribe(); reply.raw.end(); }); }); app.get("/api/workers", async () => engine.getState().workers); app.get("/api/workers/:workerId", async (request) => { const params = request.params as { workerId: string }; return engine.getWorker(params.workerId); }); app.post("/api/workers/register", async (request) => { const body = request.body as { name?: string; os?: "windows" | "macos" | "linux"; capabilities?: string[]; }; return engine.registerWorker({ name: body.name ?? "worker", os: body.os ?? "linux", capabilities: body.capabilities ?? ["terminal"], }); }); app.post("/api/workers/:workerId/heartbeat", async (request) => { const params = request.params as { workerId: string }; const body = (request.body ?? {}) as { load?: number }; return engine.heartbeat(params.workerId, body.load ?? 0); }); app.post("/api/workers/:workerId/offline", async (request) => { const params = request.params as { workerId: string }; return engine.markWorkerOffline(params.workerId); }); app.post("/api/workers/:workerId/claim-next", async (request) => { const params = request.params as { workerId: string }; return { task: engine.claimNextTask(params.workerId), }; }); app.post("/api/tasks/:taskId/progress", async (request) => { const params = request.params as { taskId: string }; const body = request.body as { workerId: string; progressPercent: number; summary: string; currentStep: string; nextStep: string; }; return engine.reportProgress(params.taskId, body.workerId, { progressPercent: body.progressPercent, summary: body.summary, currentStep: body.currentStep, nextStep: body.nextStep, }); }); app.post("/api/tasks/:taskId/complete", async (request) => { const params = request.params as { taskId: string }; const body = request.body as { workerId: string; summary: string; }; return engine.completeTask(params.taskId, body.workerId, body.summary); }); app.post("/api/tasks/:taskId/fail", async (request) => { const params = request.params as { taskId: string }; const body = request.body as { workerId: string; errorMessage: string; }; return engine.failTask(params.taskId, body.workerId, body.errorMessage); }); app.post("/api/tasks/:taskId/pause", async (request) => { const params = request.params as { taskId: string }; return engine.pauseTask(params.taskId); }); app.post("/api/tasks/:taskId/cancel", async (request) => { const params = request.params as { taskId: string }; return engine.cancelTask(params.taskId); }); app.post("/api/tasks/:taskId/resume", async (request) => { const params = request.params as { taskId: string }; return engine.resumeTask(params.taskId); }); app.post("/api/tasks/:taskId/requeue", async (request) => { const params = request.params as { taskId: string }; return engine.requeueTask(params.taskId); }); app.post("/api/approvals/:approvalId/respond", async (request) => { const params = request.params as { approvalId: string }; const body = request.body as { approved: boolean; responder?: string; }; return engine.respondApproval(params.approvalId, body.approved, body.responder ?? "user"); }); app.post("/api/demo/reset", async () => { engine.store.reset(); return ok(); }); app.post("/api/reconcile", async () => engine.reconcileNow()); const port = Number(process.env.PORT ?? 43210); await app.listen({ port, host: "0.0.0.0" });