diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2cec261..bc69c52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,3 +17,5 @@ jobs: - run: bun install - run: bun run build + + - run: bun ./scripts/validate-mcpb-artifact.mjs diff --git a/.gitignore b/.gitignore index 6482e36..cee0a73 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,7 @@ node_modules tmp/ temp/ -# Built assets -*.mcpb +# Built assets (keep committed MCPB; un-ignore dist/ first so children can match) +**/dist/** +!**/dist/ +!**/dist/paper.mcpb diff --git a/README.md b/README.md index 52adc98..b1b1449 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,13 @@ Are we missing one, you have feedback for us, or just want to chat? Join us on o ```sh /plugin install paper-desktop@paper ``` + +## Claude Desktop + +Pack the config-only MCPB extension, then open or drag `claude-extensions/paper-desktop/dist/paper.mcpb` into Claude Desktop → Extensions: + +```sh +bun run pack:mcpb +``` + +Requires Paper Desktop installed and opened once (so `~/.paper/bin/paper` exists). See [`claude-extensions/paper-desktop`](claude-extensions/paper-desktop). diff --git a/bun.lock b/bun.lock index 75c3df3..bae9a52 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "agent-plugins", "devDependencies": { + "@anthropic-ai/mcpb": "^2.1.2", "@types/bun": "latest", "ajv": "^8.18.0", "ajv-formats": "^3.0.1", @@ -15,26 +16,132 @@ }, }, "packages": { + "@anthropic-ai/mcpb": ["@anthropic-ai/mcpb@2.1.2", "", { "dependencies": { "@inquirer/prompts": "^6.0.1", "commander": "^13.1.0", "fflate": "^0.8.2", "galactus": "^1.0.0", "ignore": "^7.0.5", "node-forge": "^1.3.2", "pretty-bytes": "^5.6.0", "zod": "^3.25.67", "zod-to-json-schema": "^3.24.6" }, "bin": { "mcpb": "dist/cli/cli.js" } }, "sha512-goRbBC8ySo7SWb7tRzr+tL6FxDc4JPTRCdgfD2omba7freofvjq5rom1lBnYHZHo6Mizs1jAHJeN53aZbDoy8A=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ=="], + + "@inquirer/confirm": ["@inquirer/confirm@4.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0" } }, "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w=="], + + "@inquirer/core": ["@inquirer/core@9.2.1", "", { "dependencies": { "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg=="], + + "@inquirer/editor": ["@inquirer/editor@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0", "external-editor": "^3.1.0" } }, "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q=="], + + "@inquirer/expand": ["@inquirer/expand@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0" } }, "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg=="], + + "@inquirer/number": ["@inquirer/number@2.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0" } }, "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ=="], + + "@inquirer/password": ["@inquirer/password@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0", "ansi-escapes": "^4.3.2" } }, "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ=="], + + "@inquirer/prompts": ["@inquirer/prompts@6.0.1", "", { "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", "@inquirer/editor": "^3.0.1", "@inquirer/expand": "^3.0.1", "@inquirer/input": "^3.0.1", "@inquirer/number": "^2.0.1", "@inquirer/password": "^3.0.1", "@inquirer/rawlist": "^3.0.1", "@inquirer/search": "^2.0.1", "@inquirer/select": "^3.0.1" } }, "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ=="], + + "@inquirer/search": ["@inquirer/search@2.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg=="], + + "@inquirer/select": ["@inquirer/select@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q=="], + + "@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="], + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + "@types/mute-stream": ["@types/mute-stream@0.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow=="], + "@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], + "@types/wrap-ansi": ["@types/wrap-ansi@3.0.0", "", {}, "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "flora-colossus": ["flora-colossus@2.0.0", "", { "dependencies": { "debug": "^4.3.4", "fs-extra": "^10.1.0" } }, "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA=="], + + "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "galactus": ["galactus@1.0.0", "", { "dependencies": { "debug": "^4.3.4", "flora-colossus": "^2.0.0", "fs-extra": "^10.1.0" } }, "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + + "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], } } diff --git a/claude-extensions/paper-desktop/README.md b/claude-extensions/paper-desktop/README.md new file mode 120000 index 0000000..c4633c8 --- /dev/null +++ b/claude-extensions/paper-desktop/README.md @@ -0,0 +1 @@ +../../plugins/paper-desktop/README.md \ No newline at end of file diff --git a/claude-extensions/paper-desktop/assets b/claude-extensions/paper-desktop/assets new file mode 120000 index 0000000..e2f1f1e --- /dev/null +++ b/claude-extensions/paper-desktop/assets @@ -0,0 +1 @@ +../../plugins/paper-desktop/assets \ No newline at end of file diff --git a/claude-extensions/paper-desktop/dist/paper.mcpb b/claude-extensions/paper-desktop/dist/paper.mcpb new file mode 100644 index 0000000..8c2b745 Binary files /dev/null and b/claude-extensions/paper-desktop/dist/paper.mcpb differ diff --git a/claude-extensions/paper-desktop/manifest.json b/claude-extensions/paper-desktop/manifest.json new file mode 100644 index 0000000..1549084 --- /dev/null +++ b/claude-extensions/paper-desktop/manifest.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json", + "manifest_version": "0.4", + "name": "paper", + "display_name": "Paper", + "version": "0.1.0", + "description": "Design on a canvas that Claude can read and write to — built on web standards.", + "author": { + "name": "Paper", + "email": "team@paper.design", + "url": "https://paper.design" + }, + "homepage": "https://paper.design", + "documentation": "https://paper.design/docs/mcp", + "support": "mailto:team@paper.design", + "repository": { + "type": "git", + "url": "https://github.com/paper-design/agent-plugins" + }, + "license": "MIT", + "privacy_policies": ["https://paper.design/privacy"], + "icon": "assets/logo@512w.png", + "screenshots": [ + "assets/barley-layout.png", + "assets/barley-feedback.png", + "assets/barley-responsive-frames.png" + ], + "keywords": [ + "paper", + "design", + "ui", + "canvas", + "html", + "css", + "design-to-code", + "code-to-design", + "mcp", + "design-system" + ], + "server": { + "type": "binary", + "entry_point": "${HOME}/.paper/bin/paper", + "mcp_config": { + "command": "${HOME}/.paper/bin/paper", + "args": ["mcp"] + } + }, + "compatibility": { + "claude_desktop": ">=0.1.0", + "platforms": ["darwin", "win32", "linux"] + }, + "tools_generated": true +} diff --git a/claude-extensions/paper-desktop/package.json b/claude-extensions/paper-desktop/package.json new file mode 100644 index 0000000..0720fe3 --- /dev/null +++ b/claude-extensions/paper-desktop/package.json @@ -0,0 +1,8 @@ +{ + "name": "@paper/claude-desktop-mcpb", + "private": true, + "type": "module", + "scripts": { + "pack": "bun ./scripts/pack.ts" + } +} diff --git a/claude-extensions/paper-desktop/scripts/pack.ts b/claude-extensions/paper-desktop/scripts/pack.ts new file mode 100644 index 0000000..5c8f09b --- /dev/null +++ b/claude-extensions/paper-desktop/scripts/pack.ts @@ -0,0 +1,101 @@ +#!/usr/bin/env bun +/** + * Pack a config-only MCPB (zip with manifest.json at archive root). + * + * Injects README.md as long_description at pack time — the source + * manifest.json is left unchanged. + * + * Bundles icon/screenshot paths referenced by the built manifest (resolving + * symlinks into the archive). + * + * Uses a plain zip instead of `mcpb pack` because the binary entry_point is an + * external `${HOME}/.paper/bin/paper` path installed by Paper Desktop — not a + * file inside the bundle. + */ +import { + chmod, + cp, + mkdir, + readdir, + rm, + stat, + utimes, + writeFile, +} from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { $ } from "bun"; +import { buildMcpbManifest } from "../../../scripts/mcpb-manifest.mjs"; + +/** DOS-era zip epoch — fixed so rebuilds are bit-identical across runs. */ +const ZIP_EPOCH = new Date(Date.UTC(1980, 0, 1, 0, 0, 0)); +/** Fixed Unix modes so Info-ZIP external attributes ignore umask / cp defaults. */ +const FILE_MODE = 0o644; +const DIR_MODE = 0o755; + +async function normalizeStaging(dir: string) { + await chmod(dir, DIR_MODE); + await utimes(dir, ZIP_EPOCH, ZIP_EPOCH); + for (const name of await readdir(dir)) { + const path = join(dir, name); + if ((await stat(path)).isDirectory()) { + await normalizeStaging(path); + } else { + await chmod(path, FILE_MODE); + await utimes(path, ZIP_EPOCH, ZIP_EPOCH); + } + } +} + +const extensionDir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const repoRoot = join(extensionDir, "../.."); +const distDir = join(extensionDir, "dist"); +const stageDir = join(distDir, "stage"); +const outFile = join(distDir, "paper.mcpb"); +const builtManifestPath = join(stageDir, "manifest.json"); + +const { manifest, readmeLabel } = buildMcpbManifest(extensionDir, { + root: repoRoot, +}); + +const assetPaths = [ + ...(typeof manifest.icon === "string" ? [manifest.icon] : []), + ...(Array.isArray(manifest.screenshots) ? manifest.screenshots : []), +]; + +await rm(distDir, { recursive: true, force: true }); +await mkdir(stageDir, { recursive: true }); +await writeFile(builtManifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + +for (const rel of assetPaths) { + if (typeof rel !== "string" || rel.includes("..") || rel.startsWith("/")) { + console.error(`Refusing to pack unsafe asset path: ${rel}`); + process.exit(1); + } + const src = join(extensionDir, rel); + const dest = join(stageDir, rel); + await mkdir(dirname(dest), { recursive: true }); + await cp(src, dest); +} + +await normalizeStaging(stageDir); + +// TZ=UTC so DOS timestamps in the zip are timezone-independent (CI is UTC). +const result = await $`zip -r -X ${outFile} manifest.json ${assetPaths}` + .cwd(stageDir) + .env({ ...process.env, TZ: "UTC" }) + .nothrow(); + +if (result.exitCode !== 0) { + console.error(result.stderr.toString() || result.stdout.toString()); + process.exit(result.exitCode ?? 1); +} + +// Keep a copy of the built manifest next to the .mcpb for inspection +await cp(builtManifestPath, join(distDir, "manifest.json")); +await rm(stageDir, { recursive: true, force: true }); + +const listing = await $`unzip -l ${outFile}`.text(); +console.log(listing.trimEnd()); +console.log(`\nInjected long_description from ${readmeLabel}`); +console.log(`Wrote ${outFile}`); diff --git a/package.json b/package.json index e30a069..8d0622e 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,12 @@ "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-mcpb.mjs && bun run pack:mcpb", + "pack:mcpb": "bun ./claude-extensions/paper-desktop/scripts/pack.ts", + "test": "bun run build" }, "devDependencies": { + "@anthropic-ai/mcpb": "^2.1.2", "@types/bun": "latest", "ajv": "^8.18.0", "ajv-formats": "^3.0.1" diff --git a/plugins/paper-desktop/README.md b/plugins/paper-desktop/README.md index a718093..7a43163 100644 --- a/plugins/paper-desktop/README.md +++ b/plugins/paper-desktop/README.md @@ -1,10 +1,10 @@ -# Paper Desktop +Paper connects your designs, agents, code, and data on a single canvas built on web standards. Because Paper is based on web technology and LLMs are fluent in HTML/CSS, your agent can read and write to your design files with high fidelity — turning your canvas into a live collaboration surface between you and your agent. -## Description +**Prerequisites** -Paper connects your designs, agents, code, and data on a single canvas built on web standards. Because Paper is based on web technology and LLMs are fluent in HTML/CSS, Claude can read and write to your design files with high fidelity — turning your canvas into a live collaboration surface between you and your agent. +Make sure to install [Paper Desktop](https://paper.design/downloads) and open it at least once. -## Features +**Features** - **Read designs**: Inspect artboard structure, screenshots, computed styles, JSX output, and text content directly from Paper. - **Write to the canvas**: Create artboards, add or replace HTML nodes, update styles, set text, and duplicate elements — all from a prompt. @@ -12,47 +12,28 @@ Paper connects your designs, agents, code, and data on a single canvas built on - **Design-to-code**: Turn Paper designs into production code by reading the canvas structure and generating components in your framework of choice. - **Code-to-design**: Use your codebase (tokens, styles, components) as context to generate new designs on the canvas. -## Prerequisites +**Examples** -Install [Paper Desktop](https://paper.design/downloads) and open it once. - -## Examples - -### Example 1: Design from your codebase - -**User prompt:** "Use the CSS styles from my repo and design a settings page in Paper" +**Prompt:** "Use the CSS styles from my repo and design a settings page in Paper". **Expected behavior:** -- Claude reads your project's stylesheets, tokens, or theme files to understand your existing design language. +- Your agent will read your project's stylesheets, tokens, or theme files to understand your existing design language. - Creates a new artboard in Paper and builds a settings page that matches your codebase's visual style. - Uses your actual colors, typography, spacing, and component patterns — not generic defaults. -### Example 2: Turn a design into code - -**User prompt:** "Implement my design from Paper in my codebase, using my code conventions" +**Prompt:** "Implement my design from Paper in my codebase, using my code conventions". **Expected behavior:** -- Claude reads the selected frame in Paper — structure, styles, text content, and images. +- Your agent will read the selected frame in Paper — structure, styles, text content, and images. - Generates production-ready components in your project's framework and coding style. - Matches the design's layout, spacing, typography, and colors using your existing conventions (e.g. Tailwind classes, CSS modules, styled-components). -### Example 3: Sync tokens from Figma into Paper - -**User prompt:** "Grab my tokens from Figma and create a design system on the canvas in Paper" +**Prompt:** "Grab my tokens from Figma and create a design system on the canvas in Paper". **Expected behavior:** -- Claude reads color variables, text styles, and spacing tokens from your open Figma file via the Figma MCP server. +- Your agent will read color variables, text styles, and spacing tokens from your open Figma file via the Figma MCP server. - Creates a design system sticker sheet on the Paper canvas with swatches, type scales, and spacing references. - Note: requires the [Figma MCP server](https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server) to also be connected. - -## Privacy Policy - -See: [paper.design/privacy](https://paper.design/privacy) - -## Support - -- Documentation: [paper.design/docs/mcp](https://paper.design/docs/mcp) -- For issues or questions: team@paper.design diff --git a/scripts/mcpb-manifest.mjs b/scripts/mcpb-manifest.mjs new file mode 100644 index 0000000..da7ace5 --- /dev/null +++ b/scripts/mcpb-manifest.mjs @@ -0,0 +1,70 @@ +/** + * Build the effective MCPB manifest for an extension directory: + * source manifest.json + README.md as long_description. + */ + +import { existsSync, readFileSync, realpathSync } from "fs"; +import { join, relative } from "path"; + +export function buildMcpbManifest(extensionDir, { root } = {}) { + const manifestPath = join(extensionDir, "manifest.json"); + const readmePath = join(extensionDir, "README.md"); + + if (!existsSync(manifestPath)) { + throw new Error(`missing manifest.json`); + } + if (!existsSync(readmePath)) { + throw new Error(`missing README.md`); + } + + const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); + const readme = readFileSync(readmePath, "utf-8").replace(/\r\n/g, "\n").trimEnd(); + + const built = { + ...manifest, + long_description: readme, + }; + + // Prefer a stable field order for the packed artifact. + const ordered = { + ...(built.$schema !== undefined ? { $schema: built.$schema } : {}), + manifest_version: built.manifest_version, + name: built.name, + ...(built.display_name !== undefined ? { display_name: built.display_name } : {}), + version: built.version, + description: built.description, + long_description: built.long_description, + author: built.author, + ...(built.homepage !== undefined ? { homepage: built.homepage } : {}), + ...(built.documentation !== undefined ? { documentation: built.documentation } : {}), + ...(built.support !== undefined ? { support: built.support } : {}), + ...(built.repository !== undefined ? { repository: built.repository } : {}), + ...(built.license !== undefined ? { license: built.license } : {}), + ...(built.privacy_policies !== undefined + ? { privacy_policies: built.privacy_policies } + : {}), + ...(built.icon !== undefined ? { icon: built.icon } : {}), + ...(built.screenshots !== undefined ? { screenshots: built.screenshots } : {}), + ...(built.keywords !== undefined ? { keywords: built.keywords } : {}), + server: built.server, + ...(built.compatibility !== undefined ? { compatibility: built.compatibility } : {}), + ...(built.tools_generated !== undefined + ? { tools_generated: built.tools_generated } + : {}), + }; + + for (const [key, value] of Object.entries(built)) { + if (!(key in ordered)) ordered[key] = value; + } + + let readmeLabel = readmePath; + if (root) { + try { + readmeLabel = relative(root, realpathSync(readmePath)); + } catch { + readmeLabel = relative(root, readmePath); + } + } + + return { manifest: ordered, readmeLabel }; +} diff --git a/scripts/validate-mcpb-artifact.mjs b/scripts/validate-mcpb-artifact.mjs new file mode 100644 index 0000000..16ceb1a --- /dev/null +++ b/scripts/validate-mcpb-artifact.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +/** + * Ensure the committed paper.mcpb matches a fresh deterministic pack. + * Relies on pack.ts producing bit-identical zips (fixed modes/mtimes + TZ=UTC). + */ + +import { existsSync } from "fs"; +import { resolve, dirname, relative } from "path"; +import { fileURLToPath } from "url"; +import { spawnSync } from "child_process"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, ".."); +const artifact = resolve( + root, + "claude-extensions/paper-desktop/dist/paper.mcpb" +); +const label = relative(root, artifact); + +if (!existsSync(artifact)) { + console.error(`ERROR: missing committed MCPB artifact: ${label}`); + console.error("Run: bun run pack:mcpb && git add " + label); + process.exit(1); +} + +const diff = spawnSync( + "git", + ["diff", "--exit-code", "--", label], + { cwd: root, encoding: "utf8" } +); + +if (diff.status === 0) { + console.log(`${label}: matches deterministic pack.`); + process.exit(0); +} + +if (diff.status !== 1) { + console.error(`ERROR: git diff failed for ${label}`); + if (diff.stderr) console.error(diff.stderr.trimEnd()); + process.exit(diff.status ?? 1); +} + +console.error(`ERROR: ${label} is out of date (or not packed deterministically).`); +console.error("Run: bun run pack:mcpb && git add " + label); +if (diff.stdout) console.error(diff.stdout.trimEnd()); +process.exit(1); diff --git a/scripts/validate-mcpb.mjs b/scripts/validate-mcpb.mjs new file mode 100644 index 0000000..a9c1c8a --- /dev/null +++ b/scripts/validate-mcpb.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node + +/** + * Validate Claude Desktop MCPB manifests with the official @anthropic-ai/mcpb CLI. + * Injects README.md as long_description into a temp manifest (source file + * unchanged), and stages referenced icon/screenshot files. + */ + +import { + readdirSync, + existsSync, + statSync, + mkdtempSync, + mkdirSync, + writeFileSync, + cpSync, + rmSync, +} from "fs"; +import { resolve, dirname, relative, join } from "path"; +import { tmpdir } from "os"; +import { fileURLToPath } from "url"; +import { spawnSync } from "child_process"; +import { buildMcpbManifest } from "./mcpb-manifest.mjs"; + +function stageManifestAssets(extensionDir, tempDir, manifest) { + const assetPaths = [ + ...(typeof manifest.icon === "string" ? [manifest.icon] : []), + ...(Array.isArray(manifest.screenshots) ? manifest.screenshots : []), + ]; + + for (const rel of assetPaths) { + if (typeof rel !== "string" || rel.includes("..") || rel.startsWith("/")) { + throw new Error(`refusing unsafe asset path: ${rel}`); + } + const src = join(extensionDir, rel); + const dest = join(tempDir, rel); + mkdirSync(dirname(dest), { recursive: true }); + cpSync(src, dest); + } +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, ".."); +const extensionsRoot = resolve(root, "claude-extensions"); + +if (!existsSync(extensionsRoot) || !statSync(extensionsRoot).isDirectory()) { + console.error( + `ERROR: claude-extensions directory not found at ${relative(root, extensionsRoot)}` + ); + process.exit(1); +} + +const extensions = readdirSync(extensionsRoot, { withFileTypes: true }).filter( + (entry) => entry.isDirectory() +); + +if (extensions.length === 0) { + console.error("ERROR: no Claude Desktop extensions found under claude-extensions/"); + process.exit(1); +} + +let failed = false; + +for (const entry of extensions) { + const extensionDir = join(extensionsRoot, entry.name); + const label = `claude-extensions/${entry.name}`; + let tempDir; + + try { + const { manifest, readmeLabel } = buildMcpbManifest(extensionDir, { root }); + tempDir = mkdtempSync(join(tmpdir(), "mcpb-validate-")); + const tempManifest = join(tempDir, "manifest.json"); + writeFileSync(tempManifest, `${JSON.stringify(manifest, null, 2)}\n`); + stageManifestAssets(extensionDir, tempDir, manifest); + + const result = spawnSync( + "bunx", + ["@anthropic-ai/mcpb", "validate", tempManifest], + { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + } + ); + + if (result.status !== 0) { + failed = true; + console.error(`ERROR: ${label}: mcpb validate failed`); + if (result.stdout) console.error(result.stdout.trimEnd()); + if (result.stderr) console.error(result.stderr.trimEnd()); + continue; + } + + console.log( + `${label}: MCPB manifest valid (long_description from ${readmeLabel}).` + ); + } catch (err) { + failed = true; + console.error(`ERROR: ${label}: ${err.message}`); + } finally { + if (tempDir) rmSync(tempDir, { recursive: true, force: true }); + } +} + +if (failed) { + console.error("\nMCPB validation failed."); + process.exit(1); +} + +console.log("MCPB validation passed.");