@remixmate/cli 0.1.1 → 0.9.1

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 (62) hide show
  1. package/README.md +46 -0
  2. package/README.zh-CN.md +22 -0
  3. package/dist/auth/auth-lock.d.ts +26 -0
  4. package/dist/auth/auth-lock.js +100 -0
  5. package/dist/auth/auto-login.d.ts +20 -0
  6. package/dist/auth/auto-login.js +66 -0
  7. package/dist/auth/commands.d.ts +8 -0
  8. package/dist/auth/commands.js +130 -0
  9. package/dist/auth/credential-store.d.ts +44 -0
  10. package/dist/auth/credential-store.js +126 -0
  11. package/dist/auth/device-flow-runner.d.ts +43 -0
  12. package/dist/auth/device-flow-runner.js +62 -0
  13. package/dist/auth/device-flow.d.ts +52 -0
  14. package/dist/auth/device-flow.js +115 -0
  15. package/dist/auth/environment.d.ts +25 -0
  16. package/dist/auth/environment.js +48 -0
  17. package/dist/auth/resolve.d.ts +30 -0
  18. package/dist/auth/resolve.js +44 -0
  19. package/dist/cli.js +11 -0
  20. package/dist/handlers/gen-digital-human.js +1 -1
  21. package/dist/handlers/gen-image.js +1 -1
  22. package/dist/handlers/gen-video.js +1 -1
  23. package/dist/handlers/gen-voice.js +2 -2
  24. package/dist/http.d.ts +9 -5
  25. package/dist/http.js +24 -10
  26. package/dist/manifest.json +14 -3
  27. package/dist/runner.d.ts +1 -1
  28. package/dist/skill-schema.d.ts +18 -0
  29. package/dist/skill-schema.js +4 -0
  30. package/package.json +1 -1
  31. package/skills/export-jianying/skill.json +1 -0
  32. package/skills/gen-digital-human/SKILL.md +11 -11
  33. package/skills/gen-digital-human/skill.json +1 -0
  34. package/skills/gen-digital-human/version.json +1 -1
  35. package/skills/gen-image/SKILL.md +6 -6
  36. package/skills/gen-image/skill.json +1 -0
  37. package/skills/gen-image/version.json +1 -1
  38. package/skills/gen-script/scripts/gen_script.py +105 -31
  39. package/skills/gen-script/skill.json +3 -1
  40. package/skills/gen-script/version.json +1 -1
  41. package/skills/gen-video/SKILL.md +6 -6
  42. package/skills/gen-video/skill.json +1 -0
  43. package/skills/gen-video/version.json +1 -1
  44. package/skills/gen-voice/SKILL.md +6 -6
  45. package/skills/gen-voice/skill.json +1 -0
  46. package/skills/gen-voice/version.json +1 -1
  47. package/skills/prepare-video-assets/skill.json +1 -0
  48. package/skills/render-video/scripts/remote_renderer_client.py +9 -7
  49. package/skills/render-video/scripts/render_video.py +72 -15
  50. package/skills/render-video/skill.json +1 -0
  51. package/skills/render-video/version.json +1 -1
  52. package/skills/template-registry/README.md +12 -13
  53. package/skills/template-registry/SKILL.md +11 -12
  54. package/skills/template-registry/scripts/list_templates.py +87 -2
  55. package/skills/template-registry/scripts/registry_loader.py +117 -96
  56. package/skills/template-registry/scripts/render_job_client.py +12 -0
  57. package/skills/template-registry/skill.json +4 -2
  58. package/skills/template-registry/version.json +1 -1
  59. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +2 -2
  60. package/skills/video-parser/skill.json +1 -0
  61. package/skills/web-capture/skill.json +1 -1
  62. package/skills/web-capture/version.json +1 -1
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-voice",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "338",
5
- "version": "V6",
5
+ "version": "V7",
6
6
  "skillDescription": "语音合成(TTS)技能,将文字转为语音音频(调用 ab-api /tool/minimaxT2aV2,基于 Minimax TTS)。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- AI 配音、语音合成、文字转语音、TTS、文本转音频、生成语音\n- 使用 minimax 语音合成\n- 用户想要将一段文字朗读出来、生成音频\n\n即使用户没有明确说「使用 AI」,只要他们想要将文字转为语音,也要使用本 skill。"
7
7
  }
@@ -2,6 +2,7 @@
2
2
  "name": "prepare-video-assets",
3
3
  "toolName": "prepare_video_assets",
4
4
  "tier": "orchestration",
5
+ "category": "authoring",
5
6
  "title": "Video Asset Preparation",
6
7
  "description": "Resolves and generates every asset (image / audio / video) referenced by a Video DSL, persists a RenderPlan to the database, and returns a job_id for the subsequent render_video call. This is Phase 1 of the two-phase video pipeline; Phase 3 (Remotion render) lives in render_video.",
7
8
  "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "MM_BACKEND_API_URL", "AGENT_NAME", "REMOTION_RENDER_API_URL", "REMOTION_RENDER_MODE", "REMOTION_OUTPUT_DIR", "ASSET_CACHE_DIR"],
@@ -121,11 +121,12 @@ def start_render(
121
121
  base_url: Optional[str] = None,
122
122
  timeout: float = 120.0,
123
123
  conversation_id: Optional[str] = None,
124
+ path: str = "/render",
124
125
  ) -> str:
125
- """POST /render;返回 taskId。"""
126
+ """POST <path>(默认 /render,私有模板动态渲染传 /renderDraft);返回 taskId。"""
126
127
  result = _request(
127
128
  "POST",
128
- "/render",
129
+ path,
129
130
  private_token=private_token,
130
131
  payload=payload,
131
132
  base_url=base_url,
@@ -135,7 +136,7 @@ def start_render(
135
136
  data = _check_code(result)
136
137
  task_id = data.get("taskId")
137
138
  if not task_id:
138
- raise RemoteRenderError(f"/render did not return a taskId: {result}")
139
+ raise RemoteRenderError(f"{path} did not return a taskId: {result}")
139
140
  return str(task_id)
140
141
 
141
142
 
@@ -150,8 +151,9 @@ def poll_render(
150
151
  max_consecutive_errors: int = 5,
151
152
  on_progress: Optional[Callable[[dict], None]] = None,
152
153
  adaptive_interval: bool = False,
154
+ status_path: str = "/renderStatus",
153
155
  ) -> dict:
154
- """POST /renderStatus 轮询直到完成/失败/超时。
156
+ """POST <status_path>(默认 /renderStatus,私有模板传 /renderDraftStatus)轮询直到完成/失败/超时。
155
157
 
156
158
  成功时返回 data dict(至少包含 fileUrl),否则抛 RemoteRenderError。
157
159
  on_progress 回调在每次成功请求后触发,参数为完整 data dict。
@@ -170,13 +172,13 @@ def poll_render(
170
172
  if elapsed > timeout:
171
173
  raise RemoteRenderError(
172
174
  f"remote render polling timed out (waited {elapsed:.0f}s, taskId={task_id}); "
173
- f"the job may still be running on the backend — query /renderStatus later"
175
+ f"the job may still be running on the backend — query {status_path} later"
174
176
  )
175
177
 
176
178
  try:
177
179
  result = _request(
178
180
  "POST",
179
- "/renderStatus",
181
+ status_path,
180
182
  private_token=private_token,
181
183
  payload={"taskId": task_id},
182
184
  base_url=base_url,
@@ -201,7 +203,7 @@ def poll_render(
201
203
  body = exc.read().decode("utf-8")
202
204
  except Exception:
203
205
  pass
204
- raise RemoteRenderError(f"HTTP {exc.code} /renderStatus: {body[:200]}") from exc
206
+ raise RemoteRenderError(f"HTTP {exc.code} {status_path}: {body[:200]}") from exc
205
207
 
206
208
  consecutive_errors = 0
207
209
 
@@ -1607,26 +1607,73 @@ def render_with_remote_api(
1607
1607
  template_id = render_plan.get("templateId", "")
1608
1608
  cover_composition_id = resolve_cover_composition_id(template_id)
1609
1609
 
1610
- payload = {
1611
- "compositionId": composition_id,
1612
- "renderConfig": config,
1613
- "inputProps": input_props,
1614
- "uploadTitle": effective_title,
1615
- }
1616
- if cover_composition_id:
1617
- payload["coverCompositionId"] = cover_composition_id
1610
+ # ─── 私有模板路由 ────────────────────────────────────────────────────
1611
+ # 仅【用户私有模板】(isBuiltin=false 且带 sourceOssKey) 源码在 OSS、不在
1612
+ # ab-render 启动 bundle,需走动态渲染:presignSource 取临时 GET URL → /renderDraft。
1613
+ # 内置模板即使带 sourceOssKey(builtin-template-registry 回填,仅服务
1614
+ # derive_template 派生),渲染仍走 ab-render 预构建的 /render,绝不走 /renderDraft,
1615
+ # 否则会用 OSS 上的旧快照 + DraftMainVideo 复刻渲染,与生产 MainVideo 漂移。
1616
+ source_oss_key = None
1617
+ template_id = render_plan.get("templateId", "")
1618
+ if template_id:
1619
+ try:
1620
+ from registry_loader import get_template # type: ignore
1621
+ _meta = get_template(template_id)
1622
+ if _meta and not _meta.get("isBuiltin"):
1623
+ source_oss_key = _meta.get("sourceOssKey")
1624
+ except Exception:
1625
+ source_oss_key = None
1626
+
1627
+ if source_oss_key:
1628
+ import render_job_client # type: ignore
1629
+ try:
1630
+ src = render_job_client.presign_template_source(template_id, private_token)
1631
+ tarball_url = src.get("tarballUrl")
1632
+ if not tarball_url:
1633
+ raise RuntimeError(f"presignSource 未返回 tarballUrl: {src}")
1634
+ except Exception as exc:
1635
+ LogPrint(f"❌ presignSource failed for private template {template_id}: {exc}", file=sys.stderr)
1636
+ render_plan["status"] = "failed"
1637
+ render_plan["errors"].append({
1638
+ "phase": "render",
1639
+ "message": f"presignSource failed: {exc}",
1640
+ "timestamp": now_iso(),
1641
+ })
1642
+ return False
1643
+ payload = {
1644
+ "tarballUrl": tarball_url,
1645
+ "inputProps": input_props,
1646
+ "upload": True,
1647
+ "uploadTitle": effective_title,
1648
+ }
1649
+ render_path = "/renderDraft"
1650
+ status_path = "/renderDraftStatus"
1651
+ else:
1652
+ payload = {
1653
+ "compositionId": composition_id,
1654
+ "renderConfig": config,
1655
+ "inputProps": input_props,
1656
+ "uploadTitle": effective_title,
1657
+ }
1658
+ if cover_composition_id:
1659
+ payload["coverCompositionId"] = cover_composition_id
1660
+ render_path = "/render"
1661
+ status_path = "/renderStatus"
1618
1662
 
1619
1663
  total_frames = config.get("totalFrames", 0)
1620
1664
  LogPrint(f"🎬 Submitting remote render task...", file=sys.stderr)
1621
- LogPrint(f" Composition: {composition_id}", file=sys.stderr)
1622
- if cover_composition_id:
1665
+ if source_oss_key:
1666
+ LogPrint(f" Private template (OSS dynamic bundle): {template_id}", file=sys.stderr)
1667
+ else:
1668
+ LogPrint(f" Composition: {composition_id}", file=sys.stderr)
1669
+ if cover_composition_id and not source_oss_key:
1623
1670
  LogPrint(f" Cover: {cover_composition_id}", file=sys.stderr)
1624
1671
  LogPrint(f" size: {config.get('width')}x{config.get('height')}", file=sys.stderr)
1625
1672
  LogPrint(f" total frames: {total_frames}", file=sys.stderr)
1626
1673
  sys.stderr.flush()
1627
1674
 
1628
1675
  try:
1629
- task_id = remote_renderer_client.start_render(payload, private_token=private_token, conversation_id=conversation_id)
1676
+ task_id = remote_renderer_client.start_render(payload, private_token=private_token, conversation_id=conversation_id, path=render_path)
1630
1677
  except Exception as exc:
1631
1678
  LogPrint(f"❌ remote render submission failed: {exc}", file=sys.stderr)
1632
1679
  render_plan["status"] = "failed"
@@ -1686,6 +1733,7 @@ def render_with_remote_api(
1686
1733
  interval=poll_interval,
1687
1734
  on_progress=_on_progress,
1688
1735
  adaptive_interval=True,
1736
+ status_path=status_path,
1689
1737
  )
1690
1738
  except Exception as exc:
1691
1739
  LogPrint(f"❌ remote render polling failed: {exc}", file=sys.stderr)
@@ -1904,13 +1952,22 @@ Examples:
1904
1952
  LogPrint(f"📋 loaded existing RenderPlan: {args.render_plan}", file=sys.stderr)
1905
1953
  else:
1906
1954
  if not args.dsl and not args.dsl_json:
1907
- LogPrint("❌ pass --dsl or --dsl-json (with --template-id), or --render-plan / --job-id", file=sys.stderr)
1908
- parser.print_help()
1955
+ LogPrint(
1956
+ "❌ render_video needs a DSL source. You provided neither --dsl/--dsl-json "
1957
+ "nor --render-plan/--job-id.\n"
1958
+ " Typical flow: 1) gen_script → DSL skeleton 2) prepare_video_assets "
1959
+ "(returns a job_id) 3) render_video with job_id=<int>.\n"
1960
+ " Or pass the DSL inline: render_video --dsl-json '<json>' --template-id <id>.",
1961
+ file=sys.stderr,
1962
+ )
1909
1963
  sys.exit(1)
1910
1964
 
1911
1965
  if not args.template_id:
1912
- LogPrint("❌ pass --template-id (--binding is no longer supported; binding is now computed in-memory from --template-id)", file=sys.stderr)
1913
- parser.print_help()
1966
+ LogPrint(
1967
+ "❌ render_video needs --template-id when rendering from --dsl/--dsl-json "
1968
+ "(binding is computed in-memory from the template id; --binding is no longer supported).",
1969
+ file=sys.stderr,
1970
+ )
1914
1971
  sys.exit(1)
1915
1972
 
1916
1973
  # 解析 DSL:优先 --dsl-json(inline),其次 --dsl(文件路径)
@@ -2,6 +2,7 @@
2
2
  "name": "render-video",
3
3
  "toolName": "render_video",
4
4
  "tier": "orchestration",
5
+ "category": "authoring",
5
6
  "title": "Remotion Video Renderer",
6
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.",
7
8
  "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "MM_BACKEND_API_URL", "AGENT_NAME", "REMOTION_RENDER_API_URL", "REMOTION_RENDER_MODE", "REMOTION_OUTPUT_DIR", "ASSET_CACHE_DIR"],
@@ -2,6 +2,6 @@
2
2
  "skillName": "render-video",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "473",
5
- "version": "V15",
5
+ "version": "V18",
6
6
  "skillDescription": "Final-render skill (Phase 3 of the two-phase video pipeline). Loads a persisted RenderPlan by job_id and drives Remotion to produce the final video. Assets must already be generated via prepare_video_assets."
7
7
  }
@@ -4,30 +4,29 @@ CLI 暴露面是「列出可用模板」(`--list-templates`)。把 Video DSL 场
4
4
 
5
5
  ## 模板数据来源
6
6
 
7
- 模板元数据维护在独立仓库 **template-library**。本 skill 通过 `scripts/registry_loader.py` 按以下顺序解析:
7
+ 模板元数据维护在独立仓库 **template-library**,发布到 ab-api,运行时由本 skill 通过 `scripts/registry_loader.py` 从 **单一数据源(ab-api HTTP)** 加载:
8
8
 
9
- | 优先级 | 来源 | 触发条件 | 适用场景 |
10
- |---|---|---|---|
11
- | 1 | `VIDEO_TEMPLATE_REGISTRY` 指向的本地 JSON 文件 | 环境变量非空 | CI / 离线 / pinned 调试 |
12
- | 2 | `VIDEO_TEMPLATE_REGISTRY_URL`(ab-api 接口) | URL 设置 + `PRIV_TOKEN` 有效 | **生产** —— 多租户隔离由 ab-api |
13
- | 3 | monorepo `template-library/packages/metadata/registry.json` | 本地源码可见 | 本地 dev / 单仓部署 |
9
+ | 来源 | 触发条件 | 适用场景 |
10
+ |---|---|---|
11
+ | 显式 `VIDEO_TEMPLATE_REGISTRY_URL`(ab-api 接口) | 环境变量非空 + `PRIV_TOKEN` 有效 | 指定后端 |
12
+ | 派生默认 URL `<MM_API_BASE_URL>/remotionTemplate/registry` | `VIDEO_TEMPLATE_REGISTRY_URL` 未设置 | 默认(含独立安装 codex / `npm i -g`,默认 `http://localhost:2999/api`) |
13
+
14
+ 只从 ab-api 取数,是为了避免「本地文件 / monorepo 源码 / 数据库」多源并存导致的不一致——私有 / 多租户模板只存在于 ab-api,本地源永远不全。默认 URL **内置在 remixmate-cli 自身**(按 CLI 后端约定 `MM_API_BASE_URL` 推导),不再依赖 ab-agent 等宿主在 spawn 时注入,独立运行即可找到 registry。指向你自己的 ab-api 只需设 `MM_API_BASE_URL`(或直接设 `VIDEO_TEMPLATE_REGISTRY_URL`)并配置 `PRIV_TOKEN`。
14
15
 
15
- `VIDEO_TEMPLATE_REGISTRY_PREFER_LOCAL=1` 开启后,#3 抢在 #2 之前——本地源码改动立即生效,不被 HTTP 缓存覆盖。
16
+ HTTP 拉取带磁盘缓存(TTL + ETag/304);瞬时故障时降级复用同一 URL 的上一份缓存(同源容错,非第二个数据源),无缓存则直接报错。
16
17
 
17
18
  ab-api 响应包装格式 `{code, msg, data}` 由 `registry_loader._fetch_http` 透明拆封;静态 JSON endpoint 也支持(直接返回 registry 文档)。
18
19
 
19
- > 早期文档曾描述本 skill 通过 `@ab-templates/metadata` npm 包消费 registry —— 该路径已被 ab-api HTTP 取代,但保留 monorepo 文件路径作为兜底。
20
+ > 早期文档曾描述本 skill 通过 `@ab-templates/metadata` npm / monorepo 文件消费 registry —— 这些本地来源已移除,运行时只走 ab-api HTTP。monorepo 文件仅供 `check_contracts.py` / `sync_registry.py` 等 monorepo-only 维护脚本使用。
20
21
 
21
22
  ## 新增模板工作流
22
23
 
23
- 新增模板**不需要改 ab-skill / template-registry 代码**:
24
+ 新增模板**不需要改 remixmate / template-registry 代码**:
24
25
 
25
26
  1. 在 template-library 仓库定义新模板(`template.json` + 组件代码)。
26
27
  2. template-library CI 校验 schema + 契约。
27
28
  3. 合并后 template-library 的发布流水线将新 registry 推到 ab-api。
28
- 4. ab-skill 端无需更新——下一次 `--list-templates` 即可看到新模板。
29
-
30
- 本地调试时可通过 `VIDEO_TEMPLATE_REGISTRY_PREFER_LOCAL=1` 让 template-library 的源码改动立即生效,不依赖发布周期。
29
+ 4. remixmate 端无需更新——下一次 `--list-templates` 即可看到新模板。
31
30
 
32
31
  ## 契约一致性
33
32
 
@@ -45,7 +44,7 @@ python3 <SkillDir>/scripts/check_contracts.py
45
44
 
46
45
  ## 共享 Python 模块
47
46
 
48
- `scripts/` 是 ab-skill 内**跨 skill 共享 Python 代码**的约定位置。当前住户:
47
+ `scripts/` 是 remixmate 内**跨 skill 共享 Python 代码**的约定位置。当前住户:
49
48
 
50
49
  | 模块 | 谁在用 | 作用 |
51
50
  |---|---|---|
@@ -23,9 +23,9 @@ Stores every video-template definition, loads a template by **template-id**, and
23
23
 
24
24
  ## Template registry
25
25
 
26
- Template metadata lives in the standalone **template-library** monorepo and ships via the `@ab-templates/metadata` package.
26
+ Template metadata is authored in the standalone **template-library** monorepo, published to ab-api, and served to the CLI over HTTP. At runtime template-registry loads the registry from a single source — the ab-api endpoint (see the env table below) — which returns every template definition (full slotMapping, compositions, etc.) including the caller's private/tenant templates.
27
27
 
28
- template-registry reads `template-library/packages/metadata/registry.json` to load every template definition (with the full slotMapping, compositions, etc.).
28
+ > The `template-library/packages/metadata/registry.json` file below is the authoring layout in the monorepo. It is **not** read at runtime anymore; only monorepo-only maintenance tooling (`check_contracts.py`, `sync_registry.py`) touches it directly.
29
29
 
30
30
  ```
31
31
  template-library/packages/
@@ -62,22 +62,21 @@ The skill itself only reads the registry; whether a token is needed depends on t
62
62
 
63
63
  | Env var | Description | Default |
64
64
  |---------|-------------|---------|
65
- | `VIDEO_TEMPLATE_REGISTRY_URL` | ab-api endpoint returning the registry (production source of truth). | unset |
66
- | `PRIV_TOKEN` | Sent as `X-Priv-Token` when hitting `VIDEO_TEMPLATE_REGISTRY_URL`. | unset |
65
+ | `VIDEO_TEMPLATE_REGISTRY_URL` | ab-api endpoint returning the registry (the single source of truth). | derived from `MM_API_BASE_URL` |
66
+ | `MM_API_BASE_URL` | ab-api base URL the CLI talks to. When `VIDEO_TEMPLATE_REGISTRY_URL` is unset, the registry endpoint is derived as `<base>/remotionTemplate/registry`. | `http://localhost:2999/api` |
67
+ | `PRIV_TOKEN` | Sent as `X-Priv-Token` when hitting the registry endpoint. Falls back to `~/.config/remixmate/credentials.json`. | unset |
67
68
  | `VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD` | `POST` (default) or `GET`. POST shape matches ab-api `{code,msg,data}`. | `POST` |
68
- | `VIDEO_TEMPLATE_REGISTRY` | Explicit local file path — bypasses HTTP/fallback entirely (CI / pinned debug). | unset |
69
- | `VIDEO_TEMPLATE_REGISTRY_PREFER_LOCAL` | When `1`, monorepo `template-library/packages/metadata/registry.json` wins over HTTP. Defaults to HTTP-first. | unset |
70
69
 
71
- Resolution order (see `scripts/registry_loader.py` for the canonical implementation):
72
- `VIDEO_TEMPLATE_REGISTRY` (optional PREFER_LOCAL fast-path to monorepo registry) `VIDEO_TEMPLATE_REGISTRY_URL` monorepo `template-library/packages/metadata/registry.json` fallback.
70
+ Resolution (single source — see `scripts/registry_loader.py` for the canonical implementation):
71
+ the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDEO_TEMPLATE_REGISTRY_URL`, otherwise `<MM_API_BASE_URL>/remotionTemplate/registry` (default `http://localhost:2999/api/...`). There is no local-file / monorepo / PREFER_LOCAL fallback: those multi-source paths were removed to avoid registry skew (private/multi-tenant templates only exist on ab-api). On a transient HTTP failure the loader degrades to the on-disk cache of the same URL; with no cache it fails loud. A standalone install (Codex / `npm i -g`) just needs `MM_API_BASE_URL` (or `VIDEO_TEMPLATE_REGISTRY_URL`) pointed at your ab-api plus a valid `PRIV_TOKEN`.
73
72
 
74
73
  ## Steps
75
74
 
76
- > This skill is a Python skill of the ab-skill CLI (`entry.type: python` → `scripts/list_templates.py`). The agent tool name `template_registry` is the only entry; locally reproduce via `ab-skill template-registry --list-templates`. The list command delegates to `scripts/registry_loader.py` — the same loader (with caching + stable/beta gating) that `render-video` and `gen-script` import in-process, so there is a single registry-reading implementation.
75
+ > This skill is a Python skill of the remixmate CLI (`entry.type: python` → `scripts/list_templates.py`). The agent tool name `template_registry` is the only entry; locally reproduce via `remixmate template-registry --list-templates`. The list command delegates to `scripts/registry_loader.py` — the same loader (with caching + stable/beta gating) that `render-video` and `gen-script` import in-process, so there is a single registry-reading implementation.
77
76
  >
78
77
  > The Python binding logic that maps DSL → TemplateBinding lives in `scripts/match_template.py` but is **not exposed as a CLI** — it is only consumed as a Python library by `render-video`'s `render_video.py` via `import match_template`.
79
78
 
80
- 1. **List available templates**: run `ab-skill template-registry --list-templates` to view the templates in the registry along with their supported aspect ratios / style tags, and decide which `templateId` to pick.
79
+ 1. **List available templates**: run `remixmate template-registry --list-templates` to view the templates in the registry along with their supported aspect ratios / style tags, and decide which `templateId` to pick.
81
80
  2. **Write the DSL**: when generating the Video DSL, put the chosen `templateId` into `meta.templateId` (the canonical location). Use `meta.templateVariant` / `renderHints.templateVariant` to explicitly select a variant. The legacy `renderHints.templatePreference[0]` is still tolerated by `match_template.py` and `dsl_validator._pick_template_id` during transition, but new authors should write `meta.templateId`.
82
81
  3. **Produce the TemplateBinding**: there is no standalone CLI for DSL → TemplateBinding; `prepare_video_assets` calls `match_template.build_binding(template, dsl)` inline during the asset-resolution pipeline and embeds the binding into the RenderPlan it hands to the renderer — no separate `*.binding.json` file is written.
83
82
 
@@ -86,7 +85,7 @@ Resolution order (see `scripts/registry_loader.py` for the canonical implementat
86
85
  ### List available templates
87
86
 
88
87
  ```bash
89
- ab-skill template-registry --list-templates
88
+ remixmate template-registry --list-templates
90
89
  ```
91
90
 
92
91
  ### Sync the registry cache (optional, used for offline / LLM prompt)
@@ -233,4 +232,4 @@ With `picture-book-en`, **narration reads English only, never Chinese**:
233
232
 
234
233
  - **No template specified**: the DSL must set `meta.templateId` (or, for legacy DSLs only, `renderHints.templatePreference[0]`); otherwise `prepare_video_assets` refuses to build a binding and prints the list of available templates.
235
234
  - **Malformed DSL**: validate first with `gen-script --validate` (delegates to `video_dsl.runtime.dsl_validator.validate_structural`).
236
- - **Empty template registry**: confirm `VIDEO_TEMPLATE_REGISTRY_URL` is reachable, or that the monorepo `template-library/packages/metadata/registry.json` fallback exists.
235
+ - **Empty template registry**: confirm the ab-api registry endpoint (`VIDEO_TEMPLATE_REGISTRY_URL`, or the one derived from `MM_API_BASE_URL`) is reachable and that `PRIV_TOKEN` is valid.
@@ -15,12 +15,18 @@ in-process by ``render-video``; it is intentionally NOT exposed as a CLI verb.
15
15
 
16
16
  Flags:
17
17
  --list-templates (accepted; listing is the only verb)
18
+ --list-examples list *.dsl.json / *.binding.json reference
19
+ examples grouped by templateId (consumed
20
+ by downstream agents that want to read a
21
+ template's reference shape before producing
22
+ new DSL)
18
23
  --filter-tag <substr> keep templates whose styleTags match
19
24
  --filter-aspect <ratio> keep templates supporting this aspect ratio
20
25
  --filter-language <zh|en|...> keep templates tagged with this content lang
21
26
  (templates with no contentLanguage are kept)
22
27
  --include-beta also show beta templates (else stable-only)
23
- --json-output emit {"templates": [...]} instead of a table
28
+ --json-output emit {"templates": [...]} or {"examples": [...]}
29
+ instead of a table
24
30
  """
25
31
 
26
32
  from __future__ import annotations
@@ -29,6 +35,7 @@ import argparse
29
35
  import json
30
36
  import os
31
37
  import sys
38
+ from pathlib import Path
32
39
 
33
40
  # When run as `python3 <skillDir>/scripts/list_templates.py`, this script's own
34
41
  # directory is sys.path[0], so the sibling shared modules import directly.
@@ -61,17 +68,69 @@ def _status_of(tpl: dict) -> str:
61
68
  return "stable"
62
69
 
63
70
 
71
+ # ----- Examples discovery (--list-examples) ---------------------------------
72
+ #
73
+ # Examples live under <SkillDir>/video_dsl/schema/examples/ as `*.dsl.json` and
74
+ # `*.binding.json` files. Naming convention (per gen-script SKILL.md) is
75
+ # `<templateId>.dsl.json` / `<templateId>.binding.json`, occasionally with
76
+ # variant suffixes (`<templateId>.<variant>.dsl.json`).
77
+ #
78
+ # We do not parse the JSON content here (downstream agents read the raw files
79
+ # they care about); we just enumerate paths so the agent doesn't have to guess
80
+ # the directory layout. When the directory does not exist (e.g. inside a slim
81
+ # package distribution), we return an empty list — non-fatal.
82
+
83
+ # Resolve the examples directory relative to this script.
84
+ # <skill>/scripts/list_templates.py → <skill>/video_dsl/schema/examples/
85
+ _EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "video_dsl" / "schema" / "examples"
86
+
87
+
88
+ def _collect_examples() -> list[dict]:
89
+ if not _EXAMPLES_DIR.is_dir():
90
+ return []
91
+ by_template: dict[str, dict] = {}
92
+ for entry in sorted(_EXAMPLES_DIR.iterdir()):
93
+ if not entry.is_file():
94
+ continue
95
+ name = entry.name
96
+ # Recognise *.dsl.json / *.binding.json
97
+ if name.endswith(".dsl.json"):
98
+ kind = "dsl"
99
+ base = name[: -len(".dsl.json")]
100
+ elif name.endswith(".binding.json"):
101
+ kind = "binding"
102
+ base = name[: -len(".binding.json")]
103
+ else:
104
+ continue
105
+ # Variant: 'foo.bar.dsl.json' → templateId='foo', variant='bar'
106
+ if "." in base:
107
+ template_id, variant = base.split(".", 1)
108
+ else:
109
+ template_id, variant = base, None
110
+ bucket = by_template.setdefault(template_id, {"templateId": template_id, "files": []})
111
+ bucket["files"].append({
112
+ "kind": kind,
113
+ "variant": variant,
114
+ "path": str(entry.relative_to(_EXAMPLES_DIR.parent.parent.parent)), # relative to skill dir
115
+ "absolutePath": str(entry),
116
+ "sizeBytes": entry.stat().st_size,
117
+ })
118
+ return list(by_template.values())
119
+
120
+
64
121
  def main() -> None:
65
122
  ap = argparse.ArgumentParser(
66
123
  description="List available video templates from the registry.",
67
124
  )
68
125
  ap.add_argument("--list-templates", action="store_true",
69
126
  help="List every available template (the only CLI verb).")
127
+ ap.add_argument("--list-examples", action="store_true",
128
+ help="List reference examples (*.dsl.json / *.binding.json) grouped by templateId.")
70
129
  ap.add_argument("--filter-tag", help="Keep templates whose styleTags match this substring.")
71
130
  ap.add_argument("--filter-aspect", help="Keep templates supporting this aspect ratio (e.g. 9:16).")
72
131
  ap.add_argument("--filter-language", help="Keep templates tagged with this content language (zh|en|...).")
73
132
  ap.add_argument("--include-beta", action="store_true", help="Also show beta templates (default: stable only).")
74
- ap.add_argument("--json-output", action="store_true", help='Emit {"templates": [...]} JSON instead of a table.')
133
+ ap.add_argument("--json-output", action="store_true", help='Emit JSON ({"templates":[...]} or {"examples":[...]}) instead of a table.')
75
134
  args = ap.parse_args()
76
135
 
77
136
  # --include-beta flips registry_loader's documented status gate via env, so
@@ -79,6 +138,32 @@ def main() -> None:
79
138
  if args.include_beta:
80
139
  os.environ["ENABLE_BETA_TEMPLATES"] = "1"
81
140
 
141
+ # ----- examples mode -----------------------------------------------------
142
+ if args.list_examples:
143
+ examples = _collect_examples()
144
+ if args.json_output:
145
+ print(json.dumps({"examples": examples}, ensure_ascii=False))
146
+ return
147
+ if not examples:
148
+ print(
149
+ f"⚠️ No examples found under {_EXAMPLES_DIR}.\n"
150
+ " This package may have shipped without reference examples; "
151
+ "check the cli source repo or upgrade @remixmate/cli.",
152
+ )
153
+ return
154
+ print(f"\n{'Template ID':<28} Files")
155
+ print(f"{'-' * 28} {'-' * 60}")
156
+ for bucket in examples:
157
+ tid = str(bucket["templateId"])[:28].ljust(28)
158
+ files = bucket["files"]
159
+ for i, f in enumerate(files):
160
+ marker = tid if i == 0 else " " * 28
161
+ kind = f["kind"]
162
+ variant = f"@{f['variant']}" if f.get("variant") else ""
163
+ print(f"{marker} [{kind}{variant}] {f['path']} ({f['sizeBytes']}B)")
164
+ print(f"\n{sum(len(b['files']) for b in examples)} example file(s) across {len(examples)} template(s)")
165
+ return
166
+
82
167
  try:
83
168
  templates = load_visible_templates()
84
169
  except RuntimeError as exc: