import fs from "node:fs"; import path from "node:path"; import { resolveAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId, } from "../agents/agent-scope.js"; import { DEFAULT_IDENTITY_FILENAME } from "../agents/workspace.js"; import type { ClawdbotConfig } from "../config/config.js"; import { normalizeAgentId } from "../routing/session-key.js"; export type AgentSummary = { id: string; name?: string; identityName?: string; identityEmoji?: string; identitySource?: "identity" | "config"; workspace: string; agentDir: string; model?: string; bindings: number; bindingDetails?: string[]; routes?: string[]; providers?: string[]; isDefault: boolean; }; type AgentEntry = NonNullable< NonNullable["list"] >[number]; type AgentIdentity = { name?: string; emoji?: string; creature?: string; vibe?: string; }; export function listAgentEntries(cfg: ClawdbotConfig): AgentEntry[] { const list = cfg.agents?.list; if (!Array.isArray(list)) return []; return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object"), ); } export function findAgentEntryIndex( list: AgentEntry[], agentId: string, ): number { const id = normalizeAgentId(agentId); return list.findIndex((entry) => normalizeAgentId(entry.id) === id); } function resolveAgentName(cfg: ClawdbotConfig, agentId: string) { const entry = listAgentEntries(cfg).find( (agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId), ); return entry?.name?.trim() || undefined; } function resolveAgentModel(cfg: ClawdbotConfig, agentId: string) { const entry = listAgentEntries(cfg).find( (agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId), ); if (entry?.model) { if (typeof entry.model === "string" && entry.model.trim()) { return entry.model.trim(); } if (typeof entry.model === "object") { const primary = entry.model.primary?.trim(); if (primary) return primary; } } const raw = cfg.agents?.defaults?.model; if (typeof raw === "string") return raw; return raw?.primary?.trim() || undefined; } function parseIdentityMarkdown(content: string): AgentIdentity { const identity: AgentIdentity = {}; const lines = content.split(/\r?\n/); for (const line of lines) { const match = line.match(/^\s*(?:-\s*)?([A-Za-z ]+):\s*(.+?)\s*$/); if (!match) continue; const label = match[1]?.trim().toLowerCase(); const value = match[2]?.trim(); if (!value) continue; if (label === "name") identity.name = value; if (label === "emoji") identity.emoji = value; if (label === "creature") identity.creature = value; if (label === "vibe") identity.vibe = value; } return identity; } function loadAgentIdentity(workspace: string): AgentIdentity | null { const identityPath = path.join(workspace, DEFAULT_IDENTITY_FILENAME); try { const content = fs.readFileSync(identityPath, "utf-8"); const parsed = parseIdentityMarkdown(content); if (!parsed.name && !parsed.emoji) return null; return parsed; } catch { return null; } } export function buildAgentSummaries(cfg: ClawdbotConfig): AgentSummary[] { const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg)); const configuredAgents = listAgentEntries(cfg); const orderedIds = configuredAgents.length > 0 ? configuredAgents.map((agent) => normalizeAgentId(agent.id)) : [defaultAgentId]; const bindingCounts = new Map(); for (const binding of cfg.bindings ?? []) { const agentId = normalizeAgentId(binding.agentId); bindingCounts.set(agentId, (bindingCounts.get(agentId) ?? 0) + 1); } const ordered = orderedIds.filter( (id, index) => orderedIds.indexOf(id) === index, ); return ordered.map((id) => { const workspace = resolveAgentWorkspaceDir(cfg, id); const identity = loadAgentIdentity(workspace); const configIdentity = configuredAgents.find( (agent) => normalizeAgentId(agent.id) === id, )?.identity; const identityName = identity?.name ?? configIdentity?.name?.trim(); const identityEmoji = identity?.emoji ?? configIdentity?.emoji?.trim(); const identitySource = identity ? "identity" : configIdentity && (identityName || identityEmoji) ? "config" : undefined; return { id, name: resolveAgentName(cfg, id), identityName, identityEmoji, identitySource, workspace, agentDir: resolveAgentDir(cfg, id), model: resolveAgentModel(cfg, id), bindings: bindingCounts.get(id) ?? 0, isDefault: id === defaultAgentId, }; }); } export function applyAgentConfig( cfg: ClawdbotConfig, params: { agentId: string; name?: string; workspace?: string; agentDir?: string; model?: string; }, ): ClawdbotConfig { const agentId = normalizeAgentId(params.agentId); const name = params.name?.trim(); const list = listAgentEntries(cfg); const index = findAgentEntryIndex(list, agentId); const base = index >= 0 ? list[index] : { id: agentId }; const nextEntry: AgentEntry = { ...base, ...(name ? { name } : {}), ...(params.workspace ? { workspace: params.workspace } : {}), ...(params.agentDir ? { agentDir: params.agentDir } : {}), ...(params.model ? { model: params.model } : {}), }; const nextList = [...list]; if (index >= 0) { nextList[index] = nextEntry; } else { if ( nextList.length === 0 && agentId !== normalizeAgentId(resolveDefaultAgentId(cfg)) ) { nextList.push({ id: resolveDefaultAgentId(cfg) }); } nextList.push(nextEntry); } return { ...cfg, agents: { ...cfg.agents, list: nextList, }, }; } export function pruneAgentConfig( cfg: ClawdbotConfig, agentId: string, ): { config: ClawdbotConfig; removedBindings: number; removedAllow: number; } { const id = normalizeAgentId(agentId); const agents = listAgentEntries(cfg); const nextAgentsList = agents.filter( (entry) => normalizeAgentId(entry.id) !== id, ); const nextAgents = nextAgentsList.length > 0 ? nextAgentsList : undefined; const bindings = cfg.bindings ?? []; const filteredBindings = bindings.filter( (binding) => normalizeAgentId(binding.agentId) !== id, ); const allow = cfg.tools?.agentToAgent?.allow ?? []; const filteredAllow = allow.filter((entry) => entry !== id); const nextAgentsConfig = cfg.agents ? { ...cfg.agents, list: nextAgents } : nextAgents ? { list: nextAgents } : undefined; const nextTools = cfg.tools?.agentToAgent ? { ...cfg.tools, agentToAgent: { ...cfg.tools.agentToAgent, allow: filteredAllow.length > 0 ? filteredAllow : undefined, }, } : cfg.tools; return { config: { ...cfg, agents: nextAgentsConfig, bindings: filteredBindings.length > 0 ? filteredBindings : undefined, tools: nextTools, }, removedBindings: bindings.length - filteredBindings.length, removedAllow: allow.length - filteredAllow.length, }; }