Skip to content

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:

  1. Pre-commit mode (default): Detects CRUD operations on plugins and their components during pre-commit, then updates manifests and bumps versions.

  2. 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.

  3. 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 (major, minor, patch), or None if the path is

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 HEAD) to read the file from.

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 plugins/.

required
changes ComponentChanges

Component changes with added, deleted, and modified lists.

required

Returns:

Type Description
bool

(updated, version) — updated is True when the file was written;

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 <name> in plugins/<name>/)

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]

plugin.json version at head_ref is not strictly greater than at

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 plugins/<name>/ changed after the plugin's last version-bump

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 (plugins/<name>).

required

Returns:

Type Description
tuple[str, str] | None

(old_version, new_version) on success, or None when

tuple[str, str] | None

plugin.json is missing, unreadable, malformed, or lacks a

tuple[str, str] | None

well-formed major.minor.patch string version field.

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 ./-style source, or None for a remote or malformed entry.

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.