@remixmate/cli 0.9.13 → 0.9.15
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/billing.d.ts +44 -0
- package/dist/billing.js +76 -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 +20 -6
- package/dist/http.js +43 -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 +36 -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.md +12 -0
- package/skills/gen-digital-human/skill.json +1 -0
- package/skills/gen-image/SKILL.md +12 -0
- package/skills/gen-image/skill.json +1 -0
- package/skills/gen-script/skill.json +1 -0
- package/skills/gen-video/SKILL.md +12 -0
- package/skills/gen-video/skill.json +1 -0
- package/skills/gen-voice/SKILL.md +12 -0
- package/skills/gen-voice/skill.json +1 -0
- package/skills/prepare-video-assets/SKILL.md +12 -0
- package/skills/prepare-video-assets/skill.json +1 -0
- package/skills/render-video/SKILL.md +12 -0
- package/skills/render-video/scripts/render_video.py +63 -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/scripts/render_job_client.py +27 -0
- 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
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide error type and exit-code contract.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from http.ts so the auth gateway (auth/ensure.ts) can throw the same
|
|
5
|
+
* error type without creating an import cycle (http.ts → ensure.ts → http.ts).
|
|
6
|
+
* `SkillError` is still re-exported from http.ts, so existing imports are unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes are a CONTRACT with hosts (Claude Code / Codex / ab-agent): an agent
|
|
9
|
+
* reads the code to decide whether to guide the user through authorization, retry
|
|
10
|
+
* later, or surface a hard failure. Never reuse a code for a different meaning.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EXIT: {
|
|
13
|
+
/** Success. */
|
|
14
|
+
readonly OK: 0;
|
|
15
|
+
/** Generic runtime failure. */
|
|
16
|
+
readonly ERROR: 1;
|
|
17
|
+
/** Usage error — unknown skill / unknown option. */
|
|
18
|
+
readonly USAGE: 2;
|
|
19
|
+
/** No usable credential, or the credential was rejected. Host should offer to authorize. */
|
|
20
|
+
readonly NOT_AUTHENTICATED: 4;
|
|
21
|
+
/** Backend unreachable / timed out. Distinct from 4 so "log in" isn't suggested for an outage. */
|
|
22
|
+
readonly BACKEND_UNREACHABLE: 5;
|
|
23
|
+
/** Failed to spawn the underlying interpreter. */
|
|
24
|
+
readonly SPAWN_FAILED: 127;
|
|
25
|
+
};
|
|
26
|
+
export declare class SkillError extends Error {
|
|
27
|
+
readonly exitCode: number;
|
|
28
|
+
constructor(message: string, exitCode?: number);
|
|
29
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide error type and exit-code contract.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from http.ts so the auth gateway (auth/ensure.ts) can throw the same
|
|
5
|
+
* error type without creating an import cycle (http.ts → ensure.ts → http.ts).
|
|
6
|
+
* `SkillError` is still re-exported from http.ts, so existing imports are unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes are a CONTRACT with hosts (Claude Code / Codex / ab-agent): an agent
|
|
9
|
+
* reads the code to decide whether to guide the user through authorization, retry
|
|
10
|
+
* later, or surface a hard failure. Never reuse a code for a different meaning.
|
|
11
|
+
*/
|
|
12
|
+
export const EXIT = {
|
|
13
|
+
/** Success. */
|
|
14
|
+
OK: 0,
|
|
15
|
+
/** Generic runtime failure. */
|
|
16
|
+
ERROR: 1,
|
|
17
|
+
/** Usage error — unknown skill / unknown option. */
|
|
18
|
+
USAGE: 2,
|
|
19
|
+
/** No usable credential, or the credential was rejected. Host should offer to authorize. */
|
|
20
|
+
NOT_AUTHENTICATED: 4,
|
|
21
|
+
/** Backend unreachable / timed out. Distinct from 4 so "log in" isn't suggested for an outage. */
|
|
22
|
+
BACKEND_UNREACHABLE: 5,
|
|
23
|
+
/** Failed to spawn the underlying interpreter. */
|
|
24
|
+
SPAWN_FAILED: 127,
|
|
25
|
+
};
|
|
26
|
+
export class SkillError extends Error {
|
|
27
|
+
exitCode;
|
|
28
|
+
constructor(message, exitCode = EXIT.ERROR) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.exitCode = exitCode;
|
|
31
|
+
this.name = 'SkillError';
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/exec.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate exec -- <command> [args...]`
|
|
3
|
+
*
|
|
4
|
+
* Runs an arbitrary command with the resolved credential injected into its
|
|
5
|
+
* environment (`PRIV_TOKEN` + `MM_API_BASE_URL`), using the same gateway that
|
|
6
|
+
* skill dispatch uses.
|
|
7
|
+
*
|
|
8
|
+
* This is the general form of the invariant the auth rework establishes: Node
|
|
9
|
+
* is the only thing that reads the credential store / keychain / device flow,
|
|
10
|
+
* and every child process receives the result through its environment. Without
|
|
11
|
+
* it, maintenance scripts that import `registry_loader` directly (e.g.
|
|
12
|
+
* scripts/test-template-pipeline.py) would have to re-implement credential
|
|
13
|
+
* lookup — which is exactly the duplication, and the cross-origin token leak,
|
|
14
|
+
* that was just removed.
|
|
15
|
+
*
|
|
16
|
+
* The token is never printed; it only ever reaches the child's environment.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runExec(argv: string[]): Promise<number>;
|
package/dist/exec.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate exec -- <command> [args...]`
|
|
3
|
+
*
|
|
4
|
+
* Runs an arbitrary command with the resolved credential injected into its
|
|
5
|
+
* environment (`PRIV_TOKEN` + `MM_API_BASE_URL`), using the same gateway that
|
|
6
|
+
* skill dispatch uses.
|
|
7
|
+
*
|
|
8
|
+
* This is the general form of the invariant the auth rework establishes: Node
|
|
9
|
+
* is the only thing that reads the credential store / keychain / device flow,
|
|
10
|
+
* and every child process receives the result through its environment. Without
|
|
11
|
+
* it, maintenance scripts that import `registry_loader` directly (e.g.
|
|
12
|
+
* scripts/test-template-pipeline.py) would have to re-implement credential
|
|
13
|
+
* lookup — which is exactly the duplication, and the cross-origin token leak,
|
|
14
|
+
* that was just removed.
|
|
15
|
+
*
|
|
16
|
+
* The token is never printed; it only ever reaches the child's environment.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
import { parseArgv } from './argv.js';
|
|
20
|
+
import { EXIT } from './errors.js';
|
|
21
|
+
import { authChildEnv, ensureAuth } from './auth/ensure.js';
|
|
22
|
+
const USAGE = 'Usage: remixmate exec [--api-base-url URL] [--token TOKEN] -- <command> [args...]\n';
|
|
23
|
+
export async function runExec(argv) {
|
|
24
|
+
const separator = argv.indexOf('--');
|
|
25
|
+
if (separator === -1 || separator === argv.length - 1) {
|
|
26
|
+
process.stderr.write(`❌ exec 需要一个 \`--\` 分隔符与要执行的命令。\n${USAGE}`);
|
|
27
|
+
return EXIT.USAGE;
|
|
28
|
+
}
|
|
29
|
+
const flags = parseArgv(argv.slice(0, separator));
|
|
30
|
+
const [command, ...args] = argv.slice(separator + 1);
|
|
31
|
+
const auth = await ensureAuth({
|
|
32
|
+
mode: 'required',
|
|
33
|
+
apiBaseUrl: typeof flags.api_base_url === 'string' ? flags.api_base_url : undefined,
|
|
34
|
+
flagToken: typeof flags.token === 'string' ? flags.token : undefined,
|
|
35
|
+
});
|
|
36
|
+
return await new Promise((resolve) => {
|
|
37
|
+
const proc = spawn(command, args, {
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
env: { ...process.env, ...authChildEnv(auth) },
|
|
40
|
+
});
|
|
41
|
+
proc.on('exit', (code, signal) => resolve(signal ? EXIT.ERROR : code ?? EXIT.ERROR));
|
|
42
|
+
proc.on('error', (err) => {
|
|
43
|
+
process.stderr.write(`❌ failed to spawn ${command}: ${err.message}\n`);
|
|
44
|
+
resolve(EXIT.SPAWN_FAILED);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -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,19 @@
|
|
|
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
|
+
import { type Billing } from './billing.js';
|
|
22
|
+
export { SkillError, EXIT } from './errors.js';
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the ab-api base URL the same way for authenticated and device-flow calls.
|
|
25
|
+
*
|
|
26
|
+
* `MM_BACKEND_API_URL` is honored because several Python skills already fall back
|
|
27
|
+
* to it (registry_loader, render_job_client, _vod_polling). Since runner.ts now
|
|
28
|
+
* injects the resolved base URL into those child processes, leaving it out here
|
|
29
|
+
* would let a prod default silently override an ab-agent-injected backend.
|
|
19
30
|
*/
|
|
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
31
|
export declare function resolveApiBaseUrl(flag?: string): string;
|
|
26
32
|
export interface HttpContext {
|
|
27
33
|
apiBaseUrl: string;
|
|
@@ -33,11 +39,19 @@ export interface HttpContext {
|
|
|
33
39
|
export declare function resolveHttpContext(skillName: string, opts?: {
|
|
34
40
|
apiBaseUrl?: string;
|
|
35
41
|
privateToken?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Token already resolved by the dispatcher's auth preflight (runner.ts).
|
|
44
|
+
* Reusing it avoids a second credential-store read — and, more importantly,
|
|
45
|
+
* a second device-flow attempt — per skill invocation.
|
|
46
|
+
*/
|
|
47
|
+
preflightToken?: string;
|
|
36
48
|
}): Promise<HttpContext>;
|
|
37
49
|
export interface MmResponse<T = unknown> {
|
|
38
50
|
code: number;
|
|
39
51
|
msg?: string;
|
|
40
52
|
data?: T;
|
|
53
|
+
/** Present only on responses that charged credits — see billing.ts. */
|
|
54
|
+
billing?: Billing;
|
|
41
55
|
}
|
|
42
56
|
/**
|
|
43
57
|
* POST JSON to ab-api and return the parsed business payload.
|
package/dist/http.js
CHANGED
|
@@ -15,26 +15,37 @@
|
|
|
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
|
*/
|
|
21
|
+
import { recordBilling } from './billing.js';
|
|
20
22
|
import { resolvePrivToken, NOT_AUTHENTICATED_HINT } from './auth/resolve.js';
|
|
21
23
|
import { attemptAutoLogin } from './auth/auto-login.js';
|
|
24
|
+
import { EXIT, SkillError } from './errors.js';
|
|
22
25
|
const DEFAULT_API_BASE_URL = 'https://api.remixmate.com/api';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
// Re-exported so the many `import { SkillError } from '../http.js'` call sites
|
|
27
|
+
// stay valid; the definition moved to errors.ts to break an import cycle with
|
|
28
|
+
// the auth gateway (see errors.ts).
|
|
29
|
+
export { SkillError, EXIT } from './errors.js';
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the ab-api base URL the same way for authenticated and device-flow calls.
|
|
32
|
+
*
|
|
33
|
+
* `MM_BACKEND_API_URL` is honored because several Python skills already fall back
|
|
34
|
+
* to it (registry_loader, render_job_client, _vod_polling). Since runner.ts now
|
|
35
|
+
* injects the resolved base URL into those child processes, leaving it out here
|
|
36
|
+
* would let a prod default silently override an ab-agent-injected backend.
|
|
37
|
+
*/
|
|
32
38
|
export function resolveApiBaseUrl(flag) {
|
|
33
|
-
|
|
39
|
+
const fromEnv = (process.env.MM_API_BASE_URL ?? '').trim() || (process.env.MM_BACKEND_API_URL ?? '').trim();
|
|
40
|
+
return ((flag ?? '').trim() || fromEnv || DEFAULT_API_BASE_URL).replace(/\/+$/, '');
|
|
34
41
|
}
|
|
35
42
|
export async function resolveHttpContext(skillName, opts = {}) {
|
|
36
43
|
const apiBaseUrl = resolveApiBaseUrl(opts.apiBaseUrl);
|
|
37
|
-
|
|
44
|
+
// An explicit --token flag still wins over the preflight result.
|
|
45
|
+
let resolved = await resolvePrivToken({
|
|
46
|
+
flag: opts.privateToken ?? opts.preflightToken,
|
|
47
|
+
apiBaseUrl,
|
|
48
|
+
});
|
|
38
49
|
// Only when flag/env/store are all empty do we try automatic browser auth.
|
|
39
50
|
// attemptAutoLogin self-gates (cloud env / headless / disabled → returns null
|
|
40
51
|
// immediately), so the env-injected path never reaches it (zero regression).
|
|
@@ -42,7 +53,7 @@ export async function resolveHttpContext(skillName, opts = {}) {
|
|
|
42
53
|
resolved = await attemptAutoLogin(apiBaseUrl);
|
|
43
54
|
}
|
|
44
55
|
if (!resolved) {
|
|
45
|
-
throw new SkillError(NOT_AUTHENTICATED_HINT);
|
|
56
|
+
throw new SkillError(NOT_AUTHENTICATED_HINT, EXIT.NOT_AUTHENTICATED);
|
|
46
57
|
}
|
|
47
58
|
return {
|
|
48
59
|
apiBaseUrl,
|
|
@@ -83,15 +94,24 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
|
|
|
83
94
|
}
|
|
84
95
|
catch (err) {
|
|
85
96
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
86
|
-
throw new SkillError(`❌ request timed out: ${url}
|
|
97
|
+
throw new SkillError(`❌ request timed out: ${url}`, EXIT.BACKEND_UNREACHABLE);
|
|
87
98
|
}
|
|
88
|
-
throw new SkillError(`❌ network error: ${err.message}
|
|
99
|
+
throw new SkillError(`❌ network error: ${err.message}`, EXIT.BACKEND_UNREACHABLE);
|
|
89
100
|
}
|
|
90
101
|
finally {
|
|
91
102
|
clearTimeout(timeout);
|
|
92
103
|
}
|
|
93
104
|
const text = await resp.text();
|
|
94
105
|
if (!resp.ok) {
|
|
106
|
+
// 401/403 is not a generic API failure — it means the credential is missing,
|
|
107
|
+
// expired or revoked. Surfacing it as such (with exit code 4) is what lets a
|
|
108
|
+
// host offer to re-authorize instead of reporting an opaque backend error.
|
|
109
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
110
|
+
throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 HTTP ${resp.status})`, EXIT.NOT_AUTHENTICATED);
|
|
111
|
+
}
|
|
112
|
+
if (resp.status >= 500) {
|
|
113
|
+
throw new SkillError(`❌ API request failed (HTTP ${resp.status}): ${text}`, EXIT.BACKEND_UNREACHABLE);
|
|
114
|
+
}
|
|
95
115
|
throw new SkillError(`❌ API request failed (HTTP ${resp.status}): ${text}`);
|
|
96
116
|
}
|
|
97
117
|
let parsed;
|
|
@@ -101,7 +121,15 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
|
|
|
101
121
|
catch {
|
|
102
122
|
throw new SkillError(`❌ failed to parse response, body is not JSON: ${text.slice(0, 200)}`);
|
|
103
123
|
}
|
|
124
|
+
// Before the code check: a charged response is always code=0 today, but a
|
|
125
|
+
// partial-failure envelope that still billed must not lose its billing line.
|
|
126
|
+
recordBilling(parsed.billing);
|
|
104
127
|
if (parsed.code !== 0) {
|
|
128
|
+
// ab-api reports auth failures in the envelope (HTTP 200 + code=401), so the
|
|
129
|
+
// business-code path needs the same 401 → "re-authorize" mapping as above.
|
|
130
|
+
if (parsed.code === 401 || parsed.code === 403) {
|
|
131
|
+
throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 code=${parsed.code}${parsed.msg ? `: ${parsed.msg}` : ''})`, EXIT.NOT_AUTHENTICATED);
|
|
132
|
+
}
|
|
105
133
|
throw new SkillError(`❌ API returned a business error: ${parsed.msg ?? 'unknown error'} (code=${parsed.code})`);
|
|
106
134
|
}
|
|
107
135
|
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,44 @@
|
|
|
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
|
+
import { emitBillingFooter } from './billing.js';
|
|
19
|
+
/** Read the token override accepted by both the TS handlers and the Python skills. */
|
|
20
|
+
function tokenFlag(args) {
|
|
21
|
+
const value = args.token ?? args.priv_token;
|
|
22
|
+
return typeof value === 'string' ? value : undefined;
|
|
23
|
+
}
|
|
13
24
|
export async function runSkill(skillName, opts) {
|
|
14
25
|
const skill = findSkill(skillName, opts.baseDir ?? SKILLS_DIR);
|
|
15
26
|
if (!skill) {
|
|
16
27
|
process.stderr.write(`❌ skill not found: ${skillName}\n`);
|
|
17
|
-
return
|
|
28
|
+
return EXIT.USAGE;
|
|
18
29
|
}
|
|
19
30
|
try {
|
|
31
|
+
const auth = await ensureAuth({
|
|
32
|
+
mode: skill.auth,
|
|
33
|
+
apiBaseUrl: typeof opts.parsedArgs.api_base_url === 'string' ? opts.parsedArgs.api_base_url : undefined,
|
|
34
|
+
flagToken: tokenFlag(opts.parsedArgs),
|
|
35
|
+
});
|
|
20
36
|
switch (skill.entry.type) {
|
|
21
37
|
case 'python':
|
|
22
|
-
|
|
38
|
+
// Python children talk to ab-api themselves and print their own billing
|
|
39
|
+
// footer; nothing was charged through this process.
|
|
40
|
+
return await runPython(skill, opts.rawArgs, auth);
|
|
23
41
|
case 'http':
|
|
24
42
|
case 'builtin':
|
|
25
|
-
return await runHandler(skill, opts.parsedArgs);
|
|
43
|
+
return await runHandler(skill, opts.parsedArgs, auth);
|
|
26
44
|
}
|
|
27
45
|
}
|
|
28
46
|
catch (err) {
|
|
@@ -31,10 +49,16 @@ export async function runSkill(skillName, opts) {
|
|
|
31
49
|
return err.exitCode;
|
|
32
50
|
}
|
|
33
51
|
process.stderr.write(`❌ unexpected error: ${err.message}\n`);
|
|
34
|
-
return
|
|
52
|
+
return EXIT.ERROR;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
// In `finally` because a run can fail *after* a billed step (e.g. the image
|
|
56
|
+
// generated and was charged, then the upload timed out) — spend gets
|
|
57
|
+
// reported either way. No-ops when nothing was charged.
|
|
58
|
+
emitBillingFooter();
|
|
35
59
|
}
|
|
36
60
|
}
|
|
37
|
-
async function runPython(skill, rawArgs) {
|
|
61
|
+
async function runPython(skill, rawArgs, auth) {
|
|
38
62
|
if (!skill.scriptAbsolutePath) {
|
|
39
63
|
throw new SkillError(`skill ${skill.name} has entry.type=python but no scriptAbsolutePath`);
|
|
40
64
|
}
|
|
@@ -43,21 +67,21 @@ async function runPython(skill, rawArgs) {
|
|
|
43
67
|
const proc = spawn('python3', [skill.scriptAbsolutePath, ...rawArgs], {
|
|
44
68
|
cwd,
|
|
45
69
|
stdio: 'inherit',
|
|
46
|
-
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
70
|
+
env: { ...process.env, ...authChildEnv(auth), PYTHONUNBUFFERED: '1' },
|
|
47
71
|
});
|
|
48
|
-
proc.on('exit', (code) => resolve(code ??
|
|
72
|
+
proc.on('exit', (code) => resolve(code ?? EXIT.ERROR));
|
|
49
73
|
proc.on('error', (err) => {
|
|
50
74
|
process.stderr.write(`❌ failed to spawn python3: ${err.message}\n`);
|
|
51
|
-
resolve(
|
|
75
|
+
resolve(EXIT.SPAWN_FAILED);
|
|
52
76
|
});
|
|
53
77
|
});
|
|
54
78
|
}
|
|
55
|
-
async function runHandler(skill, args) {
|
|
79
|
+
async function runHandler(skill, args, auth) {
|
|
56
80
|
const handlerKey = skill.entry.handler;
|
|
57
81
|
const handler = HANDLERS[handlerKey];
|
|
58
82
|
if (!handler) {
|
|
59
83
|
throw new SkillError(`handler not registered: ${handlerKey} (skill ${skill.name})`);
|
|
60
84
|
}
|
|
61
|
-
await handler(args, { skillName: skill.name });
|
|
62
|
-
return
|
|
85
|
+
await handler(args, { skillName: skill.name, auth });
|
|
86
|
+
return EXIT.OK;
|
|
63
87
|
}
|
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.15",
|
|
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": {
|
|
@@ -175,6 +175,18 @@ remixmate gen-digital-human --check-status --generation-id 123
|
|
|
175
175
|
- Keep individual jobs under ~500 characters.
|
|
176
176
|
- Tone and style of the script affect the perceived voice.
|
|
177
177
|
|
|
178
|
+
## Credits
|
|
179
|
+
|
|
180
|
+
Every run charges credits. The CLI prints a footer on stdout when it does:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
💳 Charged 31 credits · balance 1,240
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Relay it to the user whenever it appears — it is the only signal they get about what a
|
|
187
|
+
generation cost, and the balance is the only warning before a run fails with
|
|
188
|
+
`insufficient_credits`. Do not drop it from your summary.
|
|
189
|
+
|
|
178
190
|
## Error handling
|
|
179
191
|
|
|
180
192
|
- **401** / **token missing** (non-OpenClaw): set `PRIV_TOKEN`.
|