@remixmate/cli 0.9.13 → 0.9.14

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 (53) hide show
  1. package/README.md +54 -1
  2. package/README.zh-CN.md +37 -1
  3. package/dist/auth/auto-login.js +10 -1
  4. package/dist/auth/commands.js +181 -21
  5. package/dist/auth/credential-store.d.ts +16 -0
  6. package/dist/auth/credential-store.js +17 -0
  7. package/dist/auth/device-flow-runner.d.ts +15 -0
  8. package/dist/auth/device-flow-runner.js +60 -12
  9. package/dist/auth/device-flow.js +24 -7
  10. package/dist/auth/ensure.d.ts +55 -0
  11. package/dist/auth/ensure.js +62 -0
  12. package/dist/auth/environment.d.ts +13 -0
  13. package/dist/auth/environment.js +21 -0
  14. package/dist/auth/pending-store.d.ts +33 -0
  15. package/dist/auth/pending-store.js +45 -0
  16. package/dist/cli.js +75 -8
  17. package/dist/doctor.d.ts +22 -0
  18. package/dist/doctor.js +142 -0
  19. package/dist/errors.d.ts +29 -0
  20. package/dist/errors.js +33 -0
  21. package/dist/exec.d.ts +18 -0
  22. package/dist/exec.js +47 -0
  23. package/dist/handlers/gen-digital-human.js +1 -0
  24. package/dist/handlers/gen-image.js +1 -0
  25. package/dist/handlers/gen-video.js +1 -0
  26. package/dist/handlers/gen-voice.js +2 -0
  27. package/dist/handlers/index.d.ts +7 -0
  28. package/dist/http.d.ts +17 -6
  29. package/dist/http.js +39 -15
  30. package/dist/manifest.json +2 -2
  31. package/dist/registry.d.ts +4 -2
  32. package/dist/registry.js +2 -1
  33. package/dist/runner.d.ts +4 -0
  34. package/dist/runner.js +27 -12
  35. package/dist/skill-schema.d.ts +19 -0
  36. package/dist/skill-schema.js +18 -0
  37. package/dist/text.d.ts +8 -0
  38. package/dist/text.js +15 -0
  39. package/package.json +2 -2
  40. package/skills/export-jianying/skill.json +1 -0
  41. package/skills/gen-digital-human/skill.json +1 -0
  42. package/skills/gen-image/skill.json +1 -0
  43. package/skills/gen-script/skill.json +1 -0
  44. package/skills/gen-video/skill.json +1 -0
  45. package/skills/gen-voice/skill.json +1 -0
  46. package/skills/prepare-video-assets/skill.json +1 -0
  47. package/skills/render-video/skill.json +1 -0
  48. package/skills/template-registry/scripts/list_templates.py +16 -1
  49. package/skills/template-registry/scripts/registry_loader.py +63 -63
  50. package/skills/template-registry/skill.json +1 -0
  51. package/skills/video-parser/skill.json +1 -0
  52. package/skills/web-record/skill.json +1 -0
  53. package/skills/web-screenshot/skill.json +1 -0
@@ -19,7 +19,8 @@
19
19
  兼容 ab-agent 注入的 MM_BACKEND_API_URL。默认值内置在 CLI 自身,独立安装
20
20
  (codex / npm i -g)开箱即用,无需宿主在 spawn 时注入。
21
21
 
22
- 鉴权:PRIV_TOKEN(env)或 ~/.config/remixmate/credentials.json(同源条目)。
22
+ 鉴权:只读 PRIV_TOKEN 环境变量。凭证库 / 钥匙串 / 设备授权流由 CLI 的 Node 侧
23
+ 唯一鉴权入口负责(src/auth/ensure.ts),并在 spawn 时注入本进程环境。
23
24
 
24
25
  monorepo / 显式本地文件等其它来源已**移除**。需要离线 / pinned 数据的维护脚本
25
26
  (check_contracts.py / sync_registry.py)直接走 template_paths 读 monorepo,
@@ -47,7 +48,6 @@ import urllib.error
47
48
  import urllib.request
48
49
  from functools import lru_cache
49
50
  from typing import Optional
50
- from urllib.parse import urlparse
51
51
 
52
52
  __all__ = [
53
53
  "load_registry_data",
@@ -55,8 +55,29 @@ __all__ = [
55
55
  "get_template",
56
56
  "list_templates",
57
57
  "invalidate_cache",
58
+ "RegistryAuthError",
59
+ "RegistryUnreachableError",
60
+ "EXIT_NOT_AUTHENTICATED",
61
+ "EXIT_BACKEND_UNREACHABLE",
58
62
  ]
59
63
 
64
+ # 与 Node 侧 src/errors.ts 的 EXIT 常量保持一致:宿主据此判断「该引导用户授权」
65
+ # 还是「后端故障,等一会儿再试」,而不是把两者都当成一次普通失败。
66
+ EXIT_NOT_AUTHENTICATED = 4
67
+ EXIT_BACKEND_UNREACHABLE = 5
68
+
69
+
70
+ class RegistryAuthError(RuntimeError):
71
+ """凭证缺失 / 失效 / 被吊销 —— 与「后端不可达」区分开的独立失败模式。
72
+
73
+ 这两种情况以前被压成同一段「请确认 ab-api 可达…并已设置有效 PRIV_TOKEN」的
74
+ 文案,用户无从判断该去登录还是该等后端恢复。
75
+ """
76
+
77
+
78
+ class RegistryUnreachableError(RuntimeError):
79
+ """网络不通 / 超时 / 5xx —— 用户无需重新登录,重试或修配置即可。"""
80
+
60
81
  # 默认缓存 TTL(秒)。HTTP 拉取后该时间内不再重新请求。
61
82
  _DEFAULT_TTL_SECONDS = 300
62
83
 
@@ -127,58 +148,20 @@ def _write_cached(cache_file: str, payload: dict) -> None:
127
148
  raise
128
149
 
129
150
 
130
- # ── PrivToken 解析(env → 凭证库回退)──────────────────────────────────────
151
+ # ── PrivToken 解析 ────────────────────────────────────────────────────────
131
152
  #
132
- # CLI resolve.ts 共享同一优先级与同一凭证文件路径(Req 7.5)。Python 侧只读
133
- # 文件存储(不读 keychain —— keychain 由 CLI 写/读,Python 用纯 stdlib 不引依赖)。
134
-
135
- def _credentials_file_path() -> str:
136
- """凭证文件路径,与 CLI credential-store.ts 的 CRED_FILE 保持一致。"""
137
- return os.path.join(os.path.expanduser("~"), ".config", "remixmate", "credentials.json")
138
-
139
-
140
- def _origin(url: str) -> tuple:
141
- p = urlparse(url)
142
- return (p.scheme, p.hostname, p.port)
143
-
153
+ # 只读 PRIV_TOKEN 环境变量 —— 凭证库 / 系统钥匙串 / 设备授权流全部由 Node 侧的
154
+ # 唯一鉴权入口(src/auth/ensure.ts)负责,并在 spawn 时把解析结果注入本进程的
155
+ # 环境变量(src/runner.ts)。
156
+ #
157
+ # 这里曾经自己读 ~/.config/remixmate/credentials.json,带来两个问题:一是与 Node
158
+ # 侧的解析逻辑各写一份、只有本 skill 享受得到;二是「只有一条凭证就直接用」的兜底
159
+ # 会把某个后端(如本地开发环境)的 token 发往另一个后端(如生产),属于跨源泄露。
144
160
 
145
- def _priv_token_from_credential_store(url: str) -> str:
146
- """从 ~/.config/remixmate/credentials.json 取与 url 同源的 PrivToken。
147
161
 
148
- 匹配顺序:① registry URL 同源(scheme/host/port)的条目;② 仅有单条目时直接用。
149
- secret=='keychain' 的条目跳过(Python 无法读取 keychain)。任何异常静默返回空串。
150
- """
151
- try:
152
- with open(_credentials_file_path(), encoding="utf-8") as f:
153
- doc = json.load(f)
154
- except (OSError, json.JSONDecodeError):
155
- return ""
156
- creds = doc.get("credentials") if isinstance(doc, dict) else None
157
- if not isinstance(creds, dict) or not creds:
158
- return ""
159
-
160
- target = _origin(url)
161
- for base_url, entry in creds.items():
162
- if not isinstance(entry, dict) or entry.get("secret") == "keychain":
163
- continue
164
- if _origin(base_url) == target:
165
- tok = str(entry.get("privToken") or "").strip()
166
- if tok:
167
- return tok
168
- # 单条目兜底:CLI 通常只登录一个后端。
169
- if len(creds) == 1:
170
- entry = next(iter(creds.values()))
171
- if isinstance(entry, dict) and entry.get("secret") != "keychain":
172
- return str(entry.get("privToken") or "").strip()
173
- return ""
174
-
175
-
176
- def _resolve_priv_token(url: str) -> str:
177
- """PrivToken 解析:环境变量优先,空则回退凭证库(与 CLI 一致的优先级)。"""
178
- env = os.environ.get("PRIV_TOKEN", "").strip()
179
- if env:
180
- return env
181
- return _priv_token_from_credential_store(url)
162
+ def _resolve_priv_token(_url: str) -> str:
163
+ """PrivToken 解析:只认由 CLI 注入 / 宿主设置的 PRIV_TOKEN 环境变量。"""
164
+ return os.environ.get("PRIV_TOKEN", "").strip()
182
165
 
183
166
 
184
167
  def _fetch_http(url: str, timeout: int) -> dict:
@@ -214,9 +197,13 @@ def _fetch_http(url: str, timeout: int) -> dict:
214
197
 
215
198
  # ab-api 风格:{code, msg, data} → 取 data;其他场景视作直接 registry
216
199
  if isinstance(parsed, dict) and "data" in parsed and "code" in parsed:
217
- if parsed.get("code") not in (0, "0"):
200
+ code = parsed.get("code")
201
+ if str(code) in ("401", "403"):
202
+ # ab-api 用 HTTP 200 + code=401 表达鉴权失败,所以这里也要判。
203
+ raise RegistryAuthError(str(parsed.get("msg") or f"code={code}"))
204
+ if code not in (0, "0"):
218
205
  raise RuntimeError(
219
- f"registry HTTP 业务失败: code={parsed.get('code')} msg={parsed.get('msg')}"
206
+ f"registry HTTP 业务失败: code={code} msg={parsed.get('msg')}"
220
207
  )
221
208
  data = parsed["data"]
222
209
  else:
@@ -233,6 +220,8 @@ def _fetch_http(url: str, timeout: int) -> dict:
233
220
  cached["fetchedAt"] = time.time()
234
221
  _write_cached(cache_file, cached)
235
222
  return cached["data"]
223
+ if e.code in (401, 403):
224
+ raise RegistryAuthError(f"HTTP {e.code}") from e
236
225
  raise
237
226
 
238
227
 
@@ -249,6 +238,13 @@ def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
249
238
 
250
239
  try:
251
240
  return _fetch_http(url, timeout)
241
+ except RegistryAuthError as exc:
242
+ # 鉴权失败不降级到缓存:缓存可能属于另一个账号,静默返回它会把「凭证已失效」
243
+ # 掩盖成一份看似正常、实则不属于当前用户的模板列表。
244
+ raise RegistryAuthError(
245
+ f"凭证无效或已过期({exc})。运行 remixmate login 重新授权,"
246
+ f"或设置有效的 PRIV_TOKEN。当前后端:{url}"
247
+ ) from exc
252
248
  except Exception as exc:
253
249
  # HTTP 故障时降级:有磁盘缓存就继续用(同一数据源的容错,不是第二个源)
254
250
  if cached:
@@ -257,10 +253,9 @@ def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
257
253
  file=sys.stderr,
258
254
  )
259
255
  return cached["data"]
260
- raise RuntimeError(
261
- f"registry HTTP 拉取失败且无可用缓存: {url} ({exc})。"
262
- f" 请确认 ab-api 可达、VIDEO_TEMPLATE_REGISTRY_URL / MM_API_BASE_URL"
263
- f" 配置正确,并已设置有效 PRIV_TOKEN。"
256
+ raise RegistryUnreachableError(
257
+ f"registry 后端不可达且无可用缓存: {url} ({exc})。"
258
+ f" 请确认 ab-api 可达、VIDEO_TEMPLATE_REGISTRY_URL / MM_API_BASE_URL 配置正确。"
264
259
  ) from exc
265
260
 
266
261
 
@@ -275,16 +270,21 @@ def load_registry_data(
275
270
  未设置则按 ``MM_API_BASE_URL`` 推导(见 ``_default_registry_url``)。
276
271
  带磁盘缓存(TTL + ETag/304),返回 ``data.get("templates", [])`` 形状的 dict。
277
272
  """
273
+ # 这两行是实现细节(endpoint 从哪来),对终端用户没有意义,却出现在每一次
274
+ # 成功调用的 stderr 上。默认收起,排障时用 REMIXMATE_DEBUG=1 打开。
275
+ debug = os.environ.get("REMIXMATE_DEBUG", "").strip() == "1"
278
276
  url = os.environ.get("VIDEO_TEMPLATE_REGISTRY_URL", "").strip()
279
277
  if url:
280
- print(f"[registry_loader] using HTTP registry: {url}", file=sys.stderr)
278
+ if debug:
279
+ print(f"[registry_loader] using HTTP registry: {url}", file=sys.stderr)
281
280
  else:
282
281
  url = _default_registry_url()
283
- print(
284
- f"[registry_loader] VIDEO_TEMPLATE_REGISTRY_URL unset; using derived"
285
- f" default URL: {url}",
286
- file=sys.stderr,
287
- )
282
+ if debug:
283
+ print(
284
+ f"[registry_loader] VIDEO_TEMPLATE_REGISTRY_URL unset; using derived"
285
+ f" default URL: {url}",
286
+ file=sys.stderr,
287
+ )
288
288
  return _load_from_url(url, ttl=ttl_seconds, timeout=http_timeout)
289
289
 
290
290
 
@@ -5,6 +5,7 @@
5
5
  "category": "authoring",
6
6
  "title": "Video Template Registry",
7
7
  "description": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
8
+ "auth": "required",
8
9
  "envVars": ["PRIV_TOKEN", "VIDEO_TEMPLATE_REGISTRY_URL", "MM_API_BASE_URL", "VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD"],
9
10
  "entry": { "type": "python", "scriptPath": "scripts/list_templates.py" },
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "consuming",
6
6
  "title": "Video Deconstruction",
7
7
  "description": "Video deconstruction: split a video into reusable assets (audio extraction, ASR transcript, keyframe extraction, scene segmentation).",
8
+ "auth": "required",
8
9
  "envVars": ["RENDER_API_URL", "PRIV_TOKEN", "CONVERSATION_ID"],
9
10
  "scriptPath": "scripts/parse_via_render.py",
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "consuming",
6
6
  "title": "Web Page Recording",
7
7
  "description": "Drive a headless browser (Playwright Python) to RECORD any URL to a video, then (by default) transcode to mp4, grab a cover frame, upload to VOD and return a playable CDN URL. Modes: fixed-duration recording, condition-triggered stop (element appears / disappears), auto-scroll from top to bottom, custom storyboards, and parameterized templates. Storyboard scenes: highlight / focus / zoom / scroll / virtual-cursor click / type / hover / caption / title-card / arrow / numbered sequence / redact / code-line highlight. For still images (png/jpg) use the web_screenshot tool instead. Use this tool whenever the user wants a video / recording / screencast of a web page: record a page, scroll-through video, page-from-top-to-bottom clip, demo of clicks/typing/hover, storyboard / multi-scene intro video, or a template-based clip.",
8
+ "auth": "optional",
8
9
  "envVars": ["WEB_CAPTURE_BROWSER", "WEB_CAPTURE_OUTPUT_BASE", "WEB_CAPTURE_NO_SANDBOX", "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS", "PLAYWRIGHT_BROWSERS_PATH", "REMOTION_RENDER_API_URL", "PRIV_TOKEN"],
9
10
  "entry": { "type": "python", "scriptPath": "../web-screenshot/scripts/record.py" },
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "consuming",
6
6
  "title": "Web Page Screenshot",
7
7
  "description": "Drive a headless browser (Playwright Python) to capture any URL to a local STILL IMAGE (png/jpg): full-page / viewport / element / region screenshots, with device emulation, waiting, hide/mask/redact, and static annotations (highlight / arrow / caption / numbered sequence / redact). Images only — for video / recording / scroll-through / storyboard clips (webm) use the web_record tool instead.",
8
+ "auth": "none",
8
9
  "envVars": ["WEB_CAPTURE_BROWSER", "WEB_CAPTURE_OUTPUT_BASE", "WEB_CAPTURE_NO_SANDBOX", "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS", "PLAYWRIGHT_BROWSERS_PATH"],
9
10
  "entry": { "type": "python", "scriptPath": "scripts/screenshot.py" },
10
11
  "parameters": {