From b5890d3d9a4d7f4abaf81883999edd4e82a409f2 Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Thu, 16 Jul 2026 22:01:45 -0700 Subject: [PATCH] feat: include listing metadata in catalog feeds (#3135) --- convex/catalogFeed.test.ts | 20 ++++++++++++++++---- convex/catalogFeed.ts | 12 ++++++++++++ packages/schema/src/catalogFeed.test.ts | 22 ++++++++++++++++++++++ packages/schema/src/catalogFeed.ts | 4 ++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/convex/catalogFeed.test.ts b/convex/catalogFeed.test.ts index baa950ba..0aa210a5 100644 --- a/convex/catalogFeed.test.ts +++ b/convex/catalogFeed.test.ts @@ -191,9 +191,17 @@ describe("catalog feed projection", () => { it("projects official releases into ClawHub install candidates", async () => { const result = await listOfficialEntriesHandler( - makeCtx([makePackage()], { - "packageReleases:1": makeRelease(), - }), + makeCtx( + [ + makePackage({ + summary: "Search flights, stays, and travel options.", + icon: "https://cdn.example.test/expedia.png", + }), + ], + { + "packageReleases:1": makeRelease(), + }, + ), { family: "code-plugin" }, ); @@ -202,6 +210,8 @@ describe("catalog feed projection", () => { type: "plugin", id: "@openclaw/demo", title: "Demo", + description: "Search flights, stays, and travel options.", + icon: "https://cdn.example.test/expedia.png", version: "1.2.3", state: "available", featured: false, @@ -296,7 +306,7 @@ describe("catalog feed projection", () => { it("projects only published skills from verified organization publishers", async () => { const result = (await listOfficialSkillEntriesHandler( - makeCtx([makeSkill()], { + makeCtx([makeSkill({ summary: "Deploy AIQ services.", icon: "lucide:rocket" })], { "publishers:1": { _id: "publishers:1", kind: "org", handle: "openclaw" }, "skillVersions:1": makeSkillVersion(), }), @@ -309,6 +319,8 @@ describe("catalog feed projection", () => { type: "skill", id: "@openclaw/demo", title: "Demo skill", + description: "Deploy AIQ services.", + icon: "lucide:rocket", version: "1.2.3", state: "available", featured: false, diff --git a/convex/catalogFeed.ts b/convex/catalogFeed.ts index 9bd884b5..ed602678 100644 --- a/convex/catalogFeed.ts +++ b/convex/catalogFeed.ts @@ -54,6 +54,8 @@ function appendEntriesWithinFeedLimit(target: T[], entries: T[]) { const catalogFeedEntryFields = { id: v.string(), title: v.string(), + description: v.optional(v.string()), + icon: v.optional(v.string()), version: v.string(), state: v.union( v.literal("available"), @@ -117,6 +119,8 @@ async function buildEntry( const packageName = pkg.name.trim(); const id = pkg.normalizedName.trim(); const title = pkg.displayName.trim() || packageName; + const description = pkg.summary?.trim(); + const icon = pkg.icon?.trim(); const version = release.version.trim(); if (!packageName || !id || !title || !version) return null; const highlighted = await ctx.db @@ -128,6 +132,8 @@ async function buildEntry( type: "plugin", id, title, + ...(description ? { description } : {}), + ...(icon ? { icon } : {}), version, state: "available", featured: Boolean(highlighted), @@ -202,6 +208,8 @@ async function buildSkillEntry( const publisherId = owner.handle?.trim(); const slug = skill.slug.trim(); const title = skill.displayName.trim() || slug; + const description = skill.summary?.trim(); + const icon = skill.icon?.trim(); const packageName = `@${publisherId}/${slug}`; if (!publisherId || !slug || !title) return null; @@ -230,6 +238,8 @@ async function buildSkillEntry( type: "skill", id: packageName, title, + ...(description ? { description } : {}), + ...(icon ? { icon } : {}), version: commit, state: "available", featured: isSkillHighlighted(skill), @@ -275,6 +285,8 @@ async function buildSkillEntry( type: "skill", id: packageName, title, + ...(description ? { description } : {}), + ...(icon ? { icon } : {}), version: versionName, state: "available", featured: isSkillHighlighted(skill), diff --git a/packages/schema/src/catalogFeed.test.ts b/packages/schema/src/catalogFeed.test.ts index 44d60e3c..7c0eb590 100644 --- a/packages/schema/src/catalogFeed.test.ts +++ b/packages/schema/src/catalogFeed.test.ts @@ -162,6 +162,28 @@ describe("catalog feed schema", () => { expect(parseCatalogFeed(makeFeed()).entries[0]).not.toHaveProperty("featured"); }); + it("round-trips optional listing metadata without changing schema version 1", () => { + const feed = makeFeed({ + entries: [ + { + ...makeFeed().entries[0], + description: "Search flights, stays, and travel options.", + icon: "https://cdn.example.test/expedia.png", + }, + ], + }); + + const parsed = parseCatalogFeed(JSON.parse(serializeCatalogFeed(feed))); + + expect(parsed.schemaVersion).toBe(1); + expect(parsed.entries[0]).toMatchObject({ + description: "Search flights, stays, and travel options.", + icon: "https://cdn.example.test/expedia.png", + }); + expect(parseCatalogFeed(makeFeed()).entries[0]).not.toHaveProperty("description"); + expect(parseCatalogFeed(makeFeed()).entries[0]).not.toHaveProperty("icon"); + }); + it("preserves public GitHub source identity on skill candidates", () => { const feed = makeFeed({ id: CATALOG_SKILLS_FEED_ID, diff --git a/packages/schema/src/catalogFeed.ts b/packages/schema/src/catalogFeed.ts index df65046e..2c563cca 100644 --- a/packages/schema/src/catalogFeed.ts +++ b/packages/schema/src/catalogFeed.ts @@ -31,6 +31,8 @@ const CatalogFeedEntryBaseSchema = { "+": "reject", id: "string", title: "string", + description: "string?", + icon: "string?", version: "string", state: CatalogFeedStateSchema, // Additive v1 metadata: existing hosted-feed consumers ignore unknown entry fields. @@ -116,6 +118,8 @@ export function serializeCatalogFeed(feed: CatalogFeed): string { type: entry.type, id: entry.id, title: entry.title, + ...(entry.description === undefined ? {} : { description: entry.description }), + ...(entry.icon === undefined ? {} : { icon: entry.icon }), version: entry.version, state: entry.state, ...(entry.featured === undefined ? {} : { featured: entry.featured }),