Expand description
A canonical install / uninstall / self-heal lifecycle for the Claude Code
plugin a Rust binary ships, so a setup subcommand replaces the user typing
/plugin marketplace add + /plugin install. The same plugin tree can fan
out to 24 other coding agents (codex, gemini, opencode, cursor, goose, …),
each behind a cargo feature.
For Claude Code the lifecycle orchestrates the claude CLI (≥ 2.1.196) as its
transaction boundary; it never forges Claude Code’s on-disk registry state,
with one exception: repoint_install_paths re-spells installPath values a
host’s own remap targets (design §installPath convergence).
Non-Claude backends need no CLI at all: each translates the plugin’s components
(MCP servers, hooks, commands, agents) into that tool’s own config files via
atomic read-modify-write merges that leave the user’s entries untouched, and
only runs when the tool is detected on the machine.
The derive-driven example stays ignore: the macro reads a plugin.json tree
and the emitted guard needs AGENTGEAR_GUARD, neither of which a doctest has. A
compiled example of the derive-free value types follows below.
use agentgear::{PluginHost, Scope, Source};
#[derive(PluginHost)]
#[plugin(name = "claudix", agents = ["claude", "codex", "gemini"])]
struct ClaudixHost;
ClaudixHost::install(Scope::User, Source::Embedded)?; // all detected agents
ClaudixHost::install_into(Scope::User, Source::Embedded, &["gemini"])?; // one
ClaudixHost::self_heal()?; // SessionStart entrypointThe value types need no derive, so this block is a real, compiled doctest. It
builds a Source/Scope, then reads an Outcome and an AgentReport’s
per-agent results.
use agentgear::{AgentReport, AgentResult, AgentStatus, Outcome, Scope, Source};
let _source = Source::Path("./plugin".into());
let _scope = Scope::Project { path: ".".into() };
let outcome = Outcome::Updated { from: Some("0.1.0".into()), to: "0.2.0".into() };
let line = match outcome {
Outcome::Installed => "installed".to_string(),
Outcome::Updated { from, to } => format!("updated {from:?} -> {to}"),
other => other.to_string(),
};
assert_eq!(line, "updated Some(\"0.1.0\") -> 0.2.0");
let result = AgentResult { agent: "claude", status: AgentStatus::Converged(Outcome::Installed) };
assert!(matches!(result.status, AgentStatus::Converged(_)));
// AgentReport is #[non_exhaustive]; only a lifecycle call builds one, so this
// reader is compile-checked against the live signatures without an instance.
fn summarize(report: &AgentReport) -> bool {
for entry in &report.results {
let _ = (entry.agent, &entry.status);
}
let _merged: Outcome = report.merged();
report.is_healthy()
}
let _ = summarize as fn(&AgentReport) -> bool;The host also authors a one-line build.rs:
fn main() { agentgear::build::assert_plugin_version(); }.
Five runnable hosts live in the repo’s examples/. hello-mcp is the minimal
Claude-only host; kitchen-sink exercises every component type across seven
harnesses; multi-installer builds an agent picker through backend_for;
hooks-everywhere fans one hook event across many harnesses; from-github is a
zero-embed host tracking a GitHub source.
§Feature flags
default = ["derive", "claude", "embed"]: the PluginHost derive macro, the
Claude Code backend, and baking the plugin tree into the binary as a compressed
blob so setup works offline. Turning embed off (paired with embed = false
on the derive) ships a zero-embed binary for a host that installs from a GitHub
or path Source instead.
Every other coding agent is its own feature, named by its backend id;
all-agents enables all 25 at once. Only four pull an extra dependency:
| feature (= backend id) | extra dependency |
|---|---|
codex, kimi | toml_edit (toml config) |
omp, goose | serde_norway (yaml config) |
opencode, gemini, cursor, cline, devin, qwen-code, copilot-cli, vscode-copilot, jetbrains-copilot, kiro, zed, openclaw, kilo, antigravity, antigravity-cli, pi, amp, crush, droid, augment | none |
Backends are selected per host binary with the derive’s agents = [...] list;
backend_for resolves an enabled id to its AgentBackend, and AGENT_IDS
is the roster of ids compiled into the current build, so a host can drive its own
picker UI without hardcoding the list. The full set of 25 ids: plugin-native
claude and copilot-cli (full lifecycle through the tool’s own CLI), plus the
23 config-merge backends codex, opencode, gemini, cursor, cline,
devin, qwen-code, vscode-copilot, jetbrains-copilot, kimi, kiro,
zed, omp, openclaw, kilo, antigravity, antigravity-cli, pi, goose,
amp, crush, droid, augment. Design rationale and the full per-backend
reference live in the project wiki.
Re-exports§
pub use statusline::StatusLineDecl;
Modules§
- build
- Host-facing build helper. The host authors a one-line
build.rs: - prelude
- A ready-to-glob prelude for host binaries.
- statusline
- The runtime helpers a host’s own status-line print subcommand composes with.
Structs§
- Agent
Report - Per-agent results of one lifecycle fan-out, one entry per configured agent in
plugin.agentsorder (agents excluded by an explicitinstall_intofilter get no entry — they were never asked for). The merged-Outcomelifecycle methods collapse this to first-change-wins; a host that wants to tell its user which agents were installed, skipped, or failed reads the*_reportvariants and prints this (itsDisplayis a readysetupsummary, one line per agent). - Agent
Result - One agent’s slice of a lifecycle fan-out.
- Capabilities
- What an agent backend can host. Each surface flag is
trueiff the backend’sreconcileactually writes/manages that surface for a plugin declaring it (a conditionally-gated surface — e.g. one a native registry may already cover — still counts, since the backend can translate it). Letssetupreport “codex: mcp only” instead of silently dropping features, and is the runtime truth behind the README’s supported-agents matrix. - Desired
- What a reconcile should converge to. Held separately from
Scopebecause a backend converges the same desired state across scopes. - Doctor
Check - One inspected thing and how it fared.
- Doctor
Report - An ordered health report: one
DoctorCheckper thing inspected, rendered by itsDisplayinto a[ ok ]/[warn]/[fail]list. - Hook
Binding - One hook binding: a command wired to an event, optionally matcher-gated.
- Markdown
Doc - A markdown component (a command or agent) with its frontmatter split out.
- McpServer
- One MCP server the plugin declares.
- Plugin
- A resolved plugin descriptor. Built by
PluginHost::descriptorfrom the derive-emitted metadata; passed to backends. - Plugin
Components - A plugin tree parsed into a harness-agnostic shape. Lossless: a backend that lacks a surface skips that field, the parser never drops one.
- Repoint
Report claude - What one
repoint_install_pathspass did. Both vectors follow the file’s value order; a spelling recorded by several entries is one row. - Repoint
Skip claude - A targeted path this pass named and left alone.
- Repointed
claude - One recorded path this pass re-rooted.
- Skill
Dir - A skill directory and its files, ready to copy into a harness’s skills dir.
Enums§
- Agent
Status - What one agent’s slice of the fan-out did.
- Backend
State - What
probeclassifies a plugin’s per-agent state as. Drives self_heal’s marker × state table (never resurrect, never re-enable, repair drift). - Check
Status - A single check’s verdict. Only
CheckStatus::Failmakes a report unhealthy. - Error
- Everything a lifecycle call can fail with. Environment problems the user can fix
(missing or old
claude/copilot) are distinct variants from genuine bugs (io/json), so a caller renders a fix-hint rather than a stack trace. - McpKind
- An MCP server’s transport.
- Outcome
- What a lifecycle op actually did.
non_exhaustive: adding a case is additive. - Remap
claude - What a remap decided for one candidate path value.
- Scope
- Where a plugin is installed.
Localis intentionally absent (design §API): binary-driven install of user-wide tooling has no coherent local-scope story.non_exhaustiveso a future variant lands without a semver major. - Skip
Reason - Why an agent was skipped rather than converged.
- Source
- Runtime origin of the plugin tree, defaulted from the derive attrs but
overridable per call. One binary can ship an embedded tree yet still let users
track a GitHub ref so
claude plugin updatepulls new plugin versions without waiting on a binary release.
Constants§
- AGENTGEAR_
CLIENT_ TOKEN - The portability token every backend expands to ITS OWN canonical client id
(
claude,codex,copilot-cli, …). Unlike${CLAUDE_PLUGIN_ROOT}— which only Claude Code expands, sois_portableflags it as a skip — this token is known to agentgear and expanded per harness in an executable command, so an author writes the client id once and each harness sees its own. Plugin-native backends (CC/copilot) copy the whole tree and substitute it everywhere (see materialize); the config-translating backends expand only the command surfaces they render — hook commands and mcp command/args (with_client). - AGENT_
IDS - Every agent id compiled into this build, in the order
backend_forresolves them: one entry per enabled backend feature. A host enumerates these to know which backends it can target without hardcoding the roster — every id here resolves toSomethroughbackend_for, pinned by a test. Feature-gated, so a default build holds only["claude"]andall-agentsholds all 25.
Traits§
- Agent
Backend - One coding agent’s translation of the plugin.
reconcileis the single shape install/update/self_heal reduce to; each backend decides what “converged” means for its harness. The trait is unsealed, so an out-of-crate crate can add a backend and drive it viaPlugin::componentsplus a directreconcile/remove(the derive’sagentslist only names built-in ids). - Plugin
Host - Implemented by the
#[derive(PluginHost)]macro. The consts carry the compile-time metadata; the provided methods are the lifecycle the host calls.
Functions§
- backend_
for - Resolve a backend by id. Each non-CC arm is feature-gated so a default build
ships only Claude;
all-agents(fixture + docker legs) lights every arm. Public so a host can enumerate itsAGENTS(detect/capabilities) to build its own setup UI. - current_
pointer - The
current@<client>pointer path [crate::materialize] publishes forplugin_name, resolved like [data_root] but without materializing anything. A host reads its registered marketplace source and compares it against this to spot a divergent registration with a plain filesystem read, never a CLI spawn. - repoint_
install_ paths claude - Scan the registry for quoted values, ask
remapabout each, rewrite the targeted ones, and write the file only when something changed. The scan sees keys and values alike — a remap answersRemap::Keepfor anything that is not a path it owns. The file is never parsed and never reformatted: output bytes equal the input bytes except that each rewritten value’s interior is the new spelling.
Type Aliases§
Derive Macros§
- Plugin
Host derive - Derive
PluginHostfor a host unit struct, wiring the plugin’s install/update/self-heal lifecycle from one#[plugin(..)]attribute.