openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
189 lines (188 loc) • 8.09 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-6kR-86XC.mjs";
import { r as DEFAULT_IDENTITY_FILENAME } from "./workspace-bootstrap-policy-DaS4N8nC.mjs";
import fs from "node:fs";
import path from "node:path";
import { openRootFileSync, readFileDescriptorBoundedSync } from "@openclaw/fs-safe/advanced";
import { FsSafeError } from "@openclaw/fs-safe/errors";
//#region src/agents/identity-file.ts
const MAX_IDENTITY_FILE_BYTES = 4194304;
const WRITABLE_IDENTITY_FIELDS = [
["name", "Name"],
["theme", "Theme"],
["emoji", "Emoji"],
["avatar", "Avatar"]
];
const RICH_IDENTITY_LABELS = /* @__PURE__ */ new Set([
"name",
"creature",
"vibe",
"theme",
"emoji",
"avatar"
]);
const IDENTITY_PLACEHOLDER_VALUES = /* @__PURE__ */ new Set([
"not set yet",
"pick something you like",
"ai? robot? familiar? ghost in the machine? something weirder?",
"how do you come across? sharp? warm? chaotic? calm?",
"your signature - pick one that feels right",
"workspace-relative path, http(s) url, or data uri"
]);
function sanitizeAgentIdentityLine(value) {
return value.replace(/\s+/g, " ").trim();
}
function normalizeIdentityForFile(identity) {
const resolved = {};
for (const [field] of WRITABLE_IDENTITY_FIELDS) {
const value = identity?.[field]?.trim();
if (value) resolved[field] = value;
}
return Object.keys(resolved).length ? resolved : void 0;
}
function createAgentIdentityConfig(params) {
return normalizeIdentityForFile({
...params.name ? { name: sanitizeAgentIdentityLine(params.name) } : {},
emoji: sanitizeAgentIdentityLine(normalizeOptionalString(params.emoji) ?? ""),
avatar: sanitizeAgentIdentityLine(normalizeOptionalString(params.avatar) ?? "")
});
}
function normalizeIdentityValue(value) {
let normalized = value.trim();
normalized = normalized.replace(/^[*_`\s]+|[*_`\s]+$/g, "").trim();
if (normalized.startsWith("(") && normalized.endsWith(")")) normalized = normalized.slice(1, -1).trim();
normalized = normalized.replace(/[\u2013\u2014]/g, "-");
return normalizeLowercaseStringOrEmpty(normalized.replace(/\s+/g, " "));
}
function normalizeIdentityLabel(label) {
return normalizeLowercaseStringOrEmpty(label.replace(/[*_`]/g, ""));
}
function parseIdentityLine(line) {
const cleaned = line.trim().replace(/^\s*-\s*/, "");
const colonIndex = cleaned.indexOf(":");
return colonIndex === -1 ? void 0 : {
label: normalizeIdentityLabel(cleaned.slice(0, colonIndex)),
value: cleaned.slice(colonIndex + 1)
};
}
function parseIdentityMarkdown(content) {
const identity = {};
for (const line of content.split(/\r?\n/)) {
const parsed = parseIdentityLine(line);
if (!parsed) continue;
const value = parsed.value.replace(/^[*_`\s]+|[*_`\s]+$/g, "").trim();
if (!value || IDENTITY_PLACEHOLDER_VALUES.has(normalizeIdentityValue(value))) continue;
switch (parsed.label) {
case "name":
case "emoji":
case "creature":
case "vibe":
case "theme":
case "avatar": identity[parsed.label] = value;
}
}
return Object.keys(identity).length > 0 ? identity : null;
}
function resolveIdentityInsertIndex(lines) {
const lastIdentityIndex = lines.findLastIndex((line) => RICH_IDENTITY_LABELS.has(parseIdentityLine(line)?.label ?? ""));
if (lastIdentityIndex >= 0) return lastIdentityIndex + 1;
const headingIndex = lines.findIndex((line) => line.trim().startsWith("#"));
if (headingIndex === -1) return 0;
let insertIndex = headingIndex + 1;
while (insertIndex < lines.length && lines[insertIndex]?.trim() === "") insertIndex += 1;
return insertIndex;
}
/**
* Merge writable identity fields into existing IDENTITY.md content, replacing
* duplicate labels and preserving unrelated markdown.
*/
function mergeIdentityMarkdownContent(content, identity) {
const nextLines = content ? content.replace(/\r\n/g, "\n").split("\n") : ["# IDENTITY.md - Agent Identity", ""];
for (const [field, label] of WRITABLE_IDENTITY_FIELDS) {
const value = identity[field]?.trim();
if (!value) continue;
const identityLine = `- ${label}: ${value}`;
const [firstIndex, ...duplicateIndexes] = nextLines.reduce((indexes, line, index) => {
if (line.trim().startsWith("-") && parseIdentityLine(line)?.label === field) indexes.push(index);
return indexes;
}, []);
if (firstIndex !== void 0) {
nextLines[firstIndex] = identityLine;
for (const duplicateIndex of duplicateIndexes.toReversed()) nextLines.splice(duplicateIndex, 1);
continue;
}
const insertIndex = resolveIdentityInsertIndex(nextLines);
nextLines.splice(insertIndex, 0, identityLine);
}
return nextLines.join("\n").replace(/\n*$/, "\n");
}
async function buildIdentityMarkdownForWrite(params) {
const workspaces = params.fallbackWorkspaceDir ? params.preferFallbackWorkspaceContent ? [params.fallbackWorkspaceDir, params.workspaceDir] : [params.workspaceDir, params.fallbackWorkspaceDir] : [params.workspaceDir];
for (const workspaceDir of workspaces) {
const content = await params.readWorkspaceFileContent(workspaceDir, DEFAULT_IDENTITY_FILENAME);
if (content !== void 0) return mergeIdentityMarkdownContent(content, params.identity);
}
return mergeIdentityMarkdownContent(void 0, params.identity);
}
/** Shared admission kernel for the worker and the shipped synchronous SDK reader. */
function readIdentityFileSnapshot(input) {
try {
const resolvedPath = fs.realpathSync(input.identityPath);
const opened = openRootFileSync({
absolutePath: resolvedPath,
rootPath: path.dirname(resolvedPath),
rootRealPath: path.dirname(resolvedPath),
boundaryLabel: "identity file directory",
rejectHardlinks: false
});
if (!opened.ok) return { kind: "missing" };
try {
const { dev, ino, size, mtimeMs, ctimeMs } = opened.stat;
if (size > MAX_IDENTITY_FILE_BYTES) return { kind: "too-large" };
const revision = JSON.stringify([
opened.path,
dev,
ino,
size,
mtimeMs,
ctimeMs
]);
if (revision === input.knownRevision) return { kind: "unchanged" };
const buffer = readFileDescriptorBoundedSync(opened.fd, MAX_IDENTITY_FILE_BYTES);
const identity = parseIdentityMarkdown(buffer.toString("utf-8"));
return {
kind: "loaded",
revision,
size: buffer.byteLength,
identity
};
} finally {
fs.closeSync(opened.fd);
}
} catch (error) {
return { kind: error instanceof FsSafeError && error.code === "too-large" ? "too-large" : "missing" };
}
}
/** Load a specific identity file when it exists and contains real values. */
async function loadAgentIdentityFromFile(identityPath) {
const { prepareIdentityFile } = await import("./identity-file-runtime-Br9lquza.mjs");
const result = await prepareIdentityFile(identityPath);
if (result.kind === "too-large") throw new Error(`Identity file ${identityPath} exceeds the maximum size of ${MAX_IDENTITY_FILE_BYTES} bytes`, { cause: new FsSafeError("too-large", `File exceeds ${MAX_IDENTITY_FILE_BYTES} bytes: ${identityPath}`) });
return result.kind === "loaded" ? result.identity : null;
}
/** Retained synchronous contract for the shipped agent-avatar Plugin SDK facade. */
function loadAgentIdentityFromWorkspace(workspace) {
const result = readIdentityFileSnapshot({ identityPath: path.join(workspace, DEFAULT_IDENTITY_FILENAME) });
return result.kind === "loaded" ? result.identity : null;
}
/** Workspace presentation treats unavailable or unreadable identity files as absent. */
async function loadAgentIdentityFromWorkspaceAsync(workspace) {
try {
const { prepareIdentityFile } = await import("./identity-file-runtime-Br9lquza.mjs");
const result = await prepareIdentityFile(path.join(workspace, DEFAULT_IDENTITY_FILENAME));
return result.kind === "loaded" ? result.identity : null;
} catch {
return null;
}
}
//#endregion
export { loadAgentIdentityFromWorkspaceAsync as a, readIdentityFileSnapshot as c, loadAgentIdentityFromWorkspace as i, sanitizeAgentIdentityLine as l, createAgentIdentityConfig as n, mergeIdentityMarkdownContent as o, loadAgentIdentityFromFile as r, normalizeIdentityForFile as s, buildIdentityMarkdownForWrite as t };