diff --git a/bun.lock b/bun.lock index 7d530a1..f91107b 100644 --- a/bun.lock +++ b/bun.lock @@ -39,6 +39,14 @@ "name": "@microvoid/convax-plugin-hello-convax", "version": "0.1.0" }, + "packages/plugins/jianying-editor": { + "name": "@microvoid/convax-plugin-jianying-editor", + "version": "2.0.0", + "dependencies": { + "@microvoid/convax-jianying-editor-mcp": "workspace:*", + "@microvoid/convax-skill-jianying-editor": "workspace:*" + } + }, "packages/plugins/multi-angle": { "name": "@microvoid/convax-plugin-multi-angle", "version": "0.1.0" @@ -106,6 +114,10 @@ "name": "@microvoid/convax-skill-image-remix", "version": "0.2.0" }, + "packages/skills/jianying-editor": { + "name": "@microvoid/convax-skill-jianying-editor", + "version": "2.0.0" + }, "packages/skills/short-drama-screenwriter": { "name": "@microvoid/convax-skill-short-drama-screenwriter", "version": "0.3.0" @@ -155,6 +167,17 @@ "typescript": "5.9.3" } }, + "packages/tools/jianying-editor-mcp": { + "name": "@microvoid/convax-jianying-editor-mcp", + "version": "1.0.0", + "bin": { + "convax-jianying-editor-mcp": "dist/convax-jianying-editor-mcp" + }, + "devDependencies": { + "@types/bun": "1.3.14", + "typescript": "5.9.3" + } + }, "packages/tools/xiaoyunque-mcp": { "name": "@microvoid/convax-xiaoyunque-mcp", "version": "0.3.4", @@ -171,154 +194,168 @@ "@floating-ui/core": [ "@floating-ui/core@1.8.0", "", - { - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, + { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==" ], + "@floating-ui/dom": [ "@floating-ui/dom@1.8.0", "", - { - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, + { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==" ], + "@floating-ui/react-dom": [ "@floating-ui/react-dom@2.1.9", "", { - "dependencies": { - "@floating-ui/dom": "^1.8.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } + "dependencies": { "@floating-ui/dom": "^1.8.0" }, + "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==" ], + "@floating-ui/utils": [ "@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==" ], + "@microvoid/convax-chatcut-media-import-mcp": [ "@microvoid/convax-chatcut-media-import-mcp@workspace:packages/tools/chatcut-media-import-mcp" ], - "@microvoid/convax-codex-mcp": [ - "@microvoid/convax-codex-mcp@workspace:packages/tools/codex-mcp" - ], - "@microvoid/convax-ffmpeg-mcp": [ - "@microvoid/convax-ffmpeg-mcp@workspace:packages/tools/ffmpeg-mcp" - ], - "@microvoid/convax-plugin-chatcut": [ - "@microvoid/convax-plugin-chatcut@workspace:packages/plugins/chatcut" - ], + + "@microvoid/convax-codex-mcp": ["@microvoid/convax-codex-mcp@workspace:packages/tools/codex-mcp"], + + "@microvoid/convax-ffmpeg-mcp": ["@microvoid/convax-ffmpeg-mcp@workspace:packages/tools/ffmpeg-mcp"], + + "@microvoid/convax-jianying-editor-mcp": [ + "@microvoid/convax-jianying-editor-mcp@workspace:packages/tools/jianying-editor-mcp" + ], + + "@microvoid/convax-plugin-chatcut": ["@microvoid/convax-plugin-chatcut@workspace:packages/plugins/chatcut"], + "@microvoid/convax-plugin-codex-service": [ "@microvoid/convax-plugin-codex-service@workspace:packages/plugins/codex-service" ], + "@microvoid/convax-plugin-convax-pet": [ "@microvoid/convax-plugin-convax-pet@workspace:packages/plugins/convax-pet" ], + "@microvoid/convax-plugin-ffmpeg-tools": [ "@microvoid/convax-plugin-ffmpeg-tools@workspace:packages/plugins/ffmpeg-tools" ], + "@microvoid/convax-plugin-hello-convax": [ "@microvoid/convax-plugin-hello-convax@workspace:packages/plugins/hello-convax" ], + + "@microvoid/convax-plugin-jianying-editor": [ + "@microvoid/convax-plugin-jianying-editor@workspace:packages/plugins/jianying-editor" + ], + "@microvoid/convax-plugin-multi-angle": [ "@microvoid/convax-plugin-multi-angle@workspace:packages/plugins/multi-angle" ], + "@microvoid/convax-plugin-panorama-viewer": [ "@microvoid/convax-plugin-panorama-viewer@workspace:packages/plugins/panorama-viewer" ], + "@microvoid/convax-plugin-relight-studio": [ "@microvoid/convax-plugin-relight-studio@workspace:packages/plugins/relight-studio" ], + + "@microvoid/convax-plugin-storyai-3d-director-desk": [ + "@microvoid/convax-plugin-storyai-3d-director-desk@workspace:packages/plugins/storyai-3d-director-desk" + ], + "@microvoid/convax-plugin-xiaoyunque-generation": [ "@microvoid/convax-plugin-xiaoyunque-generation@workspace:packages/plugins/xiaoyunque-generation" ], - "@microvoid/convax-skill-ad-idea": [ - "@microvoid/convax-skill-ad-idea@workspace:packages/skills/ad-idea" - ], - "@microvoid/convax-skill-audiobook": [ - "@microvoid/convax-skill-audiobook@workspace:packages/skills/audiobook" - ], - "@microvoid/convax-skill-chatcut": [ - "@microvoid/convax-skill-chatcut@workspace:packages/skills/chatcut" - ], + + "@microvoid/convax-skill-ad-idea": ["@microvoid/convax-skill-ad-idea@workspace:packages/skills/ad-idea"], + + "@microvoid/convax-skill-audiobook": ["@microvoid/convax-skill-audiobook@workspace:packages/skills/audiobook"], + + "@microvoid/convax-skill-chatcut": ["@microvoid/convax-skill-chatcut@workspace:packages/skills/chatcut"], + "@microvoid/convax-skill-clip-export": [ "@microvoid/convax-skill-clip-export@workspace:packages/skills/clip-export" ], + "@microvoid/convax-skill-ecommerce-image": [ "@microvoid/convax-skill-ecommerce-image@workspace:packages/skills/ecommerce-image" ], + "@microvoid/convax-skill-ffmpeg-canvas": [ "@microvoid/convax-skill-ffmpeg-canvas@workspace:packages/skills/ffmpeg-canvas" ], - "@microvoid/convax-skill-film-shot": [ - "@microvoid/convax-skill-film-shot@workspace:packages/skills/film-shot" - ], + + "@microvoid/convax-skill-film-shot": ["@microvoid/convax-skill-film-shot@workspace:packages/skills/film-shot"], + "@microvoid/convax-skill-hello-convax-guide": [ "@microvoid/convax-skill-hello-convax-guide@workspace:packages/skills/hello-convax-guide" ], + "@microvoid/convax-skill-image-remix": [ "@microvoid/convax-skill-image-remix@workspace:packages/skills/image-remix" ], + + "@microvoid/convax-skill-jianying-editor": [ + "@microvoid/convax-skill-jianying-editor@workspace:packages/skills/jianying-editor" + ], + "@microvoid/convax-skill-short-drama-screenwriter": [ "@microvoid/convax-skill-short-drama-screenwriter@workspace:packages/skills/short-drama-screenwriter" ], + "@microvoid/convax-skill-skill-creator": [ "@microvoid/convax-skill-skill-creator@workspace:packages/skills/skill-creator" ], + "@microvoid/convax-skill-skill-reviewer": [ "@microvoid/convax-skill-skill-reviewer@workspace:packages/skills/skill-reviewer" ], + "@microvoid/convax-skill-video-prompting": [ "@microvoid/convax-skill-video-prompting@workspace:packages/skills/video-prompting" ], - "@microvoid/convax-xiaoyunque-mcp": [ - "@microvoid/convax-xiaoyunque-mcp@workspace:packages/tools/xiaoyunque-mcp" - ], + + "@microvoid/convax-xiaoyunque-mcp": ["@microvoid/convax-xiaoyunque-mcp@workspace:packages/tools/xiaoyunque-mcp"], + "@radix-ui/number": [ "@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==" ], + "@radix-ui/primitive": [ "@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==" ], + "@radix-ui/react-accessible-icon": [ "@radix-ui/react-accessible-icon@1.1.11", "", { - "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.7" - }, + "dependencies": { "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HQDOFTKwSnmUij6l54wYJJtxTAnxI71+YJLOrjm2ladFB8HAV5Jt7hwaZPhWTGBkYoW4+ZAOfNZrLDh/qvxSYA==" ], + "@radix-ui/react-accordion": [ "@radix-ui/react-accordion@1.2.16", "", @@ -340,13 +377,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ==" ], + "@radix-ui/react-alert-dialog": [ "@radix-ui/react-alert-dialog@1.1.19", "", @@ -364,53 +399,43 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FA7n1f6D/DwGE0+AWxiY5LacNbbExQuEgMubeG06idEaH+mSLuf9dp/qBNqOnvbTQ+4gZ2ue1RATF1Ub91Mg5g==" ], + "@radix-ui/react-arrow": [ "@radix-ui/react-arrow@1.1.11", "", { - "dependencies": { - "@radix-ui/react-primitive": "2.1.7" - }, + "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==" ], + "@radix-ui/react-aspect-ratio": [ "@radix-ui/react-aspect-ratio@1.1.11", "", { - "dependencies": { - "@radix-ui/react-primitive": "2.1.7" - }, + "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUAhIVpBUvP5NNICjlaB1OFmtRLGqQqTF3ZOSGPoq3XeLXRFtHiWTRxSVEULgOd9GQR2c7tsYqDnhUennapZnw==" ], + "@radix-ui/react-avatar": [ "@radix-ui/react-avatar@1.2.2", "", @@ -428,13 +453,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sST0qh8GzOB7besQ3tMLWLyngnRuSk0gc/Hm+667KYKQFCt6Y6ZXv25WlqM7dIDK54ULCh5+CHmk4LIolzfz+A==" ], + "@radix-ui/react-checkbox": [ "@radix-ui/react-checkbox@1.3.7", "", @@ -455,13 +478,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JroKHfQBfh+fDuzpPsBC+pESkhuq8ql4hljTguz8MWnS35cISr3d/Jhl9kYrB44FlDtxCArYdDvTx+BSsJ64rQ==" ], + "@radix-ui/react-collapsible": [ "@radix-ui/react-collapsible@1.1.16", "", @@ -482,13 +503,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA==" ], + "@radix-ui/react-collection": [ "@radix-ui/react-collection@1.1.12", "", @@ -505,41 +524,31 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==" ], + "@radix-ui/react-compose-refs": [ "@radix-ui/react-compose-refs@1.1.3", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==" ], + "@radix-ui/react-context": [ "@radix-ui/react-context@1.2.0", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==" ], + "@radix-ui/react-context-menu": [ "@radix-ui/react-context-menu@2.3.3", "", @@ -557,13 +566,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-PS+gKE0z2prJ74Y0sM+brAGK4mYOHIR7TlcV5EJgUQ6E0xMvyswkK2X4yRqyganrzsRL+WCSKAPu0NQITICRWg==" ], + "@radix-ui/react-dialog": [ "@radix-ui/react-dialog@1.1.19", "", @@ -590,27 +597,21 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg==" ], + "@radix-ui/react-direction": [ "@radix-ui/react-direction@1.1.2", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==" ], + "@radix-ui/react-dismissable-layer": [ "@radix-ui/react-dismissable-layer@1.1.15", "", @@ -628,13 +629,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==" ], + "@radix-ui/react-dropdown-menu": [ "@radix-ui/react-dropdown-menu@2.1.20", "", @@ -654,27 +653,21 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw==" ], + "@radix-ui/react-focus-guards": [ "@radix-ui/react-focus-guards@1.1.4", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==" ], + "@radix-ui/react-focus-scope": [ "@radix-ui/react-focus-scope@1.1.12", "", @@ -690,13 +683,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==" ], + "@radix-ui/react-form": [ "@radix-ui/react-form@0.1.12", "", @@ -715,13 +706,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JTX94E4LDL91rzLg7X0mHPdxr0A8JEdVwZEmeOwZJSMDHCGW5DFtSlTSJozUyUs807IQmnvbfzKZFVCK5DmkqQ==" ], + "@radix-ui/react-hover-card": [ "@radix-ui/react-hover-card@1.1.19", "", @@ -743,50 +732,38 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2KTgMLQtKvicznQgbindEI2RZ3QbDIwU5gabjUPwFJsormjGDz+rUvO4NANmYwzEEpTcTONUt33vBHIfTIVSfw==" ], + "@radix-ui/react-id": [ "@radix-ui/react-id@1.1.2", "", { - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==" ], + "@radix-ui/react-label": [ "@radix-ui/react-label@2.1.11", "", { - "dependencies": { - "@radix-ui/react-primitive": "2.1.7" - }, + "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==" ], + "@radix-ui/react-menu": [ "@radix-ui/react-menu@2.1.20", "", @@ -817,13 +794,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w==" ], + "@radix-ui/react-menubar": [ "@radix-ui/react-menubar@1.1.20", "", @@ -846,13 +821,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gzFZvybgmwYsFBWDqanycIoEYnhyk8MMnuLamdFVHUZYGp4COM+sqXiwbnn0VMWqGLeeU7GV7jm+dXRa+Wufag==" ], + "@radix-ui/react-navigation-menu": [ "@radix-ui/react-navigation-menu@1.2.18", "", @@ -879,13 +852,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-K9HiuxZ6xCwSaHcIuUpxyhy4w5gpwzWjh9dHTSbMN3Ix4qAyVObS9RlU3zMycb0PO3v9Tpk0BXMwWvXOUbVXew==" ], + "@radix-ui/react-one-time-password-field": [ "@radix-ui/react-one-time-password-field@0.1.12", "", @@ -910,13 +881,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nQLu5OAcORDQp1EHAv6k3mJGV1hjMTw2NTGVAsGE1g/mWeNqAd1R5jyaAs3U+A8ZD/W8XNPY2yKT0ZdQnqo3NA==" ], + "@radix-ui/react-password-toggle-field": [ "@radix-ui/react-password-toggle-field@0.1.7", "", @@ -937,13 +906,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gB1Mr8vzdv1XzDjrtJTXmL0JORRs1B4g7ngUs0F+H2VvMOwXTZMTmLCl0wZZ3m7ylX8TssI7NCvgiSHmLuTm/A==" ], + "@radix-ui/react-popover": [ "@radix-ui/react-popover@1.1.19", "", @@ -971,13 +938,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ==" ], + "@radix-ui/react-popper": [ "@radix-ui/react-popper@1.3.3", "", @@ -1000,95 +965,75 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g==" ], + "@radix-ui/react-portal": [ "@radix-ui/react-portal@1.1.13", "", { - "dependencies": { - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, + "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==" ], + "@radix-ui/react-presence": [ "@radix-ui/react-presence@1.1.7", "", { - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, + "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==" ], + "@radix-ui/react-primitive": [ "@radix-ui/react-primitive@2.1.7", "", { - "dependencies": { - "@radix-ui/react-slot": "1.3.0" - }, + "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==" ], + "@radix-ui/react-progress": [ "@radix-ui/react-progress@1.1.12", "", { - "dependencies": { - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7" - }, + "dependencies": { "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZPHyI0JyzoH/rP0tq2uRaIZTj/4s8+kAbqPz+e2N8+ejHvwPJ889dHhqn+vh7PNvNeq+boAoH9yzqeoShzwF2w==" ], + "@radix-ui/react-radio-group": [ "@radix-ui/react-radio-group@1.4.3", "", @@ -1111,13 +1056,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WwZFjWV4s3aC1QtR3k04R+oANHtX2q6fgKlc7MCEiDNlnTxCZ3H8k3mHtEgVlOejystwk1WQgarQhNOQZ2bK1g==" ], + "@radix-ui/react-roving-focus": [ "@radix-ui/react-roving-focus@1.1.15", "", @@ -1141,13 +1084,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==" ], + "@radix-ui/react-scroll-area": [ "@radix-ui/react-scroll-area@1.2.14", "", @@ -1169,13 +1110,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bBODCWZK7JTbQLHs0uIP4f73wIWatakK4OS33UzkR1x897wu0PuO658a3f+6P2GEGyDzGYMuHRatMVoAk9WZTw==" ], + "@radix-ui/react-select": [ "@radix-ui/react-select@2.3.3", "", @@ -1210,33 +1149,27 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-L5RQTXz6Anxsf9CCv+pTgiAsUpyVj7rJxsGtmhFaEOJ++cVfXucv4qWfsIO0AIB4NAhi3yovWGVMKKS1Xf1Wrg==" ], + "@radix-ui/react-separator": [ "@radix-ui/react-separator@1.1.11", "", { - "dependencies": { - "@radix-ui/react-primitive": "2.1.7" - }, + "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==" ], + "@radix-ui/react-slider": [ "@radix-ui/react-slider@1.4.3", "", @@ -1260,30 +1193,22 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CWVVj+XaTom0SKCqw1EUgb0NuiLwS+N3OFG73mVEezKEjgNIvZiu0EevMelSSU+CbX3owbqJweG2gPU31WGC5A==" ], + "@radix-ui/react-slot": [ "@radix-ui/react-slot@1.3.0", "", { - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==" ], + "@radix-ui/react-switch": [ "@radix-ui/react-switch@1.3.3", "", @@ -1303,13 +1228,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1+mlB4/lxJfk5tgJ4g+R5mUCbRpPE1T9+UsEyeLYbGgMtwiMgmuTnfKz4Mw1nHALHjuwyxw4MLd4cSHn6pNSlQ==" ], + "@radix-ui/react-tabs": [ "@radix-ui/react-tabs@1.1.17", "", @@ -1330,13 +1253,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ==" ], + "@radix-ui/react-toast": [ "@radix-ui/react-toast@1.2.19", "", @@ -1361,13 +1282,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-SxfVZfVOibWKWdkf0Xx1awW2d09fQu4V4PXDY1j5hi4MVf7MWdJZqTBJMa1KWtOr1S6GGtCk02nniZ0Iia+dHw==" ], + "@radix-ui/react-toggle": [ "@radix-ui/react-toggle@1.1.14", "", @@ -1383,13 +1302,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw==" ], + "@radix-ui/react-toggle-group": [ "@radix-ui/react-toggle-group@1.1.15", "", @@ -1409,13 +1326,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ==" ], + "@radix-ui/react-toolbar": [ "@radix-ui/react-toolbar@1.1.15", "", @@ -1435,13 +1350,11 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t/iEuVjUnXXtrsGK40AA43uIx37sn3AqZ7oAVnPICK6lFJP6dzMzWR3U9b6eCfFjb6wtSEqkJ9Rn9xDjiOx20g==" ], + "@radix-ui/react-tooltip": [ "@radix-ui/react-tooltip@1.2.12", "", @@ -1466,269 +1379,199 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA==" ], + "@radix-ui/react-use-callback-ref": [ "@radix-ui/react-use-callback-ref@1.1.2", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==" ], + "@radix-ui/react-use-controllable-state": [ "@radix-ui/react-use-controllable-state@1.2.3", "", { - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==" ], + "@radix-ui/react-use-effect-event": [ "@radix-ui/react-use-effect-event@0.0.3", "", { - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==" ], + "@radix-ui/react-use-escape-keydown": [ "@radix-ui/react-use-escape-keydown@1.1.3", "", { - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.2" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg==" ], + "@radix-ui/react-use-is-hydrated": [ "@radix-ui/react-use-is-hydrated@0.1.1", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==" ], + "@radix-ui/react-use-layout-effect": [ "@radix-ui/react-use-layout-effect@1.1.2", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==" ], + "@radix-ui/react-use-previous": [ "@radix-ui/react-use-previous@1.1.2", "", { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==" ], + "@radix-ui/react-use-rect": [ "@radix-ui/react-use-rect@1.1.2", "", { - "dependencies": { - "@radix-ui/rect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "@radix-ui/rect": "1.1.2" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==" ], + "@radix-ui/react-use-size": [ "@radix-ui/react-use-size@1.1.2", "", { - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==" ], + "@radix-ui/react-visually-hidden": [ "@radix-ui/react-visually-hidden@1.2.7", "", { - "dependencies": { - "@radix-ui/react-primitive": "2.1.7" - }, + "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==" ], + "@radix-ui/rect": [ "@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==" ], + "@types/bun": [ "@types/bun@1.3.14", "", - { - "dependencies": { - "bun-types": "1.3.14" - } - }, + { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==" ], + "@types/node": [ "@types/node@26.1.1", "", - { - "dependencies": { - "undici-types": "~8.3.0" - } - }, + { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==" ], + "@types/react": [ "@types/react@19.2.14", "", - { - "dependencies": { - "csstype": "^3.2.2" - } - }, + { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==" ], + "@types/react-dom": [ "@types/react-dom@19.2.3", "", - { - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, + { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==" ], + "acorn": [ "acorn@8.17.0", "", - { - "bin": { - "acorn": "bin/acorn" - } - }, + { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==" ], + "aria-hidden": [ "aria-hidden@1.2.6", "", - { - "dependencies": { - "tslib": "^2.0.0" - } - }, + { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==" ], + "bun-types": [ "bun-types@1.3.14", "", - { - "dependencies": { - "@types/node": "*" - } - }, + { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==" ], + "csstype": [ "csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" ], + "detect-node-es": [ "detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" ], + "get-nonce": [ "get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" ], + "radix-ui": [ "radix-ui@1.6.2", "", @@ -1796,32 +1639,25 @@ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] + "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-OwYUjzMwiInCUxgAWpPsavXC3Kh4iyi/49uU1/qZTG3RQDlvegyk1GOMiGvSkjua1RDb3JD3fo3eroL9FV4GQw==" ], + "react": [ "react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==" ], + "react-dom": [ "react-dom@19.2.4", "", - { - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, + { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==" ], + "react-remove-scroll": [ "react-remove-scroll@2.7.2", "", @@ -1833,113 +1669,80 @@ "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==" ], + "react-remove-scroll-bar": [ "react-remove-scroll-bar@2.3.8", "", { - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, + "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==" ], + "react-style-singleton": [ "react-style-singleton@2.2.3", "", { - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==" ], + "scheduler": [ "scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" ], + "tslib": [ "tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" ], + "typescript": [ "typescript@5.9.3", "", - { - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - } - }, + { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==" ], + "undici-types": [ "undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" ], + "use-callback-ref": [ "use-callback-ref@1.3.3", "", { - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "tslib": "^2.0.0" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==" ], + "use-sidecar": [ "use-sidecar@1.1.3", "", { - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": [ - "@types/react" - ] + "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, + "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, + "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==" ] diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..f6ad7b5 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install] +registry = "https://registry.npmjs.org" diff --git a/packages/plugins/jianying-editor/convax-package.json b/packages/plugins/jianying-editor/convax-package.json new file mode 100644 index 0000000..4b6fe08 --- /dev/null +++ b/packages/plugins/jianying-editor/convax-package.json @@ -0,0 +1,28 @@ +{ + "schema": "convax.package/1", + "kind": "plugin", + "id": "jianying-editor", + "name": "剪映导入", + "description": "将直接连接的 Canvas 图片和视频安全导入剪映当前草稿或新草稿。", + "version": "2.0.0", + "license": "MIT", + "compatibility": { + "pluginSchema": "convax.plugin/6", + "pluginHost": "convax.plugin-capability/1" + }, + "companions": [ + { + "command": "convax-jianying-editor-mcp", + "version": "1.0.0", + "source": "packages/tools/jianying-editor-mcp", + "targets": [ + { + "platform": "darwin", + "arch": "arm64", + "path": "dist/darwin-arm64/convax-jianying-editor-mcp" + } + ] + } + ], + "yanked": false +} diff --git a/packages/plugins/jianying-editor/package.json b/packages/plugins/jianying-editor/package.json new file mode 100644 index 0000000..5c233a3 --- /dev/null +++ b/packages/plugins/jianying-editor/package.json @@ -0,0 +1,15 @@ +{ + "name": "@microvoid/convax-plugin-jianying-editor", + "version": "2.0.0", + "private": true, + "type": "module", + "dependencies": { + "@microvoid/convax-jianying-editor-mcp": "workspace:*", + "@microvoid/convax-skill-jianying-editor": "workspace:*" + }, + "scripts": { + "validate": "bun ../../../tooling/validate.mjs --kind plugin --id jianying-editor", + "pack": "bun ../../../tooling/pack.mjs --kind plugin --id jianying-editor", + "test": "bun test" + } +} diff --git a/packages/plugins/jianying-editor/package/LICENSE b/packages/plugins/jianying-editor/package/LICENSE new file mode 100644 index 0000000..31b01ad --- /dev/null +++ b/packages/plugins/jianying-editor/package/LICENSE @@ -0,0 +1,7 @@ +MIT License + +Copyright (c) 2026 Microvoid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, subject to the conditions of the MIT License. diff --git a/packages/plugins/jianying-editor/package/README.md b/packages/plugins/jianying-editor/package/README.md new file mode 100644 index 0000000..cddd24a --- /dev/null +++ b/packages/plugins/jianying-editor/package/README.md @@ -0,0 +1,14 @@ +# 剪映导入 for Convax + +该插件将直接连接到插件节点的 Canvas 图片和视频导入本机剪映。它不会随 +Convax 默认安装;用户需要从 Convax Plugin Registry 主动安装。 + +插件 iframe 只能看到宿主提供的项目、画布和直接入边投影,不能读取原生 +路径或文件字节。点击导入后,Convax 才会把仍然有效的直接连接素材暂存为 +短期副本,并把副本交给 Registry 校验过的 `convax-jianying-editor-mcp` +companion。companion 负责 macOS 剪映进程/草稿识别、Deep Link 分发和 +loopback 文件传输,不包含 Canvas、Project、IPC 或 Convax Desktop 逻辑。 + +Agent 工作流由插件拥有的 `jianying-editor` Skill 约束:先检查草稿,当前 +草稿存在时必须让用户选择;新草稿只能从安全确认的无活动草稿状态创建。 +不确定或部分传输结果不会自动重试,以免重复导入。 diff --git a/packages/plugins/jianying-editor/package/UPSTREAM.md b/packages/plugins/jianying-editor/package/UPSTREAM.md new file mode 100644 index 0000000..f2b16c2 --- /dev/null +++ b/packages/plugins/jianying-editor/package/UPSTREAM.md @@ -0,0 +1,10 @@ +# Provenance + +This package is independently authored for Convax from the public +`convax.plugin/6`, `convax.plugin-capability/1`, generation-call, MCP, and JianYing +Deep Link contracts. + +The package contains no JianYing application code, credential, browser profile, +private draft database reader, or vendor SDK. The local companion observes only +the running process' open draft lock and transfers host-staged media over a +short-lived loopback server. diff --git a/packages/plugins/jianying-editor/package/assets/app.js b/packages/plugins/jianying-editor/package/assets/app.js new file mode 100644 index 0000000..e634014 --- /dev/null +++ b/packages/plugins/jianying-editor/package/assets/app.js @@ -0,0 +1,145 @@ +(() => { + "use strict" + + const PROTOCOL = "convax.plugin-capability/1" + const PLUGIN_ID = "jianying-editor" + const pending = new Map() + const elements = { + connection: document.getElementById("connection"), + count: document.getElementById("count"), + empty: document.getElementById("empty"), + export: document.getElementById("export"), + inputs: document.getElementById("inputs"), + inspect: document.getElementById("inspect"), + result: document.getElementById("result"), + scope: document.getElementById("scope"), + } + let inputs = [] + let port = null + let sequence = 0 + let busy = false + + function object(value) { + return value !== null && typeof value === "object" && !Array.isArray(value) + } + + function request(method, params) { + if (!port) return Promise.reject(new Error("Convax host is not connected")) + const id = `${PLUGIN_ID}-${++sequence}` + return new Promise((resolve, reject) => { + pending.set(id, { reject, resolve }) + port.postMessage({ + id, + method, + ...(params === undefined ? {} : { params }), + protocol: PROTOCOL, + type: "request", + }) + }) + } + + function receive(event) { + const message = event.data + if (!object(message) || message.protocol !== PROTOCOL) return + if (message.type === "event" && message.event === "canvas.connectedInputs.changed") { + void loadInputs() + return + } + if (message.type !== "response" || typeof message.id !== "string") return + const operation = pending.get(message.id) + if (!operation) return + pending.delete(message.id) + if (message.ok === true) operation.resolve(message.result) + else operation.reject(new Error(typeof message.error === "string" ? message.error : "Host request failed")) + } + + function normalizeInput(value) { + if (!object(value)) return null + const id = typeof value.id === "string" ? value.id : value.nodeId + const kind = typeof value.kind === "string" ? value.kind.toLowerCase() : "" + if (typeof id !== "string" || !["image", "video"].includes(kind)) return null + return { + id, + kind, + name: typeof value.name === "string" ? value.name : typeof value.label === "string" ? value.label : id, + role: kind === "image" ? "reference_image" : "reference_video", + } + } + + function render() { + elements.count.textContent = String(inputs.length) + elements.empty.hidden = inputs.length > 0 + elements.inputs.replaceChildren(...inputs.map((input) => { + const item = document.createElement("li") + const name = document.createElement("span") + const kind = document.createElement("span") + name.textContent = input.name + kind.textContent = input.kind === "image" ? "图片" : "视频" + item.append(name, kind) + return item + })) + elements.inspect.disabled = !port || busy + elements.export.disabled = !port || busy || inputs.length === 0 + } + + async function loadInputs() { + const result = await request("canvas.connectedInputs.list") + const values = object(result) && Array.isArray(result.inputs) ? result.inputs : Array.isArray(result) ? result : [] + inputs = values.map(normalizeInput).filter(Boolean) + render() + } + + async function execute(toolId, references = []) { + const result = await request("generation.canvas.execute", { + output: "text", + prompt: toolId === "draft.status" ? "Inspect JianYing draft state" : "Import connected Canvas media into JianYing", + references, + resultMode: "return", + toolId, + }) + if (!object(result) || typeof result.outputText !== "string") { + throw new Error("剪映工具没有返回有效结果") + } + return result.outputText + } + + async function run(action) { + busy = true + render() + elements.result.textContent = action === "inspect" ? "正在检查剪映…" : "正在交给剪映读取素材…" + try { + elements.result.textContent = action === "inspect" + ? await execute("draft.status") + : await execute("media.export", inputs.map((input) => ({ nodeId: input.id, role: input.role }))) + } catch (error) { + elements.result.textContent = error instanceof Error ? error.message : String(error) + } finally { + busy = false + render() + } + } + + async function initialize() { + const context = await request("host.context.get") + if (!object(context) || !object(context.project) || !object(context.canvas)) { + throw new Error("Convax returned an invalid Canvas context") + } + elements.scope.textContent = `${context.project.name ?? context.project.id} / ${context.canvas.name ?? context.canvas.id}` + elements.connection.textContent = "已连接" + await loadInputs() + } + + elements.inspect.addEventListener("click", () => void run("inspect")) + elements.export.addEventListener("click", () => void run("export")) + window.addEventListener("message", (event) => { + if (event.source !== window.parent || event.ports.length !== 1 || port) return + port = event.ports[0] + port.addEventListener("message", receive) + port.start() + void initialize().catch((error) => { + elements.connection.textContent = "连接失败" + elements.result.textContent = error instanceof Error ? error.message : String(error) + render() + }) + }) +})() diff --git a/packages/plugins/jianying-editor/package/assets/styles.css b/packages/plugins/jianying-editor/package/assets/styles.css new file mode 100644 index 0000000..8fbd553 --- /dev/null +++ b/packages/plugins/jianying-editor/package/assets/styles.css @@ -0,0 +1,81 @@ +:root { + color: #f5f6fb; + background: #111218; + font: 14px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +body { min-height: 100vh; margin: 0; } +main { + min-height: 100vh; + padding: 18px; + background: + radial-gradient(circle at 90% -10%, rgb(50 226 217 / 18%), transparent 38%), + linear-gradient(145deg, #191b24, #0e0f14); +} +header { display: flex; align-items: center; gap: 11px; } +header > span:nth-child(2) { display: grid; flex: 1; } +header small, section > p, .empty { color: #9da3b4; } +.logo { + display: grid; + width: 42px; + height: 42px; + place-items: center; + border-radius: 13px; + color: #0c1516; + background: linear-gradient(135deg, #65f4e7, #25cbd1); + font-size: 20px; + font-weight: 800; +} +.status, .count { + padding: 4px 9px; + border: 1px solid rgb(255 255 255 / 10%); + border-radius: 999px; + color: #b9c0d1; + background: rgb(255 255 255 / 5%); + font-size: 12px; +} +section { + margin-top: 18px; + padding: 16px; + border: 1px solid rgb(255 255 255 / 8%); + border-radius: 16px; + background: rgb(255 255 255 / 3%); +} +.title-row { display: flex; align-items: center; justify-content: space-between; } +.title-row small { color: #61ded8; font-size: 10px; font-weight: 700; letter-spacing: .12em; } +h1 { margin: 2px 0 0; font-size: 17px; } +section > p { margin: 8px 0 12px; } +ol { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } +li { + display: flex; + justify-content: space-between; + padding: 9px 11px; + border-radius: 10px; + background: rgb(255 255 255 / 5%); +} +li span:last-child { color: #8e96a9; } +.empty { margin: 0; text-align: center; } +.actions { display: grid; grid-template-columns: 1fr 1.6fr; gap: 10px; border: 0; padding: 0; background: none; } +button { + min-height: 42px; + border: 0; + border-radius: 11px; + color: inherit; + font: inherit; + font-weight: 700; + cursor: pointer; +} +button:disabled { cursor: not-allowed; opacity: .42; } +.secondary { background: #292c37; } +.primary { color: #071414; background: linear-gradient(135deg, #67f0e5, #2ad0d3); } +output { + display: block; + min-height: 42px; + margin-top: 14px; + padding: 10px 12px; + border-radius: 10px; + color: #b8bfce; + background: rgb(0 0 0 / 18%); + white-space: pre-wrap; +} diff --git a/packages/plugins/jianying-editor/package/index.html b/packages/plugins/jianying-editor/package/index.html new file mode 100644 index 0000000..bcc4895 --- /dev/null +++ b/packages/plugins/jianying-editor/package/index.html @@ -0,0 +1,34 @@ + + + + + + + 剪映导入 + + + +
+
+ + 剪映导入正在连接 Convax… + 等待宿主 +
+
+
+ CANVAS INPUTS

已连接素材

+ 0 +
+

将图片或视频节点直接连入此节点。只有点击导入后,宿主才会暂存并交给本机剪映。

+
    +

    暂无可导入素材。

    +
    +
    + + +
    + 安装插件不会自动启动剪映,也不会上传素材到网络。 +
    + + + diff --git a/packages/plugins/jianying-editor/package/manifest.json b/packages/plugins/jianying-editor/package/manifest.json new file mode 100644 index 0000000..f4e81c5 --- /dev/null +++ b/packages/plugins/jianying-editor/package/manifest.json @@ -0,0 +1,68 @@ +{ + "schema": "convax.plugin/6", + "id": "jianying-editor", + "name": "剪映导入", + "description": "将直接连接的 Canvas 图片和视频安全导入剪映当前草稿或新草稿。", + "version": "2.0.0", + "entry": "index.html", + "capabilities": [ + "canvas.connectedInputs.read", + "generation.execute" + ], + "contributes": { + "agent": { + "tools": [ + { + "id": "get_draft_status", + "tool": "draft.status" + }, + { + "id": "export_connected_media", + "tool": "media.export" + } + ] + }, + "generation": { + "models": [], + "tools": [ + { + "id": "draft.status", + "title": "检查剪映草稿", + "description": "检查剪映是否运行以及当前是否有稳定打开的草稿,并返回短期草稿令牌。", + "output": "text", + "delivery": "return", + "acceptedInputs": [] + }, + { + "id": "media.export", + "title": "导入到剪映", + "description": "将直接连接、由宿主暂存的 Canvas 图片和视频导入剪映当前草稿或新草稿。", + "output": "text", + "delivery": "return", + "inputBinding": "direct-incoming", + "acceptedInputs": [ + "reference_image", + "reference_video" + ] + } + ] + }, + "canvas": { + "renderer": { + "create": true, + "width": 600, + "height": 440 + } + }, + "skills": [ + { + "name": "jianying-editor", + "path": "skills/jianying-editor" + } + ] + }, + "runtime": { + "type": "mcp-stdio", + "command": "convax-jianying-editor-mcp" + } +} diff --git a/packages/plugins/jianying-editor/test/package.test.ts b/packages/plugins/jianying-editor/test/package.test.ts new file mode 100644 index 0000000..9e79985 --- /dev/null +++ b/packages/plugins/jianying-editor/test/package.test.ts @@ -0,0 +1,59 @@ +import fs from "node:fs/promises" +import path from "node:path" + +import { describe, expect, test } from "bun:test" + +const packageRoot = path.resolve(import.meta.dir, "..", "package") + +async function read(relativePath: string) { + return fs.readFile(path.join(packageRoot, ...relativePath.split("/")), "utf8") +} + +describe("JianYing Plugin package", () => { + test("declares an explicit-install, return-only, direct-input integration", async () => { + const manifest = JSON.parse(await read("manifest.json")) + expect(manifest).toMatchObject({ + capabilities: ["canvas.connectedInputs.read", "generation.execute"], + id: "jianying-editor", + schema: "convax.plugin/6", + version: "2.0.0", + runtime: { + command: "convax-jianying-editor-mcp", + type: "mcp-stdio", + }, + }) + expect(manifest.contributes.generation.tools).toEqual([ + expect.objectContaining({ acceptedInputs: [], delivery: "return", id: "draft.status" }), + expect.objectContaining({ + acceptedInputs: ["reference_image", "reference_video"], + delivery: "return", + id: "media.export", + inputBinding: "direct-incoming", + }), + ]) + expect(manifest.contributes.skills).toEqual([ + { name: "jianying-editor", path: "skills/jianying-editor" }, + ]) + }) + + test("keeps the iframe offline and delegates native work through host capabilities", async () => { + const html = await read("index.html") + const application = await read("assets/app.js") + const readme = await read("README.md") + + expect(html).toContain('src="assets/app.js"') + expect(html).not.toMatch(/(?:src|href)=["'](?:https?:|\/\/|\/)/u) + expect(application).toContain('PROTOCOL = "convax.plugin-capability/1"') + expect(application).toContain('PLUGIN_ID = "jianying-editor"') + expect(application).toContain('request("canvas.connectedInputs.list")') + expect(application).toContain('request("generation.canvas.execute"') + expect(application).toContain('resultMode: "return"') + expect(application).not.toMatch(/\bfetch\s*\(/u) + expect(application).not.toContain("XMLHttpRequest") + expect(application).not.toContain("localStorage") + expect(() => new Function(application)).not.toThrow() + expect(readme).toContain("不会随") + expect(readme).toContain("主动安装") + expect(readme).toContain("不包含 Canvas、Project、IPC") + }) +}) diff --git a/packages/skills/jianying-editor/convax-package.json b/packages/skills/jianying-editor/convax-package.json new file mode 100644 index 0000000..292fa55 --- /dev/null +++ b/packages/skills/jianying-editor/convax-package.json @@ -0,0 +1,14 @@ +{ + "schema": "convax.package/1", + "kind": "skill", + "id": "jianying-editor", + "name": "剪映导入", + "description": "将直接连接的 Convax Canvas 图片和视频安全导入剪映当前草稿或新草稿。", + "version": "2.0.0", + "license": "MIT", + "compatibility": { + "skillSchema": "opencode.skill/1" + }, + "yanked": false, + "ownerPluginId": "jianying-editor" +} diff --git a/packages/skills/jianying-editor/package.json b/packages/skills/jianying-editor/package.json new file mode 100644 index 0000000..86a531f --- /dev/null +++ b/packages/skills/jianying-editor/package.json @@ -0,0 +1,11 @@ +{ + "name": "@microvoid/convax-skill-jianying-editor", + "version": "2.0.0", + "private": true, + "type": "module", + "scripts": { + "validate": "bun ../../../tooling/validate.mjs --kind skill --id jianying-editor", + "pack": "bun ../../../tooling/pack.mjs --kind skill --id jianying-editor", + "test": "bun test" + } +} diff --git a/packages/skills/jianying-editor/package/LICENSE b/packages/skills/jianying-editor/package/LICENSE new file mode 100644 index 0000000..31b01ad --- /dev/null +++ b/packages/skills/jianying-editor/package/LICENSE @@ -0,0 +1,7 @@ +MIT License + +Copyright (c) 2026 Microvoid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, subject to the conditions of the MIT License. diff --git a/packages/skills/jianying-editor/package/SKILL.md b/packages/skills/jianying-editor/package/SKILL.md new file mode 100644 index 0000000..dfec32b --- /dev/null +++ b/packages/skills/jianying-editor/package/SKILL.md @@ -0,0 +1,60 @@ +--- +name: jianying-editor +description: Import directly connected Convax Canvas images and videos into JianYing, either into the stable current draft or a safely created new draft. Use when the user asks to send, import, or export Canvas media to 剪映 or JianYing. +--- + +# JianYing Canvas import + +Use only the installed JianYing Plugin operations advertised in the current +session. Do not inspect native paths, edit JianYing draft JSON, run shell +commands, call a Deep Link directly, or recreate the local companion. + +## Resolve the source + +1. Require the host-provided JianYing Plugin `ownerNodeId` and an ordered list of + direct incoming image or video node ids. If these are missing, ask the user to + connect the desired media to a JianYing Plugin node and start the import there. +2. Preserve direct-edge order. Never add a disconnected or merely selected Canvas + node. Never pass a native path, file URL, data URL, bytes, Project path, or + Canvas id. +3. Invoke the currently advertised local status operation corresponding to + `draft.status`. It accepts no media references or custom fields. + +## Select the draft + +Handle the returned JSON state exactly: + +- `active`: tell the user the returned `draftName` and ask whether to import into + that current draft or create a new draft. Wait for the answer. +- `no_active_draft` or `not_running`: select `new` without asking an unnecessary + extra question. +- `ambiguous`, `unavailable`, or `unsupported`: stop and report the returned + reason. Never reinterpret the state as no active draft. + +The returned `draftToken` is a short-lived, single-use observation receipt. Pass +it only as `draft_token` to the installed export operation. Never quote it in +prose, logs, node state, another tool, or the final answer. + +Creating a new draft while another draft remains active fails closed. If the user +chooses new from an active state, ask them to return JianYing to its home screen, +inspect again, and proceed only after the status becomes `no_active_draft` or +`not_running`. + +## Import and verify + +Call the advertised operation corresponding to `media.export` with: + +- the Plugin node as `ownerNodeId`; +- the ordered direct incoming references, using only `reference_image` or + `reference_video`; +- `toolInput.target` set to the explicit `current` or `new` decision; +- `toolInput.draft_token` set to the exact latest token. + +The host revalidates Plugin ownership and direct incoming edges, stages bounded +copies, and rechecks the sources immediately before launching the companion. Do +not bypass that boundary or call an operation from another Plugin. + +Report the returned draft name, imported media count, whether a new draft was +created, and the verified transfer status. On cancellation, expired observation, +changed draft, timeout, partial transfer, or unknown native outcome, do not retry +automatically because another attempt may duplicate materials. diff --git a/packages/skills/jianying-editor/package/agents/openai.yaml b/packages/skills/jianying-editor/package/agents/openai.yaml new file mode 100644 index 0000000..856a3bd --- /dev/null +++ b/packages/skills/jianying-editor/package/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "剪映导入" + short_description: "安全导入直接连接的 Canvas 图片和视频" + default_prompt: "Use $jianying-editor to inspect the current JianYing draft, confirm the target when required, and import only the directly connected Canvas media." diff --git a/packages/skills/jianying-editor/test/package.test.ts b/packages/skills/jianying-editor/test/package.test.ts new file mode 100644 index 0000000..033a9fc --- /dev/null +++ b/packages/skills/jianying-editor/test/package.test.ts @@ -0,0 +1,26 @@ +import fs from "node:fs/promises" +import path from "node:path" + +import { describe, expect, test } from "bun:test" + +const root = path.resolve(import.meta.dir, "..") + +describe("JianYing Skill package", () => { + test("keeps Canvas authority in the host and native behavior in the companion", async () => { + const skill = await fs.readFile(path.join(root, "package", "SKILL.md"), "utf8") + const metadata = JSON.parse(await fs.readFile(path.join(root, "convax-package.json"), "utf8")) + + expect(metadata).toMatchObject({ + id: "jianying-editor", + ownerPluginId: "jianying-editor", + version: "2.0.0", + }) + expect(skill).toContain("direct incoming") + expect(skill).toContain("draft.status") + expect(skill).toContain("draft_token") + expect(skill).toContain("single-use") + expect(skill).toContain("fails closed") + expect(skill).toContain("do not retry") + expect(skill).toContain("Do not inspect native paths, edit JianYing draft JSON") + }) +}) diff --git a/packages/tools/jianying-editor-mcp/.gitignore b/packages/tools/jianying-editor-mcp/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/packages/tools/jianying-editor-mcp/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/tools/jianying-editor-mcp/AGENTS.md b/packages/tools/jianying-editor-mcp/AGENTS.md new file mode 100644 index 0000000..87fe31f --- /dev/null +++ b/packages/tools/jianying-editor-mcp/AGENTS.md @@ -0,0 +1,11 @@ +# JianYing companion contract + +- Keep this process headless and independent of Convax source packages. +- Accept only the exact `convax.generation-call/1` host envelope. +- Treat every path as a temporary host-staged input; validate it immediately and + never persist or return it. +- Observe only process metadata and open draft lock files. Never parse or mutate + JianYing draft JSON. +- Deep Links must launch by the fixed bundle id without a shell. File transfer + stays on loopback, is tokenized, bounded, cancellable, and closed after use. +- Unknown or partial native outcomes fail closed and are never retried here. diff --git a/packages/tools/jianying-editor-mcp/LICENSE b/packages/tools/jianying-editor-mcp/LICENSE new file mode 100644 index 0000000..31b01ad --- /dev/null +++ b/packages/tools/jianying-editor-mcp/LICENSE @@ -0,0 +1,7 @@ +MIT License + +Copyright (c) 2026 Microvoid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, subject to the conditions of the MIT License. diff --git a/packages/tools/jianying-editor-mcp/README.md b/packages/tools/jianying-editor-mcp/README.md new file mode 100644 index 0000000..ab92df2 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/README.md @@ -0,0 +1,11 @@ +# Convax JianYing companion + +Reviewed local MCP companion for the `jianying-editor` Convax Plugin. It supports +macOS only and exposes two operations: + +- `draft.status`: stable observation of the running JianYing draft; +- `media.export`: import host-staged image/video references through a short-lived + loopback server and the JianYing Deep Link. + +This package intentionally has no Canvas, Project, Electron, IPC, renderer, +registry, credential, or persistent staging logic. diff --git a/packages/tools/jianying-editor-mcp/package.json b/packages/tools/jianying-editor-mcp/package.json new file mode 100644 index 0000000..a1c3a48 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/package.json @@ -0,0 +1,22 @@ +{ + "name": "@microvoid/convax-jianying-editor-mcp", + "version": "1.0.0", + "private": true, + "license": "MIT", + "type": "module", + "bin": { + "convax-jianying-editor-mcp": "dist/convax-jianying-editor-mcp" + }, + "scripts": { + "build": "bun build --minify --target=bun --outfile dist/convax-jianying-editor-mcp src/index.ts", + "build:release": "bun run build:release:darwin-arm64", + "build:release:darwin-arm64": "bun build --minify --target=bun --outfile dist/darwin-arm64/convax-jianying-editor-mcp src/index.ts && chmod 755 dist/darwin-arm64/convax-jianying-editor-mcp", + "clean": "rm -rf dist", + "test": "bun test", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/bun": "1.3.14", + "typescript": "5.9.3" + } +} diff --git a/packages/tools/jianying-editor-mcp/src/contracts.ts b/packages/tools/jianying-editor-mcp/src/contracts.ts new file mode 100644 index 0000000..17d4e1a --- /dev/null +++ b/packages/tools/jianying-editor-mcp/src/contracts.ts @@ -0,0 +1,138 @@ +import path from "node:path" + +export const generationCallSchema = "convax.generation-call/1" as const +export type DraftStatus = "active" | "ambiguous" | "no_active_draft" | "not_running" | "unsupported" +export type ExportTarget = "auto" | "current" | "new" + +export interface StagedReference { + kind: "file" + mimeType: string + name: string + nodeId: string + path: string + role: "reference_image" | "reference_video" +} + +export interface GenerationCall { + draftToken?: string + output: "text" + outputDirectory: string + prompt: string + references: StagedReference[] + schema: typeof generationCallSchema + target: ExportTarget +} + +export interface DraftObservation { + draft?: { name: string; path: string; pid: number } + processIds: number[] + reason?: string + status: DraftStatus +} + +export interface PublicDraftStatus { + draftName?: string + draftToken?: string + reason?: string + schema: "convax.jianying-draft-status/1" + status: DraftStatus +} + +export interface ExportResult { + createdDraft: boolean + draftName: string + importedMediaCount: number + schema: "convax.jianying-export-result/1" + transferStatus: "verified" +} + +export class InputError extends Error { + constructor(readonly publicMessage: string) { + super(publicMessage) + this.name = "InputError" + } +} + +export function record(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) throw new InputError(`${label} must be an object.`) + return value as Record +} + +function exactKeys(value: Record, allowed: readonly string[], label: string) { + const keys = new Set(allowed) + if (Object.keys(value).some((key) => !keys.has(key))) throw new InputError(`${label} contains unsupported fields.`) +} + +function text(value: unknown, label: string, maximum: number) { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > maximum || + value !== value.trim() || + /[\u0000-\u001f\u007f]/u.test(value) + ) { + throw new InputError(`${label} is invalid.`) + } + return value +} + +export function parseGenerationCall(value: unknown, operation: "draft.status" | "media.export"): GenerationCall { + const input = record(value, "generation call") + exactKeys( + input, + ["draft_token", "output", "output_directory", "prompt", "references", "schema", "target"], + "generation call", + ) + if (input.schema !== generationCallSchema || input.output !== "text") { + throw new InputError("generation call contract is not supported.") + } + if (!Array.isArray(input.references)) throw new InputError("generation references must be an array.") + const minimum = operation === "media.export" ? 1 : 0 + const maximum = operation === "media.export" ? 32 : 0 + if (input.references.length < minimum || input.references.length > maximum) { + throw new InputError(operation === "media.export" + ? "JianYing export requires between one and 32 staged references." + : "JianYing status does not accept media references.") + } + const references = input.references.map((raw, index): StagedReference => { + const reference = record(raw, `reference ${index}`) + exactKeys(reference, ["kind", "mime_type", "name", "node_id", "path", "role"], `reference ${index}`) + if (reference.kind !== "file") throw new InputError(`reference ${index} must be a staged file.`) + if (reference.role !== "reference_image" && reference.role !== "reference_video") { + throw new InputError(`reference ${index} has an unsupported role.`) + } + const mimeType = text(reference.mime_type, `reference ${index} mime_type`, 256).toLowerCase().split(";", 1)[0]! + const expectedPrefix = reference.role === "reference_image" ? "image/" : "video/" + if (!mimeType.startsWith(expectedPrefix)) throw new InputError(`reference ${index} MIME type does not match its role.`) + const stagedPath = text(reference.path, `reference ${index} path`, 4_096) + if (!path.isAbsolute(stagedPath)) throw new InputError(`reference ${index} path must be host-staged.`) + return { + kind: "file", + mimeType, + name: text(reference.name, `reference ${index} name`, 512), + nodeId: text(reference.node_id, `reference ${index} node_id`, 256), + path: stagedPath, + role: reference.role, + } + }) + const target = input.target === undefined ? "auto" : input.target + if (target !== "auto" && target !== "current" && target !== "new") { + throw new InputError("JianYing target must be auto, current, or new.") + } + const draftToken = input.draft_token === undefined ? undefined : text(input.draft_token, "draft_token", 256) + if (operation === "draft.status" && (target !== "auto" || draftToken !== undefined)) { + throw new InputError("JianYing status does not accept target fields.") + } + if (operation === "media.export" && target !== "auto" && draftToken === undefined) { + throw new InputError("Explicit JianYing export requires draft_token.") + } + return { + ...(draftToken ? { draftToken } : {}), + output: "text", + outputDirectory: text(input.output_directory, "output_directory", 4_096), + prompt: text(input.prompt, "prompt", 20_000), + references, + schema: generationCallSchema, + target, + } +} diff --git a/packages/tools/jianying-editor-mcp/src/index.ts b/packages/tools/jianying-editor-mcp/src/index.ts new file mode 100644 index 0000000..2ccc85f --- /dev/null +++ b/packages/tools/jianying-editor-mcp/src/index.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env convax-bun + +import { McpServer } from "./mcp-server.ts" + +async function main() { + if (process.argv.length !== 2) throw new Error("Usage: convax-jianying-editor-mcp") + const server = new McpServer() + let shutdown: Promise | undefined + const stop = () => { + shutdown ??= server.shutdown(5_000) + void shutdown.then((drained) => process.exit(drained ? 0 : 1), () => process.exit(1)) + } + process.once("SIGINT", stop) + process.once("SIGTERM", stop) + try { + await server.run() + } finally { + process.removeListener("SIGINT", stop) + process.removeListener("SIGTERM", stop) + await (shutdown ?? server.shutdown(5_000)) + } +} + +if (import.meta.main) await main() diff --git a/packages/tools/jianying-editor-mcp/src/inspector.ts b/packages/tools/jianying-editor-mcp/src/inspector.ts new file mode 100644 index 0000000..d470164 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/src/inspector.ts @@ -0,0 +1,166 @@ +import { execFile as execFileCallback } from "node:child_process" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +import type { DraftObservation } from "./contracts.ts" + +const executableName = "VideoFusion-macOS" +const contentNames = ["draft_info.json", "draft_content.json"] as const + +export interface CommandResult { + exitCode: number + stderr: string + stdout: string +} + +export type CommandRunner = ( + executable: string, + args: readonly string[], + timeoutMs: number, + signal?: AbortSignal, +) => Promise + +export function runCommand( + executable: string, + args: readonly string[], + timeoutMs: number, + signal?: AbortSignal, +): Promise { + return new Promise((resolve, reject) => { + execFileCallback( + executable, + [...args], + { encoding: "utf8", maxBuffer: 1024 * 1024, signal, timeout: timeoutMs }, + (error, stdout, stderr) => { + const exitCode = error && "code" in error && typeof error.code === "number" ? error.code : error ? 1 : 0 + if (signal?.aborted) return reject(abortReason(signal)) + resolve({ exitCode, stderr, stdout }) + }, + ) + }) +} + +function abortReason(signal: AbortSignal) { + return signal.reason instanceof Error ? signal.reason : new DOMException("Operation cancelled", "AbortError") +} + +function processIds(stdout: string) { + const ids: number[] = [] + for (const line of stdout.split(/\r?\n/u)) { + const match = /^\s*(\d+)\s+(.+?)\s*$/u.exec(line) + if (!match || path.basename(match[2]!) !== executableName) continue + const pid = Number(match[1]) + if (Number.isSafeInteger(pid) && pid > 0) ids.push(pid) + } + return [...new Set(ids)].sort((left, right) => left - right) +} + +function defaultDraftRoots() { + const home = os.homedir() + return [ + path.join(home, "Movies", "JianyingPro", "User Data", "Projects", "com.lveditor.draft"), + path.join( + home, + "Library", + "Containers", + "com.lemon.lvpro", + "Data", + "Movies", + "JianyingPro", + "User Data", + "Projects", + "com.lveditor.draft", + ), + ] +} + +function contained(root: string, candidate: string) { + const relative = path.relative(root, candidate) + return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative) +} + +function lsofNames(stdout: string) { + return stdout.split(/\0|\r?\n/u).filter((field) => field.startsWith("n")).map((field) => field.slice(1)) +} + +async function draftFromLock( + lockPath: string, + pid: number, + roots: readonly string[], +): Promise { + if (path.basename(lockPath) !== ".locked") return undefined + const draftPath = path.dirname(lockPath) + if (!roots.some((root) => contained(root, draftPath))) return undefined + const stat = await fs.lstat(lockPath).catch(() => null) + if (!stat?.isFile()) return undefined + const contentFound = await Promise.all( + contentNames.map((name) => fs.lstat(path.join(draftPath, name)).then((value) => value.isFile(), () => false)), + ) + if (!contentFound.some(Boolean)) return undefined + return { name: path.basename(draftPath), path: draftPath, pid } +} + +export class JianyingDraftInspector { + constructor( + private readonly options: { + platform?: NodeJS.Platform + roots?: readonly string[] + run?: CommandRunner + sleep?: (milliseconds: number) => Promise + } = {}, + ) {} + + async inspect(signal?: AbortSignal): Promise { + if ((this.options.platform ?? process.platform) !== "darwin") { + return { processIds: [], reason: "JianYing import is supported only on macOS.", status: "unsupported" } + } + const first = await this.sample(signal) + await (this.options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds)))(100) + if (signal?.aborted) throw abortReason(signal) + const second = await this.sample(signal) + if ( + first.status === second.status && + first.processIds.join(",") === second.processIds.join(",") && + first.draft?.path === second.draft?.path && + first.draft?.pid === second.draft?.pid + ) { + return second + } + return { + processIds: second.processIds, + reason: "JianYing draft state changed while it was inspected. Try again after it settles.", + status: "ambiguous", + } + } + + private async sample(signal?: AbortSignal): Promise { + const run = this.options.run ?? runCommand + const ps = await run("/bin/ps", ["-axo", "pid=,comm="], 3_000, signal) + if (ps.exitCode !== 0) { + return { processIds: [], reason: "Could not inspect the JianYing process safely.", status: "ambiguous" } + } + const ids = processIds(ps.stdout) + if (ids.length === 0) return { processIds: [], status: "not_running" } + const drafts = [] + for (const pid of ids) { + const opened = await run("/usr/sbin/lsof", ["-F0n", "-p", String(pid)], 3_000, signal) + if (opened.exitCode !== 0) { + return { processIds: ids, reason: "Could not inspect JianYing's open draft safely.", status: "ambiguous" } + } + for (const name of lsofNames(opened.stdout)) { + const draft = await draftFromLock(name, pid, this.options.roots ?? defaultDraftRoots()) + if (draft) drafts.push(draft) + } + } + const unique = [...new Map(drafts.map((draft) => [draft.path, draft])).values()] + if (unique.length === 0) return { processIds: ids, status: "no_active_draft" } + const draft = unique[0] + if (unique.length === 1 && draft) return { draft, processIds: ids, status: "active" } + return { + processIds: ids, + reason: "More than one JianYing draft appears active.", + status: "ambiguous", + } + } +} diff --git a/packages/tools/jianying-editor-mcp/src/mcp-server.ts b/packages/tools/jianying-editor-mcp/src/mcp-server.ts new file mode 100644 index 0000000..27bf860 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/src/mcp-server.ts @@ -0,0 +1,214 @@ +import { + InputError, + parseGenerationCall, + record, +} from "./contracts.ts" +import { JianyingService } from "./service.ts" + +const protocolVersion = "2025-03-26" +const maximumRequestBytes = 1024 * 1024 + +const referenceSchema = { + additionalProperties: false, + properties: { + kind: { const: "file", type: "string" }, + mime_type: { maxLength: 256, minLength: 3, type: "string" }, + name: { maxLength: 512, minLength: 1, type: "string" }, + node_id: { maxLength: 256, minLength: 1, type: "string" }, + path: { maxLength: 4_096, minLength: 1, type: "string" }, + role: { enum: ["reference_image", "reference_video"], type: "string" }, + }, + required: ["kind", "mime_type", "name", "node_id", "path", "role"], + type: "object", +} as const + +function envelope(referenceLimits: { maximum: number; minimum: number }, explicitTarget: boolean) { + return { + additionalProperties: false, + properties: { + ...(explicitTarget + ? { + draft_token: { maxLength: 256, minLength: 1, type: "string" }, + target: { enum: ["auto", "current", "new"], type: "string" }, + } + : {}), + output: { const: "text", type: "string" }, + output_directory: { maxLength: 4_096, minLength: 1, type: "string" }, + prompt: { maxLength: 20_000, minLength: 1, type: "string" }, + references: { + items: referenceSchema, + maxItems: referenceLimits.maximum, + minItems: referenceLimits.minimum, + type: "array", + }, + schema: { const: "convax.generation-call/1", type: "string" }, + }, + required: ["output", "output_directory", "prompt", "references", "schema"], + type: "object", + } +} + +export const tools = [ + { + description: "Inspect the stable current JianYing draft and return a short-lived observation token.", + inputSchema: envelope({ maximum: 0, minimum: 0 }, false), + name: "draft.status", + }, + { + description: "Import directly connected, host-staged images and videos into a stable JianYing draft.", + inputSchema: envelope({ maximum: 32, minimum: 1 }, true), + name: "media.export", + }, +] as const + +interface Request { + id?: number | string | null + jsonrpc: "2.0" + method: string + params?: unknown +} + +function request(value: unknown): value is Request { + return Boolean(value && typeof value === "object" && !Array.isArray(value) + && (value as Record).jsonrpc === "2.0" + && typeof (value as Record).method === "string") +} + +export class McpServer { + readonly #handlers = new Set>() + readonly #inflight = new Map() + #closed = false + #reader: ReadableStreamDefaultReader | undefined + + constructor( + private readonly service = new JianyingService(), + private readonly write: (value: string) => void = (value) => { void Bun.stdout.write(value) }, + private readonly log: (message: string) => void = (message) => console.error(message), + ) {} + + async run(input: ReadableStream = Bun.stdin.stream()) { + if (this.#reader) throw new Error("MCP server is already running") + const reader = input.getReader() + this.#reader = reader + const decoder = new TextDecoder() + let buffer = "" + try { + while (!this.#closed) { + const chunk = await reader.read() + if (chunk.done) break + buffer += decoder.decode(chunk.value, { stream: true }) + if (Buffer.byteLength(buffer) > maximumRequestBytes) throw new Error("MCP request exceeded the size limit") + let newline = buffer.indexOf("\n") + while (newline >= 0) { + const line = buffer.slice(0, newline).trim() + buffer = buffer.slice(newline + 1) + if (line) { + try { + this.dispatch(JSON.parse(line)) + } catch { + this.error(null, -32700, "Parse error") + } + } + newline = buffer.indexOf("\n") + } + } + } finally { + this.close() + if (this.#reader === reader) this.#reader = undefined + reader.releaseLock() + } + } + + close() { + if (this.#closed) return + this.#closed = true + for (const controller of this.#inflight.values()) controller.abort("MCP server is closing") + void this.#reader?.cancel().catch(() => undefined) + } + + async shutdown(milliseconds: number) { + this.close() + if (this.#handlers.size === 0) return true + return Promise.race([ + Promise.allSettled([...this.#handlers]).then(() => true), + Bun.sleep(milliseconds).then(() => false), + ]) + } + + private dispatch(value: unknown) { + const operation = this.handle(value) + this.#handlers.add(operation) + void operation.finally(() => this.#handlers.delete(operation)) + } + + private async handle(value: unknown) { + if (!request(value)) return this.error(null, -32600, "Invalid Request") + if (value.method === "notifications/initialized") return + if (value.method === "notifications/cancelled") { + const params = value.params && typeof value.params === "object" ? value.params as Record : {} + if (typeof params.requestId === "string" || typeof params.requestId === "number") { + this.#inflight.get(params.requestId)?.abort("Request was cancelled") + } + return + } + if (value.id === undefined || value.id === null) return + if (value.method === "initialize") { + let params: Record + try { + params = record(value.params, "initialize params") + } catch { + return this.error(value.id, -32602, "Invalid initialize params") + } + if (params.protocolVersion !== protocolVersion) return this.error(value.id, -32602, "Unsupported MCP protocol") + return this.result(value.id, { + capabilities: { tools: {} }, + protocolVersion, + serverInfo: { name: "convax-jianying-editor-mcp", version: "1.0.0" }, + }) + } + if (value.method === "tools/list") return this.result(value.id, { tools }) + if (value.method !== "tools/call") return this.error(value.id, -32601, "Method not found") + const controller = new AbortController() + this.#inflight.set(value.id, controller) + try { + const params = record(value.params, "tools/call params") + if (params.name !== "draft.status" && params.name !== "media.export") { + return this.error(value.id, -32602, "Unknown tool") + } + const call = parseGenerationCall(params.arguments, params.name) + const output = params.name === "draft.status" + ? await this.service.status(controller.signal) + : await this.service.export(call, controller.signal) + const text = JSON.stringify(output) + this.result(value.id, { content: [{ text, type: "text" }], structuredContent: output }) + } catch (error) { + const cancelled = controller.signal.aborted || (error instanceof DOMException && error.name === "AbortError") + this.log(`[jianying-editor] ${cancelled ? "cancelled" : error instanceof InputError ? "invalid-input" : "failed"}`) + this.result(value.id, { + content: [{ + text: cancelled + ? "JianYing operation was cancelled." + : error instanceof InputError + ? error.publicMessage + : error instanceof Error ? error.message : "JianYing operation failed.", + type: "text", + }], + isError: true, + }) + } finally { + this.#inflight.delete(value.id) + } + } + + private result(id: number | string, result: unknown) { + this.send({ id, jsonrpc: "2.0", result }) + } + + private error(id: number | string | null, code: number, message: string) { + this.send({ error: { code, message }, id, jsonrpc: "2.0" }) + } + + private send(value: unknown) { + if (!this.#closed) this.write(`${JSON.stringify(value)}\n`) + } +} diff --git a/packages/tools/jianying-editor-mcp/src/service.ts b/packages/tools/jianying-editor-mcp/src/service.ts new file mode 100644 index 0000000..a9c9717 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/src/service.ts @@ -0,0 +1,145 @@ +import { randomUUID } from "node:crypto" + +import type { + DraftObservation, + ExportResult, + GenerationCall, + PublicDraftStatus, +} from "./contracts.ts" +import { JianyingDraftInspector } from "./inspector.ts" +import { JianyingTransport } from "./transport.ts" + +const tokenLifetimeMs = 5 * 60_000 +const actionable = new Set(["active", "no_active_draft", "not_running"]) + +function sameObservation(left: DraftObservation, right: DraftObservation) { + return ( + left.status === right.status && + left.processIds.join(",") === right.processIds.join(",") && + left.draft?.path === right.draft?.path && + left.draft?.pid === right.draft?.pid + ) +} + +function abortReason(signal: AbortSignal) { + return signal.reason instanceof Error ? signal.reason : new DOMException("Operation cancelled", "AbortError") +} + +export interface DraftInspector { + inspect(signal?: AbortSignal): Promise +} + +export interface MediaTransport { + createDraft(signal?: AbortSignal): Promise + import(references: GenerationCall["references"], signal?: AbortSignal): Promise<{ completed: number }> +} + +export class JianyingService { + readonly #tokens = new Map() + #queue = Promise.resolve() + + constructor( + private readonly inspector: DraftInspector = new JianyingDraftInspector(), + private readonly transport: MediaTransport = new JianyingTransport(), + ) {} + + async status(signal?: AbortSignal): Promise { + const observation = await this.inspector.inspect(signal) + const result: PublicDraftStatus = { + ...(observation.draft ? { draftName: observation.draft.name } : {}), + ...(observation.reason ? { reason: observation.reason } : {}), + schema: "convax.jianying-draft-status/1", + status: observation.status, + } + if (!actionable.has(observation.status)) return result + this.prune() + while (this.#tokens.size >= 1_000) this.#tokens.delete(this.#tokens.keys().next().value!) + const draftToken = `jianying_${randomUUID()}` + this.#tokens.set(draftToken, { expiresAt: Date.now() + tokenLifetimeMs, observation }) + return { ...result, draftToken } + } + + export(call: GenerationCall, signal?: AbortSignal): Promise { + const operation = this.#queue.then(() => this.exportLocked(call, signal)) + this.#queue = operation.then(() => undefined, () => undefined) + return operation + } + + private async exportLocked(call: GenerationCall, signal?: AbortSignal): Promise { + if (signal?.aborted) throw abortReason(signal) + const expected = call.target === "auto" + ? await this.requireActionable(await this.inspector.inspect(signal)) + : this.consume(call.draftToken!) + if (call.target === "current" && !expected.draft) { + throw new Error("The inspected JianYing draft is no longer active.") + } + if (call.target === "new" && expected.draft) { + throw new Error("Return JianYing to its home screen before creating a new draft.") + } + const before = await this.requireActionable(await this.inspector.inspect(signal)) + if (!sameObservation(expected, before)) { + throw new Error("JianYing draft state changed before import. Inspect it again.") + } + + const target = call.target === "auto" ? (before.draft ? "current" : "new") : call.target + let draft = before.draft + let createdDraft = false + if (target === "new") { + await this.transport.createDraft(signal) + draft = await this.waitForNewDraft(before, signal) + createdDraft = true + } + if (!draft) throw new Error("JianYing did not activate a draft before import.") + + const stable = await this.requireActionable(await this.inspector.inspect(signal)) + if (stable.status !== "active" || stable.draft?.path !== draft.path || stable.draft.pid !== draft.pid) { + throw new Error("JianYing draft changed before media transfer. Inspect it again.") + } + const transfer = await this.transport.import(call.references, signal) + if (transfer.completed !== call.references.length) { + throw new Error("JianYing media transfer could not be verified.") + } + return { + createdDraft, + draftName: draft.name, + importedMediaCount: call.references.length, + schema: "convax.jianying-export-result/1", + transferStatus: "verified", + } + } + + private consume(token: string) { + this.prune() + const record = this.#tokens.get(token) + this.#tokens.delete(token) + if (!record) throw new Error("The JianYing draft observation expired. Inspect it again.") + return record.observation + } + + private prune() { + const now = Date.now() + for (const [token, record] of this.#tokens) if (record.expiresAt <= now) this.#tokens.delete(token) + } + + private async requireActionable(observation: DraftObservation) { + if (actionable.has(observation.status)) return observation + throw new Error(observation.reason ?? "JianYing draft state could not be determined safely.") + } + + private async waitForNewDraft(previous: DraftObservation, signal?: AbortSignal) { + const deadline = Date.now() + 10_000 + while (Date.now() < deadline) { + if (signal?.aborted) throw abortReason(signal) + const observation = await this.inspector.inspect(signal) + if ( + observation.status === "active" && + observation.draft && + (observation.draft.path !== previous.draft?.path || observation.draft.pid !== previous.draft?.pid) + ) { + return observation.draft + } + await Bun.sleep(150) + } + throw new Error("JianYing did not expose a stable new draft in time.") + } +} diff --git a/packages/tools/jianying-editor-mcp/src/transport.ts b/packages/tools/jianying-editor-mcp/src/transport.ts new file mode 100644 index 0000000..48dc9fd --- /dev/null +++ b/packages/tools/jianying-editor-mcp/src/transport.ts @@ -0,0 +1,213 @@ +import { spawn } from "node:child_process" +import { randomBytes } from "node:crypto" +import { createReadStream } from "node:fs" +import fs from "node:fs/promises" +import { createServer, type Server, type ServerResponse } from "node:http" +import path from "node:path" +import type { Socket } from "node:net" + +import type { StagedReference } from "./contracts.ts" + +const bundleId = "com.lemon.lvpro" +const importBase = "videocut://com.ies.videocut/uganchor/anchor_point/nothing" +const createDraftUrl = "videocut://com.ies.videocut/main/draft/new_draft?force_create=true" + +interface PreparedReference extends StagedReference { + endpoint: string + size: number +} + +interface Range { + end: number + start: number +} + +function aborted(signal?: AbortSignal) { + if (signal?.aborted) { + throw signal.reason instanceof Error ? signal.reason : new DOMException("Operation cancelled", "AbortError") + } +} + +export async function openJianying(url: string, signal?: AbortSignal) { + if (!url.startsWith("videocut://")) throw new Error("JianYing Deep Link is invalid") + aborted(signal) + await new Promise((resolve, reject) => { + const child = spawn("/usr/bin/open", ["-b", bundleId, url], { stdio: ["ignore", "ignore", "pipe"] }) + const cancel = () => child.kill("SIGTERM") + signal?.addEventListener("abort", cancel, { once: true }) + child.once("error", reject) + child.once("close", (code) => { + signal?.removeEventListener("abort", cancel) + if (signal?.aborted) reject(signal.reason) + else if (code === 0) resolve() + else reject(new Error(`Could not open JianYing (exit code ${code ?? "unknown"})`)) + }) + }) +} + +function importUrl(urls: readonly string[]) { + const featureEntry = { + enter_from: "agent", + extension: {}, + feature: "nothing", + feature_context: { + material_import: true, + material_infos: urls.map((materialUri) => ({ + material_param: { add_to_material_panel: true }, + material_uri: materialUri, + })), + }, + sence: "editor", + sence_context: { material_import_by_user: true, new_draft: false }, + } + return `${importBase}?featureEntry=${encodeURIComponent(JSON.stringify(featureEntry))}` +} + +async function prepare(references: readonly StagedReference[]): Promise { + return Promise.all(references.map(async (reference) => { + const stat = await fs.lstat(reference.path) + if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 1) { + throw new Error("A staged JianYing input is not a non-empty regular file") + } + return { + ...reference, + endpoint: `/${randomBytes(32).toString("hex")}`, + size: stat.size, + } + })) +} + +function requestedRange(header: string | undefined, size: number): Range | undefined { + if (!header) return { end: size - 1, start: 0 } + const match = /^bytes=(\d*)-(\d*)$/u.exec(header.trim()) + if (!match || (!match[1] && !match[2])) return undefined + const start = match[1] ? Number(match[1]) : Math.max(0, size - Number(match[2])) + const end = match[2] ? Number(match[2]) : size - 1 + if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || start >= size) { + return undefined + } + return { end: Math.min(end, size - 1), start } +} + +function covered(intervals: readonly Range[]) { + const sorted = [...intervals].sort((left, right) => left.start - right.start) + let end = -1 + for (const interval of sorted) { + if (interval.start > end + 1) break + end = Math.max(end, interval.end) + } + return end + 1 +} + +function respondFile( + response: ServerResponse, + item: PreparedReference, + method: string, + rangeHeader: string | undefined, + completed: (range: Range) => void, +) { + const range = requestedRange(rangeHeader, item.size) + if (!range) { + response.statusCode = 416 + response.setHeader("Content-Range", `bytes */${item.size}`) + response.end() + return + } + response.setHeader("Accept-Ranges", "bytes") + response.setHeader("Content-Length", range.end - range.start + 1) + response.setHeader("Content-Type", item.mimeType) + if (rangeHeader) response.setHeader("Content-Range", `bytes ${range.start}-${range.end}/${item.size}`) + if (method === "HEAD") { + response.statusCode = rangeHeader ? 206 : 200 + response.end() + return + } + response.statusCode = rangeHeader ? 206 : 200 + const stream = createReadStream(item.path, { end: range.end, start: range.start }) + stream.once("error", () => response.destroy()) + stream.once("end", () => completed(range)) + stream.pipe(response) +} + +async function listen(server: Server) { + await new Promise((resolve, reject) => { + server.once("error", reject) + server.listen(0, "127.0.0.1", () => { + server.removeListener("error", reject) + resolve() + }) + }) + const address = server.address() + if (!address || typeof address === "string") throw new Error("Loopback server did not bind a TCP port") + return address.port +} + +async function close(server: Server, sockets: Set) { + for (const socket of sockets) socket.destroy() + await new Promise((resolve) => server.close(() => resolve())) +} + +export class JianyingTransport { + constructor( + private readonly options: { + open?: (url: string, signal?: AbortSignal) => Promise + timeoutMs?: number + } = {}, + ) {} + + createDraft(signal?: AbortSignal) { + return (this.options.open ?? openJianying)(createDraftUrl, signal) + } + + async import(references: readonly StagedReference[], signal?: AbortSignal) { + aborted(signal) + const prepared = await prepare(references) + const intervals = new Map(prepared.map((item) => [item.endpoint, [] as Range[]])) + let resolveAll!: () => void + const all = new Promise((resolve) => { + resolveAll = resolve + }) + const sockets = new Set() + const server = createServer((request, response) => { + if ((request.method !== "GET" && request.method !== "HEAD") || !request.url) { + response.statusCode = 405 + response.end() + return + } + const item = prepared.find((candidate) => candidate.endpoint === request.url) + if (!item) { + response.statusCode = 404 + response.end() + return + } + respondFile(response, item, request.method, request.headers.range, (range) => { + intervals.get(item.endpoint)!.push(range) + if (prepared.every((candidate) => covered(intervals.get(candidate.endpoint)!) >= candidate.size)) resolveAll() + }) + }) + server.on("connection", (socket) => { + sockets.add(socket) + socket.once("close", () => sockets.delete(socket)) + }) + try { + const port = await listen(server) + const urls = prepared.map((item) => `http://127.0.0.1:${port}${item.endpoint}`) + await (this.options.open ?? openJianying)(importUrl(urls), signal) + let timer: ReturnType | undefined + await Promise.race([ + all, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("JianYing did not finish reading all media in time")), this.options.timeoutMs ?? 15_000) + }), + signal + ? new Promise((_, reject) => signal.addEventListener("abort", () => reject(signal.reason), { once: true })) + : new Promise(() => undefined), + ]).finally(() => { + if (timer) clearTimeout(timer) + }) + return { completed: prepared.length } + } finally { + await close(server, sockets) + } + } +} diff --git a/packages/tools/jianying-editor-mcp/test/contracts.test.ts b/packages/tools/jianying-editor-mcp/test/contracts.test.ts new file mode 100644 index 0000000..86104aa --- /dev/null +++ b/packages/tools/jianying-editor-mcp/test/contracts.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "bun:test" + +import { InputError, parseGenerationCall } from "../src/contracts.ts" + +const envelope = { + output: "text", + output_directory: "/tmp/output", + prompt: "Import media", + references: [{ + kind: "file", + mime_type: "image/png", + name: "frame.png", + node_id: "node-1", + path: "/tmp/staged/frame.png", + role: "reference_image", + }], + schema: "convax.generation-call/1", +} + +describe("generation-call contract", () => { + test("accepts only host-staged image and video references", () => { + expect(parseGenerationCall(envelope, "media.export")).toMatchObject({ + references: [{ mimeType: "image/png", nodeId: "node-1", role: "reference_image" }], + target: "auto", + }) + expect(() => parseGenerationCall({ + ...envelope, + references: [{ ...envelope.references[0], path: "relative.png" }], + }, "media.export")).toThrow(InputError) + expect(() => parseGenerationCall({ + ...envelope, + references: [{ ...envelope.references[0], role: "audio" }], + }, "media.export")).toThrow("unsupported role") + }) + + test("requires an observation token for an explicit target", () => { + expect(() => parseGenerationCall({ ...envelope, target: "current" }, "media.export")).toThrow("draft_token") + expect(parseGenerationCall({ + ...envelope, + draft_token: "jianying_token", + target: "new", + }, "media.export")).toMatchObject({ + draftToken: "jianying_token", + target: "new", + }) + }) + + test("keeps status free of references and target input", () => { + const status = { ...envelope, prompt: "Inspect", references: [] } + expect(parseGenerationCall(status, "draft.status")).toMatchObject({ references: [], target: "auto" }) + expect(() => parseGenerationCall({ ...status, target: "current" }, "draft.status")).toThrow("target fields") + }) +}) diff --git a/packages/tools/jianying-editor-mcp/test/inspector.test.ts b/packages/tools/jianying-editor-mcp/test/inspector.test.ts new file mode 100644 index 0000000..1a2c246 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/test/inspector.test.ts @@ -0,0 +1,62 @@ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +import { afterEach, describe, expect, test } from "bun:test" + +import { JianyingDraftInspector, type CommandRunner } from "../src/inspector.ts" + +const roots: string[] = [] + +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => fs.rm(root, { force: true, recursive: true }))) +}) + +describe("JianYing draft inspection", () => { + test("accepts one stable open lock under the configured draft root", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "jianying-inspector-")) + roots.push(root) + const draft = path.join(root, "Demo") + await fs.mkdir(draft) + await fs.writeFile(path.join(draft, ".locked"), "") + await fs.writeFile(path.join(draft, "draft_info.json"), "{}") + const run: CommandRunner = async (executable) => executable === "/bin/ps" + ? { exitCode: 0, stderr: "", stdout: " 42 /Applications/JianyingPro.app/Contents/MacOS/VideoFusion-macOS\n" } + : { exitCode: 0, stderr: "", stdout: `p42\0n${path.join(draft, ".locked")}\0` } + + const result = await new JianyingDraftInspector({ + platform: "darwin", + roots: [root], + run, + sleep: async () => undefined, + }).inspect() + + expect(result).toEqual({ + draft: { name: "Demo", path: draft, pid: 42 }, + processIds: [42], + status: "active", + }) + }) + + test("does not guess when two samples disagree", async () => { + let calls = 0 + const run: CommandRunner = async (executable) => { + if (executable === "/bin/ps") { + calls += 1 + return { + exitCode: 0, + stderr: "", + stdout: calls === 1 ? "" : " 42 VideoFusion-macOS\n", + } + } + return { exitCode: 0, stderr: "", stdout: "" } + } + const result = await new JianyingDraftInspector({ + platform: "darwin", + roots: [], + run, + sleep: async () => undefined, + }).inspect() + expect(result).toMatchObject({ processIds: [42], status: "ambiguous" }) + }) +}) diff --git a/packages/tools/jianying-editor-mcp/test/service.test.ts b/packages/tools/jianying-editor-mcp/test/service.test.ts new file mode 100644 index 0000000..b18a490 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/test/service.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, mock, test } from "bun:test" + +import type { DraftObservation, GenerationCall } from "../src/contracts.ts" +import { JianyingService } from "../src/service.ts" + +const active: DraftObservation = { + draft: { name: "Demo", path: "/drafts/demo", pid: 42 }, + processIds: [42], + status: "active", +} + +function call(overrides: Partial = {}): GenerationCall { + return { + output: "text", + outputDirectory: "/tmp/output", + prompt: "Import", + references: [{ + kind: "file", + mimeType: "image/png", + name: "frame.png", + nodeId: "node-1", + path: "/tmp/frame.png", + role: "reference_image", + }], + schema: "convax.generation-call/1", + target: "auto", + ...overrides, + } +} + +describe("JianYing service", () => { + test("issues a single-use token and verifies current-draft transfer", async () => { + const inspect = mock(async () => active) + const importMedia = mock(async () => ({ completed: 1 })) + const service = new JianyingService( + { inspect }, + { createDraft: mock(async () => undefined), import: importMedia }, + ) + const status = await service.status() + expect(status).toMatchObject({ draftName: "Demo", status: "active" }) + expect(status.draftToken).toStartWith("jianying_") + if (!status.draftToken) throw new Error("Expected a draft token") + + await expect(service.export(call({ + draftToken: status.draftToken, + target: "current", + }))).resolves.toEqual({ + createdDraft: false, + draftName: "Demo", + importedMediaCount: 1, + schema: "convax.jianying-export-result/1", + transferStatus: "verified", + }) + expect(importMedia).toHaveBeenCalledTimes(1) + await expect(service.export(call({ + draftToken: status.draftToken, + target: "current", + }))).rejects.toThrow("expired") + }) + + test("fails closed when the observed draft changes", async () => { + const inspect = mock() + .mockResolvedValueOnce(active) + .mockResolvedValueOnce({ + ...active, + draft: { ...active.draft!, path: "/drafts/other" }, + }) + const importMedia = mock(async () => ({ completed: 1 })) + const service = new JianyingService( + { inspect }, + { createDraft: mock(async () => undefined), import: importMedia }, + ) + await expect(service.export(call())).rejects.toThrow("changed before import") + expect(importMedia).not.toHaveBeenCalled() + }) +}) diff --git a/packages/tools/jianying-editor-mcp/test/transport.test.ts b/packages/tools/jianying-editor-mcp/test/transport.test.ts new file mode 100644 index 0000000..83f3674 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/test/transport.test.ts @@ -0,0 +1,50 @@ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +import { afterEach, describe, expect, test } from "bun:test" + +import type { StagedReference } from "../src/contracts.ts" +import { JianyingTransport } from "../src/transport.ts" + +const roots: string[] = [] + +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => fs.rm(root, { force: true, recursive: true }))) +}) + +describe("JianYing loopback transport", () => { + test("serves only opaque Deep Link URLs and verifies ranged transfer coverage", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "jianying-transport-")) + roots.push(root) + const file = path.join(root, "frame.png") + await fs.writeFile(file, "abcdef") + const references: StagedReference[] = [{ + kind: "file", + mimeType: "image/png", + name: "frame.png", + nodeId: "node-1", + path: file, + role: "reference_image", + }] + let materialUrl = "" + const transport = new JianyingTransport({ + async open(url) { + const parsed = new URL(url) + const featureEntry = JSON.parse(parsed.searchParams.get("featureEntry") ?? "{}") + materialUrl = featureEntry.feature_context.material_infos[0].material_uri + expect(materialUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{64}$/u) + const first = await fetch(materialUrl, { headers: { Range: "bytes=0-2" } }) + expect(first.status).toBe(206) + expect(await first.text()).toBe("abc") + const second = await fetch(materialUrl, { headers: { Range: "bytes=3-5" } }) + expect(second.status).toBe(206) + expect(await second.text()).toBe("def") + }, + timeoutMs: 2_000, + }) + + await expect(transport.import(references)).resolves.toEqual({ completed: 1 }) + await expect(fetch(materialUrl)).rejects.toThrow() + }) +}) diff --git a/packages/tools/jianying-editor-mcp/tsconfig.json b/packages/tools/jianying-editor-mcp/tsconfig.json new file mode 100644 index 0000000..1a36fe1 --- /dev/null +++ b/packages/tools/jianying-editor-mcp/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "exactOptionalPropertyTypes": true, + "lib": ["ES2023", "DOM"], + "module": "Preserve", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2023", + "types": ["bun"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/registry/config.json b/registry/config.json index 063c492..82e1365 100644 --- a/registry/config.json +++ b/registry/config.json @@ -1,4 +1,4 @@ { - "sequence": 34, + "sequence": 35, "yanked": [] } diff --git a/tooling/registry.test.js b/tooling/registry.test.js index 10b6dbf..e0a2305 100644 --- a/tooling/registry.test.js +++ b/tooling/registry.test.js @@ -38,6 +38,7 @@ describe("source packages", () => { "plugin/convax-pet", "plugin/ffmpeg-tools", "plugin/hello-convax", + "plugin/jianying-editor", "plugin/multi-angle", "plugin/panorama-viewer", "plugin/relight-studio", @@ -52,6 +53,7 @@ describe("source packages", () => { "skill/film-shot", "skill/hello-convax-guide", "skill/image-remix", + "skill/jianying-editor", "skill/short-drama-screenwriter", "skill/skill-creator", "skill/skill-reviewer", @@ -61,6 +63,7 @@ describe("source packages", () => { const ffmpeg = packages.find((pkg) => pkg.metadata.id === "ffmpeg-tools") const ffmpegSkill = packages.find((pkg) => pkg.metadata.kind === "skill" && pkg.metadata.id === "ffmpeg-canvas") const hello = packages.find((pkg) => pkg.metadata.id === "hello-convax") + const jianying = packages.find((pkg) => pkg.metadata.id === "jianying-editor" && pkg.metadata.kind === "plugin") const codex = packages.find((pkg) => pkg.metadata.id === "codex-service") const multiAngle = packages.find((pkg) => pkg.metadata.id === "multi-angle") const panorama = packages.find((pkg) => pkg.metadata.id === "panorama-viewer") @@ -91,6 +94,31 @@ describe("source packages", () => { ) expect(hello.manifest.schema).toBe("convax.plugin/1") expect(hello.manifest.capabilities).toEqual([]) + expect(jianying.manifest).toEqual(expect.objectContaining({ + capabilities: ["canvas.connectedInputs.read", "generation.execute"], + runtime: { command: "convax-jianying-editor-mcp", type: "mcp-stdio" }, + schema: "convax.plugin/6", + version: "2.0.0", + })) + expect(jianying.manifest.contributes.generation.tools).toEqual([ + expect.objectContaining({ acceptedInputs: [], delivery: "return", id: "draft.status" }), + expect.objectContaining({ + acceptedInputs: ["reference_image", "reference_video"], + delivery: "return", + id: "media.export", + inputBinding: "direct-incoming", + }), + ]) + expect(jianying.metadata.companions).toEqual([{ + command: "convax-jianying-editor-mcp", + version: "1.0.0", + source: "packages/tools/jianying-editor-mcp", + targets: [{ + platform: "darwin", + arch: "arm64", + path: "dist/darwin-arm64/convax-jianying-editor-mcp", + }], + }]) expect(codex.manifest).toEqual(expect.objectContaining({ runtime: { command: "convax-codex-mcp", type: "mcp-stdio" }, schema: "convax.plugin/5", diff --git a/tooling/workspaces.test.js b/tooling/workspaces.test.js index 4bfb712..e7721f4 100644 --- a/tooling/workspaces.test.js +++ b/tooling/workspaces.test.js @@ -32,8 +32,23 @@ describe("Bun workspace ownership", () => { } }) - test("keeps one frozen root lockfile", async () => { - const lock = await readJson(path.join(root, "bun.lock")) + test("uses the official npm registry and keeps one frozen root lockfile", async () => { + const lockPath = path.join(root, "bun.lock") + const [lock, lockText, bunfig] = await Promise.all([ + readJson(lockPath), + fs.readFile(lockPath, "utf8"), + fs.readFile(path.join(root, "bunfig.toml"), "utf8"), + ]) + + expect(bunfig).toBe('[install]\nregistry = "https://registry.npmjs.org"\n') + expect(lockText).not.toContain("bnpm.byted.org") + for (const entry of Object.values(lock.packages)) { + const source = Array.isArray(entry) ? entry[1] : undefined + if (typeof source === "string" && source.startsWith("http")) { + expect(source).toStartWith("https://registry.npmjs.org/") + } + } + expect(lock.lockfileVersion).toBe(1) expect(Object.keys(lock.workspaces)).toContain("packages/tools/codex-mcp") expect(Object.keys(lock.workspaces)).toContain("packages/plugins/codex-service")