* Rename agnes-metadata.json to marketplace-metadata.json
Curated marketplace enrichment file (.claude-plugin/agnes-metadata.json)
becomes marketplace-metadata.json. Clean cut, no fallback — curators of
upstream marketplace repos must rename the file on their side.
Python API renames mirror the file rename: read_agnes_metadata →
read_marketplace_metadata, AGNES_METADATA_REL → MARKETPLACE_METADATA_REL,
AGNES_METADATA_MAX_BYTES → MARKETPLACE_METADATA_MAX_BYTES. Synth Claude
Code marketplace strip rule (.agnes/** + the metadata file) follows the
new filename.
* Marketplace detail polish: window cover + 715:310 aspect + helper alignment
- Plugin & item (skill/agent) detail hero: 160x160 square cover replaced
with a macOS-style window frame (3 traffic-light dots + titlebar label
showing the entity name). Body is constrained to 715:310 so curator-
uploaded covers no longer crop to a square. Window is 380px wide; meta
column and absolutely-positioned top-right install/remove actions stay
put. Fallback when no cover_photo_url (translucent gradient + PL/SK/AG
initials) is unchanged, just inside the window body.
- Inner skill/agent cards in the plugin detail's Internal structure
section adopt the same 715:310 aspect (was fixed 78px tall). No window
chrome on inner cards — just the matching proportions so covers read
consistently across hero, grid tiles, and listing cards.
- Curated nested item helper text ("This skill is part of ... — add the
bundle to your stack to use it") now stacks UNDER the "Open parent
plugin" button instead of being a side-by-side flex sibling in the
actions-row. Added align-self: flex-end so the 260px helper box
anchors at the right edge of the 300px actions column, matching the
button's right edge.
* Marketplace My tab: surface the same category + type filters as Flea
- Frontend: mp-cat-row and mp-type-row now show on tab=my (previously
hidden — type was flea-only, category was flea/curated-only). Curated
browse stays plugin-only and continues to hide the type pills.
fetchOne() sends the `type` param for tab=my too, so the items
endpoint's existing my-branch filter actually receives it.
- Backend categories endpoint, tab=my branch: when the type filter is
set to skill/agent, skip counting curated subscriptions. Curated
plugins are always type='plugin', so they wouldn't survive the items
endpoint's type filter; including them in the category counts made
the pill numbers overstate what users could actually see in the
grid. type=None or type='plugin' keeps the previous behaviour.
- CHANGELOG entry under [Unreleased].
* Marketplace plugin detail: render rich content from marketplace-metadata.json
Adds five optional plugin-level fields to marketplace-metadata.json and
renders them on the curated plugin detail page + listing card:
* display_name — friendly h1 / listing-card name / mac-window titlebar
label (overrides the technical plugin id)
* tagline — punchy 1-line value prop for the hero subtitle and the
listing card description (replacing the verbose marketplace.json
description on cards)
* description — multi-paragraph markdown body, server-side rendered
through markdown-it-py and sanitized through nh3 with a
description-scoped allowlist (no iframes / no raw HTML / no
javascript: links). Powers the "What it does" panel.
* use_cases[] — {title, description, prompt} entries that render as a
3-column "When to use it" card grid; each card shows the literal
prompt as a code chip so users can copy-paste into Claude Code.
* sample_interaction — {user, assistant} dialog rendered in a Claude
Code-style dark Catppuccin Mocha transcript panel: monospace user
row with a green ">" prompt indicator + sans-serif assistant body
with markdown formatting (peach bold, yellow italic, pink inline
code, mantle-dark fenced code blocks).
All five fields are optional; UI sections only render when populated,
so plugins without enrichment look identical to before. Fields are
read on-demand from the working tree (cached by mtime per marketplace
slug) so curator edits land at the next request without waiting for
a sync cycle — same pattern as the existing inner-skill/agent
enrichment path. No DB schema bump.
Skill / agent rich-content rendering is deferred to a later phase
(needs a source-of-truth decision: extend plugin.yml? LLM-generate
from SKILL.md / agent.md?). The schema accepts the same fields at
skill/agent level today for forward compatibility but the UI ignores
them for now.
Also: stripped a stale `background-color: var(--bg)` from the global
`code` rule in style.css (was making inline code visually disappear
on the page background).
* Skill / agent detail: render rich content from marketplace-metadata.json
Brings the skill/agent detail pages to parity with the plugin detail
page. Same rich-content schema (display_name, tagline, description as
markdown, use_cases[], sample_interaction) plus two per-item additions:
* invocation — curator-provided literal command string. When set,
overrides the computed "<manifest_name>:<inner_name>" chip and
cleanly supports both "/" skill prefix and "@" agent prefix (the
hardcoded "/" in the chip markup is hidden when the curator provides
the invocation, so /grpn-eng:query <q> and @grpn-eng:cto-architect
both render correctly).
* when_to_use — markdown disambiguation block ("Use this for X. For
similar Y, see /other-skill") rendered into a new "When to use this"
panel below the Example section.
Skill / agent category is now per-item overridable in
marketplace-metadata.json. When absent, the API keeps the parent
plugin's category as the badge so existing items don't lose their
category until curators opt in to per-item categorization.
The new "Example" Q&A panel uses the same Claude Code-style dark
Catppuccin Mocha transcript treatment as the plugin detail —
monospace user row with a green ">" prompt indicator + sans-serif
assistant body with markdown formatting.
All new fields are optional and read on-demand from the working tree.
Skills / agents whose marketplace-metadata.json doesn't carry rich
content render exactly the same way they did before (frontmatter
description + computed slash command + cover from existing v32
enrichment). No DB schema bump.
* Fix TypeError in skill / agent detail when curator sets per-item category
`curated_skill_detail` and `curated_agent_detail` were passing both
`**parent` (from `_curated_inner_parent_fields`, which returns the
parent plugin's category as a fallback) and `**enrichment` (from
`_curated_inner_enrichment`, which returns the per-item category
override when the curator set one) into `InnerDetailResponse(...)`.
Python function-call kwargs unpacking with overlapping keys raises
`TypeError: got multiple values for keyword argument 'category'`
— it doesn't merge like a literal dict does. The bug only surfaced
when the marketplace-metadata.json carried a `category` field at
skill / agent level (curator opting into per-item categorization);
items without that override hit the endpoint cleanly because only
parent provided the key.
Fix: build `merged = {**parent, **enrichment}` first (literal-dict
syntax DOES merge, with the right-hand-side winning) and unpack the
merged dict. Curator override still wins via the merge order, and
the same pattern is future-proof for any other field that lands in
both layers later.
Plus a regression test in test_marketplace_metadata.py asserting
that the inner-resolver carries `category` for downstream merging.
* Marketplace detail: tolerate partial curator JSON
Server constructed UseCase / SampleInteraction via raw dict indexing
(uc["title"], sample["assistant"]), so a curator commit missing any
required Pydantic field crashed the whole plugin / skill / agent detail
endpoint with a 500. Route both constructions through _safe_use_case /
_safe_sample_interaction helpers — partial input silently drops the
malformed card / section instead of breaking the page.
Regression test in test_marketplace_api.py covers the three shapes:
use_case missing a key, use_case with an empty string, and
sample_interaction with only user (no assistant). Sibling rich fields
still render.
* Address PR-251 review (must-fixes + S2/S3 polish) + release-cut 0.50.0
Five must-fixes from the review pass (3 from @cvrysanek's two-stage
review, 2 from my independent pass), plus the 0.50.0 release-cut as the
last commit on this PR per CLAUDE.md (CLAUDE.md "Release-cut belongs
to the PR" rule added in v0.49.1).
Must-fixes
----------
1. Cache eviction: bounded LRU instead of per-marketplace predicate.
The previous predicate (`k[0] == marketplace_id and k[1] != mtime_ns`)
only swept stale entries for the CURRENT marketplace; with N>100
distinct marketplaces each holding one mtime key, the cap silently
failed and memory grew linearly. Replaced with OrderedDict-backed
bounded LRU at cap=256, drop oldest insert on overflow.
Cache stress test pinned in test_marketplace_metadata.py.
2. Render CPU cap: per-field byte cap on description / when_to_use /
sample_interaction.assistant via MARKETPLACE_METADATA_FIELD_MAX_BYTES
(= 64 KiB). Without this, a 1 MiB curator markdown body × QPS =
curator-controlled CPU burn through pure-Python markdown-it-py.
Truncation respects UTF-8 boundaries and logs a warning so the
curator sees the cap fire on the next sync. Test for cap +
UTF-8-boundary preservation.
3. Inner-detail bypassed the metadata cache. _curated_inner_enrichment,
_curated_inner_cover, and curated_detail all called
read_marketplace_metadata directly, defeating the mtime cache the
plugin listing already shared. Routed all three through
_read_metadata_cached so skill/agent detail hits are O(1) re-parses
per marketplace per mtime instead of O(QPS).
4. Truthy-vs-presence trap in plugin/inner enrichment merge. API-layer
writers used `if resolved.get(k):` which silently dropped any
future falsy-but-valid resolver field (bool featured=False, int
priority=0, str category=''). Switched to presence check
(`if k in resolved`) so the resolver is the authority on field
presence; `{**parent, **enrichment}` merge respects whatever the
resolver decided to ship.
5. Vendor-agnostic OSS cleanup. Removed operator-specific token
references (/grpn-eng:, @grpn-eng:, .foundryai/) from
src/marketplace_metadata.py docstring, app/web/templates/
marketplace_item_detail.html JS comment, docs/curated-marketplace-
format.md, and tests/test_marketplace_metadata.py fixtures. Replaced
with generic /my-plugin:tool / @my-agent:role / .example/ placeholders.
CHANGELOG
---------
- New "### Fixed (PR #251 follow-ups)" section documenting all 4
code-side must-fixes
- New "### Internal" section noting the vendor cleanup + new tests
- BREAKING bullet for the file rename now covers operator-side
migration: running instances see plugin enrichment disappear from
the UI until upstream curator renames + nightly sync overwrites the
working tree; POST /api/marketplaces/{id}/sync forces refresh sooner
- Stripped /grpn-eng: leaks from the existing skill/agent rich-content
bullet
Tests
-----
128 targeted tests pass (test_marketplace_metadata, test_marketplace_api,
test_marketplace, test_markdown_render, test_marketplace_synth_strip,
test_marketplace_filter). New tests added:
- 6 XSS regression tests on render_safe (javascript:/data:/vbscript:
schemes via autolink, reference link, and mixed-case + positive
http/https/mailto + noopener noreferrer rel)
- 3 byte-cap tests (truncation + UTF-8 boundary + under-cap pass-through)
- 1 cache eviction stress test (>256 marketplaces -> bounded at cap)
- 1 truthy-vs-presence resolver-contract test
Release-cut
-----------
- pyproject.toml 0.49.1 -> 0.50.0 (minor; BREAKING file rename per
pre-1.0 CHANGELOG note: "breaking changes called out under Changed
or Removed with the BREAKING marker")
- CHANGELOG [Unreleased] -> [0.50.0] - 2026-05-12, new empty
[Unreleased] on top.
---------
Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com>
Co-authored-by: ZdenekSrotyr <zdenek.srotyr@keboola.com>
8.5 KiB
How to add Agnes-side info to your Curated Marketplace
This guide is for the Curated Marketplace channel only — git-hosted marketplaces an Agnes admin registers in
/admin/marketplaces.
You're maintaining a Claude Code marketplace registered as Curated in Agnes, and you'd like the plugins in it to show with cover photos, demo videos, and doc links inside the Agnes web UI. This guide is the whole story.
Quickstart
Create a JSON file at this exact path in your repo:
.claude-plugin/marketplace-metadata.json
Put the keys you want filled in. Every key is optional. Skip a field, skip a plugin, skip the entire file — Agnes will render whatever you provided and nothing else. Adding the file later (or expanding it) just shows more on the next sync.
Minimal example — one plugin, one cover photo, nothing else:
{
"plugins": {
"my-plugin": {
"cover_photo": ".agnes/my-plugin-cover.png"
}
}
}
Done. After the next sync, the card for my-plugin in Agnes shows your
cover photo. Other plugins (and other fields) keep their defaults.
The schema
The same shape applies at three levels: plugin, skill, agent. The rich
content fields (display_name, tagline, description, use_cases,
sample_interaction) render at all three levels. Skill/agent items
additionally accept invocation (literal command string for the chip on
the detail page) and when_to_use (markdown disambiguation block).
{
"plugins": {
"<plugin-name>": {
"cover_photo": "...",
"video_url": "...",
"category": "...",
"display_name": "Friendly Plugin Name",
"tagline": "One punchy line explaining what this does.",
"description": "Multi-paragraph **markdown** body...",
"use_cases": [
{
"title": "Understand a service",
"description": "Find owners, deps, tech stack.",
"prompt": "What does order-orchestration do?"
}
],
"sample_interaction": {
"user": "What does the order-orchestration service do?",
"assistant": "The order-orchestration service is a B2B order-routing layer..."
},
"doc_links": [
{ "name": "Setup", "path": "docs/setup.md" },
{ "name": "API ref", "url": "https://example.com/api.pdf" }
],
"skills": {
"<skill-name>": {
"cover_photo": "...",
"video_url": "...",
"doc_links": [...]
}
},
"agents": {
"<agent-name>": {
"cover_photo": "...",
"video_url": "...",
"doc_links": [...]
}
}
}
}
}
Fields, all optional:
| Field | What it does | Where it renders |
|---|---|---|
cover_photo |
Image (715 : 310 aspect recommended). | Hero window, listing card, inner-card grid. |
video_url |
Demo video URL — YouTube / Vimeo / direct .mp4. |
Detail page "Demo video" panel. |
category |
Override the marketplace.json category. Must match Agnes vocabulary: Code & Engineering, Data & Analytics, Documentation, Productivity, Communication, DevOps & Infra, Security, Research, Other. |
Category pill on cards + filter chips. |
doc_links[] |
Optional standalone docs (PDF / MD / TXT). {name, path} for repo files or {name, url} for external. Use only for genuine extras (deep-dive PDFs, examples) — don't dump README / CLAUDE.md / SKILL.md here; the curated marketplace UI shows them only as downloadables, not rendered docs. |
Detail page "Documentation" panel. |
display_name |
Friendly plugin name (1 line, ≤ ~40 chars). | Hero h1, listing card name, mac-window titlebar label. |
tagline |
Punchy value prop (1 line, ≤ ~120 chars — beyond that the listing card 2-line clamp truncates). | Hero subtitle, listing card description. |
description |
Multi-paragraph markdown body. Bold, italic, lists, links, fenced code, tables, blockquotes supported. Raw HTML and inline JavaScript are stripped by the server-side sanitizer. | Detail page "What it does" panel, rendered as HTML. |
use_cases[] |
Concrete usage examples. Each entry: title (heading), description (1-2 sentences), prompt (the literal text a user pastes into Claude Code). |
Detail page "When to use it" 3-column card grid. |
sample_interaction |
One example dialog. {user, assistant} — both required; assistant accepts markdown (renders to safe HTML). |
Detail page "Example" Claude Code-style dark Q&A panel. |
invocation |
Skill / agent only. Literal command the user should run, e.g. /my-plugin:tool <your question> or @my-agent:role. Overrides the computed <manifest_name>:<inner_name> chip. Use this to add an args hint (<your question>) or to fix the prefix for agents (@ instead of /). |
"How to call it" code chip + Copy button. |
when_to_use |
Skill / agent only. Markdown body explaining when to pick this skill/agent over a similar one. Sample: Use this for **Confluence only**. For mixed sources, see /my-plugin:query. |
"When to use this" panel below "Example". |
skills |
Map keyed by skill name (matching name: in the skill's SKILL.md frontmatter). |
Skill detail page. |
agents |
Map keyed by agent name (the agent .md filename without extension). |
Agent detail page. |
The rich-content fields (display_name, tagline, description,
use_cases, sample_interaction) are read on-demand from the working
tree at request time — curator edits to marketplace-metadata.json land at
the next page refresh without waiting for the next sync cycle. The visual
fields (cover_photo, video_url, category, doc_links) are persisted
into the marketplace database at sync time, because they participate in the
asset-mirror flow that needs to run once per push.
<plugin-name> matches the name field of the plugin in your
marketplace.json. Same for skill and agent names — they match what's
in the corresponding files inside the plugin.
Where to put cover photos and docs
You can either ship them in your repo or link to a public URL.
In your repo — convention: drop them under .agnes/ at the repo
root, then reference by path:
{ "cover_photo": ".agnes/my-plugin-cover.png" }
{ "doc_links": [{ "name": "Setup", "path": "docs/setup.md" }] }
Files under .agnes/ are stripped from the synthetic Claude Code
marketplace Agnes serves to user instances, so you can put
Agnes-only content there without bloating the plugin distribution.
Public URL — Agnes detects any value starting with https:// (or
http://) and downloads it once at sync time, then serves the cached
copy:
{ "cover_photo": "https://cdn.example.com/cover.png" }
{ "doc_links": [{ "name": "API ref", "url": "https://example.com/api.pdf" }] }
If the original URL goes 404 later, Agnes keeps showing the cached copy it already has — link rot doesn't break your plugin's UI.
Worked example
Copy this and adjust:
{
"plugins": {
"data-explorer": {
"cover_photo": ".agnes/data-explorer-cover.png",
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"category": "Data analysis",
"doc_links": [
{ "name": "Setup guide", "path": "docs/setup.md" },
{ "name": "API reference (PDF)", "url": "https://example.com/data-explorer-api.pdf" }
],
"skills": {
"explore-table": {
"cover_photo": ".agnes/skills/explore-table-cover.png",
"doc_links": [
{ "name": "Cheatsheet", "path": "docs/skills/explore-table.md" }
]
}
},
"agents": {
"query-planner": {
"cover_photo": "https://cdn.example.com/agents/query-planner.webp",
"doc_links": [
{ "name": "Decision flow (PDF)", "url": "https://example.com/query-planner-flow.pdf" }
]
}
}
},
"report-generator": {
"cover_photo": "https://cdn.example.com/report-generator-cover.png",
"category": "Reporting"
}
}
}
The same example lives at
docs/examples/marketplace-metadata.json
in the source repo.
Allowed file types
Agnes accepts a small set of formats — anything else is silently skipped.
Cover photos: PNG (.png), JPEG (.jpg / .jpeg), WebP (.webp).
Documentation files: PDF (.pdf), Markdown (.md / .markdown),
plain text (.txt).