@remixmate/cli 0.9.20 → 0.9.22
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.
- package/README.md +19 -19
- package/README.zh-CN.md +15 -19
- package/dist/capabilities.d.ts +3 -0
- package/dist/cli.js +11 -0
- package/dist/doctor.js +17 -0
- package/dist/handlers/gen-video.d.ts +3 -1
- package/dist/handlers/gen-video.js +62 -7
- package/dist/install.d.ts +57 -0
- package/dist/install.js +292 -0
- package/dist/manifest.json +48 -22
- package/package.json +1 -1
- package/skills/gen-image/skill.json +7 -7
- package/skills/gen-script/skill.json +6 -7
- package/skills/gen-video/SKILL.md +56 -1
- package/skills/gen-video/skill.json +31 -5
package/README.md
CHANGED
|
@@ -11,32 +11,32 @@ AI media generation skills for Claude Code / Codex.
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install -g @remixmate/cli
|
|
15
|
-
remixmate --help
|
|
16
|
-
remixmate --list
|
|
14
|
+
npm install -g @remixmate/cli && remixmate install
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
`npm install` gives you the `remixmate` binary. `remixmate install` is what makes an
|
|
18
|
+
agent *discover* the skills on its own: it copies the bundled `skills/` into every
|
|
19
|
+
agent home it finds — Claude Code (`~/.claude/skills`) and Codex (`$CODEX_HOME/skills`,
|
|
20
|
+
default `~/.codex/skills`), which share the same `SKILL.md` format. Restart the agent
|
|
21
|
+
afterwards.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# or global
|
|
29
|
-
mkdir -p ~/.claude/skills
|
|
30
|
-
cp -R "$(npm root -g)/@remixmate/cli/skills/." ~/.claude/skills/
|
|
24
|
+
remixmate install --host codex # only one host
|
|
25
|
+
remixmate install --project # ./.claude/skills (Claude Code only)
|
|
26
|
+
remixmate install --dir <path> # explicit target
|
|
27
|
+
remixmate uninstall # remove exactly what was installed
|
|
31
28
|
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
The install is *managed*, not a `cp -R`: each skills directory gets a
|
|
31
|
+
`.remixmate-install.json` receipt recording the version and the skill list. That is
|
|
32
|
+
what lets a re-run upgrade in place, lets `uninstall` remove only our own directories,
|
|
33
|
+
and lets `remixmate doctor` warn when a global `npm update` has left the installed
|
|
34
|
+
copies behind. A same-named skill that this CLI did not install is never overwritten
|
|
35
|
+
without `--force`.
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
Without installing, the CLI still works — you just have to name it (`remixmate --list`,
|
|
38
|
+
or tell the agent to run `remixmate <skill> --help`). The agent will not reach for it
|
|
39
|
+
unprompted.
|
|
40
40
|
|
|
41
41
|
## Authentication
|
|
42
42
|
|
package/README.zh-CN.md
CHANGED
|
@@ -11,32 +11,28 @@
|
|
|
11
11
|
## 安装
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install -g @remixmate/cli
|
|
15
|
-
remixmate --help
|
|
16
|
-
remixmate --list
|
|
14
|
+
npm install -g @remixmate/cli && remixmate install
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
`npm install` 装的是 `remixmate` 这个命令;`remixmate install` 才让 agent **自己发现**
|
|
18
|
+
这些 skill —— 它把内置的 `skills/` 铺进检测到的每个 agent 目录:Claude Code
|
|
19
|
+
(`~/.claude/skills`)与 Codex(`$CODEX_HOME/skills`,默认 `~/.codex/skills`),两者
|
|
20
|
+
用的是同一套 `SKILL.md` 格式。装完重启 agent 生效。
|
|
22
21
|
|
|
23
22
|
```bash
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# 或全局
|
|
29
|
-
mkdir -p ~/.claude/skills
|
|
30
|
-
cp -R "$(npm root -g)/@remixmate/cli/skills/." ~/.claude/skills/
|
|
23
|
+
remixmate install --host codex # 只装某一个宿主
|
|
24
|
+
remixmate install --project # 装到 ./.claude/skills(仅 Claude Code)
|
|
25
|
+
remixmate install --dir <path> # 指定目录
|
|
26
|
+
remixmate uninstall # 按装机记录精确卸载
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
> 当你需要 AI 媒体生成(图片 / 视频 / 语音 / 数字人 / 剪映导出)时,先运行 `remixmate <skill-id> --help` 查看参数,再用 `remixmate <skill-id> --json-output ...` 执行。可用技能:`remixmate --list`。
|
|
29
|
+
这里的安装是**受管**的,不是 `cp -R`:每个 skills 目录下会写一份
|
|
30
|
+
`.remixmate-install.json` 回执,记录版本与本次铺下去的 skill 名单。靠它,重跑即原地升级、
|
|
31
|
+
`uninstall` 只删自己装的目录、`remixmate doctor` 能在 `npm update -g` 之后提示铺出去的副本
|
|
32
|
+
已经落后。同名但不是本 CLI 装的 skill,不加 `--force` 绝不覆盖。
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
不装也能用,只是得点名(`remixmate --list`,或让 agent 去跑 `remixmate <skill> --help`);
|
|
35
|
+
agent 不会主动想到它。
|
|
40
36
|
|
|
41
37
|
## 认证
|
|
42
38
|
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export interface ModelConstraints {
|
|
|
18
18
|
durationMax?: number;
|
|
19
19
|
durationDefault?: number;
|
|
20
20
|
refImageMax?: number;
|
|
21
|
+
/** Omni-modal reference caps (Seedance 2.0): videos and audio clips per request. */
|
|
22
|
+
refVideoMax?: number;
|
|
23
|
+
refAudioMax?: number;
|
|
21
24
|
sizes?: string[];
|
|
22
25
|
/** aspect-ratio → [width, height] pixel preset (Seedream). Per model: the same
|
|
23
26
|
* ratio maps to different pixels on different Seedream variants. */
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { parseArgv } from './argv.js';
|
|
|
16
16
|
import { runSkill } from './runner.js';
|
|
17
17
|
import { runAuthCommand } from './auth/commands.js';
|
|
18
18
|
import { HOST_VERB, PROJECT_VERB, runHostCommand, runProjectCommand } from './project/commands.js';
|
|
19
|
+
import { INSTALL_VERB, UNINSTALL_VERB, runInstall, runUninstall } from './install.js';
|
|
19
20
|
import { runExec } from './exec.js';
|
|
20
21
|
import { authStatusLine, runDoctor } from './doctor.js';
|
|
21
22
|
import { padDisplay } from './text.js';
|
|
@@ -51,6 +52,8 @@ function printUsage() {
|
|
|
51
52
|
const skills = loadSkills(resolveBaseDir());
|
|
52
53
|
process.stdout.write('Usage:\n');
|
|
53
54
|
process.stdout.write(' remixmate --list List all skills\n');
|
|
55
|
+
process.stdout.write(' remixmate install [--host <h>] Install the skills into Claude Code / Codex\n');
|
|
56
|
+
process.stdout.write(' remixmate uninstall Remove skills installed by this CLI\n');
|
|
54
57
|
process.stdout.write(' remixmate doctor Diagnose environment, credentials, backend\n');
|
|
55
58
|
process.stdout.write(' remixmate login Authorize this CLI via the browser\n');
|
|
56
59
|
process.stdout.write(' remixmate login --start | --wait Non-blocking authorization (for agent hosts)\n');
|
|
@@ -128,6 +131,14 @@ async function main() {
|
|
|
128
131
|
await printAuthFooter();
|
|
129
132
|
process.exit(EXIT.OK);
|
|
130
133
|
}
|
|
134
|
+
// Host-install verbs, like the auth verbs, resolve before skill dispatch so a
|
|
135
|
+
// future skill named `install` could never shadow them.
|
|
136
|
+
if (argv[0] === INSTALL_VERB) {
|
|
137
|
+
process.exit(runInstall(argv.slice(1)));
|
|
138
|
+
}
|
|
139
|
+
if (argv[0] === UNINSTALL_VERB) {
|
|
140
|
+
process.exit(runUninstall(argv.slice(1)));
|
|
141
|
+
}
|
|
131
142
|
if (argv[0] === 'doctor') {
|
|
132
143
|
const code = await runDoctor({ skipNetwork: argv.includes('--offline') });
|
|
133
144
|
process.exit(code);
|
package/dist/doctor.js
CHANGED
|
@@ -20,6 +20,7 @@ import { listCredentials } from './auth/credential-store.js';
|
|
|
20
20
|
import { resolveTakeKey } from './project/host.js';
|
|
21
21
|
import { readBinding } from './project/store.js';
|
|
22
22
|
import { canAutoAuth, isHeadless } from './auth/environment.js';
|
|
23
|
+
import { installStates } from './install.js';
|
|
23
24
|
import { padDisplay } from './text.js';
|
|
24
25
|
/**
|
|
25
26
|
* Short auth status suitable for a footer. Never performs network I/O — a
|
|
@@ -100,6 +101,22 @@ export async function runDoctor(opts = {}) {
|
|
|
100
101
|
detail: playwright ?? '当前解释器下未安装 —— 仅 web-screenshot / web-record 需要,首次运行会自动装',
|
|
101
102
|
});
|
|
102
103
|
checks.push({ name: '后端地址', status: 'ok', detail: apiBaseUrl });
|
|
104
|
+
// 宿主安装状态。这套方案唯一的静默失败模式是漂移:`npm update -g` 只动包,不动
|
|
105
|
+
// 已经铺进 ~/.claude/skills、~/.codex/skills 的副本,用户会拿着旧 SKILL.md 打新
|
|
106
|
+
// 后端,而且第一个症状往往是某个参数「莫名其妙不认」。
|
|
107
|
+
for (const s of installStates()) {
|
|
108
|
+
checks.push({
|
|
109
|
+
// 只取宿主名的第一段(Claude Code → Claude)——检查名要留在 padDisplay 的
|
|
110
|
+
// 对齐宽度内,宿主目录已经写在 detail 里,不会有歧义。
|
|
111
|
+
name: `${s.label.split(' ')[0]} skill`,
|
|
112
|
+
status: s.installedVersion == null ? 'warn' : s.drifted ? 'warn' : 'ok',
|
|
113
|
+
detail: s.installedVersion == null
|
|
114
|
+
? `未安装 —— 该 agent 不会自动发现这些 skill(remixmate install 可安装到 ${s.skillsDir})`
|
|
115
|
+
: s.drifted
|
|
116
|
+
? `已装 ${s.installedVersion},当前包 ${s.currentVersion} → 运行 remixmate install 覆盖`
|
|
117
|
+
: `${s.installedVersion}(${s.skillsDir})`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
103
120
|
// Where will this machine's renders land, and will repeated renders group?
|
|
104
121
|
// Both were previously invisible — the first sign of trouble was a finished
|
|
105
122
|
// video that appeared nowhere in the web app.
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* server-side, so an unknown value still passes through to the backend.
|
|
12
12
|
*
|
|
13
13
|
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
14
|
-
* into a data: URI; https / data: URIs pass through unchanged.
|
|
14
|
+
* into a data: URI; https / data: URIs pass through unchanged. Reference videos
|
|
15
|
+
* and audio are the exception — they must already be public URLs, see
|
|
16
|
+
* `requirePublicUrls`.
|
|
15
17
|
*/
|
|
16
18
|
import type { HandlerContext, HandlerInput } from './index.js';
|
|
17
19
|
export declare function genVideo(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
|
|
@@ -11,14 +11,16 @@
|
|
|
11
11
|
* server-side, so an unknown value still passes through to the backend.
|
|
12
12
|
*
|
|
13
13
|
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
14
|
-
* into a data: URI; https / data: URIs pass through unchanged.
|
|
14
|
+
* into a data: URI; https / data: URIs pass through unchanged. Reference videos
|
|
15
|
+
* and audio are the exception — they must already be public URLs, see
|
|
16
|
+
* `requirePublicUrls`.
|
|
15
17
|
*/
|
|
16
18
|
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
17
19
|
import { getCapabilities, matchModel, defaultModel } from '../capabilities.js';
|
|
18
20
|
import { emitProgress } from '../progress.js';
|
|
19
21
|
import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
|
|
20
22
|
/** Generic, catalog-driven validation. No model names live here. */
|
|
21
|
-
function validateAgainstConstraints(descriptor, duration, ratio, resolution,
|
|
23
|
+
function validateAgainstConstraints(descriptor, duration, ratio, resolution, refs) {
|
|
22
24
|
const c = descriptor.constraints;
|
|
23
25
|
const label = descriptor.label || descriptor.id;
|
|
24
26
|
if (c.durations && c.durations.length > 0) {
|
|
@@ -37,12 +39,35 @@ function validateAgainstConstraints(descriptor, duration, ratio, resolution, ref
|
|
|
37
39
|
if (c.resolutions && c.resolutions.length > 0 && !c.resolutions.includes(resolution)) {
|
|
38
40
|
throw new SkillError(`❌ ${label} resolution must be one of: ${c.resolutions.join(', ')}; got: ${resolution}`);
|
|
39
41
|
}
|
|
40
|
-
if (c.refImageMax !== undefined &&
|
|
42
|
+
if (c.refImageMax !== undefined && refs.images > c.refImageMax) {
|
|
41
43
|
if (c.refImageMax === 0) {
|
|
42
44
|
throw new SkillError(`❌ ${label} does not support a reference-image array; use --first-frame / --last-frame instead`);
|
|
43
45
|
}
|
|
44
|
-
throw new SkillError(`❌ ${label} accepts at most ${c.refImageMax} reference images; got: ${
|
|
46
|
+
throw new SkillError(`❌ ${label} accepts at most ${c.refImageMax} reference images; got: ${refs.images}`);
|
|
45
47
|
}
|
|
48
|
+
if (c.refVideoMax !== undefined && refs.videos > c.refVideoMax) {
|
|
49
|
+
throw new SkillError(`❌ ${label} accepts at most ${c.refVideoMax} reference videos; got: ${refs.videos}`);
|
|
50
|
+
}
|
|
51
|
+
if (c.refAudioMax !== undefined && refs.audios > c.refAudioMax) {
|
|
52
|
+
throw new SkillError(`❌ ${label} accepts at most ${c.refAudioMax} reference audio clips; got: ${refs.audios}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reference videos / audio must be public https URLs.
|
|
57
|
+
*
|
|
58
|
+
* Unlike images, they are NOT inlined as data URIs: the caps are 50 MB per video
|
|
59
|
+
* and 15 MB per clip, and base64 inflates that by a third before it ever reaches
|
|
60
|
+
* the gateway. Failing here with the offending path beats a truncated upload or
|
|
61
|
+
* an opaque 413 three hops downstream.
|
|
62
|
+
*/
|
|
63
|
+
function requirePublicUrls(values, flag) {
|
|
64
|
+
return values.map((value) => {
|
|
65
|
+
const url = value.trim();
|
|
66
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
67
|
+
throw new SkillError(`❌ ${flag} needs a public http(s) URL (local files are not uploaded), received: ${value}`);
|
|
68
|
+
}
|
|
69
|
+
return url;
|
|
70
|
+
});
|
|
46
71
|
}
|
|
47
72
|
async function pollVideoStatus(ctx, taskId) {
|
|
48
73
|
return pollUntil(async () => {
|
|
@@ -69,10 +94,26 @@ export async function genVideo(input, ctxIn) {
|
|
|
69
94
|
const firstFrame = input.first_frame ? resolveImageInput(String(input.first_frame)) : '';
|
|
70
95
|
const lastFrame = input.last_frame ? resolveImageInput(String(input.last_frame)) : '';
|
|
71
96
|
const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
|
|
97
|
+
const referenceVideos = requirePublicUrls(toStringArray(input.reference_video).filter(Boolean), '--reference-video');
|
|
98
|
+
const referenceAudios = requirePublicUrls(toStringArray(input.reference_audio).filter(Boolean), '--reference-audio');
|
|
72
99
|
const prompt = (input.prompt ?? '').trim();
|
|
73
|
-
const hasVisual = Boolean(firstFrame || lastFrame || references.length > 0);
|
|
100
|
+
const hasVisual = Boolean(firstFrame || lastFrame || references.length > 0 || referenceVideos.length > 0);
|
|
74
101
|
if (!prompt && !hasVisual) {
|
|
75
|
-
throw new SkillError('❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference');
|
|
102
|
+
throw new SkillError('❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference / --reference-video');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Two shapes the backend rejects outright; catching them here saves a round
|
|
106
|
+
* trip and names the flag that caused it.
|
|
107
|
+
*
|
|
108
|
+
* - first/last frame pins exact frames, omni-modal reference lets the model
|
|
109
|
+
* re-cut — the API has no meaning for both at once;
|
|
110
|
+
* - audio-only (or text + audio) input is not a supported modality mix.
|
|
111
|
+
*/
|
|
112
|
+
if ((firstFrame || lastFrame) && (referenceVideos.length > 0 || referenceAudios.length > 0)) {
|
|
113
|
+
throw new SkillError('❌ --reference-video / --reference-audio cannot be combined with --first-frame / --last-frame; use --reference for images instead');
|
|
114
|
+
}
|
|
115
|
+
if (referenceAudios.length > 0 && references.length === 0 && referenceVideos.length === 0) {
|
|
116
|
+
throw new SkillError('❌ --reference-audio needs at least one --reference image or --reference-video alongside it');
|
|
76
117
|
}
|
|
77
118
|
const ratio = input.ratio ?? '16:9';
|
|
78
119
|
const resolution = input.resolution ?? '720p';
|
|
@@ -80,7 +121,11 @@ export async function genVideo(input, ctxIn) {
|
|
|
80
121
|
const seed = toNumber(input.seed, 'seed');
|
|
81
122
|
// Pre-flight validation only when we recognize the model (have its constraints).
|
|
82
123
|
if (descriptor) {
|
|
83
|
-
validateAgainstConstraints(descriptor, duration, ratio, resolution,
|
|
124
|
+
validateAgainstConstraints(descriptor, duration, ratio, resolution, {
|
|
125
|
+
images: references.length,
|
|
126
|
+
videos: referenceVideos.length,
|
|
127
|
+
audios: referenceAudios.length,
|
|
128
|
+
});
|
|
84
129
|
}
|
|
85
130
|
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
86
131
|
apiBaseUrl,
|
|
@@ -103,6 +148,16 @@ export async function genVideo(input, ctxIn) {
|
|
|
103
148
|
payload.lastFrameImage = lastFrame;
|
|
104
149
|
if (references.length > 0)
|
|
105
150
|
payload.referenceImages = references;
|
|
151
|
+
if (referenceVideos.length > 0)
|
|
152
|
+
payload.referenceVideos = referenceVideos;
|
|
153
|
+
if (referenceAudios.length > 0)
|
|
154
|
+
payload.referenceAudios = referenceAudios;
|
|
155
|
+
// Web search only fires on text-only input; sending it alongside attachments
|
|
156
|
+
// would be a knob the model silently ignores.
|
|
157
|
+
if (isTrue(input.web_search) && !hasVisual)
|
|
158
|
+
payload.webSearch = true;
|
|
159
|
+
if (isTrue(input.return_last_frame))
|
|
160
|
+
payload.returnLastFrame = true;
|
|
106
161
|
if (input.negative_prompt && String(input.negative_prompt).trim()) {
|
|
107
162
|
payload.negativePrompt = String(input.negative_prompt).trim();
|
|
108
163
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate install` / `remixmate uninstall` — 把包内 skills/ 铺进 agent 宿主。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要一条命令,而不是让站点给出 `cp -R "$(npm root -g)/…/skills/." ~/.claude/skills/`:
|
|
5
|
+
*
|
|
6
|
+
* 1. 目标已经不止一个。Claude Code 与 Codex 现在用同一套 SKILL.md 格式
|
|
7
|
+
* (Codex 装到 $CODEX_HOME/skills/<name>),手工 cp 的心智成本直接翻倍。
|
|
8
|
+
* 2. 拷贝出去的副本没有主人。`npm update -g` 之后包升了级、副本停在旧版;
|
|
9
|
+
* `npm uninstall -g` 之后副本原地变成孤儿。两种都是静默的 —— 本项目作者机器上
|
|
10
|
+
* 就留着一批 5 月建的、指向早已不存在路径的 skill 目录,没有任何东西负责清理。
|
|
11
|
+
*
|
|
12
|
+
* 所以这里的安装是**受管**的:每个宿主的 skills 目录下写一份装机回执
|
|
13
|
+
* (.remixmate-install.json),记下版本、来源与这次铺下去的 skill 名单。升级照名单
|
|
14
|
+
* 覆盖、卸载照名单删除、doctor 照名单比对版本 —— 三件事都不靠猜目录里哪些是我们的。
|
|
15
|
+
*
|
|
16
|
+
* 刻意不做的两件事:
|
|
17
|
+
* - 不挂 npm postinstall。`npm i -g` 时往 ~/.claude、~/.codex 写东西太越界,
|
|
18
|
+
* 且 `--ignore-scripts` 下会静默失效。安装必须是用户显式发起的动作。
|
|
19
|
+
* - 不用软链。指向 $(npm root -g) 看着优雅,但 node 版本管理器一切换路径就变,
|
|
20
|
+
* 全部断掉且极难排查。拷贝 + 回执 + doctor 漂移检查更稳。
|
|
21
|
+
*/
|
|
22
|
+
export declare const INSTALL_VERB = "install";
|
|
23
|
+
export declare const UNINSTALL_VERB = "uninstall";
|
|
24
|
+
export type HostId = 'claude' | 'codex';
|
|
25
|
+
interface Host {
|
|
26
|
+
id: HostId | 'custom';
|
|
27
|
+
label: string;
|
|
28
|
+
/** skill 的父目录 —— 安装写到 <skillsDir>/<skill-name>/。 */
|
|
29
|
+
skillsDir: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 检测「这台机器上用了哪些 agent」。判据是配置根目录存在,而不是 skills/ 存在 ——
|
|
33
|
+
* 从没装过 skill 的用户 skills/ 本来就没有,那恰恰是最需要装的人。
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectHosts(): Host[];
|
|
36
|
+
export declare function installUsage(): void;
|
|
37
|
+
export declare function uninstallUsage(): void;
|
|
38
|
+
export declare function runInstall(argv: string[]): number;
|
|
39
|
+
export declare function runUninstall(argv: string[]): number;
|
|
40
|
+
export interface InstallState {
|
|
41
|
+
label: string;
|
|
42
|
+
skillsDir: string;
|
|
43
|
+
/** 回执里的版本;null = 该宿主下没有本 CLI 的安装。 */
|
|
44
|
+
installedVersion: string | null;
|
|
45
|
+
/** 当前包的版本,供报告直接引用。 */
|
|
46
|
+
currentVersion: string;
|
|
47
|
+
/** 回执版本与当前包版本不一致。 */
|
|
48
|
+
drifted: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 供 doctor 使用:每个宿主的安装状态。
|
|
52
|
+
*
|
|
53
|
+
* 漂移是这套方案唯一的静默失败模式 —— `npm update -g` 只动包,不动铺出去的副本,
|
|
54
|
+
* 用户会拿着旧 SKILL.md 打新后端。doctor 是「为什么跑不起来」的统一入口,检查放这里。
|
|
55
|
+
*/
|
|
56
|
+
export declare function installStates(): InstallState[];
|
|
57
|
+
export {};
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate install` / `remixmate uninstall` — 把包内 skills/ 铺进 agent 宿主。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要一条命令,而不是让站点给出 `cp -R "$(npm root -g)/…/skills/." ~/.claude/skills/`:
|
|
5
|
+
*
|
|
6
|
+
* 1. 目标已经不止一个。Claude Code 与 Codex 现在用同一套 SKILL.md 格式
|
|
7
|
+
* (Codex 装到 $CODEX_HOME/skills/<name>),手工 cp 的心智成本直接翻倍。
|
|
8
|
+
* 2. 拷贝出去的副本没有主人。`npm update -g` 之后包升了级、副本停在旧版;
|
|
9
|
+
* `npm uninstall -g` 之后副本原地变成孤儿。两种都是静默的 —— 本项目作者机器上
|
|
10
|
+
* 就留着一批 5 月建的、指向早已不存在路径的 skill 目录,没有任何东西负责清理。
|
|
11
|
+
*
|
|
12
|
+
* 所以这里的安装是**受管**的:每个宿主的 skills 目录下写一份装机回执
|
|
13
|
+
* (.remixmate-install.json),记下版本、来源与这次铺下去的 skill 名单。升级照名单
|
|
14
|
+
* 覆盖、卸载照名单删除、doctor 照名单比对版本 —— 三件事都不靠猜目录里哪些是我们的。
|
|
15
|
+
*
|
|
16
|
+
* 刻意不做的两件事:
|
|
17
|
+
* - 不挂 npm postinstall。`npm i -g` 时往 ~/.claude、~/.codex 写东西太越界,
|
|
18
|
+
* 且 `--ignore-scripts` 下会静默失效。安装必须是用户显式发起的动作。
|
|
19
|
+
* - 不用软链。指向 $(npm root -g) 看着优雅,但 node 版本管理器一切换路径就变,
|
|
20
|
+
* 全部断掉且极难排查。拷贝 + 回执 + doctor 漂移检查更稳。
|
|
21
|
+
*/
|
|
22
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { SKILLS_DIR } from './registry.js';
|
|
27
|
+
import { EXIT } from './errors.js';
|
|
28
|
+
import { padDisplay } from './text.js';
|
|
29
|
+
export const INSTALL_VERB = 'install';
|
|
30
|
+
export const UNINSTALL_VERB = 'uninstall';
|
|
31
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
33
|
+
/** 装机回执。升级 / 卸载 / 漂移检查都只认它。 */
|
|
34
|
+
const RECEIPT = '.remixmate-install.json';
|
|
35
|
+
const PACKAGE_NAME = '@remixmate/cli';
|
|
36
|
+
/**
|
|
37
|
+
* 宿主的配置根目录。两个 agent 都允许用环境变量改位置,检测必须跟着走 ——
|
|
38
|
+
* 否则把 CODEX_HOME 指到别处的用户会看到「未检测到 Codex」。
|
|
39
|
+
*/
|
|
40
|
+
function hostHome(id) {
|
|
41
|
+
return id === 'claude'
|
|
42
|
+
? process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
43
|
+
: process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
44
|
+
}
|
|
45
|
+
const HOST_LABEL = {
|
|
46
|
+
claude: 'Claude Code',
|
|
47
|
+
codex: 'Codex',
|
|
48
|
+
};
|
|
49
|
+
function hostOf(id) {
|
|
50
|
+
return { id, label: HOST_LABEL[id], skillsDir: path.join(hostHome(id), 'skills') };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 检测「这台机器上用了哪些 agent」。判据是配置根目录存在,而不是 skills/ 存在 ——
|
|
54
|
+
* 从没装过 skill 的用户 skills/ 本来就没有,那恰恰是最需要装的人。
|
|
55
|
+
*/
|
|
56
|
+
export function detectHosts() {
|
|
57
|
+
return ['claude', 'codex'].filter((id) => existsSync(hostHome(id))).map(hostOf);
|
|
58
|
+
}
|
|
59
|
+
function packageVersion() {
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf-8');
|
|
62
|
+
return JSON.parse(raw).version ?? 'unknown';
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 包内可安装的 skill 名单。
|
|
70
|
+
*
|
|
71
|
+
* 用 SKILLS_DIR 而不是 resolveBaseDir():install 铺的永远是**本包自带**的那份,
|
|
72
|
+
* SKILL_BASE_DIR 是给 ab-agent 之类宿主在运行期改 skill 树用的,不该改变安装来源。
|
|
73
|
+
*/
|
|
74
|
+
function bundledSkills() {
|
|
75
|
+
if (!existsSync(SKILLS_DIR))
|
|
76
|
+
return [];
|
|
77
|
+
return readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
78
|
+
.filter((e) => e.isDirectory() && existsSync(path.join(SKILLS_DIR, e.name, 'SKILL.md')))
|
|
79
|
+
.map((e) => e.name)
|
|
80
|
+
.sort();
|
|
81
|
+
}
|
|
82
|
+
function readReceipt(skillsDir) {
|
|
83
|
+
const file = path.join(skillsDir, RECEIPT);
|
|
84
|
+
if (!existsSync(file))
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// 回执损坏时按「没装过」处理:那样只会退化成需要 --force,而不是把用户的目录删错。
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function parseArgs(argv, verb) {
|
|
95
|
+
const out = { hosts: [], force: false, help: false };
|
|
96
|
+
const picked = [];
|
|
97
|
+
let dir = '';
|
|
98
|
+
let project = false;
|
|
99
|
+
for (let i = 0; i < argv.length; i++) {
|
|
100
|
+
const a = argv[i];
|
|
101
|
+
const eq = a.indexOf('=');
|
|
102
|
+
const key = eq > 0 ? a.slice(0, eq) : a;
|
|
103
|
+
const inline = eq > 0 ? a.slice(eq + 1) : null;
|
|
104
|
+
const next = () => inline ?? argv[++i] ?? '';
|
|
105
|
+
switch (key) {
|
|
106
|
+
case '--host': {
|
|
107
|
+
const v = next();
|
|
108
|
+
if (v === 'all')
|
|
109
|
+
picked.push('claude', 'codex');
|
|
110
|
+
else if (v === 'claude' || v === 'codex')
|
|
111
|
+
picked.push(v);
|
|
112
|
+
else {
|
|
113
|
+
out.unknown = `--host ${v}`;
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case '--dir':
|
|
119
|
+
dir = next();
|
|
120
|
+
break;
|
|
121
|
+
case '--project':
|
|
122
|
+
project = true;
|
|
123
|
+
break;
|
|
124
|
+
case '--force':
|
|
125
|
+
out.force = true;
|
|
126
|
+
break;
|
|
127
|
+
case '-h':
|
|
128
|
+
case '--help':
|
|
129
|
+
out.help = true;
|
|
130
|
+
return out;
|
|
131
|
+
default:
|
|
132
|
+
out.unknown = a;
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (dir) {
|
|
137
|
+
out.hosts = [{ id: 'custom', label: '自定义目录', skillsDir: path.resolve(dir) }];
|
|
138
|
+
}
|
|
139
|
+
else if (project) {
|
|
140
|
+
// 只有 Claude Code 有项目级 skill 目录。Codex 的官方安装位置只有
|
|
141
|
+
// $CODEX_HOME/skills(见其内置 skill-installer),没有项目级约定,不臆造一个。
|
|
142
|
+
out.hosts = [{ id: 'claude', label: 'Claude Code(项目级)', skillsDir: path.join(process.cwd(), '.claude', 'skills') }];
|
|
143
|
+
}
|
|
144
|
+
else if (picked.length > 0) {
|
|
145
|
+
out.hosts = [...new Set(picked)].map(hostOf);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
out.hosts = verb === UNINSTALL_VERB ? ['claude', 'codex'].map(hostOf) : detectHosts();
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
export function installUsage() {
|
|
153
|
+
process.stdout.write('Usage: remixmate install [options]\n' +
|
|
154
|
+
'\n' +
|
|
155
|
+
'把本包自带的 skill 装进已检测到的 agent 宿主,让它们能自动发现这些能力\n' +
|
|
156
|
+
'(Claude Code: ~/.claude/skills,Codex: ~/.codex/skills)。重复运行即升级。\n' +
|
|
157
|
+
'\n' +
|
|
158
|
+
'Options:\n' +
|
|
159
|
+
' --host <claude|codex|all> 只装指定宿主(默认:装到所有检测到的宿主)\n' +
|
|
160
|
+
' --project 装到当前目录 ./.claude/skills(仅 Claude Code)\n' +
|
|
161
|
+
' --dir <path> 自定义 skills 父目录(覆盖上面两者)\n' +
|
|
162
|
+
' --force 覆盖同名的、非本 CLI 安装的 skill 目录\n');
|
|
163
|
+
}
|
|
164
|
+
export function uninstallUsage() {
|
|
165
|
+
process.stdout.write('Usage: remixmate uninstall [options]\n' +
|
|
166
|
+
'\n' +
|
|
167
|
+
'按装机回执删除本 CLI 装过的 skill —— 只删回执里列出的目录,不碰其它 skill。\n' +
|
|
168
|
+
'\n' +
|
|
169
|
+
'Options:\n' +
|
|
170
|
+
' --host <claude|codex|all> 只卸载指定宿主(默认:两个宿主都查)\n' +
|
|
171
|
+
' --project 卸载当前目录 ./.claude/skills 下的安装\n' +
|
|
172
|
+
' --dir <path> 自定义 skills 父目录\n');
|
|
173
|
+
}
|
|
174
|
+
function installInto(host, names, version, force) {
|
|
175
|
+
const prev = readReceipt(host.skillsDir);
|
|
176
|
+
const owned = new Set(prev?.skills ?? []);
|
|
177
|
+
// 目录已存在、又不在上一次的回执里 —— 那是别人的 skill(同名很容易撞:gen-image
|
|
178
|
+
// 这种名字一点也不独特)。默认拒绝而不是覆盖,让用户显式 --force。
|
|
179
|
+
const conflicts = names.filter((n) => existsSync(path.join(host.skillsDir, n)) && !owned.has(n));
|
|
180
|
+
if (conflicts.length > 0 && !force) {
|
|
181
|
+
return (`${host.skillsDir} 下已存在同名目录,且不是本 CLI 装的:${conflicts.join('、')}\n` +
|
|
182
|
+
' 确认可以覆盖后加 --force 重跑。');
|
|
183
|
+
}
|
|
184
|
+
mkdirSync(host.skillsDir, { recursive: true });
|
|
185
|
+
// 上一版装过、这一版没有了的 skill(上游改名或下线)要一并清掉,否则宿主会一直
|
|
186
|
+
// 加载一个已经没有后端支撑的 SKILL.md。
|
|
187
|
+
for (const stale of owned) {
|
|
188
|
+
if (!names.includes(stale))
|
|
189
|
+
rmSync(path.join(host.skillsDir, stale), { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
for (const name of names) {
|
|
192
|
+
const dest = path.join(host.skillsDir, name);
|
|
193
|
+
// 先删再拷:直接覆盖会把上一版多出来的文件留在原地,攒出一个既非旧版也非新版的目录。
|
|
194
|
+
rmSync(dest, { recursive: true, force: true });
|
|
195
|
+
cpSync(path.join(SKILLS_DIR, name), dest, { recursive: true });
|
|
196
|
+
}
|
|
197
|
+
const receipt = {
|
|
198
|
+
package: PACKAGE_NAME,
|
|
199
|
+
version,
|
|
200
|
+
installedAt: new Date().toISOString(),
|
|
201
|
+
source: SKILLS_DIR,
|
|
202
|
+
skills: names,
|
|
203
|
+
};
|
|
204
|
+
writeFileSync(path.join(host.skillsDir, RECEIPT), JSON.stringify(receipt, null, 2) + '\n', 'utf-8');
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
export function runInstall(argv) {
|
|
208
|
+
const opts = parseArgs(argv, INSTALL_VERB);
|
|
209
|
+
if (opts.help) {
|
|
210
|
+
installUsage();
|
|
211
|
+
return EXIT.OK;
|
|
212
|
+
}
|
|
213
|
+
if (opts.unknown) {
|
|
214
|
+
process.stderr.write(`❌ 未知参数: ${opts.unknown}\n`);
|
|
215
|
+
installUsage();
|
|
216
|
+
return EXIT.USAGE;
|
|
217
|
+
}
|
|
218
|
+
const names = bundledSkills();
|
|
219
|
+
if (names.length === 0) {
|
|
220
|
+
process.stderr.write(`❌ 包内没有可安装的 skill(${SKILLS_DIR})\n`);
|
|
221
|
+
return EXIT.ERROR;
|
|
222
|
+
}
|
|
223
|
+
if (opts.hosts.length === 0) {
|
|
224
|
+
process.stderr.write('❌ 没有检测到任何 agent 宿主。找过这两个位置:\n' +
|
|
225
|
+
` Claude Code ${hostHome('claude')}\n` +
|
|
226
|
+
` Codex ${hostHome('codex')}\n` +
|
|
227
|
+
' 先把 agent 装好再重跑,或用 --dir <path> 指定目录。\n');
|
|
228
|
+
return EXIT.USAGE;
|
|
229
|
+
}
|
|
230
|
+
const version = packageVersion();
|
|
231
|
+
let failures = 0;
|
|
232
|
+
for (const host of opts.hosts) {
|
|
233
|
+
const err = installInto(host, names, version, opts.force);
|
|
234
|
+
if (err) {
|
|
235
|
+
failures += 1;
|
|
236
|
+
process.stderr.write(`❌ ${padDisplay(host.label, 16)}${err}\n`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
process.stdout.write(`✅ ${padDisplay(host.label, 16)}${host.skillsDir}(${names.length} 个 skill,${version})\n`);
|
|
240
|
+
}
|
|
241
|
+
if (failures > 0)
|
|
242
|
+
return EXIT.ERROR;
|
|
243
|
+
process.stdout.write('\n重启 agent 后即可生效。升级后重跑 remixmate install 覆盖即可。\n');
|
|
244
|
+
return EXIT.OK;
|
|
245
|
+
}
|
|
246
|
+
export function runUninstall(argv) {
|
|
247
|
+
const opts = parseArgs(argv, UNINSTALL_VERB);
|
|
248
|
+
if (opts.help) {
|
|
249
|
+
uninstallUsage();
|
|
250
|
+
return EXIT.OK;
|
|
251
|
+
}
|
|
252
|
+
if (opts.unknown) {
|
|
253
|
+
process.stderr.write(`❌ 未知参数: ${opts.unknown}\n`);
|
|
254
|
+
uninstallUsage();
|
|
255
|
+
return EXIT.USAGE;
|
|
256
|
+
}
|
|
257
|
+
let removed = 0;
|
|
258
|
+
for (const host of opts.hosts) {
|
|
259
|
+
const receipt = readReceipt(host.skillsDir);
|
|
260
|
+
if (!receipt)
|
|
261
|
+
continue;
|
|
262
|
+
for (const name of receipt.skills) {
|
|
263
|
+
rmSync(path.join(host.skillsDir, name), { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
rmSync(path.join(host.skillsDir, RECEIPT), { force: true });
|
|
266
|
+
removed += 1;
|
|
267
|
+
process.stdout.write(`✅ ${padDisplay(host.label, 16)}已移除 ${receipt.skills.length} 个 skill(${host.skillsDir})\n`);
|
|
268
|
+
}
|
|
269
|
+
if (removed === 0) {
|
|
270
|
+
process.stdout.write('没有找到本 CLI 的装机记录,无需卸载。\n');
|
|
271
|
+
}
|
|
272
|
+
return EXIT.OK;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 供 doctor 使用:每个宿主的安装状态。
|
|
276
|
+
*
|
|
277
|
+
* 漂移是这套方案唯一的静默失败模式 —— `npm update -g` 只动包,不动铺出去的副本,
|
|
278
|
+
* 用户会拿着旧 SKILL.md 打新后端。doctor 是「为什么跑不起来」的统一入口,检查放这里。
|
|
279
|
+
*/
|
|
280
|
+
export function installStates() {
|
|
281
|
+
const current = packageVersion();
|
|
282
|
+
return detectHosts().map((h) => {
|
|
283
|
+
const receipt = readReceipt(h.skillsDir);
|
|
284
|
+
return {
|
|
285
|
+
label: h.label,
|
|
286
|
+
skillsDir: h.skillsDir,
|
|
287
|
+
installedVersion: receipt?.version ?? null,
|
|
288
|
+
currentVersion: current,
|
|
289
|
+
drifted: receipt != null && receipt.version !== current,
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
}
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"version": "0.9.22",
|
|
4
|
+
"generatedAt": "2026-09-06T10:24:59.463Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "export-jianying",
|
|
@@ -302,21 +302,21 @@
|
|
|
302
302
|
"ui": {
|
|
303
303
|
"primary": [
|
|
304
304
|
"prompt",
|
|
305
|
+
"reference",
|
|
305
306
|
"model",
|
|
306
|
-
"size"
|
|
307
|
+
"size",
|
|
308
|
+
"n"
|
|
307
309
|
],
|
|
308
310
|
"advanced": [
|
|
309
|
-
"
|
|
310
|
-
"reference",
|
|
311
|
-
"negative_prompt",
|
|
312
|
-
"image_strength"
|
|
311
|
+
"negative_prompt"
|
|
313
312
|
],
|
|
314
313
|
"hidden": [
|
|
315
314
|
"json_output",
|
|
315
|
+
"watermark",
|
|
316
|
+
"resolution",
|
|
316
317
|
"seed",
|
|
317
318
|
"guidance_scale",
|
|
318
|
-
"
|
|
319
|
-
"resolution"
|
|
319
|
+
"image_strength"
|
|
320
320
|
]
|
|
321
321
|
}
|
|
322
322
|
},
|
|
@@ -427,11 +427,12 @@
|
|
|
427
427
|
},
|
|
428
428
|
"ui": {
|
|
429
429
|
"primary": [
|
|
430
|
-
"topic"
|
|
431
|
-
"platform",
|
|
432
|
-
"duration"
|
|
430
|
+
"topic"
|
|
433
431
|
],
|
|
434
|
-
"advanced": [
|
|
432
|
+
"advanced": [],
|
|
433
|
+
"hidden": [
|
|
434
|
+
"platform",
|
|
435
|
+
"duration",
|
|
435
436
|
"style",
|
|
436
437
|
"ratio",
|
|
437
438
|
"scenes",
|
|
@@ -440,9 +441,7 @@
|
|
|
440
441
|
"headline",
|
|
441
442
|
"subheadline",
|
|
442
443
|
"carousel_items",
|
|
443
|
-
"caption_lines"
|
|
444
|
-
],
|
|
445
|
-
"hidden": [
|
|
444
|
+
"caption_lines",
|
|
446
445
|
"stub_image_url",
|
|
447
446
|
"stub_video_url",
|
|
448
447
|
"skip_asset_generation"
|
|
@@ -459,7 +458,8 @@
|
|
|
459
458
|
"triggers": [
|
|
460
459
|
"Text-to-video, AI-generated clip, \"make a short video of ...\"",
|
|
461
460
|
"Generate video with Doubao / Seedance",
|
|
462
|
-
"Image-to-video, first-frame / last-frame, reference-image-to-video"
|
|
461
|
+
"Image-to-video, first-frame / last-frame, reference-image-to-video",
|
|
462
|
+
"Reference video / reference audio, \"same camera move as this clip\", \"use this track\""
|
|
463
463
|
],
|
|
464
464
|
"entry": {
|
|
465
465
|
"type": "http",
|
|
@@ -510,6 +510,28 @@
|
|
|
510
510
|
},
|
|
511
511
|
"description": "Reference images: local file path, https URL, or data URI. Up to 9; combinable with first_frame / last_frame."
|
|
512
512
|
},
|
|
513
|
+
"reference_video": {
|
|
514
|
+
"type": "array",
|
|
515
|
+
"items": {
|
|
516
|
+
"type": "string"
|
|
517
|
+
},
|
|
518
|
+
"description": "Reference videos (https URL only, must be publicly reachable). Up to 3. The model borrows their subject, camera work and style. Cannot be combined with first_frame / last_frame."
|
|
519
|
+
},
|
|
520
|
+
"reference_audio": {
|
|
521
|
+
"type": "array",
|
|
522
|
+
"items": {
|
|
523
|
+
"type": "string"
|
|
524
|
+
},
|
|
525
|
+
"description": "Reference audio (https URL only, must be publicly reachable). Up to 3. Borrows timbre, melody or dialogue. Needs at least one reference image or video alongside it; cannot be combined with first_frame / last_frame."
|
|
526
|
+
},
|
|
527
|
+
"web_search": {
|
|
528
|
+
"type": "boolean",
|
|
529
|
+
"description": "Let the model search the web for up-to-date subjects before generating. Text-only input — ignored once any image / video / audio is attached."
|
|
530
|
+
},
|
|
531
|
+
"return_last_frame": {
|
|
532
|
+
"type": "boolean",
|
|
533
|
+
"description": "Also return the generated clip's last frame, to chain it into the next shot"
|
|
534
|
+
},
|
|
513
535
|
"generate_audio": {
|
|
514
536
|
"type": "boolean",
|
|
515
537
|
"description": "Generate native audio"
|
|
@@ -524,7 +546,7 @@
|
|
|
524
546
|
},
|
|
525
547
|
"seed": {
|
|
526
548
|
"type": "number",
|
|
527
|
-
"description": "Random seed. Pass the same seed with the same prompt and model to make a run reproducible."
|
|
549
|
+
"description": "Random seed, -1 for random. Pass the same seed with the same prompt and model to make a run reproducible."
|
|
528
550
|
},
|
|
529
551
|
"person_generation": {
|
|
530
552
|
"type": "string",
|
|
@@ -548,19 +570,23 @@
|
|
|
548
570
|
"prompt",
|
|
549
571
|
"model",
|
|
550
572
|
"duration",
|
|
551
|
-
"ratio"
|
|
573
|
+
"ratio",
|
|
574
|
+
"resolution"
|
|
552
575
|
],
|
|
553
576
|
"advanced": [
|
|
554
|
-
"resolution",
|
|
555
577
|
"first_frame",
|
|
556
578
|
"last_frame",
|
|
557
579
|
"reference",
|
|
580
|
+
"reference_video",
|
|
581
|
+
"reference_audio",
|
|
558
582
|
"generate_audio",
|
|
559
|
-
"camera_fixed"
|
|
583
|
+
"camera_fixed",
|
|
584
|
+
"web_search",
|
|
585
|
+
"return_last_frame",
|
|
586
|
+
"seed"
|
|
560
587
|
],
|
|
561
588
|
"hidden": [
|
|
562
589
|
"json_output",
|
|
563
|
-
"seed",
|
|
564
590
|
"negative_prompt",
|
|
565
591
|
"person_generation"
|
|
566
592
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.22",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -84,21 +84,21 @@
|
|
|
84
84
|
"ui": {
|
|
85
85
|
"primary": [
|
|
86
86
|
"prompt",
|
|
87
|
+
"reference",
|
|
87
88
|
"model",
|
|
88
|
-
"size"
|
|
89
|
+
"size",
|
|
90
|
+
"n"
|
|
89
91
|
],
|
|
90
92
|
"advanced": [
|
|
91
|
-
"
|
|
92
|
-
"reference",
|
|
93
|
-
"negative_prompt",
|
|
94
|
-
"image_strength"
|
|
93
|
+
"negative_prompt"
|
|
95
94
|
],
|
|
96
95
|
"hidden": [
|
|
97
96
|
"json_output",
|
|
97
|
+
"watermark",
|
|
98
|
+
"resolution",
|
|
98
99
|
"seed",
|
|
99
100
|
"guidance_scale",
|
|
100
|
-
"
|
|
101
|
-
"resolution"
|
|
101
|
+
"image_strength"
|
|
102
102
|
]
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -97,11 +97,12 @@
|
|
|
97
97
|
},
|
|
98
98
|
"ui": {
|
|
99
99
|
"primary": [
|
|
100
|
-
"topic"
|
|
101
|
-
"platform",
|
|
102
|
-
"duration"
|
|
100
|
+
"topic"
|
|
103
101
|
],
|
|
104
|
-
"advanced": [
|
|
102
|
+
"advanced": [],
|
|
103
|
+
"hidden": [
|
|
104
|
+
"platform",
|
|
105
|
+
"duration",
|
|
105
106
|
"style",
|
|
106
107
|
"ratio",
|
|
107
108
|
"scenes",
|
|
@@ -110,9 +111,7 @@
|
|
|
110
111
|
"headline",
|
|
111
112
|
"subheadline",
|
|
112
113
|
"carousel_items",
|
|
113
|
-
"caption_lines"
|
|
114
|
-
],
|
|
115
|
-
"hidden": [
|
|
114
|
+
"caption_lines",
|
|
116
115
|
"stub_image_url",
|
|
117
116
|
"stub_video_url",
|
|
118
117
|
"skip_asset_generation"
|
|
@@ -7,12 +7,14 @@ description: |
|
|
|
7
7
|
- Text-to-video, AI-generated clip, "make a short video of ..."
|
|
8
8
|
- Generate video with Doubao / Seedance
|
|
9
9
|
- Image-to-video, first-frame / last-frame, reference-image-to-video
|
|
10
|
+
- Omni-modal reference: borrow the look of a reference image, the camera work of a reference video, or the voice / music of a reference audio clip
|
|
10
11
|
|
|
11
12
|
Even without an explicit "use AI", any request that turns a description into a moving clip should route here.
|
|
12
13
|
triggers:
|
|
13
14
|
- Text-to-video, AI-generated clip, "make a short video of ..."
|
|
14
15
|
- Generate video with Doubao / Seedance
|
|
15
16
|
- Image-to-video, first-frame / last-frame, reference-image-to-video
|
|
17
|
+
- Reference video / reference audio, "same camera move as this clip", "use this track"
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
# AI Video Generation Skill
|
|
@@ -52,10 +54,34 @@ so they are absent from the catalog and `--model veo` will fail.
|
|
|
52
54
|
| **Duration** | 4–15 seconds (continuous integers) |
|
|
53
55
|
| **First / last frame** | yes |
|
|
54
56
|
| **Reference images** | yes, up to 9 |
|
|
57
|
+
| **Reference videos** | yes, up to 3 (public https URLs) |
|
|
58
|
+
| **Reference audio** | yes, up to 3 (public https URLs) |
|
|
55
59
|
| **Generated audio** | yes |
|
|
56
60
|
| **Fixed camera** | yes |
|
|
61
|
+
| **Web search** | yes, text-only input |
|
|
57
62
|
| **Negative prompt** | ignored (Veo-only parameter) |
|
|
58
63
|
|
|
64
|
+
4k output exists on the standard tier upstream but is **not offered here** — the credit
|
|
65
|
+
pricing table has no 4k row, so a 4k run would be billed at the 720p rate.
|
|
66
|
+
|
|
67
|
+
### Two input modes, and they do not mix
|
|
68
|
+
|
|
69
|
+
- **Pinned frames** — `--first-frame` / `--last-frame` fix the exact opening and closing
|
|
70
|
+
images. Reference images may be added alongside.
|
|
71
|
+
- **Omni-modal reference** — `--reference` (images) / `--reference-video` / `--reference-audio`
|
|
72
|
+
hand the model material to borrow from; it decides the framing.
|
|
73
|
+
|
|
74
|
+
Combining a pinned frame with a reference video or audio is rejected (by the CLI, and by the
|
|
75
|
+
backend). Audio alone is not a valid input either: `--reference-audio` needs at least one
|
|
76
|
+
reference image or video with it.
|
|
77
|
+
|
|
78
|
+
In the prompt, refer to attachments as **"image 1" / "video 2" / "audio 1"**, numbered per
|
|
79
|
+
type in the order you passed them. That is the only way the model can tell them apart —
|
|
80
|
+
file names and asset ids mean nothing to it.
|
|
81
|
+
|
|
82
|
+
Reference images and videos containing **real human faces are rejected upstream** by the
|
|
83
|
+
Seedance 2.0 content filter; the run fails before it charges.
|
|
84
|
+
|
|
59
85
|
## Auth & environment
|
|
60
86
|
|
|
61
87
|
No skill-local env file — the executing process inherits the system environment.
|
|
@@ -123,6 +149,31 @@ remixmate gen-video \
|
|
|
123
149
|
--duration 8
|
|
124
150
|
```
|
|
125
151
|
|
|
152
|
+
### Omni-modal reference (image + video + audio)
|
|
153
|
+
|
|
154
|
+
Videos and audio must be public https URLs — unlike images they are not uploaded.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
remixmate gen-video \
|
|
158
|
+
--prompt "Keep the first-person framing of video 1 and use audio 1 as the score; the product from image 1 is lifted toward the lens" \
|
|
159
|
+
--reference ./product.png \
|
|
160
|
+
--reference-video https://example.com/handheld.mp4 \
|
|
161
|
+
--reference-audio https://example.com/score.mp3 \
|
|
162
|
+
--duration 8 \
|
|
163
|
+
--generate-audio
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Fresh subjects (web search)
|
|
167
|
+
|
|
168
|
+
Only fires on text-only input; attaching any image / video / audio turns it off.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
remixmate gen-video \
|
|
172
|
+
--prompt "<a subject the model may not know: a new product, a current event>" \
|
|
173
|
+
--web-search \
|
|
174
|
+
--duration 5
|
|
175
|
+
```
|
|
176
|
+
|
|
126
177
|
## Common CLI flags
|
|
127
178
|
|
|
128
179
|
| Flag | Description | Default |
|
|
@@ -135,10 +186,14 @@ remixmate gen-video \
|
|
|
135
186
|
| `--first-frame` | First-frame image: local path, https URL, or data URI | none |
|
|
136
187
|
| `--last-frame` | Last-frame image | none |
|
|
137
188
|
| `--reference` | Reference image (repeatable, max 9) | none |
|
|
189
|
+
| `--reference-video` | Reference video, public https URL (repeatable, max 3) | none |
|
|
190
|
+
| `--reference-audio` | Reference audio, public https URL (repeatable, max 3) | none |
|
|
138
191
|
| `--generate-audio` | Generate native audio | off |
|
|
139
192
|
| `--camera-fixed` | Fixed camera | off |
|
|
193
|
+
| `--web-search` | Search the web first (text-only input) | off |
|
|
194
|
+
| `--return-last-frame` | Also return the clip's last frame, to chain shots | off |
|
|
140
195
|
| `--negative-prompt` | Negative prompt (Veo only; ignored by Seedance) | none |
|
|
141
|
-
| `--seed` | Random seed | none |
|
|
196
|
+
| `--seed` | Random seed; `-1` = random | none |
|
|
142
197
|
| `--person-generation` | Person policy: `allow_all` / `dont_allow` (Veo) | none |
|
|
143
198
|
| `--api-base-url` | Override API root | see above |
|
|
144
199
|
| `--priv-token` | Override token | see above |
|
|
@@ -55,6 +55,28 @@
|
|
|
55
55
|
},
|
|
56
56
|
"description": "Reference images: local file path, https URL, or data URI. Up to 9; combinable with first_frame / last_frame."
|
|
57
57
|
},
|
|
58
|
+
"reference_video": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "string"
|
|
62
|
+
},
|
|
63
|
+
"description": "Reference videos (https URL only, must be publicly reachable). Up to 3. The model borrows their subject, camera work and style. Cannot be combined with first_frame / last_frame."
|
|
64
|
+
},
|
|
65
|
+
"reference_audio": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": {
|
|
68
|
+
"type": "string"
|
|
69
|
+
},
|
|
70
|
+
"description": "Reference audio (https URL only, must be publicly reachable). Up to 3. Borrows timbre, melody or dialogue. Needs at least one reference image or video alongside it; cannot be combined with first_frame / last_frame."
|
|
71
|
+
},
|
|
72
|
+
"web_search": {
|
|
73
|
+
"type": "boolean",
|
|
74
|
+
"description": "Let the model search the web for up-to-date subjects before generating. Text-only input — ignored once any image / video / audio is attached."
|
|
75
|
+
},
|
|
76
|
+
"return_last_frame": {
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"description": "Also return the generated clip's last frame, to chain it into the next shot"
|
|
79
|
+
},
|
|
58
80
|
"generate_audio": {
|
|
59
81
|
"type": "boolean",
|
|
60
82
|
"description": "Generate native audio"
|
|
@@ -69,7 +91,7 @@
|
|
|
69
91
|
},
|
|
70
92
|
"seed": {
|
|
71
93
|
"type": "number",
|
|
72
|
-
"description": "Random seed. Pass the same seed with the same prompt and model to make a run reproducible."
|
|
94
|
+
"description": "Random seed, -1 for random. Pass the same seed with the same prompt and model to make a run reproducible."
|
|
73
95
|
},
|
|
74
96
|
"person_generation": {
|
|
75
97
|
"type": "string",
|
|
@@ -93,19 +115,23 @@
|
|
|
93
115
|
"prompt",
|
|
94
116
|
"model",
|
|
95
117
|
"duration",
|
|
96
|
-
"ratio"
|
|
118
|
+
"ratio",
|
|
119
|
+
"resolution"
|
|
97
120
|
],
|
|
98
121
|
"advanced": [
|
|
99
|
-
"resolution",
|
|
100
122
|
"first_frame",
|
|
101
123
|
"last_frame",
|
|
102
124
|
"reference",
|
|
125
|
+
"reference_video",
|
|
126
|
+
"reference_audio",
|
|
103
127
|
"generate_audio",
|
|
104
|
-
"camera_fixed"
|
|
128
|
+
"camera_fixed",
|
|
129
|
+
"web_search",
|
|
130
|
+
"return_last_frame",
|
|
131
|
+
"seed"
|
|
105
132
|
],
|
|
106
133
|
"hidden": [
|
|
107
134
|
"json_output",
|
|
108
|
-
"seed",
|
|
109
135
|
"negative_prompt",
|
|
110
136
|
"person_generation"
|
|
111
137
|
]
|