openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
392 lines (391 loc) • 16.6 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { a as isPathInsideWithRealpath } from "./path-BlG8lhgR.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { u as readRootJsonObjectSync } from "./json-files-2umMHm0W.js";
import { o as mergeBundlePathLists, s as normalizeBundlePathList, t as CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH } from "./bundle-manifest-DN_uOlFE.js";
import { l as resolveEffectivePluginActivationState, r as hasExplicitPluginConfig, s as normalizePluginsConfig } from "./config-state-CikNNz7T.js";
import "./agent-scope-MrLta7Pq.js";
import { t as resolveEffectiveAgentSkillFilter } from "./agent-filter-Ckm_ksHY.js";
import { n as listAgentIds, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js";
import { n as loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-ChcJPHWl.js";
import "./scan-paths-Bve2UhXh.js";
import { r as logVerbose } from "./globals-GTrXU4s9.js";
import { t as getChatCommands } from "./commands-registry.data-CJQNnHNA.js";
import { u as parseFrontmatterBlock } from "./frontmatter-BJNWSkRQ.js";
import { n as resolveSkillTelemetrySource } from "./source-UVo0hlSr.js";
import { h as filterUserInvocableSkillEntries, i as filterWorkspaceSkillEntriesWithOptions, o as loadVisibleWorkspaceSkillEntries } from "./workspace-_gXfieOh.js";
import { t as canExecRequestNode } from "./exec-defaults-BenGbA5f.js";
import { t as getRemoteSkillEligibility } from "./remote-DoOmoEi1.js";
import fs from "node:fs";
import path from "node:path";
//#region src/skills/discovery/chat-command-invocation.ts
/** 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) => {
if (normalizeOptionalLowercaseString(entry.name) === lowered) return true;
if (normalizeOptionalLowercaseString(entry.skillName) === lowered) return true;
return normalizeSkillCommandLookup(entry.name) === normalized || normalizeSkillCommandLookup(entry.skillName) === normalized;
});
}
function resolveSkillCommandInvocation(params) {
const trimmed = params.commandBodyNormalized.trim();
if (!trimmed.startsWith("/")) return null;
const match = trimmed.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
if (!match) return null;
const commandName = normalizeOptionalLowercaseString(match[1]);
if (!commandName) return null;
if (commandName === "skill") {
const remainder = match[2]?.trim();
if (!remainder) return null;
const skillMatch = remainder.match(/^([^\s]+)(?:\s+([\s\S]+))?$/);
if (!skillMatch) return null;
const skillCommand = findSkillCommand(params.skillCommands, skillMatch[1] ?? "");
if (!skillCommand) return null;
return {
command: skillCommand,
args: skillMatch[2]?.trim() || void 0
};
}
const command = params.skillCommands.find((entry) => normalizeOptionalLowercaseString(entry.name) === commandName);
if (!command) return null;
return {
command,
args: match[2]?.trim() || void 0
};
}
//#endregion
//#region src/plugins/bundle-commands.ts
function parseFrontmatterBool(value, fallback) {
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized) return fallback;
if (normalized === "true" || normalized === "yes" || normalized === "1") return true;
if (normalized === "false" || normalized === "no" || normalized === "0") return false;
return fallback;
}
function stripFrontmatter(content) {
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
if (!normalized.startsWith("---")) return normalized.trim();
const endIndex = normalized.indexOf("\n---", 3);
if (endIndex === -1) return normalized.trim();
return normalized.slice(endIndex + 4).trim();
}
function readClaudeBundleManifest(rootDir) {
const result = readRootJsonObjectSync({
rootDir,
relativePath: CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH,
boundaryLabel: "plugin root",
rejectHardlinks: true
});
return result.ok ? result.value : {};
}
function resolveClaudeCommandRootDirs(rootDir) {
const declared = normalizeBundlePathList(readClaudeBundleManifest(rootDir).commands);
return mergeBundlePathLists(fs.existsSync(path.join(rootDir, "commands")) ? ["commands"] : [], declared);
}
function listMarkdownFilesRecursive(rootDir) {
const pending = [rootDir];
const files = [];
while (pending.length > 0) {
const current = pending.pop();
if (!current) continue;
let entries;
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.name.startsWith(".")) continue;
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
pending.push(fullPath);
continue;
}
if (entry.isFile() && normalizeOptionalLowercaseString(entry.name)?.endsWith(".md")) files.push(fullPath);
}
}
return files.toSorted((a, b) => a.localeCompare(b));
}
function toDefaultCommandName(rootDir, filePath) {
return path.relative(rootDir, filePath).replace(/\.[^.]+$/u, "").split(path.sep).join(":");
}
function toDefaultDescription(rawName, promptTemplate) {
return promptTemplate.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || rawName;
}
function loadBundleCommandsFromRoot(params) {
const entries = [];
for (const filePath of listMarkdownFilesRecursive(params.commandRoot)) {
let raw;
try {
raw = fs.readFileSync(filePath, "utf-8");
} catch {
continue;
}
const frontmatter = parseFrontmatterBlock(raw);
if (parseFrontmatterBool(frontmatter["disable-model-invocation"], false)) continue;
const promptTemplate = stripFrontmatter(raw);
if (!promptTemplate) continue;
const rawName = normalizeOptionalString(frontmatter.name) || toDefaultCommandName(params.commandRoot, filePath);
if (!rawName) continue;
const description = normalizeOptionalString(frontmatter.description) || toDefaultDescription(rawName, promptTemplate);
entries.push({
pluginId: params.pluginId,
rawName,
description,
promptTemplate,
sourceFilePath: filePath
});
}
return entries;
}
function loadEnabledClaudeBundleCommands(params) {
if (!hasExplicitPluginConfig(params.cfg?.plugins)) return [];
const registry = loadPluginManifestRegistryForPluginRegistry({
workspaceDir: params.workspaceDir,
config: params.cfg,
includeDisabled: true
});
const normalizedPlugins = normalizePluginsConfig(params.cfg?.plugins);
const commands = [];
for (const record of registry.plugins) {
if (record.format !== "bundle" || record.bundleFormat !== "claude" || !(record.bundleCapabilities ?? []).includes("commands")) continue;
if (!resolveEffectivePluginActivationState({
id: record.id,
origin: record.origin,
config: normalizedPlugins,
rootConfig: params.cfg
}).activated) continue;
for (const relativeRoot of resolveClaudeCommandRootDirs(record.rootDir)) {
const commandRoot = path.resolve(record.rootDir, relativeRoot);
if (!fs.existsSync(commandRoot)) continue;
if (!isPathInsideWithRealpath(record.rootDir, commandRoot, { requireRealpath: true })) continue;
commands.push(...loadBundleCommandsFromRoot({
pluginId: record.id,
commandRoot
}));
}
}
return commands;
}
//#endregion
//#region src/skills/discovery/command-specs.ts
const skillsLogger = createSubsystemLogger("skills");
const skillCommandDebugOnce = /* @__PURE__ */ new Set();
const SKILL_COMMAND_MAX_LENGTH = 32;
const SKILL_COMMAND_FALLBACK = "skill";
const SKILL_COMMAND_DESCRIPTION_MAX_LENGTH = 100;
function debugSkillCommandOnce(messageKey, message, meta) {
if (skillCommandDebugOnce.has(messageKey)) return;
skillCommandDebugOnce.add(messageKey);
skillsLogger.debug(message, meta);
}
function traceSkillCommandOnce(messageKey, message, meta) {
if (skillCommandDebugOnce.has(messageKey)) return;
skillCommandDebugOnce.add(messageKey);
skillsLogger.trace(message, meta);
}
function sanitizeSkillCommandName(raw) {
return normalizeLowercaseStringOrEmpty(raw).replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, SKILL_COMMAND_MAX_LENGTH) || SKILL_COMMAND_FALLBACK;
}
function resolveUniqueSkillCommandName(base, used) {
const normalizedBase = normalizeLowercaseStringOrEmpty(base);
if (!used.has(normalizedBase)) return base;
for (let index = 2; index < 1e3; index += 1) {
const suffix = `_${index}`;
const maxBaseLength = Math.max(1, SKILL_COMMAND_MAX_LENGTH - suffix.length);
const candidate = `${base.slice(0, maxBaseLength)}${suffix}`;
const candidateKey = normalizeLowercaseStringOrEmpty(candidate);
if (!used.has(candidateKey)) return candidate;
}
return `${base.slice(0, Math.max(1, SKILL_COMMAND_MAX_LENGTH - 2))}_x`;
}
/** Builds user-invocable slash command specs for visible workspace skills. */
function buildWorkspaceSkillCommandSpecs(workspaceDir, opts) {
const effectiveSkillFilter = opts?.skillFilter ?? resolveEffectiveAgentSkillFilter(opts?.config, opts?.agentId);
const userInvocable = filterUserInvocableSkillEntries(opts?.entries ? filterWorkspaceSkillEntriesWithOptions(opts.entries, {
config: opts?.config,
skillFilter: effectiveSkillFilter,
eligibility: opts?.eligibility
}) : loadVisibleWorkspaceSkillEntries(workspaceDir, {
config: opts?.config,
managedSkillsDir: opts?.managedSkillsDir,
bundledSkillsDir: opts?.bundledSkillsDir,
skillFilter: effectiveSkillFilter,
eligibility: opts?.eligibility
}));
const used = /* @__PURE__ */ new Set();
for (const reserved of opts?.reservedNames ?? []) used.add(normalizeLowercaseStringOrEmpty(reserved));
const specs = [];
for (const entry of userInvocable) {
const rawName = entry.skill.name;
const base = sanitizeSkillCommandName(rawName);
if (base !== rawName) traceSkillCommandOnce(`sanitize:${rawName}:${base}`, `Sanitized skill command name "${rawName}" to "/${base}".`, {
rawName,
sanitized: `/${base}`
});
const unique = resolveUniqueSkillCommandName(base, used);
if (unique !== base) traceSkillCommandOnce(`dedupe:${rawName}:${unique}`, `De-duplicated skill command name for "${rawName}" to "/${unique}".`, {
rawName,
deduped: `/${unique}`
});
used.add(normalizeLowercaseStringOrEmpty(unique));
const rawDescription = entry.skill.description?.trim() || rawName;
const description = rawDescription.length > SKILL_COMMAND_DESCRIPTION_MAX_LENGTH ? rawDescription.slice(0, SKILL_COMMAND_DESCRIPTION_MAX_LENGTH - 1) + "…" : rawDescription;
const dispatch = (() => {
const kindRaw = normalizeLowercaseStringOrEmpty(entry.frontmatter?.["command-dispatch"] ?? entry.frontmatter?.["command_dispatch"] ?? "");
if (!kindRaw || kindRaw !== "tool") return;
const toolName = (entry.frontmatter?.["command-tool"] ?? entry.frontmatter?.["command_tool"] ?? "").trim();
if (!toolName) {
debugSkillCommandOnce(`dispatch:missingTool:${rawName}`, `Skill command "/${unique}" requested tool dispatch but did not provide command-tool. Ignoring dispatch.`, {
skillName: rawName,
command: unique
});
return;
}
const argModeRaw = normalizeOptionalLowercaseString(entry.frontmatter?.["command-arg-mode"] ?? entry.frontmatter?.["command_arg_mode"] ?? "");
if (!(!argModeRaw || argModeRaw === "raw" ? "raw" : null)) debugSkillCommandOnce(`dispatch:badArgMode:${rawName}:${argModeRaw}`, `Skill command "/${unique}" requested tool dispatch but has unknown command-arg-mode. Falling back to raw.`, {
skillName: rawName,
command: unique,
argMode: argModeRaw
});
return {
kind: "tool",
toolName,
argMode: "raw"
};
})();
specs.push({
name: unique,
skillName: rawName,
description,
skillSource: resolveSkillTelemetrySource(entry.skill),
...dispatch ? { dispatch } : {}
});
}
const bundleCommands = loadEnabledClaudeBundleCommands({
workspaceDir,
cfg: opts?.config
});
for (const entry of bundleCommands) {
const base = sanitizeSkillCommandName(entry.rawName);
if (base !== entry.rawName) debugSkillCommandOnce(`bundle-sanitize:${entry.rawName}:${base}`, `Sanitized bundle command name "${entry.rawName}" to "/${base}".`, {
rawName: entry.rawName,
sanitized: `/${base}`
});
const unique = resolveUniqueSkillCommandName(base, used);
if (unique !== base) debugSkillCommandOnce(`bundle-dedupe:${entry.rawName}:${unique}`, `De-duplicated bundle command name for "${entry.rawName}" to "/${unique}".`, {
rawName: entry.rawName,
deduped: `/${unique}`
});
used.add(normalizeLowercaseStringOrEmpty(unique));
const description = entry.description.length > SKILL_COMMAND_DESCRIPTION_MAX_LENGTH ? entry.description.slice(0, SKILL_COMMAND_DESCRIPTION_MAX_LENGTH - 1) + "…" : entry.description;
specs.push({
name: unique,
skillName: entry.rawName,
description,
promptTemplate: entry.promptTemplate,
sourceFilePath: entry.sourceFilePath
});
}
return specs;
}
//#endregion
//#region src/skills/discovery/chat-commands.ts
function listSkillCommandsForWorkspace(params) {
return buildWorkspaceSkillCommandSpecs(params.workspaceDir, {
config: params.cfg,
agentId: params.agentId,
skillFilter: params.skillFilter,
eligibility: { remote: getRemoteSkillEligibility({ advertiseExecNode: canExecRequestNode({
cfg: params.cfg,
agentId: params.agentId
}) }) },
reservedNames: listReservedChatSlashCommandNames()
});
}
function dedupeBySkillName(commands) {
const seen = /* @__PURE__ */ new Set();
const out = [];
for (const cmd of commands) {
const key = normalizeOptionalLowercaseString(cmd.skillName);
if (key && seen.has(key)) continue;
if (key) seen.add(key);
out.push(cmd);
}
return out;
}
function listSkillCommandsForAgents(params) {
const mergeSkillFilters = (existing, incoming) => {
if (existing === void 0 || incoming === void 0) return;
if (existing.length === 0) return uniqueStrings(incoming);
if (incoming.length === 0) return uniqueStrings(existing);
return uniqueStrings([...existing, ...incoming]);
};
const agentIds = params.agentIds ?? listAgentIds(params.cfg);
const used = listReservedChatSlashCommandNames();
const entries = [];
const workspaceFilters = /* @__PURE__ */ new Map();
for (const agentId of agentIds) {
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId);
if (!fs.existsSync(workspaceDir)) {
logVerbose(`Skipping agent "${agentId}": workspace does not exist: ${workspaceDir}`);
continue;
}
let canonicalDir;
try {
canonicalDir = fs.realpathSync(workspaceDir);
} catch {
logVerbose(`Skipping agent "${agentId}": cannot resolve workspace: ${workspaceDir}`);
continue;
}
const skillFilter = resolveEffectiveAgentSkillFilter(params.cfg, agentId);
const existing = workspaceFilters.get(canonicalDir);
if (existing) {
existing.skillFilter = mergeSkillFilters(existing.skillFilter, skillFilter);
continue;
}
workspaceFilters.set(canonicalDir, {
workspaceDir,
skillFilter
});
}
for (const { workspaceDir, skillFilter } of workspaceFilters.values()) {
const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, {
config: params.cfg,
skillFilter,
eligibility: { remote: getRemoteSkillEligibility({ advertiseExecNode: canExecRequestNode({ cfg: params.cfg }) }) },
reservedNames: used
});
for (const command of commands) {
used.add(normalizeLowercaseStringOrEmpty(command.name));
entries.push(command);
}
}
return dedupeBySkillName(entries);
}
//#endregion
export { resolveSkillCommandInvocation as i, listSkillCommandsForWorkspace as n, listReservedChatSlashCommandNames as r, listSkillCommandsForAgents as t };