From 82932feabe1eca8ab2419ba0fe89ea55d973e14f Mon Sep 17 00:00:00 2001 From: Michael Dougall <6801309+itsdouges@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:33:30 +1000 Subject: [PATCH 1/5] Switch Paper MCP from localhost HTTP to stdio via app-data CLI. Agent harnesses can load the plugin without Paper Desktop already listening on :29979. Co-authored-by: Cursor --- plugins/paper-desktop/.claude-plugin/plugin.json | 3 +-- plugins/paper-desktop/.codex-plugin/plugin.json | 3 +-- plugins/paper-desktop/.cursor-plugin/plugin.json | 2 +- plugins/paper-desktop/README.md | 2 +- plugins/paper-desktop/mcp.json | 4 ++-- plugins/paper-desktop/rules/ensure-paper-started.md | 5 ----- plugins/paper-desktop/skills/code-to-design/SKILL.md | 8 -------- plugins/paper-desktop/skills/design-to-code/SKILL.md | 8 -------- 8 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 plugins/paper-desktop/rules/ensure-paper-started.md delete mode 100644 plugins/paper-desktop/skills/code-to-design/SKILL.md delete mode 100644 plugins/paper-desktop/skills/design-to-code/SKILL.md diff --git a/plugins/paper-desktop/.claude-plugin/plugin.json b/plugins/paper-desktop/.claude-plugin/plugin.json index bdb45a5..dd80a5d 100644 --- a/plugins/paper-desktop/.claude-plugin/plugin.json +++ b/plugins/paper-desktop/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "paper-desktop", - "version": "0.1.0", + "version": "0.2.0", "description": "Design on a canvas that Claude can read and write to — built on web standards.", "author": { "name": "Paper", @@ -21,6 +21,5 @@ "mcp", "design-system" ], - "skills": "./skills", "mcpServers": "./mcp.json" } diff --git a/plugins/paper-desktop/.codex-plugin/plugin.json b/plugins/paper-desktop/.codex-plugin/plugin.json index 18d14da..9234f81 100644 --- a/plugins/paper-desktop/.codex-plugin/plugin.json +++ b/plugins/paper-desktop/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "paper-desktop", - "version": "0.1.0", + "version": "0.2.0", "description": "Design on a canvas that Codex can read and write to — built on web standards.", "author": { "name": "Paper", @@ -23,7 +23,6 @@ "mcp", "design-system" ], - "skills": "./skills/", "mcpServers": "./mcp.json", "interface": { "displayName": "Paper Desktop", diff --git a/plugins/paper-desktop/.cursor-plugin/plugin.json b/plugins/paper-desktop/.cursor-plugin/plugin.json index f36e0ef..2833fb3 100644 --- a/plugins/paper-desktop/.cursor-plugin/plugin.json +++ b/plugins/paper-desktop/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "paper-desktop", "displayName": "Paper", - "version": "0.1.0", + "version": "0.2.0", "description": "Design on a canvas that Cursor can read and write to — built on web standards.", "author": { "name": "Paper", diff --git a/plugins/paper-desktop/README.md b/plugins/paper-desktop/README.md index c285d22..a718093 100644 --- a/plugins/paper-desktop/README.md +++ b/plugins/paper-desktop/README.md @@ -14,7 +14,7 @@ Paper connects your designs, agents, code, and data on a single canvas built on ## Prerequisites -Paper Desktop must be running with a file open for the MCP server to be available. Download it at [paper.design/downloads](https://paper.design/downloads). +Install [Paper Desktop](https://paper.design/downloads) and open it once. ## Examples diff --git a/plugins/paper-desktop/mcp.json b/plugins/paper-desktop/mcp.json index 034d089..919252f 100644 --- a/plugins/paper-desktop/mcp.json +++ b/plugins/paper-desktop/mcp.json @@ -1,8 +1,8 @@ { "mcpServers": { "paper": { - "type": "http", - "url": "http://127.0.0.1:29979/mcp" + "command": "${userHome}/.paper/bin/paper", + "args": ["mcp"] } } } diff --git a/plugins/paper-desktop/rules/ensure-paper-started.md b/plugins/paper-desktop/rules/ensure-paper-started.md deleted file mode 100644 index 71eff56..0000000 --- a/plugins/paper-desktop/rules/ensure-paper-started.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -description: Guidelines for using the Paper MCP server ---- - -Before using any Paper MCP tools, ensure the Paper Desktop app is running on the user's machine. If a connection to the Paper MCP server fails, remind the user to open Paper Desktop first. diff --git a/plugins/paper-desktop/skills/code-to-design/SKILL.md b/plugins/paper-desktop/skills/code-to-design/SKILL.md deleted file mode 100644 index 2ba0ace..0000000 --- a/plugins/paper-desktop/skills/code-to-design/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: code-to-design -description: Generate a Paper design from the project's codebase — using its tokens, styles, and components as context. ---- - -Before starting, ensure Paper Desktop is running with a file open. If a connection to the Paper MCP server fails, remind the user to open Paper Desktop first. - -Read the project's stylesheets, design tokens, or theme files to understand the existing design language. Then create a new artboard in Paper and build the requested UI using the codebase's actual colors, typography, spacing, and component patterns — not generic defaults. diff --git a/plugins/paper-desktop/skills/design-to-code/SKILL.md b/plugins/paper-desktop/skills/design-to-code/SKILL.md deleted file mode 100644 index d3b68a3..0000000 --- a/plugins/paper-desktop/skills/design-to-code/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: design-to-code -description: Turn a Paper design into production code using the project's existing conventions. ---- - -Before starting, ensure Paper Desktop is running with a file open. If a connection to the Paper MCP server fails, remind the user to open Paper Desktop first. - -Read the selected frame or artboard from Paper — including structure, styles, text content, and computed layout. Then generate production-ready components in the project's framework and coding style. Match the design's layout, spacing, typography, and colors using the codebase's existing conventions (e.g. Tailwind classes, CSS modules, design tokens, styled-components). From 98afba04f47f90de869bb49ff8e0ec2247215a98 Mon Sep 17 00:00:00 2001 From: Michael Dougall <6801309+itsdouges@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:55:30 +1000 Subject: [PATCH 2/5] chore: use HOME --- plugins/paper-desktop/mcp.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/paper-desktop/mcp.json b/plugins/paper-desktop/mcp.json index 919252f..794d7b0 100644 --- a/plugins/paper-desktop/mcp.json +++ b/plugins/paper-desktop/mcp.json @@ -1,7 +1,7 @@ { "mcpServers": { "paper": { - "command": "${userHome}/.paper/bin/paper", + "command": "${HOME}/.paper/bin/paper", "args": ["mcp"] } } From bfc0c98776cff0a4e82c2c05325cdd470fea8e89 Mon Sep 17 00:00:00 2001 From: Michael Dougall <6801309+itsdouges@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:00:08 +1000 Subject: [PATCH 3/5] chore: remove codex --- .agents/plugins/marketplace.json | 20 - README.md | 18 - package.json | 2 +- .../paper-desktop/.codex-plugin/plugin.json | 50 --- scripts/validate-codex.mjs | 364 ------------------ 5 files changed, 1 insertion(+), 453 deletions(-) delete mode 100644 .agents/plugins/marketplace.json delete mode 100644 plugins/paper-desktop/.codex-plugin/plugin.json delete mode 100644 scripts/validate-codex.mjs diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json deleted file mode 100644 index 17ba18f..0000000 --- a/.agents/plugins/marketplace.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "paper", - "interface": { - "displayName": "Paper" - }, - "plugins": [ - { - "name": "paper-desktop", - "source": { - "source": "local", - "path": "./plugins/paper-desktop" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Design" - } - ] -} diff --git a/README.md b/README.md index e2e0a9d..52adc98 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,3 @@ Are we missing one, you have feedback for us, or just want to chat? Join us on o ```sh /plugin install paper-desktop@paper ``` - -## Codex - -**Add the custom marketplace** - -```sh -codex plugin marketplace add paper-design/agent-plugins -``` - -**Install the plugin** - -```sh -codex plugin install paper-desktop@paper -``` - -You can also browse and install plugins interactively by running `/plugins` inside Codex CLI after adding the marketplace. - -- [Read more about installing Codex plugins](https://developers.openai.com/codex/plugins) diff --git a/package.json b/package.json index 4448776..e30a069 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "private": true, "scripts": { - "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs && bun ./scripts/validate-codex.mjs" + "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs" }, "devDependencies": { "@types/bun": "latest", diff --git a/plugins/paper-desktop/.codex-plugin/plugin.json b/plugins/paper-desktop/.codex-plugin/plugin.json deleted file mode 100644 index 9234f81..0000000 --- a/plugins/paper-desktop/.codex-plugin/plugin.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "paper-desktop", - "version": "0.2.0", - "description": "Design on a canvas that Codex can read and write to — built on web standards.", - "author": { - "name": "Paper", - "email": "team@paper.design", - "url": "https://paper.design" - }, - "homepage": "https://paper.design", - "repository": "https://github.com/paper-design/agent-plugins.git", - "license": "MIT", - "keywords": [ - "paper", - "design", - "ui", - "canvas", - "html", - "css", - "design-to-code", - "code-to-design", - "prototyping", - "mcp", - "design-system" - ], - "mcpServers": "./mcp.json", - "interface": { - "displayName": "Paper Desktop", - "shortDescription": "Design on a canvas that Codex can read and write to.", - "longDescription": "Paper is a professional design tool built on HTML and CSS. With this plugin, Codex can read your designs — structure, styles, screenshots, and text — and write back to the canvas in real time. Use it to turn designs into code, generate UI from your codebase, pull in real content from connected data sources, and keep your designs in sync with the tools your team already uses.", - "developerName": "Paper", - "category": "Design", - "capabilities": ["Read", "Write"], - "websiteURL": "https://paper.design", - "privacyPolicyURL": "https://paper.design/privacy", - "brandColor": "#81ADEC", - "composerIcon": "./assets/logo.svg", - "logo": "./assets/logo.svg", - "defaultPrompt": [ - "Use the CSS styles from my repo and design a settings page in Paper", - "Implement my design from Paper in my codebase, using my code conventions", - "Grab my tokens from Figma and create a design system on the canvas in Paper" - ], - "screenshots": [ - "./assets/barley-layout.png", - "./assets/barley-feedback.png", - "./assets/barley-responsive-frames.png" - ] - } -} diff --git a/scripts/validate-codex.mjs b/scripts/validate-codex.mjs deleted file mode 100644 index ff3cba7..0000000 --- a/scripts/validate-codex.mjs +++ /dev/null @@ -1,364 +0,0 @@ -#!/usr/bin/env node - -/** - * Codex plugin validator — ported from codex-rs/core/src/plugins/marketplace.rs - * and codex-rs/core/src/plugins/manifest.rs - */ - -import { readFileSync, existsSync, readdirSync, statSync } from "fs"; -import { resolve, dirname, basename, relative } from "path"; -import { fileURLToPath } from "url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const root = resolve(__dirname, ".."); - -let errors = 0; -let warnings = 0; - -function fail(message) { - console.error(`ERROR: ${message}`); - errors++; -} - -function warn(message) { - console.warn(`WARN: ${message}`); - warnings++; -} - -function loadJSON(path, context) { - let raw; - try { - raw = readFileSync(path, "utf-8"); - } catch { - fail(`${context}: file not found at ${relative(root, path)}`); - return null; - } - try { - return JSON.parse(raw); - } catch (err) { - fail(`${context}: invalid JSON — ${err.message}`); - return null; - } -} - -// --- Path validation (mirrors resolve_manifest_path / resolve_plugin_source_path) --- - -function isValidRelativePath(path) { - if (typeof path !== "string" || path.length === 0) return false; - const stripped = path.startsWith("./") ? path.slice(2) : null; - if (stripped === null) return false; - if (stripped.length === 0) return false; - // Must not contain .. or other non-normal components - const segments = stripped.replace(/\/+$/, "").split("/"); - return segments.every((s) => s.length > 0 && s !== ".." && s !== "."); -} - -// --- Marketplace validation (mirrors marketplace.rs) --- - -function validateMarketplace() { - const marketplacePath = resolve( - root, - ".agents", - "plugins", - "marketplace.json" - ); - const marketplace = loadJSON(marketplacePath, "Marketplace"); - if (!marketplace) return; - - if (typeof marketplace.name !== "string" || marketplace.name.length === 0) { - fail("Marketplace: missing or empty `name`"); - } - - if (!Array.isArray(marketplace.plugins)) { - fail("Marketplace: `plugins` must be an array"); - return; - } - - for (const [index, plugin] of marketplace.plugins.entries()) { - const label = `Marketplace plugins[${index}]`; - - if (typeof plugin.name !== "string" || plugin.name.length === 0) { - fail(`${label}: missing or empty \`name\``); - continue; - } - - const pluginLabel = `Marketplace plugin "${plugin.name}"`; - - // Validate source - if (!plugin.source || typeof plugin.source !== "object") { - fail(`${pluginLabel}: \`source\` must be an object`); - continue; - } - - if (plugin.source.source !== "local") { - fail( - `${pluginLabel}: \`source.source\` must be "local", got "${plugin.source.source}"` - ); - continue; - } - - const sourcePath = plugin.source.path; - if (!isValidRelativePath(sourcePath)) { - fail( - `${pluginLabel}: \`source.path\` must be a relative path starting with "./" — got "${sourcePath}"` - ); - continue; - } - - // Resolve relative to marketplace root (2 levels up from .agents/plugins/marketplace.json) - const pluginDir = resolve(root, sourcePath.slice(2)); - - if (!existsSync(pluginDir) || !statSync(pluginDir).isDirectory()) { - fail(`${pluginLabel}: source directory does not exist — ${sourcePath}`); - continue; - } - - // Validate policy - const validInstallPolicies = [ - "NOT_AVAILABLE", - "AVAILABLE", - "INSTALLED_BY_DEFAULT", - ]; - const validAuthPolicies = ["ON_INSTALL", "ON_USE"]; - - if (plugin.policy) { - if ( - plugin.policy.installation && - !validInstallPolicies.includes(plugin.policy.installation) - ) { - fail( - `${pluginLabel}: invalid policy.installation "${plugin.policy.installation}"` - ); - } - if ( - plugin.policy.authentication && - !validAuthPolicies.includes(plugin.policy.authentication) - ) { - fail( - `${pluginLabel}: invalid policy.authentication "${plugin.policy.authentication}"` - ); - } - } - - // Validate plugin manifest - validatePluginManifest(pluginDir, plugin.name); - } -} - -// --- Plugin manifest validation (mirrors manifest.rs) --- - -const MAX_SKILL_NAME_LENGTH = 64; -const MAX_DEFAULT_PROMPT_COUNT = 3; -const MAX_DEFAULT_PROMPT_LENGTH = 128; - -function validatePluginManifest(pluginDir, marketplaceName) { - const manifestPath = resolve(pluginDir, ".codex-plugin", "plugin.json"); - const label = `Plugin "${marketplaceName}"`; - const manifest = loadJSON(manifestPath, `${label} manifest`); - if (!manifest) return; - - // Name validation - if (typeof manifest.name === "string" && manifest.name.length > 0) { - if (manifest.name !== marketplaceName) { - fail( - `${label}: marketplace name does not match plugin.json name "${manifest.name}"` - ); - } - } - - // Validate manifest paths (skills, mcpServers, apps) - for (const field of ["skills", "mcpServers", "apps"]) { - const value = manifest[field]; - if (value === undefined || value === null) continue; - if (typeof value !== "string") { - fail(`${label}: \`${field}\` must be a string path`); - continue; - } - if (!isValidRelativePath(value)) { - fail( - `${label}: \`${field}\` must be a relative path starting with "./" — got "${value}"` - ); - continue; - } - const resolved = resolve(pluginDir, value.slice(2)); - if (!existsSync(resolved)) { - fail(`${label}: \`${field}\` references missing path "${value}"`); - } - } - - // Validate interface - if (manifest.interface) { - validatePluginInterface(pluginDir, label, manifest.interface); - } - - // Validate skills - validateSkills(pluginDir, label, manifest.skills); -} - -function validatePluginInterface(pluginDir, label, iface) { - // Validate asset paths - for (const field of ["composerIcon", "logo"]) { - const value = iface[field]; - if (!value) continue; - if (!isValidRelativePath(value)) { - fail( - `${label}: interface.${field} must be a relative path starting with "./" — got "${value}"` - ); - continue; - } - const resolved = resolve(pluginDir, value.slice(2)); - if (!existsSync(resolved)) { - fail(`${label}: interface.${field} references missing path "${value}"`); - } - } - - // Validate screenshots - if (Array.isArray(iface.screenshots)) { - for (const [i, screenshot] of iface.screenshots.entries()) { - if (!isValidRelativePath(screenshot)) { - fail( - `${label}: interface.screenshots[${i}] must be a relative path starting with "./"` - ); - continue; - } - const resolved = resolve(pluginDir, screenshot.slice(2)); - if (!existsSync(resolved)) { - fail( - `${label}: interface.screenshots[${i}] references missing path "${screenshot}"` - ); - } - } - } - - // Validate defaultPrompt - if (iface.defaultPrompt !== undefined) { - const prompts = Array.isArray(iface.defaultPrompt) - ? iface.defaultPrompt - : typeof iface.defaultPrompt === "string" - ? [iface.defaultPrompt] - : null; - - if (prompts === null) { - fail( - `${label}: interface.defaultPrompt must be a string or array of strings` - ); - } else { - if (prompts.length > MAX_DEFAULT_PROMPT_COUNT) { - warn( - `${label}: interface.defaultPrompt has ${prompts.length} entries, maximum is ${MAX_DEFAULT_PROMPT_COUNT}` - ); - } - for (const [i, prompt] of prompts.entries()) { - if (typeof prompt !== "string") { - fail( - `${label}: interface.defaultPrompt[${i}] must be a string` - ); - continue; - } - const normalized = prompt.split(/\s+/).join(" ").trim(); - if (normalized.length > MAX_DEFAULT_PROMPT_LENGTH) { - warn( - `${label}: interface.defaultPrompt[${i}] exceeds ${MAX_DEFAULT_PROMPT_LENGTH} characters` - ); - } - } - } - } -} - -// --- Skill validation (mirrors quick_validate.py) --- - -function validateSkills(pluginDir, label, skillsPath) { - const skillsDir = skillsPath - ? resolve(pluginDir, skillsPath.replace(/^\.\//, "")) - : resolve(pluginDir, "skills"); - - if (!existsSync(skillsDir) || !statSync(skillsDir).isDirectory()) return; - - for (const entry of readdirSync(skillsDir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const skillMdPath = resolve(skillsDir, entry.name, "SKILL.md"); - if (!existsSync(skillMdPath)) { - warn(`${label}: skill "${entry.name}" is missing SKILL.md`); - continue; - } - - const content = readFileSync(skillMdPath, "utf-8").replace(/\r\n/g, "\n"); - if (!content.startsWith("---\n")) { - fail( - `${label}: skill "${entry.name}" SKILL.md missing YAML frontmatter` - ); - continue; - } - - const closingIndex = content.indexOf("\n---\n", 4); - if (closingIndex === -1) { - fail( - `${label}: skill "${entry.name}" SKILL.md has unclosed frontmatter` - ); - continue; - } - - const frontmatter = parseFrontmatter(content.slice(4, closingIndex)); - const skillLabel = `${label} skill "${entry.name}"`; - - if (!frontmatter.name) { - fail(`${skillLabel}: missing "name" in frontmatter`); - } else { - const name = frontmatter.name.trim(); - if (!/^[a-z0-9-]+$/.test(name)) { - fail( - `${skillLabel}: name "${name}" must be hyphen-case (lowercase letters, digits, hyphens)` - ); - } else if (name.startsWith("-") || name.endsWith("-") || name.includes("--")) { - fail( - `${skillLabel}: name "${name}" cannot start/end with hyphen or contain consecutive hyphens` - ); - } - if (name.length > MAX_SKILL_NAME_LENGTH) { - fail( - `${skillLabel}: name exceeds ${MAX_SKILL_NAME_LENGTH} characters` - ); - } - } - - if (!frontmatter.description) { - fail(`${skillLabel}: missing "description" in frontmatter`); - } else { - const desc = frontmatter.description.trim(); - if (/<|>/.test(desc)) { - fail(`${skillLabel}: description cannot contain angle brackets`); - } - if (desc.length > 1024) { - fail(`${skillLabel}: description exceeds 1024 characters`); - } - } - } -} - -function parseFrontmatter(block) { - const fields = {}; - for (const line of block.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const sep = line.indexOf(":"); - if (sep === -1) continue; - fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim(); - } - return fields; -} - -// --- Main --- - -validateMarketplace(); - -if (errors > 0) { - console.error(`\nCodex validation failed with ${errors} error(s).`); - process.exit(1); -} else { - if (warnings > 0) { - console.log(`Codex validation passed with ${warnings} warning(s).`); - } else { - console.log("Codex validation passed."); - } -} From 7e277762704ebcece9f2b70fc569f6df562fec1f Mon Sep 17 00:00:00 2001 From: Michael Dougall <6801309+itsdouges@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:25:38 +1000 Subject: [PATCH 4/5] chore: update --- .agents/plugins/marketplace.json | 20 + README.md | 18 + package.json | 2 +- .../paper-desktop/.codex-plugin/plugin.json | 50 +++ plugins/paper-desktop/mcp-installed.json | 8 + scripts/validate-codex.mjs | 364 ++++++++++++++++++ 6 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 plugins/paper-desktop/.codex-plugin/plugin.json create mode 100644 plugins/paper-desktop/mcp-installed.json create mode 100644 scripts/validate-codex.mjs diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..17ba18f --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "paper", + "interface": { + "displayName": "Paper" + }, + "plugins": [ + { + "name": "paper-desktop", + "source": { + "source": "local", + "path": "./plugins/paper-desktop" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Design" + } + ] +} diff --git a/README.md b/README.md index 52adc98..e2e0a9d 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,21 @@ Are we missing one, you have feedback for us, or just want to chat? Join us on o ```sh /plugin install paper-desktop@paper ``` + +## Codex + +**Add the custom marketplace** + +```sh +codex plugin marketplace add paper-design/agent-plugins +``` + +**Install the plugin** + +```sh +codex plugin install paper-desktop@paper +``` + +You can also browse and install plugins interactively by running `/plugins` inside Codex CLI after adding the marketplace. + +- [Read more about installing Codex plugins](https://developers.openai.com/codex/plugins) diff --git a/package.json b/package.json index e30a069..4448776 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "private": true, "scripts": { - "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs" + "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs && bun ./scripts/validate-codex.mjs" }, "devDependencies": { "@types/bun": "latest", diff --git a/plugins/paper-desktop/.codex-plugin/plugin.json b/plugins/paper-desktop/.codex-plugin/plugin.json new file mode 100644 index 0000000..ced2946 --- /dev/null +++ b/plugins/paper-desktop/.codex-plugin/plugin.json @@ -0,0 +1,50 @@ +{ + "name": "paper-desktop", + "version": "0.2.0", + "description": "Design on a canvas that Codex can read and write to — built on web standards.", + "author": { + "name": "Paper", + "email": "team@paper.design", + "url": "https://paper.design" + }, + "homepage": "https://paper.design", + "repository": "https://github.com/paper-design/agent-plugins.git", + "license": "MIT", + "keywords": [ + "paper", + "design", + "ui", + "canvas", + "html", + "css", + "design-to-code", + "code-to-design", + "prototyping", + "mcp", + "design-system" + ], + "mcpServers": "./mcp-installed.json", + "interface": { + "displayName": "Paper Desktop", + "shortDescription": "Design on a canvas that Codex can read and write to.", + "longDescription": "Paper is a professional design tool built on HTML and CSS. With this plugin, Codex can read your designs — structure, styles, screenshots, and text — and write back to the canvas in real time. Use it to turn designs into code, generate UI from your codebase, pull in real content from connected data sources, and keep your designs in sync with the tools your team already uses.", + "developerName": "Paper", + "category": "Design", + "capabilities": ["Read", "Write"], + "websiteURL": "https://paper.design", + "privacyPolicyURL": "https://paper.design/privacy", + "brandColor": "#81ADEC", + "composerIcon": "./assets/logo.svg", + "logo": "./assets/logo.svg", + "defaultPrompt": [ + "Use the CSS styles from my repo and design a settings page in Paper", + "Implement my design from Paper in my codebase, using my code conventions", + "Grab my tokens from Figma and create a design system on the canvas in Paper" + ], + "screenshots": [ + "./assets/barley-layout.png", + "./assets/barley-feedback.png", + "./assets/barley-responsive-frames.png" + ] + } +} diff --git a/plugins/paper-desktop/mcp-installed.json b/plugins/paper-desktop/mcp-installed.json new file mode 100644 index 0000000..98bbd74 --- /dev/null +++ b/plugins/paper-desktop/mcp-installed.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "paper": { + "command": "paper", + "args": ["mcp"] + } + } +} diff --git a/scripts/validate-codex.mjs b/scripts/validate-codex.mjs new file mode 100644 index 0000000..ff3cba7 --- /dev/null +++ b/scripts/validate-codex.mjs @@ -0,0 +1,364 @@ +#!/usr/bin/env node + +/** + * Codex plugin validator — ported from codex-rs/core/src/plugins/marketplace.rs + * and codex-rs/core/src/plugins/manifest.rs + */ + +import { readFileSync, existsSync, readdirSync, statSync } from "fs"; +import { resolve, dirname, basename, relative } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, ".."); + +let errors = 0; +let warnings = 0; + +function fail(message) { + console.error(`ERROR: ${message}`); + errors++; +} + +function warn(message) { + console.warn(`WARN: ${message}`); + warnings++; +} + +function loadJSON(path, context) { + let raw; + try { + raw = readFileSync(path, "utf-8"); + } catch { + fail(`${context}: file not found at ${relative(root, path)}`); + return null; + } + try { + return JSON.parse(raw); + } catch (err) { + fail(`${context}: invalid JSON — ${err.message}`); + return null; + } +} + +// --- Path validation (mirrors resolve_manifest_path / resolve_plugin_source_path) --- + +function isValidRelativePath(path) { + if (typeof path !== "string" || path.length === 0) return false; + const stripped = path.startsWith("./") ? path.slice(2) : null; + if (stripped === null) return false; + if (stripped.length === 0) return false; + // Must not contain .. or other non-normal components + const segments = stripped.replace(/\/+$/, "").split("/"); + return segments.every((s) => s.length > 0 && s !== ".." && s !== "."); +} + +// --- Marketplace validation (mirrors marketplace.rs) --- + +function validateMarketplace() { + const marketplacePath = resolve( + root, + ".agents", + "plugins", + "marketplace.json" + ); + const marketplace = loadJSON(marketplacePath, "Marketplace"); + if (!marketplace) return; + + if (typeof marketplace.name !== "string" || marketplace.name.length === 0) { + fail("Marketplace: missing or empty `name`"); + } + + if (!Array.isArray(marketplace.plugins)) { + fail("Marketplace: `plugins` must be an array"); + return; + } + + for (const [index, plugin] of marketplace.plugins.entries()) { + const label = `Marketplace plugins[${index}]`; + + if (typeof plugin.name !== "string" || plugin.name.length === 0) { + fail(`${label}: missing or empty \`name\``); + continue; + } + + const pluginLabel = `Marketplace plugin "${plugin.name}"`; + + // Validate source + if (!plugin.source || typeof plugin.source !== "object") { + fail(`${pluginLabel}: \`source\` must be an object`); + continue; + } + + if (plugin.source.source !== "local") { + fail( + `${pluginLabel}: \`source.source\` must be "local", got "${plugin.source.source}"` + ); + continue; + } + + const sourcePath = plugin.source.path; + if (!isValidRelativePath(sourcePath)) { + fail( + `${pluginLabel}: \`source.path\` must be a relative path starting with "./" — got "${sourcePath}"` + ); + continue; + } + + // Resolve relative to marketplace root (2 levels up from .agents/plugins/marketplace.json) + const pluginDir = resolve(root, sourcePath.slice(2)); + + if (!existsSync(pluginDir) || !statSync(pluginDir).isDirectory()) { + fail(`${pluginLabel}: source directory does not exist — ${sourcePath}`); + continue; + } + + // Validate policy + const validInstallPolicies = [ + "NOT_AVAILABLE", + "AVAILABLE", + "INSTALLED_BY_DEFAULT", + ]; + const validAuthPolicies = ["ON_INSTALL", "ON_USE"]; + + if (plugin.policy) { + if ( + plugin.policy.installation && + !validInstallPolicies.includes(plugin.policy.installation) + ) { + fail( + `${pluginLabel}: invalid policy.installation "${plugin.policy.installation}"` + ); + } + if ( + plugin.policy.authentication && + !validAuthPolicies.includes(plugin.policy.authentication) + ) { + fail( + `${pluginLabel}: invalid policy.authentication "${plugin.policy.authentication}"` + ); + } + } + + // Validate plugin manifest + validatePluginManifest(pluginDir, plugin.name); + } +} + +// --- Plugin manifest validation (mirrors manifest.rs) --- + +const MAX_SKILL_NAME_LENGTH = 64; +const MAX_DEFAULT_PROMPT_COUNT = 3; +const MAX_DEFAULT_PROMPT_LENGTH = 128; + +function validatePluginManifest(pluginDir, marketplaceName) { + const manifestPath = resolve(pluginDir, ".codex-plugin", "plugin.json"); + const label = `Plugin "${marketplaceName}"`; + const manifest = loadJSON(manifestPath, `${label} manifest`); + if (!manifest) return; + + // Name validation + if (typeof manifest.name === "string" && manifest.name.length > 0) { + if (manifest.name !== marketplaceName) { + fail( + `${label}: marketplace name does not match plugin.json name "${manifest.name}"` + ); + } + } + + // Validate manifest paths (skills, mcpServers, apps) + for (const field of ["skills", "mcpServers", "apps"]) { + const value = manifest[field]; + if (value === undefined || value === null) continue; + if (typeof value !== "string") { + fail(`${label}: \`${field}\` must be a string path`); + continue; + } + if (!isValidRelativePath(value)) { + fail( + `${label}: \`${field}\` must be a relative path starting with "./" — got "${value}"` + ); + continue; + } + const resolved = resolve(pluginDir, value.slice(2)); + if (!existsSync(resolved)) { + fail(`${label}: \`${field}\` references missing path "${value}"`); + } + } + + // Validate interface + if (manifest.interface) { + validatePluginInterface(pluginDir, label, manifest.interface); + } + + // Validate skills + validateSkills(pluginDir, label, manifest.skills); +} + +function validatePluginInterface(pluginDir, label, iface) { + // Validate asset paths + for (const field of ["composerIcon", "logo"]) { + const value = iface[field]; + if (!value) continue; + if (!isValidRelativePath(value)) { + fail( + `${label}: interface.${field} must be a relative path starting with "./" — got "${value}"` + ); + continue; + } + const resolved = resolve(pluginDir, value.slice(2)); + if (!existsSync(resolved)) { + fail(`${label}: interface.${field} references missing path "${value}"`); + } + } + + // Validate screenshots + if (Array.isArray(iface.screenshots)) { + for (const [i, screenshot] of iface.screenshots.entries()) { + if (!isValidRelativePath(screenshot)) { + fail( + `${label}: interface.screenshots[${i}] must be a relative path starting with "./"` + ); + continue; + } + const resolved = resolve(pluginDir, screenshot.slice(2)); + if (!existsSync(resolved)) { + fail( + `${label}: interface.screenshots[${i}] references missing path "${screenshot}"` + ); + } + } + } + + // Validate defaultPrompt + if (iface.defaultPrompt !== undefined) { + const prompts = Array.isArray(iface.defaultPrompt) + ? iface.defaultPrompt + : typeof iface.defaultPrompt === "string" + ? [iface.defaultPrompt] + : null; + + if (prompts === null) { + fail( + `${label}: interface.defaultPrompt must be a string or array of strings` + ); + } else { + if (prompts.length > MAX_DEFAULT_PROMPT_COUNT) { + warn( + `${label}: interface.defaultPrompt has ${prompts.length} entries, maximum is ${MAX_DEFAULT_PROMPT_COUNT}` + ); + } + for (const [i, prompt] of prompts.entries()) { + if (typeof prompt !== "string") { + fail( + `${label}: interface.defaultPrompt[${i}] must be a string` + ); + continue; + } + const normalized = prompt.split(/\s+/).join(" ").trim(); + if (normalized.length > MAX_DEFAULT_PROMPT_LENGTH) { + warn( + `${label}: interface.defaultPrompt[${i}] exceeds ${MAX_DEFAULT_PROMPT_LENGTH} characters` + ); + } + } + } + } +} + +// --- Skill validation (mirrors quick_validate.py) --- + +function validateSkills(pluginDir, label, skillsPath) { + const skillsDir = skillsPath + ? resolve(pluginDir, skillsPath.replace(/^\.\//, "")) + : resolve(pluginDir, "skills"); + + if (!existsSync(skillsDir) || !statSync(skillsDir).isDirectory()) return; + + for (const entry of readdirSync(skillsDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const skillMdPath = resolve(skillsDir, entry.name, "SKILL.md"); + if (!existsSync(skillMdPath)) { + warn(`${label}: skill "${entry.name}" is missing SKILL.md`); + continue; + } + + const content = readFileSync(skillMdPath, "utf-8").replace(/\r\n/g, "\n"); + if (!content.startsWith("---\n")) { + fail( + `${label}: skill "${entry.name}" SKILL.md missing YAML frontmatter` + ); + continue; + } + + const closingIndex = content.indexOf("\n---\n", 4); + if (closingIndex === -1) { + fail( + `${label}: skill "${entry.name}" SKILL.md has unclosed frontmatter` + ); + continue; + } + + const frontmatter = parseFrontmatter(content.slice(4, closingIndex)); + const skillLabel = `${label} skill "${entry.name}"`; + + if (!frontmatter.name) { + fail(`${skillLabel}: missing "name" in frontmatter`); + } else { + const name = frontmatter.name.trim(); + if (!/^[a-z0-9-]+$/.test(name)) { + fail( + `${skillLabel}: name "${name}" must be hyphen-case (lowercase letters, digits, hyphens)` + ); + } else if (name.startsWith("-") || name.endsWith("-") || name.includes("--")) { + fail( + `${skillLabel}: name "${name}" cannot start/end with hyphen or contain consecutive hyphens` + ); + } + if (name.length > MAX_SKILL_NAME_LENGTH) { + fail( + `${skillLabel}: name exceeds ${MAX_SKILL_NAME_LENGTH} characters` + ); + } + } + + if (!frontmatter.description) { + fail(`${skillLabel}: missing "description" in frontmatter`); + } else { + const desc = frontmatter.description.trim(); + if (/<|>/.test(desc)) { + fail(`${skillLabel}: description cannot contain angle brackets`); + } + if (desc.length > 1024) { + fail(`${skillLabel}: description exceeds 1024 characters`); + } + } + } +} + +function parseFrontmatter(block) { + const fields = {}; + for (const line of block.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const sep = line.indexOf(":"); + if (sep === -1) continue; + fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim(); + } + return fields; +} + +// --- Main --- + +validateMarketplace(); + +if (errors > 0) { + console.error(`\nCodex validation failed with ${errors} error(s).`); + process.exit(1); +} else { + if (warnings > 0) { + console.log(`Codex validation passed with ${warnings} warning(s).`); + } else { + console.log("Codex validation passed."); + } +} From e96e5e4babca4333f392635f7a46b3607ca97a60 Mon Sep 17 00:00:00 2001 From: Michael Dougall <6801309+itsdouges@users.noreply.github.com> Date: Sat, 8 Aug 2026 03:40:29 +1000 Subject: [PATCH 5/5] chore: delete codex --- .agents/plugins/marketplace.json | 20 - README.md | 18 - package.json | 2 +- .../paper-desktop/.codex-plugin/plugin.json | 50 --- scripts/validate-codex.mjs | 364 ------------------ 5 files changed, 1 insertion(+), 453 deletions(-) delete mode 100644 .agents/plugins/marketplace.json delete mode 100644 plugins/paper-desktop/.codex-plugin/plugin.json delete mode 100644 scripts/validate-codex.mjs diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json deleted file mode 100644 index 17ba18f..0000000 --- a/.agents/plugins/marketplace.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "paper", - "interface": { - "displayName": "Paper" - }, - "plugins": [ - { - "name": "paper-desktop", - "source": { - "source": "local", - "path": "./plugins/paper-desktop" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Design" - } - ] -} diff --git a/README.md b/README.md index e2e0a9d..52adc98 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,3 @@ Are we missing one, you have feedback for us, or just want to chat? Join us on o ```sh /plugin install paper-desktop@paper ``` - -## Codex - -**Add the custom marketplace** - -```sh -codex plugin marketplace add paper-design/agent-plugins -``` - -**Install the plugin** - -```sh -codex plugin install paper-desktop@paper -``` - -You can also browse and install plugins interactively by running `/plugins` inside Codex CLI after adding the marketplace. - -- [Read more about installing Codex plugins](https://developers.openai.com/codex/plugins) diff --git a/package.json b/package.json index 4448776..e30a069 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "private": true, "scripts": { - "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs && bun ./scripts/validate-codex.mjs" + "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs" }, "devDependencies": { "@types/bun": "latest", diff --git a/plugins/paper-desktop/.codex-plugin/plugin.json b/plugins/paper-desktop/.codex-plugin/plugin.json deleted file mode 100644 index ced2946..0000000 --- a/plugins/paper-desktop/.codex-plugin/plugin.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "paper-desktop", - "version": "0.2.0", - "description": "Design on a canvas that Codex can read and write to — built on web standards.", - "author": { - "name": "Paper", - "email": "team@paper.design", - "url": "https://paper.design" - }, - "homepage": "https://paper.design", - "repository": "https://github.com/paper-design/agent-plugins.git", - "license": "MIT", - "keywords": [ - "paper", - "design", - "ui", - "canvas", - "html", - "css", - "design-to-code", - "code-to-design", - "prototyping", - "mcp", - "design-system" - ], - "mcpServers": "./mcp-installed.json", - "interface": { - "displayName": "Paper Desktop", - "shortDescription": "Design on a canvas that Codex can read and write to.", - "longDescription": "Paper is a professional design tool built on HTML and CSS. With this plugin, Codex can read your designs — structure, styles, screenshots, and text — and write back to the canvas in real time. Use it to turn designs into code, generate UI from your codebase, pull in real content from connected data sources, and keep your designs in sync with the tools your team already uses.", - "developerName": "Paper", - "category": "Design", - "capabilities": ["Read", "Write"], - "websiteURL": "https://paper.design", - "privacyPolicyURL": "https://paper.design/privacy", - "brandColor": "#81ADEC", - "composerIcon": "./assets/logo.svg", - "logo": "./assets/logo.svg", - "defaultPrompt": [ - "Use the CSS styles from my repo and design a settings page in Paper", - "Implement my design from Paper in my codebase, using my code conventions", - "Grab my tokens from Figma and create a design system on the canvas in Paper" - ], - "screenshots": [ - "./assets/barley-layout.png", - "./assets/barley-feedback.png", - "./assets/barley-responsive-frames.png" - ] - } -} diff --git a/scripts/validate-codex.mjs b/scripts/validate-codex.mjs deleted file mode 100644 index ff3cba7..0000000 --- a/scripts/validate-codex.mjs +++ /dev/null @@ -1,364 +0,0 @@ -#!/usr/bin/env node - -/** - * Codex plugin validator — ported from codex-rs/core/src/plugins/marketplace.rs - * and codex-rs/core/src/plugins/manifest.rs - */ - -import { readFileSync, existsSync, readdirSync, statSync } from "fs"; -import { resolve, dirname, basename, relative } from "path"; -import { fileURLToPath } from "url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const root = resolve(__dirname, ".."); - -let errors = 0; -let warnings = 0; - -function fail(message) { - console.error(`ERROR: ${message}`); - errors++; -} - -function warn(message) { - console.warn(`WARN: ${message}`); - warnings++; -} - -function loadJSON(path, context) { - let raw; - try { - raw = readFileSync(path, "utf-8"); - } catch { - fail(`${context}: file not found at ${relative(root, path)}`); - return null; - } - try { - return JSON.parse(raw); - } catch (err) { - fail(`${context}: invalid JSON — ${err.message}`); - return null; - } -} - -// --- Path validation (mirrors resolve_manifest_path / resolve_plugin_source_path) --- - -function isValidRelativePath(path) { - if (typeof path !== "string" || path.length === 0) return false; - const stripped = path.startsWith("./") ? path.slice(2) : null; - if (stripped === null) return false; - if (stripped.length === 0) return false; - // Must not contain .. or other non-normal components - const segments = stripped.replace(/\/+$/, "").split("/"); - return segments.every((s) => s.length > 0 && s !== ".." && s !== "."); -} - -// --- Marketplace validation (mirrors marketplace.rs) --- - -function validateMarketplace() { - const marketplacePath = resolve( - root, - ".agents", - "plugins", - "marketplace.json" - ); - const marketplace = loadJSON(marketplacePath, "Marketplace"); - if (!marketplace) return; - - if (typeof marketplace.name !== "string" || marketplace.name.length === 0) { - fail("Marketplace: missing or empty `name`"); - } - - if (!Array.isArray(marketplace.plugins)) { - fail("Marketplace: `plugins` must be an array"); - return; - } - - for (const [index, plugin] of marketplace.plugins.entries()) { - const label = `Marketplace plugins[${index}]`; - - if (typeof plugin.name !== "string" || plugin.name.length === 0) { - fail(`${label}: missing or empty \`name\``); - continue; - } - - const pluginLabel = `Marketplace plugin "${plugin.name}"`; - - // Validate source - if (!plugin.source || typeof plugin.source !== "object") { - fail(`${pluginLabel}: \`source\` must be an object`); - continue; - } - - if (plugin.source.source !== "local") { - fail( - `${pluginLabel}: \`source.source\` must be "local", got "${plugin.source.source}"` - ); - continue; - } - - const sourcePath = plugin.source.path; - if (!isValidRelativePath(sourcePath)) { - fail( - `${pluginLabel}: \`source.path\` must be a relative path starting with "./" — got "${sourcePath}"` - ); - continue; - } - - // Resolve relative to marketplace root (2 levels up from .agents/plugins/marketplace.json) - const pluginDir = resolve(root, sourcePath.slice(2)); - - if (!existsSync(pluginDir) || !statSync(pluginDir).isDirectory()) { - fail(`${pluginLabel}: source directory does not exist — ${sourcePath}`); - continue; - } - - // Validate policy - const validInstallPolicies = [ - "NOT_AVAILABLE", - "AVAILABLE", - "INSTALLED_BY_DEFAULT", - ]; - const validAuthPolicies = ["ON_INSTALL", "ON_USE"]; - - if (plugin.policy) { - if ( - plugin.policy.installation && - !validInstallPolicies.includes(plugin.policy.installation) - ) { - fail( - `${pluginLabel}: invalid policy.installation "${plugin.policy.installation}"` - ); - } - if ( - plugin.policy.authentication && - !validAuthPolicies.includes(plugin.policy.authentication) - ) { - fail( - `${pluginLabel}: invalid policy.authentication "${plugin.policy.authentication}"` - ); - } - } - - // Validate plugin manifest - validatePluginManifest(pluginDir, plugin.name); - } -} - -// --- Plugin manifest validation (mirrors manifest.rs) --- - -const MAX_SKILL_NAME_LENGTH = 64; -const MAX_DEFAULT_PROMPT_COUNT = 3; -const MAX_DEFAULT_PROMPT_LENGTH = 128; - -function validatePluginManifest(pluginDir, marketplaceName) { - const manifestPath = resolve(pluginDir, ".codex-plugin", "plugin.json"); - const label = `Plugin "${marketplaceName}"`; - const manifest = loadJSON(manifestPath, `${label} manifest`); - if (!manifest) return; - - // Name validation - if (typeof manifest.name === "string" && manifest.name.length > 0) { - if (manifest.name !== marketplaceName) { - fail( - `${label}: marketplace name does not match plugin.json name "${manifest.name}"` - ); - } - } - - // Validate manifest paths (skills, mcpServers, apps) - for (const field of ["skills", "mcpServers", "apps"]) { - const value = manifest[field]; - if (value === undefined || value === null) continue; - if (typeof value !== "string") { - fail(`${label}: \`${field}\` must be a string path`); - continue; - } - if (!isValidRelativePath(value)) { - fail( - `${label}: \`${field}\` must be a relative path starting with "./" — got "${value}"` - ); - continue; - } - const resolved = resolve(pluginDir, value.slice(2)); - if (!existsSync(resolved)) { - fail(`${label}: \`${field}\` references missing path "${value}"`); - } - } - - // Validate interface - if (manifest.interface) { - validatePluginInterface(pluginDir, label, manifest.interface); - } - - // Validate skills - validateSkills(pluginDir, label, manifest.skills); -} - -function validatePluginInterface(pluginDir, label, iface) { - // Validate asset paths - for (const field of ["composerIcon", "logo"]) { - const value = iface[field]; - if (!value) continue; - if (!isValidRelativePath(value)) { - fail( - `${label}: interface.${field} must be a relative path starting with "./" — got "${value}"` - ); - continue; - } - const resolved = resolve(pluginDir, value.slice(2)); - if (!existsSync(resolved)) { - fail(`${label}: interface.${field} references missing path "${value}"`); - } - } - - // Validate screenshots - if (Array.isArray(iface.screenshots)) { - for (const [i, screenshot] of iface.screenshots.entries()) { - if (!isValidRelativePath(screenshot)) { - fail( - `${label}: interface.screenshots[${i}] must be a relative path starting with "./"` - ); - continue; - } - const resolved = resolve(pluginDir, screenshot.slice(2)); - if (!existsSync(resolved)) { - fail( - `${label}: interface.screenshots[${i}] references missing path "${screenshot}"` - ); - } - } - } - - // Validate defaultPrompt - if (iface.defaultPrompt !== undefined) { - const prompts = Array.isArray(iface.defaultPrompt) - ? iface.defaultPrompt - : typeof iface.defaultPrompt === "string" - ? [iface.defaultPrompt] - : null; - - if (prompts === null) { - fail( - `${label}: interface.defaultPrompt must be a string or array of strings` - ); - } else { - if (prompts.length > MAX_DEFAULT_PROMPT_COUNT) { - warn( - `${label}: interface.defaultPrompt has ${prompts.length} entries, maximum is ${MAX_DEFAULT_PROMPT_COUNT}` - ); - } - for (const [i, prompt] of prompts.entries()) { - if (typeof prompt !== "string") { - fail( - `${label}: interface.defaultPrompt[${i}] must be a string` - ); - continue; - } - const normalized = prompt.split(/\s+/).join(" ").trim(); - if (normalized.length > MAX_DEFAULT_PROMPT_LENGTH) { - warn( - `${label}: interface.defaultPrompt[${i}] exceeds ${MAX_DEFAULT_PROMPT_LENGTH} characters` - ); - } - } - } - } -} - -// --- Skill validation (mirrors quick_validate.py) --- - -function validateSkills(pluginDir, label, skillsPath) { - const skillsDir = skillsPath - ? resolve(pluginDir, skillsPath.replace(/^\.\//, "")) - : resolve(pluginDir, "skills"); - - if (!existsSync(skillsDir) || !statSync(skillsDir).isDirectory()) return; - - for (const entry of readdirSync(skillsDir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const skillMdPath = resolve(skillsDir, entry.name, "SKILL.md"); - if (!existsSync(skillMdPath)) { - warn(`${label}: skill "${entry.name}" is missing SKILL.md`); - continue; - } - - const content = readFileSync(skillMdPath, "utf-8").replace(/\r\n/g, "\n"); - if (!content.startsWith("---\n")) { - fail( - `${label}: skill "${entry.name}" SKILL.md missing YAML frontmatter` - ); - continue; - } - - const closingIndex = content.indexOf("\n---\n", 4); - if (closingIndex === -1) { - fail( - `${label}: skill "${entry.name}" SKILL.md has unclosed frontmatter` - ); - continue; - } - - const frontmatter = parseFrontmatter(content.slice(4, closingIndex)); - const skillLabel = `${label} skill "${entry.name}"`; - - if (!frontmatter.name) { - fail(`${skillLabel}: missing "name" in frontmatter`); - } else { - const name = frontmatter.name.trim(); - if (!/^[a-z0-9-]+$/.test(name)) { - fail( - `${skillLabel}: name "${name}" must be hyphen-case (lowercase letters, digits, hyphens)` - ); - } else if (name.startsWith("-") || name.endsWith("-") || name.includes("--")) { - fail( - `${skillLabel}: name "${name}" cannot start/end with hyphen or contain consecutive hyphens` - ); - } - if (name.length > MAX_SKILL_NAME_LENGTH) { - fail( - `${skillLabel}: name exceeds ${MAX_SKILL_NAME_LENGTH} characters` - ); - } - } - - if (!frontmatter.description) { - fail(`${skillLabel}: missing "description" in frontmatter`); - } else { - const desc = frontmatter.description.trim(); - if (/<|>/.test(desc)) { - fail(`${skillLabel}: description cannot contain angle brackets`); - } - if (desc.length > 1024) { - fail(`${skillLabel}: description exceeds 1024 characters`); - } - } - } -} - -function parseFrontmatter(block) { - const fields = {}; - for (const line of block.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const sep = line.indexOf(":"); - if (sep === -1) continue; - fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim(); - } - return fields; -} - -// --- Main --- - -validateMarketplace(); - -if (errors > 0) { - console.error(`\nCodex validation failed with ${errors} error(s).`); - process.exit(1); -} else { - if (warnings > 0) { - console.log(`Codex validation passed with ${warnings} warning(s).`); - } else { - console.log("Codex validation passed."); - } -}