@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
@@ -91,6 +91,7 @@ export async function genDigitalHuman(input, ctxIn) {
91
91
  const ctx = await resolveHttpContext(ctxIn.skillName, {
92
92
  apiBaseUrl: input.api_base_url,
93
93
  privateToken: input.priv_token,
94
+ preflightToken: ctxIn.auth?.token,
94
95
  });
95
96
  // ── Mode 1: list avatars ────────────────────────────────────────────────
96
97
  if (isTrue(input.list_avatars)) {
@@ -90,6 +90,7 @@ export async function genImage(input, ctxIn) {
90
90
  const ctx = await resolveHttpContext(ctxIn.skillName, {
91
91
  apiBaseUrl,
92
92
  privateToken: input.priv_token,
93
+ preflightToken: ctxIn.auth?.token,
93
94
  });
94
95
  emitProgress({ phase: 'gen-image:request', model, size, n });
95
96
  const data = await mmPost(ctx, '/model/genImg', payload, { timeoutMs: 120_000 });
@@ -85,6 +85,7 @@ export async function genVideo(input, ctxIn) {
85
85
  const ctx = await resolveHttpContext(ctxIn.skillName, {
86
86
  apiBaseUrl,
87
87
  privateToken: input.priv_token,
88
+ preflightToken: ctxIn.auth?.token,
88
89
  });
89
90
  const payload = {
90
91
  model,
@@ -56,6 +56,7 @@ export async function genVoice(input, ctxIn) {
56
56
  const ctx = await resolveHttpContext(ctxIn.skillName, {
57
57
  apiBaseUrl: input.api_base_url,
58
58
  privateToken: input.priv_token,
59
+ preflightToken: ctxIn.auth?.token,
59
60
  });
60
61
  return listVoicesRemote(ctx);
61
62
  }
@@ -74,6 +75,7 @@ export async function genVoice(input, ctxIn) {
74
75
  const ctx = await resolveHttpContext(ctxIn.skillName, {
75
76
  apiBaseUrl,
76
77
  privateToken: input.priv_token,
78
+ preflightToken: ctxIn.auth?.token,
77
79
  });
78
80
  emitProgress({ phase: 'gen-voice:request', voiceId, speed });
79
81
  const data = await mmPost(ctx, '/tool/tts', { provider: voice.provider, text, voiceSetting: { voiceId, speed }, outputFormat: 'url' }, { timeoutMs: 120_000 });
@@ -4,10 +4,17 @@
4
4
  * Only `entry.type` in ('http' | 'builtin') uses this registry; python
5
5
  * skills go through spawn() in runner.ts and bypass handlers entirely.
6
6
  */
7
+ import type { AuthPreflight } from '../auth/ensure.js';
7
8
  export type HandlerInput = Record<string, unknown>;
8
9
  export interface HandlerContext {
9
10
  /** ISO-style skill name from the registry (e.g. `gen-image`). Used for x-invoke-skill. */
10
11
  skillName: string;
12
+ /**
13
+ * Credential resolved by the dispatcher's preflight (runner.ts). Handlers pass
14
+ * `auth.token` into resolveHttpContext so the device flow can't run twice for
15
+ * a single invocation.
16
+ */
17
+ auth?: AuthPreflight;
11
18
  }
12
19
  export type SkillHandler = (input: HandlerInput, ctx: HandlerContext) => Promise<void>;
13
20
  export declare const HANDLERS: Record<string, SkillHandler>;
package/dist/http.d.ts CHANGED
@@ -15,13 +15,18 @@
15
15
  * Base URL resolution:
16
16
  * 1. opts.apiBaseUrl — caller override
17
17
  * 2. process.env.MM_API_BASE_URL — process env (point at local/staging here)
18
- * 3. https://api.remixmate.com/api production default (zero-config)
18
+ * 3. process.env.MM_BACKEND_API_URL ab-agent's convention (back-compat)
19
+ * 4. https://api.remixmate.com/api — production default (zero-config)
20
+ */
21
+ export { SkillError, EXIT } from './errors.js';
22
+ /**
23
+ * Resolve the ab-api base URL the same way for authenticated and device-flow calls.
24
+ *
25
+ * `MM_BACKEND_API_URL` is honored because several Python skills already fall back
26
+ * to it (registry_loader, render_job_client, _vod_polling). Since runner.ts now
27
+ * injects the resolved base URL into those child processes, leaving it out here
28
+ * would let a prod default silently override an ab-agent-injected backend.
19
29
  */
20
- export declare class SkillError extends Error {
21
- readonly exitCode: number;
22
- constructor(message: string, exitCode?: number);
23
- }
24
- /** Resolve the ab-api base URL the same way for authenticated and device-flow calls. */
25
30
  export declare function resolveApiBaseUrl(flag?: string): string;
26
31
  export interface HttpContext {
27
32
  apiBaseUrl: string;
@@ -33,6 +38,12 @@ export interface HttpContext {
33
38
  export declare function resolveHttpContext(skillName: string, opts?: {
34
39
  apiBaseUrl?: string;
35
40
  privateToken?: string;
41
+ /**
42
+ * Token already resolved by the dispatcher's auth preflight (runner.ts).
43
+ * Reusing it avoids a second credential-store read — and, more importantly,
44
+ * a second device-flow attempt — per skill invocation.
45
+ */
46
+ preflightToken?: string;
36
47
  }): Promise<HttpContext>;
37
48
  export interface MmResponse<T = unknown> {
38
49
  code: number;
package/dist/http.js CHANGED
@@ -15,26 +15,36 @@
15
15
  * Base URL resolution:
16
16
  * 1. opts.apiBaseUrl — caller override
17
17
  * 2. process.env.MM_API_BASE_URL — process env (point at local/staging here)
18
- * 3. https://api.remixmate.com/api production default (zero-config)
18
+ * 3. process.env.MM_BACKEND_API_URL ab-agent's convention (back-compat)
19
+ * 4. https://api.remixmate.com/api — production default (zero-config)
19
20
  */
20
21
  import { resolvePrivToken, NOT_AUTHENTICATED_HINT } from './auth/resolve.js';
21
22
  import { attemptAutoLogin } from './auth/auto-login.js';
23
+ import { EXIT, SkillError } from './errors.js';
22
24
  const DEFAULT_API_BASE_URL = 'https://api.remixmate.com/api';
23
- export class SkillError extends Error {
24
- exitCode;
25
- constructor(message, exitCode = 1) {
26
- super(message);
27
- this.exitCode = exitCode;
28
- this.name = 'SkillError';
29
- }
30
- }
31
- /** Resolve the ab-api base URL the same way for authenticated and device-flow calls. */
25
+ // Re-exported so the many `import { SkillError } from '../http.js'` call sites
26
+ // stay valid; the definition moved to errors.ts to break an import cycle with
27
+ // the auth gateway (see errors.ts).
28
+ export { SkillError, EXIT } from './errors.js';
29
+ /**
30
+ * Resolve the ab-api base URL the same way for authenticated and device-flow calls.
31
+ *
32
+ * `MM_BACKEND_API_URL` is honored because several Python skills already fall back
33
+ * to it (registry_loader, render_job_client, _vod_polling). Since runner.ts now
34
+ * injects the resolved base URL into those child processes, leaving it out here
35
+ * would let a prod default silently override an ab-agent-injected backend.
36
+ */
32
37
  export function resolveApiBaseUrl(flag) {
33
- return (flag ?? process.env.MM_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, '');
38
+ const fromEnv = (process.env.MM_API_BASE_URL ?? '').trim() || (process.env.MM_BACKEND_API_URL ?? '').trim();
39
+ return ((flag ?? '').trim() || fromEnv || DEFAULT_API_BASE_URL).replace(/\/+$/, '');
34
40
  }
35
41
  export async function resolveHttpContext(skillName, opts = {}) {
36
42
  const apiBaseUrl = resolveApiBaseUrl(opts.apiBaseUrl);
37
- let resolved = await resolvePrivToken({ flag: opts.privateToken, apiBaseUrl });
43
+ // An explicit --token flag still wins over the preflight result.
44
+ let resolved = await resolvePrivToken({
45
+ flag: opts.privateToken ?? opts.preflightToken,
46
+ apiBaseUrl,
47
+ });
38
48
  // Only when flag/env/store are all empty do we try automatic browser auth.
39
49
  // attemptAutoLogin self-gates (cloud env / headless / disabled → returns null
40
50
  // immediately), so the env-injected path never reaches it (zero regression).
@@ -42,7 +52,7 @@ export async function resolveHttpContext(skillName, opts = {}) {
42
52
  resolved = await attemptAutoLogin(apiBaseUrl);
43
53
  }
44
54
  if (!resolved) {
45
- throw new SkillError(NOT_AUTHENTICATED_HINT);
55
+ throw new SkillError(NOT_AUTHENTICATED_HINT, EXIT.NOT_AUTHENTICATED);
46
56
  }
47
57
  return {
48
58
  apiBaseUrl,
@@ -83,15 +93,24 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
83
93
  }
84
94
  catch (err) {
85
95
  if (err instanceof Error && err.name === 'AbortError') {
86
- throw new SkillError(`❌ request timed out: ${url}`);
96
+ throw new SkillError(`❌ request timed out: ${url}`, EXIT.BACKEND_UNREACHABLE);
87
97
  }
88
- throw new SkillError(`❌ network error: ${err.message}`);
98
+ throw new SkillError(`❌ network error: ${err.message}`, EXIT.BACKEND_UNREACHABLE);
89
99
  }
90
100
  finally {
91
101
  clearTimeout(timeout);
92
102
  }
93
103
  const text = await resp.text();
94
104
  if (!resp.ok) {
105
+ // 401/403 is not a generic API failure — it means the credential is missing,
106
+ // expired or revoked. Surfacing it as such (with exit code 4) is what lets a
107
+ // host offer to re-authorize instead of reporting an opaque backend error.
108
+ if (resp.status === 401 || resp.status === 403) {
109
+ throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 HTTP ${resp.status})`, EXIT.NOT_AUTHENTICATED);
110
+ }
111
+ if (resp.status >= 500) {
112
+ throw new SkillError(`❌ API request failed (HTTP ${resp.status}): ${text}`, EXIT.BACKEND_UNREACHABLE);
113
+ }
95
114
  throw new SkillError(`❌ API request failed (HTTP ${resp.status}): ${text}`);
96
115
  }
97
116
  let parsed;
@@ -102,6 +121,11 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
102
121
  throw new SkillError(`❌ failed to parse response, body is not JSON: ${text.slice(0, 200)}`);
103
122
  }
104
123
  if (parsed.code !== 0) {
124
+ // ab-api reports auth failures in the envelope (HTTP 200 + code=401), so the
125
+ // business-code path needs the same 401 → "re-authorize" mapping as above.
126
+ if (parsed.code === 401 || parsed.code === 403) {
127
+ throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 code=${parsed.code}${parsed.msg ? `: ${parsed.msg}` : ''})`, EXIT.NOT_AUTHENTICATED);
128
+ }
105
129
  throw new SkillError(`❌ API returned a business error: ${parsed.msg ?? 'unknown error'} (code=${parsed.code})`);
106
130
  }
107
131
  return parsed.data ?? undefined;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.13",
4
- "generatedAt": "2026-08-12T07:37:17.101Z",
3
+ "version": "0.9.14",
4
+ "generatedAt": "2026-08-12T07:37:21.011Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "export-jianying",
@@ -5,15 +5,17 @@
5
5
  * the build-time manifest generator and smoke test). A record that fails
6
6
  * validation is skipped with a warning rather than crashing the whole CLI.
7
7
  */
8
- import { type SkillEntry } from './skill-schema.js';
8
+ import { type SkillAuthMode, type SkillEntry } from './skill-schema.js';
9
9
  export declare const SKILLS_DIR: string;
10
- export type { SkillEntry } from './skill-schema.js';
10
+ export type { SkillEntry, SkillAuthMode } from './skill-schema.js';
11
11
  export interface SkillDef {
12
12
  name: string;
13
13
  toolName: string;
14
14
  description: string;
15
15
  parameters: Record<string, unknown>;
16
16
  entry: SkillEntry;
17
+ /** Authorization requirement enforced by the dispatcher before invocation. */
18
+ auth: SkillAuthMode;
17
19
  skillDir: string;
18
20
  /** Absolute path to the python entry script (only when entry.type === 'python'). */
19
21
  scriptAbsolutePath?: string;
package/dist/registry.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
- import { normalizeEntry, validateSkillJson, } from './skill-schema.js';
11
+ import { normalizeEntry, resolveAuthMode, validateSkillJson, } from './skill-schema.js';
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
  export const SKILLS_DIR = path.resolve(__dirname, '..', 'skills');
14
14
  function loadOne(skillDir) {
@@ -37,6 +37,7 @@ function loadOne(skillDir) {
37
37
  description: raw.description,
38
38
  parameters: raw.parameters ?? {},
39
39
  entry,
40
+ auth: resolveAuthMode(raw),
40
41
  skillDir,
41
42
  };
42
43
  if (entry.type === 'python') {
package/dist/runner.d.ts CHANGED
@@ -3,6 +3,10 @@
3
3
  * - spawn `python3 <scriptAbsolutePath> --flag value ...` (entry.type === 'python')
4
4
  * - call a TS handler from handlers/ (entry.type === 'http' | 'builtin')
5
5
  *
6
+ * Authorization happens HERE, once, before either path — see auth/ensure.ts for
7
+ * why. Python children receive the resolved credential through their environment
8
+ * and never read the credential store themselves.
9
+ *
6
10
  * Resolves to the child process's exit code so cli.ts can propagate it.
7
11
  */
8
12
  import type { ParsedArgs } from './argv.js';
package/dist/runner.js CHANGED
@@ -3,26 +3,41 @@
3
3
  * - spawn `python3 <scriptAbsolutePath> --flag value ...` (entry.type === 'python')
4
4
  * - call a TS handler from handlers/ (entry.type === 'http' | 'builtin')
5
5
  *
6
+ * Authorization happens HERE, once, before either path — see auth/ensure.ts for
7
+ * why. Python children receive the resolved credential through their environment
8
+ * and never read the credential store themselves.
9
+ *
6
10
  * Resolves to the child process's exit code so cli.ts can propagate it.
7
11
  */
8
12
  import { spawn } from 'node:child_process';
9
13
  import path from 'node:path';
10
14
  import { findSkill, SKILLS_DIR } from './registry.js';
11
15
  import { HANDLERS } from './handlers/index.js';
12
- import { SkillError } from './http.js';
16
+ import { EXIT, SkillError } from './errors.js';
17
+ import { authChildEnv, ensureAuth } from './auth/ensure.js';
18
+ /** Read the token override accepted by both the TS handlers and the Python skills. */
19
+ function tokenFlag(args) {
20
+ const value = args.token ?? args.priv_token;
21
+ return typeof value === 'string' ? value : undefined;
22
+ }
13
23
  export async function runSkill(skillName, opts) {
14
24
  const skill = findSkill(skillName, opts.baseDir ?? SKILLS_DIR);
15
25
  if (!skill) {
16
26
  process.stderr.write(`❌ skill not found: ${skillName}\n`);
17
- return 2;
27
+ return EXIT.USAGE;
18
28
  }
19
29
  try {
30
+ const auth = await ensureAuth({
31
+ mode: skill.auth,
32
+ apiBaseUrl: typeof opts.parsedArgs.api_base_url === 'string' ? opts.parsedArgs.api_base_url : undefined,
33
+ flagToken: tokenFlag(opts.parsedArgs),
34
+ });
20
35
  switch (skill.entry.type) {
21
36
  case 'python':
22
- return await runPython(skill, opts.rawArgs);
37
+ return await runPython(skill, opts.rawArgs, auth);
23
38
  case 'http':
24
39
  case 'builtin':
25
- return await runHandler(skill, opts.parsedArgs);
40
+ return await runHandler(skill, opts.parsedArgs, auth);
26
41
  }
27
42
  }
28
43
  catch (err) {
@@ -31,10 +46,10 @@ export async function runSkill(skillName, opts) {
31
46
  return err.exitCode;
32
47
  }
33
48
  process.stderr.write(`❌ unexpected error: ${err.message}\n`);
34
- return 1;
49
+ return EXIT.ERROR;
35
50
  }
36
51
  }
37
- async function runPython(skill, rawArgs) {
52
+ async function runPython(skill, rawArgs, auth) {
38
53
  if (!skill.scriptAbsolutePath) {
39
54
  throw new SkillError(`skill ${skill.name} has entry.type=python but no scriptAbsolutePath`);
40
55
  }
@@ -43,21 +58,21 @@ async function runPython(skill, rawArgs) {
43
58
  const proc = spawn('python3', [skill.scriptAbsolutePath, ...rawArgs], {
44
59
  cwd,
45
60
  stdio: 'inherit',
46
- env: { ...process.env, PYTHONUNBUFFERED: '1' },
61
+ env: { ...process.env, ...authChildEnv(auth), PYTHONUNBUFFERED: '1' },
47
62
  });
48
- proc.on('exit', (code) => resolve(code ?? 1));
63
+ proc.on('exit', (code) => resolve(code ?? EXIT.ERROR));
49
64
  proc.on('error', (err) => {
50
65
  process.stderr.write(`❌ failed to spawn python3: ${err.message}\n`);
51
- resolve(127);
66
+ resolve(EXIT.SPAWN_FAILED);
52
67
  });
53
68
  });
54
69
  }
55
- async function runHandler(skill, args) {
70
+ async function runHandler(skill, args, auth) {
56
71
  const handlerKey = skill.entry.handler;
57
72
  const handler = HANDLERS[handlerKey];
58
73
  if (!handler) {
59
74
  throw new SkillError(`handler not registered: ${handlerKey} (skill ${skill.name})`);
60
75
  }
61
- await handler(args, { skillName: skill.name });
62
- return 0;
76
+ await handler(args, { skillName: skill.name, auth });
77
+ return EXIT.OK;
63
78
  }
@@ -38,6 +38,14 @@ export type SkillEntry = {
38
38
  */
39
39
  export type SkillCategory = 'authoring' | 'consuming' | 'asset' | 'meta';
40
40
  export declare const CATEGORY_VALUES: readonly ["authoring", "consuming", "asset", "meta"];
41
+ /**
42
+ * Authorization requirement — consumed by the dispatcher's auth preflight.
43
+ * Semantics are documented on `AuthMode` in src/auth/ensure.ts (kept there so
44
+ * the runtime behavior and its description live together); this file only owns
45
+ * the literal values and their validation.
46
+ */
47
+ export type SkillAuthMode = 'required' | 'optional' | 'none';
48
+ export declare const AUTH_VALUES: readonly ["required", "optional", "none"];
41
49
  export interface RawSkillJson {
42
50
  name: string;
43
51
  toolName: string;
@@ -45,11 +53,22 @@ export interface RawSkillJson {
45
53
  title?: string;
46
54
  tier?: string;
47
55
  category?: SkillCategory;
56
+ auth?: SkillAuthMode;
48
57
  parameters?: Record<string, unknown>;
49
58
  scriptPath?: string;
50
59
  entry?: SkillEntry;
51
60
  envVars?: string[];
52
61
  }
62
+ /**
63
+ * Resolve a skill's auth mode, defaulting for records that predate the field.
64
+ *
65
+ * The default is derived from `envVars` rather than hardcoded to `'required'`:
66
+ * a skill that never declared PRIV_TOKEN cannot possibly need a credential, and
67
+ * defaulting it to `required` would make a purely local skill (web-screenshot)
68
+ * pop a browser. Every skill shipped in this package declares `auth` explicitly,
69
+ * so this only covers externally supplied skill dirs (SKILL_BASE_DIR).
70
+ */
71
+ export declare function resolveAuthMode(raw: Pick<RawSkillJson, 'auth' | 'envVars'>): SkillAuthMode;
53
72
  export declare const TIER_VALUES: readonly ["atomic", "orchestration", "tool"];
54
73
  export declare const REQUIRED_SKILL_JSON_FIELDS: readonly ["name", "tier", "title", "description"];
55
74
  /**
@@ -12,6 +12,21 @@
12
12
  * the build treats them as fatal.
13
13
  */
14
14
  export const CATEGORY_VALUES = ['authoring', 'consuming', 'asset', 'meta'];
15
+ export const AUTH_VALUES = ['required', 'optional', 'none'];
16
+ /**
17
+ * Resolve a skill's auth mode, defaulting for records that predate the field.
18
+ *
19
+ * The default is derived from `envVars` rather than hardcoded to `'required'`:
20
+ * a skill that never declared PRIV_TOKEN cannot possibly need a credential, and
21
+ * defaulting it to `required` would make a purely local skill (web-screenshot)
22
+ * pop a browser. Every skill shipped in this package declares `auth` explicitly,
23
+ * so this only covers externally supplied skill dirs (SKILL_BASE_DIR).
24
+ */
25
+ export function resolveAuthMode(raw) {
26
+ if (raw.auth)
27
+ return raw.auth;
28
+ return raw.envVars?.includes('PRIV_TOKEN') ? 'required' : 'none';
29
+ }
15
30
  export const TIER_VALUES = ['atomic', 'orchestration', 'tool'];
16
31
  export const REQUIRED_SKILL_JSON_FIELDS = ['name', 'tier', 'title', 'description'];
17
32
  /**
@@ -43,6 +58,9 @@ export function validateSkillJson(raw, skillId) {
43
58
  if (raw.category != null && !CATEGORY_VALUES.includes(raw.category)) {
44
59
  errors.push(`skill.json.category must be one of ${CATEGORY_VALUES.join(' | ')}, got '${raw.category}'`);
45
60
  }
61
+ if (raw.auth != null && !AUTH_VALUES.includes(raw.auth)) {
62
+ errors.push(`skill.json.auth must be one of ${AUTH_VALUES.join(' | ')}, got '${raw.auth}'`);
63
+ }
46
64
  if (raw.name && raw.name !== skillId) {
47
65
  errors.push(`skill.json.name='${raw.name}' does not match directory name '${skillId}'`);
48
66
  }
package/dist/text.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Terminal text helpers shared by the human-facing commands.
3
+ */
4
+ /**
5
+ * Pad to a target terminal width. CJK characters occupy two columns but count as
6
+ * one code unit, so String.padEnd misaligns any column that contains them.
7
+ */
8
+ export declare function padDisplay(text: string, width: number): string;
package/dist/text.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Terminal text helpers shared by the human-facing commands.
3
+ */
4
+ // Ranges covering the CJK / fullwidth blocks the CLI's Chinese output uses.
5
+ const WIDE = /[ᄀ-ᅟ⺀-䳿一-鿿가-힣豈-﫿︰-﹏＀-⦆¢-₩]/;
6
+ /**
7
+ * Pad to a target terminal width. CJK characters occupy two columns but count as
8
+ * one code unit, so String.padEnd misaligns any column that contains them.
9
+ */
10
+ export function padDisplay(text, width) {
11
+ let cols = 0;
12
+ for (const ch of text)
13
+ cols += WIDE.test(ch) ? 2 : 1;
14
+ return text + ' '.repeat(Math.max(1, width - cols));
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.13",
3
+ "version": "0.9.14",
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",
@@ -21,7 +21,7 @@
21
21
  "smoke": "node scripts/smoke.mjs",
22
22
  "test:cli": "npm run build && node --test test/*.test.mjs",
23
23
  "test:validators": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-validators.py",
24
- "test:template-pipeline": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-template-pipeline.py",
24
+ "test:template-pipeline": "PYTHONDONTWRITEBYTECODE=1 node dist/cli.js exec -- python3 scripts/test-template-pipeline.py",
25
25
  "test:props-contract": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-props-contract.py",
26
26
  "test:render-plan": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py",
27
27
  "test:render-plan:update": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py --update",
@@ -5,6 +5,7 @@
5
5
  "category": "consuming",
6
6
  "title": "Jianying (CapCut) Draft Export",
7
7
  "description": "Jianying (CapCut) draft export: package asset URLs into a draft ZIP that Jianying can import. Supports automatic conversion from a RenderPlan.",
8
+ "auth": "required",
8
9
  "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME"],
9
10
  "scriptPath": "scripts/gen_jianying_draft.py",
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "asset",
6
6
  "title": "Digital-Human Talking-Head",
7
7
  "description": "Digital-human video: list available avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL.",
8
+ "auth": "required",
8
9
  "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME"],
9
10
  "entry": { "type": "http", "handler": "gen-digital-human" },
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "asset",
6
6
  "title": "AI Image Generation",
7
7
  "description": "AI image generation: produce an image from a text prompt. Supports Seedream and Gemini models, plus image-to-image with reference images.",
8
+ "auth": "required",
8
9
  "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME", "MM_IMAGE_MODEL"],
9
10
  "entry": { "type": "http", "handler": "gen-image" },
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "authoring",
6
6
  "title": "Video Script Generation",
7
7
  "description": "Video script generation: turn a topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.",
8
+ "auth": "none",
8
9
  "envVars": ["DEFAULT_IMAGE_MODEL", "DEFAULT_VIDEO_MODEL", "STUB_IMAGE_URL", "STUB_VIDEO_URL"],
9
10
  "scriptPath": "scripts/gen_script.py",
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "asset",
6
6
  "title": "AI Video Generation",
7
7
  "description": "AI video generation: produce a short video clip from a text prompt. Supports Seedance and Veo models, plus first/last frame and reference images.",
8
+ "auth": "required",
8
9
  "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME", "MM_VIDEO_MODEL"],
9
10
  "entry": { "type": "http", "handler": "gen-video" },
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "asset",
6
6
  "title": "Text-to-Speech (Minimax)",
7
7
  "description": "Text-to-speech (TTS): synthesize narration audio from text via the Minimax TTS model. Returns the persisted audio URL — no download needed.",
8
+ "auth": "required",
8
9
  "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME"],
9
10
  "entry": { "type": "http", "handler": "gen-voice" },
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "authoring",
6
6
  "title": "Video Asset Preparation",
7
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.",
8
+ "auth": "required",
8
9
  "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"],
9
10
  "scriptPath": "scripts/prepare_video_assets.py",
10
11
  "parameters": {
@@ -5,6 +5,7 @@
5
5
  "category": "authoring",
6
6
  "title": "Remotion Video Renderer",
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
+ "auth": "required",
8
9
  "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"],
9
10
  "scriptPath": "scripts/render_video.py",
10
11
  "parameters": {
@@ -39,7 +39,13 @@ from pathlib import Path
39
39
 
40
40
  # When run as `python3 <skillDir>/scripts/list_templates.py`, this script's own
41
41
  # directory is sys.path[0], so the sibling shared modules import directly.
42
- from registry_loader import list_templates as load_visible_templates # noqa: E402
42
+ from registry_loader import ( # noqa: E402
43
+ EXIT_BACKEND_UNREACHABLE,
44
+ EXIT_NOT_AUTHENTICATED,
45
+ RegistryAuthError,
46
+ RegistryUnreachableError,
47
+ list_templates as load_visible_templates,
48
+ )
43
49
 
44
50
 
45
51
  def _matches(tpl: dict, tag: str | None, aspect: str | None, language: str | None) -> bool:
@@ -166,6 +172,15 @@ def main() -> None:
166
172
 
167
173
  try:
168
174
  templates = load_visible_templates()
175
+ except RegistryAuthError as exc:
176
+ # 退出码 4 = 需要授权(见 registry_loader.EXIT_NOT_AUTHENTICATED),宿主
177
+ # 据此引导用户重新登录,而不是把它当成一次普通失败。
178
+ print(f"❌ {exc}", file=sys.stderr)
179
+ sys.exit(EXIT_NOT_AUTHENTICATED)
180
+ except RegistryUnreachableError as exc:
181
+ # 退出码 5 = 后端不可达;重试或修配置即可,不需要重新登录。
182
+ print(f"❌ {exc}", file=sys.stderr)
183
+ sys.exit(EXIT_BACKEND_UNREACHABLE)
169
184
  except RuntimeError as exc:
170
185
  print(f"❌ {exc}", file=sys.stderr)
171
186
  sys.exit(1)