feat: add external-source plugins for third-party caveman/ponytail
Lets a plugin.json declare an `external` target->source map instead of carrying local content, so the generator emits that Claude Code marketplace source (e.g. a GitHub repo) verbatim rather than a local dist path. Adds caveman and ponytail as external entries, installed straight from their upstream repos with nothing vendored or copied. Claude Code-only for now — agy has no equivalent external-source mechanism, and both plugins' hooks/MCP assume Claude Code's runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YVWWAnvR5TDQzmfY3RXG9
This commit is contained in:
@@ -6,6 +6,13 @@
|
|||||||
},
|
},
|
||||||
"description": "Reusable AI skills for Claude Code and Antigravity CLI",
|
"description": "Reusable AI skills for Claude Code and Antigravity CLI",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "caveman",
|
||||||
|
"source": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "JuliusBrussee/caveman"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "core-essentials",
|
"name": "core-essentials",
|
||||||
"source": "./dist/claude-code/core-essentials"
|
"source": "./dist/claude-code/core-essentials"
|
||||||
@@ -30,6 +37,13 @@
|
|||||||
"name": "github-mcp",
|
"name": "github-mcp",
|
||||||
"source": "./dist/claude-code/github-mcp"
|
"source": "./dist/claude-code/github-mcp"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ponytail",
|
||||||
|
"source": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "DietrichGebert/ponytail"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "readme-sync-audit",
|
"name": "readme-sync-audit",
|
||||||
"source": "./dist/claude-code/readme-sync-audit"
|
"source": "./dist/claude-code/readme-sync-audit"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ from one source tree and published as a native marketplace for both tools.
|
|||||||
- [core-essentials](#core-essentials)
|
- [core-essentials](#core-essentials)
|
||||||
- [github-mcp](#github-mcp)
|
- [github-mcp](#github-mcp)
|
||||||
- [gitea-mcp](#gitea-mcp)
|
- [gitea-mcp](#gitea-mcp)
|
||||||
|
- [caveman](#caveman)
|
||||||
|
- [ponytail](#ponytail)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Claude Code](#claude-code)
|
- [Claude Code](#claude-code)
|
||||||
- [Antigravity CLI (agy)](#antigravity-cli-agy)
|
- [Antigravity CLI (agy)](#antigravity-cli-agy)
|
||||||
@@ -158,6 +160,33 @@ both, or neither independently.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### `caveman`
|
||||||
|
|
||||||
|
**Purpose:** Ultra-compressed communication mode — cuts output tokens while
|
||||||
|
keeping full technical accuracy.
|
||||||
|
|
||||||
|
This is a third-party plugin from
|
||||||
|
[JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman), not
|
||||||
|
maintained in this repo. Our marketplace entry points straight at that
|
||||||
|
repo's own `.claude-plugin/plugin.json` (via `source: {"source": "github",
|
||||||
|
"repo": "JuliusBrussee/caveman"}`), so installing it through us always
|
||||||
|
fetches the current upstream version — nothing is vendored or copied.
|
||||||
|
Claude Code-only; see [Antigravity CLI (agy)](#antigravity-cli-agy) below
|
||||||
|
for why.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ponytail`
|
||||||
|
|
||||||
|
**Purpose:** Lazy senior dev mode — forces the simplest, shortest solution
|
||||||
|
that actually works (YAGNI, stdlib first, no unrequested abstractions).
|
||||||
|
|
||||||
|
Also third-party, from
|
||||||
|
[DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail),
|
||||||
|
installed the same external-source way as `caveman` above. Claude Code-only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
This repository is a native plugin marketplace for both tools — there is no
|
This repository is a native plugin marketplace for both tools — there is no
|
||||||
@@ -181,6 +210,12 @@ Run `/plugin marketplace update` to pick up newly published plugins.
|
|||||||
|
|
||||||
### Antigravity CLI (`agy`)
|
### Antigravity CLI (`agy`)
|
||||||
|
|
||||||
|
Third-party plugins pulled in via `external` (see below) aren't offered
|
||||||
|
here at all — agy has no marketplace-style external-source mechanism, and
|
||||||
|
their hooks/MCP servers are built assuming Claude Code's plugin runtime
|
||||||
|
(e.g. resolving `${CLAUDE_PLUGIN_ROOT}`), so there'd be nothing correct to
|
||||||
|
translate even if we vendored them.
|
||||||
|
|
||||||
agy documents a `plugins.json` "entries" mechanism for pointing it at an
|
agy documents a `plugins.json` "entries" mechanism for pointing it at an
|
||||||
external directory of plugins, but empirically it doesn't reliably load
|
external directory of plugins, but empirically it doesn't reliably load
|
||||||
one as a bundle — hooks and MCP servers inside a `plugins/<name>/` folder
|
one as a bundle — hooks and MCP servers inside a `plugins/<name>/` folder
|
||||||
@@ -242,6 +277,30 @@ verbatim where the two tools' schemas diverge:
|
|||||||
target; `commands/`/`agents/` have no agy equivalent and are skipped for
|
target; `commands/`/`agents/` have no agy equivalent and are skipped for
|
||||||
that target.
|
that target.
|
||||||
|
|
||||||
|
A plugin can instead declare `external` in its `plugin.json` — a
|
||||||
|
target-keyed map of Claude Code marketplace `source` objects — to point at
|
||||||
|
a third-party plugin we don't own or vendor, instead of carrying any of
|
||||||
|
the local content above:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "caveman",
|
||||||
|
"description": "...",
|
||||||
|
"targets": ["claude-code"],
|
||||||
|
"external": {
|
||||||
|
"claude-code": { "source": "github", "repo": "owner/repo" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The generator emits that `source` object verbatim into
|
||||||
|
`.claude-plugin/marketplace.json` instead of a local `./dist/...` path, so
|
||||||
|
`/plugin install` fetches the plugin's real content straight from upstream
|
||||||
|
at install time — it's never copied into this repo and never goes stale.
|
||||||
|
A plugin with `external` must not also carry local skills/hooks/mcp/rules/
|
||||||
|
commands/agents, and today only the `claude-code` target supports it (see
|
||||||
|
[caveman](#caveman) and [ponytail](#ponytail) above).
|
||||||
|
|
||||||
### Restricting a Plugin (or a Skill) to Specific Targets
|
### Restricting a Plugin (or a Skill) to Specific Targets
|
||||||
|
|
||||||
Some content only makes sense for one tool — `delegate-agy` (Claude Code
|
Some content only makes sense for one tool — `delegate-agy` (Claude Code
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "caveman",
|
||||||
|
"description": "Ultra-compressed communication mode from JuliusBrussee/caveman — cuts output tokens while keeping full technical accuracy. Installed straight from the upstream repo, not vendored here.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Rootiest",
|
||||||
|
"targets": ["claude-code"],
|
||||||
|
"external": {
|
||||||
|
"claude-code": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "JuliusBrussee/caveman"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "ponytail",
|
||||||
|
"description": "Lazy senior dev mode from DietrichGebert/ponytail — forces the simplest, shortest solution that actually works (YAGNI, stdlib first, no unrequested abstractions). Installed straight from the upstream repo, not vendored here.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Rootiest",
|
||||||
|
"targets": ["claude-code"],
|
||||||
|
"external": {
|
||||||
|
"claude-code": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "DietrichGebert/ponytail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,22 @@ A "plugin" is a directory under `plugins/<name>/` that may bundle any mix of:
|
|||||||
- commands/*.md (Claude Code-only slash commands)
|
- commands/*.md (Claude Code-only slash commands)
|
||||||
- agents/*.md (Claude Code-only subagents)
|
- agents/*.md (Claude Code-only subagents)
|
||||||
|
|
||||||
|
Alternatively, a plugin.json may declare `external` instead of carrying any local
|
||||||
|
content at all — a target-keyed map of Claude Code marketplace `source` objects
|
||||||
|
pointing at a third-party plugin we don't own or vendor, e.g.:
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "caveman",
|
||||||
|
"external": { "claude-code": { "source": "github", "repo": "owner/repo" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
For each listed target, the marketplace entry's `source` is emitted verbatim instead
|
||||||
|
of a `./dist/<target>/<name>` path — the plugin's real content is fetched straight
|
||||||
|
from upstream at install time, so it's never copied into this repo and never goes
|
||||||
|
stale. A plugin with `external` must not also carry local skills/hooks/mcp/rules/
|
||||||
|
commands/agents (pick one or the other), and today only the claude-code target
|
||||||
|
supports it — agy has no marketplace-style external-source mechanism.
|
||||||
|
|
||||||
A plugin that ends up with no content at all for a given target (every
|
A plugin that ends up with no content at all for a given target (every
|
||||||
skill excluded, and no hooks/mcp/rules/commands/agents of its own) is
|
skill excluded, and no hooks/mcp/rules/commands/agents of its own) is
|
||||||
skipped entirely for that target — it never gets an empty output directory
|
skipped entirely for that target — it never gets an empty output directory
|
||||||
@@ -143,6 +159,20 @@ def validate_targets(value: "list | None", valid_targets: list[str], context: st
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_external(value: "dict | None", valid_targets: list[str], context: str) -> "dict | None":
|
||||||
|
"""Validate an optional 'external' map of target -> marketplace source object."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(value, dict) or not value:
|
||||||
|
raise ValidationError(f"{context}: 'external' must be a non-empty object keyed by target")
|
||||||
|
for target, source in value.items():
|
||||||
|
if target not in valid_targets:
|
||||||
|
raise ValidationError(f"{context}: unknown target '{target}' in 'external' (valid: {', '.join(valid_targets)})")
|
||||||
|
if not isinstance(source, dict) or not source.get("source"):
|
||||||
|
raise ValidationError(f"{context}: external.{target} must be an object with a 'source' field")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def discover_plugins(merged_root: Path, valid_targets: list[str]) -> list[dict]:
|
def discover_plugins(merged_root: Path, valid_targets: list[str]) -> list[dict]:
|
||||||
plugins = []
|
plugins = []
|
||||||
for plugin_dir in sorted(merged_root.iterdir()):
|
for plugin_dir in sorted(merged_root.iterdir()):
|
||||||
@@ -160,6 +190,13 @@ def discover_plugins(merged_root: Path, valid_targets: list[str]) -> list[dict]:
|
|||||||
if not meta.get(field):
|
if not meta.get(field):
|
||||||
raise ValidationError(f"{manifest_path}: missing required field '{field}'")
|
raise ValidationError(f"{manifest_path}: missing required field '{field}'")
|
||||||
plugin_targets = validate_targets(meta.get("targets"), valid_targets, str(manifest_path))
|
plugin_targets = validate_targets(meta.get("targets"), valid_targets, str(manifest_path))
|
||||||
|
plugin_external = validate_external(meta.get("external"), valid_targets, str(manifest_path))
|
||||||
|
if plugin_external is not None:
|
||||||
|
for target in plugin_external:
|
||||||
|
if plugin_targets is not None and target not in plugin_targets:
|
||||||
|
raise ValidationError(
|
||||||
|
f"{manifest_path}: external.{target} is set but 'targets' does not include '{target}'"
|
||||||
|
)
|
||||||
|
|
||||||
skills = []
|
skills = []
|
||||||
skills_dir = plugin_dir / "skills"
|
skills_dir = plugin_dir / "skills"
|
||||||
@@ -184,17 +221,30 @@ def discover_plugins(merged_root: Path, valid_targets: list[str]) -> list[dict]:
|
|||||||
mcp_path = plugin_dir / "mcp.json"
|
mcp_path = plugin_dir / "mcp.json"
|
||||||
mcp = load_json(mcp_path) if mcp_path.exists() else None
|
mcp = load_json(mcp_path) if mcp_path.exists() else None
|
||||||
|
|
||||||
|
rules_dir = plugin_dir / "rules" if (plugin_dir / "rules").is_dir() else None
|
||||||
|
commands_dir = plugin_dir / "commands" if (plugin_dir / "commands").is_dir() else None
|
||||||
|
agents_dir = plugin_dir / "agents" if (plugin_dir / "agents").is_dir() else None
|
||||||
|
|
||||||
|
if plugin_external is not None and any(
|
||||||
|
(skills, hooks is not None, mcp is not None, rules_dir, commands_dir, agents_dir)
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
f"{manifest_path}: a plugin with 'external' must not also carry local "
|
||||||
|
"skills/hooks/mcp/rules/commands/agents content"
|
||||||
|
)
|
||||||
|
|
||||||
plugins.append(
|
plugins.append(
|
||||||
{
|
{
|
||||||
"dir": plugin_dir,
|
"dir": plugin_dir,
|
||||||
"meta": meta,
|
"meta": meta,
|
||||||
"targets": plugin_targets,
|
"targets": plugin_targets,
|
||||||
|
"external": plugin_external,
|
||||||
"skills": skills,
|
"skills": skills,
|
||||||
"hooks": hooks,
|
"hooks": hooks,
|
||||||
"mcp": mcp,
|
"mcp": mcp,
|
||||||
"rules_dir": plugin_dir / "rules" if (plugin_dir / "rules").is_dir() else None,
|
"rules_dir": rules_dir,
|
||||||
"commands_dir": plugin_dir / "commands" if (plugin_dir / "commands").is_dir() else None,
|
"commands_dir": commands_dir,
|
||||||
"agents_dir": plugin_dir / "agents" if (plugin_dir / "agents").is_dir() else None,
|
"agents_dir": agents_dir,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -352,6 +402,11 @@ def gen_claude_code(manifest: dict, plugins: list[dict], out_dir: Path) -> None:
|
|||||||
marketplace_entries = []
|
marketplace_entries = []
|
||||||
for p in plugins:
|
for p in plugins:
|
||||||
name = p["meta"]["name"]
|
name = p["meta"]["name"]
|
||||||
|
external_source = (p["external"] or {}).get("claude-code") if p["external"] else None
|
||||||
|
if external_source is not None:
|
||||||
|
if plugin_supports(p, "claude-code"):
|
||||||
|
marketplace_entries.append({"name": name, "source": external_source})
|
||||||
|
continue
|
||||||
if write_claude_plugin(p, plugins_out / name):
|
if write_claude_plugin(p, plugins_out / name):
|
||||||
marketplace_entries.append({"name": name, "source": f"./dist/claude-code/{name}"})
|
marketplace_entries.append({"name": name, "source": f"./dist/claude-code/{name}"})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user