[taskboard] migrate fleet console to nextjs
This commit is contained in:
268
lib/agents.ts
Normal file
268
lib/agents.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
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<string, { systemPrompt?: string }>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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<Omit<TaskRecord, "tags"> & { 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<Omit<TaskRecord, "tags"> & { 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";
|
||||
}
|
||||
Reference in New Issue
Block a user