UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

403 lines (402 loc) • 17.3 kB
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { k as walkDirectorySync } from "./fs-safe-BUXBSN-4.mjs"; import { m as isPathInsideWithRealpath } from "./includes-BkzfmxST.mjs"; import { o as racePromiseWithAbortSignal } from "./src-D1BHGNTj.mjs"; import "./abort-signal-QqAmLWTl.mjs"; import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs"; import { c as readRootJsonObjectSync } from "./json-files-C5mNTScg.mjs"; import { d as resolveAgentWorkspaceDir } from "./agent-scope-config-CDYpzA0r.mjs"; import { i as listAgentIds } from "./agent-roster-BXD1uMJD.mjs"; import { t as createDedupeCache } from "./dedupe-qhwfyIea.mjs"; import { i as hasExplicitPluginConfig, p as resolveEffectivePluginActivationState, u as normalizePluginsConfig } from "./config-state-DY_WJYue.mjs"; import { c as normalizeBundlePathList, n as CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH, s as mergeBundlePathLists } from "./bundle-manifest-BiKYAnae.mjs"; import "./agent-scope-CG17uXFn.mjs"; import { n as resolveEffectiveAgentSkillFilter } from "./agent-filter-KIvrTe_y.mjs"; import { n as loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-contributions-Dzd0IlT8.mjs"; import { r as logVerbose } from "./globals-DyT306Z_.mjs"; import { f as stripFrontmatterBlock, r as parseFrontmatterBool, u as parseFrontmatterBlock } from "./frontmatter-ByhFzIgC.mjs"; import { a as isWorkspaceAccessUnavailableError, i as getAgentWorkspaceAccess } from "./workspace-access-BnPMMciK.mjs"; import { n as resolveSkillTelemetrySource } from "./source-DbWOnpPI.mjs"; import { i as resolveSkillFileHost } from "./skill-file-host-CJXc2mOP.mjs"; import { t as canonicalizePath } from "./paths-DqLuHi-X.mjs"; import { i as resolveNodeExecEligibility } from "./exec-defaults-BO0vGSET.mjs"; import { h as sanitizeSkillCommandName } from "./store-Bfghpqtp.mjs"; import { n as isSkillUserInvocable, t as isSkillPromptVisible } from "./skill-index-fJ6kPuHX.mjs"; import { i as prepareWorkspaceSkills, s as filterSkillEntries, t as loadVisibleSkills } from "./workspace-skill-loader-Q7ZO-JjE.mjs"; import { n as prepareRemoteSkillConnections } from "./remote-skills-CVJsA9ab.mjs"; import { r as recordSkillCommandFileHost } from "./skill-command-provenance-D3weQ69x.mjs"; import { t as getRemoteSkillEligibility } from "./remote-B1XF-V4S.mjs"; import { i as listReservedChatSlashCommandNames } from "./chat-command-invocation-9A7eUh7l.mjs"; import fs from "node:fs"; import path from "node:path"; import { readRegularFileSync } from "@openclaw/fs-safe/advanced"; //#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) { return walkDirectorySync(rootDir, { symlinks: "skip", descend: ({ name }) => !name.startsWith("."), include: ({ kind, name }) => kind === "file" && !name.startsWith(".") && Boolean(normalizeOptionalLowercaseString(name)?.endsWith(".md")) }).entries.map((entry) => entry.path).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 logSkillCommandOnce(messageKey, message, meta, level = "debug") { if (skillCommandDebugOnce.check(messageKey)) return; skillsLogger[level](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`; } function resolveCommandSkillLoadOptions(opts) { return { bundledSkillName: opts?.bundledSkillName, config: opts?.config, managedSkillsDir: opts?.managedSkillsDir, bundledSkillsDir: opts?.bundledSkillsDir, librarySelections: opts?.librarySelections, agentId: opts?.agentId, agentSkillFilter: opts?.includeAllowlistHidden ? "ignore" : "apply", skillFilter: opts?.includeAllowlistHidden ? void 0 : opts?.skillFilter ?? resolveEffectiveAgentSkillFilter(opts?.config, opts?.agentId), eligibility: opts?.eligibility, pluginMetadataSnapshot: opts?.pluginMetadataSnapshot }; } /** Builds user-invocable slash command specs for synchronous SDK consumers. */ function buildWorkspaceSkillCommandSpecs(workspaceDir, opts) { const loadOptions = { ...resolveCommandSkillLoadOptions(opts), gatewayOnly: opts?.gatewayOnly }; return assembleWorkspaceSkillCommandSpecs(workspaceDir, opts?.entries ? filterSkillEntries(opts.entries, loadOptions) : loadVisibleSkills(workspaceDir, loadOptions), opts); } /** Prepares eligibility once before sharing the synchronous command assembly. */ async function prepareWorkspaceSkillCommandSpecs(workspaceDir, opts, assertCurrent) { const eligible = await prepareWorkspaceSkills(workspaceDir, resolveCommandSkillLoadOptions(opts), assertCurrent); assertCurrent?.(); return assembleWorkspaceSkillCommandSpecs(workspaceDir, eligible, opts); } function assembleWorkspaceSkillCommandSpecs(workspaceDir, eligible, opts) { const userInvocable = eligible.filter(isSkillUserInvocable); const used = /* @__PURE__ */ new Set(); for (const reserved of opts?.reservedNames ?? []) { used.add(normalizeLowercaseStringOrEmpty(reserved)); used.add(sanitizeSkillCommandName(reserved)); } const specs = []; const claimName = (rawName, bundle = false) => { const prefix = bundle ? "bundle-" : ""; const label = bundle ? "bundle" : "skill"; const level = bundle ? "debug" : "trace"; const base = sanitizeSkillCommandName(rawName); if (base !== rawName) logSkillCommandOnce(`${prefix}sanitize:${rawName}:${base}`, `Sanitized ${label} command name "${rawName}" to "/${base}".`, { rawName, sanitized: `/${base}` }, level); const unique = resolveUniqueSkillCommandName(base, used); if (unique !== base) logSkillCommandOnce(`${prefix}dedupe:${rawName}:${unique}`, `De-duplicated ${label} command name for "${rawName}" to "/${unique}".`, { rawName, deduped: `/${unique}` }, level); used.add(normalizeLowercaseStringOrEmpty(unique)); return unique; }; for (const entry of userInvocable) { const rawName = entry.skill.name; const unique = claimName(rawName); const description = entry.skill.description?.trim() || rawName; const dispatch = entry.disableCommandDispatch ? void 0 : (() => { if (normalizeLowercaseStringOrEmpty(entry.frontmatter?.["command-dispatch"] ?? entry.frontmatter?.["command_dispatch"] ?? "") !== "tool") return; const toolName = (entry.frontmatter?.["command-tool"] ?? entry.frontmatter?.["command_tool"] ?? "").trim(); if (!toolName) { logSkillCommandOnce(`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") logSkillCommandOnce(`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" }; })(); const spec = { 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 fileHost = resolveSkillFileHost(entry.skill); if (fileHost) recordSkillCommandFileHost(spec, fileHost); specs.push(spec); } const bundleCommands = loadEnabledClaudeBundleCommands({ workspaceDir, cfg: opts?.config }); for (const entry of bundleCommands) specs.push({ name: claimName(entry.rawName, true), 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 resolveWorkspaceSkillCommandOptions(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 { config: params.cfg, agentId: params.agentId, skillFilter: params.skillFilter, includeAllowlistHidden: params.includeAllowlistHidden, eligibility, pluginMetadataSnapshot: params.pluginMetadataSnapshot, librarySelections: params.sessionEntry?.skillLibrarySelections }; } function hasRemoteWorkspace(workspaceDir) { try { return Boolean(getAgentWorkspaceAccess(workspaceDir, "loadSkills")?.loadSkills); } catch (error) { if (isWorkspaceAccessUnavailableError(error)) return true; throw error; } } /** Synchronous public SDK contract; remote workspace menus are deferred. */ function listSkillCommandsForWorkspace(params) { return buildWorkspaceSkillCommandSpecs(params.workspaceDir, { ...resolveWorkspaceSkillCommandOptions(params), reservedNames: listReservedChatSlashCommandNames(), gatewayOnly: hasRemoteWorkspace(params.workspaceDir) }); } async function prepareSkillCommandsForWorkspace(params, assertCurrent) { assertCurrent?.(); await prepareRemoteSkillConnections(); assertCurrent?.(); const commands = await prepareWorkspaceSkillCommandSpecs(params.workspaceDir, { ...resolveWorkspaceSkillCommandOptions(params), reservedNames: listReservedChatSlashCommandNames() }, assertCurrent); assertCurrent?.(); return commands; } /** Resolve Gateway-bundled commands with the active Harness eligibility checks. */ async function prepareBundledSkillCommandForWorkspace(params) { await prepareRemoteSkillConnections(); return (await prepareWorkspaceSkillCommandSpecs(params.workspaceDir, { ...resolveWorkspaceSkillCommandOptions(params), reservedNames: listReservedChatSlashCommandNames(), bundledSkillName: params.skillName })).find((command) => command.skillSource === "bundled" && command.skillName.trim().toLowerCase() === params.skillName.trim().toLowerCase()); } function finalizeSkillCommands(commands) { const seen = /* @__PURE__ */ new Set(); return commands.filter((cmd) => { const key = normalizeOptionalLowercaseString(cmd.skillName); if (key && seen.has(key)) return false; if (key) seen.add(key); return true; }).toSorted((left, right) => left.skillName.localeCompare(right.skillName, "en")); } function* resolveAgentSkillCommandWorkspaces(params, allowRemote = false) { const agentIds = params.agentIds ?? listAgentIds(params.cfg); const hasSingleAgentContext = agentIds.length === 1; const workspaceAgents = []; for (const agentId of agentIds) { const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId); const remote = allowRemote ? Boolean(getAgentWorkspaceAccess(workspaceDir, "loadSkills")?.loadSkills) : hasRemoteWorkspace(workspaceDir); if (!remote) { 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, gatewayOnly: remote && !allowRemote, skillFilter: resolveEffectiveAgentSkillFilter(params.cfg, agentId) }); } for (const { agentId, workspaceDir, skillFilter, gatewayOnly } of workspaceAgents) yield { workspaceDir, options: { ...resolveWorkspaceSkillCommandOptions({ cfg: params.cfg, agentId, workspaceDir, skillFilter, ...hasSingleAgentContext ? { sessionEntry: params.sessionEntry, sessionKey: params.sessionKey, execOverrides: params.execOverrides } : {} }), gatewayOnly } }; } function appendSkillCommands(entries, used, commands) { for (const command of commands) { used.add(normalizeLowercaseStringOrEmpty(command.name)); entries.push(command); } } /** Synchronous public SDK contract for native command consumers. */ function listSkillCommandsForAgents(params) { const used = listReservedChatSlashCommandNames(); const entries = []; for (const { workspaceDir, options } of resolveAgentSkillCommandWorkspaces(params)) appendSkillCommands(entries, used, buildWorkspaceSkillCommandSpecs(workspaceDir, { ...options, reservedNames: used })); return finalizeSkillCommands(entries); } async function prepareSkillCommandsForAgents(params) { params.signal?.throwIfAborted(); await prepareRemoteSkillConnections(); params.signal?.throwIfAborted(); const used = listReservedChatSlashCommandNames(); const entries = []; for (const { workspaceDir, options } of resolveAgentSkillCommandWorkspaces(params, true)) { const commands = await racePromiseWithAbortSignal(prepareWorkspaceSkillCommandSpecs(workspaceDir, { ...options, reservedNames: used }), params.signal); params.signal?.throwIfAborted(); appendSkillCommands(entries, used, commands); } return finalizeSkillCommands(entries); } //#endregion export { prepareSkillCommandsForWorkspace as a, prepareSkillCommandsForAgents as i, listSkillCommandsForWorkspace as n, prepareBundledSkillCommandForWorkspace as r, listSkillCommandsForAgents as t };