From 37d6783ea46f4e3d2916ae23b8cc95195d1a6eea Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 24 Aug 2026 20:47:13 -0400 Subject: [PATCH] feat: add external-source plugins for third-party caveman/ponytail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_014YVWWAnvR5TDQzmfY3RXG9 --- .claude-plugin/marketplace.json | 14 ++++++++ README.md | 59 +++++++++++++++++++++++++++++++ plugins/caveman/plugin.json | 13 +++++++ plugins/ponytail/plugin.json | 13 +++++++ scripts/generate_plugins.py | 61 +++++++++++++++++++++++++++++++-- 5 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 plugins/caveman/plugin.json create mode 100644 plugins/ponytail/plugin.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3f38507..0ea962d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,6 +6,13 @@ }, "description": "Reusable AI skills for Claude Code and Antigravity CLI", "plugins": [ + { + "name": "caveman", + "source": { + "source": "github", + "repo": "JuliusBrussee/caveman" + } + }, { "name": "core-essentials", "source": "./dist/claude-code/core-essentials" @@ -30,6 +37,13 @@ "name": "github-mcp", "source": "./dist/claude-code/github-mcp" }, + { + "name": "ponytail", + "source": { + "source": "github", + "repo": "DietrichGebert/ponytail" + } + }, { "name": "readme-sync-audit", "source": "./dist/claude-code/readme-sync-audit" diff --git a/README.md b/README.md index c8ae7ad..dce99eb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ from one source tree and published as a native marketplace for both tools. - [core-essentials](#core-essentials) - [github-mcp](#github-mcp) - [gitea-mcp](#gitea-mcp) + - [caveman](#caveman) + - [ponytail](#ponytail) - [Installation](#installation) - [Claude Code](#claude-code) - [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 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`) +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 external directory of plugins, but empirically it doesn't reliably load one as a bundle — hooks and MCP servers inside a `plugins//` folder @@ -242,6 +277,30 @@ verbatim where the two tools' schemas diverge: target; `commands/`/`agents/` have no agy equivalent and are skipped for 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 Some content only makes sense for one tool — `delegate-agy` (Claude Code diff --git a/plugins/caveman/plugin.json b/plugins/caveman/plugin.json new file mode 100644 index 0000000..87d0ff6 --- /dev/null +++ b/plugins/caveman/plugin.json @@ -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" + } + } +} diff --git a/plugins/ponytail/plugin.json b/plugins/ponytail/plugin.json new file mode 100644 index 0000000..c26f548 --- /dev/null +++ b/plugins/ponytail/plugin.json @@ -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" + } + } +} diff --git a/scripts/generate_plugins.py b/scripts/generate_plugins.py index 2cc98fa..2cccb71 100755 --- a/scripts/generate_plugins.py +++ b/scripts/generate_plugins.py @@ -17,6 +17,22 @@ A "plugin" is a directory under `plugins//` that may bundle any mix of: - commands/*.md (Claude Code-only slash commands) - 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//` 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 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 @@ -143,6 +159,20 @@ def validate_targets(value: "list | None", valid_targets: list[str], context: st 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]: plugins = [] 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): raise ValidationError(f"{manifest_path}: missing required field '{field}'") 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_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 = 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( { "dir": plugin_dir, "meta": meta, "targets": plugin_targets, + "external": plugin_external, "skills": skills, "hooks": hooks, "mcp": mcp, - "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, + "rules_dir": rules_dir, + "commands_dir": commands_dir, + "agents_dir": agents_dir, } ) @@ -352,6 +402,11 @@ def gen_claude_code(manifest: dict, plugins: list[dict], out_dir: Path) -> None: marketplace_entries = [] for p in plugins: 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): marketplace_entries.append({"name": name, "source": f"./dist/claude-code/{name}"})