import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { ChannelStore, ChannelMode } from "../core/channel-store.js"; import type { IdentityRegistry } from "../core/identity-registry.js"; import { fetchAdminGuilds, fetchGuildChannels } from "../core/moderator-discord.js"; import { scanPaddedCell } from "../core/padded-cell.js"; import path from "node:path"; import os from "node:os"; const SWITCHABLE_MODES: ChannelMode[] = ["none", "chat", "report"]; const LOCKED_MODES = new Set(["work", "discussion"]); function html(strings: TemplateStringsArray, ...values: unknown[]): string { return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), ""); } function escapeHtml(s: unknown): string { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function modeBadge(mode: ChannelMode): string { const colors: Record = { none: "#888", chat: "#5865f2", report: "#57f287", work: "#fee75c", discussion: "#eb459e", }; return `${escapeHtml(mode)}`; } function buildPage(content: string): string { return ` Dirigent

Dirigent

OpenClaw multi-agent turn management

${content} `; } export function registerControlPage(deps: { api: OpenClawPluginApi; channelStore: ChannelStore; identityRegistry: IdentityRegistry; moderatorBotToken: string | undefined; openclawDir: string; hasPaddedCell: () => boolean; }): void { const { api, channelStore, identityRegistry, moderatorBotToken, openclawDir, hasPaddedCell } = deps; // ── Main page ────────────────────────────────────────────────────────────── api.registerHttpRoute({ path: "/dirigent", auth: "plugin", match: "exact", handler: async (_req, res) => { const entries = identityRegistry.list(); const paddedCellBtn = hasPaddedCell() ? `` : ""; // Build identity table rows const identityRows = entries.map((e) => html` ${escapeHtml(e.discordUserId)} ${escapeHtml(e.agentId)} ${escapeHtml(e.agentName)} `).join(""); // Build guild sections let guildHtml = "

Loading guilds…

"; if (moderatorBotToken) { try { const guilds = await fetchAdminGuilds(moderatorBotToken); if (guilds.length === 0) { guildHtml = "

No guilds with admin permissions found.

"; } else { guildHtml = ""; for (const guild of guilds) { const channels = await fetchGuildChannels(moderatorBotToken, guild.id); const channelRows = channels.map((ch) => { const mode = channelStore.getMode(ch.id); const locked = LOCKED_MODES.has(mode); const dropdown = locked ? modeBadge(mode) : ``; return html` ${escapeHtml(ch.id)} #${escapeHtml(ch.name)} ${dropdown} `; }).join(""); guildHtml += html`
${escapeHtml(guild.name)} ${escapeHtml(guild.id)}
${channelRows}
Channel IDNameMode
`; } } } catch (err) { guildHtml = `

Failed to load guilds: ${escapeHtml(String(err))}

`; } } else { guildHtml = "

moderatorBotToken not configured — cannot list guilds.

"; } const content = html`

Identity Registry

${identityRows}
Discord User IDAgent IDAgent Name
${paddedCellBtn}

Guild & Channel Configuration

${guildHtml} `; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(buildPage(content)); }, }); // ── API: add identity ────────────────────────────────────────────────────── api.registerHttpRoute({ path: "/dirigent/api/identity", auth: "plugin", match: "exact", handler: (req, res) => { if (req.method !== "POST") { res.writeHead(405); res.end(); return; } let body = ""; req.on("data", (c: Buffer) => { body += c.toString(); }); req.on("end", () => { try { const { discordUserId, agentId, agentName } = JSON.parse(body); if (!discordUserId || !agentId) throw new Error("discordUserId and agentId required"); identityRegistry.upsert({ discordUserId, agentId, agentName: agentName ?? agentId }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); } catch (err) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: String(err) })); } }); }, }); // ── API: remove identity ─────────────────────────────────────────────────── api.registerHttpRoute({ path: "/dirigent/api/identity/", auth: "plugin", match: "prefix", handler: (req, res) => { if (req.method !== "DELETE") { res.writeHead(405); res.end(); return; } const agentId = decodeURIComponent((req.url ?? "").replace("/dirigent/api/identity/", "")); const removed = identityRegistry.remove(agentId); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: removed, error: removed ? undefined : "Not found" })); }, }); // ── API: set channel mode ────────────────────────────────────────────────── api.registerHttpRoute({ path: "/dirigent/api/channel-mode", auth: "plugin", match: "exact", handler: (req, res) => { if (req.method !== "POST") { res.writeHead(405); res.end(); return; } let body = ""; req.on("data", (c: Buffer) => { body += c.toString(); }); req.on("end", () => { try { const { channelId, mode } = JSON.parse(body) as { channelId: string; mode: ChannelMode }; if (!channelId || !mode) throw new Error("channelId and mode required"); if (LOCKED_MODES.has(mode)) throw new Error(`Mode "${mode}" is locked to creation tools`); channelStore.setMode(channelId, mode); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); } catch (err) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: String(err) })); } }); }, }); // ── API: rescan padded-cell ──────────────────────────────────────────────── api.registerHttpRoute({ path: "/dirigent/api/rescan-padded-cell", auth: "plugin", match: "exact", handler: (req, res) => { if (req.method !== "POST") { res.writeHead(405); res.end(); return; } const count = scanPaddedCell(identityRegistry, openclawDir, api.logger); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: count >= 0, count, error: count < 0 ? "padded-cell not detected" : undefined })); }, }); }