openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
386 lines (385 loc) • 15.5 kB
JavaScript
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { r as isPathInsideWithRealpath } from "./path-safety-Bi0ppMWC.js";
import "./redact-BtvPPfTi.js";
import { m as resolveAgentWorkspaceDir, o as listAgentIds } from "./agent-scope-config-DcbEhP0R.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { i as readRegularFileSync } from "./regular-file-Equ4rrfE.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { u as readRootJsonObjectSync } from "./json-files-Bq1lIlQB.js";
import { t as createDedupeCache } from "./dedupe-gst1CUro.js";
import { d as resolveEffectivePluginActivationState, l as normalizePluginsConfig, r as hasExplicitPluginConfig } from "./config-state-BkU1frVq.js";
import { c as normalizeBundlePathList, n as CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH, s as mergeBundlePathLists } from "./bundle-manifest-C0Vv9-Hj.js";
import { n as loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-contributions-BwuDvm89.js";
import "./agent-scope-DbtJyKUL.js";
import { n as resolveEffectiveAgentSkillFilter } from "./agent-filter-DZ-Tmqcd.js";
import { r as logVerbose } from "./globals-CTaGxEqj.js";
import { i as stripFrontmatterBlock, n as parseFrontmatterBlock } from "./frontmatter-CWJFi-rx.js";
import { t as canonicalizePath } from "./paths-CIwQeLl6.js";
import { i as parseFrontmatterBool } from "./frontmatter-SwS2n7L7.js";
import { n as resolveSkillTelemetrySource } from "./source-0ivX3dtK.js";
import { i as isSkillPromptVisible, r as filterUserInvocableSkillEntries } from "./skill-index-BgtIQy35.js";
import { P as sanitizeSkillCommandName } from "./selection-NYtFFG08.js";
import { i as loadVisibleSkills, n as loadBundledSkillEntryByName, t as filterWorkspaceSkills } from "./workspace-skill-loader-_a5Sci8B.js";
import { n as resolveNodeExecEligibility } from "./exec-defaults-CET-_UfH.js";
import { t as getRemoteSkillEligibility } from "./remote-D-j1AEXQ.js";
import { i as listReservedChatSlashCommandNames } from "./chat-command-invocation-ClP5x94p.js";
import fs from "node:fs";
import path from "node:path";
//#region src/plugins/bundle-commands.ts
const BUNDLE_COMMAND_MAX_BYTES = 1048576;
const log = createSubsystemLogger("plugins/bundle-commands");
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 raw = readClaudeBundleManifest(rootDir);
const declared = normalizeBundlePathList(raw.commands);
const defaults = fs.existsSync(path.join(rootDir, "commands")) ? ["commands"] : [];
return mergeBundlePathLists(defaults, 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(promptTemplate) {
const lineEnd = promptTemplate.indexOf("\n");
return (lineEnd < 0 ? promptTemplate : promptTemplate.slice(0, lineEnd)).trimEnd();
}
function loadBundleCommandsFromRoot(params) {
const entries = [];
for (const filePath of listMarkdownFilesRecursive(params.commandRoot)) {
let raw;
try {
raw = readRegularFileSync({
filePath,
maxBytes: BUNDLE_COMMAND_MAX_BYTES
}).buffer.toString("utf-8");
} catch (error) {
log.warn(`skipping unreadable bundle command file ${filePath}: ${formatErrorMessage(error)}`);
continue;
}
const frontmatter = parseFrontmatterBlock(raw);
if (!parseFrontmatterBool(frontmatter["user-invocable"], true)) continue;
const promptTemplate = stripFrontmatterBlock(raw);
if (!promptTemplate) continue;
const rawName = normalizeOptionalString(frontmatter.name) || toDefaultCommandName(params.commandRoot, filePath);
if (!rawName) continue;
const description = normalizeOptionalString(frontmatter.description) || toDefaultDescription(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,
channelIds: record.channels,
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 = createDedupeCache({
ttlMs: 0,
maxSize: 1024
});
function debugSkillCommandOnce(messageKey, message, meta) {
if (skillCommandDebugOnce.check(messageKey)) return;
skillsLogger.debug(message, meta);
}
function traceSkillCommandOnce(messageKey, message, meta) {
if (skillCommandDebugOnce.check(messageKey)) return;
skillsLogger.trace(message, meta);
}
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, 32 - 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, 30))}_x`;
}
/** Builds user-invocable slash command specs for visible workspace skills. */
function buildWorkspaceSkillCommandSpecs(workspaceDir, opts) {
const effectiveSkillFilter = opts?.includeAllowlistHidden ? void 0 : opts?.skillFilter ?? resolveEffectiveAgentSkillFilter(opts?.config, opts?.agentId);
const eligible = opts?.entries ? filterWorkspaceSkills(opts.entries, {
config: opts?.config,
skillFilter: effectiveSkillFilter,
eligibility: opts?.eligibility
}) : loadVisibleSkills(workspaceDir, {
config: opts?.config,
managedSkillsDir: opts?.managedSkillsDir,
bundledSkillsDir: opts?.bundledSkillsDir,
librarySelections: opts?.librarySelections,
skillFilter: effectiveSkillFilter,
eligibility: opts?.eligibility,
pluginMetadataSnapshot: opts?.pluginMetadataSnapshot
});
const userInvocable = filterUserInvocableSkillEntries(eligible);
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 description = entry.skill.description?.trim() || rawName;
const dispatch = entry.disableCommandDispatch ? void 0 : (() => {
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,
displayName: entry.skill.displayName ?? rawName,
skillFile: canonicalizePath(entry.skill.filePath),
skillName: rawName,
description,
modelVisible: isSkillPromptVisible(entry),
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));
specs.push({
name: unique,
skillName: entry.rawName,
description: entry.description,
modelVisible: false,
promptTemplate: entry.promptTemplate,
sourceFilePath: entry.sourceFilePath
});
}
return specs;
}
//#endregion
//#region src/skills/discovery/chat-commands.ts
function listSkillCommandsForWorkspace(params) {
const nodeSkills = resolveNodeExecEligibility({
cfg: params.cfg,
agentId: params.agentId,
sessionEntry: params.sessionEntry,
sessionKey: params.sessionKey,
execOverrides: params.execOverrides
});
const eligibility = {
nodeSkills,
remote: getRemoteSkillEligibility({ advertiseExecNode: nodeSkills.canExec })
};
return buildWorkspaceSkillCommandSpecs(params.workspaceDir, {
config: params.cfg,
agentId: params.agentId,
skillFilter: params.skillFilter,
includeAllowlistHidden: params.includeAllowlistHidden,
eligibility,
pluginMetadataSnapshot: params.pluginMetadataSnapshot,
librarySelections: params.sessionEntry?.skillLibrarySelections,
reservedNames: listReservedChatSlashCommandNames()
});
}
/** Resolves one eligible bundled skill before normal workspace precedence is applied. */
function findBundledSkillCommandForWorkspace(params) {
const nodeSkills = resolveNodeExecEligibility({
cfg: params.cfg,
agentId: params.agentId,
sessionEntry: params.sessionEntry,
sessionKey: params.sessionKey,
execOverrides: params.execOverrides
});
const eligibility = {
nodeSkills,
remote: getRemoteSkillEligibility({ advertiseExecNode: nodeSkills.canExec })
};
const entry = loadBundledSkillEntryByName(params.skillName, {
config: params.cfg,
agentId: params.agentId,
skillFilter: params.skillFilter,
eligibility
});
if (!entry) return;
return buildWorkspaceSkillCommandSpecs(params.workspaceDir, {
config: params.cfg,
agentId: params.agentId,
skillFilter: params.skillFilter,
eligibility,
entries: [entry],
reservedNames: listReservedChatSlashCommandNames()
}).find((command) => command.skillSource === "bundled" && command.skillName.trim().toLowerCase() === params.skillName.trim().toLowerCase());
}
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 agentIds = params.agentIds ?? listAgentIds(params.cfg);
const used = listReservedChatSlashCommandNames();
const entries = [];
const hasSingleAgentContext = agentIds.length === 1;
const workspaceAgents = [];
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;
}
try {
fs.realpathSync(workspaceDir);
} catch {
logVerbose(`Skipping agent "${agentId}": cannot resolve workspace: ${workspaceDir}`);
continue;
}
workspaceAgents.push({
agentId,
workspaceDir,
skillFilter: resolveEffectiveAgentSkillFilter(params.cfg, agentId)
});
}
for (const { agentId, workspaceDir, skillFilter } of workspaceAgents) {
if (hasSingleAgentContext && params.sessionEntry?.skillLibrarySelections?.length) {
entries.push(...listSkillCommandsForWorkspace({
...params,
workspaceDir,
agentId,
skillFilter
}));
continue;
}
const nodeSkills = resolveNodeExecEligibility({
cfg: params.cfg,
agentId,
...hasSingleAgentContext ? {
sessionEntry: params.sessionEntry,
sessionKey: params.sessionKey,
execOverrides: params.execOverrides
} : {}
});
const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, {
config: params.cfg,
agentId,
skillFilter,
eligibility: {
nodeSkills,
remote: getRemoteSkillEligibility({ advertiseExecNode: nodeSkills.canExec })
},
reservedNames: used
});
for (const command of commands) {
used.add(normalizeLowercaseStringOrEmpty(command.name));
entries.push(command);
}
}
return dedupeBySkillName(entries).toSorted((left, right) => left.skillName.localeCompare(right.skillName, "en"));
}
//#endregion
export { listSkillCommandsForAgents as n, listSkillCommandsForWorkspace as r, findBundledSkillCommandForWorkspace as t };