Files
NianToB/electron/utils/agent-system-documents.ts

250 lines
6.7 KiB
TypeScript

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<boolean> {
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<string | null> {
const templatePath = resolveTemplatePath(fileName);
if (!(await fileExists(templatePath))) return null;
return readFile(templatePath, 'utf8');
}
async function readDocument(agent: AgentSystemDocumentAgent, kind: AgentSystemDocumentKind): Promise<AgentSystemDocument> {
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<AgentSystemDocumentsSnapshot> {
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<AgentSystemDocumentsSnapshot> {
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<AgentSystemDocumentsSnapshot> {
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);
}