UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

392 lines (391 loc) 16.6 kB
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 };