Describe the bug
For a monorepo that is its own marketplace and references its packages by local virtual-subdirectory source (source: ./packages/<name>), apm pack produces a marketplace.json whose plugin entries have no description and no version — even though every package's own apm.yml declares both. apm marketplace browse <name> therefore renders -- in the Description and Version columns for every plugin.
The metadata enrichment that fills these fields runs for remote packages only: the builder fetches description/version from a package's apm.yml over the network, but for a local-path package it never reads the apm.yml sitting on disk next to the manifest. The Claude output mapper has no local fallback, so unless the curator duplicates description/version onto each marketplace entry by hand, local entries are emitted with only name/source/tags. Note the asymmetry: tags declared on the entry pass through fine, so the manifest is valid — only the package-owned description/version are silently lost.
Verified against apm-cli 0.19.0 by reading the source and with live apm pack runs.
To Reproduce
Steps to reproduce the behavior:
-
A monorepo marketplace whose entries reference packages by local subpath, with the per-package metadata owned by each package's manifest:
# apm.yml (repo root)
marketplace:
owner: { name: tester, url: https://github.com/tester }
outputs: { claude: {} }
packages:
- name: foo
source: ./packages/foo
tags: [demo]
# packages/foo/apm.yml
name: foo
version: 1.2.3
description: A local foo package with real metadata.
type: skill
target: all
-
Run apm pack --offline.
-
Inspect .claude-plugin/marketplace.json.
Observed — description and version are dropped, even though packages/foo/apm.yml declares them:
{
"name": "foo",
"tags": ["demo"],
"source": "./packages/foo"
}
Duplicating the same fields onto the marketplace entry makes them appear, which confirms the serialization path itself works — only the local-source-of-truth read is missing:
- name: foo
source: ./packages/foo
tags: [demo]
description: A local foo package with real metadata.
version: 1.2.3
{
"name": "foo",
"description": "A local foo package with real metadata.",
"version": "1.2.3",
"tags": ["demo"],
"source": "./packages/foo"
}
A real-world instance: a 96-package monorepo marketplace, all local subpath sources, every package apm.yml carrying version + description → all 96 browse entries show empty Description/Version.
Expected behavior
For a local-path (virtual-subdirectory) package, the builder should read <source>/apm.yml from disk and inherit description/version (and the other Anthropic pass-through metadata) the same way it fetches them from the remote apm.yml for remote packages — a local-filesystem mirror of the existing remote-metadata fetch. Curator-supplied values on the marketplace entry should still take precedence. This matches the intent already stated in the code (_prefetch_metadata documents that "Local-path packages are skipped (they carry their own metadata)") — but that local metadata is never actually read, so the field ends up empty instead of inherited.
Environment (please complete the following information):
- OS: Linux (WSL2)
- Python Version: 3.14
- APM Version: 0.19.0
- VSCode Version (if relevant): n/a
Logs
apm marketplace browse against such a marketplace (Description / Version empty for every plugin):
Plugins in 'example-marketplace'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin ┃ Description ┃ Version ┃ Install ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩
│ foo │ -- │ -- │ foo@example-marketpl… │
└──────────────────────────────┴─────────────┴─────────┴───────────────────────┘
apm pack --offline reports success while emitting the metadata-less entries:
[*] Built marketplace.json [claude] (1 package(s)) -> .claude-plugin/marketplace.json
Additional context
Source-level root cause (apm-cli 0.19.0):
-
marketplace/builder.py (_resolve_entry) resolves a local entry with source_repo="" (the if entry.is_local: branch). The source_repo field is what later marks a package as "remote".
-
marketplace/builder.py (_prefetch_metadata) builds the {name: {description, version}} map but first filters to remote entries only:
# Local-path packages are skipped (they carry their own metadata).
remote = [pkg for pkg in resolved if pkg.source_repo]
Because local packages have source_repo == "", they are excluded from enrichment entirely. The comment asserts local packages "carry their own metadata", but nothing downstream reads it.
-
marketplace/builder.py (_fetch_remote_metadata) is documented as "fetch description and version from the package's remote apm.yml" and reaches over the network (GitHub host / token aware). There is no local-filesystem counterpart that reads <source>/apm.yml.
-
marketplace/output_mappers.py (ClaudeMarketplaceMapper, uses_remote_metadata = True) emits description/version differently for local vs remote:
if is_local:
if entry.description:
plugin["description"] = entry.description
if entry.version:
plugin["version"] = entry.version
else:
meta = remote_metadata.get(pkg.name, {})
...
elif meta.get("description"):
plugin["description"] = meta["description"]
...
elif meta.get("version"):
plugin["version"] = meta["version"]
The remote branch falls back to the fetched remote_metadata; the local branch only emits curator-supplied entry.description/entry.version and has no fallback to the package's on-disk apm.yml. (The CodexMarketplaceMapper is unaffected — that format carries category, not description/version.)
Net: for a monorepo-as-marketplace using local subpath sources, every plugin's apm.yml (with version + description) is on disk right beside its manifest, but the builder never reads it, so the generated marketplace.json omits both fields.
Workaround in use: a CI step discovers every packages/*/apm.yml and writes description/version onto each marketplace entry in the root apm.yml before apm pack, so the fields are present on the entry by the time the Claude mapper runs.
Describe the bug
For a monorepo that is its own marketplace and references its packages by local virtual-subdirectory source (
source: ./packages/<name>),apm packproduces amarketplace.jsonwhose plugin entries have nodescriptionand noversion— even though every package's ownapm.ymldeclares both.apm marketplace browse <name>therefore renders--in the Description and Version columns for every plugin.The metadata enrichment that fills these fields runs for remote packages only: the builder fetches
description/versionfrom a package'sapm.ymlover the network, but for a local-path package it never reads theapm.ymlsitting on disk next to the manifest. The Claude output mapper has no local fallback, so unless the curator duplicatesdescription/versiononto each marketplace entry by hand, local entries are emitted with onlyname/source/tags. Note the asymmetry:tagsdeclared on the entry pass through fine, so the manifest is valid — only the package-owneddescription/versionare silently lost.Verified against apm-cli 0.19.0 by reading the source and with live
apm packruns.To Reproduce
Steps to reproduce the behavior:
A monorepo marketplace whose entries reference packages by local subpath, with the per-package metadata owned by each package's manifest:
Run
apm pack --offline.Inspect
.claude-plugin/marketplace.json.Observed —
descriptionandversionare dropped, even thoughpackages/foo/apm.ymldeclares them:{ "name": "foo", "tags": ["demo"], "source": "./packages/foo" }Duplicating the same fields onto the marketplace entry makes them appear, which confirms the serialization path itself works — only the local-source-of-truth read is missing:
{ "name": "foo", "description": "A local foo package with real metadata.", "version": "1.2.3", "tags": ["demo"], "source": "./packages/foo" }A real-world instance: a 96-package monorepo marketplace, all local subpath sources, every package
apm.ymlcarryingversion+description→ all 96 browse entries show empty Description/Version.Expected behavior
For a local-path (virtual-subdirectory) package, the builder should read
<source>/apm.ymlfrom disk and inheritdescription/version(and the other Anthropic pass-through metadata) the same way it fetches them from the remoteapm.ymlfor remote packages — a local-filesystem mirror of the existing remote-metadata fetch. Curator-supplied values on the marketplace entry should still take precedence. This matches the intent already stated in the code (_prefetch_metadatadocuments that "Local-path packages are skipped (they carry their own metadata)") — but that local metadata is never actually read, so the field ends up empty instead of inherited.Environment (please complete the following information):
Logs
apm marketplace browseagainst such a marketplace (Description / Version empty for every plugin):apm pack --offlinereports success while emitting the metadata-less entries:Additional context
Source-level root cause (apm-cli 0.19.0):
marketplace/builder.py(_resolve_entry) resolves a local entry withsource_repo=""(theif entry.is_local:branch). Thesource_repofield is what later marks a package as "remote".marketplace/builder.py(_prefetch_metadata) builds the{name: {description, version}}map but first filters to remote entries only:Because local packages have
source_repo == "", they are excluded from enrichment entirely. The comment asserts local packages "carry their own metadata", but nothing downstream reads it.marketplace/builder.py(_fetch_remote_metadata) is documented as "fetchdescriptionandversionfrom the package's remoteapm.yml" and reaches over the network (GitHub host / token aware). There is no local-filesystem counterpart that reads<source>/apm.yml.marketplace/output_mappers.py(ClaudeMarketplaceMapper,uses_remote_metadata = True) emitsdescription/versiondifferently for local vs remote:The remote branch falls back to the fetched
remote_metadata; the local branch only emits curator-suppliedentry.description/entry.versionand has no fallback to the package's on-diskapm.yml. (TheCodexMarketplaceMapperis unaffected — that format carriescategory, notdescription/version.)Net: for a monorepo-as-marketplace using local subpath sources, every plugin's
apm.yml(withversion+description) is on disk right beside its manifest, but the builder never reads it, so the generatedmarketplace.jsonomits both fields.Workaround in use: a CI step discovers every
packages/*/apm.ymland writesdescription/versiononto each marketplace entry in the rootapm.ymlbeforeapm pack, so the fields are present on the entry by the time the Claude mapper runs.