openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
195 lines (194 loc) • 7.75 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { i as readRegularFileSync, r as readRegularFile } from "./regular-file-Equ4rrfE.js";
import { r as DEFAULT_IDENTITY_FILENAME } from "./workspace-ConDEamr.js";
import fs from "node:fs";
import path from "node:path";
//#region src/agents/identity-file.ts
/**
* IDENTITY.md parsing and writing support.
* The parser accepts human-authored markdown, while the writer only updates
* stable rich identity fields.
*/
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();
}
const IDENTITY_CONFIG_FIELDS = [
"name",
"theme",
"emoji",
"avatar"
];
function compactIdentityConfig(identity) {
const resolved = {};
for (const field of IDENTITY_CONFIG_FIELDS) {
const value = identity[field]?.trim();
if (value) resolved[field] = value;
}
return Object.keys(resolved).length ? resolved : void 0;
}
function createAgentIdentityConfig(params) {
return compactIdentityConfig({
...params.name ? { name: sanitizeAgentIdentityLine(params.name) } : {},
emoji: sanitizeAgentIdentityLine(normalizeOptionalString(params.emoji) ?? ""),
avatar: sanitizeAgentIdentityLine(normalizeOptionalString(params.avatar) ?? "")
});
}
function normalizeIdentityForFile(identity) {
return identity ? compactIdentityConfig(identity) : void 0;
}
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 isIdentityPlaceholder(value) {
const normalized = normalizeIdentityValue(value);
return IDENTITY_PLACEHOLDER_VALUES.has(normalized);
}
/** Parse rich identity fields from human-authored markdown content. */
function parseIdentityMarkdown(content) {
const identity = {};
const lines = content.split(/\r?\n/);
for (const line of lines) {
const cleaned = line.trim().replace(/^\s*-\s*/, "");
const colonIndex = cleaned.indexOf(":");
if (colonIndex === -1) continue;
const label = normalizeIdentityLabel(cleaned.slice(0, colonIndex));
const value = cleaned.slice(colonIndex + 1).replace(/^[*_`\s]+|[*_`\s]+$/g, "").trim();
if (!value) continue;
if (isIdentityPlaceholder(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;
if (label === "theme") identity.theme = value;
if (label === "avatar") identity.avatar = value;
}
return identity;
}
/** Return true when the parsed identity has any meaningful user-supplied value. */
function identityHasValues(identity) {
return Boolean(identity.name || identity.emoji || identity.theme || identity.creature || identity.vibe || identity.avatar);
}
function buildIdentityLine(label, value) {
return `- ${label}: ${value}`;
}
function matchesIdentityLabel(line, label) {
const trimmed = line.trim();
if (!trimmed.startsWith("-")) return false;
const cleaned = trimmed.replace(/^\s*-\s*/, "");
const colonIndex = cleaned.indexOf(":");
if (colonIndex === -1) return false;
return normalizeIdentityLabel(cleaned.slice(0, colonIndex)) === normalizeIdentityLabel(label);
}
function normalizeIdentityContent(content) {
if (!content) return [];
return content.replace(/\r\n/g, "\n").split("\n");
}
function resolveIdentityInsertIndex(lines) {
let lastIdentityIndex = -1;
for (const [index, line] of lines.entries()) {
const cleaned = line.trim().replace(/^\s*-\s*/, "");
const colonIndex = cleaned.indexOf(":");
if (colonIndex === -1) continue;
const label = normalizeIdentityLabel(cleaned.slice(0, colonIndex));
if (RICH_IDENTITY_LABELS.has(label)) lastIdentityIndex = index;
}
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 lines = normalizeIdentityContent(content);
const nextLines = lines.length > 0 ? [...lines] : ["# IDENTITY.md - Agent Identity", ""];
for (const [field, label] of WRITABLE_IDENTITY_FIELDS) {
const value = identity[field]?.trim();
if (!value) continue;
const matchingIndexes = nextLines.reduce((indexes, line, index) => {
if (matchesIdentityLabel(line, label)) indexes.push(index);
return indexes;
}, []);
if (matchingIndexes.length > 0) {
const [firstIndex, ...duplicateIndexes] = matchingIndexes;
if (firstIndex === void 0) continue;
nextLines[firstIndex] = buildIdentityLine(label, value);
for (const duplicateIndex of duplicateIndexes.toReversed()) nextLines.splice(duplicateIndex, 1);
continue;
}
const insertIndex = resolveIdentityInsertIndex(nextLines);
nextLines.splice(insertIndex, 0, buildIdentityLine(label, value));
}
return nextLines.join("\n").replace(/\n*$/, "\n");
}
function loadIdentityFromFile(identityPath) {
try {
const resolvedPath = fs.realpathSync(identityPath);
const { buffer } = readRegularFileSync({
filePath: resolvedPath,
maxBytes: MAX_IDENTITY_FILE_BYTES
});
const parsed = parseIdentityMarkdown(buffer.toString("utf-8"));
if (!identityHasValues(parsed)) return null;
return parsed;
} catch {
return null;
}
}
/** Load a specific identity file when it exists and contains real values. */
async function loadAgentIdentityFromFile(identityPath) {
let resolvedPath;
try {
resolvedPath = await fs.promises.realpath(identityPath);
const { buffer } = await readRegularFile({
filePath: resolvedPath,
maxBytes: MAX_IDENTITY_FILE_BYTES
});
const parsed = parseIdentityMarkdown(buffer.toString("utf-8"));
if (!identityHasValues(parsed)) return null;
return parsed;
} catch (error) {
if (resolvedPath && error instanceof Error && error.message === `File exceeds ${MAX_IDENTITY_FILE_BYTES} bytes: ${resolvedPath}`) throw new Error(`Identity file ${identityPath} exceeds the maximum size of ${MAX_IDENTITY_FILE_BYTES} bytes`, { cause: error });
return null;
}
}
/** Load the workspace identity file when it exists and contains real values. */
function loadAgentIdentityFromWorkspace(workspace) {
return loadIdentityFromFile(path.join(workspace, DEFAULT_IDENTITY_FILENAME));
}
//#endregion
export { normalizeIdentityForFile as a, mergeIdentityMarkdownContent as i, loadAgentIdentityFromFile as n, sanitizeAgentIdentityLine as o, loadAgentIdentityFromWorkspace as r, createAgentIdentityConfig as t };