@remixmate/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/dist/argv.d.ts +15 -0
  4. package/dist/argv.js +51 -0
  5. package/dist/cli.d.ts +13 -0
  6. package/dist/cli.js +83 -0
  7. package/dist/handlers/gen-image.d.ts +13 -0
  8. package/dist/handlers/gen-image.js +161 -0
  9. package/dist/handlers/gen-voice.d.ts +16 -0
  10. package/dist/handlers/gen-voice.js +110 -0
  11. package/dist/handlers/index.d.ts +13 -0
  12. package/dist/handlers/index.js +14 -0
  13. package/dist/handlers/template-bind.d.ts +15 -0
  14. package/dist/handlers/template-bind.js +149 -0
  15. package/dist/http.d.ts +52 -0
  16. package/dist/http.js +110 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +8 -0
  19. package/dist/manifest.json +260 -0
  20. package/dist/progress.d.ts +23 -0
  21. package/dist/progress.js +15 -0
  22. package/dist/registry.d.ts +33 -0
  23. package/dist/registry.js +72 -0
  24. package/dist/runner.d.ts +17 -0
  25. package/dist/runner.js +63 -0
  26. package/package.json +54 -0
  27. package/skills/export-jianying/SKILL.md +251 -0
  28. package/skills/export-jianying/scripts/gen_jianying_draft.py +1099 -0
  29. package/skills/export-jianying/skill.json +24 -0
  30. package/skills/export-jianying/version.json +7 -0
  31. package/skills/gen-digital-human/SKILL.md +211 -0
  32. package/skills/gen-digital-human/scripts/gen_digital_human_video.py +574 -0
  33. package/skills/gen-digital-human/skill.json +23 -0
  34. package/skills/gen-digital-human/version.json +7 -0
  35. package/skills/gen-image/SKILL.md +130 -0
  36. package/skills/gen-image/skill.json +22 -0
  37. package/skills/gen-image/version.json +7 -0
  38. package/skills/gen-script/SKILL.md +283 -0
  39. package/skills/gen-script/scripts/gen_script.py +1208 -0
  40. package/skills/gen-script/skill.json +41 -0
  41. package/skills/gen-script/version.json +7 -0
  42. package/skills/gen-video/SKILL.md +170 -0
  43. package/skills/gen-video/scripts/gen_video.py +591 -0
  44. package/skills/gen-video/skill.json +24 -0
  45. package/skills/gen-video/version.json +7 -0
  46. package/skills/gen-voice/SKILL.md +104 -0
  47. package/skills/gen-voice/skill.json +21 -0
  48. package/skills/gen-voice/version.json +7 -0
  49. package/skills/prepare-video-assets/SKILL.md +192 -0
  50. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +57 -0
  51. package/skills/prepare-video-assets/skill.json +23 -0
  52. package/skills/prepare-video-assets/version.json +7 -0
  53. package/skills/render-video/SKILL.md +187 -0
  54. package/skills/render-video/scripts/_chrome_vendor.py +305 -0
  55. package/skills/render-video/scripts/_video_probe.py +174 -0
  56. package/skills/render-video/scripts/_vod_polling.py +86 -0
  57. package/skills/render-video/scripts/remote_renderer_client.py +236 -0
  58. package/skills/render-video/scripts/render_video.py +2288 -0
  59. package/skills/render-video/scripts/upload_video.py +193 -0
  60. package/skills/render-video/skill.json +21 -0
  61. package/skills/render-video/version.json +7 -0
  62. package/skills/template-bind/README.md +63 -0
  63. package/skills/template-bind/SKILL.md +234 -0
  64. package/skills/template-bind/scripts/check_contracts.py +196 -0
  65. package/skills/template-bind/scripts/match_template.py +215 -0
  66. package/skills/template-bind/scripts/registry_loader.py +379 -0
  67. package/skills/template-bind/scripts/render_job_client.py +162 -0
  68. package/skills/template-bind/scripts/sync_registry.py +44 -0
  69. package/skills/template-bind/scripts/template_paths.py +72 -0
  70. package/skills/template-bind/skill.json +20 -0
  71. package/skills/template-bind/version.json +7 -0
  72. package/skills/template-bind/video_dsl/README.md +97 -0
  73. package/skills/template-bind/video_dsl/runtime/__init__.py +11 -0
  74. package/skills/template-bind/video_dsl/runtime/dsl_validator.py +495 -0
  75. package/skills/template-bind/video_dsl/runtime/prompt_enhancer.py +274 -0
  76. package/skills/template-bind/video_dsl/runtime/template_binder.py +87 -0
  77. package/skills/template-bind/video_dsl/runtime/timeline_compiler.py +312 -0
  78. package/skills/template-bind/video_dsl/schema/render-plan-v1alpha1.json +193 -0
  79. package/skills/template-bind/video_dsl/schema/template-binding-v1alpha1.json +122 -0
  80. package/skills/template-bind/video_dsl/schema/template-definition-v1alpha1.json +247 -0
  81. package/skills/template-bind/video_dsl/schema/video-dsl-v1alpha1.json +378 -0
  82. package/skills/video-parser/SKILL.md +238 -0
  83. package/skills/video-parser/scripts/analyze_video.py +1038 -0
  84. package/skills/video-parser/scripts/deconstruct_video.py +602 -0
  85. package/skills/video-parser/scripts/parse_via_render.py +148 -0
  86. package/skills/video-parser/skill.json +21 -0
  87. package/skills/video-parser/version.json +7 -0
  88. package/skills/web-capture/SKILL.md +183 -0
  89. package/skills/web-capture/scripts/web_capture.py +461 -0
  90. package/skills/web-capture/skill.json +29 -0
  91. package/skills/web-capture/version.json +7 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aibearer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @remixmate/cli
2
+
3
+ > 原名 `ab-skill-cli`(bin `ab-skill`ļ¼‰ļ¼ŒēŽ°å·²ę›“åäøŗ **`@remixmate/cli`**(bin **`remixmate`**ļ¼‰ć€‚ę—§åŒ…å·²åœØ npm ę ‡č®°å¼ƒē”Øļ¼ŒčÆ·čæē§»åˆ°ę–°åŒ…ć€‚
4
+
5
+ AI media generation skills for Claude Code / Codex.
6
+
7
+ 9 skills covering the full short-video production pipeline: image / video / voice / digital-human asset generation, script planning, template binding, Remotion rendering, and Jianying (CapCut) draft export.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g @remixmate/cli
13
+ remixmate --help
14
+ remixmate --list
15
+ ```
16
+
17
+ ## Use in Claude Code
18
+
19
+ Copy the bundled `skills/` into your project-level or global `.claude/skills/` directory and Claude Code will pick them up:
20
+
21
+ ```bash
22
+ # project-level
23
+ mkdir -p .claude/skills
24
+ cp -R "$(npm root -g)/@remixmate/cli/skills/." .claude/skills/
25
+
26
+ # or global
27
+ mkdir -p ~/.claude/skills
28
+ cp -R "$(npm root -g)/@remixmate/cli/skills/." ~/.claude/skills/
29
+ ```
30
+
31
+ ## Use in Codex
32
+
33
+ Codex does not load `SKILL.md` natively. Tell Codex about the CLI in your system prompt and let it invoke `remixmate <skill> ...` when it needs media generation:
34
+
35
+ > When you need AI media generation (image / video / voice / talking-head / Jianying export), prefer running `remixmate <skill-id> --help` first to discover the flags, then `remixmate <skill-id> --json-output ...` to execute. Available skills: `remixmate --list`.
36
+
37
+ See the full intro page (Claude Code / Codex setup, env vars, end-to-end workflow examples) at the project homepage.
38
+
39
+ ## Environment
40
+
41
+ Most skills authenticate against ab-api with a `PRIV_TOKEN`. Without a valid token (and network access to ab-api) the template / media-generation skills are unavailable — the code is open-source, but the generation capabilities are hosted on the ab-api service. Copy `.env.example` to `.env` and source it before running:
42
+
43
+ ```bash
44
+ cp .env.example .env
45
+ # edit values
46
+ source .env
47
+ ```
48
+
49
+ Python skills (6 of 9) require `python3 >= 3.10`. `video-parser` additionally requires `ffmpeg`.
50
+
51
+ ---
52
+
53
+ ## AI åŖ’ä½“ē”Ÿęˆ Agent Skillsļ¼ˆäø­ę–‡ļ¼‰
54
+
55
+ ęœ¬é”¹ē›®åŒ…å« 9 äøŖ AI åŖ’ä½“ē”ŸęˆęŠ€čƒ½ļ¼Œč¦†ē›–ä»Žē“ ęē”Ÿęˆć€č„šęœ¬ē¼–ęŽ’ć€ęØ”ęæē»‘å®šć€č§†é¢‘ęø²ęŸ“åˆ°å‰Ŗę˜ åÆ¼å‡ŗēš„å®Œę•“č§†é¢‘å†…å®¹ē”Ÿäŗ§é“¾č·Æć€‚
56
+
57
+ ### ęŠ€čƒ½åˆ†å±‚
58
+
59
+ ```
60
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
61
+ │ ē¼–ęŽ’å±‚ Skills │
62
+ │ gen-script 主题 → Video DSLļ¼ˆč„šęœ¬ē”Ÿęˆļ¼‰ │
63
+ │ template-bind DSL → TemplateBindingļ¼ˆęØ”ęæåˆ—č”Øļ¼‰ │
64
+ │ prepare-video-assets DSL + Binding → ē“ ęč”„é½ļ¼ˆPhase 1) │
65
+ │ render-video RenderPlan → Remotion ęø²ęŸ“ļ¼ˆPhase 3) │
66
+ │ export-jianying ē“ ę → å‰Ŗę˜ č‰ēØæ ZIP │
67
+ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
68
+ │ 原子层 Skills │
69
+ │ gen-image ę–‡ē”Ÿå›¾ / å›¾ē”Ÿå›¾ļ¼ˆSeedream / Gemini) │
70
+ │ gen-video ę–‡ē”Ÿč§†é¢‘ļ¼ˆSeedance / Veo) │
71
+ │ gen-voice 语音合成(Minimax TTS) │
72
+ │ gen-digital-human ę•°å­—äŗŗå£ę’­ļ¼ˆå³ę¢¦ / é£žå½±ļ¼‰ │
73
+ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
74
+ │ 巄具层 Skills │
75
+ │ video-parser č§†é¢‘č§£ęž„ļ¼ˆéŸ³é¢‘ęå– / ASR / 关键帧) │
76
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
77
+ ```
78
+
79
+ ### ē›®å½•ē»“ęž„
80
+
81
+ ```
82
+ ā”œā”€ā”€ docs/
83
+ │ └── video-production-architecture.md # ęž¶ęž„ę–‡ę”£
84
+ ā”œā”€ā”€ skills/
85
+ │ ā”œā”€ā”€ gen-image/ # 原子: AI ē”Ÿå›¾
86
+ │ ā”œā”€ā”€ gen-video/ # 原子: AI ē”Ÿč§†é¢‘
87
+ │ ā”œā”€ā”€ gen-voice/ # 原子: 语音合成
88
+ │ ā”œā”€ā”€ gen-digital-human/ # 原子: ę•°å­—äŗŗå£ę’­
89
+ │ ā”œā”€ā”€ gen-script/ # ē¼–ęŽ’: 主题 → Video DSL
90
+ │ ā”œā”€ā”€ template-bind/ # ē¼–ęŽ’: ęØ”ęæåˆ—č”Øļ¼ˆå«ęØ”ęæę³Øå†Œč”Ø + DSL Schema)
91
+ │ ā”œā”€ā”€ prepare-video-assets/ # ē¼–ęŽ’: Phase 1 ē“ ęå‡†å¤‡ļ¼ˆthin wrapper)
92
+ │ ā”œā”€ā”€ render-video/ # ē¼–ęŽ’: Phase 3 Remotion ęø²ęŸ“ļ¼ˆå« render_video.py å®žēŽ°ļ¼‰
93
+ │ ā”œā”€ā”€ export-jianying/ # ē¼–ęŽ’: åÆ¼å‡ŗå‰Ŗę˜ č‰ēØæ ZIP
94
+ │ └── video-parser/ # å·„å…·: č§†é¢‘č§£ęž„äøŽåˆ†ęž
95
+ └── README.md
96
+ ```
97
+
98
+ ### ęŠ€čƒ½čÆ“ę˜Ž
99
+
100
+ | ęŠ€čƒ½ | ē±»åž‹ | čÆ“ę˜Ž | ę øåæƒč„šęœ¬ |
101
+ |------|------|------|----------|
102
+ | gen-image | 原子 | ę–‡ē”Ÿå›¾ / å›¾ē”Ÿå›¾ļ¼ˆSeedream态Gemini) | `gen_image.py` |
103
+ | gen-video | 原子 | ę–‡ē”Ÿč§†é¢‘ļ¼ˆSeedance态Veo) | `gen_video.py` |
104
+ | gen-voice | 原子 | 语音合成(Minimax TTS) | `gen_voice.py` |
105
+ | gen-digital-human | 原子 | ę•°å­—äŗŗå£ę’­ļ¼ˆå³ę¢¦ / é£žå½±ļ¼‰ | `gen_digital_human_video.py` |
106
+ | gen-script | ē¼–ęŽ’ | 主题 → Video DSL JSON | `gen_script.py` |
107
+ | template-bind | ē¼–ęŽ’ | ęØ”ęæåˆ—č”Øļ¼ˆē»‘å®šé€»č¾‘å†…åµŒäŗŽ prepare-video-assets) | `match_template.py` |
108
+ | prepare-video-assets | ē¼–ęŽ’ | DSL + Binding → ē“ ęč”„é½ → 落库 RenderPlan | `render_video.py --resolve-only`(wrapper) |
109
+ | render-video | ē¼–ęŽ’ | job_id → Remotion ęø²ęŸ“ → 上传 | `render_video.py` |
110
+ | export-jianying | ē¼–ęŽ’ | ē“ ę URL → å‰Ŗę˜ č‰ēØæ ZIPļ¼ˆę”ÆęŒä»Ž RenderPlan č‡ŖåŠØč½¬ę¢ļ¼‰ | `gen_jianying_draft.py` |
111
+ | video-parser | å·„å…· | 视频 → 音频 + ASR + 关键帧 + åœŗę™Æåˆ†ę®µ | `deconstruct_video.py` |
112
+
113
+ ### ę øåæƒé“¾č·Æ
114
+
115
+ 详见 [č§†é¢‘å†…å®¹ē”Ÿäŗ§ęž¶ęž„ę–‡ę”£](docs/video-production-architecture.md) 和 [ē¼–ęŽ’ęµēØ‹ęŒ‡å—](docs/orchestration-guide.md)怂
116
+
117
+ **链路 A:主题 → Remotion 视频**
118
+ ```
119
+ gen-script → āœ…ē”Øęˆ·ē”®č®¤č„šęœ¬ → prepare-video-assets(--template-id)
120
+ → āœ…ē”Øęˆ·ē”®č®¤ē“ ę → render-video(--job-id) → MP4
121
+ ```
122
+
123
+ **链路 B:主题 → å‰Ŗę˜ č‰ēØæ**
124
+ ```
125
+ gen-script → āœ…ē”Øęˆ·ē”®č®¤č„šęœ¬ → prepare-video-assets(--template-id)
126
+ → āœ…ē”Øęˆ·ē”®č®¤ē“ ę → export-jianying(--from-job-id) → å‰Ŗę˜  ZIP
127
+ ```
128
+
129
+ ### 测试
130
+
131
+ ```bash
132
+ # å„č„šęœ¬ --help åÆę­£åøøčæč”Œ
133
+ python3 skills/gen-image/scripts/gen_image.py --help
134
+ python3 skills/gen-video/scripts/gen_video.py --help
135
+ python3 skills/gen-voice/scripts/gen_voice.py --help
136
+ python3 skills/gen-digital-human/scripts/gen_digital_human_video.py --help
137
+ python3 skills/gen-script/scripts/gen_script.py --help
138
+ python3 skills/template-bind/scripts/match_template.py --help
139
+ python3 skills/render-video/scripts/render_video.py --help
140
+ python3 skills/prepare-video-assets/scripts/prepare_video_assets.py --help
141
+ python3 skills/export-jianying/scripts/gen_jianying_draft.py --help
142
+ python3 skills/video-parser/scripts/deconstruct_video.py --help
143
+
144
+ # ęŸ„ēœ‹åÆē”ØéŸ³č‰²
145
+ python3 skills/gen-voice/scripts/gen_voice.py --list-voices
146
+
147
+ # ęŸ„ēœ‹åÆē”ØęØ”ęæ
148
+ python3 skills/template-bind/scripts/match_template.py --list-templates
149
+ ```
150
+
151
+ ### å¼€å‘ęµēØ‹
152
+
153
+ - å…‹éš†ę­¤é”¹ē›®åˆ°ęœ¬åœ°
154
+ - 在 skills ē›®å½•äø‹å„ęŠ€čƒ½ę–‡ä»¶å¤¹äø­å¼€å‘
155
+ - ęÆäøŖęŠ€čƒ½åŒ…å« `SKILL.md`ļ¼ˆęŠ€čƒ½čÆ“ę˜Žļ¼‰ć€`version.json`ļ¼ˆå…ƒę•°ę®ļ¼‰å’Œ `scripts/`ļ¼ˆč„šęœ¬ļ¼‰
156
+
157
+ ### åæ«é€Ÿä½“éŖŒ
158
+
159
+ ```
160
+ @skills/gen-image/SKILL.md ē”Ÿęˆäø€å¼ ē†ŠēŒ«ēš„å›¾ē‰‡ļ¼Œ9:16ļ¼Œč°ƒē”Øgemini 3.1ļ¼Œå›½ē”»é£Ž + äø„ę ¼ęŒ‰čÆ„ę–‡ę”£ę‰§č”Œ
161
+ @skills/gen-video/SKILL.md ē”Ÿęˆäø€ę®µē†ŠēŒ«åœØē«¹ęž—å„”č·‘ēš„č§†é¢‘ļ¼Œ9:16ļ¼Œé•æåŗ¦6ē§’ļ¼Œč°ƒē”Øveo + äø„ę ¼ęŒ‰čÆ„ę–‡ę”£ę‰§č”Œ
162
+ @skills/gen-voice/SKILL.md ē”Ÿęˆäø€ę®µčÆ­éŸ³ļ¼Œä»‹ē»ē†ŠēŒ«ēš„ä¹ ę€§ļ¼Œå¤§ę¦‚100字左右 + äø„ę ¼ęŒ‰čÆ„ę–‡ę”£ę‰§č”Œ
163
+ @skills/gen-digital-human/SKILL.md čŽ·å–ę•°å­—äŗŗåˆ—č”Ø + äø„ę ¼ęŒ‰čÆ„ę–‡ę”£ę‰§č”Œ
164
+ @skills/template-bind/SKILL.md čŽ·å–ęØ”ē‰ˆåˆ—č”Ø + äø„ę ¼ęŒ‰čÆ„ę–‡ę”£ę‰§č”Œ
165
+ @skills/gen-script/SKILL.md åŸŗäŗŽęØ”ē‰ˆ image-slideļ¼Œåˆ›ä½œäø€äøŖå…³äŗŽAIå­¦ä¹ ę–¹ę³•ēš„č§†é¢‘ + äø„ę ¼ęŒ‰čÆ„ę–‡ę”£ę‰§č”Œ
166
+ ```
package/dist/argv.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Argv parser shared by cli.ts and python passthrough.
3
+ *
4
+ * Convention (matches ab-agent's buildCliArgs):
5
+ * --foo-bar value → foo_bar: 'value'
6
+ * --foo-bar → foo_bar: true
7
+ * --foo-bar a --foo-bar b → foo_bar: ['a', 'b']
8
+ * -p value → short flags also supported as-is (kebab → snake)
9
+ *
10
+ * Booleans are detected when the next token starts with '--' or is absent.
11
+ * Negative number values (-1.5) are NOT supported as flag values in this
12
+ * parser — Python skills handle them via argparse downstream when needed.
13
+ */
14
+ export type ParsedArgs = Record<string, string | boolean | string[]>;
15
+ export declare function parseArgv(argv: string[]): ParsedArgs;
package/dist/argv.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Argv parser shared by cli.ts and python passthrough.
3
+ *
4
+ * Convention (matches ab-agent's buildCliArgs):
5
+ * --foo-bar value → foo_bar: 'value'
6
+ * --foo-bar → foo_bar: true
7
+ * --foo-bar a --foo-bar b → foo_bar: ['a', 'b']
8
+ * -p value → short flags also supported as-is (kebab → snake)
9
+ *
10
+ * Booleans are detected when the next token starts with '--' or is absent.
11
+ * Negative number values (-1.5) are NOT supported as flag values in this
12
+ * parser — Python skills handle them via argparse downstream when needed.
13
+ */
14
+ function flagToKey(flag) {
15
+ return flag.replace(/^--?/, '').replace(/-/g, '_');
16
+ }
17
+ export function parseArgv(argv) {
18
+ const out = {};
19
+ let i = 0;
20
+ while (i < argv.length) {
21
+ const token = argv[i];
22
+ if (!token.startsWith('-')) {
23
+ i++;
24
+ continue;
25
+ }
26
+ const key = flagToKey(token);
27
+ const next = argv[i + 1];
28
+ if (next === undefined || next.startsWith('--')) {
29
+ out[key] = true;
30
+ i++;
31
+ continue;
32
+ }
33
+ // Treat next token as value.
34
+ const existing = out[key];
35
+ if (existing === undefined) {
36
+ out[key] = next;
37
+ }
38
+ else if (Array.isArray(existing)) {
39
+ existing.push(next);
40
+ }
41
+ else if (typeof existing === 'string') {
42
+ out[key] = [existing, next];
43
+ }
44
+ else {
45
+ // Was boolean — overwrite (last write wins; unusual case)
46
+ out[key] = next;
47
+ }
48
+ i += 2;
49
+ }
50
+ return out;
51
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * remixmate CLI entrypoint.
4
+ *
5
+ * remixmate --list list registered skills
6
+ * remixmate <name> --help forward --help to underlying skill
7
+ * remixmate <name> [--flag value ...] invoke the skill
8
+ *
9
+ * For python skills (entry.type === 'python') we spawn the script and inherit
10
+ * its stdio so progress / errors flow through unchanged.
11
+ * For http / builtin skills we dispatch in-process to handlers/.
12
+ */
13
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * remixmate CLI entrypoint.
4
+ *
5
+ * remixmate --list list registered skills
6
+ * remixmate <name> --help forward --help to underlying skill
7
+ * remixmate <name> [--flag value ...] invoke the skill
8
+ *
9
+ * For python skills (entry.type === 'python') we spawn the script and inherit
10
+ * its stdio so progress / errors flow through unchanged.
11
+ * For http / builtin skills we dispatch in-process to handlers/.
12
+ */
13
+ import path from 'node:path';
14
+ import { loadSkills, SKILLS_DIR } from './registry.js';
15
+ import { parseArgv } from './argv.js';
16
+ import { runSkill } from './runner.js';
17
+ /**
18
+ * SKILL_BASE_DIR can arrive as a relative path (e.g. from a developer .env or
19
+ * from ab-agent's spawn env). The cli script's cwd isn't a stable anchor for
20
+ * that — it's whatever the caller set, often the bundled `skills-cli/`
21
+ * directory. Resolve against process.cwd() (= the caller's intended root) up
22
+ * front so any later existsSync / readdirSync call sees an absolute,
23
+ * predictable path.
24
+ */
25
+ function resolveBaseDir() {
26
+ const raw = process.env.SKILL_BASE_DIR;
27
+ if (!raw)
28
+ return SKILLS_DIR;
29
+ return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
30
+ }
31
+ function describeEntryTail(s) {
32
+ if (s.entry.type === 'python') {
33
+ return `(scripts/${s.entry.scriptPath.replace(/^scripts\//, '')})`;
34
+ }
35
+ return `[${s.entry.handler}]`;
36
+ }
37
+ function printUsage() {
38
+ // Match runSkill: honor SKILL_BASE_DIR so `remixmate --help` and the
39
+ // subsequent `remixmate <name>` invocation list / run from the same tree.
40
+ const skills = loadSkills(resolveBaseDir());
41
+ process.stdout.write('Usage:\n');
42
+ process.stdout.write(' remixmate --list List all skills\n');
43
+ process.stdout.write(' remixmate <name> [--flag value ...] Invoke a skill\n');
44
+ process.stdout.write(' remixmate <name> --help Show skill-specific help\n');
45
+ process.stdout.write('\nAvailable skills:\n');
46
+ for (const s of skills) {
47
+ const summary = s.description.split('\n')[0].slice(0, 60);
48
+ process.stdout.write(` ${s.name.padEnd(22)} ${s.entry.type.padEnd(8)} ${summary}\n`);
49
+ }
50
+ }
51
+ function printList() {
52
+ for (const s of loadSkills(resolveBaseDir())) {
53
+ process.stdout.write(`${s.name}\t${s.toolName}\t${s.entry.type}\t${describeEntryTail(s)}\n`);
54
+ }
55
+ }
56
+ async function main() {
57
+ const argv = process.argv.slice(2);
58
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
59
+ printUsage();
60
+ process.exit(0);
61
+ }
62
+ if (argv[0] === '--list' || argv[0] === 'list') {
63
+ printList();
64
+ process.exit(0);
65
+ }
66
+ if (argv[0].startsWith('-')) {
67
+ process.stderr.write(`āŒ unknown option: ${argv[0]}\n`);
68
+ printUsage();
69
+ process.exit(2);
70
+ }
71
+ const skillName = argv[0];
72
+ const rest = argv.slice(1);
73
+ const code = await runSkill(skillName, {
74
+ baseDir: resolveBaseDir(),
75
+ rawArgs: rest,
76
+ parsedArgs: parseArgv(rest),
77
+ });
78
+ process.exit(code);
79
+ }
80
+ main().catch((err) => {
81
+ process.stderr.write(`āŒ fatal: ${err?.stack ?? err?.message ?? String(err)}\n`);
82
+ process.exit(1);
83
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * gen-image handler — POST ab-api /model/genImg.
3
+ *
4
+ * Mirrors the previous `scripts/gen_image.py` behavior:
5
+ * - Seedream models accept width/height (via SIZE_PRESETS lookup or WxH parse)
6
+ * - Gemini models accept aspectRatio + size (1K/2K/4K)
7
+ * - Reference images: HTTPS URLs and data: URIs pass through; local paths
8
+ * are read and base64-encoded into a data: URI
9
+ * - Async response (generationId + status:'generating'/'pending') is polled
10
+ * against /model/getImgStatus until completed/failed/timeout
11
+ */
12
+ import type { HandlerContext, HandlerInput } from './index.js';
13
+ export declare function genImage(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * gen-image handler — POST ab-api /model/genImg.
3
+ *
4
+ * Mirrors the previous `scripts/gen_image.py` behavior:
5
+ * - Seedream models accept width/height (via SIZE_PRESETS lookup or WxH parse)
6
+ * - Gemini models accept aspectRatio + size (1K/2K/4K)
7
+ * - Reference images: HTTPS URLs and data: URIs pass through; local paths
8
+ * are read and base64-encoded into a data: URI
9
+ * - Async response (generationId + status:'generating'/'pending') is polled
10
+ * against /model/getImgStatus until completed/failed/timeout
11
+ */
12
+ import { readFileSync, existsSync, statSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
15
+ import { emitProgress } from '../progress.js';
16
+ const DEFAULT_IMAGE_MODEL = 'doubao/doubao-seedream-5-0-260128';
17
+ // Maps aspect-ratio shorthand → (width, height) for Seedream models.
18
+ const SIZE_PRESETS = {
19
+ '1:1': [2048, 2048],
20
+ '16:9': [2848, 1600],
21
+ '9:16': [1600, 2848],
22
+ '4:3': [2304, 1728],
23
+ '3:4': [1728, 2304],
24
+ '2:3': [1664, 2496],
25
+ };
26
+ const MIME_BY_EXT = {
27
+ '.png': 'image/png',
28
+ '.jpg': 'image/jpeg',
29
+ '.jpeg': 'image/jpeg',
30
+ '.webp': 'image/webp',
31
+ '.gif': 'image/gif',
32
+ };
33
+ // Per-model reference-image caps (matches ab-api validation).
34
+ const SEEDREAM_REF_LIMIT = 14;
35
+ const GEMINI_REF_LIMIT = 4;
36
+ function isSeedreamModel(model) {
37
+ const lower = model.toLowerCase();
38
+ return lower.includes('seedream') || lower.includes('seedance');
39
+ }
40
+ function resolveImageInput(pathOrUrl) {
41
+ const trimmed = pathOrUrl.trim();
42
+ if (!trimmed)
43
+ return '';
44
+ const lower = trimmed.toLowerCase();
45
+ if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) {
46
+ return trimmed;
47
+ }
48
+ if (!existsSync(trimmed) || !statSync(trimmed).isFile()) {
49
+ throw new SkillError(`āŒ image path does not exist or is not a file: ${trimmed}`);
50
+ }
51
+ const ext = path.extname(trimmed).toLowerCase();
52
+ const mime = MIME_BY_EXT[ext] ?? 'image/jpeg';
53
+ const b64 = readFileSync(trimmed).toString('base64');
54
+ return `data:${mime};base64,${b64}`;
55
+ }
56
+ /** Coerce a CLI flag value (string|string[]|boolean) into an array of strings. */
57
+ function toStringArray(value) {
58
+ if (value === undefined || value === null)
59
+ return [];
60
+ if (Array.isArray(value))
61
+ return value.map(String);
62
+ return [String(value)];
63
+ }
64
+ function toNumber(value, name) {
65
+ if (value === undefined || value === null)
66
+ return undefined;
67
+ const n = Number(value);
68
+ if (!Number.isFinite(n))
69
+ throw new SkillError(`āŒ ${name} must be a number, received: ${value}`);
70
+ return n;
71
+ }
72
+ /** Resolve {width, height} for Seedream from --size (preset key or WxH). */
73
+ function resolveSeedreamSize(size) {
74
+ if (SIZE_PRESETS[size]) {
75
+ const [w, h] = SIZE_PRESETS[size];
76
+ return { width: w, height: h };
77
+ }
78
+ if (/^\d+x\d+$/i.test(size)) {
79
+ const [w, h] = size.toLowerCase().split('x').map((v) => parseInt(v, 10));
80
+ return { width: w, height: h };
81
+ }
82
+ throw new SkillError(`āŒ Seedream model does not support size format: ${size} (use 1:1 / 9:16 / 16:9 ... or WxH like 1600x2848)`);
83
+ }
84
+ async function pollImageStatus(ctx, generationId) {
85
+ return pollUntil(async () => {
86
+ const data = await mmPost(ctx, '/model/getImgStatus', { generationId }, { timeoutMs: 30_000 });
87
+ const status = data.status ?? 'unknown';
88
+ emitProgress({ phase: 'gen-image:poll', generationId, status });
89
+ if (status === 'completed' || status === 'succeeded') {
90
+ return (data.imageUrls ?? []).filter((u) => !!u);
91
+ }
92
+ if (status === 'failed') {
93
+ const err = data.errorMsg ?? data.error ?? 'unknown error';
94
+ throw new SkillError(`āŒ image generation failed: ${err}`);
95
+ }
96
+ return null;
97
+ }, { intervalMs: 5_000, timeoutMs: 120_000 });
98
+ }
99
+ export async function genImage(input, ctxIn) {
100
+ const prompt = input.prompt;
101
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
102
+ throw new SkillError('āŒ Please provide --prompt to describe the image');
103
+ }
104
+ const ctx = resolveHttpContext(ctxIn.skillName, {
105
+ apiBaseUrl: input.api_base_url,
106
+ privateToken: input.priv_token,
107
+ });
108
+ const model = input.model ?? process.env.MM_IMAGE_MODEL ?? DEFAULT_IMAGE_MODEL;
109
+ const size = input.size ?? '1:1';
110
+ const n = toNumber(input.n, 'n') ?? 1;
111
+ const resolution = input.resolution ?? '1K';
112
+ const seed = toNumber(input.seed, 'seed');
113
+ const imageStrength = toNumber(input.image_strength, 'image_strength');
114
+ const guidanceScale = toNumber(input.guidance_scale, 'guidance_scale');
115
+ const negativePrompt = input.negative_prompt;
116
+ // --watermark / --no-watermark — parseArgv only sees `--watermark true` style;
117
+ // when ab-agent's buildCliArgs passes booleans, only the truthy flag appears.
118
+ // Treat presence as true; explicit `--no-watermark` isn't supported here.
119
+ const watermark = input.watermark === true ? true : undefined;
120
+ const seedream = isSeedreamModel(model);
121
+ const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
122
+ const refLimit = seedream ? SEEDREAM_REF_LIMIT : GEMINI_REF_LIMIT;
123
+ if (references.length > refLimit) {
124
+ throw new SkillError(`āŒ ${seedream ? 'Seedream' : 'Gemini'} model accepts at most ${refLimit} reference images, got ${references.length}`);
125
+ }
126
+ const payload = { model, prompt, imageCount: n };
127
+ if (guidanceScale !== undefined)
128
+ payload.guidanceScale = guidanceScale;
129
+ if (references.length > 0)
130
+ payload.referenceImages = references;
131
+ if (imageStrength !== undefined)
132
+ payload.imageStrength = imageStrength;
133
+ if (negativePrompt)
134
+ payload.negativePrompt = negativePrompt;
135
+ if (seed !== undefined)
136
+ payload.seed = seed;
137
+ if (watermark !== undefined)
138
+ payload.watermark = watermark;
139
+ if (seedream) {
140
+ Object.assign(payload, resolveSeedreamSize(size));
141
+ }
142
+ else {
143
+ payload.aspectRatio = size;
144
+ payload.size = resolution;
145
+ }
146
+ emitProgress({ phase: 'gen-image:request', model, size, n });
147
+ const data = await mmPost(ctx, '/model/genImg', payload, { timeoutMs: 120_000 });
148
+ let imageUrls = (data.imageUrls ?? []).filter((u) => !!u);
149
+ if (imageUrls.length === 0 && data.generationId && (data.status === 'generating' || data.status === 'pending')) {
150
+ emitProgress({ phase: 'gen-image:async', generationId: data.generationId });
151
+ imageUrls = await pollImageStatus(ctx, data.generationId);
152
+ }
153
+ if (imageUrls.length === 0) {
154
+ throw new SkillError(`āŒ API response did not contain any image URL: ${JSON.stringify(data)}`);
155
+ }
156
+ process.stdout.write(`\nšŸ“ø Generated ${imageUrls.length} image(s)\n`);
157
+ process.stdout.write(`\nšŸ”— Image URLs (renders directly, no download needed):\n`);
158
+ for (const url of imageUrls)
159
+ process.stdout.write(`${url}\n`);
160
+ process.stdout.write(`\nšŸŽ‰ Image generation done!\n`);
161
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * gen-voice handler — Minimax TTS via ab-api /tool/minimaxT2aV2.
3
+ *
4
+ * Operations:
5
+ * --list-voices true → GET available voice IDs from /voice/page (live)
6
+ * --list-voices true --local true → print the built-in language-tagged
7
+ * fallback catalog (offline, no API call)
8
+ * default → synthesize text into an MP3 URL
9
+ *
10
+ * The previous Python script also offered `--download` to write the MP3 to
11
+ * disk. That's intentionally dropped here: the agent flow never used it (the
12
+ * audio URL is persisted in cloud) and removing local file I/O keeps the
13
+ * handler stateless.
14
+ */
15
+ import type { HandlerContext, HandlerInput } from './index.js';
16
+ export declare function genVoice(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * gen-voice handler — Minimax TTS via ab-api /tool/minimaxT2aV2.
3
+ *
4
+ * Operations:
5
+ * --list-voices true → GET available voice IDs from /voice/page (live)
6
+ * --list-voices true --local true → print the built-in language-tagged
7
+ * fallback catalog (offline, no API call)
8
+ * default → synthesize text into an MP3 URL
9
+ *
10
+ * The previous Python script also offered `--download` to write the MP3 to
11
+ * disk. That's intentionally dropped here: the agent flow never used it (the
12
+ * audio URL is persisted in cloud) and removing local file I/O keeps the
13
+ * handler stateless.
14
+ */
15
+ import { mmPost, resolveHttpContext, SkillError } from '../http.js';
16
+ import { emitProgress } from '../progress.js';
17
+ const DEFAULT_VOICE_ID = 'Chinese (Mandarin)_Male_Announcer';
18
+ /**
19
+ * Fallback voice catalog printed when `--list-voices --local` is passed.
20
+ * Mirrors the language-keyed defaults that gen_script.py uses, kept in sync
21
+ * by hand because the two callers live on different runtimes.
22
+ *
23
+ * Promote to a JSON config once this grows beyond a handful of entries.
24
+ */
25
+ const LOCAL_VOICE_CATALOG = [
26
+ { id: 'Chinese (Mandarin)_Male_Announcer', language: 'zh', name: 'Mandarin Male Announcer (legacy default)' },
27
+ // TODO: replace once gen-voice service confirms an English default voice id.
28
+ { id: 'english-male-friendly-01', language: 'en', name: 'Friendly Male (English, TBD)' },
29
+ ];
30
+ function toNumber(value, name, defaultValue) {
31
+ if (value === undefined || value === null || value === '')
32
+ return defaultValue;
33
+ const n = Number(value);
34
+ if (!Number.isFinite(n))
35
+ throw new SkillError(`āŒ ${name} must be a number, received: ${value}`);
36
+ return n;
37
+ }
38
+ function isTrue(value) {
39
+ return value === true || value === 'true';
40
+ }
41
+ async function listVoicesRemote(ctx) {
42
+ const data = await mmPost(ctx, '/voice/page', { current: 1, pageSize: 100 }, { timeoutMs: 30_000 });
43
+ const voices = data.list ?? [];
44
+ if (voices.length === 0) {
45
+ process.stdout.write('āš ļø No voices available\n');
46
+ return;
47
+ }
48
+ process.stdout.write(`\n${'Voice ID'.padEnd(30)} Name\n`);
49
+ process.stdout.write(`${'-'.repeat(30)} ${'-'.repeat(20)}\n`);
50
+ for (const v of voices) {
51
+ process.stdout.write(`${(v.uniqId ?? '').padEnd(30)} ${v.name ?? ''}\n`);
52
+ }
53
+ process.stdout.write(`\n${voices.length} voice(s) available\n`);
54
+ process.stdout.write(`šŸ’” Use --voice-id <id> to select a voice for synthesis\n`);
55
+ }
56
+ function listVoicesLocal() {
57
+ for (const v of LOCAL_VOICE_CATALOG) {
58
+ process.stdout.write(`${v.id}\t${v.language}\t${v.name}\n`);
59
+ }
60
+ }
61
+ export async function genVoice(input, ctxIn) {
62
+ // ── --list-voices: catalog mode (no synthesis) ──────────────────────────
63
+ if (isTrue(input.list_voices)) {
64
+ if (isTrue(input.local))
65
+ return listVoicesLocal();
66
+ const ctx = resolveHttpContext(ctxIn.skillName, {
67
+ apiBaseUrl: input.api_base_url,
68
+ privateToken: input.priv_token,
69
+ });
70
+ return listVoicesRemote(ctx);
71
+ }
72
+ // ── synthesize ──────────────────────────────────────────────────────────
73
+ const text = input.text;
74
+ if (!text || typeof text !== 'string' || !text.trim()) {
75
+ throw new SkillError('āŒ Please provide --text to specify the text to synthesize');
76
+ }
77
+ const voiceId = input.voice_id ?? DEFAULT_VOICE_ID;
78
+ const speed = toNumber(input.speed, 'speed', 1.0);
79
+ if (speed < 0.5 || speed > 2.0) {
80
+ throw new SkillError('āŒ speed must be in the range 0.5~2.0');
81
+ }
82
+ const ctx = resolveHttpContext(ctxIn.skillName, {
83
+ apiBaseUrl: input.api_base_url,
84
+ privateToken: input.priv_token,
85
+ });
86
+ emitProgress({ phase: 'gen-voice:request', voiceId, speed });
87
+ const data = await mmPost(ctx, '/tool/minimaxT2aV2', { text, voiceSetting: { voiceId, speed }, outputFormat: 'url' }, { timeoutMs: 120_000 });
88
+ const audioUrl = data.audio;
89
+ if (!audioUrl) {
90
+ throw new SkillError(`āŒ API response did not contain an audio URL: ${JSON.stringify(data)}`);
91
+ }
92
+ if (isTrue(input.json_output)) {
93
+ process.stdout.write(JSON.stringify({
94
+ url: audioUrl,
95
+ audio_length_ms: data.extraInfo?.audioLength ?? null,
96
+ subtitles: (data.subtitles ?? []).map((s) => ({
97
+ text: s.text ?? '',
98
+ timeBegin: s.timeBegin ?? 0,
99
+ timeEnd: s.timeEnd ?? 0,
100
+ })),
101
+ }) + '\n');
102
+ return;
103
+ }
104
+ const lengthMs = data.extraInfo?.audioLength;
105
+ if (typeof lengthMs === 'number') {
106
+ process.stdout.write(`\nšŸ• Audio length: ${(lengthMs / 1000).toFixed(1)} s\n`);
107
+ }
108
+ process.stdout.write(`\nšŸ”— Audio URL (plays directly, no download needed):\n${audioUrl}\n`);
109
+ process.stdout.write(`\nšŸŽ‰ TTS synthesis done!\n`);
110
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Handler registry — maps `entry.handler` strings to their implementation.
3
+ *
4
+ * Only `entry.type` in ('http' | 'builtin') uses this registry; python
5
+ * skills go through spawn() in runner.ts and bypass handlers entirely.
6
+ */
7
+ export type HandlerInput = Record<string, unknown>;
8
+ export interface HandlerContext {
9
+ /** ISO-style skill name from the registry (e.g. `gen-image`). Used for x-invoke-skill. */
10
+ skillName: string;
11
+ }
12
+ export type SkillHandler = (input: HandlerInput, ctx: HandlerContext) => Promise<void>;
13
+ export declare const HANDLERS: Record<string, SkillHandler>;