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
This commit is contained in:
Kieran Klaassen
2026-03-03 20:30:27 -08:00
parent 020eb8836e
commit 4e9899f346
4 changed files with 59 additions and 6 deletions

View File

@@ -64,6 +64,10 @@ function buildManifest(plugin: ClaudePlugin, skillDirs: string[]): OpenClawPlugi
id: plugin.manifest.name, id: plugin.manifest.name,
name: formatDisplayName(plugin.manifest.name), name: formatDisplayName(plugin.manifest.name),
kind: "tool", kind: "tool",
configSchema: {
type: "object",
properties: {},
},
skills: skillDirs.map((dir) => `skills/${dir}`), skills: skillDirs.map((dir) => `skills/${dir}`),
} }
} }

View File

@@ -2,16 +2,18 @@ export type OpenClawPluginManifest = {
id: string id: string
name: string name: string
kind: "tool" kind: "tool"
configSchema?: { configSchema: OpenClawConfigSchema
type: "object"
additionalProperties: boolean
properties: Record<string, OpenClawConfigProperty>
required?: string[]
}
uiHints?: Record<string, OpenClawUiHint> uiHints?: Record<string, OpenClawUiHint>
skills?: string[] skills?: string[]
} }
export type OpenClawConfigSchema = {
type: "object"
properties: Record<string, OpenClawConfigProperty>
additionalProperties?: boolean
required?: string[]
}
export type OpenClawConfigProperty = { export type OpenClawConfigProperty = {
type: string type: string
description?: string description?: string

View File

@@ -108,6 +108,10 @@ describe("convertClaudeToOpenClaw", () => {
expect(bundle.manifest.id).toBe("compound-engineering") expect(bundle.manifest.id).toBe("compound-engineering")
expect(bundle.manifest.name).toBe("Compound Engineering") expect(bundle.manifest.name).toBe("Compound Engineering")
expect(bundle.manifest.kind).toBe("tool") 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/agent-security-reviewer")
expect(bundle.manifest.skills).toContain("skills/cmd-workflows:plan") expect(bundle.manifest.skills).toContain("skills/cmd-workflows:plan")
expect(bundle.manifest.skills).toContain("skills/existing-skill") expect(bundle.manifest.skills).toContain("skills/existing-skill")

View File

@@ -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: {},
})
})
})