From 94f1a94a9ce0f5ab00c39a6267ecdd7eaf6fd929 Mon Sep 17 00:00:00 2001 From: Sasan Sotoodehfar Date: Mon, 19 Oct 2026 18:07:04 -0400 Subject: [PATCH] catalog: add OpenClaw and Hermes hosts for mlx-agent --- README.md | 22 +++++++++++++---- catalog.json | 4 ++- catalog.schema.json | 4 ++- providers/hermes/catalog.json | 13 ++++++++++ providers/openclaw/catalog.json | 13 ++++++++++ scripts/validate-catalog.mjs | 19 ++++++++++---- scripts/validate-catalog.test.mjs | 41 +++++++++++++++++++++++++------ 7 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 providers/hermes/catalog.json create mode 100644 providers/openclaw/catalog.json diff --git a/README.md b/README.md index c09bce6..f1b6b69 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ ## Plugins -| Plugin | Claude | Codex | Gemini | OpenCode | AgentSkills | -| --- | :---: | :---: | :---: | :---: | :---: | -| [`mlx-agent`](https://github.com/cavi-ai/mlx-agent) — discover, verify, and wire local MLX models | ✓ | ✓ | — | ✓ | ✓ | -| [`obsidian-agent`](https://github.com/cavi-ai/obsidian-agent) — portable vault workflows over the official Obsidian CLI | ✓ | ✓ | ✓ | ✓ | ✓ | +| Plugin | Claude | Codex | Gemini | OpenCode | OpenClaw | Hermes | AgentSkills | +| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| [`mlx-agent`](https://github.com/cavi-ai/mlx-agent) — discover, verify, and wire local MLX models | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | +| [`obsidian-agent`](https://github.com/cavi-ai/obsidian-agent) — portable vault workflows over the official Obsidian CLI | ✓ | ✓ | ✓ | ✓ | — | — | ✓ | -The machine-readable source of truth is [`catalog.json`](catalog.json). Claude and Codex consume native marketplace projections. Gemini and OpenCode use the discovery records under `providers/`, which link to each source repository's tested installer; those files do not claim a native marketplace protocol. Portable AgentSkills packages remain available from each plugin repository. +The machine-readable source of truth is [`catalog.json`](catalog.json). Claude and Codex consume native marketplace projections. Gemini, OpenCode, OpenClaw, and Hermes use the discovery records under `providers/`, which link to each source repository's tested installer; those files do not claim a native marketplace protocol. Portable AgentSkills packages remain available from each plugin repository. ## Claude Code @@ -59,6 +59,18 @@ node obsidian-agent/scripts/install.mjs --host opencode --scope user --dry-run Review the destination paths and use the exact preview hash when confirming. +## OpenClaw and Hermes + +OpenClaw and Hermes discovery metadata is in [`providers/openclaw/catalog.json`](providers/openclaw/catalog.json) and [`providers/hermes/catalog.json`](providers/hermes/catalog.json). The `mlx-agent` installer places its skills in `$OPENCLAW_STATE_DIR/skills` or `$HERMES_HOME/skills`: + +```sh +git clone https://github.com/cavi-ai/mlx-agent.git +python3 mlx-agent/scripts/mlx-agent install openclaw --scope user --dry-run --json +python3 mlx-agent/scripts/mlx-agent install hermes --scope user --dry-run --json +``` + +Review the destination paths and use the exact preview hash when confirming. `obsidian-agent` does not ship an OpenClaw or Hermes adapter. + ## AgentSkills Both plugins publish portable skills under their AgentSkills adapter. Follow the selected repository's project- or user-scope installation instructions; the catalog does not copy or fork skill content. diff --git a/catalog.json b/catalog.json index 2bde1d6..aab0469 100644 --- a/catalog.json +++ b/catalog.json @@ -7,11 +7,13 @@ "repository": "cavi-ai/mlx-agent", "license": "MIT", "summary": "Discover, verify, and wire local MLX-optimized models on Apple Silicon.", - "hosts": ["claude", "codex", "opencode", "agentskills"], + "hosts": ["claude", "codex", "opencode", "openclaw", "hermes", "agentskills"], "packages": { "claude": { "path": "." }, "codex": { "path": "packages/codex/mlx-agent" }, "opencode": { "path": "providers/opencode" }, + "openclaw": { "path": "providers/agentskills" }, + "hermes": { "path": "providers/agentskills" }, "agentskills": { "path": "providers/agentskills" } } }, diff --git a/catalog.schema.json b/catalog.schema.json index f4b8d05..9dc432c 100644 --- a/catalog.schema.json +++ b/catalog.schema.json @@ -16,7 +16,7 @@ } }, "$defs": { - "host": { "enum": ["claude", "codex", "gemini", "opencode", "agentskills"] }, + "host": { "enum": ["claude", "codex", "gemini", "opencode", "openclaw", "hermes", "agentskills"] }, "package": { "type": "object", "additionalProperties": false, @@ -41,6 +41,8 @@ "codex": { "$ref": "#/$defs/package" }, "gemini": { "$ref": "#/$defs/package" }, "opencode": { "$ref": "#/$defs/package" }, + "openclaw": { "$ref": "#/$defs/package" }, + "hermes": { "$ref": "#/$defs/package" }, "agentskills": { "$ref": "#/$defs/package" } } } diff --git a/providers/hermes/catalog.json b/providers/hermes/catalog.json new file mode 100644 index 0000000..e8c3f33 --- /dev/null +++ b/providers/hermes/catalog.json @@ -0,0 +1,13 @@ +{ + "name": "cavi-ai", + "host": "hermes", + "protocol": "discovery", + "plugins": [ + { + "name": "mlx-agent", + "repository": "cavi-ai/mlx-agent", + "package": "providers/agentskills", + "install": { "command": "git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install hermes --scope user --dry-run --json" } + } + ] +} diff --git a/providers/openclaw/catalog.json b/providers/openclaw/catalog.json new file mode 100644 index 0000000..42e40d8 --- /dev/null +++ b/providers/openclaw/catalog.json @@ -0,0 +1,13 @@ +{ + "name": "cavi-ai", + "host": "openclaw", + "protocol": "discovery", + "plugins": [ + { + "name": "mlx-agent", + "repository": "cavi-ai/mlx-agent", + "package": "providers/agentskills", + "install": { "command": "git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install openclaw --scope user --dry-run --json" } + } + ] +} diff --git a/scripts/validate-catalog.mjs b/scripts/validate-catalog.mjs index 6e47900..38c9aa1 100644 --- a/scripts/validate-catalog.mjs +++ b/scripts/validate-catalog.mjs @@ -4,7 +4,7 @@ import { createHash } from "node:crypto"; import path from "node:path"; import { fileURLToPath } from "node:url"; -export const HOSTS = ["claude", "codex", "gemini", "opencode", "agentskills"]; +export const HOSTS = ["claude", "codex", "gemini", "opencode", "openclaw", "hermes", "agentskills"]; // Marketplace identity: what users type after @ in `plugin install @`. export const MARKETPLACE = "cavi-ai"; const REQUIRED = ["mlx-agent", "obsidian-agent"]; @@ -24,14 +24,23 @@ const DISCOVERY_COMMANDS = { opencode: { "mlx-agent": "git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install opencode --scope user --dry-run --json", "obsidian-agent": "git clone https://github.com/cavi-ai/obsidian-agent.git && node obsidian-agent/scripts/install.mjs --host opencode --scope user --dry-run" + }, + openclaw: { + "mlx-agent": "git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install openclaw --scope user --dry-run --json" + }, + hermes: { + "mlx-agent": "git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install hermes --scope user --dry-run --json" } }; +const DISCOVERY_HOSTS = Object.keys(DISCOVERY_COMMANDS); const LEGACY = new Set(["claude-plugins", "claude-obsidian", "claude-obsidian-plugin"]); const PROJECTIONS = { claude: ".claude-plugin/marketplace.json", codex: ".agents/plugins/marketplace.json", gemini: "providers/gemini/catalog.json", - opencode: "providers/opencode/catalog.json" + opencode: "providers/opencode/catalog.json", + openclaw: "providers/openclaw/catalog.json", + hermes: "providers/hermes/catalog.json" }; const JSON_FAILURE = Symbol("JSON_FAILURE"); @@ -277,7 +286,7 @@ export async function validateCatalog(root = process.cwd()) { if (!projection || typeof projection !== "object" || Array.isArray(projection)) { errors.push(`${host} projection must be an object`); continue; } if (projection.name !== MARKETPLACE) errors.push(`${host} projection name must be ${MARKETPLACE}`); if (host === "claude" || host === "codex") validateNativeProjection(host, projection, errors); - if (host === "gemini" || host === "opencode") { + if (DISCOVERY_HOSTS.includes(host)) { if (projection.host !== host) errors.push(`${host} projection host mismatch`); if (projection.protocol !== "discovery") errors.push(`${host} projection must use discovery protocol`); } @@ -288,10 +297,10 @@ export async function validateCatalog(root = process.cwd()) { const projected = new Set(); for (const raw of projection.plugins) { const entry = projectionIdentity(raw); - if ((host === "gemini" || host === "opencode") && (typeof raw?.install?.command !== "string" || !raw.install.command.trim())) { + if (DISCOVERY_HOSTS.includes(host) && (typeof raw?.install?.command !== "string" || !raw.install.command.trim())) { errors.push(`${host} install command missing: ${entry.name}`); } - if ((host === "gemini" || host === "opencode") && DISCOVERY_COMMANDS[host]?.[entry.name] !== raw?.install?.command) errors.push(`${host} install command mismatch: ${entry.name}`); + if (DISCOVERY_HOSTS.includes(host) && DISCOVERY_COMMANDS[host]?.[entry.name] !== raw?.install?.command) errors.push(`${host} install command mismatch: ${entry.name}`); if (projected.has(entry.name)) errors.push(`${host} duplicate projection: ${entry.name}`); projected.add(entry.name); const canonical = byName.get(entry.name); diff --git a/scripts/validate-catalog.test.mjs b/scripts/validate-catalog.test.mjs index 53a9168..c7387ca 100644 --- a/scripts/validate-catalog.test.mjs +++ b/scripts/validate-catalog.test.mjs @@ -7,13 +7,16 @@ import test from "node:test"; import { MARKETPLACE, validateCatalog } from "./validate-catalog.mjs"; -const hosts = ["claude", "codex", "gemini", "opencode", "agentskills"]; +const hosts = ["claude", "codex", "gemini", "opencode", "openclaw", "hermes", "agentskills"]; const inventories = { "mlx-agent": ["mlx-adopt", "mlx-bench", "mlx-scout", "mlx-wire"], "obsidian-agent": ["build-retrospective", "connection-finder", "consistent-tagging", "daily-rollup", "dedup-merge", "frontmatter-normalizer", "manifest-content", "manifest-core", "manifest-feature", "manifest-infra", "manifest-pm", "manifest-research", "manifest-risk", "manifest-vault", "meeting-cleanup", "moc-builder", "note-splitter", "outline-to-draft", "plan-to-spec", "source-digest", "summarize-and-link", "task-harvester", "tracker-driver", "vault-grounding", "vault-synthesis", "wikilink-weaver"] }; const sourceCommits = { "mlx-agent": "923627988cf98e984fa36e0b940f86e7263e2958", "obsidian-agent": "7efecb058fce8185056cbe7868d6de1af11879a9" }; -const pluginHosts = { "mlx-agent": ["claude", "codex", "opencode", "agentskills"] }; +const pluginHosts = { + "mlx-agent": ["claude", "codex", "opencode", "openclaw", "hermes", "agentskills"], + "obsidian-agent": ["claude", "codex", "gemini", "opencode", "agentskills"] +}; function plugin(name, repository = `cavi-ai/${name}`, supportedHosts = pluginHosts[name] ?? hosts) { return { @@ -46,12 +49,11 @@ async function createFixturePackage(root, name) { await writeFile(path.join(packageRoot, "provenance.json"), JSON.stringify({ repository: `cavi-ai/${name}`, source_commit: sourceCommits[name], source_path: name === "mlx-agent" ? "providers/codex" : ".", included: [".codex-plugin", "skills"], ...(name === "mlx-agent" ? { excluded: [".mlx-agent-generated-files.json", "skills/*/scripts/mlx-agent-mcp", "skills/*/src/mlx_agent/mcp_server.py"], exclusion_reason: "Codex skills use the packaged CLI entrypoint; MCP transport artifacts are not required or distributed by this catalog." } : {}), integrity: { algorithm: "sha256", tree: await fixtureTreeHash(packageRoot) } })); } -async function fixture({ catalog, claude = [], codex = [], gemini = [], opencode = [] }) { +async function fixture({ catalog, claude = [], codex = [], gemini = [], opencode = [], openclaw = [], hermes = [] }) { const root = await mkdtemp(path.join(tmpdir(), "plugins-catalog-")); await mkdir(path.join(root, ".claude-plugin"), { recursive: true }); await mkdir(path.join(root, ".agents/plugins"), { recursive: true }); - await mkdir(path.join(root, "providers/gemini"), { recursive: true }); - await mkdir(path.join(root, "providers/opencode"), { recursive: true }); + for (const host of ["gemini", "opencode", "openclaw", "hermes"]) await mkdir(path.join(root, "providers", host), { recursive: true }); await cp(path.resolve(new URL("../catalog.schema.json", import.meta.url).pathname), path.join(root, "catalog.schema.json")); await writeFile(path.join(root, "catalog.json"), JSON.stringify(catalog)); const nativeClaude = claude.map((item) => item.source ? item : { name: item.name, source: { source: "github", repo: item.repository }, description: `${item.name} fixture` }); @@ -60,6 +62,8 @@ async function fixture({ catalog, claude = [], codex = [], gemini = [], opencode await writeFile(path.join(root, ".agents/plugins/marketplace.json"), JSON.stringify({ name: MARKETPLACE, interface: { displayName: "Cavi AI" }, plugins: nativeCodex })); await writeFile(path.join(root, "providers/gemini/catalog.json"), JSON.stringify({ name: MARKETPLACE, host: "gemini", protocol: "discovery", plugins: gemini })); await writeFile(path.join(root, "providers/opencode/catalog.json"), JSON.stringify({ name: MARKETPLACE, host: "opencode", protocol: "discovery", plugins: opencode })); + await writeFile(path.join(root, "providers/openclaw/catalog.json"), JSON.stringify({ name: MARKETPLACE, host: "openclaw", protocol: "discovery", plugins: openclaw })); + await writeFile(path.join(root, "providers/hermes/catalog.json"), JSON.stringify({ name: MARKETPLACE, host: "hermes", protocol: "discovery", plugins: hermes })); for (const name of Object.keys(inventories)) await createFixturePackage(root, name); const expectedIntegrity = {}; for (const name of Object.keys(inventories)) { @@ -81,7 +85,8 @@ const entry = (name, host) => ({ repository: `cavi-ai/${name}`, package: canonical.plugins.find((item) => item.name === name).packages[host].path, ...(host === "gemini" ? { install: { command: "gemini extensions install https://github.com/cavi-ai/obsidian-agent" } } : {}), - ...(host === "opencode" ? { install: { command: name === "mlx-agent" ? "git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install opencode --scope user --dry-run --json" : "git clone https://github.com/cavi-ai/obsidian-agent.git && node obsidian-agent/scripts/install.mjs --host opencode --scope user --dry-run" } } : {}) + ...(host === "opencode" ? { install: { command: name === "mlx-agent" ? "git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install opencode --scope user --dry-run --json" : "git clone https://github.com/cavi-ai/obsidian-agent.git && node obsidian-agent/scripts/install.mjs --host opencode --scope user --dry-run" } } : {}), + ...(host === "openclaw" || host === "hermes" ? { install: { command: `git clone https://github.com/cavi-ai/mlx-agent.git && python3 mlx-agent/scripts/mlx-agent install ${host} --scope user --dry-run --json` } } : {}) }); test("accepts exactly the two canonical plugins and truthful host projections", async () => { @@ -90,11 +95,33 @@ test("accepts exactly the two canonical plugins and truthful host projections", claude: [entry("mlx-agent", "claude"), entry("obsidian-agent", "claude")], codex: [entry("mlx-agent", "codex"), entry("obsidian-agent", "codex")], gemini: [entry("obsidian-agent", "gemini")], - opencode: [entry("mlx-agent", "opencode"), entry("obsidian-agent", "opencode")] + opencode: [entry("mlx-agent", "opencode"), entry("obsidian-agent", "opencode")], + openclaw: [entry("mlx-agent", "openclaw")], + hermes: [entry("mlx-agent", "hermes")] }); assert.deepEqual(await validateCatalog(root), []); }); +test("requires openclaw and hermes discovery records only for plugins that declare them", async () => { + const root = await fixture({ + catalog: canonical, + claude: [entry("mlx-agent", "claude"), entry("obsidian-agent", "claude")], + codex: [entry("mlx-agent", "codex"), entry("obsidian-agent", "codex")], + gemini: [entry("obsidian-agent", "gemini")], + opencode: [entry("mlx-agent", "opencode"), entry("obsidian-agent", "opencode")], + openclaw: [entry("mlx-agent", "openclaw"), { name: "obsidian-agent", repository: "cavi-ai/obsidian-agent", package: "providers/agentskills", install: { command: "git clone https://github.com/cavi-ai/obsidian-agent.git && node obsidian-agent/scripts/install.mjs --host openclaw --scope user --dry-run" } }], + hermes: [{ ...entry("mlx-agent", "hermes"), install: { command: entry("mlx-agent", "openclaw").install.command } }] + }); + const errors = await validateCatalog(root); + assert(errors.some((error) => error.includes("openclaw projects unsupported plugin host: obsidian-agent"))); + assert(errors.some((error) => error.includes("openclaw install command mismatch: obsidian-agent"))); + assert(errors.some((error) => error.includes("hermes install command mismatch: mlx-agent"))); + const missing = await validateCatalog(await fixture({ catalog: canonical })); + assert(missing.includes("openclaw projection missing: mlx-agent")); + assert(missing.includes("hermes projection missing: mlx-agent")); + assert(!missing.some((error) => error.includes("projection missing: obsidian-agent") && /^(openclaw|hermes) /.test(error))); +}); + test("rejects a discovery projection for a host the plugin does not declare", async () => { const root = await fixture({ catalog: canonical,