#!/usr/bin/env node import { createServer } from "node:http"; import { createRequire } from "node:module"; import { networkInterfaces } from "node:os"; import { dirname, extname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { existsSync } from "node:fs"; import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const defaultStorageRoot = resolve(repoRoot, ".voidbot"); const defaultStatusDir = resolve(defaultStorageRoot, "status"); const defaultSnapshotPath = resolve(defaultStatusDir, "swarm-state.json"); const defaultDashboardPath = resolve(defaultStatusDir, "swarm-dashboard.html"); const defaultCultMeshStorePath = resolve(defaultStatusDir, "cultmesh", "voidbot-swarm-state.cc"); const snapshotDocumentType = "voidbot.swarm_state_snapshot"; const snapshotSchemaId = "voidbot.swarm_state_snapshot.v1"; const args = parseArgs(process.argv.slice(2)); const env = await readDotEnv(resolve(repoRoot, ".env")); const storageRoot = resolveConfigPath(env.STORAGE_ROOT, defaultStorageRoot); const statusDir = resolve(storageRoot, "status"); const heartbeatStatePath = resolveConfigPath( env.REPO_FACE_HEARTBEAT_STATE_PATH, resolve(statusDir, "repo-face-heartbeats.json"), ); const orchestratorPath = resolve(statusDir, "gamecult-orchestrator.json"); const pausePath = resolve(repoRoot, "state", "agent-swarm-paused.json"); const snapshotPath = resolveConfigPath(args.snapshot, defaultSnapshotPath.replace(defaultStatusDir, statusDir)); const dashboardPath = resolveConfigPath(args.out, defaultDashboardPath.replace(defaultStatusDir, statusDir)); const cultMeshStorePath = resolveConfigPath( args.cultmeshStore, defaultCultMeshStorePath.replace(defaultStatusDir, statusDir), ); const renderResult = await render(); if (args.open) { console.log(pathToFileURL(renderResult.dashboardPath).href); } if (args.serve) { await serveDashboard({ host: args.host ?? "0.0.0.0", port: Number.parseInt(args.port ?? "8787", 10), rootDir: dirname(renderResult.dashboardPath), refreshSeconds: Number.parseInt(args.refreshSeconds ?? "10", 10), }); } else { console.log(`Swarm dashboard HTML: ${renderResult.dashboardPath}`); console.log(`Swarm snapshot JSON: ${renderResult.snapshotPath}`); console.log(`CultMesh snapshot store: ${renderResult.cultMeshStorePath}`); } async function render() { let snapshot = await buildSnapshot(); if (args.public) { snapshot = redactSnapshot(snapshot); } await mkdir(dirname(snapshotPath), { recursive: true }); await writeFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); await mkdir(dirname(dashboardPath), { recursive: true }); await writeFile(dashboardPath, renderHtml(snapshot), "utf8"); const cultMeshWrite = await writeCultMeshSnapshot(snapshot, cultMeshStorePath); const finalSnapshot = { ...snapshot, cultMesh: { ...snapshot.cultMesh, ...cultMeshWrite, }, }; await writeFile(snapshotPath, `${JSON.stringify(finalSnapshot, null, 2)}\n`, "utf8"); await writeFile(dashboardPath, renderHtml(finalSnapshot), "utf8"); return { dashboardPath, snapshotPath, cultMeshStorePath, }; } async function buildSnapshot() { const now = new Date(); const heartbeat = await readJsonFile(heartbeatStatePath); const orchestrator = await readJsonFile(orchestratorPath); const pause = await readJsonFile(pausePath); const heartbeatMtime = await getMtime(heartbeatStatePath); const orchestratorMtime = await getMtime(orchestratorPath); const paused = pause.ok ? pause.value?.paused !== false : false; const pauseReason = pause.ok && typeof pause.value?.reason === "string" ? pause.value.reason : undefined; const participants = Array.isArray(heartbeat.value?.participants) ? heartbeat.value.participants : []; const pendingMentions = Array.isArray(heartbeat.value?.pendingMentions) ? heartbeat.value.pendingMentions : []; const initiativeClock = numberOrNull(heartbeat.value?.initiativeClock); const participantSnapshots = participants.map((participant) => projectParticipant(participant, pendingMentions, initiativeClock), ); const activeTurns = participantSnapshots.filter((participant) => participant.activeJobId); const readyNow = participantSnapshots.filter((participant) => participant.status === "active" && !participant.activeJobId && typeof participant.nextTurnInMinutes === "number" && participant.nextTurnInMinutes <= 0, ); const nextParticipant = [...participantSnapshots] .filter((participant) => typeof participant.nextTurnInMinutes === "number") .sort((left, right) => left.nextTurnInMinutes - right.nextTurnInMinutes)[0]; const organs = projectOrgans(orchestrator.value); const recentEvents = Array.isArray(heartbeat.value?.history) ? heartbeat.value.history.slice(-24).reverse().map(projectHistoryEvent) : []; return { schemaVersion: snapshotSchemaId, generatedAt: now.toISOString(), title: "VoidBot Swarm State", sources: { heartbeatStatePath, heartbeatStateUpdatedAt: heartbeatMtime, heartbeatStateReadable: heartbeat.ok, heartbeatStateError: heartbeat.ok ? undefined : heartbeat.error, orchestratorPath, orchestratorUpdatedAt: orchestratorMtime, orchestratorReadable: orchestrator.ok, orchestratorError: orchestrator.ok ? undefined : orchestrator.error, pausePath, pauseReadable: pause.ok, pauseError: pause.ok ? undefined : pause.error, }, cultMesh: { documentType: snapshotDocumentType, schemaId: snapshotSchemaId, documentKey: "voidbot-swarm", storePath: cultMeshStorePath, writeStatus: "pending", note: "CultMesh should distribute this typed CultCache snapshot; the web page remains read-only.", }, summary: { state: deriveSwarmState({ heartbeat, orchestrator, paused, activeTurns }), paused, pauseReason, participantCount: participantSnapshots.length, activeTurnCount: activeTurns.length, pendingMentionCount: pendingMentions.length, readyNowCount: readyNow.length, nextIdentityId: nextParticipant?.identityId ?? null, nextDisplayName: nextParticipant?.displayName ?? null, nextTurnInMinutes: nextParticipant?.nextTurnInMinutes ?? null, initiativeClock, baseRecoveryMinutes: numberOrNull(heartbeat.value?.baseRecoveryMinutes), globalHeat: numberOrNull(heartbeat.value?.globalHeat), lastTickAt: stringOrNull(heartbeat.value?.lastTickAt), }, participants: participantSnapshots, activeTurns, pendingMentions: pendingMentions.map(projectPendingMention), orchestrator: { state: deriveOrchestratorState(organs), organs, }, recentEvents, }; } function projectParticipant(participant, pendingMentions, initiativeClock) { const identityId = String(participant.identityId ?? "unknown"); const nextTurnAt = numberOrNull(participant.nextTurnAt); const lastTurnAt = numberOrNull(participant.lastTurnAt); const activeTurnStartedAt = numberOrNull(participant.activeTurnStartedAt); const channelCount = Array.isArray(participant.groups) ? participant.groups.filter((entry) => typeof entry === "string" && entry.startsWith("channel:")).length : 0; const mentionCount = pendingMentions.filter((mention) => mention?.identityId === identityId).length; const nextTurnInMinutes = typeof nextTurnAt === "number" && typeof initiativeClock === "number" ? round(nextTurnAt - initiativeClock, 3) : null; return { identityId, displayName: String(participant.displayName ?? identityId), repoName: String(participant.repoName ?? "unknown"), participantKind: String(participant.participantKind ?? "repo_face"), turnKind: String(participant.turnKind ?? "repo_face_rumination"), status: String(participant.status ?? "unknown"), currentLoad: numberOrNull(participant.currentLoad), heat: numberOrNull(participant.heat), effectiveSpeed: numberOrNull(participant.effectiveSpeed), initiativeSpeed: numberOrNull(participant.initiativeSpeed), nextTurnAt, nextTurnInMinutes, lastTurnAt, activeTurnStartedAt, activeJobId: stringOrNull(participant.activeJobId), lastQueuedAt: stringOrNull(participant.lastQueuedAt), queuedCount: numberOrNull(participant.queuedCount), pendingMentionCount: mentionCount, channelCount, constraintCount: Array.isArray(participant.constraints) ? participant.constraints.length : 0, constraints: Array.isArray(participant.constraints) ? participant.constraints.slice(0, 4).map(String) : [], }; } function projectOrgans(orchestrator) { const organs = orchestrator?.organs && typeof orchestrator.organs === "object" ? orchestrator.organs : {}; return Object.entries(organs).map(([id, organ]) => ({ id, label: String(organ?.label ?? id), intervalMinutes: numberOrNull(organ?.intervalMinutes), lastStartedAt: stringOrNull(organ?.lastStartedAt), lastFinishedAt: stringOrNull(organ?.lastFinishedAt), lastExitCode: numberOrNull(organ?.lastExitCode), lastStatus: String(organ?.lastStatus ?? "unknown"), lastLogPath: stringOrNull(organ?.lastLogPath), })); } function projectHistoryEvent(event) { return { type: String(event?.type ?? "event"), identityId: stringOrNull(event?.identityId), observedAt: stringOrNull(event?.observedAt ?? event?.queuedAt), activeJobId: stringOrNull(event?.activeJobId), statusPath: stringOrNull(event?.statusPath), reason: stringOrNull(event?.reason), nextTurnAt: numberOrNull(event?.nextTurnAt), recoveryMinutes: numberOrNull(event?.recoveryMinutes), }; } function projectPendingMention(mention) { return { identityId: stringOrNull(mention?.identityId), sourceChannelId: stringOrNull(mention?.sourceChannelId), sourceMessageId: stringOrNull(mention?.sourceMessageId), createdAt: stringOrNull(mention?.createdAt), prompt: truncate(String(mention?.prompt ?? mention?.content ?? ""), 220), }; } function deriveSwarmState({ heartbeat, orchestrator, paused, activeTurns }) { if (!heartbeat.ok) return "missing"; if (paused) return "paused"; const organ = projectOrgans(orchestrator.value).find((entry) => entry.id === "repo-face-heartbeats"); if (organ && organ.lastStatus && !["ok", "skipped_disabled"].includes(organ.lastStatus)) { return "warning"; } if (activeTurns.length > 0) return "running"; return "ready"; } function deriveOrchestratorState(organs) { if (organs.length === 0) return "missing"; if (organs.some((organ) => ["failed", "error", "stalled"].includes(organ.lastStatus))) return "warning"; if (organs.some((organ) => organ.lastStatus === "running")) return "running"; return "ok"; } async function writeCultMeshSnapshot(snapshot, storePath) { try { const cultCacheRoot = resolve(repoRoot, "..", "CultCacheTS"); const cultCachePackage = resolve(cultCacheRoot, "package.json"); if (!existsSync(cultCachePackage)) { return { writeStatus: "skipped", writeError: `CultCacheTS package was not found at ${cultCachePackage}.`, }; } const requireCultCache = createRequire(cultCachePackage); const { CultCache, SingleFileMessagePackBackingStore, defineDocumentType } = requireCultCache( resolve(cultCacheRoot, "dist", "index.js"), ); const snapshotDefinition = defineDocumentType({ type: snapshotDocumentType, schemaName: snapshotDocumentType, schemaId: snapshotSchemaId, schemaVersion: snapshotSchemaId, contentHash: snapshotSchemaId, canonicalSchemaJson: JSON.stringify({ schemaName: snapshotDocumentType, schemaVersion: snapshotSchemaId, description: "Redacted read-only VoidBot swarm state snapshot for CultMesh distribution.", }), global: true, schema: { parse(input) { if (!input || typeof input !== "object") { throw new Error("VoidBot swarm snapshot must be an object."); } return input; }, }, }); const store = new SingleFileMessagePackBackingStore(storePath); const cache = CultCache.builder() .withDocumentType(snapshotDefinition) .withGenericStore(store) .build(); await cache.pullAllBackingStores(); await cache.put(snapshotDefinition, "voidbot-swarm", snapshot); return { writeStatus: "ok", storePath, writtenAt: new Date().toISOString(), }; } catch (error) { return { writeStatus: "failed", storePath, writeError: error instanceof Error ? error.message : String(error), }; } } function renderHtml(snapshot) { return ` VoidBot Swarm State
`; } function redactSnapshot(snapshot) { const redacted = JSON.parse(JSON.stringify(snapshot)); redacted.visibility = "public_redacted"; redacted.sources = { heartbeatStateReadable: snapshot.sources?.heartbeatStateReadable ?? false, heartbeatStateUpdatedAt: snapshot.sources?.heartbeatStateUpdatedAt ?? null, orchestratorReadable: snapshot.sources?.orchestratorReadable ?? false, orchestratorUpdatedAt: snapshot.sources?.orchestratorUpdatedAt ?? null, pauseReadable: snapshot.sources?.pauseReadable ?? false, }; redacted.cultMesh = { documentType: snapshot.cultMesh?.documentType, schemaId: snapshot.cultMesh?.schemaId, documentKey: snapshot.cultMesh?.documentKey, writeStatus: snapshot.cultMesh?.writeStatus, writtenAt: snapshot.cultMesh?.writtenAt, note: "Public redacted snapshot. Local paths and job ids are withheld.", }; redacted.participants = redacted.participants.map((participant) => ({ ...participant, activeJobId: participant.activeJobId ? "active" : null, constraints: [], constraintCount: participant.constraintCount, })); redacted.activeTurns = redacted.activeTurns.map((participant) => ({ ...participant, activeJobId: participant.activeJobId ? "active" : null, constraints: [], })); redacted.pendingMentions = redacted.pendingMentions.map((mention) => ({ identityId: mention.identityId, createdAt: mention.createdAt, })); redacted.orchestrator.organs = redacted.orchestrator.organs.map((organ) => ({ ...organ, lastLogPath: null, })); redacted.recentEvents = redacted.recentEvents.map((event) => ({ ...event, activeJobId: event.activeJobId ? "active" : null, statusPath: null, })); return redacted; } async function serveDashboard({ host, port, rootDir, refreshSeconds }) { const interval = setInterval(async () => { try { await render(); } catch (error) { console.error(`Swarm dashboard refresh failed: ${error instanceof Error ? error.message : String(error)}`); } }, Math.max(refreshSeconds, 2) * 1000); interval.unref(); const server = createServer(async (request, response) => { try { const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`); const pathname = requestUrl.pathname === "/" ? "/swarm-dashboard.html" : decodeURIComponent(requestUrl.pathname); const target = resolve(rootDir, `.${pathname}`); if (!target.startsWith(rootDir)) { response.writeHead(403); response.end("Forbidden"); return; } const fileStat = await stat(target); if (!fileStat.isFile()) { response.writeHead(404); response.end("Not found"); return; } response.writeHead(200, { "content-type": contentType(target), "cache-control": "no-store", }); response.end(await readFile(target)); } catch { response.writeHead(404); response.end("Not found"); } }); await new Promise((resolveServer) => server.listen(port, host, resolveServer)); console.log(`Swarm dashboard serving ${rootDir}`); for (const url of localUrls(port, host)) { console.log(url); } } function contentType(path) { switch (extname(path).toLowerCase()) { case ".html": return "text/html; charset=utf-8"; case ".json": return "application/json; charset=utf-8"; case ".cc": return "application/octet-stream"; default: return "text/plain; charset=utf-8"; } } function localUrls(port, host) { const urls = []; if (host === "127.0.0.1" || host === "localhost") { urls.push(`http://127.0.0.1:${port}/`); return urls; } urls.push(`http://127.0.0.1:${port}/`); for (const entries of Object.values(networkInterfaces())) { for (const entry of entries ?? []) { if (entry.family === "IPv4" && !entry.internal) { urls.push(`http://${entry.address}:${port}/`); } } } return [...new Set(urls)]; } async function readJsonFile(path) { try { const raw = await readFile(path, "utf8"); return { ok: true, value: JSON.parse(stripBom(raw)) }; } catch (error) { return { ok: false, value: null, error: error instanceof Error ? error.message : String(error), }; } } async function getMtime(path) { try { return (await stat(path)).mtime.toISOString(); } catch { return null; } } async function readDotEnv(path) { const values = {}; try { const raw = await readFile(path, "utf8"); for (const line of raw.split(/\r?\n/u)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const separator = line.indexOf("="); if (separator < 1) continue; values[line.slice(0, separator).trim()] = line.slice(separator + 1); } } catch {} return values; } function resolveConfigPath(value, fallback) { const raw = typeof value === "string" && value.trim() ? value.trim() : fallback; return resolve(repoRoot, raw); } function parseArgs(argv) { const parsed = {}; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--serve") parsed.serve = true; else if (arg === "--open") parsed.open = true; else if (arg.startsWith("--")) { const key = arg.slice(2); const next = argv[index + 1]; if (next && !next.startsWith("--")) { parsed[key] = next; index += 1; } else { parsed[key] = "true"; } } } return parsed; } function stripBom(value) { return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value; } function numberOrNull(value) { return typeof value === "number" && Number.isFinite(value) ? value : null; } function stringOrNull(value) { return typeof value === "string" && value.length > 0 ? value : null; } function round(value, places) { const factor = 10 ** places; return Math.round(value * factor) / factor; } function truncate(value, maxLength) { return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value; } function escapeScriptJson(value) { return value.replace(//g, "\\u003e").replace(/&/g, "\\u0026"); }