UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

386 lines (385 loc) 15.5 kB
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 };