openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
260 lines (259 loc) • 12.7 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { m as normalizeUniqueSingleOrTrimmedStringList } from "./string-normalization-CECOqf64.mjs";
import { i as matchRootFileOpenFailure } from "./boundary-file-read-CCth2Hwg.mjs";
import "./utils-DgZn2dLu.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs";
import { f as readPluginCacheFile, o as parsePluginCacheJson, s as pluginCacheExistsSync, t as DEFAULT_PLUGIN_ENTRY_CANDIDATES, u as pluginCacheStatSync } from "./package-manifest-CEbdS_C4.mjs";
import { i as normalizeManifestActivation } from "./manifest-BXjFCp0F.mjs";
import path from "node:path";
//#region src/plugins/bundle-manifest.ts
/** Reads Agent/Codex/Claude/Cursor bundle manifests into OpenClaw plugin manifest metadata. */
/** Relative manifest path for Codex-style plugin bundles. */
const CODEX_BUNDLE_MANIFEST_RELATIVE_PATH = ".codex-plugin/plugin.json";
const CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH = ".claude-plugin/plugin.json";
const CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH = ".cursor-plugin/plugin.json";
const AGENT_BUNDLE_MANIFEST_RELATIVE_PATH = "plugin.json";
const AGENT_BUNDLE_EXTENSION_NAMESPACE = "ai.openclaw";
const AGENT_BUNDLE_MANIFEST_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
const MAX_AGENT_BUNDLE_MANIFEST_BYTES = 262144;
const log = createSubsystemLogger("plugins/bundle-manifest");
/** Normalizes string-or-list path fields from bundle manifests. */
function normalizeBundlePathList(value) {
return normalizeUniqueSingleOrTrimmedStringList(value);
}
function mergeBundlePathLists(...groups) {
return [...new Set(groups.flat())];
}
function hasInlineCapabilityValue(value) {
if (typeof value === "string") return value.trim().length > 0;
if (Array.isArray(value)) return value.length > 0;
if (isRecord(value)) return Object.keys(value).length > 0;
return value === true;
}
function slugifyPluginId(raw, rootDir) {
const fallback = path.basename(rootDir);
return (normalizeLowercaseStringOrEmpty(raw) || normalizeLowercaseStringOrEmpty(fallback)).replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "bundle-plugin";
}
function loadBundleManifestFile(params) {
const manifestPath = path.join(params.rootDir, params.manifestRelativePath);
const file = readPluginCacheFile({
rootDir: params.rootDir,
rootRealPath: params.rootRealPath,
relativePath: params.manifestRelativePath,
rejectHardlinks: params.rejectHardlinks,
maxBytes: MAX_AGENT_BUNDLE_MANIFEST_BYTES
});
if (!file.ok) return matchRootFileOpenFailure(file.failure, {
path: () => {
if (params.allowMissing) return {
ok: true,
raw: {},
manifestPath
};
return {
ok: false,
error: `plugin manifest not found: ${manifestPath}`,
manifestPath
};
},
fallback: (failure) => ({
ok: false,
error: `unsafe plugin manifest path: ${manifestPath} (${failure.reason})`,
manifestPath
})
});
const result = parsePluginCacheJson(file, { json5: !params.strictJson });
if (!result.ok) return {
ok: false,
error: `failed to parse plugin manifest: ${formatErrorMessage(result.error)}`,
manifestPath
};
if (!isRecord(result.value)) return {
ok: false,
error: "plugin manifest must be an object",
manifestPath
};
return {
ok: true,
raw: result.value,
manifestPath
};
}
function resolveCodexComponentDirs(raw, rootDir, component) {
const declared = normalizeBundlePathList(raw[component]);
if (declared.length > 0) return declared;
return pluginCacheExistsSync(path.join(rootDir, component)) ? [component] : [];
}
function resolveCursorCommandRootDirs(raw, rootDir) {
return resolveBundleComponentPaths(raw.commands, rootDir, [".cursor/commands"]);
}
function resolveCursorSkillDirs(raw, rootDir) {
return mergeBundlePathLists(resolveBundleComponentPaths(raw.skills, rootDir, ["skills"]), resolveCursorCommandRootDirs(raw, rootDir));
}
function resolveCursorAgentDirs(raw, rootDir) {
return resolveBundleComponentPaths(raw.subagents ?? raw.agents, rootDir, [".cursor/agents"]);
}
function resolveBundleComponentPaths(value, rootDir, defaults) {
const declared = normalizeBundlePathList(value);
return mergeBundlePathLists(defaults.filter((candidate) => pluginCacheExistsSync(path.join(rootDir, candidate))), declared);
}
function buildCursorCapabilities(raw, rootDir) {
const capabilities = [
...resolveCursorSkillDirs(raw, rootDir).length > 0 ? ["skills"] : [],
...resolveCursorCommandRootDirs(raw, rootDir).length > 0 ? ["commands"] : [],
...resolveCursorAgentDirs(raw, rootDir).length > 0 ? ["agents"] : []
];
for (const [capability, defaultPath] of [
["hooks", ".cursor/hooks.json"],
["rules", ".cursor/rules"],
["mcpServers", ".mcp.json"]
]) if (hasInlineCapabilityValue(raw[capability]) || pluginCacheExistsSync(path.join(rootDir, defaultPath))) capabilities.push(capability);
return capabilities;
}
function resolveAgentSkillDirs(rootDir) {
try {
return pluginCacheStatSync(path.join(rootDir, "skills"))?.isDirectory() ? ["skills"] : [];
} catch {
return [];
}
}
function resolveAgentActivation(raw, manifestPath) {
if (raw.extensions === void 0) return;
if (!isRecord(raw.extensions)) {
log.warn(`ignoring Agent Plugins extensions in ${manifestPath}: expected an object`);
return;
}
const openclawExtension = raw.extensions[AGENT_BUNDLE_EXTENSION_NAMESPACE];
if (openclawExtension === void 0) return;
if (!isRecord(openclawExtension)) {
log.warn(`ignoring Agent Plugins ${AGENT_BUNDLE_EXTENSION_NAMESPACE} extension in ${manifestPath}: expected an object`);
return;
}
return normalizeManifestActivation(openclawExtension.activation);
}
function loadBundleManifest(params) {
const rejectHardlinks = params.rejectHardlinks ?? true;
const manifestRelativePath = params.bundleFormat === "codex" ? CODEX_BUNDLE_MANIFEST_RELATIVE_PATH : params.bundleFormat === "cursor" ? CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH : params.bundleFormat === "agent" ? AGENT_BUNDLE_MANIFEST_RELATIVE_PATH : CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH;
const loaded = loadBundleManifestFile({
rootDir: params.rootDir,
...params.rootRealPath !== void 0 ? { rootRealPath: params.rootRealPath } : {},
manifestRelativePath,
rejectHardlinks,
allowMissing: params.bundleFormat === "claude",
strictJson: params.bundleFormat === "agent"
});
if (!loaded.ok) return loaded;
const raw = loaded.raw;
const interfaceRecord = isRecord(raw.interface) ? raw.interface : void 0;
const name = normalizeOptionalString(raw.name);
const description = normalizeOptionalString(raw.description) ?? normalizeOptionalString(raw.shortDescription) ?? normalizeOptionalString(interfaceRecord?.shortDescription);
const version = normalizeOptionalString(raw.version);
const manifest = {
id: slugifyPluginId(name, params.rootDir),
name,
description,
version,
skills: [],
settingsFiles: [],
hooks: [],
bundleFormat: params.bundleFormat,
activation: void 0,
capabilities: []
};
if (params.bundleFormat === "agent") {
if (raw.$schema !== AGENT_BUNDLE_MANIFEST_SCHEMA) return {
ok: false,
error: `root plugin.json is not an Agent Plugins manifest; expected $schema ${AGENT_BUNDLE_MANIFEST_SCHEMA}`,
manifestPath: loaded.manifestPath
};
if (!name) return {
ok: false,
error: "agent plugin manifest name must be a non-empty string",
manifestPath: loaded.manifestPath
};
manifest.skills = resolveAgentSkillDirs(params.rootDir);
manifest.activation = resolveAgentActivation(raw, loaded.manifestPath);
manifest.capabilities = [...manifest.skills.length > 0 ? ["skills"] : [], ...pluginCacheExistsSync(path.join(params.rootDir, "mcp.json")) ? ["mcpServers"] : []];
} else {
manifest.activation = normalizeManifestActivation(raw.activation);
if (params.bundleFormat === "codex") {
manifest.skills = resolveCodexComponentDirs(raw, params.rootDir, "skills");
manifest.hooks = resolveCodexComponentDirs(raw, params.rootDir, "hooks");
manifest.capabilities = [...manifest.skills.length > 0 ? ["skills"] : [], ...manifest.hooks.length > 0 ? ["hooks"] : []];
for (const [capability, defaultPath] of [["mcpServers", ".mcp.json"], ["apps", ".app.json"]]) if (hasInlineCapabilityValue(raw[capability]) || pluginCacheExistsSync(path.join(params.rootDir, defaultPath))) manifest.capabilities.push(capability);
} else if (params.bundleFormat === "cursor") {
manifest.skills = resolveCursorSkillDirs(raw, params.rootDir);
manifest.capabilities = buildCursorCapabilities(raw, params.rootDir);
} else Object.assign(manifest, resolveClaudeComponents(raw, params.rootDir));
}
const extensions = isRecord(raw.extensions) ? raw.extensions : void 0;
const openai = isRecord(extensions?.["com.openai"]) ? extensions["com.openai"] : void 0;
const onboardingSkill = normalizeOptionalString(openai?.onboardingSkill);
if (onboardingSkill) {
const relative = path.normalize(onboardingSkill);
if (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && path.basename(relative) === "SKILL.md" && manifest.skills.some((root) => {
const fromRoot = path.relative(path.resolve(params.rootDir, root), path.resolve(params.rootDir, relative));
return fromRoot !== ".." && !fromRoot.startsWith(`..${path.sep}`) && !path.isAbsolute(fromRoot);
})) manifest.onboardingSkill = relative;
}
return {
ok: true,
manifest,
manifestPath: loaded.manifestPath
};
}
function resolveClaudeComponents(raw, rootDir) {
const skillRoots = resolveBundleComponentPaths(raw.skills, rootDir, ["skills"]);
const commands = resolveBundleComponentPaths(raw.commands, rootDir, ["commands"]);
const agents = resolveBundleComponentPaths(raw.agents, rootDir, ["agents"]);
const outputStyles = resolveBundleComponentPaths(raw.outputStyles, rootDir, ["output-styles"]);
const skills = mergeBundlePathLists(skillRoots, commands, agents, outputStyles);
const settingsFiles = pluginCacheExistsSync(path.join(rootDir, "settings.json")) ? ["settings.json"] : [];
const hooks = resolveBundleComponentPaths(raw.hooks, rootDir, ["hooks/hooks.json"]);
return {
skills,
settingsFiles,
hooks,
capabilities: [
...skills.length > 0 ? ["skills"] : [],
...commands.length > 0 ? ["commands"] : [],
...agents.length > 0 ? ["agents"] : [],
...hasInlineCapabilityValue(raw.hooks) || hooks.length > 0 ? ["hooks"] : [],
...hasInlineCapabilityValue(raw.mcpServers) || resolveBundleComponentPaths(raw.mcpServers, rootDir, [".mcp.json"]).length > 0 ? ["mcpServers"] : [],
...hasInlineCapabilityValue(raw.lspServers) || resolveBundleComponentPaths(raw.lspServers, rootDir, [".lsp.json"]).length > 0 ? ["lspServers"] : [],
...hasInlineCapabilityValue(raw.outputStyles) || outputStyles.length > 0 ? ["outputStyles"] : [],
...settingsFiles.length > 0 ? ["settings"] : []
]
};
}
function detectBundleManifestFormat(rootDir, hasPackageExtensions = false) {
if (hasPackageExtensions && pluginCacheExistsSync(path.join(rootDir, "openclaw.plugin.json"))) return null;
if (pluginCacheExistsSync(path.join(rootDir, ".codex-plugin/plugin.json"))) return "codex";
if (pluginCacheExistsSync(path.join(rootDir, ".cursor-plugin/plugin.json"))) return "cursor";
if (pluginCacheExistsSync(path.join(rootDir, ".claude-plugin/plugin.json"))) return "claude";
if (pluginCacheExistsSync(path.join(rootDir, "openclaw.plugin.json"))) return null;
if (pluginCacheExistsSync(path.join(rootDir, "plugin.json"))) {
const agentManifest = loadBundleManifestFile({
rootDir,
manifestRelativePath: AGENT_BUNDLE_MANIFEST_RELATIVE_PATH,
rejectHardlinks: false,
strictJson: true
});
if (agentManifest.ok && agentManifest.raw.$schema === AGENT_BUNDLE_MANIFEST_SCHEMA) return "agent";
}
if (DEFAULT_PLUGIN_ENTRY_CANDIDATES.some((candidate) => pluginCacheExistsSync(path.join(rootDir, candidate)))) return null;
if ([
path.join(rootDir, "skills"),
path.join(rootDir, "commands"),
path.join(rootDir, "agents"),
path.join(rootDir, "hooks", "hooks.json"),
path.join(rootDir, ".mcp.json"),
path.join(rootDir, ".lsp.json"),
path.join(rootDir, "settings.json")
].some((candidate) => pluginCacheExistsSync(candidate))) return "claude";
return null;
}
//#endregion
export { detectBundleManifestFormat as a, normalizeBundlePathList as c, CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH as i, resolveBundleComponentPaths as l, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH as n, loadBundleManifest as o, CODEX_BUNDLE_MANIFEST_RELATIVE_PATH as r, mergeBundlePathLists as s, AGENT_BUNDLE_MANIFEST_RELATIVE_PATH as t };