openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
73 lines (72 loc) • 3.89 kB
JavaScript
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
//#region src/skills/loading/skill-contract.ts
function escapeSkillXml(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
const SKILL_FRONTMATTER_BLOCK = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u;
const SKILL_TITLE_HEADING = /^#\s+(.+?)\s*#*\s*$/mu;
function humanizeSkillIdentifier(value) {
return value.trim().split(/[-_]+/u).filter(Boolean).map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join(" ");
}
function resolveSkillDisplayName(content, fallbackName) {
return content.replace(SKILL_FRONTMATTER_BLOCK, "").match(SKILL_TITLE_HEADING)?.[1]?.trim() || humanizeSkillIdentifier(fallbackName) || fallbackName;
}
function truncateSkillDescription(description, maxChars) {
const normalized = description.replace(/\s+/g, " ").trim();
if (normalized.length <= maxChars) return normalized;
if (maxChars <= 3) return truncateUtf16Safe(normalized, maxChars);
return `${truncateUtf16Safe(normalized, maxChars - 3).trimEnd()}...`;
}
/**
* Keep this formatter's XML layout byte-for-byte aligned with the upstream
* Agent Skills formatter so we can avoid importing the full session runtime
* package root on the cold skills path. Visibility policy is applied upstream
* before calling this helper.
*/
function formatSkillsForPromptCore(skills) {
if (skills.length === 0) return "";
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
"Read a skill's file at its listed location when the task matches its description.",
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
"",
"<available_skills>"
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeSkillXml(skill.name)}</name>`);
lines.push(` <description>${escapeSkillXml(skill.description)}</description>`);
lines.push(` <location>${escapeSkillXml(skill.filePath)}</location>`);
if (skill.locationNote) lines.push(` <location_note>${escapeSkillXml(skill.locationNote)}</location_note>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}
/** Compact prompt catalog with descriptions bounded independently from identities. */
function formatSkillsCompactForPrompt(skills, opts) {
if (skills.length === 0) return "";
const descriptionMaxChars = Math.max(0, Math.floor(opts?.descriptionMaxChars ?? 220));
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
descriptionMaxChars > 0 ? "Read a skill's file at its listed location when the task matches its name or description." : "Read a skill's file at its listed location when the task matches its name.",
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
"",
"<available_skills>"
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeSkillXml(skill.name)}</name>`);
if (descriptionMaxChars > 0) {
const description = truncateSkillDescription(skill.description, descriptionMaxChars);
if (description) lines.push(` <description>${escapeSkillXml(description)}</description>`);
}
lines.push(` <location>${escapeSkillXml(skill.filePath)}</location>`);
if (skill.locationNote) lines.push(` <location_note>${escapeSkillXml(skill.locationNote)}</location_note>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}
//#endregion
export { resolveSkillDisplayName as i, formatSkillsCompactForPrompt as n, formatSkillsForPromptCore as r, escapeSkillXml as t };