Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

| Plugin | Claude | Codex | Gemini | OpenCode | AgentSkills |
| --- | :---: | :---: | :---: | :---: | :---: |
| [`mlx-agent`](https://github.com/cavi-ai/mlx-agent) — discover, verify, and wire local MLX models | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`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.
Expand Down Expand Up @@ -37,16 +37,13 @@ These package projections are distribution artifacts, not independent workflow s

## Gemini CLI

Gemini discovery metadata is in [`providers/gemini/catalog.json`](providers/gemini/catalog.json). Each entry records the source repository's native installation command. For example:
Gemini discovery metadata is in [`providers/gemini/catalog.json`](providers/gemini/catalog.json). Each entry records the source repository's native installation command:

```sh
git clone https://github.com/cavi-ai/mlx-agent.git
gemini extensions install ./mlx-agent/providers/gemini

gemini extensions install https://github.com/cavi-ai/obsidian-agent
```

Both commands use Gemini's native extension loader. The discovery catalog itself does not install anything.
The command uses Gemini's native extension loader. The discovery catalog itself does not install anything. `mlx-agent` does not ship a Gemini extension.

## OpenCode

Expand Down Expand Up @@ -75,4 +72,4 @@ node --test
node scripts/validate-catalog.mjs
```

Validation rejects identity drift, unknown or missing hosts, duplicate entries, non-plugin products, and projections that disagree with the canonical repository or package path.
Validation rejects identity drift, unknown hosts, packages for undeclared hosts, duplicate entries, non-plugin products, and projections that disagree with the canonical host list, repository, or package path.
3 changes: 1 addition & 2 deletions catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
"repository": "cavi-ai/mlx-agent",
"license": "MIT",
"summary": "Discover, verify, and wire local MLX-optimized models on Apple Silicon.",
"hosts": ["claude", "codex", "gemini", "opencode", "agentskills"],
"hosts": ["claude", "codex", "opencode", "agentskills"],
"packages": {
"claude": { "path": "." },
"codex": { "path": "packages/codex/mlx-agent" },
"gemini": { "path": "providers/gemini" },
"opencode": { "path": "providers/opencode" },
"agentskills": { "path": "providers/agentskills" }
}
Expand Down
3 changes: 1 addition & 2 deletions catalog.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@
"repository": { "type": "string", "pattern": "^cavi-ai/[a-z0-9-]+$" },
"license": { "type": "string", "minLength": 1 },
"summary": { "type": "string", "minLength": 1 },
"hosts": { "type": "array", "minItems": 5, "maxItems": 5, "uniqueItems": true, "items": { "$ref": "#/$defs/host" } },
"hosts": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/host" } },
"packages": {
"type": "object",
"additionalProperties": false,
"required": ["claude", "codex", "gemini", "opencode", "agentskills"],
"properties": {
"claude": { "$ref": "#/$defs/package" },
"codex": { "$ref": "#/$defs/package" },
Expand Down
6 changes: 0 additions & 6 deletions providers/gemini/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@
"host": "gemini",
"protocol": "discovery",
"plugins": [
{
"name": "mlx-agent",
"repository": "cavi-ai/mlx-agent",
"package": "providers/gemini",
"install": { "command": "git clone https://github.com/cavi-ai/mlx-agent.git && gemini extensions install ./mlx-agent/providers/gemini" }
},
{
"name": "obsidian-agent",
"repository": "cavi-ai/obsidian-agent",
Expand Down
8 changes: 4 additions & 4 deletions scripts/validate-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const MLX_EXCLUDED = [".mlx-agent-generated-files.json", "skills/*/scripts/mlx-a
const MLX_EXCLUSION_REASON = "Codex skills use the packaged CLI entrypoint; MCP transport artifacts are not required or distributed by this catalog.";
const DISCOVERY_COMMANDS = {
gemini: {
"mlx-agent": "git clone https://github.com/cavi-ai/mlx-agent.git && gemini extensions install ./mlx-agent/providers/gemini",
"obsidian-agent": "gemini extensions install https://github.com/cavi-ai/obsidian-agent"
},
opencode: {
Expand Down Expand Up @@ -106,15 +105,17 @@ function validateCanonicalSchema(schema, catalog, errors) {
if (Object.hasOwn(plugin, "hosts")) {
if (!Array.isArray(plugin.hosts)) violation(`${location}.hosts`, "must be an array");
else {
if (plugin.hosts.length !== 5) violation(`${location}.hosts`, "must contain exactly 5 items");
if (plugin.hosts.length < 1) violation(`${location}.hosts`, "must contain at least 1 item");
if (new Set(plugin.hosts.map((host) => JSON.stringify(host))).size !== plugin.hosts.length) violation(`${location}.hosts`, "must contain unique items");
plugin.hosts.forEach((host, hostIndex) => { if (!HOSTS.includes(host)) violation(`${location}.hosts[${hostIndex}]`, "must be a supported host"); });
}
}
if (!Object.hasOwn(plugin, "packages")) continue;
if (!plugin.packages || typeof plugin.packages !== "object" || Array.isArray(plugin.packages)) { violation(`${location}.packages`, "must be an object"); continue; }
for (const key of unknownKeys(plugin.packages, HOSTS)) errors.push(`catalog plugin ${plugin?.name} unknown package host: ${key}`);
for (const host of HOSTS) {
const declared = Array.isArray(plugin.hosts) ? [...new Set(plugin.hosts.filter((host) => HOSTS.includes(host)))] : [];
for (const key of Object.keys(plugin.packages)) if (HOSTS.includes(key) && !declared.includes(key)) errors.push(`catalog plugin ${plugin?.name} package for undeclared host: ${key}`);
for (const host of declared) {
if (!Object.hasOwn(plugin.packages, host)) { violation(`${location}.packages.${host}`, "is required"); continue; }
const pkg = plugin.packages[host];
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) { violation(`${location}.packages.${host}`, "must be an object"); continue; }
Expand Down Expand Up @@ -262,7 +263,6 @@ export async function validateCatalog(root = process.cwd()) {
seenHosts.add(host);
}
for (const host of plugin.hosts) if (!HOSTS.includes(host)) errors.push(`${prefix} unknown host: ${host}`);
for (const host of HOSTS) if (!seenHosts.has(host)) errors.push(`${prefix} required host missing: ${host}`);
for (const host of new Set(plugin.hosts.filter((item) => HOSTS.includes(item)))) {
if (!plugin.packages?.[host]?.path) errors.push(`${prefix} package path missing for host: ${host}`);
}
Expand Down
Loading
Loading