Files
clawdbot/src/commands/agents.config.ts
2026-01-14 05:39:47 +00:00

247 lines
7.0 KiB
TypeScript

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<ClawdbotConfig["agents"]>["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<string, number>();
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,
};
}