import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises'; import { constants } from 'node:fs'; import { join } from 'node:path'; import { listAgentsSnapshot, type AgentSummary } from './agent-config'; import { expandPath, getOpenClawDir } from './paths'; export const AGENT_SYSTEM_DOCUMENTS = [ { kind: 'soul', fileName: 'SOUL.md', }, { kind: 'identity', fileName: 'IDENTITY.md', }, { kind: 'user', fileName: 'USER.md', }, { kind: 'agent', fileName: 'AGENTS.md', }, { kind: 'tool', fileName: 'TOOLS.md', }, { kind: 'heartbeat', fileName: 'HEARTBEAT.md', }, { kind: 'boot', fileName: 'BOOT.md', }, ] as const; export type AgentSystemDocumentKind = typeof AGENT_SYSTEM_DOCUMENTS[number]['kind']; export type AgentSystemDocumentSource = 'workspace' | 'template' | 'empty'; export interface AgentSystemDocumentAgent { id: string; name: string; isDefault: boolean; workspace: string; } export interface AgentSystemDocument { kind: AgentSystemDocumentKind; fileName: string; path: string; exists: boolean; source: AgentSystemDocumentSource; content: string; size: number; updatedAt: number | null; templateAvailable: boolean; templatePath: string; } export interface AgentSystemDocumentsSnapshot { success: true; selectedAgentId: string; defaultAgentId: string; agents: AgentSystemDocumentAgent[]; documents: AgentSystemDocument[]; paths: { workspace: string; templateDir: string; }; } const MAX_DOCUMENT_BYTES = 512 * 1024; function getDescriptor(kind: string): typeof AGENT_SYSTEM_DOCUMENTS[number] | null { return AGENT_SYSTEM_DOCUMENTS.find((document) => document.kind === kind) ?? null; } export function isAgentSystemDocumentKind(kind: string): kind is AgentSystemDocumentKind { return getDescriptor(kind) !== null; } async function fileExists(path: string): Promise { try { await access(path, constants.F_OK); return true; } catch { return false; } } function resolveTemplateDir(): string { return join(getOpenClawDir(), 'docs', 'reference', 'templates'); } function resolveTemplatePath(fileName: string): string { return join(resolveTemplateDir(), fileName); } function toDocumentAgent(agent: AgentSummary): AgentSystemDocumentAgent { return { id: agent.id, name: agent.name, isDefault: agent.isDefault, workspace: agent.workspace, }; } async function resolveAgent(agentId?: string): Promise<{ selected: AgentSystemDocumentAgent; defaultAgentId: string; agents: AgentSystemDocumentAgent[]; }> { const snapshot = await listAgentsSnapshot(); const agents = snapshot.agents.map(toDocumentAgent); if (agents.length === 0) { throw new Error('No OpenClaw agents are configured'); } const requestedAgentId = typeof agentId === 'string' ? agentId.trim() : ''; const selected = requestedAgentId ? agents.find((agent) => agent.id === requestedAgentId) : agents.find((agent) => agent.id === snapshot.defaultAgentId) ?? agents[0]; if (!selected) { throw new Error(`Agent "${requestedAgentId}" not found`); } return { selected, defaultAgentId: snapshot.defaultAgentId, agents, }; } async function readTemplate(fileName: string): Promise { const templatePath = resolveTemplatePath(fileName); if (!(await fileExists(templatePath))) return null; return readFile(templatePath, 'utf8'); } async function readDocument(agent: AgentSystemDocumentAgent, kind: AgentSystemDocumentKind): Promise { const descriptor = getDescriptor(kind); if (!descriptor) { throw new Error(`Unsupported system document kind "${kind}"`); } const workspace = expandPath(agent.workspace); const documentPath = join(workspace, descriptor.fileName); const templatePath = resolveTemplatePath(descriptor.fileName); const template = await readTemplate(descriptor.fileName); if (await fileExists(documentPath)) { const [content, stats] = await Promise.all([ readFile(documentPath, 'utf8'), stat(documentPath), ]); return { kind, fileName: descriptor.fileName, path: documentPath, exists: true, source: 'workspace', content, size: stats.size, updatedAt: stats.mtimeMs, templateAvailable: template !== null, templatePath, }; } const content = template ?? ''; return { kind, fileName: descriptor.fileName, path: documentPath, exists: false, source: template === null ? 'empty' : 'template', content, size: 0, updatedAt: null, templateAvailable: template !== null, templatePath, }; } export async function readAgentSystemDocuments(agentId?: string): Promise { const { selected, defaultAgentId, agents } = await resolveAgent(agentId); const documents = await Promise.all( AGENT_SYSTEM_DOCUMENTS.map((document) => readDocument(selected, document.kind)), ); return { success: true, selectedAgentId: selected.id, defaultAgentId, agents, documents, paths: { workspace: expandPath(selected.workspace), templateDir: resolveTemplateDir(), }, }; } export async function saveAgentSystemDocument( agentId: string | undefined, kind: AgentSystemDocumentKind, content: string, ): Promise { if (typeof content !== 'string') { throw new Error('Document content must be a string'); } const byteLength = Buffer.byteLength(content, 'utf8'); if (byteLength > MAX_DOCUMENT_BYTES) { throw new Error(`Document is too large (${byteLength} bytes, max ${MAX_DOCUMENT_BYTES})`); } const { selected } = await resolveAgent(agentId); const descriptor = getDescriptor(kind); if (!descriptor) { throw new Error(`Unsupported system document kind "${kind}"`); } const workspace = expandPath(selected.workspace); await mkdir(workspace, { recursive: true }); await writeFile(join(workspace, descriptor.fileName), content, 'utf8'); return readAgentSystemDocuments(selected.id); } export async function resetAgentSystemDocument( agentId: string | undefined, kind: AgentSystemDocumentKind, ): Promise { const { selected } = await resolveAgent(agentId); const descriptor = getDescriptor(kind); if (!descriptor) { throw new Error(`Unsupported system document kind "${kind}"`); } const template = await readTemplate(descriptor.fileName); if (template === null) { throw new Error(`Template for ${descriptor.fileName} is not available`); } const workspace = expandPath(selected.workspace); await mkdir(workspace, { recursive: true }); await writeFile(join(workspace, descriptor.fileName), template, 'utf8'); return readAgentSystemDocuments(selected.id); }