@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.
- package/README.md +54 -1
- package/README.zh-CN.md +37 -1
- package/dist/auth/auto-login.js +10 -1
- package/dist/auth/commands.js +181 -21
- package/dist/auth/credential-store.d.ts +16 -0
- package/dist/auth/credential-store.js +17 -0
- package/dist/auth/device-flow-runner.d.ts +15 -0
- package/dist/auth/device-flow-runner.js +60 -12
- package/dist/auth/device-flow.js +24 -7
- package/dist/auth/ensure.d.ts +55 -0
- package/dist/auth/ensure.js +62 -0
- package/dist/auth/environment.d.ts +13 -0
- package/dist/auth/environment.js +21 -0
- package/dist/auth/pending-store.d.ts +33 -0
- package/dist/auth/pending-store.js +45 -0
- package/dist/cli.js +75 -8
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +142 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +33 -0
- package/dist/exec.d.ts +18 -0
- package/dist/exec.js +47 -0
- package/dist/handlers/gen-digital-human.js +1 -0
- package/dist/handlers/gen-image.js +1 -0
- package/dist/handlers/gen-video.js +1 -0
- package/dist/handlers/gen-voice.js +2 -0
- package/dist/handlers/index.d.ts +7 -0
- package/dist/http.d.ts +17 -6
- package/dist/http.js +39 -15
- package/dist/manifest.json +2 -2
- package/dist/registry.d.ts +4 -2
- package/dist/registry.js +2 -1
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +27 -12
- package/dist/skill-schema.d.ts +19 -0
- package/dist/skill-schema.js +18 -0
- package/dist/text.d.ts +8 -0
- package/dist/text.js +15 -0
- package/package.json +2 -2
- package/skills/export-jianying/skill.json +1 -0
- package/skills/gen-digital-human/skill.json +1 -0
- package/skills/gen-image/skill.json +1 -0
- package/skills/gen-script/skill.json +1 -0
- package/skills/gen-video/skill.json +1 -0
- package/skills/gen-voice/skill.json +1 -0
- package/skills/prepare-video-assets/skill.json +1 -0
- package/skills/render-video/skill.json +1 -0
- package/skills/template-registry/scripts/list_templates.py +16 -1
- package/skills/template-registry/scripts/registry_loader.py +63 -63
- package/skills/template-registry/skill.json +1 -0
- package/skills/video-parser/skill.json +1 -0
- package/skills/web-record/skill.json +1 -0
- 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 });
|
|
@@ -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 });
|
package/dist/handlers/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/manifest.json
CHANGED
package/dist/registry.d.ts
CHANGED
|
@@ -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 './
|
|
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
|
|
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
|
|
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 ??
|
|
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(
|
|
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
|
|
76
|
+
await handler(args, { skillName: skill.name, auth });
|
|
77
|
+
return EXIT.OK;
|
|
63
78
|
}
|
package/dist/skill-schema.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/skill-schema.js
CHANGED
|
@@ -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.
|
|
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
|
|
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)
|