250 lines
6.7 KiB
TypeScript
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);
|
|
}
|