API Reference
Version management for agent marketplaces.
auto_sync_manifests
Automatically sync plugin.json and marketplace.json based on git changes.
This script has three modes:
-
Pre-commit mode (default): Detects CRUD operations on plugins and their components during pre-commit, then updates manifests and bumps versions.
-
Reconcile mode (
--reconcile): Full directory scan that compares filesystem state against plugin.json and marketplace.json entries. Adds missing components, removes stale references, and reports drift. -
Sync-marketplace mode (
--sync-marketplace): Post-merge CI mode. Reconciles the marketplace.json plugin list against disk, then bumps the marketplace version. Called by CI after push to main so that marketplace.json version bumps do not appear in PR branches.
CRUD Detection (pre-commit mode): - Plugin created: New plugins/ directory with .claude-plugin/plugin.json - Plugin deleted: Removed plugins/ directory - Component created: New skill/agent/command/hook/mcp file - Component deleted: Removed skill/agent/command/hook/mcp file
Version Bumping: - New plugin: Bump marketplace minor version - Deleted plugin: Bump marketplace major version - New component in plugin: Bump plugin minor version - Deleted component: Bump plugin major version - Modified component: Bump plugin patch version
PluginPathInfo
Bases: TypedDict
Parsed plugin path information.
ComponentChange
Bases: TypedDict
Component change information.
ComponentChanges
Bases: TypedDict
Changes for a plugin's components.
MarketplaceChanges
Bases: TypedDict
Changes for marketplace plugins.
run_git_command(args)
Run git command and return output.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
args
|
list[str]
|
List of git command arguments (e.g., ['status', '--short']) |
required |
Returns:
| Type | Description |
|---|---|
str
|
Git command stdout output stripped of whitespace |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If git binary is not found in PATH. |
get_git_status()
Get staged file changes categorized by operation.
Returns:
| Type | Description |
|---|---|
_GitStatus
|
Paths grouped by operation and paired native manifest relocations. |
sync_staged_manifests(root=Path())
Apply staged content changes to every affected native plugin manifest.
Returns:
| Type | Description |
|---|---|
dict[Path, str]
|
The source roots whose version-owning manifests were updated. |
sync_native_marketplaces(root=Path(), *, bump=True, dry_run=False, manifests=None, base_ref=None, head_ref='HEAD', preserve_unstaged=False)
Reconcile every Git-visible native marketplace with local plugin manifests.
Returns:
| Type | Description |
|---|---|
list[Path]
|
Marketplace paths updated in this synchronization run. |
parse_plugin_path(filepath)
Parse file path to extract plugin name and component type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filepath
|
str
|
Relative file path from repository root |
required |
Returns:
| Type | Description |
|---|---|
PluginPathInfo | None
|
{ 'plugin': 'plugin-name', 'component_type': 'skill' | 'agent' | 'command' | 'hook' | 'mcp' | None, 'component_path': 'skills/skill-name', |
PluginPathInfo | None
|
} |
bump_version(current, bump_type)
Bump semantic version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current
|
str
|
Current version string (e.g., "1.2.3") |
required |
bump_type
|
Literal['major', 'minor', 'patch']
|
Type of version bump |
required |
Returns:
| Type | Description |
|---|---|
str
|
New version string after bumping |
extract_version_from_json(data, key_path)
Traverse a JSON object by key path and parse the version string found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
object
|
Parsed JSON data (typically a nested dict). |
required |
key_path
|
list[str]
|
Keys to traverse to reach the version value. |
required |
Returns:
| Type | Description |
|---|---|
tuple[int, int, int] | None
|
Version tuple |
tuple[int, int, int] | None
|
invalid, the value is not a string, or the version is malformed. |
resolve_base()
Resolve the best available base ref for version comparison.
Tries refs in order: origin/main → main. Returns None when no ref is resolvable (fresh or shallow clone).
In the CI manifest-sync job (fetch-depth: 1), origin/main is
absent. Returning None causes callers to fall back to HEAD-based
behaviour — identical to the pre-refactor logic — so shallow clones are
fully supported without any CI-side changes.
Returns:
| Type | Description |
|---|---|
str | None
|
The first resolvable ref string, or None if none resolve. |
read_ref_json(ref, filepath)
Read and parse JSON content of a file at an arbitrary git ref.
When ref is "HEAD", delegates to _read_head_json so that
existing monkeypatches of _read_head_json in tests remain effective.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref
|
str
|
Git ref (branch, tag, or |
required |
filepath
|
str | Path
|
Path to the JSON file relative to the repo root. |
required |
Returns:
| Type | Description |
|---|---|
object | None
|
Parsed JSON data, or None if the file does not exist at the ref |
object | None
|
or cannot be parsed. |
update_plugin_json(plugin_name, changes)
Update plugin.json based on component changes.
Resolves a base ref (origin/main → main) and bumps from the base version when available, so that concurrent PR branches never collide on the same version number. Falls back to HEAD-based comparison when no base ref is resolvable (shallow CI clones, fresh checkouts).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plugin_name
|
str
|
Name of the plugin directory under |
required |
changes
|
ComponentChanges
|
Component changes with |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
str
|
version is the new version on update or the unchanged version otherwise. |
update_marketplace_json(plugin_changes)
Update marketplace.json based on plugin changes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plugin_changes
|
MarketplaceChanges
|
Added, deleted, and modified plugin names grouped by change type. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
updated |
bool
|
bool |
reconcile(*, dry_run)
Run full filesystem reconciliation.
Scans all plugin directories, compares against manifest files, and fixes drift.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dry_run
|
bool
|
If True, only report drift without modifying files |
required |
Returns:
| Type | Description |
|---|---|
int
|
Exit code (0 for success) |
reconcile_native_manifests(*, dry_run, staged=False)
Reconcile native component arrays and catalog membership.
With staged, manifests, catalogs, and components are read from the Git index, so untracked files and unstaged edits do not count.
Returns:
| Type | Description |
|---|---|
int
|
One for detected drift in dry-run mode, otherwise zero. |
main()
Main entry point — dispatches to pre-commit, reconcile, or sync-marketplace mode.
Returns:
| Type | Description |
|---|---|
int
|
Exit code (0 for success) |
check_plugin_version_bump
CI gate, retroactive audit, and repair for plugin.json version-bump drift.
Companion to auto_sync_manifests.py (the pre-commit hook). That hook only
inspects staged changes (git diff --cached), which is always empty in
a CI checkout -- nothing is ever staged there. A PR merged via GitHub's UI or
API never runs the local pre-commit hook either. Both gaps combined let PR
3005 land a plugin content change with zero corresponding plugin.json
version bump, and the marketplace cache -- keyed on that version -- kept serving stale content indefinitely. See issue #3021.
Three modes:
--check (CI gate, required check)
Diffs base-ref against head-ref (a real base-vs-head tree comparison,
not a staged-index comparison) and fails when any plugin with a changed
file did not also raise its plugin.json version relative to
base-ref. Newly added or fully deleted plugins are exempt -- there is
no prior version to compare against. head-ref defaults to HEAD but
should be passed explicitly (e.g. origin/$GITHUB_HEAD_REF) on a
GitHub Actions pull_request trigger, where the checked-out HEAD
is a synthetic merge commit rather than the PR's actual head commit.
--audit (retroactive, report-only)
Scans every plugin currently on disk, finds the most recent commit that
changed its plugin.json version, and reports drift when any file
under that plugin changed after that commit without a further bump.
This is how a gap like #3021's -- already merged before this gate
existed -- gets found without a manual git show investigation.
--repair (post-merge, main-only, authoritative)
Same drift detection as --audit, but patch-bumps each drifted
plugin's plugin.json version in place instead of only reporting it.
Run on main after every push -- this is the only correctness
mechanism for the version-collision race (#3027): two branches deriving
the same next version from an identical origin/main snapshot land on
the same number, and this is what un-collides them after merge. See
.claude/rules/plugin-development.md.
check_native_version_bumps(base_ref, head_ref='HEAD')
Return changed native manifests whose declared version did not increase.
Returns:
| Type | Description |
|---|---|
list[Path]
|
Repository-relative paths of existing manifests missing a version bump. |
plugins_with_diff(base_ref, head_ref='HEAD')
Return plugin directory names with any file changed between two refs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_ref
|
str
|
The git ref to diff against (e.g. a PR's merge-base with main). |
required |
head_ref
|
str
|
The git ref representing the current state. |
'HEAD'
|
Returns:
| Type | Description |
|---|---|
set[str]
|
Set of plugin directory names (the |
set[str]
|
touched anywhere in the diff between the two refs. |
check_version_bumps(base_ref, head_ref='HEAD')
Find plugins whose files changed between refs without a version bump.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_ref
|
str
|
The git ref to diff against (e.g. the PR's merge-base with main). |
required |
head_ref
|
str
|
The git ref representing the proposed merge state. |
'HEAD'
|
Returns:
| Type | Description |
|---|---|
list[str]
|
Sorted list of plugin directory names that changed but whose |
list[str]
|
|
list[str]
|
base_ref. Plugins created or deleted within the diff are excluded -- |
list[str]
|
there is no prior version to compare against. |
find_last_version_bump_commit(plugin_json_relpath)
Find the most recent commit that changed plugin.json's version field.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plugin_json_relpath
|
str
|
Path to plugin.json, relative to the repo root. |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
The commit SHA of the most recent version change. When the version was |
str | None
|
set once at file creation and never changed since, the creation |
str | None
|
(root) commit is returned -- it is still a valid drift baseline. |
str | None
|
Returns None only when plugin.json has no commit history at all. |
audit_version_drift(plugins_root)
Find plugins whose content changed after their last recorded version bump.
Assumes the current working directory is the repository root (true for
every caller in this script -- git subcommands here rely on it). Plugin
paths are always rebuilt as plugins/<name>/... strings rather than
reused from plugins_root directly: git show ref:path -- unlike
git log -- path or git diff -- path -- requires a repo-root-relative
path and silently returns nothing for a filesystem-absolute one, so an
absolute plugins_root (e.g. a pytest tmp_path fixture) would
otherwise make every plugin look falsely un-bumped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plugins_root
|
Path
|
Path to the plugins/ directory (relative or absolute -- only used to list plugin directory names, never passed to git). |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
Sorted list of plugin directory names exhibiting drift -- i.e. a file |
list[str]
|
under |
list[str]
|
commit, with no further bump since. |
repair_plugin_version(plugin_dir)
Bump a drifted plugin's version by one patch and write it to disk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plugin_dir
|
Path
|
Path to the plugin's directory ( |
required |
Returns:
| Type | Description |
|---|---|
tuple[str, str] | None
|
|
tuple[str, str] | None
|
|
tuple[str, str] | None
|
well-formed |
main()
Dispatch to the requested mode.
Returns:
| Type | Description |
|---|---|
int
|
Process exit code. |
cli
Typer application exposed by the command-line entry point.
main()
Manage versions across agent marketplaces.
sync(marketplace=typer.Option(False, '--marketplace', help='Reconcile and version native marketplace manifests.'), base_ref=typer.Option(None, '--base-ref', help='Base revision for a marketplace version bump.'), head_ref=typer.Option('HEAD', '--head-ref', help='Candidate revision for a marketplace version bump.'))
Synchronize staged native manifests or post-merge marketplaces.
check(base_ref=typer.Option(None, '--base-ref', help='Git base ref; defaults to origin/main or main.'), head_ref=typer.Option(None, '--head-ref', help='Git head ref; defaults to HEAD.'))
Fail when changed native manifests did not increase their version.
audit()
Report native manifests with content drift after their last version bump.
repair()
Patch-bump native manifests with content drift after their last version bump.
reconcile(dry_run=typer.Option(False, '--dry-run', help='Report manifest drift without changing files.'), staged=typer.Option(False, '--staged', help='Read manifests and components from the Git index; ignore untracked files and unstaged edits.'))
Reconcile native component arrays and marketplace membership.
native_manifests
Discovery and source resolution for conventional native manifests.
NativeManifestError
Bases: ValueError
Raised when a native manifest cannot be interpreted safely.
NativeManifest
dataclass
A discovered conventional manifest with its declared version location.
manifest_kind(path)
Classify a conventional native manifest path.
Returns:
| Type | Description |
|---|---|
Literal['plugin', 'marketplace'] | None
|
The manifest kind, or None when the path is not conventional. |
is_git_visible(root, path)
Return whether a path is not ignored under a repository root.
marketplace_root(manifest)
Return the directory from which a marketplace's local sources resolve.
Returns:
| Type | Description |
|---|---|
Path
|
The repository-relative catalog root. |
discover_all_manifests(root=Path(), read_json=None)
Find every Git-visible conventional manifest below root, nested ones included.
read_json reads the tree that decides which package.json files are Pi packages;
it defaults to the working tree.
Returns:
| Type | Description |
|---|---|
list[NativeManifest]
|
Native manifests sorted by repository-relative path. |
is_pi_package(path)
Return whether a manifest path is a Pi package package.json.
with_pi_packages(manifests, paths, read_json)
Add each Pi package package.json that shares a source root with a plugin manifest.
A package.json is a Pi package when it declares a pi key or the pi-package
keyword. It then versions with the plugin manifests of its directory. Any other
package.json, and a Pi package with no plugin manifest beside it, stays unmanaged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifests
|
list[NativeManifest]
|
Native manifests from one tree: worktree, index, or Git ref. |
required |
paths
|
list[Path]
|
Every candidate path in that same tree. |
required |
read_json
|
Callable[[Path], object]
|
Reads a file's parsed JSON from that same tree. |
required |
Returns:
| Type | Description |
|---|---|
list[NativeManifest]
|
The manifests plus the managed Pi packages, sorted by repository-relative path. |
discover_manifests(root=Path())
Find every Git-visible conventional manifest below root that is not plugin content.
Returns:
| Type | Description |
|---|---|
list[NativeManifest]
|
Native manifests sorted by repository-relative path. |
marketplace_entry_source(entry)
Return the relative local source of one marketplace plugin entry.
Returns:
| Type | Description |
|---|---|
str | None
|
The |
without_nested_manifests(manifests, read_json)
Drop manifests that are content of an enclosing plugin, such as eval fixtures.
A plugin manifest nests when its source root lies strictly inside another plugin's source root; a catalog nests when its source-resolution root does. A nested plugin stays a plugin when a non-nested catalog declares its source root as a local source. A plugin at the repository root encloses nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifests
|
list[NativeManifest]
|
Candidate manifests from one tree: worktree, index, or Git ref. |
required |
read_json
|
Callable[[Path], object]
|
Reads a manifest's parsed JSON from that same tree. |
required |
Returns:
| Type | Description |
|---|---|
list[NativeManifest]
|
The manifests that are not plugin content, in their input order. |
marketplace_sources(manifest, root=Path())
Return repository-relative local source roots declared by one marketplace.
Returns:
| Type | Description |
|---|---|
list[Path]
|
Existing local source roots in the marketplace order. |
manifest_root(manifest)
Return the source root that owns a conventional plugin manifest.
Returns:
| Type | Description |
|---|---|
Path
|
The directory containing a loose manifest or enclosing a harness manifest. |
manifests_for_source(manifests, source_root)
Return all plugin manifests directly owned by one source root.
Returns:
| Type | Description |
|---|---|
list[NativeManifest]
|
Plugin manifests ordered by their repository-relative paths. |
source_for_path(manifests, path)
Find the most-specific conventional plugin source root containing path.
Returns:
| Type | Description |
|---|---|
Path | None
|
The owning source root, or None when no native plugin owns the path. |