openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
305 lines (299 loc) • 13.1 kB
JavaScript
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { c as isRecord } from "./utils-CCC-BEJH.js";
import { n as defaultRuntime } from "./runtime-B4lgFmsS.js";
import { i as loadPluginManifest, n as PLUGIN_MANIFEST_FILENAME, o as resolvePackageExtensionEntries } from "./manifest-BcY4wyAU.js";
import { n as getCachedPluginModuleLoader, t as createPluginModuleLoaderCache, u as toSafeImportPath } from "./plugin-module-loader-cache-lz0qhu-X.js";
import { t as buildPluginLoaderAliasMap } from "./sdk-alias-ZgondhPg.js";
import { t as unwrapDefaultModuleExport } from "./module-export-DsZgGIbX.js";
import { n as getToolPluginMetadata } from "./tool-plugin-DT5spzlx.js";
import fs from "node:fs";
import path from "node:path";
//#region src/cli/plugins-authoring-command.ts
const toolPluginEntryModuleLoaders = createPluginModuleLoaderCache();
function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function writeJsonFile(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
function jsStringLiteral(value) {
return JSON.stringify(value);
}
function normalizeRelativePath(rootDir, targetPath) {
const relative = path.relative(rootDir, path.resolve(rootDir, targetPath)).replaceAll(path.sep, "/");
if (relative === ".." || relative.startsWith("../")) throw new Error(`entry must stay inside plugin root: ${targetPath}`);
return relative.startsWith(".") ? relative : `./${relative}`;
}
function resolveRootDir(input) {
return path.resolve(input ?? process.cwd());
}
function resolveEntryPath(rootDir, entry) {
if (entry) return path.resolve(rootDir, entry);
const packagePath = path.join(rootDir, "package.json");
if (fs.existsSync(packagePath)) {
const extensionResolution = resolvePackageExtensionEntries(readJsonFile(packagePath));
if (extensionResolution.status === "ok" && extensionResolution.entries[0]) return path.resolve(rootDir, extensionResolution.entries[0]);
}
return path.resolve(rootDir, "src/index.ts");
}
function readPackageManifest(rootDir) {
const packagePath = path.join(rootDir, "package.json");
if (!fs.existsSync(packagePath)) throw new Error(`package.json not found: ${packagePath}`);
return readJsonFile(packagePath);
}
async function importToolPluginEntry(entryPath) {
const loaded = getCachedPluginModuleLoader({
cache: toolPluginEntryModuleLoaders,
modulePath: entryPath,
importerUrl: import.meta.url,
loaderFilename: entryPath,
aliasMap: buildPluginLoaderAliasMap(entryPath, process.argv[1], import.meta.url)
})(toSafeImportPath(entryPath));
const mod = loaded && typeof loaded === "object" ? loaded : void 0;
const candidate = unwrapDefaultModuleExport(mod?.default ?? mod?.createEntry ?? mod?.entry ?? loaded);
return typeof candidate === "function" ? candidate() : candidate;
}
async function loadToolPlugin(params) {
if (!fs.existsSync(params.entryPath)) throw new Error(`plugin entry not found: ${normalizeRelativePath(params.rootDir, params.entryPath)}`);
const entry = await importToolPluginEntry(params.entryPath);
const metadata = getToolPluginMetadata(entry);
if (!metadata) throw new Error(`plugin entry does not expose defineToolPlugin metadata: ${normalizeRelativePath(params.rootDir, params.entryPath)}`);
return {
entry,
metadata
};
}
function buildToolPluginManifest(params) {
const toolMetadata = buildToolPluginToolMetadata(params.metadata, params.existingManifest);
const existingContracts = isRecord(params.existingManifest?.contracts) ? params.existingManifest.contracts : {};
const { contracts: _existingContracts, toolMetadata: _existingToolMetadata, ...existingManifestFields } = params.existingManifest ?? {};
return {
...existingManifestFields,
id: params.metadata.id,
name: params.metadata.name,
description: params.metadata.description,
version: typeof params.packageManifest.version === "string" ? params.packageManifest.version : "0.0.0",
configSchema: params.metadata.configSchema,
activation: params.metadata.activation,
contracts: {
...existingContracts,
tools: params.metadata.tools.map((tool) => tool.name)
},
...toolMetadata ? { toolMetadata } : {}
};
}
function buildToolPluginToolMetadata(metadata, existingManifest) {
const existingToolMetadata = isRecord(existingManifest?.toolMetadata) ? existingManifest.toolMetadata : {};
const nextEntries = metadata.tools.map((tool) => {
const existingValue = existingToolMetadata[tool.name];
const existing = isRecord(existingValue) ? { ...existingValue } : {};
if (tool.optional) existing.optional = true;
else delete existing.optional;
return [tool.name, existing];
}).filter(([, value]) => Object.keys(value).length > 0);
return nextEntries.length > 0 ? Object.fromEntries(nextEntries) : void 0;
}
function buildToolPluginPackageManifest(params) {
const openclaw = params.packageManifest.openclaw && typeof params.packageManifest.openclaw === "object" && !Array.isArray(params.packageManifest.openclaw) ? { ...params.packageManifest.openclaw } : {};
const extensions = uniqueStrings([...Array.isArray(openclaw.extensions) ? openclaw.extensions.filter((entry) => typeof entry === "string") : [], params.entry]);
return {
...params.packageManifest,
openclaw: {
...openclaw,
extensions
}
};
}
function validateToolPluginProject(params) {
const errors = [];
const expectedManifest = buildToolPluginManifest({
metadata: params.metadata,
packageManifest: params.packageManifest,
existingManifest: params.manifest
});
if (JSON.stringify(params.manifest) !== JSON.stringify(expectedManifest)) errors.push("openclaw.plugin.json generated metadata is stale. Run openclaw plugins build.");
if (params.manifest.id !== params.metadata.id) errors.push(`openclaw.plugin.json id (${String(params.manifest.id)}) must match entry id (${params.metadata.id})`);
if (!params.manifest.configSchema || typeof params.manifest.configSchema !== "object") errors.push("openclaw.plugin.json must include object configSchema");
const manifestContracts = params.manifest.contracts;
const manifestTools = Array.isArray(manifestContracts?.tools) ? manifestContracts.tools.filter((tool) => typeof tool === "string") : [];
const metadataTools = params.metadata.tools.map((tool) => tool.name);
const missing = metadataTools.filter((tool) => !manifestTools.includes(tool));
const extra = manifestTools.filter((tool) => !metadataTools.includes(tool));
if (missing.length > 0) errors.push(`openclaw.plugin.json contracts.tools is missing: ${missing.join(", ")}`);
if (extra.length > 0) errors.push(`openclaw.plugin.json contracts.tools has no matching defineToolPlugin tool: ${extra.join(", ")}`);
const extensionResolution = resolvePackageExtensionEntries(params.packageManifest);
if (extensionResolution.status !== "ok") errors.push(extensionResolution.status === "missing" || extensionResolution.status === "empty" ? "package.json must include openclaw.extensions" : extensionResolution.error);
else if (!extensionResolution.entries.includes(params.entry)) errors.push(`package.json openclaw.extensions must include ${params.entry}`);
return errors;
}
async function runPluginsBuildCommand(opts) {
const rootDir = resolveRootDir(opts.root);
const entryPath = resolveEntryPath(rootDir, opts.entry);
const entryRelative = normalizeRelativePath(rootDir, entryPath);
const packagePath = path.join(rootDir, "package.json");
const packageManifest = readPackageManifest(rootDir);
const { metadata } = await loadToolPlugin({
rootDir,
entryPath
});
const manifestPath = path.join(rootDir, PLUGIN_MANIFEST_FILENAME);
const currentManifest = fs.existsSync(manifestPath) ? readJsonFile(manifestPath) : void 0;
const manifest = buildToolPluginManifest({
metadata,
packageManifest,
existingManifest: currentManifest
});
const nextPackageManifest = buildToolPluginPackageManifest({
packageManifest,
entry: entryRelative
});
if (opts.check) {
const currentPackage = readJsonFile(packagePath);
if (JSON.stringify(currentManifest) !== JSON.stringify(manifest) || JSON.stringify(currentPackage) !== JSON.stringify(nextPackageManifest)) {
defaultRuntime.error("Generated plugin metadata is out of date. Run openclaw plugins build.");
return defaultRuntime.exit(1);
}
defaultRuntime.log("Plugin metadata is up to date.");
return;
}
writeJsonFile(manifestPath, manifest);
writeJsonFile(packagePath, nextPackageManifest);
defaultRuntime.log(`Wrote ${path.relative(process.cwd(), manifestPath) || "openclaw.plugin.json"}`);
defaultRuntime.log(`Updated ${path.relative(process.cwd(), packagePath) || "package.json"}`);
}
async function runPluginsValidateCommand(opts) {
const rootDir = resolveRootDir(opts.root);
const entryPath = resolveEntryPath(rootDir, opts.entry);
const entryRelative = normalizeRelativePath(rootDir, entryPath);
const packageManifest = readPackageManifest(rootDir);
const manifestResult = loadPluginManifest(rootDir, false);
if (!manifestResult.ok) {
defaultRuntime.error(manifestResult.error);
return defaultRuntime.exit(1);
}
const manifest = readJsonFile(path.join(rootDir, PLUGIN_MANIFEST_FILENAME));
const { metadata } = await loadToolPlugin({
rootDir,
entryPath
});
const errors = validateToolPluginProject({
metadata,
manifest,
packageManifest,
entry: entryRelative
});
if (errors.length > 0) {
for (const error of errors) defaultRuntime.error(error);
return defaultRuntime.exit(1);
}
defaultRuntime.log(`Plugin ${metadata.id} is valid.`);
}
function assertCanCreate(filePath, force) {
if (!force && fs.existsSync(filePath)) throw new Error(`Refusing to overwrite existing path: ${filePath}`);
}
function titleFromId(id) {
return id.split(/[-_]/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
}
async function runPluginsInitCommand(id, opts) {
const rootDir = path.resolve(opts.directory ?? id);
const force = opts.force === true;
const name = opts.name ?? titleFromId(id);
assertCanCreate(rootDir, force);
fs.mkdirSync(path.join(rootDir, "src"), { recursive: true });
const packageManifest = {
name: `openclaw-plugin-${id}`,
version: "0.1.0",
type: "module",
private: true,
scripts: {
build: "tsc -p tsconfig.json",
"plugin:build": "npm run build && openclaw plugins build --entry ./dist/index.js",
"plugin:validate": "npm run build && openclaw plugins validate --entry ./dist/index.js",
test: "vitest run"
},
files: [
"dist",
"openclaw.plugin.json",
"README.md"
],
peerDependencies: { openclaw: ">=2026.5.17" },
dependencies: { typebox: "^1.1.38" },
devDependencies: {
openclaw: "latest",
typescript: "^5.9.0",
vitest: "^3.2.0"
},
openclaw: { extensions: ["./dist/index.js"] }
};
const idLiteral = jsStringLiteral(id);
const indexSource = `import { Type } from "typebox";
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
export default defineToolPlugin({
id: ${idLiteral},
name: ${jsStringLiteral(name)},
description: ${jsStringLiteral(`Add ${name} tools to OpenClaw.`)},
tools: (tool) => [
tool({
name: "echo",
description: "Echo input text.",
parameters: Type.Object({
input: Type.String({ description: "Text to echo." }),
}),
execute: async ({ input }) => ({ input }),
}),
],
});
`;
const testSource = `import { describe, expect, it } from "vitest";
import entry from "./index.js";
import { getToolPluginMetadata } from "openclaw/plugin-sdk/tool-plugin";
describe(${idLiteral}, () => {
it("declares tool metadata", () => {
expect(getToolPluginMetadata(entry)?.tools.map((tool) => tool.name)).toEqual(["echo"]);
});
});
`;
const readmeSource = `# ${name}
Simple OpenClaw tool plugin.
## Build
\`\`\`bash
npm install
npm run plugin:build
npm run plugin:validate
npm test
\`\`\`
`;
const tsconfig = {
compilerOptions: {
target: "ES2022",
module: "NodeNext",
moduleResolution: "NodeNext",
strict: true,
declaration: true,
outDir: "dist",
skipLibCheck: true
},
include: ["src/**/*.ts"]
};
writeJsonFile(path.join(rootDir, "package.json"), packageManifest);
fs.writeFileSync(path.join(rootDir, "src/index.ts"), indexSource);
fs.writeFileSync(path.join(rootDir, "src/index.test.ts"), testSource);
fs.writeFileSync(path.join(rootDir, "README.md"), readmeSource);
writeJsonFile(path.join(rootDir, "tsconfig.json"), tsconfig);
writeJsonFile(path.join(rootDir, PLUGIN_MANIFEST_FILENAME), {
id,
name,
description: `Add ${name} tools to OpenClaw.`,
version: packageManifest.version,
configSchema: {
type: "object",
additionalProperties: false,
properties: {}
},
activation: { onStartup: true },
contracts: { tools: ["echo"] }
});
defaultRuntime.log(`Created ${path.relative(process.cwd(), rootDir) || "."}`);
}
//#endregion
export { buildToolPluginManifest, buildToolPluginPackageManifest, loadToolPlugin, runPluginsBuildCommand, runPluginsInitCommand, runPluginsValidateCommand, validateToolPluginProject };