@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.17",
4
- "generatedAt": "2026-08-29T15:20:08.872Z",
3
+ "version": "0.9.18",
4
+ "generatedAt": "2026-08-30T03:02:58.749Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "export-jianying",
@@ -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
- if (!skill.createsTake && !skill.joinsTake)
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 (skill.createsTake) {
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;
@@ -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.
@@ -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.17",
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 `upload.fileUrl` field. **Prefer `upload.playbackUrl`** (transcoded) over `upload.fileUrl` (raw VOD) when surfacing the URL to a user.
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",