import fs from "node:fs"; import path from "node:path"; import { ARCHITECTURE_DOCUMENT, OPENCLAW_AGENTS_DIR, OPENCLAW_CONFIG_PATH, ZEROCLAW_CONTROL_DIR, ZEROCLAW_PRIMARY_DIR, } from "@/lib/fleet-config"; import { all } from "@/lib/db"; import { normalizeTask } from "@/lib/tasks"; import type { AgentStatus, FleetAgent, TaskRecord } from "@/lib/types"; type OpenClawAgentConfig = { id: string; name?: string; model?: { primary?: string }; identity?: { name?: string; emoji?: string; theme?: string }; subagents?: { allowAgents?: string[] }; }; type OpenClawConfigShape = { agents?: { list?: OpenClawAgentConfig[] }; channels?: { telegram?: { groups?: Record< string, { topics?: Record; } >; }; }; }; function readTextFile(filePath: string) { if (!fs.existsSync(filePath)) { return ""; } return fs.readFileSync(filePath, "utf8"); } function parseBulletValues(content: string) { return content .split("\n") .map((line) => line.trim()) .filter((line) => line.startsWith("- ")) .map((line) => line.replace(/^- /, "").replace(/`/g, "").trim()); } function parseRoleFromAgentsMd(content: string) { const identityMatch = content.match(/- Scope:\s*(.+)/); if (identityMatch) { return identityMatch[1].trim(); } return "Host-scoped agent"; } function parseResponsibilities(content: string) { const sectionMatch = content.match(/## Responsibilities([\s\S]*?)(##|$)/); return sectionMatch ? parseBulletValues(sectionMatch[1]) : []; } function readWorkspaceAgent(agentRoot: string, fallbackName: string) { const workspaceRoot = path.join(agentRoot, "workspace"); const agentsMd = readTextFile(path.join(workspaceRoot, "AGENTS.md")); const toolsMd = readTextFile(path.join(workspaceRoot, "TOOLS.md")); const identityMd = readTextFile(path.join(workspaceRoot, "IDENTITY.md")); const heartbeatMd = readTextFile(path.join(workspaceRoot, "HEARTBEAT.md")); const tools = parseBulletValues(toolsMd); const capabilities = parseResponsibilities(agentsMd); const currentTaskMatch = heartbeatMd.match(/Current Task:\s*(.+)/i); return { files: ["AGENTS.md", "TOOLS.md", "IDENTITY.md"].filter((fileName) => fs.existsSync(path.join(workspaceRoot, fileName)), ), tools, capabilities, currentTask: currentTaskMatch ? currentTaskMatch[1].trim() : null, role: parseRoleFromAgentsMd(agentsMd), noteValues: parseBulletValues(identityMd), workspaceRoot, }; } function readOpenClawConfig(): OpenClawConfigShape { try { return JSON.parse(fs.readFileSync(OPENCLAW_CONFIG_PATH, "utf8")) as OpenClawConfigShape; } catch { return {}; } } function getOpenClawChannels(agentId: string, config: OpenClawConfigShape) { const summaries: { label: string; value: string }[] = [ { label: "Family", value: "OpenClaw telegram + gateway" }, ]; const topicGroups = config.channels?.telegram?.groups?.["-1003809447066"]?.topics; if (!topicGroups) { return summaries; } const topicEntries = Object.entries(topicGroups).filter(([, topic]) => { const prompt = topic.systemPrompt || ""; return prompt.toLowerCase().includes(agentId.toLowerCase()) || agentId === "main"; }); if (topicEntries.length === 0 && agentId === "main") { summaries.push({ label: "Forum", value: "Homelab HQ default route" }); return summaries; } topicEntries.forEach(([topicId]) => { summaries.push({ label: "Topic", value: `Homelab HQ topic ${topicId}` }); }); return summaries; } async function fetchTaskBuckets(aliases: string[]) { const placeholders = aliases.map(() => "?").join(", "); const activeRows = await all & { tags: string }>( `SELECT * FROM tasks WHERE assignee IN (${placeholders}) AND status IN ('Todo', 'In Progress', 'Review') ORDER BY priority DESC, created_at ASC`, aliases, ); const completedRows = await all & { tags: string }>( `SELECT * FROM tasks WHERE assignee IN (${placeholders}) AND status = 'Done' ORDER BY completed_at DESC LIMIT 5`, aliases, ); return { activeTasks: activeRows.map(normalizeTask), completedTasks: completedRows.map(normalizeTask), }; } async function buildOpenClawAgents() { const config = readOpenClawConfig(); const agents = config.agents?.list || []; return Promise.all( agents.map(async (agentConfig) => { const agentRoot = path.join(OPENCLAW_AGENTS_DIR, agentConfig.id); const workspace = readWorkspaceAgent(agentRoot, agentConfig.identity?.name || agentConfig.id); const aliases = [ agentConfig.id, agentConfig.identity?.name || agentConfig.name || agentConfig.id, ]; const taskBuckets = await fetchTaskBuckets(aliases); return { slug: agentConfig.id, assignmentKey: agentConfig.id, aliases, family: "openclaw", name: agentConfig.identity?.name || agentConfig.name || agentConfig.id, host: "ubuntu", role: agentConfig.identity?.theme || workspace.role, runtimePath: workspace.workspaceRoot || OPENCLAW_AGENTS_DIR, configPath: OPENCLAW_CONFIG_PATH, model: agentConfig.model?.primary || null, emoji: agentConfig.identity?.emoji || "🦞", channels: getOpenClawChannels(agentConfig.id, config), tools: workspace.tools, capabilities: workspace.capabilities, files: workspace.files, status: deriveStatus(taskBuckets.activeTasks.length), workload: taskBuckets.activeTasks.length, activeTasks: taskBuckets.activeTasks, completedTasks: taskBuckets.completedTasks, currentTask: workspace.currentTask, notes: workspace.noteValues, } satisfies FleetAgent; }), ); } async function buildZeroClawAgents() { const configuredAgents = [ { slug: "grizzley-zeroclaw", assignmentKey: "grizzley-zeroclaw", aliases: ["grizzley-zeroclaw", "ZeroClaw Grizzley", "grizzley"], name: "ZeroClaw Grizzley", host: "grizzley", role: "Edge host operator for grizzley", runtimePath: ZEROCLAW_PRIMARY_DIR, configPath: path.join(ZEROCLAW_PRIMARY_DIR, "config.toml"), model: "glm-4.7", emoji: "🛰️", channels: [ { label: "Gateway", value: "HTTP gateway :3000" }, { label: "Access", value: "paired remote gateway via ice" }, ], notes: ["Host-scoped runtime for Traefik, OpenCode, and local services."], }, { slug: "ice-zeroclaw", assignmentKey: "ice-zeroclaw", aliases: ["ice-zeroclaw", "ZeroClaw Ice", "ZeroClaw Admin", "ice"], name: "ZeroClaw Ice", host: "ice", role: "Control-plane operator for ice", runtimePath: ZEROCLAW_CONTROL_DIR, configPath: path.join(ZEROCLAW_CONTROL_DIR, "config.toml"), model: "glm-5", emoji: "🧊", channels: [ { label: "Telegram", value: "Homelab-Ice topics 11-15" }, { label: "Gateway", value: "paired webhook + status routing" }, ], notes: ["Control-plane runtime and topic router for remote host delegation."], }, ]; return Promise.all( configuredAgents.map(async (configuredAgent) => { const workspace = readWorkspaceAgent(configuredAgent.runtimePath, configuredAgent.name); const taskBuckets = await fetchTaskBuckets(configuredAgent.aliases); return { ...configuredAgent, family: "zeroclaw" as const, tools: workspace.tools, capabilities: workspace.capabilities, files: workspace.files, status: deriveStatus(taskBuckets.activeTasks.length), workload: taskBuckets.activeTasks.length, activeTasks: taskBuckets.activeTasks, completedTasks: taskBuckets.completedTasks, currentTask: workspace.currentTask, notes: [...configuredAgent.notes, ...workspace.noteValues], }; }), ); } export async function listFleetAgents() { const [openclawAgents, zeroclawAgents] = await Promise.all([ buildOpenClawAgents(), buildZeroClawAgents(), ]); return [...openclawAgents, ...zeroclawAgents]; } export async function listArchitecture() { const agents = await listFleetAgents(); return { ...ARCHITECTURE_DOCUMENT, generatedAt: new Date().toISOString(), sections: ARCHITECTURE_DOCUMENT.sections.map((section) => ({ ...section, configuredAgents: agents .filter((agent) => agent.family === section.id) .map((agent) => `${agent.name} (${agent.host})`), })), }; } function deriveStatus(activeTaskCount: number): AgentStatus { return activeTaskCount > 0 ? "busy" : "active"; }