openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
149 lines (148 loc) • 6.99 kB
JavaScript
import { c as normalizeOptionalLowercaseString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-6kR-86XC.mjs";
import { t as getChatCommands } from "./commands-registry.data-DtHn8ZKe.mjs";
//#region src/skills/discovery/chat-command-invocation.ts
const MAX_EXPLICIT_SKILL_REFERENCES = 8;
const MAX_EXPLICIT_SKILL_REFERENCE_CHARS = 512;
const MAX_EXPLICIT_SKILL_INSTRUCTION_CHARS = 1e3;
function skillCommandsToExplicitSelections(skills) {
return skills.flatMap((skill) => skill.skillFile ? [{
name: skill.name,
path: skill.skillFile
}] : []);
}
function mergeExplicitSkillSelections(...groups) {
const merged = /* @__PURE__ */ new Map();
for (const selection of groups.flatMap((group) => group ?? [])) merged.set(`${selection.name}\0${selection.path}`, selection);
return merged.size > 0 ? [...merged.values()] : void 0;
}
/** Lists slash command names reserved by built-in chat commands and callers. */
function listReservedChatSlashCommandNames(extraNames = []) {
const reserved = /* @__PURE__ */ new Set();
for (const command of getChatCommands()) {
if (command.nativeName) reserved.add(normalizeOptionalLowercaseString(command.nativeName) ?? "");
for (const alias of command.textAliases) {
const trimmed = alias.trim();
if (!trimmed.startsWith("/")) continue;
reserved.add(normalizeLowercaseStringOrEmpty(trimmed.slice(1)));
}
}
for (const name of extraNames) {
const trimmed = normalizeOptionalLowercaseString(name);
if (trimmed) reserved.add(trimmed);
}
return reserved;
}
function normalizeSkillCommandLookup(value) {
return (normalizeOptionalLowercaseString(value) ?? "").replace(/[\s_]+/g, "-");
}
function findSkillCommand(skillCommands, rawName) {
const trimmed = rawName.trim();
if (!trimmed) return;
const lowered = normalizeOptionalLowercaseString(trimmed) ?? "";
const normalized = normalizeSkillCommandLookup(trimmed);
return skillCommands.find((entry) => normalizeOptionalLowercaseString(entry.name) === lowered || normalizeOptionalLowercaseString(entry.skillName) === lowered || normalizeSkillCommandLookup(entry.name) === normalized || normalizeSkillCommandLookup(entry.skillName) === normalized);
}
function isEscapedReference(text, index) {
let backslashes = 0;
for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor -= 1) backslashes += 1;
return backslashes % 2 === 1;
}
function* skillReferenceNames(text) {
for (const match of text.matchAll(/\$([-a-zA-Z0-9_:]+)/gu)) {
const name = match[1]?.replace(/:+$/gu, "");
const index = match.index;
if (name && index !== void 0 && !isEscapedReference(text, index) && /[a-z]/u.test(name)) yield name;
}
}
/** Returns true when text may contain an explicit `$skill-name` reference. */
function hasSkillReferenceCandidate(text) {
return !skillReferenceNames(text).next().done;
}
function resolveSkillCommandInvocation(params) {
const match = params.commandBodyNormalized.trim().match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
if (!match) return null;
const commandName = normalizeOptionalLowercaseString(match[1]);
const invocation = commandName === "skill" ? match[2]?.trim().match(/^([^\s]+)(?:\s+([\s\S]+))?$/) : match;
if (!commandName || !invocation) return null;
const command = commandName === "skill" ? findSkillCommand(params.skillCommands, invocation[1] ?? "") : params.skillCommands.find((entry) => normalizeOptionalLowercaseString(entry.name) === commandName);
return command ? {
command,
args: invocation[2]?.trim() || void 0
} : null;
}
function expandBundleCommandPromptTemplate(template, args) {
const normalizedArgs = args?.trim() ?? "";
const rendered = template.includes("$ARGUMENTS") ? template.replaceAll("$ARGUMENTS", () => normalizedArgs) : template;
if (!normalizedArgs || template.includes("$ARGUMENTS")) return rendered.trim();
return `${rendered.trim()}\n\nUser input:\n${normalizedArgs}`;
}
/** Expands model-routed skill references while leaving unknown slash commands untouched. */
function expandExplicitSkillReferences(params) {
const leadingSlash = params.text.trimStart().startsWith("/");
const leadingInvocation = leadingSlash ? resolveSkillCommandInvocation({
commandBodyNormalized: params.text,
skillCommands: params.skillCommands
}) : null;
if (leadingInvocation?.command.promptTemplate) return {
body: expandBundleCommandPromptTemplate(leadingInvocation.command.promptTemplate, leadingInvocation.args),
skills: [leadingInvocation.command]
};
const available = [];
const allCommands = params.allSkillCommands ?? params.skillCommands;
let unavailable;
if (leadingSlash) {
if (leadingInvocation) available.push(leadingInvocation.command);
else if (allCommands !== params.skillCommands) unavailable = resolveSkillCommandInvocation({
commandBodyNormalized: params.text,
skillCommands: allCommands
})?.command;
} else {
const seen = /* @__PURE__ */ new Set();
for (const name of skillReferenceNames(params.text)) {
const command = findSkillCommand(params.skillCommands, name);
if (command) {
if (!command.promptTemplate && !seen.has(command.name)) {
seen.add(command.name);
available.push(command);
}
} else if (allCommands !== params.skillCommands) {
unavailable = findSkillCommand(allCommands, name);
if (unavailable) break;
}
}
}
const error = unavailable ? `Skill "${unavailable.skillName}" is not available for this agent. Update the skill allowlist or choose an allowed skill.` : available.length > MAX_EXPLICIT_SKILL_REFERENCES ? `Too many skill references. Use at most ${MAX_EXPLICIT_SKILL_REFERENCES} skills in one message.` : void 0;
if (error) return {
body: params.text,
error,
skills: []
};
if (available.length === 0) return {
body: params.text,
skills: []
};
const referenceLines = available.map((skill) => skill.modelVisible === false && skill.skillFile ? `- ${skill.skillName} (SKILL.md: ${skill.skillFile})` : `- ${skill.skillName}`);
if (referenceLines.some((line) => line.length > MAX_EXPLICIT_SKILL_REFERENCE_CHARS)) return {
body: params.text,
error: `Skill reference metadata is too long. Keep each rendered reference at ${MAX_EXPLICIT_SKILL_REFERENCE_CHARS} characters or less.`,
skills: []
};
const instructionPrefix = [
"Use the following explicitly referenced skills for this request. Read each skill's SKILL.md before acting:",
...referenceLines,
"",
"User request:",
""
].join("\n");
if (instructionPrefix.length > MAX_EXPLICIT_SKILL_INSTRUCTION_CHARS) return {
body: params.text,
error: "Combined skill reference metadata is too long. Use fewer or shorter skill references.",
skills: []
};
return {
body: `${instructionPrefix}${params.text}`,
skills: available
};
}
//#endregion
export { mergeExplicitSkillSelections as a, listReservedChatSlashCommandNames as i, expandExplicitSkillReferences as n, resolveSkillCommandInvocation as o, hasSkillReferenceCandidate as r, skillCommandsToExplicitSelections as s, expandBundleCommandPromptTemplate as t };