@remixmate/cli 0.9.17 → 0.9.18
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/dist/manifest.json +2 -2
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/runner.d.ts +18 -0
- package/dist/runner.js +28 -2
- package/dist/skill-schema.d.ts +9 -0
- package/dist/skill-schema.js +16 -0
- package/package.json +1 -1
- package/skills/render-video/SKILL.md +11 -1
- package/skills/render-video/scripts/render_video.py +20 -0
- package/skills/render-video/skill.json +1 -0
package/dist/manifest.json
CHANGED
package/dist/registry.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface SkillDef {
|
|
|
20
20
|
createsTake: boolean;
|
|
21
21
|
/** Attach to an existing take, never create one — see skill-schema.ts. */
|
|
22
22
|
joinsTake: boolean;
|
|
23
|
+
/** Argv keys that downgrade createsTake to joinsTake — see skill-schema.ts. */
|
|
24
|
+
createsTakeUnless: string[];
|
|
23
25
|
skillDir: string;
|
|
24
26
|
/** Absolute path to the python entry script (only when entry.type === 'python'). */
|
|
25
27
|
scriptAbsolutePath?: string;
|
package/dist/registry.js
CHANGED
|
@@ -40,6 +40,7 @@ function loadOne(skillDir) {
|
|
|
40
40
|
auth: resolveAuthMode(raw),
|
|
41
41
|
createsTake: raw.createsTake === true,
|
|
42
42
|
joinsTake: raw.joinsTake === true,
|
|
43
|
+
createsTakeUnless: Array.isArray(raw.createsTakeUnless) ? raw.createsTakeUnless : [],
|
|
43
44
|
skillDir,
|
|
44
45
|
};
|
|
45
46
|
if (entry.type === 'python') {
|
package/dist/runner.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Resolves to the child process's exit code so cli.ts can propagate it.
|
|
11
11
|
*/
|
|
12
|
+
import { type SkillDef } from './registry.js';
|
|
12
13
|
import type { ParsedArgs } from './argv.js';
|
|
13
14
|
export interface RunOptions {
|
|
14
15
|
/** Override the skills directory (defaults to the package's bundled skills/). */
|
|
@@ -19,3 +20,20 @@ export interface RunOptions {
|
|
|
19
20
|
parsedArgs: ParsedArgs;
|
|
20
21
|
}
|
|
21
22
|
export declare function runSkill(skillName: string, opts: RunOptions): Promise<number>;
|
|
23
|
+
/** What this invocation is allowed to do about attribution. */
|
|
24
|
+
export type TakeMode = 'create' | 'join' | 'none';
|
|
25
|
+
/**
|
|
26
|
+
* Decide the take mode from the skill's declaration *and* what this particular
|
|
27
|
+
* invocation was asked to do.
|
|
28
|
+
*
|
|
29
|
+
* The manifest flags alone are not enough, because they describe the skill, not
|
|
30
|
+
* the call: `render-video --help` prints usage and exits, and
|
|
31
|
+
* `render-video --resolve-only` stops after asset resolution. Both used to run
|
|
32
|
+
* the full createsTake path, so a help query was enough to mint an empty take
|
|
33
|
+
* and lazily create 「未分类」 — the exact clutter project/take.ts sets out to
|
|
34
|
+
* avoid ("an empty VideoProject is worse clutter than an unattributed image").
|
|
35
|
+
*
|
|
36
|
+
* Note that a downgraded run still *joins*: when its own pipeline already
|
|
37
|
+
* started a take under the same grouping key, assets it generates stay attached.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveTakeMode(skill: Pick<SkillDef, 'createsTake' | 'joinsTake' | 'createsTakeUnless'>, args: ParsedArgs): TakeMode;
|
package/dist/runner.js
CHANGED
|
@@ -61,6 +61,31 @@ export async function runSkill(skillName, opts) {
|
|
|
61
61
|
emitBillingFooter();
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Decide the take mode from the skill's declaration *and* what this particular
|
|
66
|
+
* invocation was asked to do.
|
|
67
|
+
*
|
|
68
|
+
* The manifest flags alone are not enough, because they describe the skill, not
|
|
69
|
+
* the call: `render-video --help` prints usage and exits, and
|
|
70
|
+
* `render-video --resolve-only` stops after asset resolution. Both used to run
|
|
71
|
+
* the full createsTake path, so a help query was enough to mint an empty take
|
|
72
|
+
* and lazily create 「未分类」 — the exact clutter project/take.ts sets out to
|
|
73
|
+
* avoid ("an empty VideoProject is worse clutter than an unattributed image").
|
|
74
|
+
*
|
|
75
|
+
* Note that a downgraded run still *joins*: when its own pipeline already
|
|
76
|
+
* started a take under the same grouping key, assets it generates stay attached.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveTakeMode(skill, args) {
|
|
79
|
+
// Introspection — the skill prints its usage and exits without producing
|
|
80
|
+
// anything, so it must not touch the backend at all.
|
|
81
|
+
if (args.help === true || args.h === true)
|
|
82
|
+
return 'none';
|
|
83
|
+
if (skill.createsTake) {
|
|
84
|
+
const downgraded = (skill.createsTakeUnless ?? []).some((key) => args[key] === true);
|
|
85
|
+
return downgraded ? 'join' : 'create';
|
|
86
|
+
}
|
|
87
|
+
return skill.joinsTake ? 'join' : 'none';
|
|
88
|
+
}
|
|
64
89
|
/**
|
|
65
90
|
* Attach this invocation to a take, publishing the result as `CONVERSATION_ID`.
|
|
66
91
|
* That variable is the only thing that makes an uploaded asset reachable from
|
|
@@ -83,7 +108,8 @@ export async function runSkill(skillName, opts) {
|
|
|
83
108
|
* asset still uploads, just unattributed — exactly the old behaviour.
|
|
84
109
|
*/
|
|
85
110
|
async function applyTakeContext(skill, opts, auth) {
|
|
86
|
-
|
|
111
|
+
const mode = resolveTakeMode(skill, opts.parsedArgs);
|
|
112
|
+
if (mode === 'none')
|
|
87
113
|
return;
|
|
88
114
|
if ((process.env.CONVERSATION_ID ?? '').trim())
|
|
89
115
|
return;
|
|
@@ -95,7 +121,7 @@ async function applyTakeContext(skill, opts, auth) {
|
|
|
95
121
|
preflightToken: auth.token,
|
|
96
122
|
});
|
|
97
123
|
let take;
|
|
98
|
-
if (
|
|
124
|
+
if (mode === 'create') {
|
|
99
125
|
const jobIdArg = opts.parsedArgs.job_id;
|
|
100
126
|
const jobId = typeof jobIdArg === 'number' ? jobIdArg : Number(jobIdArg);
|
|
101
127
|
const flagProject = opts.parsedArgs.project;
|
package/dist/skill-schema.d.ts
CHANGED
|
@@ -72,6 +72,15 @@ export interface RawSkillJson {
|
|
|
72
72
|
* worse clutter than an unattributed asset.
|
|
73
73
|
*/
|
|
74
74
|
joinsTake?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Parsed-argv keys (snake_case, as produced by argv.ts) whose presence means
|
|
77
|
+
* *this* invocation won't finish a video — `render-video --resolve-only`
|
|
78
|
+
* resolves assets and stops. Such a run degrades from createsTake to
|
|
79
|
+
* joinsTake: it still attaches to a take its own pipeline already started,
|
|
80
|
+
* but never brings one — nor the lazily created 「未分类」 — into being for a
|
|
81
|
+
* pass that may end in a contract error and no video at all.
|
|
82
|
+
*/
|
|
83
|
+
createsTakeUnless?: string[];
|
|
75
84
|
}
|
|
76
85
|
/**
|
|
77
86
|
* Resolve a skill's auth mode, defaulting for records that predate the field.
|
package/dist/skill-schema.js
CHANGED
|
@@ -67,6 +67,22 @@ export function validateSkillJson(raw, skillId) {
|
|
|
67
67
|
if (raw.joinsTake != null && typeof raw.joinsTake !== 'boolean') {
|
|
68
68
|
errors.push(`skill.json.joinsTake must be a boolean, got '${String(raw.joinsTake)}'`);
|
|
69
69
|
}
|
|
70
|
+
if (raw.createsTakeUnless != null) {
|
|
71
|
+
if (!Array.isArray(raw.createsTakeUnless) ||
|
|
72
|
+
raw.createsTakeUnless.some((f) => typeof f !== 'string' || f === '')) {
|
|
73
|
+
errors.push("skill.json.createsTakeUnless must be an array of non-empty argv keys");
|
|
74
|
+
}
|
|
75
|
+
else if (raw.createsTake !== true) {
|
|
76
|
+
// It only ever downgrades createsTake; on any other skill it would read as
|
|
77
|
+
// working while doing nothing.
|
|
78
|
+
errors.push("skill.json.createsTakeUnless requires createsTake=true");
|
|
79
|
+
}
|
|
80
|
+
else if (raw.createsTakeUnless.some((f) => f.includes('-'))) {
|
|
81
|
+
// argv.ts hands the dispatcher snake_case keys, so `--resolve-only`
|
|
82
|
+
// arrives as `resolve_only`. A kebab entry would silently never match.
|
|
83
|
+
errors.push("skill.json.createsTakeUnless entries must be snake_case argv keys (e.g. 'resolve_only', not 'resolve-only')");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
if (raw.createsTake === true && raw.joinsTake === true) {
|
|
71
87
|
errors.push("skill.json cannot set both 'createsTake' and 'joinsTake' — createsTake already implies joining");
|
|
72
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.18",
|
|
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",
|
|
@@ -114,7 +114,17 @@ python3 <SkillDir>/scripts/render_video.py --render-plan <path>.render-plan.json
|
|
|
114
114
|
python3 <SkillDir>/scripts/render_video.py --job-id 42 --upload-title "My video"
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
After rendering, the script auto-uploads by default; `render-manifest.json` then contains an
|
|
117
|
+
After rendering, the script auto-uploads by default; `render-manifest.json` then contains an
|
|
118
|
+
`upload.fileUrl` field, and `upload.playbackUrl` (transcoded) once VOD processing finishes.
|
|
119
|
+
|
|
120
|
+
**Do not retype the video URL into your reply.** On success the script prints a structured
|
|
121
|
+
`__render_video_asset__` line; the host reads the authoritative URL from there and renders a
|
|
122
|
+
player for the user. Tell the user the video is ready — the player appears on its own.
|
|
123
|
+
|
|
124
|
+
Reproducing a 32-char opaque URL from memory is unreliable: on 2026-08-30 a single character
|
|
125
|
+
was dropped (`…c1c20102` → `…c1c2012`), so the user got a 404 while the file sat fine on the
|
|
126
|
+
CDN. The URL in your prose is redundant with the structured asset and is the only copy that
|
|
127
|
+
can be wrong — the UI now renders it as plain, non-clickable text for exactly this reason.
|
|
118
128
|
|
|
119
129
|
### Render but skip the upload
|
|
120
130
|
|
|
@@ -2413,6 +2413,26 @@ Examples:
|
|
|
2413
2413
|
effective_playback = playback_url or remote_url
|
|
2414
2414
|
if effective_playback and not effective_playback.startswith("vod://"):
|
|
2415
2415
|
print(f"\n🎬 Video playback URL: {effective_playback}")
|
|
2416
|
+
|
|
2417
|
+
# 结构化资产标记 —— 宿主(ab-agent)据此把权威 URL 作为 attachment
|
|
2418
|
+
# 下发前端渲染播放器,绕开模型正文。
|
|
2419
|
+
#
|
|
2420
|
+
# 为什么必须绕开:模型复述 32 位不透明 hex 会出错。2026-08-30 实测一次
|
|
2421
|
+
# 成片链接被吞掉一个字符(…c1c20102 → …c1c2012),用户拿到 404,
|
|
2422
|
+
# 而文件本身好好地在 CDN 上。约定与 web-screenshot/scripts/record.py
|
|
2423
|
+
# 的 __web_record_asset__ 一致。
|
|
2424
|
+
#
|
|
2425
|
+
# 只在拿到**非 vod:// 的真实播放地址**时才打标记(上面的 if 已保证):
|
|
2426
|
+
# playbackUrl 来自 VOD 轮询,可能超时未就绪。宁可前端没有内联播放器
|
|
2427
|
+
# (用户仍可从成片面板看),也不要再给出一条不可用的地址。
|
|
2428
|
+
asset = {"url": effective_playback}
|
|
2429
|
+
cover = upload_info.get("coverUrl")
|
|
2430
|
+
if cover:
|
|
2431
|
+
asset["coverUrl"] = cover
|
|
2432
|
+
total_duration = (render_plan.get("renderConfig") or {}).get("totalDuration")
|
|
2433
|
+
if isinstance(total_duration, (int, float)) and total_duration > 0:
|
|
2434
|
+
asset["durationSec"] = total_duration
|
|
2435
|
+
print("__render_video_asset__ " + json.dumps(asset, ensure_ascii=False))
|
|
2416
2436
|
else:
|
|
2417
2437
|
LogPrint(f" Video: {output_path}", file=sys.stderr)
|
|
2418
2438
|
if args.save_job:
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"description": "Loads a persisted RenderPlan by job_id and drives the Remotion engine to produce the final video. Assets must already be generated via prepare_video_assets — this skill never resolves or regenerates assets.",
|
|
8
8
|
"auth": "required",
|
|
9
9
|
"createsTake": true,
|
|
10
|
+
"createsTakeUnless": ["resolve_only"],
|
|
10
11
|
"envVars": [
|
|
11
12
|
"PRIV_TOKEN",
|
|
12
13
|
"MM_API_BASE_URL",
|