@remixmate/cli 0.1.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/README.zh-CN.md +22 -0
- package/dist/auth/auth-lock.d.ts +26 -0
- package/dist/auth/auth-lock.js +100 -0
- package/dist/auth/auto-login.d.ts +20 -0
- package/dist/auth/auto-login.js +66 -0
- package/dist/auth/commands.d.ts +8 -0
- package/dist/auth/commands.js +130 -0
- package/dist/auth/credential-store.d.ts +44 -0
- package/dist/auth/credential-store.js +126 -0
- package/dist/auth/device-flow-runner.d.ts +43 -0
- package/dist/auth/device-flow-runner.js +62 -0
- package/dist/auth/device-flow.d.ts +52 -0
- package/dist/auth/device-flow.js +115 -0
- package/dist/auth/environment.d.ts +25 -0
- package/dist/auth/environment.js +48 -0
- package/dist/auth/resolve.d.ts +30 -0
- package/dist/auth/resolve.js +44 -0
- package/dist/cli.js +11 -0
- package/dist/handlers/gen-digital-human.js +1 -1
- package/dist/handlers/gen-image.js +1 -1
- package/dist/handlers/gen-video.js +1 -1
- package/dist/handlers/gen-voice.js +2 -2
- package/dist/http.d.ts +9 -5
- package/dist/http.js +24 -10
- package/dist/manifest.json +14 -3
- package/dist/runner.d.ts +1 -1
- package/dist/skill-schema.d.ts +18 -0
- package/dist/skill-schema.js +4 -0
- package/package.json +1 -1
- package/skills/export-jianying/skill.json +1 -0
- package/skills/gen-digital-human/SKILL.md +11 -11
- package/skills/gen-digital-human/skill.json +1 -0
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +6 -6
- package/skills/gen-image/skill.json +1 -0
- package/skills/gen-image/version.json +1 -1
- package/skills/gen-script/scripts/gen_script.py +105 -31
- package/skills/gen-script/skill.json +3 -1
- package/skills/gen-script/version.json +1 -1
- package/skills/gen-video/SKILL.md +6 -6
- package/skills/gen-video/skill.json +1 -0
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +6 -6
- package/skills/gen-voice/skill.json +1 -0
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/skill.json +1 -0
- package/skills/render-video/scripts/remote_renderer_client.py +9 -7
- package/skills/render-video/scripts/render_video.py +72 -15
- package/skills/render-video/skill.json +1 -0
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/README.md +12 -13
- package/skills/template-registry/SKILL.md +11 -12
- package/skills/template-registry/scripts/list_templates.py +87 -2
- package/skills/template-registry/scripts/registry_loader.py +117 -96
- package/skills/template-registry/scripts/render_job_client.py +12 -0
- package/skills/template-registry/skill.json +4 -2
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +2 -2
- package/skills/video-parser/skill.json +1 -0
- package/skills/web-capture/skill.json +1 -1
- package/skills/web-capture/version.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Device Flow orchestration used by BOTH `remixmate login` (interactive,
|
|
3
|
+
* full device-code lifetime) and the automatic browser auth that kicks in when
|
|
4
|
+
* a skill call finds no token (bounded by a short wait window).
|
|
5
|
+
*
|
|
6
|
+
* Centralizing the request → open browser → poll → persist sequence keeps the
|
|
7
|
+
* two entry points byte-for-byte consistent (Requirement 4.1/4.2) and means the
|
|
8
|
+
* credential written by auto-login is indistinguishable from `login`'s.
|
|
9
|
+
*
|
|
10
|
+
* Security: the PrivToken value is NEVER written to the provided sink.
|
|
11
|
+
*/
|
|
12
|
+
import { DeviceFlowError, openBrowser, pollForToken, requestDeviceCode, } from './device-flow.js';
|
|
13
|
+
import { setCredential } from './credential-store.js';
|
|
14
|
+
function nowSec() {
|
|
15
|
+
return Math.floor(Date.now() / 1000);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run one full Device Flow against `apiBaseUrl`. On approval the PrivToken is
|
|
19
|
+
* persisted via setCredential (keychain or file, same as `login`) and only the
|
|
20
|
+
* non-secret outcome is returned.
|
|
21
|
+
*/
|
|
22
|
+
export async function runDeviceFlow(apiBaseUrl, opts = {}) {
|
|
23
|
+
const write = opts.write ?? ((msg) => process.stderr.write(msg));
|
|
24
|
+
const clientLabel = opts.clientLabel ?? `remixmate-cli (${process.platform} ${process.arch})`;
|
|
25
|
+
try {
|
|
26
|
+
const code = await requestDeviceCode(apiBaseUrl, clientLabel);
|
|
27
|
+
const openUrl = code.verificationUriComplete ?? code.verificationUri;
|
|
28
|
+
// Always print the URL + user code, even when the browser auto-opens, so a
|
|
29
|
+
// host (Claude Code / Codex) can relay it to the user.
|
|
30
|
+
write(`\n请在浏览器中完成授权:\n ${code.verificationUri}\n设备码:${code.userCode}\n\n`);
|
|
31
|
+
if (openBrowser(openUrl)) {
|
|
32
|
+
write('已尝试自动打开浏览器…\n');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
write('无法自动打开浏览器,请手动复制上面的地址。\n');
|
|
36
|
+
}
|
|
37
|
+
write('等待授权中(可在浏览器确认)…\n');
|
|
38
|
+
const result = await pollForToken(apiBaseUrl, code, { maxWaitMs: opts.maxWaitMs });
|
|
39
|
+
if (result.status === 'approved' && result.privToken) {
|
|
40
|
+
const { usedKeychain, keychainError } = await setCredential(apiBaseUrl, {
|
|
41
|
+
privToken: result.privToken,
|
|
42
|
+
userLabel: result.userLabel,
|
|
43
|
+
createdAt: nowSec(),
|
|
44
|
+
});
|
|
45
|
+
if (!usedKeychain && keychainError) {
|
|
46
|
+
write('(系统钥匙串不可用,凭证已写入受保护的本地文件 ~/.config/remixmate/credentials.json)\n');
|
|
47
|
+
}
|
|
48
|
+
return { status: 'approved', userLabel: result.userLabel };
|
|
49
|
+
}
|
|
50
|
+
if (result.status === 'access_denied')
|
|
51
|
+
return { status: 'denied' };
|
|
52
|
+
if (result.timedOut)
|
|
53
|
+
return { status: 'timeout' };
|
|
54
|
+
return { status: 'expired' };
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err instanceof DeviceFlowError) {
|
|
58
|
+
return { status: 'error', message: err.message };
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Device Authorization Grant (RFC 8628) client for `remixmate login`.
|
|
3
|
+
*
|
|
4
|
+
* The device/code and device/token endpoints are PUBLIC (no auth), so this uses
|
|
5
|
+
* plain fetch against the ab-api base URL rather than the authenticated mmPost.
|
|
6
|
+
* On approval the backend returns the user's existing PrivToken (Route B).
|
|
7
|
+
*/
|
|
8
|
+
export interface DeviceCodeResponse {
|
|
9
|
+
deviceCode: string;
|
|
10
|
+
userCode: string;
|
|
11
|
+
verificationUri: string;
|
|
12
|
+
verificationUriComplete?: string;
|
|
13
|
+
interval: number;
|
|
14
|
+
expiresIn: number;
|
|
15
|
+
}
|
|
16
|
+
/** Device-token poll sub-states (mirrors backend model.DeviceTokenStatus*). */
|
|
17
|
+
export type TokenStatus = 'authorization_pending' | 'slow_down' | 'approved' | 'access_denied' | 'expired_token' | 'invalid_grant';
|
|
18
|
+
export interface TokenResult {
|
|
19
|
+
status: TokenStatus;
|
|
20
|
+
privToken?: string;
|
|
21
|
+
userLabel?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Set when polling stopped because the caller-supplied `maxWaitMs` window
|
|
24
|
+
* elapsed BEFORE the device code's own expiry. Lets callers distinguish a
|
|
25
|
+
* short-wait timeout ("authorize then re-run") from a real expired code.
|
|
26
|
+
*/
|
|
27
|
+
timedOut?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare class DeviceFlowError extends Error {
|
|
30
|
+
}
|
|
31
|
+
/** Request a fresh device_code/user_code. 30s timeout, no retries here. */
|
|
32
|
+
export declare function requestDeviceCode(apiBaseUrl: string, clientLabel: string): Promise<DeviceCodeResponse>;
|
|
33
|
+
/**
|
|
34
|
+
* Try to open `url` in the default browser. Returns false when no opener is
|
|
35
|
+
* available (caller then prints the URL for manual entry).
|
|
36
|
+
*/
|
|
37
|
+
export declare function openBrowser(url: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Poll device/token until a terminal outcome. Honors `interval` (+5s on
|
|
40
|
+
* slow_down), retries transient network failures up to 3 consecutive times, and
|
|
41
|
+
* bounds total time by the device code's `expiresIn`.
|
|
42
|
+
*
|
|
43
|
+
* When `opts.maxWaitMs` is supplied, polling also stops once that window
|
|
44
|
+
* elapses (whichever comes first). A stop caused by `maxWaitMs` returns
|
|
45
|
+
* `{ status: 'expired_token', timedOut: true }` so callers can tell a
|
|
46
|
+
* short-wait timeout apart from a genuinely expired device code.
|
|
47
|
+
*
|
|
48
|
+
* Resolves with the final TokenResult (status 'approved' carries the token).
|
|
49
|
+
*/
|
|
50
|
+
export declare function pollForToken(apiBaseUrl: string, code: DeviceCodeResponse, opts?: {
|
|
51
|
+
maxWaitMs?: number;
|
|
52
|
+
}): Promise<TokenResult>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Device Authorization Grant (RFC 8628) client for `remixmate login`.
|
|
3
|
+
*
|
|
4
|
+
* The device/code and device/token endpoints are PUBLIC (no auth), so this uses
|
|
5
|
+
* plain fetch against the ab-api base URL rather than the authenticated mmPost.
|
|
6
|
+
* On approval the backend returns the user's existing PrivToken (Route B).
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
export class DeviceFlowError extends Error {
|
|
10
|
+
}
|
|
11
|
+
async function postJson(url, body, timeoutMs) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
14
|
+
try {
|
|
15
|
+
const resp = await fetch(url, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
signal: controller.signal,
|
|
20
|
+
});
|
|
21
|
+
const text = await resp.text();
|
|
22
|
+
if (!resp.ok) {
|
|
23
|
+
throw new DeviceFlowError(`HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(text);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32
|
+
/** Request a fresh device_code/user_code. 30s timeout, no retries here. */
|
|
33
|
+
export async function requestDeviceCode(apiBaseUrl, clientLabel) {
|
|
34
|
+
const env = await postJson(`${apiBaseUrl}/auth/device/code`, { clientLabel }, 30_000);
|
|
35
|
+
if (env.code !== 0 || !env.data) {
|
|
36
|
+
throw new DeviceFlowError(env.msg || 'failed to obtain device code');
|
|
37
|
+
}
|
|
38
|
+
return env.data;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Try to open `url` in the default browser. Returns false when no opener is
|
|
42
|
+
* available (caller then prints the URL for manual entry).
|
|
43
|
+
*/
|
|
44
|
+
export function openBrowser(url) {
|
|
45
|
+
const cmd = process.platform === 'darwin' ? 'open' :
|
|
46
|
+
process.platform === 'win32' ? 'cmd' :
|
|
47
|
+
'xdg-open';
|
|
48
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
49
|
+
try {
|
|
50
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
51
|
+
child.on('error', () => { });
|
|
52
|
+
child.unref();
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Poll device/token until a terminal outcome. Honors `interval` (+5s on
|
|
61
|
+
* slow_down), retries transient network failures up to 3 consecutive times, and
|
|
62
|
+
* bounds total time by the device code's `expiresIn`.
|
|
63
|
+
*
|
|
64
|
+
* When `opts.maxWaitMs` is supplied, polling also stops once that window
|
|
65
|
+
* elapses (whichever comes first). A stop caused by `maxWaitMs` returns
|
|
66
|
+
* `{ status: 'expired_token', timedOut: true }` so callers can tell a
|
|
67
|
+
* short-wait timeout apart from a genuinely expired device code.
|
|
68
|
+
*
|
|
69
|
+
* Resolves with the final TokenResult (status 'approved' carries the token).
|
|
70
|
+
*/
|
|
71
|
+
export async function pollForToken(apiBaseUrl, code, opts = {}) {
|
|
72
|
+
const expiryDeadline = Date.now() + code.expiresIn * 1000;
|
|
73
|
+
const waitDeadline = opts.maxWaitMs != null ? Date.now() + opts.maxWaitMs : Number.POSITIVE_INFINITY;
|
|
74
|
+
const deadline = Math.min(expiryDeadline, waitDeadline);
|
|
75
|
+
let intervalMs = Math.max(code.interval, 1) * 1000;
|
|
76
|
+
let consecutiveErrors = 0;
|
|
77
|
+
// Wait one interval before the first poll (approval can't have happened yet).
|
|
78
|
+
await sleep(intervalMs);
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
let env;
|
|
81
|
+
try {
|
|
82
|
+
env = await postJson(`${apiBaseUrl}/auth/device/token`, { deviceCode: code.deviceCode }, 15_000);
|
|
83
|
+
consecutiveErrors = 0;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
consecutiveErrors += 1;
|
|
87
|
+
if (consecutiveErrors >= 3) {
|
|
88
|
+
throw new DeviceFlowError(`polling failed: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
await sleep(intervalMs);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (env.code !== 0 || !env.data) {
|
|
94
|
+
throw new DeviceFlowError(env.msg || 'unexpected token response');
|
|
95
|
+
}
|
|
96
|
+
const result = env.data;
|
|
97
|
+
switch (result.status) {
|
|
98
|
+
case 'approved':
|
|
99
|
+
case 'access_denied':
|
|
100
|
+
case 'expired_token':
|
|
101
|
+
case 'invalid_grant':
|
|
102
|
+
return result;
|
|
103
|
+
case 'slow_down':
|
|
104
|
+
intervalMs += 5_000;
|
|
105
|
+
break;
|
|
106
|
+
case 'authorization_pending':
|
|
107
|
+
default:
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
await sleep(intervalMs);
|
|
111
|
+
}
|
|
112
|
+
// Stopped on the wait window rather than the code's own expiry.
|
|
113
|
+
const timedOut = waitDeadline <= expiryDeadline;
|
|
114
|
+
return { status: 'expired_token', timedOut };
|
|
115
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure environment predicates that gate the auto device-login feature.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is synchronous, side-effect free, never touches the network
|
|
5
|
+
* and never reads a PrivToken. The conservative bias is: when in doubt, treat
|
|
6
|
+
* the environment as headless so we fail fast rather than pop a useless browser.
|
|
7
|
+
*
|
|
8
|
+
* See .kiro/specs/cli-auto-device-login/design.md (Requirement 1.4–1.7).
|
|
9
|
+
*/
|
|
10
|
+
/** Total kill switch: hosts can force-disable auto auth and fall back to fail-fast. */
|
|
11
|
+
export declare function autoAuthDisabled(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the process is likely running without a desktop session capable of
|
|
14
|
+
* opening a browser.
|
|
15
|
+
*
|
|
16
|
+
* - CI environments are always treated as headless.
|
|
17
|
+
* - On Linux a browser needs an X11 / Wayland display; absence → headless.
|
|
18
|
+
* - macOS / Windows are assumed to have a GUI session available.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isHeadless(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Master gate for auto authorization. Only true when auto auth is not disabled
|
|
23
|
+
* AND the environment can plausibly open a browser for the user.
|
|
24
|
+
*/
|
|
25
|
+
export declare function canAutoAuth(): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure environment predicates that gate the auto device-login feature.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is synchronous, side-effect free, never touches the network
|
|
5
|
+
* and never reads a PrivToken. The conservative bias is: when in doubt, treat
|
|
6
|
+
* the environment as headless so we fail fast rather than pop a useless browser.
|
|
7
|
+
*
|
|
8
|
+
* See .kiro/specs/cli-auto-device-login/design.md (Requirement 1.4–1.7).
|
|
9
|
+
*/
|
|
10
|
+
/** Total kill switch: hosts can force-disable auto auth and fall back to fail-fast. */
|
|
11
|
+
export function autoAuthDisabled() {
|
|
12
|
+
return (process.env.REMIXMATE_NO_BROWSER_AUTH ?? '').trim() === '1';
|
|
13
|
+
}
|
|
14
|
+
/** Heuristic detection of common CI environments (no desktop → headless). */
|
|
15
|
+
function isCI() {
|
|
16
|
+
const e = process.env;
|
|
17
|
+
return !!(e.CI ||
|
|
18
|
+
e.CONTINUOUS_INTEGRATION ||
|
|
19
|
+
e.GITHUB_ACTIONS ||
|
|
20
|
+
e.GITLAB_CI ||
|
|
21
|
+
e.BUILDKITE ||
|
|
22
|
+
e.CIRCLECI ||
|
|
23
|
+
e.TRAVIS ||
|
|
24
|
+
e.JENKINS_URL);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether the process is likely running without a desktop session capable of
|
|
28
|
+
* opening a browser.
|
|
29
|
+
*
|
|
30
|
+
* - CI environments are always treated as headless.
|
|
31
|
+
* - On Linux a browser needs an X11 / Wayland display; absence → headless.
|
|
32
|
+
* - macOS / Windows are assumed to have a GUI session available.
|
|
33
|
+
*/
|
|
34
|
+
export function isHeadless() {
|
|
35
|
+
if (isCI())
|
|
36
|
+
return true;
|
|
37
|
+
if (process.platform === 'linux') {
|
|
38
|
+
return !((process.env.DISPLAY ?? '').trim() || (process.env.WAYLAND_DISPLAY ?? '').trim());
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Master gate for auto authorization. Only true when auto auth is not disabled
|
|
44
|
+
* AND the environment can plausibly open a browser for the user.
|
|
45
|
+
*/
|
|
46
|
+
export function canAutoAuth() {
|
|
47
|
+
return !autoAuthDisabled() && !isHeadless();
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for PrivToken resolution precedence, shared by the
|
|
3
|
+
* CLI commands and the HTTP layer (src/http.ts wires this in — Wave 2 task 4.3).
|
|
4
|
+
*
|
|
5
|
+
* Precedence (high → low), per requirements 7.1–7.4:
|
|
6
|
+
* 1. explicit --token flag (caller override)
|
|
7
|
+
* 2. PRIV_TOKEN environment variable (CI / agent-embedded hosts)
|
|
8
|
+
* 3. Credential_Store entry for the active API base URL (written by `login`)
|
|
9
|
+
*
|
|
10
|
+
* A value that is empty after trimming whitespace is treated as ABSENT, so an
|
|
11
|
+
* exported-but-empty `PRIV_TOKEN=""` falls through to the credential store
|
|
12
|
+
* rather than masking it.
|
|
13
|
+
*/
|
|
14
|
+
export type CredentialSource = 'flag' | 'env' | 'credential-store';
|
|
15
|
+
export interface ResolvedToken {
|
|
16
|
+
privToken: string;
|
|
17
|
+
source: CredentialSource;
|
|
18
|
+
/** Present only for the credential-store source. */
|
|
19
|
+
userLabel?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the PrivToken following the documented precedence. Returns null when
|
|
23
|
+
* no source yields a token (callers render the not-authenticated message).
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolvePrivToken(opts: {
|
|
26
|
+
flag?: string;
|
|
27
|
+
apiBaseUrl: string;
|
|
28
|
+
}): Promise<ResolvedToken | null>;
|
|
29
|
+
/** Human-readable hint shown when resolution fails (no token echoed). */
|
|
30
|
+
export declare const NOT_AUTHENTICATED_HINT = "\u274C not authenticated \u2014 run `remixmate login`, set PRIV_TOKEN, or pass --token";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for PrivToken resolution precedence, shared by the
|
|
3
|
+
* CLI commands and the HTTP layer (src/http.ts wires this in — Wave 2 task 4.3).
|
|
4
|
+
*
|
|
5
|
+
* Precedence (high → low), per requirements 7.1–7.4:
|
|
6
|
+
* 1. explicit --token flag (caller override)
|
|
7
|
+
* 2. PRIV_TOKEN environment variable (CI / agent-embedded hosts)
|
|
8
|
+
* 3. Credential_Store entry for the active API base URL (written by `login`)
|
|
9
|
+
*
|
|
10
|
+
* A value that is empty after trimming whitespace is treated as ABSENT, so an
|
|
11
|
+
* exported-but-empty `PRIV_TOKEN=""` falls through to the credential store
|
|
12
|
+
* rather than masking it.
|
|
13
|
+
*/
|
|
14
|
+
import { getCredential } from './credential-store.js';
|
|
15
|
+
/** Trim and return undefined when empty, so empty == absent everywhere. */
|
|
16
|
+
function clean(value) {
|
|
17
|
+
if (value == null)
|
|
18
|
+
return undefined;
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the PrivToken following the documented precedence. Returns null when
|
|
24
|
+
* no source yields a token (callers render the not-authenticated message).
|
|
25
|
+
*/
|
|
26
|
+
export async function resolvePrivToken(opts) {
|
|
27
|
+
const flag = clean(opts.flag);
|
|
28
|
+
if (flag)
|
|
29
|
+
return { privToken: flag, source: 'flag' };
|
|
30
|
+
const env = clean(process.env.PRIV_TOKEN);
|
|
31
|
+
if (env)
|
|
32
|
+
return { privToken: env, source: 'env' };
|
|
33
|
+
const stored = await getCredential(opts.apiBaseUrl);
|
|
34
|
+
if (stored && clean(stored.privToken)) {
|
|
35
|
+
return {
|
|
36
|
+
privToken: stored.privToken,
|
|
37
|
+
source: 'credential-store',
|
|
38
|
+
userLabel: stored.userLabel,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** Human-readable hint shown when resolution fails (no token echoed). */
|
|
44
|
+
export const NOT_AUTHENTICATED_HINT = '❌ not authenticated — run `remixmate login`, set PRIV_TOKEN, or pass --token';
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,9 @@ import path from 'node:path';
|
|
|
14
14
|
import { loadSkills, SKILLS_DIR } from './registry.js';
|
|
15
15
|
import { parseArgv } from './argv.js';
|
|
16
16
|
import { runSkill } from './runner.js';
|
|
17
|
+
import { runAuthCommand } from './auth/commands.js';
|
|
18
|
+
/** Reserved auth verbs handled before skill dispatch. */
|
|
19
|
+
const AUTH_VERBS = new Set(['login', 'logout', 'whoami']);
|
|
17
20
|
/**
|
|
18
21
|
* SKILL_BASE_DIR can arrive as a relative path (e.g. from a developer .env or
|
|
19
22
|
* from ab-agent's spawn env). The cli script's cwd isn't a stable anchor for
|
|
@@ -40,6 +43,9 @@ function printUsage() {
|
|
|
40
43
|
const skills = loadSkills(resolveBaseDir());
|
|
41
44
|
process.stdout.write('Usage:\n');
|
|
42
45
|
process.stdout.write(' remixmate --list List all skills\n');
|
|
46
|
+
process.stdout.write(' remixmate login Authorize this CLI via the browser\n');
|
|
47
|
+
process.stdout.write(' remixmate logout Remove stored credentials\n');
|
|
48
|
+
process.stdout.write(' remixmate whoami Show the current identity\n');
|
|
43
49
|
process.stdout.write(' remixmate <name> [--flag value ...] Invoke a skill\n');
|
|
44
50
|
process.stdout.write(' remixmate <name> --help Show skill-specific help\n');
|
|
45
51
|
process.stdout.write('\nAvailable skills:\n');
|
|
@@ -63,6 +69,11 @@ async function main() {
|
|
|
63
69
|
printList();
|
|
64
70
|
process.exit(0);
|
|
65
71
|
}
|
|
72
|
+
// Auth verbs are handled before skill dispatch so they never collide with skill names.
|
|
73
|
+
if (AUTH_VERBS.has(argv[0])) {
|
|
74
|
+
const code = await runAuthCommand(argv[0], argv.slice(1));
|
|
75
|
+
process.exit(code);
|
|
76
|
+
}
|
|
66
77
|
if (argv[0].startsWith('-')) {
|
|
67
78
|
process.stderr.write(`❌ unknown option: ${argv[0]}\n`);
|
|
68
79
|
printUsage();
|
|
@@ -89,7 +89,7 @@ async function pollVideoStatus(ctx, generationId) {
|
|
|
89
89
|
return url;
|
|
90
90
|
}
|
|
91
91
|
export async function genDigitalHuman(input, ctxIn) {
|
|
92
|
-
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
92
|
+
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
93
93
|
apiBaseUrl: input.api_base_url,
|
|
94
94
|
privateToken: input.priv_token,
|
|
95
95
|
});
|
|
@@ -61,7 +61,7 @@ export async function genImage(input, ctxIn) {
|
|
|
61
61
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
62
62
|
throw new SkillError('❌ Please provide --prompt to describe the image');
|
|
63
63
|
}
|
|
64
|
-
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
64
|
+
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
65
65
|
apiBaseUrl: input.api_base_url,
|
|
66
66
|
privateToken: input.priv_token,
|
|
67
67
|
});
|
|
@@ -106,7 +106,7 @@ export async function genVideo(input, ctxIn) {
|
|
|
106
106
|
const duration = toNumber(input.duration, 'duration') ?? (isVeoModel(model) ? 8 : 5);
|
|
107
107
|
const seed = toNumber(input.seed, 'seed');
|
|
108
108
|
validateParamsForModel(model, duration, ratio, resolution, references.length);
|
|
109
|
-
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
109
|
+
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
110
110
|
apiBaseUrl: input.api_base_url,
|
|
111
111
|
privateToken: input.priv_token,
|
|
112
112
|
});
|
|
@@ -53,7 +53,7 @@ export async function genVoice(input, ctxIn) {
|
|
|
53
53
|
if (isTrue(input.list_voices)) {
|
|
54
54
|
if (isTrue(input.local))
|
|
55
55
|
return listVoicesLocal();
|
|
56
|
-
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
56
|
+
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
57
57
|
apiBaseUrl: input.api_base_url,
|
|
58
58
|
privateToken: input.priv_token,
|
|
59
59
|
});
|
|
@@ -69,7 +69,7 @@ export async function genVoice(input, ctxIn) {
|
|
|
69
69
|
if (speed < 0.5 || speed > 2.0) {
|
|
70
70
|
throw new SkillError('❌ speed must be in the range 0.5~2.0');
|
|
71
71
|
}
|
|
72
|
-
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
72
|
+
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
73
73
|
apiBaseUrl: input.api_base_url,
|
|
74
74
|
privateToken: input.priv_token,
|
|
75
75
|
});
|
package/dist/http.d.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* x-conversation-id), business-code error handling (result.code != 0), and the
|
|
6
6
|
* env var resolution order that every Python skill used to duplicate.
|
|
7
7
|
*
|
|
8
|
-
* Token resolution order (high → low)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Token resolution order (high → low), shared with the CLI auth commands via
|
|
9
|
+
* src/auth/resolve.ts:
|
|
10
|
+
* 1. opts.privateToken — caller override (maps to --token/--priv-token)
|
|
11
|
+
* 2. process.env.PRIV_TOKEN — process env (CI / agent-embedded hosts)
|
|
12
|
+
* 3. Credential_Store[apiBaseUrl] — written by `remixmate login`
|
|
13
|
+
* (none) → throws SkillError — non-interactive failure
|
|
12
14
|
*
|
|
13
15
|
* Base URL resolution:
|
|
14
16
|
* 1. opts.apiBaseUrl — caller override
|
|
@@ -19,6 +21,8 @@ export declare class SkillError extends Error {
|
|
|
19
21
|
readonly exitCode: number;
|
|
20
22
|
constructor(message: string, exitCode?: number);
|
|
21
23
|
}
|
|
24
|
+
/** Resolve the ab-api base URL the same way for authenticated and device-flow calls. */
|
|
25
|
+
export declare function resolveApiBaseUrl(flag?: string): string;
|
|
22
26
|
export interface HttpContext {
|
|
23
27
|
apiBaseUrl: string;
|
|
24
28
|
privateToken: string;
|
|
@@ -29,7 +33,7 @@ export interface HttpContext {
|
|
|
29
33
|
export declare function resolveHttpContext(skillName: string, opts?: {
|
|
30
34
|
apiBaseUrl?: string;
|
|
31
35
|
privateToken?: string;
|
|
32
|
-
}): HttpContext
|
|
36
|
+
}): Promise<HttpContext>;
|
|
33
37
|
export interface MmResponse<T = unknown> {
|
|
34
38
|
code: number;
|
|
35
39
|
msg?: string;
|
package/dist/http.js
CHANGED
|
@@ -5,16 +5,20 @@
|
|
|
5
5
|
* x-conversation-id), business-code error handling (result.code != 0), and the
|
|
6
6
|
* env var resolution order that every Python skill used to duplicate.
|
|
7
7
|
*
|
|
8
|
-
* Token resolution order (high → low)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Token resolution order (high → low), shared with the CLI auth commands via
|
|
9
|
+
* src/auth/resolve.ts:
|
|
10
|
+
* 1. opts.privateToken — caller override (maps to --token/--priv-token)
|
|
11
|
+
* 2. process.env.PRIV_TOKEN — process env (CI / agent-embedded hosts)
|
|
12
|
+
* 3. Credential_Store[apiBaseUrl] — written by `remixmate login`
|
|
13
|
+
* (none) → throws SkillError — non-interactive failure
|
|
12
14
|
*
|
|
13
15
|
* Base URL resolution:
|
|
14
16
|
* 1. opts.apiBaseUrl — caller override
|
|
15
17
|
* 2. process.env.MM_API_BASE_URL — process env
|
|
16
18
|
* 3. http://localhost:2999/api — local dev fallback
|
|
17
19
|
*/
|
|
20
|
+
import { resolvePrivToken, NOT_AUTHENTICATED_HINT } from './auth/resolve.js';
|
|
21
|
+
import { attemptAutoLogin } from './auth/auto-login.js';
|
|
18
22
|
const DEFAULT_API_BASE_URL = 'http://localhost:2999/api';
|
|
19
23
|
export class SkillError extends Error {
|
|
20
24
|
exitCode;
|
|
@@ -24,15 +28,25 @@ export class SkillError extends Error {
|
|
|
24
28
|
this.name = 'SkillError';
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/** Resolve the ab-api base URL the same way for authenticated and device-flow calls. */
|
|
32
|
+
export function resolveApiBaseUrl(flag) {
|
|
33
|
+
return (flag ?? process.env.MM_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, '');
|
|
34
|
+
}
|
|
35
|
+
export async function resolveHttpContext(skillName, opts = {}) {
|
|
36
|
+
const apiBaseUrl = resolveApiBaseUrl(opts.apiBaseUrl);
|
|
37
|
+
let resolved = await resolvePrivToken({ flag: opts.privateToken, apiBaseUrl });
|
|
38
|
+
// Only when flag/env/store are all empty do we try automatic browser auth.
|
|
39
|
+
// attemptAutoLogin self-gates (cloud env / headless / disabled → returns null
|
|
40
|
+
// immediately), so the env-injected path never reaches it (zero regression).
|
|
41
|
+
if (!resolved) {
|
|
42
|
+
resolved = await attemptAutoLogin(apiBaseUrl);
|
|
43
|
+
}
|
|
44
|
+
if (!resolved) {
|
|
45
|
+
throw new SkillError(NOT_AUTHENTICATED_HINT);
|
|
32
46
|
}
|
|
33
47
|
return {
|
|
34
48
|
apiBaseUrl,
|
|
35
|
-
privateToken,
|
|
49
|
+
privateToken: resolved.privToken,
|
|
36
50
|
skillName,
|
|
37
51
|
agentName: process.env.AGENT_NAME || undefined,
|
|
38
52
|
conversationId: process.env.CONVERSATION_ID || undefined,
|
package/dist/manifest.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.
|
|
4
|
-
"generatedAt": "2026-06-
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"generatedAt": "2026-06-13T02:35:04.526Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "export-jianying",
|
|
8
8
|
"tier": "orchestration",
|
|
9
|
+
"category": "consuming",
|
|
9
10
|
"title": "Jianying (CapCut) Draft Export",
|
|
10
11
|
"summary": "Jianying (CapCut) draft export: package asset URLs into a draft ZIP that Jianying can import. Supports automatic conversion from a RenderPlan.",
|
|
11
12
|
"triggers": [
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
{
|
|
29
30
|
"id": "gen-digital-human",
|
|
30
31
|
"tier": "atomic",
|
|
32
|
+
"category": "asset",
|
|
31
33
|
"title": "Digital-Human Talking-Head",
|
|
32
34
|
"summary": "Digital-human video: list available avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL.",
|
|
33
35
|
"triggers": [
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
{
|
|
51
53
|
"id": "gen-image",
|
|
52
54
|
"tier": "atomic",
|
|
55
|
+
"category": "asset",
|
|
53
56
|
"title": "AI Image Generation",
|
|
54
57
|
"summary": "AI image generation: produce an image from a text prompt. Supports Seedream and Gemini models, plus image-to-image with reference images.",
|
|
55
58
|
"triggers": [
|
|
@@ -73,6 +76,7 @@
|
|
|
73
76
|
{
|
|
74
77
|
"id": "gen-script",
|
|
75
78
|
"tier": "orchestration",
|
|
79
|
+
"category": "authoring",
|
|
76
80
|
"title": "Video Script Generation",
|
|
77
81
|
"summary": "Video script generation: turn a topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.",
|
|
78
82
|
"triggers": [
|
|
@@ -95,6 +99,7 @@
|
|
|
95
99
|
{
|
|
96
100
|
"id": "gen-video",
|
|
97
101
|
"tier": "atomic",
|
|
102
|
+
"category": "asset",
|
|
98
103
|
"title": "AI Video Generation",
|
|
99
104
|
"summary": "AI video generation: produce a short video clip from a text prompt. Supports Seedance and Veo models, plus first/last frame and reference images.",
|
|
100
105
|
"triggers": [
|
|
@@ -117,6 +122,7 @@
|
|
|
117
122
|
{
|
|
118
123
|
"id": "gen-voice",
|
|
119
124
|
"tier": "atomic",
|
|
125
|
+
"category": "asset",
|
|
120
126
|
"title": "Text-to-Speech (Minimax)",
|
|
121
127
|
"summary": "Text-to-speech (TTS): synthesize narration audio from text via the Minimax TTS model. Returns the persisted audio URL — no download needed.",
|
|
122
128
|
"triggers": [
|
|
@@ -138,6 +144,7 @@
|
|
|
138
144
|
{
|
|
139
145
|
"id": "prepare-video-assets",
|
|
140
146
|
"tier": "orchestration",
|
|
147
|
+
"category": "authoring",
|
|
141
148
|
"title": "Video Asset Preparation",
|
|
142
149
|
"summary": "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.",
|
|
143
150
|
"triggers": [
|
|
@@ -165,6 +172,7 @@
|
|
|
165
172
|
{
|
|
166
173
|
"id": "render-video",
|
|
167
174
|
"tier": "orchestration",
|
|
175
|
+
"category": "authoring",
|
|
168
176
|
"title": "Remotion Video Renderer",
|
|
169
177
|
"summary": "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.",
|
|
170
178
|
"triggers": [
|
|
@@ -192,6 +200,7 @@
|
|
|
192
200
|
{
|
|
193
201
|
"id": "template-registry",
|
|
194
202
|
"tier": "orchestration",
|
|
203
|
+
"category": "authoring",
|
|
195
204
|
"title": "Video Template Registry",
|
|
196
205
|
"summary": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
197
206
|
"triggers": [
|
|
@@ -204,14 +213,15 @@
|
|
|
204
213
|
"runtime": "python",
|
|
205
214
|
"envVars": [
|
|
206
215
|
"PRIV_TOKEN",
|
|
207
|
-
"VIDEO_TEMPLATE_REGISTRY",
|
|
208
216
|
"VIDEO_TEMPLATE_REGISTRY_URL",
|
|
217
|
+
"MM_API_BASE_URL",
|
|
209
218
|
"VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD"
|
|
210
219
|
]
|
|
211
220
|
},
|
|
212
221
|
{
|
|
213
222
|
"id": "video-parser",
|
|
214
223
|
"tier": "tool",
|
|
224
|
+
"category": "consuming",
|
|
215
225
|
"title": "Video Deconstruction",
|
|
216
226
|
"summary": "Video deconstruction: split a video into reusable assets (audio extraction, ASR transcript, keyframe extraction, scene segmentation).",
|
|
217
227
|
"triggers": [
|
|
@@ -233,6 +243,7 @@
|
|
|
233
243
|
{
|
|
234
244
|
"id": "web-capture",
|
|
235
245
|
"tier": "tool",
|
|
246
|
+
"category": "consuming",
|
|
236
247
|
"title": "Web Page Capture",
|
|
237
248
|
"summary": "Drive a headless browser (Playwright Python) to capture any URL to local files: full-page / element / region screenshots, fixed-duration / condition-triggered / auto-scroll recordings, and storyboard videos (highlight / focus / zoom / scroll / virtual-cursor click / caption / title-card / arrow / numbered sequence / redact / code-line highlight). Two entry scripts: scripts/screenshot.py (stills) and scripts/record.py (webm video, storyboards, templates).",
|
|
238
249
|
"triggers": [
|