diff --git a/packages/mcp/src/__tests__/plugin-manifest.test.ts b/packages/mcp/src/__tests__/plugin-manifest.test.ts index 1067625..d60d7f6 100644 --- a/packages/mcp/src/__tests__/plugin-manifest.test.ts +++ b/packages/mcp/src/__tests__/plugin-manifest.test.ts @@ -263,3 +263,31 @@ describe('agent plugin text matches what the server does', () => { for (const name of names) expect(readme, `README should name ${name}`).toContain(`\`${name}\``) }) }) + +// Anthropic's directory reads these from .claude-plugin/plugin.json for the listing +// (Claude Code itself ignores them); the Codex app shows the same artwork. +describe('directory listing fields', () => { + const pngSize = (path: string) => { + const bytes = readFileSync(path) + expect(bytes.subarray(0, 8).toString('hex'), `${path} is not a PNG`).toBe('89504e470d0a1a0a') + return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) } + } + + it('points icon at a square PNG of at least 512px inside the plugin', () => { + expect(claudeManifest.icon).toBe('./assets/icon.png') + const { width, height } = pngSize(join(PLUGIN, claudeManifest.icon)) + expect(width).toBe(height) + expect(width).toBeGreaterThanOrEqual(512) + }) + + it('links documentation and support over https', () => { + expect(claudeManifest.documentationUrl).toBe('https://github.com/refkitjs/refkit/tree/main/plugins/refkit#readme') + expect(claudeManifest.supportUrl).toBe('https://github.com/refkitjs/refkit/issues') + }) + + it('gives the Codex app the same artwork', () => { + expect(codexManifest.interface.logo).toBe(claudeManifest.icon) + expect(codexManifest.interface.composerIcon).toBe('./assets/icon.svg') + expect(existsSync(join(PLUGIN, codexManifest.interface.composerIcon))).toBe(true) + }) +}) diff --git a/plugins/refkit/.claude-plugin/plugin.json b/plugins/refkit/.claude-plugin/plugin.json index 816d1da..ead07e1 100644 --- a/plugins/refkit/.claude-plugin/plugin.json +++ b/plugins/refkit/.claude-plugin/plugin.json @@ -17,5 +17,8 @@ "creative-commons", "public-domain", "moodboard" - ] + ], + "icon": "./assets/icon.png", + "documentationUrl": "https://github.com/refkitjs/refkit/tree/main/plugins/refkit#readme", + "supportUrl": "https://github.com/refkitjs/refkit/issues" } diff --git a/plugins/refkit/.codex-plugin/plugin.json b/plugins/refkit/.codex-plugin/plugin.json index 7df5a69..64cb870 100644 --- a/plugins/refkit/.codex-plugin/plugin.json +++ b/plugins/refkit/.codex-plugin/plugin.json @@ -34,6 +34,8 @@ "Can I use this Wikimedia Commons photo in an ad?" ], "websiteURL": "https://github.com/refkitjs/refkit", - "brandColor": "#0F172A" + "brandColor": "#0F172A", + "composerIcon": "./assets/icon.svg", + "logo": "./assets/icon.png" } } diff --git a/plugins/refkit/assets/icon.png b/plugins/refkit/assets/icon.png new file mode 100644 index 0000000..9f8c2dd Binary files /dev/null and b/plugins/refkit/assets/icon.png differ diff --git a/plugins/refkit/assets/icon.svg b/plugins/refkit/assets/icon.svg new file mode 100644 index 0000000..502ba18 --- /dev/null +++ b/plugins/refkit/assets/icon.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + unsplash CC-BY-SA-3.0 CC-BY-NC-ND CC-BY-4.0 PD CC0-1.0 CC0 CC-BY-NC-SA + CC-BY-ND-4.0 CC-BY-SA-3.0 CC-BY-NC-4.0 CC-BY-ND-4.0 CC-BY-NC-SA + CC-BY-NC-4.0 CC-BY-ND-4.0 proprietary CC0-1.0 CC-BY-NC-4.0 unsplash + unsplash unknown CC-BY-4.0 CC-BY-NC-SA proprietary CC-BY-SA-4.0 + CC-BY-NC-4.0 PD PDM-1.0 CC-BY-2.0 CC-BY-NC-4.0 CC-BY-NC-ND pixabay + pixabay CC-BY-SA-4.0 pixabay proprietary CC-BY-4.0 CC-BY-NC-SA + pexels PDM-1.0 CC-BY-ND-4.0 CC-BY-4.0 CC-BY-SA-4.0 unsplash + CC-BY-SA-3.0 PDM-1.0 CC-BY-ND-4.0 CC0-1.0 CC-BY-NC-4.0 CC-BY-SA-4.0 + PD unknown CC-BY-SA-4.0 CC0 CC-BY-4.0 unsplash CC-BY-4.0 pexels + unknown CC-BY-4.0 PD CC0-1.0 CC-BY-NC-ND CC-BY-SA-3.0 PDM-1.0 + PDM-1.0 unsplash CC-BY-4.0 CC-BY-SA-3.0 proprietary CC0-1.0 + CC-BY-NC-4.0 CC0-1.0 CC-BY-SA-3.0 CC-BY-4.0 PDM-1.0 CC0-1.0 + PDM-1.0 CC-BY-3.0 pixabay CC0-1.0 proprietary CC-BY-2.0 CC-BY-ND-4.0 + CC-BY-2.0 CC0 PDM-1.0 unknown CC-BY-3.0 CC-BY-2.0 PDM-1.0 pexels + CC0 unsplash CC-BY-NC-ND CC-BY-NC-SA PDM-1.0 CC-BY-NC-SA proprietary + CC-BY-SA-3.0 proprietary CC-BY-NC-SA CC0-1.0 CC-BY-4.0 proprietary + CC-BY-3.0 CC0 PDM-1.0 PD CC-BY-NC-4.0 unknown pixabay CC0 CC-BY-NC-SA + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/build-plugin-icon.mts b/scripts/build-plugin-icon.mts new file mode 100644 index 0000000..3a1be43 --- /dev/null +++ b/scripts/build-plugin-icon.mts @@ -0,0 +1,20 @@ +// Build plugins/refkit/assets/icon.png from icon.svg next to it. The PNG is what +// Anthropic's plugin directory and the Codex app show; the SVG is the source and +// the Codex composer icon. Re-run after editing the SVG: +// +// npx tsx scripts/build-plugin-icon.mts +import { readFileSync, writeFileSync } from 'node:fs' + +const assets = new URL('../plugins/refkit/assets/', import.meta.url) +const svg = readFileSync(new URL('icon.svg', assets), 'utf8') + +// ---- rasterise to PNG (pure Rust, no browser) ---- +const { Resvg } = await import('@resvg/resvg-js') +const png = new Resvg(svg, { + fitTo: { mode: 'width', value: 512 }, + font: { loadSystemFonts: true, defaultFontFamily: 'Menlo' }, // the license-id background texture is monospace text +}) + .render() + .asPng() +writeFileSync(new URL('icon.png', assets), png) +console.log(`wrote plugins/refkit/assets/icon.png (${(png.length / 1024).toFixed(0)} KB, 512px)`)