From 4e9899f34693711b8997cf73eaa337f0da2321d6 Mon Sep 17 00:00:00 2001 From: Kieran Klaassen Date: Tue, 3 Mar 2026 20:30:27 -0800 Subject: [PATCH] fix(openclaw): emit empty configSchema in plugin manifests OpenClaw rejects generated plugin manifests that omit configSchema, even for tool plugins with no user configuration. Always emit an empty object schema so converted installs boot cleanly.\n\nAdd converter and writer regression coverage for the manifest shape.\n\nFixes #224 --- src/converters/claude-to-openclaw.ts | 4 +++ src/types/openclaw.ts | 14 +++++---- tests/openclaw-converter.test.ts | 4 +++ tests/openclaw-writer.test.ts | 43 ++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 tests/openclaw-writer.test.ts diff --git a/src/converters/claude-to-openclaw.ts b/src/converters/claude-to-openclaw.ts index 83a0192..ba2435c 100644 --- a/src/converters/claude-to-openclaw.ts +++ b/src/converters/claude-to-openclaw.ts @@ -64,6 +64,10 @@ function buildManifest(plugin: ClaudePlugin, skillDirs: string[]): OpenClawPlugi id: plugin.manifest.name, name: formatDisplayName(plugin.manifest.name), kind: "tool", + configSchema: { + type: "object", + properties: {}, + }, skills: skillDirs.map((dir) => `skills/${dir}`), } } diff --git a/src/types/openclaw.ts b/src/types/openclaw.ts index 5d68910..378336f 100644 --- a/src/types/openclaw.ts +++ b/src/types/openclaw.ts @@ -2,16 +2,18 @@ export type OpenClawPluginManifest = { id: string name: string kind: "tool" - configSchema?: { - type: "object" - additionalProperties: boolean - properties: Record - required?: string[] - } + configSchema: OpenClawConfigSchema uiHints?: Record skills?: string[] } +export type OpenClawConfigSchema = { + type: "object" + properties: Record + additionalProperties?: boolean + required?: string[] +} + export type OpenClawConfigProperty = { type: string description?: string diff --git a/tests/openclaw-converter.test.ts b/tests/openclaw-converter.test.ts index 7cde0ae..e1648d5 100644 --- a/tests/openclaw-converter.test.ts +++ b/tests/openclaw-converter.test.ts @@ -108,6 +108,10 @@ describe("convertClaudeToOpenClaw", () => { expect(bundle.manifest.id).toBe("compound-engineering") expect(bundle.manifest.name).toBe("Compound Engineering") expect(bundle.manifest.kind).toBe("tool") + expect(bundle.manifest.configSchema).toEqual({ + type: "object", + properties: {}, + }) expect(bundle.manifest.skills).toContain("skills/agent-security-reviewer") expect(bundle.manifest.skills).toContain("skills/cmd-workflows:plan") expect(bundle.manifest.skills).toContain("skills/existing-skill") diff --git a/tests/openclaw-writer.test.ts b/tests/openclaw-writer.test.ts new file mode 100644 index 0000000..ab618d9 --- /dev/null +++ b/tests/openclaw-writer.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test" +import { promises as fs } from "fs" +import os from "os" +import path from "path" +import { writeOpenClawBundle } from "../src/targets/openclaw" +import type { OpenClawBundle } from "../src/types/openclaw" + +describe("writeOpenClawBundle", () => { + test("writes openclaw.plugin.json with a configSchema", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-writer-")) + const bundle: OpenClawBundle = { + manifest: { + id: "compound-engineering", + name: "Compound Engineering", + kind: "tool", + configSchema: { + type: "object", + properties: {}, + }, + skills: [], + }, + packageJson: { + name: "openclaw-compound-engineering", + version: "1.0.0", + }, + entryPoint: "export default async function register() {}", + skills: [], + skillDirCopies: [], + commands: [], + } + + await writeOpenClawBundle(tempRoot, bundle) + + const manifest = JSON.parse( + await fs.readFile(path.join(tempRoot, "openclaw.plugin.json"), "utf8"), + ) + + expect(manifest.configSchema).toEqual({ + type: "object", + properties: {}, + }) + }) +})