UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

235 lines (234 loc) • 10.6 kB
import { c as normalizeOptionalLowercaseString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import "./utils-DgZn2dLu.mjs"; import { m as wrapCurrentPluginInstance } from "./plugin-instance-scope-DEzoxqBj.mjs"; import { u as withPluginRuntimeRegistryScope } from "./gateway-request-scope-BSmCVVpP.mjs"; import { et as getPluginCommandExecutionCount, p as getPluginRegistrationContext, v as requireActivePluginRegistry } from "./runtime-NsVZTERq.mjs"; import { f as isOperatorScope } from "./operator-scopes-DsrPoRc4.mjs"; import { r as logVerbose } from "./globals-DyT306Z_.mjs"; import { n as normalizeAgentPromptSurfaceKind } from "./agent-prompt-surface-kind-BvH6Xj9U.mjs"; import "./command-registry-state-3VtJlwBT.mjs"; //#region src/plugins/plugin-command.types.ts const AGENT_PROMPT_SURFACE_KINDS = [ "openclaw_main", "pi_main", "codex_app_server", "cli_backend", "acp_backend", "subagent" ]; //#endregion //#region src/plugins/command-registration.ts /** * Reserved command names that plugins cannot override (built-in commands). * * Constructed lazily inside validateCommandName to avoid TDZ errors: the * bundler can place this module's body after call sites within the same * output chunk, so any module-level const/let would be uninitialized when * first accessed during plugin registration. */ let reservedCommands; let agentPromptSurfaces; function hasExactKeys(value, keys) { const actual = Object.keys(value); return actual.length === keys.length && actual.every((key) => keys.includes(key)); } function validateNonemptyString(value, label) { if (typeof value !== "string") return `${label} must be a string`; return value.trim() ? null : `${label} cannot be empty`; } function getReservedCommands() { reservedCommands ??= /* @__PURE__ */ new Set([ "help", "commands", "status", "diagnostics", "codex", "whoami", "context", "btw", "stop", "restart", "reset", "new", "compact", "config", "debug", "allowlist", "activation", "skill", "learn", "loop", "subagents", "kill", "steer", "tell", "model", "models", "queue", "send", "bash", "exec", "think", "verbose", "reasoning", "elevated", "usage" ]); return reservedCommands; } function isReservedCommandName(name) { const trimmed = normalizeOptionalLowercaseString(name) ?? ""; return Boolean(trimmed && getReservedCommands().has(trimmed)); } function validateCommandName(name, opts) { const trimmed = normalizeOptionalLowercaseString(name) ?? ""; if (!trimmed) return "Command name cannot be empty"; if (!/^[a-z][a-z0-9_-]*$/.test(trimmed)) return "Command name must start with a letter and contain only letters, numbers, hyphens, and underscores"; if (!opts?.allowReservedCommandNames && getReservedCommands().has(trimmed)) return `Command name "${trimmed}" is reserved by a built-in command`; return null; } function validatePluginCommandDefinition(command, opts) { if (typeof command.handler !== "function") return "Command handler must be a function"; if (typeof command.name !== "string") return "Command name must be a string"; const descriptionError = validateNonemptyString(command.description, "Command description"); if (descriptionError) return descriptionError; if (command.agentPromptGuidance !== void 0 && !Array.isArray(command.agentPromptGuidance)) return "Agent prompt guidance must be an array of strings or objects"; for (const [index, guidance] of (command.agentPromptGuidance ?? []).entries()) { const guidanceError = validateAgentPromptGuidance(index, guidance); if (guidanceError) return guidanceError; } if (command.requiredScopes !== void 0) { if (!Array.isArray(command.requiredScopes)) return "Command requiredScopes must be an array of operator scopes"; const unknownScopeIndex = command.requiredScopes.findIndex((scope) => !isOperatorScope(scope)); if (unknownScopeIndex !== -1) { const unknownScope = command.requiredScopes[unknownScopeIndex]; return typeof unknownScope === "string" ? `Command requiredScopes contains unknown operator scope: ${unknownScope}` : "Command requiredScopes contains unknown operator scope"; } } if (command.clientPresentation !== void 0) { if (!isRecord(command.clientPresentation)) return "Command clientPresentation must be an object"; if (!hasExactKeys(command.clientPresentation, ["when", "action"])) return "Command clientPresentation must contain only when and action"; if (command.clientPresentation.when !== "no-arguments") return "Command clientPresentation when must be \"no-arguments\""; if (!isRecord(command.clientPresentation.action)) return "Command clientPresentation action must be an object"; if (!hasExactKeys(command.clientPresentation.action, ["kind"])) return "Command clientPresentation action must contain only kind"; if (command.clientPresentation.action.kind !== "device-pairing") return "Command clientPresentation action kind is not supported"; } if (command.exposeSenderIsOwner !== void 0 && typeof command.exposeSenderIsOwner !== "boolean") return "Command exposeSenderIsOwner must be a boolean"; if (command.channels !== void 0) { if (!Array.isArray(command.channels)) return "Command channels must be an array of channel ids"; for (const [index, channel] of command.channels.entries()) { const error = validateNonemptyString(channel, `Command channel ${index + 1}`); if (error) return error; } } const nameError = validateCommandName(command.name.trim(), opts); if (nameError) return nameError; if (command.nativeNames !== void 0 && !isRecord(command.nativeNames)) return "Command nativeNames must be an object"; for (const [label, alias] of Object.entries(command.nativeNames ?? {})) { if (typeof alias !== "string") continue; const aliasError = validateCommandName(alias.trim()); if (aliasError) return `Native command alias "${label}" invalid: ${aliasError}`; } for (const [property, label] of [["nativeProgressMessages", "Native progress message"], ["descriptionLocalizations", "Description localization"]]) { const values = command[property]; if (values !== void 0 && !isRecord(values)) return `Command ${property} must be an object`; for (const [key, value] of Object.entries(values ?? {})) { const error = validateNonemptyString(value, `${label} "${key}"`); if (error) return error; } } return null; } function validateAgentPromptGuidance(index, guidance) { const label = `Agent prompt guidance ${index + 1}`; if (typeof guidance === "string") return guidance.trim() ? null : `${label} cannot be empty`; if (!isRecord(guidance)) return `${label} must be a string or object`; const textError = validateNonemptyString(guidance.text, `${label} text`); if (textError) return textError; if (guidance.surfaces === void 0) return null; if (!Array.isArray(guidance.surfaces)) return `${label} surfaces must be an array of prompt surface ids`; if (guidance.surfaces.length === 0) return `${label} surfaces cannot be empty`; for (const [surfaceIndex, surface] of guidance.surfaces.entries()) { const normalizedSurface = typeof surface === "string" ? surface.trim() : ""; if (!(agentPromptSurfaces ??= new Set(AGENT_PROMPT_SURFACE_KINDS)).has(normalizedSurface)) { const surfaces = AGENT_PROMPT_SURFACE_KINDS.join(", "); return `${label} surface ${surfaceIndex + 1} must be one of: ${surfaces}`; } } return null; } function normalizeAgentPromptGuidance(guidance) { return guidance.map((entry) => { if (typeof entry === "string") return entry.trim(); const normalized = { text: entry.text.trim() }; if (entry.surfaces) normalized.surfaces = entry.surfaces.map((surface) => normalizeAgentPromptSurfaceKind(surface.trim())); return normalized; }); } function listPluginInvocationKeys(command) { const keys = /* @__PURE__ */ new Set(); const push = (value) => { const normalized = normalizeOptionalLowercaseString(value); if (!normalized) return; keys.add(`/${normalized}`); }; push(command.name); for (const alias of Object.values(command.nativeNames ?? {})) if (typeof alias === "string") push(alias); return [...keys]; } function registerPluginCommand(pluginId, command, opts) { const context = getPluginRegistrationContext(); return registerPluginCommandInRegistry(context?.registry ?? requireActivePluginRegistry(), context?.pluginId ?? pluginId, command, opts); } function registerPluginCommandInRegistry(registry, pluginId, command, opts) { if (getPluginCommandExecutionCount(registry) > 0) return { ok: false, error: "Cannot register commands while processing is in progress" }; if (command.ownership === "reserved") return { ok: false, error: "Reserved command ownership is only available to bundled reserved commands" }; const definitionError = validatePluginCommandDefinition(command, opts); if (definitionError) return { ok: false, error: definitionError }; const name = command.name.trim(); const normalizedName = normalizeLowercaseStringOrEmpty(name); const description = command.description.trim(); const normalizedCommand = { ...command, handler: wrapCurrentPluginInstance(command.handler, (handler) => (ctx) => withPluginRuntimeRegistryScope(registry, () => handler(ctx))), name, description, ...command.channels ? { channels: command.channels.map((channel) => normalizeLowercaseStringOrEmpty(channel)) } : {}, ...command.agentPromptGuidance ? { agentPromptGuidance: normalizeAgentPromptGuidance(command.agentPromptGuidance) } : {}, ...command.clientPresentation ? { clientPresentation: { when: "no-arguments", action: { kind: "device-pairing" } } } : {} }; const invocationKeys = listPluginInvocationKeys(normalizedCommand); const key = `/${normalizedName}`; for (const invocationKey of invocationKeys) { const existing = registry.commands.find((entry) => listPluginInvocationKeys(entry.command).includes(invocationKey)); if (existing) return { ok: false, error: `Command "${invocationKey.slice(1)}" already registered by plugin "${existing.pluginId}"` }; } registry.commands.push({ pluginId, pluginName: opts?.pluginName, rootDir: opts?.pluginRoot, source: opts?.pluginRoot ?? "runtime", command: normalizedCommand, ...opts?.allowOwnerStatusExposure === true && normalizedCommand.exposeSenderIsOwner === true ? { trustedOwnerStatusExposure: true } : {} }); logVerbose(`Registered plugin command: ${key} (plugin: ${pluginId})`); return { ok: true }; } //#endregion export { registerPluginCommand as n, registerPluginCommandInRegistry as r, isReservedCommandName as t };