From 82ce66b7427d506d5844016327a945e597b93ce3 Mon Sep 17 00:00:00 2001 From: Nicholas Sollazzo Date: Mon, 29 Jun 2026 21:12:18 +0200 Subject: [PATCH] feat: sync wire types with the latest API (additive fields) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit internal/api/types.go documents itself as a field-for-field mirror of the product's src/lib/types.ts; it had drifted, so --json silently dropped fields the server now returns. Re-sync the additive ones: - SoulCard/Listing: chargeCount (the "energy boost" running count) - Listing: profileTier (author seal), hasAsset / assetVersion / assetContentHash (hosted SKILL.md asset metadata — lets clients skip an identical re-download) - LoopData + new SkillData: bundles (a meta-skill/loop's bundled listing slugs) Human detail views surface CHARGES, a skill's ASSET VERSION, and loop/skill BUNDLES; everything flows into --json automatically. Adds a SkillData() decoder mirroring LoopData(), round-trip tests pinning the new fields (incl. null for the nullable asset fields), and the mock fixtures + regenerated goldens — the golden churn is the contract change. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + internal/api/types.go | 72 +++++-- internal/api/types_test.go | 93 ++++++++ internal/cli/listing.go | 11 + internal/cli/soul.go | 1 + internal/cli/testdata/golden/cli-search.json | 5 + .../cli/testdata/golden/harness-list.json | 5 + .../cli/testdata/golden/listing-create.json | 5 + .../testdata/golden/listing-unpublish.json | 8 + internal/cli/testdata/golden/loop-create.json | 5 + internal/cli/testdata/golden/loop-list.json | 8 + internal/cli/testdata/golden/loop-show.json | 8 + internal/cli/testdata/golden/loop-show.txt | 2 + internal/cli/testdata/golden/mcp-list.json | 5 + .../cli/testdata/golden/mcp-tools-list.json | 33 +++ internal/cli/testdata/golden/skill-list.json | 11 +- internal/cli/testdata/golden/soul-create.json | 1 + .../testdata/golden/soul-list-category.json | 1 + .../cli/testdata/golden/soul-list-newest.json | 3 + internal/cli/testdata/golden/soul-list.json | 3 + .../golden/soul-search-downloads.json | 2 + internal/cli/testdata/golden/soul-search.json | 3 + internal/cli/testdata/golden/soul-show.json | 1 + internal/cli/testdata/golden/soul-show.txt | 1 + .../golden/soul-update-ownership.json | 1 + internal/mockapi/mockapi.go | 199 ++++++++++-------- 26 files changed, 387 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efaa967..76ee611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **`positronick feed` (admin)**: manage the blog feed sources (GitHub release / RSS mirroring) the ingestor polls — `feed list`, `feed create --label --feed-url --kind github_release|rss --category ` (`--author`/`--listing` attribution, repeatable `--tag`, `--auto-publish`, `--enabled`), `feed update ` (`--enabled=false` pauses a feed — there is no delete verb), and `feed sync ` (ingest one feed now, surfacing the fetch summary; a fetch/parse failure maps the API's 502 to a clear error). Backed by the `/api/admin/feeds` API. Ingesting every feed on a schedule stays the cron's job — no CLI subcommand carries that admin key. +- **API type sync**: the wire types now mirror the latest `src/lib/types.ts` field-for-field, so `--json` no longer silently drops fields the server sends. Souls and listings gain `chargeCount` (the "energy boost" count); listings also gain `profileTier` (the author's seal), `hasAsset`/`assetVersion`/`assetContentHash` (hosted SKILL.md asset metadata — lets a client skip identical re-downloads), and `LoopData`/`SkillData` gain `bundles`. Human detail views surface `CHARGES`, a skill's `ASSET VERSION`, and loop/skill `BUNDLES`. ## [0.1.2] - 2026-06-16 diff --git a/internal/api/types.go b/internal/api/types.go index 7f01db7..f4a9bc5 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -62,13 +62,15 @@ type SoulCard struct { // ContentHash is the sha256 of the normalized SOUL.md body. ContentHash string `json:"contentHash"` // Status is draft | pending | published. - Status string `json:"status"` - DownloadCount int `json:"downloadCount"` - RatingAvg *float64 `json:"ratingAvg"` - RatingCount int `json:"ratingCount"` - ArenaRank *int `json:"arenaRank"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + Status string `json:"status"` + DownloadCount int `json:"downloadCount"` + // ChargeCount is the running count of user "charges" (the energy boost). + ChargeCount int `json:"chargeCount"` + RatingAvg *float64 `json:"ratingAvg"` + RatingCount int `json:"ratingCount"` + ArenaRank *int `json:"arenaRank"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } // Soul is a full soul, including the raw SOUL.md markdown body. Mirrors Soul @@ -88,7 +90,9 @@ type Listing struct { // ProfileHandle/ProfileName denormalize the authoring profile for cards. ProfileHandle string `json:"profileHandle"` ProfileName string `json:"profileName"` - Name string `json:"name"` + // ProfileTier is the author's seal — "official" | "verified" | null. + ProfileTier *string `json:"profileTier"` + Name string `json:"name"` // Type is one of ListingTypes. Type string `json:"type"` // Tagline is the short one-liner shown on cards. @@ -103,13 +107,23 @@ type Listing struct { RepoURL *string `json:"repoUrl"` // InstallCmd is the canonical official install/run command, if any. InstallCmd *string `json:"installCmd"` - // Data holds type-specific extras (e.g. LoopData for loops); {} when none. - Data map[string]any `json:"data"` - Confidence string `json:"confidence"` - Status string `json:"status"` - DownloadCount int `json:"downloadCount"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + // Data holds type-specific extras (e.g. LoopData for loops, SkillData for + // skills); {} when none. + Data map[string]any `json:"data"` + // HasAsset is true when a hosted SKILL.md asset exists for this (skill) listing. + HasAsset bool `json:"hasAsset"` + // AssetVersion is the hosted asset's semver, or null when HasAsset is false. + AssetVersion *string `json:"assetVersion"` + // AssetContentHash is the sha256 of the hosted asset body, or null — lets + // clients skip identical re-downloads. + AssetContentHash *string `json:"assetContentHash"` + Confidence string `json:"confidence"` + Status string `json:"status"` + DownloadCount int `json:"downloadCount"` + // ChargeCount is the running count of user "charges" (the energy boost). + ChargeCount int `json:"chargeCount"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } // LoopData decodes the untyped Data payload into LoopData. Empty or nil Data @@ -129,6 +143,23 @@ func (l *Listing) LoopData() (LoopData, error) { return ld, nil } +// SkillData decodes the untyped Data payload into SkillData. Empty or nil Data +// yields the zero value; a wrong-typed field is an error, never a silent zero. +func (l *Listing) SkillData() (SkillData, error) { + var sd SkillData + if len(l.Data) == 0 { + return sd, nil + } + b, err := json.Marshal(l.Data) + if err != nil { + return sd, fmt.Errorf("encoding listing data: %w", err) + } + if err := json.Unmarshal(b, &sd); err != nil { + return sd, fmt.Errorf("decoding skill data for %q: %w", l.Slug, err) + } + return sd, nil +} + // Profile is a verified person or org that authors registry tooling. Mirrors // Profile in src/lib/types.ts. Nullable TS fields (string | null) are pointers // so null round-trips as null in --json output. @@ -170,6 +201,17 @@ type LoopData struct { CompatibleTools []string `json:"compatibleTools,omitempty"` // Kickoff is the prompt a user copies to start the loop. Kickoff string `json:"kickoff,omitempty"` + // Bundles are slugs of listings this loop depends on (a loop usually drives + // several skills). + Bundles []string `json:"bundles,omitempty"` +} + +// SkillData is the type-specific extras for a `skill` listing, stored in +// Listing.Data. A skill with Bundles is a meta-skill: installing it pulls in +// every bundled listing. Mirrors SkillData in src/lib/types.ts. +type SkillData struct { + // Bundles are slugs of listings this skill bundles — its install-time deps. + Bundles []string `json:"bundles,omitempty"` } // FeedSource is a subscribed blog feed source the ingestor mirrors into posts diff --git a/internal/api/types_test.go b/internal/api/types_test.go index 1171647..47750cc 100644 --- a/internal/api/types_test.go +++ b/internal/api/types_test.go @@ -1,7 +1,9 @@ package api import ( + "encoding/json" "reflect" + "strings" "testing" ) @@ -74,3 +76,94 @@ func TestListingLoopData(t *testing.T) { }) } } + +// A meta-skill carries its bundled listing slugs in Listing.Data; the typed +// decode must round-trip them, and a wrong-typed field must fail loud rather +// than silently drop the dependency graph. +func TestListingSkillData(t *testing.T) { + tests := []struct { + name string + data map[string]any + want SkillData + wantErr bool + }{ + { + name: "bundles decode field for field", + data: map[string]any{"bundles": []any{"pr-to-green", "debugging"}}, + want: SkillData{Bundles: []string{"pr-to-green", "debugging"}}, + }, + {"empty data yields zero value", map[string]any{}, SkillData{}, false}, + {"nil data yields zero value", nil, SkillData{}, false}, + {"unknown keys ignored", map[string]any{"futureField": 1}, SkillData{}, false}, + {"wrong-typed bundles fails loud", map[string]any{"bundles": "pr-to-green"}, SkillData{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := Listing{Data: tt.data} + got, err := l.SkillData() + if tt.wantErr { + if err == nil { + t.Fatal("SkillData should error on a wrong-typed field") + } + return + } + if err != nil { + t.Fatalf("SkillData: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SkillData = %+v, want %+v", got, tt.want) + } + }) + } +} + +// The CLI mirrors src/lib/types.ts field-for-field so an agent reading --json +// sees everything the server sends. These fields were added to the API after +// the CLI's first cut; this pins that they survive a decode→encode round-trip +// (i.e. are never silently dropped), including null for the nullable asset +// fields. +func TestNewWireFieldsRoundTrip(t *testing.T) { + const listingJSON = `{"id":"01X","slug":"superpowers","profileHandle":"obra","profileName":"Jesse Vincent","profileTier":"verified","name":"Superpowers","type":"skill","tagline":"t","description":null,"category":"AI/ML","tags":[],"official":true,"sourceUrl":"https://e.x/s","repoUrl":null,"installCmd":null,"data":{},"hasAsset":true,"assetVersion":"1.0.0","assetContentHash":"abc","confidence":"official","status":"published","downloadCount":18,"chargeCount":6,"createdAt":"2026-04-01T09:00:00.000Z","updatedAt":"2026-04-02T09:00:00.000Z"}` + var l Listing + if err := json.Unmarshal([]byte(listingJSON), &l); err != nil { + t.Fatalf("decode listing: %v", err) + } + if l.ProfileTier == nil || *l.ProfileTier != "verified" { + t.Errorf("profileTier = %v, want verified", l.ProfileTier) + } + if !l.HasAsset || l.AssetVersion == nil || *l.AssetVersion != "1.0.0" { + t.Errorf("asset fields lost: hasAsset=%v version=%v", l.HasAsset, l.AssetVersion) + } + if l.ChargeCount != 6 { + t.Errorf("chargeCount = %d, want 6", l.ChargeCount) + } + out, err := json.Marshal(l) + if err != nil { + t.Fatalf("encode listing: %v", err) + } + for _, want := range []string{`"profileTier":"verified"`, `"hasAsset":true`, + `"assetVersion":"1.0.0"`, `"assetContentHash":"abc"`, `"chargeCount":6`} { + if !strings.Contains(string(out), want) { + t.Errorf("re-encoded listing dropped %s\ngot: %s", want, out) + } + } + + // A null asset (non-skill listing) must round-trip as JSON null, not "". + var bare Listing + if err := json.Unmarshal([]byte(`{"assetVersion":null,"chargeCount":0}`), &bare); err != nil { + t.Fatalf("decode bare: %v", err) + } + out, _ = json.Marshal(bare) + if !strings.Contains(string(out), `"assetVersion":null`) { + t.Errorf("nullable assetVersion must serialize as null, got: %s", out) + } + + // SoulCard gained chargeCount alongside downloadCount. + var s SoulCard + if err := json.Unmarshal([]byte(`{"downloadCount":42,"chargeCount":5}`), &s); err != nil { + t.Fatalf("decode soul: %v", err) + } + if s.ChargeCount != 5 { + t.Errorf("SoulCard.ChargeCount = %d, want 5", s.ChargeCount) + } +} diff --git a/internal/cli/listing.go b/internal/cli/listing.go index a88a7fa..2979d6c 100644 --- a/internal/cli/listing.go +++ b/internal/cli/listing.go @@ -227,9 +227,11 @@ func renderListingDetail(p *output.Printer, l *api.Listing) error { "SOURCE", l.SourceURL, "REPO", deref(l.RepoURL), "INSTALL", deref(l.InstallCmd), + "ASSET VERSION", deref(l.AssetVersion), "CONFIDENCE", l.Confidence, "STATUS", l.Status, "DOWNLOADS", strconv.Itoa(l.DownloadCount), + "CHARGES", strconv.Itoa(l.ChargeCount), "CREATED", l.CreatedAt, "UPDATED", l.UpdatedAt, ) @@ -250,9 +252,18 @@ func renderListingDetail(p *output.Printer, l *api.Listing) error { "EXIT CONDITION", loop.ExitCondition, "MAX ITERATIONS", maxIterations, "COMPATIBLE TOOLS", strings.Join(loop.CompatibleTools, ", "), + "BUNDLES", strings.Join(loop.Bundles, ", "), )...) } + if l.Type == "skill" { + skill, err := l.SkillData() + if err != nil { + return err + } + rows = append(rows, fieldRows("BUNDLES", strings.Join(skill.Bundles, ", "))...) + } + output.RenderFields(p.Out, rows) if loop.Kickoff != "" { p.Human("\nKICKOFF\n%s\n", loop.Kickoff) diff --git a/internal/cli/soul.go b/internal/cli/soul.go index 236d0eb..1938dbb 100644 --- a/internal/cli/soul.go +++ b/internal/cli/soul.go @@ -209,6 +209,7 @@ func renderSoulDetail(p *output.Printer, s *api.Soul) { "LICENSE", s.License, "REPO", deref(s.RepoURL), "DOWNLOADS", strconv.Itoa(s.DownloadCount), + "CHARGES", strconv.Itoa(s.ChargeCount), "STATUS", s.Status, "CREATED", s.CreatedAt, "UPDATED", s.UpdatedAt, diff --git a/internal/cli/testdata/golden/cli-search.json b/internal/cli/testdata/golden/cli-search.json index 4350841..a08a58e 100644 --- a/internal/cli/testdata/golden/cli-search.json +++ b/internal/cli/testdata/golden/cli-search.json @@ -7,6 +7,7 @@ "slug": "github-cli", "profileHandle": "github", "profileName": "GitHub", + "profileTier": "official", "name": "GitHub CLI", "type": "cli", "tagline": "GitHub from the command line", @@ -21,9 +22,13 @@ "repoUrl": "https://example.com/cli/cli", "installCmd": "brew install gh", "data": {}, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "official", "status": "published", "downloadCount": 64, + "chargeCount": 4, "createdAt": "2026-02-01T09:00:00.000Z", "updatedAt": "2026-02-03T09:00:00.000Z" } diff --git a/internal/cli/testdata/golden/harness-list.json b/internal/cli/testdata/golden/harness-list.json index 4294480..3016d8e 100644 --- a/internal/cli/testdata/golden/harness-list.json +++ b/internal/cli/testdata/golden/harness-list.json @@ -6,6 +6,7 @@ "slug": "claude-code", "profileHandle": "anthropic", "profileName": "Anthropic", + "profileTier": "official", "name": "Claude Code", "type": "harness", "tagline": "Agentic coding in your terminal", @@ -20,9 +21,13 @@ "repoUrl": "https://example.com/anthropics/claude-code", "installCmd": "npm install -g @anthropic-ai/claude-code", "data": {}, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "official", "status": "published", "downloadCount": 120, + "chargeCount": 9, "createdAt": "2026-01-10T09:00:00.000Z", "updatedAt": "2026-01-12T09:00:00.000Z" } diff --git a/internal/cli/testdata/golden/listing-create.json b/internal/cli/testdata/golden/listing-create.json index 12d38b5..0d02afe 100644 --- a/internal/cli/testdata/golden/listing-create.json +++ b/internal/cli/testdata/golden/listing-create.json @@ -4,6 +4,7 @@ "slug": "sentry-mcp", "profileHandle": "nsollazzo", "profileName": "Nicholas Sollazzo", + "profileTier": null, "name": "Sentry MCP", "type": "mcp", "tagline": "Errors, issues and traces as agent tools", @@ -18,9 +19,13 @@ "repoUrl": "https://example.com/getsentry/sentry-mcp", "installCmd": "npx @sentry/mcp-server", "data": {}, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "high", "status": "published", "downloadCount": 0, + "chargeCount": 0, "createdAt": "2026-06-09T09:00:00.000Z", "updatedAt": "2026-06-09T09:00:00.000Z", "source": "api" diff --git a/internal/cli/testdata/golden/listing-unpublish.json b/internal/cli/testdata/golden/listing-unpublish.json index c443c47..aa05604 100644 --- a/internal/cli/testdata/golden/listing-unpublish.json +++ b/internal/cli/testdata/golden/listing-unpublish.json @@ -4,6 +4,7 @@ "slug": "pr-to-green", "profileHandle": "nsollazzo", "profileName": "Nicholas Sollazzo", + "profileTier": "official", "name": "PR to Green", "type": "loop", "tagline": "Drive a pull request until CI is green and review approves", @@ -18,6 +19,9 @@ "repoUrl": null, "installCmd": null, "data": { + "bundles": [ + "superpowers" + ], "checkCommand": "gh pr checks --json state", "compatibleTools": [ "claude-code", @@ -28,9 +32,13 @@ "kickoff": "Run the pr-to-green loop on the open PR:\n1. Read every review finding.\n2. Fix the valid ones, push back on the invalid.\n3. Push and re-check until green.", "maxIterations": 20 }, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "official", "status": "draft", "downloadCount": 12, + "chargeCount": 3, "createdAt": "2026-05-01T09:00:00.000Z", "updatedAt": "2026-06-09T10:00:00.000Z", "source": "api" diff --git a/internal/cli/testdata/golden/loop-create.json b/internal/cli/testdata/golden/loop-create.json index 9f8da09..2ee3700 100644 --- a/internal/cli/testdata/golden/loop-create.json +++ b/internal/cli/testdata/golden/loop-create.json @@ -4,6 +4,7 @@ "slug": "docs-to-green", "profileHandle": "nsollazzo", "profileName": "Nicholas Sollazzo", + "profileTier": null, "name": "Docs to Green", "type": "loop", "tagline": "Every doc page builds without warnings", @@ -21,9 +22,13 @@ "kickoff": "Run the docs-to-green loop until the build is clean.", "maxIterations": 20 }, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "high", "status": "published", "downloadCount": 0, + "chargeCount": 0, "createdAt": "2026-06-09T09:00:00.000Z", "updatedAt": "2026-06-09T09:00:00.000Z", "source": "api" diff --git a/internal/cli/testdata/golden/loop-list.json b/internal/cli/testdata/golden/loop-list.json index 7cf50c3..c8ffd04 100644 --- a/internal/cli/testdata/golden/loop-list.json +++ b/internal/cli/testdata/golden/loop-list.json @@ -6,6 +6,7 @@ "slug": "pr-to-green", "profileHandle": "nsollazzo", "profileName": "Nicholas Sollazzo", + "profileTier": "official", "name": "PR to Green", "type": "loop", "tagline": "Drive a pull request until CI is green and review approves", @@ -20,6 +21,9 @@ "repoUrl": null, "installCmd": null, "data": { + "bundles": [ + "superpowers" + ], "checkCommand": "gh pr checks --json state", "compatibleTools": [ "claude-code", @@ -30,9 +34,13 @@ "kickoff": "Run the pr-to-green loop on the open PR:\n1. Read every review finding.\n2. Fix the valid ones, push back on the invalid.\n3. Push and re-check until green.", "maxIterations": 20 }, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "official", "status": "published", "downloadCount": 12, + "chargeCount": 3, "createdAt": "2026-05-01T09:00:00.000Z", "updatedAt": "2026-05-02T09:00:00.000Z" } diff --git a/internal/cli/testdata/golden/loop-show.json b/internal/cli/testdata/golden/loop-show.json index 6def097..d838853 100644 --- a/internal/cli/testdata/golden/loop-show.json +++ b/internal/cli/testdata/golden/loop-show.json @@ -4,6 +4,7 @@ "slug": "pr-to-green", "profileHandle": "nsollazzo", "profileName": "Nicholas Sollazzo", + "profileTier": "official", "name": "PR to Green", "type": "loop", "tagline": "Drive a pull request until CI is green and review approves", @@ -18,6 +19,9 @@ "repoUrl": null, "installCmd": null, "data": { + "bundles": [ + "superpowers" + ], "checkCommand": "gh pr checks --json state", "compatibleTools": [ "claude-code", @@ -28,9 +32,13 @@ "kickoff": "Run the pr-to-green loop on the open PR:\n1. Read every review finding.\n2. Fix the valid ones, push back on the invalid.\n3. Push and re-check until green.", "maxIterations": 20 }, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "official", "status": "published", "downloadCount": 12, + "chargeCount": 3, "createdAt": "2026-05-01T09:00:00.000Z", "updatedAt": "2026-05-02T09:00:00.000Z" } diff --git a/internal/cli/testdata/golden/loop-show.txt b/internal/cli/testdata/golden/loop-show.txt index 20fee8c..337edb9 100644 --- a/internal/cli/testdata/golden/loop-show.txt +++ b/internal/cli/testdata/golden/loop-show.txt @@ -11,6 +11,7 @@ SOURCE https://example.com/pr-to-green CONFIDENCE official STATUS published DOWNLOADS 12 +CHARGES 3 CREATED 2026-05-01T09:00:00.000Z UPDATED 2026-05-02T09:00:00.000Z GOAL The PR has a fresh approval, green CI, and is mergeable @@ -18,6 +19,7 @@ CHECK COMMAND gh pr checks --json state EXIT CONDITION All checks pass and the latest review is APPROVED MAX ITERATIONS 20 COMPATIBLE TOOLS claude-code, codex +BUNDLES superpowers KICKOFF Run the pr-to-green loop on the open PR: diff --git a/internal/cli/testdata/golden/mcp-list.json b/internal/cli/testdata/golden/mcp-list.json index c95510f..78f43d5 100644 --- a/internal/cli/testdata/golden/mcp-list.json +++ b/internal/cli/testdata/golden/mcp-list.json @@ -6,6 +6,7 @@ "slug": "grafana-mcp", "profileHandle": "grafana", "profileName": "Grafana Labs", + "profileTier": "official", "name": "Grafana MCP", "type": "mcp", "tagline": "Dashboards, alerts and incidents as agent tools", @@ -19,9 +20,13 @@ "repoUrl": null, "installCmd": null, "data": {}, + "hasAsset": false, + "assetVersion": null, + "assetContentHash": null, "confidence": "official", "status": "published", "downloadCount": 31, + "chargeCount": 2, "createdAt": "2026-03-15T09:00:00.000Z", "updatedAt": "2026-03-15T09:00:00.000Z" } diff --git a/internal/cli/testdata/golden/mcp-tools-list.json b/internal/cli/testdata/golden/mcp-tools-list.json index 7bf2361..9b87f10 100644 --- a/internal/cli/testdata/golden/mcp-tools-list.json +++ b/internal/cli/testdata/golden/mcp-tools-list.json @@ -111,9 +111,24 @@ "listing": { "additionalProperties": false, "properties": { + "assetContentHash": { + "type": [ + "null", + "string" + ] + }, + "assetVersion": { + "type": [ + "null", + "string" + ] + }, "category": { "type": "string" }, + "chargeCount": { + "type": "integer" + }, "confidence": { "type": "string" }, @@ -133,6 +148,9 @@ "downloadCount": { "type": "integer" }, + "hasAsset": { + "type": "boolean" + }, "id": { "type": "string" }, @@ -154,6 +172,12 @@ "profileName": { "type": "string" }, + "profileTier": { + "type": [ + "null", + "string" + ] + }, "repoUrl": { "type": [ "null", @@ -193,6 +217,7 @@ "slug", "profileHandle", "profileName", + "profileTier", "name", "type", "tagline", @@ -204,9 +229,13 @@ "repoUrl", "installCmd", "data", + "hasAsset", + "assetVersion", + "assetContentHash", "confidence", "status", "downloadCount", + "chargeCount", "createdAt", "updatedAt" ], @@ -421,6 +450,9 @@ "category": { "type": "string" }, + "chargeCount": { + "type": "integer" + }, "content": { "type": "string" }, @@ -535,6 +567,7 @@ "contentHash", "status", "downloadCount", + "chargeCount", "ratingAvg", "ratingCount", "arenaRank", diff --git a/internal/cli/testdata/golden/skill-list.json b/internal/cli/testdata/golden/skill-list.json index 6a927df..1d41240 100644 --- a/internal/cli/testdata/golden/skill-list.json +++ b/internal/cli/testdata/golden/skill-list.json @@ -6,6 +6,7 @@ "slug": "superpowers", "profileHandle": "obra", "profileName": "Jesse Vincent", + "profileTier": "verified", "name": "Superpowers", "type": "skill", "tagline": "A methodology pack that upgrades your coding agent", @@ -19,10 +20,18 @@ "sourceUrl": "https://example.com/superpowers", "repoUrl": "https://example.com/obra/superpowers", "installCmd": null, - "data": {}, + "data": { + "bundles": [ + "pr-to-green" + ] + }, + "hasAsset": true, + "assetVersion": "1.0.0", + "assetContentHash": "11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa", "confidence": "official", "status": "published", "downloadCount": 18, + "chargeCount": 6, "createdAt": "2026-04-01T09:00:00.000Z", "updatedAt": "2026-04-02T09:00:00.000Z" } diff --git a/internal/cli/testdata/golden/soul-create.json b/internal/cli/testdata/golden/soul-create.json index d88ba6f..d74fb23 100644 --- a/internal/cli/testdata/golden/soul-create.json +++ b/internal/cli/testdata/golden/soul-create.json @@ -27,6 +27,7 @@ "contentHash": "d275df132265c00d98044454c1819e70c7865eb45275812b4a29ff0a04165f36", "status": "published", "downloadCount": 0, + "chargeCount": 0, "ratingAvg": null, "ratingCount": 0, "arenaRank": null, diff --git a/internal/cli/testdata/golden/soul-list-category.json b/internal/cli/testdata/golden/soul-list-category.json index 8257e6a..c0bfdc3 100644 --- a/internal/cli/testdata/golden/soul-list-category.json +++ b/internal/cli/testdata/golden/soul-list-category.json @@ -31,6 +31,7 @@ "contentHash": "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", "status": "published", "downloadCount": 42, + "chargeCount": 5, "ratingAvg": 4.5, "ratingCount": 8, "arenaRank": 2, diff --git a/internal/cli/testdata/golden/soul-list-newest.json b/internal/cli/testdata/golden/soul-list-newest.json index e629ab0..261cde8 100644 --- a/internal/cli/testdata/golden/soul-list-newest.json +++ b/internal/cli/testdata/golden/soul-list-newest.json @@ -29,6 +29,7 @@ "contentHash": "cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333", "status": "published", "downloadCount": 99, + "chargeCount": 11, "ratingAvg": 4.9, "ratingCount": 21, "arenaRank": 1, @@ -59,6 +60,7 @@ "contentHash": "bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222", "status": "published", "downloadCount": 7, + "chargeCount": 0, "ratingAvg": null, "ratingCount": 0, "arenaRank": null, @@ -95,6 +97,7 @@ "contentHash": "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", "status": "published", "downloadCount": 42, + "chargeCount": 5, "ratingAvg": 4.5, "ratingCount": 8, "arenaRank": 2, diff --git a/internal/cli/testdata/golden/soul-list.json b/internal/cli/testdata/golden/soul-list.json index e43ebae..a27b9b0 100644 --- a/internal/cli/testdata/golden/soul-list.json +++ b/internal/cli/testdata/golden/soul-list.json @@ -29,6 +29,7 @@ "contentHash": "cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333", "status": "published", "downloadCount": 99, + "chargeCount": 11, "ratingAvg": 4.9, "ratingCount": 21, "arenaRank": 1, @@ -65,6 +66,7 @@ "contentHash": "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", "status": "published", "downloadCount": 42, + "chargeCount": 5, "ratingAvg": 4.5, "ratingCount": 8, "arenaRank": 2, @@ -95,6 +97,7 @@ "contentHash": "bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222", "status": "published", "downloadCount": 7, + "chargeCount": 0, "ratingAvg": null, "ratingCount": 0, "arenaRank": null, diff --git a/internal/cli/testdata/golden/soul-search-downloads.json b/internal/cli/testdata/golden/soul-search-downloads.json index 5854cc0..e8612b8 100644 --- a/internal/cli/testdata/golden/soul-search-downloads.json +++ b/internal/cli/testdata/golden/soul-search-downloads.json @@ -30,6 +30,7 @@ "contentHash": "cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333", "status": "published", "downloadCount": 99, + "chargeCount": 11, "ratingAvg": 4.9, "ratingCount": 21, "arenaRank": 1, @@ -66,6 +67,7 @@ "contentHash": "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", "status": "published", "downloadCount": 42, + "chargeCount": 5, "ratingAvg": 4.5, "ratingCount": 8, "arenaRank": 2, diff --git a/internal/cli/testdata/golden/soul-search.json b/internal/cli/testdata/golden/soul-search.json index 776456f..294d84b 100644 --- a/internal/cli/testdata/golden/soul-search.json +++ b/internal/cli/testdata/golden/soul-search.json @@ -30,6 +30,7 @@ "contentHash": "cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333", "status": "published", "downloadCount": 99, + "chargeCount": 11, "ratingAvg": 4.9, "ratingCount": 21, "arenaRank": 1, @@ -60,6 +61,7 @@ "contentHash": "bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222", "status": "published", "downloadCount": 7, + "chargeCount": 0, "ratingAvg": null, "ratingCount": 0, "arenaRank": null, @@ -96,6 +98,7 @@ "contentHash": "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", "status": "published", "downloadCount": 42, + "chargeCount": 5, "ratingAvg": 4.5, "ratingCount": 8, "arenaRank": 2, diff --git a/internal/cli/testdata/golden/soul-show.json b/internal/cli/testdata/golden/soul-show.json index 9121744..f743b54 100644 --- a/internal/cli/testdata/golden/soul-show.json +++ b/internal/cli/testdata/golden/soul-show.json @@ -29,6 +29,7 @@ "contentHash": "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", "status": "published", "downloadCount": 42, + "chargeCount": 5, "ratingAvg": 4.5, "ratingCount": 8, "arenaRank": 2, diff --git a/internal/cli/testdata/golden/soul-show.txt b/internal/cli/testdata/golden/soul-show.txt index 21a44a9..d013136 100644 --- a/internal/cli/testdata/golden/soul-show.txt +++ b/internal/cli/testdata/golden/soul-show.txt @@ -11,6 +11,7 @@ VERSION 1.2.0 LICENSE MIT REPO https://example.com/souls/sherlock DOWNLOADS 42 +CHARGES 5 STATUS published CREATED 2026-01-05T09:00:00.000Z UPDATED 2026-01-06T09:00:00.000Z diff --git a/internal/cli/testdata/golden/soul-update-ownership.json b/internal/cli/testdata/golden/soul-update-ownership.json index 9249539..f16c0d1 100644 --- a/internal/cli/testdata/golden/soul-update-ownership.json +++ b/internal/cli/testdata/golden/soul-update-ownership.json @@ -29,6 +29,7 @@ "contentHash": "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", "status": "published", "downloadCount": 42, + "chargeCount": 5, "ratingAvg": 4.5, "ratingCount": 8, "arenaRank": 2, diff --git a/internal/mockapi/mockapi.go b/internal/mockapi/mockapi.go index 77730f1..5d0dfbe 100644 --- a/internal/mockapi/mockapi.go +++ b/internal/mockapi/mockapi.go @@ -71,6 +71,7 @@ var Souls = []api.Soul{ ContentHash: "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111", Status: "published", DownloadCount: 42, + ChargeCount: 5, RatingAvg: ptr(4.5), RatingCount: 8, ArenaRank: ptr(2), @@ -100,6 +101,7 @@ var Souls = []api.Soul{ ContentHash: "bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222", Status: "published", DownloadCount: 7, + ChargeCount: 0, RatingAvg: nil, RatingCount: 0, ArenaRank: nil, @@ -129,6 +131,7 @@ var Souls = []api.Soul{ ContentHash: "cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333", Status: "published", DownloadCount: 99, + ChargeCount: 11, RatingAvg: ptr(4.9), RatingCount: 21, ArenaRank: ptr(1), @@ -145,98 +148,119 @@ var Souls = []api.Soul{ // pinned too. var Listings = []api.Listing{ { - ID: "01LSTCLAUDECODE000000000XX", - Slug: "claude-code", - ProfileHandle: "anthropic", - ProfileName: "Anthropic", - Name: "Claude Code", - Type: "harness", - Tagline: "Agentic coding in your terminal", - Description: ptr("Anthropic's agentic coding harness."), - Category: "AI/ML", - Tags: []string{"agentic", "terminal"}, - Official: true, - SourceURL: "https://example.com/claude-code", - RepoURL: ptr("https://example.com/anthropics/claude-code"), - InstallCmd: ptr("npm install -g @anthropic-ai/claude-code"), - Data: map[string]any{}, - Confidence: "official", - Status: "published", - DownloadCount: 120, - CreatedAt: "2026-01-10T09:00:00.000Z", - UpdatedAt: "2026-01-12T09:00:00.000Z", + ID: "01LSTCLAUDECODE000000000XX", + Slug: "claude-code", + ProfileHandle: "anthropic", + ProfileName: "Anthropic", + ProfileTier: ptr("official"), + Name: "Claude Code", + Type: "harness", + Tagline: "Agentic coding in your terminal", + Description: ptr("Anthropic's agentic coding harness."), + Category: "AI/ML", + Tags: []string{"agentic", "terminal"}, + Official: true, + SourceURL: "https://example.com/claude-code", + RepoURL: ptr("https://example.com/anthropics/claude-code"), + InstallCmd: ptr("npm install -g @anthropic-ai/claude-code"), + Data: map[string]any{}, + HasAsset: false, + AssetVersion: nil, + AssetContentHash: nil, + Confidence: "official", + Status: "published", + DownloadCount: 120, + ChargeCount: 9, + CreatedAt: "2026-01-10T09:00:00.000Z", + UpdatedAt: "2026-01-12T09:00:00.000Z", }, { - ID: "01LSTGITHUBCLI0000000000XX", - Slug: "github-cli", - ProfileHandle: "github", - ProfileName: "GitHub", - Name: "GitHub CLI", - Type: "cli", - Tagline: "GitHub from the command line", - Description: ptr("Work with issues, PRs and releases from the shell."), - Category: "DevOps", - Tags: []string{"git", "github"}, - Official: true, - SourceURL: "https://example.com/cli", - RepoURL: ptr("https://example.com/cli/cli"), - InstallCmd: ptr("brew install gh"), - Data: map[string]any{}, - Confidence: "official", - Status: "published", - DownloadCount: 64, - CreatedAt: "2026-02-01T09:00:00.000Z", - UpdatedAt: "2026-02-03T09:00:00.000Z", + ID: "01LSTGITHUBCLI0000000000XX", + Slug: "github-cli", + ProfileHandle: "github", + ProfileName: "GitHub", + ProfileTier: ptr("official"), + Name: "GitHub CLI", + Type: "cli", + Tagline: "GitHub from the command line", + Description: ptr("Work with issues, PRs and releases from the shell."), + Category: "DevOps", + Tags: []string{"git", "github"}, + Official: true, + SourceURL: "https://example.com/cli", + RepoURL: ptr("https://example.com/cli/cli"), + InstallCmd: ptr("brew install gh"), + Data: map[string]any{}, + HasAsset: false, + AssetVersion: nil, + AssetContentHash: nil, + Confidence: "official", + Status: "published", + DownloadCount: 64, + ChargeCount: 4, + CreatedAt: "2026-02-01T09:00:00.000Z", + UpdatedAt: "2026-02-03T09:00:00.000Z", }, { - ID: "01LSTGRAFANAMCP000000000XX", - Slug: "grafana-mcp", - ProfileHandle: "grafana", - ProfileName: "Grafana Labs", - Name: "Grafana MCP", - Type: "mcp", - Tagline: "Dashboards, alerts and incidents as agent tools", - Description: nil, - Category: "DevOps", - Tags: []string{"observability"}, - Official: true, - SourceURL: "https://example.com/grafana-mcp", - RepoURL: nil, - InstallCmd: nil, - Data: map[string]any{}, - Confidence: "official", - Status: "published", - DownloadCount: 31, - CreatedAt: "2026-03-15T09:00:00.000Z", - UpdatedAt: "2026-03-15T09:00:00.000Z", + ID: "01LSTGRAFANAMCP000000000XX", + Slug: "grafana-mcp", + ProfileHandle: "grafana", + ProfileName: "Grafana Labs", + ProfileTier: ptr("official"), + Name: "Grafana MCP", + Type: "mcp", + Tagline: "Dashboards, alerts and incidents as agent tools", + Description: nil, + Category: "DevOps", + Tags: []string{"observability"}, + Official: true, + SourceURL: "https://example.com/grafana-mcp", + RepoURL: nil, + InstallCmd: nil, + Data: map[string]any{}, + HasAsset: false, + AssetVersion: nil, + AssetContentHash: nil, + Confidence: "official", + Status: "published", + DownloadCount: 31, + ChargeCount: 2, + CreatedAt: "2026-03-15T09:00:00.000Z", + UpdatedAt: "2026-03-15T09:00:00.000Z", }, { - ID: "01LSTSUPERPOWERS00000000XX", - Slug: "superpowers", - ProfileHandle: "obra", - ProfileName: "Jesse Vincent", - Name: "Superpowers", - Type: "skill", - Tagline: "A methodology pack that upgrades your coding agent", - Description: ptr("Skills for planning, debugging and shipping."), - Category: "AI/ML", - Tags: []string{"skills", "methodology"}, - Official: true, - SourceURL: "https://example.com/superpowers", - RepoURL: ptr("https://example.com/obra/superpowers"), - InstallCmd: nil, - Data: map[string]any{}, - Confidence: "official", - Status: "published", - DownloadCount: 18, - CreatedAt: "2026-04-01T09:00:00.000Z", - UpdatedAt: "2026-04-02T09:00:00.000Z", + ID: "01LSTSUPERPOWERS00000000XX", + Slug: "superpowers", + ProfileHandle: "obra", + ProfileName: "Jesse Vincent", + ProfileTier: ptr("verified"), + Name: "Superpowers", + Type: "skill", + Tagline: "A methodology pack that upgrades your coding agent", + Description: ptr("Skills for planning, debugging and shipping."), + Category: "AI/ML", + Tags: []string{"skills", "methodology"}, + Official: true, + SourceURL: "https://example.com/superpowers", + RepoURL: ptr("https://example.com/obra/superpowers"), + InstallCmd: nil, + Data: map[string]any{"bundles": []any{"pr-to-green"}}, + HasAsset: true, + AssetVersion: ptr("1.0.0"), + AssetContentHash: ptr("11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa"), + Confidence: "official", + Status: "published", + DownloadCount: 18, + ChargeCount: 6, + CreatedAt: "2026-04-01T09:00:00.000Z", + UpdatedAt: "2026-04-02T09:00:00.000Z", }, { ID: "01LSTPRTOGREEN0000000000XX", Slug: "pr-to-green", ProfileHandle: "nsollazzo", ProfileName: "Nicholas Sollazzo", + ProfileTier: ptr("official"), Name: "PR to Green", Type: "loop", Tagline: "Drive a pull request until CI is green and review approves", @@ -254,12 +278,17 @@ var Listings = []api.Listing{ "maxIterations": 20, "compatibleTools": []any{"claude-code", "codex"}, "kickoff": "Run the pr-to-green loop on the open PR:\n1. Read every review finding.\n2. Fix the valid ones, push back on the invalid.\n3. Push and re-check until green.", + "bundles": []any{"superpowers"}, }, - Confidence: "official", - Status: "published", - DownloadCount: 12, - CreatedAt: "2026-05-01T09:00:00.000Z", - UpdatedAt: "2026-05-02T09:00:00.000Z", + HasAsset: false, + AssetVersion: nil, + AssetContentHash: nil, + Confidence: "official", + Status: "published", + DownloadCount: 12, + ChargeCount: 3, + CreatedAt: "2026-05-01T09:00:00.000Z", + UpdatedAt: "2026-05-02T09:00:00.000Z", }, }