openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
292 lines (291 loc) • 13.8 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { c as normalizeOptionalLowercaseString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import "./utils-P__uGsPB.js";
import { l as isOperatorScope } from "./operator-scopes-Dw7Gu2cA.js";
import { r as logVerbose } from "./globals-CTaGxEqj.js";
import { C as requireActivePluginRegistry, H as getPluginCommandExecutionCount, g as getPluginRegistrationContext } from "./runtime-BL4wZfTq.js";
import { t as listRegisteredPluginCommands } from "./plugin-command-registry-BOeSDUSE.js";
//#region src/plugins/agent-prompt-surface-kind.ts
/** Normalizes legacy prompt surface names to current OpenClaw surface names. */
function normalizeAgentPromptSurfaceKind(surface) {
return surface === "pi_main" ? "openclaw_main" : surface;
}
/** True when a prompt surface targets the main OpenClaw prompt. */
function isOpenClawMainPromptSurface(surface) {
return normalizeAgentPromptSurfaceKind(surface) === "openclaw_main";
}
//#endregion
//#region src/plugins/command-registry-state.ts
function clearPluginCommands() {
requireActivePluginRegistry().commands.length = 0;
}
function isTrustedReservedCommandOwner(command) {
return command.ownership === "reserved";
}
function canExposeSenderIsOwner(command) {
return Array.isArray(command.requiredScopes) && command.requiredScopes.length > 0 || command.trustedOwnerStatusExposure === true;
}
function listRegisteredPluginAgentPromptGuidance(params) {
const lines = [];
const seen = /* @__PURE__ */ new Set();
const commands = listRegisteredPluginCommands(requireActivePluginRegistry()).toSorted((left, right) => {
if (left.pluginId !== right.pluginId) return left.pluginId < right.pluginId ? -1 : 1;
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
});
for (const command of commands) for (const entry of command.agentPromptGuidance ?? []) {
const trimmed = resolveAgentPromptGuidanceTextForSurface(entry, {
surface: params?.surface ? normalizeAgentPromptSurfaceKind(params.surface) : void 0,
includeLegacyGlobalGuidance: params?.includeLegacyGlobalGuidance ?? true
});
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
lines.push(trimmed);
}
return lines;
}
function resolveAgentPromptGuidanceTextForSurface(entry, params) {
if (typeof entry === "string") return params.includeLegacyGlobalGuidance ? entry.trim() : void 0;
const text = entry.text.trim();
if (!params.surface) return text;
if (!entry.surfaces || entry.surfaces.length === 0) return params.includeLegacyGlobalGuidance ? text : void 0;
return entry.surfaces.includes(params.surface) ? text : void 0;
}
//#endregion
//#region src/plugins/plugin-command.types.ts
/**
* Definition for a plugin-registered command.
*/
const AGENT_PROMPT_SURFACE_KINDS = [
"openclaw_main",
"pi_main",
"codex_app_server",
"cli_backend",
"acp_backend",
"subagent"
];
//#endregion
//#region src/plugins/command-registration.ts
/** Validates and registers plugin command definitions into the global command registry. */
/**
* 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 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 getAgentPromptSurfaces() {
agentPromptSurfaces ??= new Set(AGENT_PROMPT_SURFACE_KINDS);
return agentPromptSurfaces;
}
/** Returns true when a command name is owned by built-in OpenClaw command handling. */
function isReservedCommandName(name) {
const trimmed = normalizeOptionalLowercaseString(name) ?? "";
return Boolean(trimmed && getReservedCommands().has(trimmed));
}
/** Validates user-visible command names before plugin registration accepts them. */
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;
}
/**
* Validate a plugin command definition without registering it.
* Returns an error message if invalid, or null if valid.
* Shared by both the global registration path and snapshot (non-activating) loads.
*/
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";
if (typeof command.description !== "string") return "Command description must be a string";
if (!command.description.trim()) return "Command description cannot be empty";
if (command.ownership === "reserved") {
if (!opts?.allowReservedCommandNames) return "Reserved command ownership is only available to bundled reserved commands";
if (!isReservedCommandName(command.name)) return `Reserved command ownership requires a reserved command name: ${normalizeOptionalLowercaseString(command.name) ?? ""}`;
}
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 unknownScope = command.requiredScopes.find((scope) => !isOperatorScope(scope));
if (unknownScope) 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()) {
if (typeof channel !== "string") return `Command channel ${index + 1} must be a string`;
if (!channel.trim()) return `Command channel ${index + 1} cannot be empty`;
}
}
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}`;
}
if (command.nativeProgressMessages !== void 0 && !isRecord(command.nativeProgressMessages)) return "Command nativeProgressMessages must be an object";
for (const [label, message] of Object.entries(command.nativeProgressMessages ?? {})) {
if (typeof message !== "string") return `Native progress message "${label}" must be a string`;
if (!message.trim()) return `Native progress message "${label}" cannot be empty`;
}
if (command.descriptionLocalizations !== void 0 && !isRecord(command.descriptionLocalizations)) return "Command descriptionLocalizations must be an object";
for (const [locale, description] of Object.entries(command.descriptionLocalizations ?? {})) {
if (typeof description !== "string") return `Description localization "${locale}" must be a string`;
if (!description.trim()) return `Description localization "${locale}" cannot be empty`;
}
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`;
if (typeof guidance.text !== "string") return `${label} text must be a string`;
if (!guidance.text.trim()) return `${label} text cannot be empty`;
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 (!getAgentPromptSurfaces().has(normalizedSurface)) {
const surfaces = AGENT_PROMPT_SURFACE_KINDS.join(", ");
return `${label} surface ${surfaceIndex + 1} must be one of: ${surfaces}`;
}
}
return null;
}
function normalizeAgentPromptGuidance(guidance) {
if (!guidance) return;
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,
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 { clearPluginCommands as a, isOpenClawMainPromptSurface as c, canExposeSenderIsOwner as i, registerPluginCommand as n, isTrustedReservedCommandOwner as o, registerPluginCommandInRegistry as r, listRegisteredPluginAgentPromptGuidance as s, isReservedCommandName as t };