spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: website/server.mjs4 * Description: Zero-dependency static server for www.khaelor.sh — serves public/, the install tarball, and /health.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 *9 * Run: node server.mjs (PORT env var, default 8790; binds 0.0.0.0)10 * Serves:11 * / → public/index.html12 * /docs/... → public/docs/...13 * /install.sh → public/install.sh (no-cache)14 * /khaelor.tgz → artifacts/khaelor.tgz (no-cache; 503 if absent)15 * /health → {"ok":true,"version":...}16 */1718import { createServer } from "node:http";19import { createReadStream, existsSync, statSync } from "node:fs";20import { extname, join, normalize, resolve, sep } from "node:path";21import { fileURLToPath } from "node:url";2223const VERSION = "1.0.0";24const PORT = Number.parseInt(process.env.PORT ?? "8790", 10);25const HOST = "0.0.0.0";2627const ROOT = fileURLToPath(new URL(".", import.meta.url));28const PUBLIC_DIR = resolve(ROOT, "public");29const TARBALL_PATH = resolve(ROOT, "artifacts", "khaelor.tgz");3031const MIME = {32 ".html": "text/html; charset=utf-8",33 ".css": "text/css; charset=utf-8",34 ".js": "text/javascript; charset=utf-8",35 ".mjs": "text/javascript; charset=utf-8",36 ".json": "application/json; charset=utf-8",37 ".sh": "text/plain; charset=utf-8",38 ".txt": "text/plain; charset=utf-8",39 ".md": "text/plain; charset=utf-8",40 ".svg": "image/svg+xml",41 ".png": "image/png",42 ".jpg": "image/jpeg",43 ".jpeg": "image/jpeg",44 ".gif": "image/gif",45 ".ico": "image/x-icon",46 ".webp": "image/webp",47 ".woff": "font/woff",48 ".woff2": "font/woff2",49 ".tgz": "application/gzip",50 ".gz": "application/gzip",51 ".xml": "application/xml; charset=utf-8",52};5354/** Assets get a day of caching; the installer and tarball must always be fresh. */55function cacheHeaderFor(urlPath) {56 if (urlPath === "/install.sh" || urlPath === "/khaelor.tgz" || urlPath === "/health") {57 return "no-cache, no-store, must-revalidate";58 }59 if (urlPath.endsWith(".html") || urlPath === "/" || extname(urlPath) === "") {60 return "no-cache"; // revalidate pages so doc updates show up immediately61 }62 return "public, max-age=86400"; // css/js/images63}6465function log(req, status, extra = "") {66 const now = new Date().toISOString();67 const ip =68 req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ??69 req.socket.remoteAddress ??70 "-";71 console.log(`${now} ${ip} ${req.method} ${req.url} ${status}${extra ? " " + extra : ""}`);72}7374function sendText(req, res, status, body, headers = {}) {75 res.writeHead(status, {76 "Content-Type": "text/plain; charset=utf-8",77 "Content-Length": Buffer.byteLength(body),78 "Cache-Control": "no-cache, no-store, must-revalidate",79 ...headers,80 });81 res.end(req.method === "HEAD" ? undefined : body);82 log(req, status);83}8485function sendFile(req, res, filePath, urlPath) {86 let st;87 try {88 st = statSync(filePath);89 } catch {90 return sendText(req, res, 404, "404 — not found\n");91 }92 if (!st.isFile()) return sendText(req, res, 404, "404 — not found\n");9394 const type = MIME[extname(filePath).toLowerCase()] ?? "application/octet-stream";95 res.writeHead(200, {96 "Content-Type": type,97 "Content-Length": st.size,98 "Cache-Control": cacheHeaderFor(urlPath),99 "X-Content-Type-Options": "nosniff",100 });101 if (req.method === "HEAD") {102 res.end();103 log(req, 200);104 return;105 }106 const stream = createReadStream(filePath);107 stream.on("error", () => {108 if (!res.headersSent) sendText(req, res, 500, "500 — read error\n");109 else res.destroy();110 });111 stream.pipe(res);112 log(req, 200, `${st.size}b`);113}114115const server = createServer((req, res) => {116 if (req.method !== "GET" && req.method !== "HEAD") {117 return sendText(req, res, 405, "405 — method not allowed\n", { Allow: "GET, HEAD" });118 }119120 let urlPath;121 try {122 urlPath = decodeURIComponent(new URL(req.url, "http://localhost").pathname);123 } catch {124 return sendText(req, res, 400, "400 — bad request\n");125 }126127 // ── health ──────────────────────────────────────────────────────────128 if (urlPath === "/health") {129 const body = JSON.stringify({ ok: true, version: VERSION });130 res.writeHead(200, {131 "Content-Type": "application/json; charset=utf-8",132 "Content-Length": Buffer.byteLength(body),133 "Cache-Control": "no-cache, no-store, must-revalidate",134 });135 res.end(req.method === "HEAD" ? undefined : body);136 return log(req, 200);137 }138139 // ── install tarball (dropped into artifacts/ at deploy time) ───────140 if (urlPath === "/khaelor.tgz") {141 if (!existsSync(TARBALL_PATH)) {142 return sendText(143 req,144 res,145 503,146 "The khaelor package tarball is not available on this server yet.\n" +147 "Try again shortly, or contact contact@spboucher.ai.\n",148 { "Retry-After": "3600" },149 );150 }151 return sendFile(req, res, TARBALL_PATH, urlPath);152 }153154 // ── static files from public/ ───────────────────────────────────────155 if (urlPath === "/") urlPath = "/index.html";156 // directory-style URLs: /docs → /docs/index.html would 404; leave as-is.157158 const resolved = resolve(PUBLIC_DIR, "." + normalize("/" + urlPath));159 if (resolved !== PUBLIC_DIR && !resolved.startsWith(PUBLIC_DIR + sep)) {160 return sendText(req, res, 403, "403 — forbidden\n");161 }162163 let target = resolved;164 if (existsSync(target) && statSync(target).isDirectory()) {165 target = join(target, "index.html");166 }167 if (!existsSync(target) && extname(target) === "" && existsSync(target + ".html")) {168 target = target + ".html"; // /docs/usage → /docs/usage.html169 }170171 return sendFile(req, res, target, urlPath);172});173174server.listen(PORT, HOST, () => {175 console.log(176 `${new Date().toISOString()} khaelor-website v${VERSION} listening on http://${HOST}:${PORT} (public: ${PUBLIC_DIR})`,177 );178});179180for (const sig of ["SIGINT", "SIGTERM"]) {181 process.on(sig, () => {182 console.log(`${new Date().toISOString()} ${sig} received — shutting down`);183 server.close(() => process.exit(0));184 setTimeout(() => process.exit(0), 2000).unref();185 });186}187