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/.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 deleted file mode 100644 index 18d14da..0000000 --- a/plugins/paper-desktop/.codex-plugin/plugin.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "paper-desktop", - "version": "0.1.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" - ], - "skills": "./skills/", - "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/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-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/plugins/paper-desktop/mcp.json b/plugins/paper-desktop/mcp.json index 034d089..794d7b0 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": "${HOME}/.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). 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."); - } -}