@remixmate/cli 0.9.71 → 0.9.73
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/LICENSE +45 -21
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/argv.js +1 -51
- package/dist/auth/auth-lock.js +1 -100
- package/dist/auth/auto-login.js +7 -75
- package/dist/auth/commands.js +40 -290
- package/dist/auth/credential-store.js +1 -143
- package/dist/auth/device-flow-runner.js +13 -110
- package/dist/auth/device-flow.js +1 -132
- package/dist/auth/ensure.js +1 -62
- package/dist/auth/environment.js +1 -69
- package/dist/auth/pending-store.js +1 -45
- package/dist/auth/resolve.js +1 -44
- package/dist/billing.js +3 -76
- package/dist/capabilities.js +1 -75
- package/dist/cli.js +39 -211
- package/dist/diagnostics.js +2 -70
- package/dist/doctor.js +9 -188
- package/dist/errors.js +1 -33
- package/dist/exec.js +4 -47
- package/dist/handlers/gen-digital-human.js +33 -264
- package/dist/handlers/gen-image.js +9 -113
- package/dist/handlers/gen-post.js +15 -188
- package/dist/handlers/gen-video.js +7 -185
- package/dist/handlers/gen-voice.js +19 -133
- package/dist/handlers/index.js +1 -20
- package/dist/handlers/shared.js +1 -66
- package/dist/handlers/template-init.js +1 -21
- package/dist/http.js +3 -155
- package/dist/index.js +1 -8
- package/dist/install.js +40 -448
- package/dist/manifest.json +53 -13
- package/dist/progress.js +2 -15
- package/dist/project/commands.js +34 -156
- package/dist/project/host.js +1 -151
- package/dist/project/resolve.js +1 -65
- package/dist/project/store.js +2 -97
- package/dist/project/take.js +1 -106
- package/dist/project/trace.js +5 -278
- package/dist/registry.js +3 -70
- package/dist/runner.js +7 -225
- package/dist/skill-schema.js +1 -194
- package/dist/template/args.js +1 -69
- package/dist/template/check.js +2 -30
- package/dist/template/commands.js +34 -163
- package/dist/template/constants.js +1 -46
- package/dist/template/init.js +5 -133
- package/dist/template/list.js +1 -46
- package/dist/template/preview.js +7 -318
- package/dist/template/proc.js +1 -43
- package/dist/template/project.js +6 -161
- package/dist/template/publish.js +5 -185
- package/dist/template/render-client.js +2 -79
- package/dist/template/upgrade.js +4 -123
- package/dist/text.js +1 -15
- package/package.json +2 -35
- package/skills/doc-parse/scripts/extract_via_render.py +32 -32
- package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
- package/skills/gen-post/SKILL.md +23 -13
- package/skills/gen-post/skill.json +9 -10
- package/skills/gen-script/SKILL.md +23 -1
- package/skills/gen-script/scripts/gen_script.py +644 -551
- package/skills/gen-script/skill.json +12 -0
- package/skills/gen-script/version.json +1 -1
- package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
- package/skills/render-video/scripts/_chrome_vendor.py +17 -17
- package/skills/render-video/scripts/_digital_human_segments.py +34 -34
- package/skills/render-video/scripts/_video_probe.py +33 -33
- package/skills/render-video/scripts/_vod_polling.py +13 -13
- package/skills/render-video/scripts/remote_renderer_client.py +42 -42
- package/skills/render-video/scripts/render_video.py +639 -639
- package/skills/render-video/scripts/required_props.py +37 -37
- package/skills/render-video/scripts/upload_video.py +35 -35
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +16 -0
- package/skills/template-registry/scripts/countdown_spec.py +92 -92
- package/skills/template-registry/scripts/doc_extract.py +40 -40
- package/skills/template-registry/scripts/list_templates.py +303 -191
- package/skills/template-registry/scripts/log_diagnostics.py +1 -1
- package/skills/template-registry/scripts/match_template.py +87 -87
- package/skills/template-registry/scripts/payload_hydrator.py +89 -89
- package/skills/template-registry/scripts/registry_loader.py +159 -159
- package/skills/template-registry/scripts/render_job_client.py +75 -75
- package/skills/template-registry/scripts/showcase_loader.py +44 -44
- package/skills/template-registry/skill.json +4 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
- package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
- package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
- package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
- package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
- package/skills/video-parser/scripts/analyze_video.py +173 -173
- package/skills/video-parser/scripts/deconstruct_video.py +91 -91
- package/skills/video-parser/scripts/parse_via_render.py +21 -21
- package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
- package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
- package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
- package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
- package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
- package/skills/web-screenshot/scripts/_media_upload.py +33 -33
- package/skills/web-screenshot/scripts/read_page.py +15 -15
- package/skills/web-screenshot/scripts/record.py +26 -26
- package/skills/web-screenshot/scripts/screenshot.py +5 -5
- package/dist/argv.d.ts +0 -15
- package/dist/auth/auth-lock.d.ts +0 -26
- package/dist/auth/auto-login.d.ts +0 -20
- package/dist/auth/commands.d.ts +0 -8
- package/dist/auth/credential-store.d.ts +0 -60
- package/dist/auth/device-flow-runner.d.ts +0 -58
- package/dist/auth/device-flow.d.ts +0 -52
- package/dist/auth/ensure.d.ts +0 -55
- package/dist/auth/environment.d.ts +0 -38
- package/dist/auth/pending-store.d.ts +0 -33
- package/dist/auth/resolve.d.ts +0 -30
- package/dist/billing.d.ts +0 -44
- package/dist/capabilities.d.ts +0 -63
- package/dist/cli.d.ts +0 -13
- package/dist/diagnostics.d.ts +0 -24
- package/dist/doctor.d.ts +0 -22
- package/dist/errors.d.ts +0 -29
- package/dist/exec.d.ts +0 -18
- package/dist/handlers/gen-digital-human.d.ts +0 -17
- package/dist/handlers/gen-image.d.ts +0 -20
- package/dist/handlers/gen-post.d.ts +0 -21
- package/dist/handlers/gen-video.d.ts +0 -19
- package/dist/handlers/gen-voice.d.ts +0 -26
- package/dist/handlers/index.d.ts +0 -20
- package/dist/handlers/shared.d.ts +0 -26
- package/dist/handlers/template-init.d.ts +0 -10
- package/dist/http.d.ts +0 -72
- package/dist/index.d.ts +0 -9
- package/dist/install.d.ts +0 -71
- package/dist/progress.d.ts +0 -23
- package/dist/project/commands.d.ts +0 -19
- package/dist/project/host.d.ts +0 -91
- package/dist/project/resolve.d.ts +0 -43
- package/dist/project/store.d.ts +0 -31
- package/dist/project/take.d.ts +0 -50
- package/dist/project/trace.d.ts +0 -98
- package/dist/registry.d.ts +0 -30
- package/dist/runner.d.ts +0 -39
- package/dist/skill-schema.d.ts +0 -138
- package/dist/template/args.d.ts +0 -25
- package/dist/template/check.d.ts +0 -23
- package/dist/template/commands.d.ts +0 -25
- package/dist/template/constants.d.ts +0 -39
- package/dist/template/init.d.ts +0 -28
- package/dist/template/list.d.ts +0 -23
- package/dist/template/preview.d.ts +0 -60
- package/dist/template/proc.d.ts +0 -26
- package/dist/template/project.d.ts +0 -47
- package/dist/template/publish.d.ts +0 -43
- package/dist/template/render-client.d.ts +0 -39
- package/dist/template/upgrade.d.ts +0 -44
- package/dist/text.d.ts +0 -8
|
@@ -1,185 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* the CLI carries no model matrix — and ab-api re-validates authoritatively.
|
|
9
|
-
*
|
|
10
|
-
* --model accepts a friendly alias or a full id; ab-api also resolves aliases
|
|
11
|
-
* server-side, so an unknown value still passes through to the backend.
|
|
12
|
-
*
|
|
13
|
-
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
14
|
-
* into a data: URI; https / data: URIs pass through unchanged. Reference videos
|
|
15
|
-
* and audio are the exception — they must already be public URLs, see
|
|
16
|
-
* `requirePublicUrls`.
|
|
17
|
-
*/
|
|
18
|
-
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
19
|
-
import { getCapabilities, matchModel, defaultModel } from '../capabilities.js';
|
|
20
|
-
import { emitProgress } from '../progress.js';
|
|
21
|
-
import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
|
|
22
|
-
/** Generic, catalog-driven validation. No model names live here. */
|
|
23
|
-
function validateAgainstConstraints(descriptor, duration, ratio, resolution, refs) {
|
|
24
|
-
const c = descriptor.constraints;
|
|
25
|
-
const label = descriptor.label || descriptor.id;
|
|
26
|
-
if (c.durations && c.durations.length > 0) {
|
|
27
|
-
if (!c.durations.includes(duration)) {
|
|
28
|
-
throw new SkillError(`❌ ${label} duration must be one of: ${c.durations.join(', ')} s; got: ${duration}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
else if (c.durationMin !== undefined && c.durationMax !== undefined) {
|
|
32
|
-
if (duration < c.durationMin || duration > c.durationMax) {
|
|
33
|
-
throw new SkillError(`❌ ${label} duration must be ${c.durationMin}–${c.durationMax} s, got: ${duration}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (c.aspectRatios && c.aspectRatios.length > 0 && !c.aspectRatios.includes(ratio)) {
|
|
37
|
-
throw new SkillError(`❌ ${label} aspect ratio must be one of: ${c.aspectRatios.join(', ')}; got: ${ratio}`);
|
|
38
|
-
}
|
|
39
|
-
if (c.resolutions && c.resolutions.length > 0 && !c.resolutions.includes(resolution)) {
|
|
40
|
-
throw new SkillError(`❌ ${label} resolution must be one of: ${c.resolutions.join(', ')}; got: ${resolution}`);
|
|
41
|
-
}
|
|
42
|
-
if (c.refImageMax !== undefined && refs.images > c.refImageMax) {
|
|
43
|
-
if (c.refImageMax === 0) {
|
|
44
|
-
throw new SkillError(`❌ ${label} does not support a reference-image array; use --first-frame / --last-frame instead`);
|
|
45
|
-
}
|
|
46
|
-
throw new SkillError(`❌ ${label} accepts at most ${c.refImageMax} reference images; got: ${refs.images}`);
|
|
47
|
-
}
|
|
48
|
-
if (c.refVideoMax !== undefined && refs.videos > c.refVideoMax) {
|
|
49
|
-
throw new SkillError(`❌ ${label} accepts at most ${c.refVideoMax} reference videos; got: ${refs.videos}`);
|
|
50
|
-
}
|
|
51
|
-
if (c.refAudioMax !== undefined && refs.audios > c.refAudioMax) {
|
|
52
|
-
throw new SkillError(`❌ ${label} accepts at most ${c.refAudioMax} reference audio clips; got: ${refs.audios}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Reference videos / audio must be public https URLs.
|
|
57
|
-
*
|
|
58
|
-
* Unlike images, they are NOT inlined as data URIs: the caps are 50 MB per video
|
|
59
|
-
* and 15 MB per clip, and base64 inflates that by a third before it ever reaches
|
|
60
|
-
* the gateway. Failing here with the offending path beats a truncated upload or
|
|
61
|
-
* an opaque 413 three hops downstream.
|
|
62
|
-
*/
|
|
63
|
-
function requirePublicUrls(values, flag) {
|
|
64
|
-
return values.map((value) => {
|
|
65
|
-
const url = value.trim();
|
|
66
|
-
if (!/^https?:\/\//i.test(url)) {
|
|
67
|
-
throw new SkillError(`❌ ${flag} needs a public http(s) URL (local files are not uploaded), received: ${value}`);
|
|
68
|
-
}
|
|
69
|
-
return url;
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
async function pollVideoStatus(ctx, taskId) {
|
|
73
|
-
return pollUntil(async () => {
|
|
74
|
-
const data = await mmPost(ctx, '/model/getVideoStatus', { taskId }, { timeoutMs: 30_000 });
|
|
75
|
-
const status = data.status ?? 'unknown';
|
|
76
|
-
emitProgress({ phase: 'gen-video:poll', taskId, status });
|
|
77
|
-
if (status === 'succeeded' || status === 'completed') {
|
|
78
|
-
return data.videoUrl ?? '';
|
|
79
|
-
}
|
|
80
|
-
if (status === 'failed') {
|
|
81
|
-
const err = data.errorMsg ?? data.error ?? data.message ?? 'unknown error';
|
|
82
|
-
throw new SkillError(`❌ video generation failed: ${err}`);
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}, { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
86
|
-
}
|
|
87
|
-
export async function genVideo(input, ctxIn) {
|
|
88
|
-
const apiBaseUrl = input.api_base_url;
|
|
89
|
-
const caps = await getCapabilities(apiBaseUrl);
|
|
90
|
-
// Resolve the model against the catalog: explicit alias/id, env override, or default.
|
|
91
|
-
const requested = input.model ?? process.env.MM_VIDEO_MODEL;
|
|
92
|
-
const descriptor = requested ? matchModel(caps.video, requested) : defaultModel(caps.video);
|
|
93
|
-
const model = descriptor?.id ?? requested ?? defaultModel(caps.video)?.id ?? '';
|
|
94
|
-
const firstFrame = input.first_frame ? resolveImageInput(String(input.first_frame)) : '';
|
|
95
|
-
const lastFrame = input.last_frame ? resolveImageInput(String(input.last_frame)) : '';
|
|
96
|
-
const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
|
|
97
|
-
const referenceVideos = requirePublicUrls(toStringArray(input.reference_video).filter(Boolean), '--reference-video');
|
|
98
|
-
const referenceAudios = requirePublicUrls(toStringArray(input.reference_audio).filter(Boolean), '--reference-audio');
|
|
99
|
-
const prompt = (input.prompt ?? '').trim();
|
|
100
|
-
const hasVisual = Boolean(firstFrame || lastFrame || references.length > 0 || referenceVideos.length > 0);
|
|
101
|
-
if (!prompt && !hasVisual) {
|
|
102
|
-
throw new SkillError('❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference / --reference-video');
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Two shapes the backend rejects outright; catching them here saves a round
|
|
106
|
-
* trip and names the flag that caused it.
|
|
107
|
-
*
|
|
108
|
-
* - first/last frame pins exact frames, omni-modal reference lets the model
|
|
109
|
-
* re-cut — the API has no meaning for both at once;
|
|
110
|
-
* - audio-only (or text + audio) input is not a supported modality mix.
|
|
111
|
-
*/
|
|
112
|
-
if ((firstFrame || lastFrame) && (referenceVideos.length > 0 || referenceAudios.length > 0)) {
|
|
113
|
-
throw new SkillError('❌ --reference-video / --reference-audio cannot be combined with --first-frame / --last-frame; use --reference for images instead');
|
|
114
|
-
}
|
|
115
|
-
if (referenceAudios.length > 0 && references.length === 0 && referenceVideos.length === 0) {
|
|
116
|
-
throw new SkillError('❌ --reference-audio needs at least one --reference image or --reference-video alongside it');
|
|
117
|
-
}
|
|
118
|
-
const ratio = input.ratio ?? '16:9';
|
|
119
|
-
const resolution = input.resolution ?? '720p';
|
|
120
|
-
const duration = toNumber(input.duration, 'duration') ?? descriptor?.constraints.durationDefault ?? 5;
|
|
121
|
-
const seed = toNumber(input.seed, 'seed');
|
|
122
|
-
// Pre-flight validation only when we recognize the model (have its constraints).
|
|
123
|
-
if (descriptor) {
|
|
124
|
-
validateAgainstConstraints(descriptor, duration, ratio, resolution, {
|
|
125
|
-
images: references.length,
|
|
126
|
-
videos: referenceVideos.length,
|
|
127
|
-
audios: referenceAudios.length,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
131
|
-
apiBaseUrl,
|
|
132
|
-
privateToken: input.priv_token,
|
|
133
|
-
preflightToken: ctxIn.auth?.token,
|
|
134
|
-
});
|
|
135
|
-
const payload = {
|
|
136
|
-
model,
|
|
137
|
-
duration,
|
|
138
|
-
ratio,
|
|
139
|
-
resolution,
|
|
140
|
-
generateAudio: isTrue(input.generate_audio),
|
|
141
|
-
cameraFixed: isTrue(input.camera_fixed),
|
|
142
|
-
};
|
|
143
|
-
if (prompt)
|
|
144
|
-
payload.prompt = prompt;
|
|
145
|
-
if (firstFrame)
|
|
146
|
-
payload.firstFrameImage = firstFrame;
|
|
147
|
-
if (lastFrame)
|
|
148
|
-
payload.lastFrameImage = lastFrame;
|
|
149
|
-
if (references.length > 0)
|
|
150
|
-
payload.referenceImages = references;
|
|
151
|
-
if (referenceVideos.length > 0)
|
|
152
|
-
payload.referenceVideos = referenceVideos;
|
|
153
|
-
if (referenceAudios.length > 0)
|
|
154
|
-
payload.referenceAudios = referenceAudios;
|
|
155
|
-
// Web search only fires on text-only input; sending it alongside attachments
|
|
156
|
-
// would be a knob the model silently ignores.
|
|
157
|
-
if (isTrue(input.web_search) && !hasVisual)
|
|
158
|
-
payload.webSearch = true;
|
|
159
|
-
if (isTrue(input.return_last_frame))
|
|
160
|
-
payload.returnLastFrame = true;
|
|
161
|
-
if (input.negative_prompt && String(input.negative_prompt).trim()) {
|
|
162
|
-
payload.negativePrompt = String(input.negative_prompt).trim();
|
|
163
|
-
}
|
|
164
|
-
if (seed !== undefined)
|
|
165
|
-
payload.seed = seed;
|
|
166
|
-
if (input.person_generation)
|
|
167
|
-
payload.personGeneration = String(input.person_generation);
|
|
168
|
-
emitProgress({ phase: 'gen-video:request', model, ratio, resolution, duration });
|
|
169
|
-
const data = await mmPost(ctx, '/model/genVideo', payload, { timeoutMs: 120_000 });
|
|
170
|
-
const taskId = data.taskId;
|
|
171
|
-
if (!taskId) {
|
|
172
|
-
throw new SkillError(`❌ could not parse the task id; API returned: ${JSON.stringify(data)}`);
|
|
173
|
-
}
|
|
174
|
-
emitProgress({ phase: 'gen-video:async', taskId });
|
|
175
|
-
const videoUrl = await pollVideoStatus(ctx, taskId);
|
|
176
|
-
if (!videoUrl) {
|
|
177
|
-
throw new SkillError(`❌ video generated but no URL was returned, task id: ${taskId}`);
|
|
178
|
-
}
|
|
179
|
-
if (isTrue(input.json_output)) {
|
|
180
|
-
process.stdout.write(JSON.stringify({ url: videoUrl }) + '\n');
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
process.stdout.write(`\n🔗 Video URL (renders directly, no download needed):\n${videoUrl}\n`);
|
|
184
|
-
process.stdout.write(`\n🎉 Video generation done!\n`);
|
|
185
|
-
}
|
|
1
|
+
var P=Object.defineProperty;var m=(e,o)=>P(e,"name",{value:o,configurable:!0});import{mmPost as A,pollUntil as B,resolveHttpContext as L,SkillError as r}from"../http.js";import{getCapabilities as j,matchModel as N,defaultModel as U}from"../capabilities.js";import{emitProgress as x}from"../progress.js";import{isTrue as u,resolveImageInput as b,toNumber as R,toStringArray as V}from"./shared.js";function O(e,o,n,i,a){const t=e.constraints,d=e.label||e.id;if(t.durations&&t.durations.length>0){if(!t.durations.includes(o))throw new r(`❌ ${d} duration must be one of: ${t.durations.join(", ")} s; got: ${o}`)}else if(t.durationMin!==void 0&&t.durationMax!==void 0&&(o<t.durationMin||o>t.durationMax))throw new r(`❌ ${d} duration must be ${t.durationMin}–${t.durationMax} s, got: ${o}`);if(t.aspectRatios&&t.aspectRatios.length>0&&!t.aspectRatios.includes(n))throw new r(`❌ ${d} aspect ratio must be one of: ${t.aspectRatios.join(", ")}; got: ${n}`);if(t.resolutions&&t.resolutions.length>0&&!t.resolutions.includes(i))throw new r(`❌ ${d} resolution must be one of: ${t.resolutions.join(", ")}; got: ${i}`);if(t.refImageMax!==void 0&&a.images>t.refImageMax)throw t.refImageMax===0?new r(`❌ ${d} does not support a reference-image array; use --first-frame / --last-frame instead`):new r(`❌ ${d} accepts at most ${t.refImageMax} reference images; got: ${a.images}`);if(t.refVideoMax!==void 0&&a.videos>t.refVideoMax)throw new r(`❌ ${d} accepts at most ${t.refVideoMax} reference videos; got: ${a.videos}`);if(t.refAudioMax!==void 0&&a.audios>t.refAudioMax)throw new r(`❌ ${d} accepts at most ${t.refAudioMax} reference audio clips; got: ${a.audios}`)}m(O,"validateAgainstConstraints");function F(e,o){return e.map(n=>{const i=n.trim();if(!/^https?:\/\//i.test(i))throw new r(`❌ ${o} needs a public http(s) URL (local files are not uploaded), received: ${n}`);return i})}m(F,"requirePublicUrls");async function q(e,o){return B(async()=>{const n=await A(e,"/model/getVideoStatus",{taskId:o},{timeoutMs:3e4}),i=n.status??"unknown";if(x({phase:"gen-video:poll",taskId:o,status:i}),i==="succeeded"||i==="completed")return n.videoUrl??"";if(i==="failed"){const a=n.errorMsg??n.error??n.message??"unknown error";throw new r(`❌ video generation failed: ${a}`)}return null},{intervalMs:1e4,timeoutMs:3e5})}m(q,"pollVideoStatus");async function G(e,o){const n=e.api_base_url,i=await j(n),a=e.model??process.env.MM_VIDEO_MODEL,t=a?N(i.video,a):U(i.video),d=t?.id??a??U(i.video)?.id??"",g=e.first_frame?b(String(e.first_frame)):"",w=e.last_frame?b(String(e.last_frame)):"",c=V(e.reference).map(b).filter(Boolean),f=F(V(e.reference_video).filter(Boolean),"--reference-video"),l=F(V(e.reference_audio).filter(Boolean),"--reference-audio"),p=(e.prompt??"").trim(),S=!!(g||w||c.length>0||f.length>0);if(!p&&!S)throw new r("❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference / --reference-video");if((g||w)&&(f.length>0||l.length>0))throw new r("❌ --reference-video / --reference-audio cannot be combined with --first-frame / --last-frame; use --reference for images instead");if(l.length>0&&c.length===0&&f.length===0)throw new r("❌ --reference-audio needs at least one --reference image or --reference-video alongside it");const v=e.ratio??"16:9",$=e.resolution??"720p",M=R(e.duration,"duration")??t?.constraints.durationDefault??5,k=R(e.seed,"seed");t&&O(t,M,v,$,{images:c.length,videos:f.length,audios:l.length});const y=await L(o.skillName,{apiBaseUrl:n,privateToken:e.priv_token,preflightToken:o.auth?.token}),s={model:d,duration:M,ratio:v,resolution:$,generateAudio:u(e.generate_audio),cameraFixed:u(e.camera_fixed)};p&&(s.prompt=p),g&&(s.firstFrameImage=g),w&&(s.lastFrameImage=w),c.length>0&&(s.referenceImages=c),f.length>0&&(s.referenceVideos=f),l.length>0&&(s.referenceAudios=l),u(e.web_search)&&!S&&(s.webSearch=!0),u(e.return_last_frame)&&(s.returnLastFrame=!0),e.negative_prompt&&String(e.negative_prompt).trim()&&(s.negativePrompt=String(e.negative_prompt).trim()),k!==void 0&&(s.seed=k),e.person_generation&&(s.personGeneration=String(e.person_generation)),x({phase:"gen-video:request",model:d,ratio:v,resolution:$,duration:M});const I=await A(y,"/model/genVideo",s,{timeoutMs:12e4}),h=I.taskId;if(!h)throw new r(`❌ could not parse the task id; API returned: ${JSON.stringify(I)}`);x({phase:"gen-video:async",taskId:h});const _=await q(y,h);if(!_)throw new r(`❌ video generated but no URL was returned, task id: ${h}`);if(u(e.json_output)){process.stdout.write(JSON.stringify({url:_})+`
|
|
2
|
+
`);return}process.stdout.write(`
|
|
3
|
+
🔗 Video URL (renders directly, no download needed):
|
|
4
|
+
${_}
|
|
5
|
+
`),process.stdout.write(`
|
|
6
|
+
🎉 Video generation done!
|
|
7
|
+
`)}m(G,"genVideo");export{G as genVideo};
|
|
@@ -1,133 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* The previous Python script also offered `--download` to write the MP3 to
|
|
21
|
-
* disk. That's intentionally dropped here: the agent flow never used it (the
|
|
22
|
-
* audio URL is persisted in cloud) and removing local file I/O keeps the
|
|
23
|
-
* handler stateless.
|
|
24
|
-
*/
|
|
25
|
-
import { mmPost, resolveHttpContext, SkillError } from '../http.js';
|
|
26
|
-
import { getCapabilities } from '../capabilities.js';
|
|
27
|
-
import { emitProgress } from '../progress.js';
|
|
28
|
-
import { isTrue, toNumber } from './shared.js';
|
|
29
|
-
/**
|
|
30
|
-
* Fallback voice catalog printed when `--list-voices --local` is passed.
|
|
31
|
-
* Mirrors the language-keyed defaults that gen_script.py uses, kept in sync
|
|
32
|
-
* by hand because the two callers live on different runtimes.
|
|
33
|
-
*
|
|
34
|
-
* This is **not** a list of available voices — it is the two ids the resolver
|
|
35
|
-
* falls back to when nothing else is specified, and one of them is still a
|
|
36
|
-
* placeholder. Never treat it as evidence about what the service offers.
|
|
37
|
-
*
|
|
38
|
-
* Promote to a JSON config once this grows beyond a handful of entries.
|
|
39
|
-
*/
|
|
40
|
-
const LOCAL_VOICE_CATALOG = [
|
|
41
|
-
{ id: 'Chinese (Mandarin)_Male_Announcer', language: 'zh', name: 'Mandarin Male Announcer (legacy default)' },
|
|
42
|
-
// TODO: replace once gen-voice service confirms an English default voice id.
|
|
43
|
-
{ id: 'english-male-friendly-01', language: 'en', name: 'Friendly Male (English, TBD)' },
|
|
44
|
-
];
|
|
45
|
-
async function listVoicesRemote(ctx) {
|
|
46
|
-
const data = await mmPost(ctx, '/voice/page', { current: 1, pageSize: 100 }, { timeoutMs: 30_000 });
|
|
47
|
-
const voices = data.list ?? [];
|
|
48
|
-
if (voices.length === 0) {
|
|
49
|
-
process.stdout.write('⚠️ No voices available\n');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
process.stdout.write(`\n${'Voice ID'.padEnd(30)} Name\n`);
|
|
53
|
-
process.stdout.write(`${'-'.repeat(30)} ${'-'.repeat(20)}\n`);
|
|
54
|
-
for (const v of voices) {
|
|
55
|
-
process.stdout.write(`${(v.uniqId ?? '').padEnd(30)} ${v.name ?? ''}\n`);
|
|
56
|
-
}
|
|
57
|
-
process.stdout.write(`\n${voices.length} voice(s) available\n`);
|
|
58
|
-
process.stdout.write(`💡 Use --voice-id <id> to select a voice for synthesis\n`);
|
|
59
|
-
}
|
|
60
|
-
function listVoicesLocal() {
|
|
61
|
-
process.stderr.write('⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. ' +
|
|
62
|
-
'Drop --local to query the live catalog.\n');
|
|
63
|
-
for (const v of LOCAL_VOICE_CATALOG) {
|
|
64
|
-
process.stdout.write(`${v.id}\t${v.language}\t${v.name}\n`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
export async function genVoice(input, ctxIn) {
|
|
68
|
-
// ── --list-voices: catalog mode (no synthesis) ──────────────────────────
|
|
69
|
-
if (isTrue(input.list_voices)) {
|
|
70
|
-
if (isTrue(input.local))
|
|
71
|
-
return listVoicesLocal();
|
|
72
|
-
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
73
|
-
apiBaseUrl: input.api_base_url,
|
|
74
|
-
privateToken: input.priv_token,
|
|
75
|
-
preflightToken: ctxIn.auth?.token,
|
|
76
|
-
});
|
|
77
|
-
return listVoicesRemote(ctx);
|
|
78
|
-
}
|
|
79
|
-
// ── synthesize ──────────────────────────────────────────────────────────
|
|
80
|
-
const text = input.text;
|
|
81
|
-
if (!text || typeof text !== 'string' || !text.trim()) {
|
|
82
|
-
throw new SkillError('❌ Please provide --text to specify the text to synthesize');
|
|
83
|
-
}
|
|
84
|
-
const apiBaseUrl = input.api_base_url;
|
|
85
|
-
const voice = (await getCapabilities(apiBaseUrl)).voice;
|
|
86
|
-
const voiceId = input.voice_id ?? voice.defaultVoiceId;
|
|
87
|
-
const speed = toNumber(input.speed, 'speed') ?? 1.0;
|
|
88
|
-
if (speed < voice.speedMin || speed > voice.speedMax) {
|
|
89
|
-
throw new SkillError(`❌ speed must be in the range ${voice.speedMin}~${voice.speedMax}`);
|
|
90
|
-
}
|
|
91
|
-
// Language hint for mixed-script narration. Like `local`, this is deliberately
|
|
92
|
-
// absent from skill.json's `parameters`: the service already applies a sane
|
|
93
|
-
// default for every caller, so handing the model a 40-value enum only invites
|
|
94
|
-
// it to guess. Reachable from the DSL (global/scene `narration.languageBoost`,
|
|
95
|
-
// forwarded by render_video) and by typing the flag yourself. Omitted =
|
|
96
|
-
// the service default.
|
|
97
|
-
const languageBoost = input.language_boost?.trim() || undefined;
|
|
98
|
-
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
99
|
-
apiBaseUrl,
|
|
100
|
-
privateToken: input.priv_token,
|
|
101
|
-
preflightToken: ctxIn.auth?.token,
|
|
102
|
-
});
|
|
103
|
-
emitProgress({ phase: 'gen-voice:request', voiceId, speed });
|
|
104
|
-
const data = await mmPost(ctx, '/tool/tts', {
|
|
105
|
-
provider: voice.provider,
|
|
106
|
-
text,
|
|
107
|
-
voiceSetting: { voiceId, speed },
|
|
108
|
-
outputFormat: 'url',
|
|
109
|
-
...(languageBoost ? { languageBoost } : {}),
|
|
110
|
-
}, { timeoutMs: 120_000 });
|
|
111
|
-
const audioUrl = data.audio;
|
|
112
|
-
if (!audioUrl) {
|
|
113
|
-
throw new SkillError(`❌ API response did not contain an audio URL: ${JSON.stringify(data)}`);
|
|
114
|
-
}
|
|
115
|
-
if (isTrue(input.json_output)) {
|
|
116
|
-
process.stdout.write(JSON.stringify({
|
|
117
|
-
url: audioUrl,
|
|
118
|
-
audio_length_ms: data.extraInfo?.audioLength ?? null,
|
|
119
|
-
subtitles: (data.subtitles ?? []).map((s) => ({
|
|
120
|
-
text: s.text ?? '',
|
|
121
|
-
timeBegin: s.timeBegin ?? 0,
|
|
122
|
-
timeEnd: s.timeEnd ?? 0,
|
|
123
|
-
})),
|
|
124
|
-
}) + '\n');
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const lengthMs = data.extraInfo?.audioLength;
|
|
128
|
-
if (typeof lengthMs === 'number') {
|
|
129
|
-
process.stdout.write(`\n🕐 Audio length: ${(lengthMs / 1000).toFixed(1)} s\n`);
|
|
130
|
-
}
|
|
131
|
-
process.stdout.write(`\n🔗 Audio URL (plays directly, no download needed):\n${audioUrl}\n`);
|
|
132
|
-
process.stdout.write(`\n🎉 TTS synthesis done!\n`);
|
|
133
|
-
}
|
|
1
|
+
var w=Object.defineProperty;var l=(e,o)=>w(e,"name",{value:o,configurable:!0});import{mmPost as m,resolveHttpContext as v,SkillError as c}from"../http.js";import{getCapabilities as _}from"../capabilities.js";import{emitProgress as y}from"../progress.js";import{isTrue as u,toNumber as $}from"./shared.js";const b=[{id:"Chinese (Mandarin)_Male_Announcer",language:"zh",name:"Mandarin Male Announcer (legacy default)"},{id:"english-male-friendly-01",language:"en",name:"Friendly Male (English, TBD)"}];async function k(e){const t=(await m(e,"/voice/page",{current:1,pageSize:100},{timeoutMs:3e4})).list??[];if(t.length===0){process.stdout.write(`⚠️ No voices available
|
|
2
|
+
`);return}process.stdout.write(`
|
|
3
|
+
${"Voice ID".padEnd(30)} Name
|
|
4
|
+
`),process.stdout.write(`${"-".repeat(30)} ${"-".repeat(20)}
|
|
5
|
+
`);for(const s of t)process.stdout.write(`${(s.uniqId??"").padEnd(30)} ${s.name??""}
|
|
6
|
+
`);process.stdout.write(`
|
|
7
|
+
${t.length} voice(s) available
|
|
8
|
+
`),process.stdout.write(`💡 Use --voice-id <id> to select a voice for synthesis
|
|
9
|
+
`)}l(k,"listVoicesRemote");function x(){process.stderr.write(`⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. Drop --local to query the live catalog.
|
|
10
|
+
`);for(const e of b)process.stdout.write(`${e.id} ${e.language} ${e.name}
|
|
11
|
+
`)}l(x,"listVoicesLocal");async function E(e,o){if(u(e.list_voices)){if(u(e.local))return x();const a=await v(o.skillName,{apiBaseUrl:e.api_base_url,privateToken:e.priv_token,preflightToken:o.auth?.token});return k(a)}const t=e.text;if(!t||typeof t!="string"||!t.trim())throw new c("❌ Please provide --text to specify the text to synthesize");const s=e.api_base_url,i=(await _(s)).voice,p=e.voice_id??i.defaultVoiceId,r=$(e.speed,"speed")??1;if(r<i.speedMin||r>i.speedMax)throw new c(`❌ speed must be in the range ${i.speedMin}~${i.speedMax}`);const g=e.language_boost?.trim()||void 0,h=await v(o.skillName,{apiBaseUrl:s,privateToken:e.priv_token,preflightToken:o.auth?.token});y({phase:"gen-voice:request",voiceId:p,speed:r});const n=await m(h,"/tool/tts",{provider:i.provider,text:t,voiceSetting:{voiceId:p,speed:r},outputFormat:"url",...g?{languageBoost:g}:{}},{timeoutMs:12e4}),d=n.audio;if(!d)throw new c(`❌ API response did not contain an audio URL: ${JSON.stringify(n)}`);if(u(e.json_output)){process.stdout.write(JSON.stringify({url:d,audio_length_ms:n.extraInfo?.audioLength??null,subtitles:(n.subtitles??[]).map(a=>({text:a.text??"",timeBegin:a.timeBegin??0,timeEnd:a.timeEnd??0}))})+`
|
|
12
|
+
`);return}const f=n.extraInfo?.audioLength;typeof f=="number"&&process.stdout.write(`
|
|
13
|
+
🕐 Audio length: ${(f/1e3).toFixed(1)} s
|
|
14
|
+
`),process.stdout.write(`
|
|
15
|
+
🔗 Audio URL (plays directly, no download needed):
|
|
16
|
+
${d}
|
|
17
|
+
`),process.stdout.write(`
|
|
18
|
+
🎉 TTS synthesis done!
|
|
19
|
+
`)}l(E,"genVoice");export{E as genVoice};
|
package/dist/handlers/index.js
CHANGED
|
@@ -1,20 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Handler registry — maps `entry.handler` strings to their implementation.
|
|
3
|
-
*
|
|
4
|
-
* Only `entry.type` in ('http' | 'builtin') uses this registry; python
|
|
5
|
-
* skills go through spawn() in runner.ts and bypass handlers entirely.
|
|
6
|
-
*/
|
|
7
|
-
import { genImage } from './gen-image.js';
|
|
8
|
-
import { genVoice } from './gen-voice.js';
|
|
9
|
-
import { genVideo } from './gen-video.js';
|
|
10
|
-
import { genDigitalHuman } from './gen-digital-human.js';
|
|
11
|
-
import { templateInit } from './template-init.js';
|
|
12
|
-
import { genPost } from './gen-post.js';
|
|
13
|
-
export const HANDLERS = {
|
|
14
|
-
'gen-image': genImage,
|
|
15
|
-
'gen-voice': genVoice,
|
|
16
|
-
'gen-video': genVideo,
|
|
17
|
-
'gen-digital-human': genDigitalHuman,
|
|
18
|
-
'template-init': templateInit,
|
|
19
|
-
'gen-post': genPost,
|
|
20
|
-
};
|
|
1
|
+
import{genImage as e}from"./gen-image.js";import{genVoice as o}from"./gen-voice.js";import{genVideo as i}from"./gen-video.js";import{genDigitalHuman as m}from"./gen-digital-human.js";import{templateInit as t}from"./template-init.js";import{genPost as n}from"./gen-post.js";const c={"gen-image":e,"gen-voice":o,"gen-video":i,"gen-digital-human":m,"template-init":t,"gen-post":n};export{c as HANDLERS};
|
package/dist/handlers/shared.js
CHANGED
|
@@ -1,66 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared helpers for TS skill handlers.
|
|
3
|
-
*
|
|
4
|
-
* These were previously copy-pasted across gen-image / gen-video /
|
|
5
|
-
* gen-digital-human / gen-voice. Centralizing them keeps argument coercion and
|
|
6
|
-
* image-input resolution identical across every atomic generator.
|
|
7
|
-
*
|
|
8
|
-
* Lives under handlers/ (not src/ root) so the compiled `dist/handlers/shared.js`
|
|
9
|
-
* travels with the rest of the handler bundle when ab-agent copies `dist/`.
|
|
10
|
-
*/
|
|
11
|
-
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import { SkillError } from '../http.js';
|
|
14
|
-
const MIME_BY_EXT = {
|
|
15
|
-
'.png': 'image/png',
|
|
16
|
-
'.jpg': 'image/jpeg',
|
|
17
|
-
'.jpeg': 'image/jpeg',
|
|
18
|
-
'.webp': 'image/webp',
|
|
19
|
-
'.gif': 'image/gif',
|
|
20
|
-
};
|
|
21
|
-
/** Truthy check tolerant of CLI string values (`--flag` → true, `--flag true` → 'true'). */
|
|
22
|
-
export function isTrue(value) {
|
|
23
|
-
return value === true || value === 'true';
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Coerce a flag value into a finite number, or `undefined` when absent/empty.
|
|
27
|
-
* Throws SkillError on a non-numeric value so callers fail fast with a clear msg.
|
|
28
|
-
*/
|
|
29
|
-
export function toNumber(value, name) {
|
|
30
|
-
if (value === undefined || value === null || value === '')
|
|
31
|
-
return undefined;
|
|
32
|
-
const n = Number(value);
|
|
33
|
-
if (!Number.isFinite(n))
|
|
34
|
-
throw new SkillError(`❌ ${name} must be a number, received: ${value}`);
|
|
35
|
-
return n;
|
|
36
|
-
}
|
|
37
|
-
/** Coerce a repeatable flag (string | string[] | undefined) into a string array. */
|
|
38
|
-
export function toStringArray(value) {
|
|
39
|
-
if (value === undefined || value === null)
|
|
40
|
-
return [];
|
|
41
|
-
if (Array.isArray(value))
|
|
42
|
-
return value.map(String);
|
|
43
|
-
return [String(value)];
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Resolve an image reference into a value the backend accepts:
|
|
47
|
-
* - http(s) URLs and data: URIs pass through unchanged
|
|
48
|
-
* - local file paths are read and base64-encoded into a `data:<mime>;base64,…` URI
|
|
49
|
-
* Throws SkillError when a local path does not exist or is not a file.
|
|
50
|
-
*/
|
|
51
|
-
export function resolveImageInput(pathOrUrl) {
|
|
52
|
-
const trimmed = pathOrUrl.trim();
|
|
53
|
-
if (!trimmed)
|
|
54
|
-
return '';
|
|
55
|
-
const lower = trimmed.toLowerCase();
|
|
56
|
-
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) {
|
|
57
|
-
return trimmed;
|
|
58
|
-
}
|
|
59
|
-
if (!existsSync(trimmed) || !statSync(trimmed).isFile()) {
|
|
60
|
-
throw new SkillError(`❌ image path does not exist or is not a file: ${trimmed}`);
|
|
61
|
-
}
|
|
62
|
-
const ext = path.extname(trimmed).toLowerCase();
|
|
63
|
-
const mime = MIME_BY_EXT[ext] ?? 'image/jpeg';
|
|
64
|
-
const b64 = readFileSync(trimmed).toString('base64');
|
|
65
|
-
return `data:${mime};base64,${b64}`;
|
|
66
|
-
}
|
|
1
|
+
var p=Object.defineProperty;var i=(t,r)=>p(t,"name",{value:r,configurable:!0});import{readFileSync as a,existsSync as g,statSync as f}from"node:fs";import u from"node:path";import{SkillError as n}from"../http.js";const c={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".gif":"image/gif"};function x(t){return t===!0||t==="true"}i(x,"isTrue");function S(t,r){if(t==null||t==="")return;const e=Number(t);if(!Number.isFinite(e))throw new n(`❌ ${r} must be a number, received: ${t}`);return e}i(S,"toNumber");function y(t){return t==null?[]:Array.isArray(t)?t.map(String):[String(t)]}i(y,"toStringArray");function j(t){const r=t.trim();if(!r)return"";const e=r.toLowerCase();if(e.startsWith("http://")||e.startsWith("https://")||e.startsWith("data:"))return r;if(!g(r)||!f(r).isFile())throw new n(`❌ image path does not exist or is not a file: ${r}`);const o=u.extname(r).toLowerCase(),s=c[o]??"image/jpeg",m=a(r).toString("base64");return`data:${s};base64,${m}`}i(j,"resolveImageInput");export{x as isTrue,j as resolveImageInput,S as toNumber,y as toStringArray};
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* template-authoring skill → `remixmate template init`.
|
|
3
|
-
*
|
|
4
|
-
* Agent hosts call skills as `remixmate template-authoring --dir X [--force]`;
|
|
5
|
-
* this handler is a thin adapter so both entry points share one implementation.
|
|
6
|
-
* Purely local (auth: none): fetches the starter via npm, no ab-api calls.
|
|
7
|
-
*/
|
|
8
|
-
import { EXIT, SkillError } from '../errors.js';
|
|
9
|
-
import { runInit } from '../template/init.js';
|
|
10
|
-
export async function templateInit(input, _ctx) {
|
|
11
|
-
const dir = typeof input.dir === 'string' ? input.dir.trim() : '';
|
|
12
|
-
if (!dir)
|
|
13
|
-
throw new SkillError('❌ 缺少 --dir:模板工程的目标目录', EXIT.USAGE);
|
|
14
|
-
await runInit({
|
|
15
|
-
dir,
|
|
16
|
-
force: input.force === true || input.force === 'true',
|
|
17
|
-
json: input.json_output === true || input.json === true,
|
|
18
|
-
skipInstall: input.skip_install === true,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
export default templateInit;
|
|
1
|
+
var i=Object.defineProperty;var e=(r,t)=>i(r,"name",{value:t,configurable:!0});import{EXIT as s,SkillError as f}from"../errors.js";import{runInit as l}from"../template/init.js";async function n(r,t){const o=typeof r.dir=="string"?r.dir.trim():"";if(!o)throw new f("❌ 缺少 --dir:模板工程的目标目录",s.USAGE);await l({dir:o,force:r.force===!0||r.force==="true",json:r.json_output===!0||r.json===!0,skipInstall:r.skip_install===!0})}e(n,"templateInit");var d=n;export{d as default,n as templateInit};
|
package/dist/http.js
CHANGED
|
@@ -1,155 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Centralizes the auth contract (X-Priv-Token + x-invoke-skill / x-invoke-agent /
|
|
5
|
-
* x-conversation-id), business-code error handling (result.code != 0), and the
|
|
6
|
-
* env var resolution order that every Python skill used to duplicate.
|
|
7
|
-
*
|
|
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
|
|
14
|
-
*
|
|
15
|
-
* Base URL resolution:
|
|
16
|
-
* 1. opts.apiBaseUrl — caller override
|
|
17
|
-
* 2. process.env.MM_API_BASE_URL — process env (point at local/staging here)
|
|
18
|
-
* 3. process.env.MM_BACKEND_API_URL — ab-agent's convention (back-compat)
|
|
19
|
-
* 4. https://api.remixmate.ai/api — production default (zero-config)
|
|
20
|
-
*/
|
|
21
|
-
import { traceHeaders, cleanDiagnosticText } from './diagnostics.js';
|
|
22
|
-
import { recordBilling } from './billing.js';
|
|
23
|
-
import { resolvePrivToken, NOT_AUTHENTICATED_HINT } from './auth/resolve.js';
|
|
24
|
-
import { attemptAutoLogin } from './auth/auto-login.js';
|
|
25
|
-
import { EXIT, SkillError } from './errors.js';
|
|
26
|
-
const DEFAULT_API_BASE_URL = 'https://api.remixmate.ai/api';
|
|
27
|
-
// Re-exported so the many `import { SkillError } from '../http.js'` call sites
|
|
28
|
-
// stay valid; the definition moved to errors.ts to break an import cycle with
|
|
29
|
-
// the auth gateway (see errors.ts).
|
|
30
|
-
export { SkillError, EXIT } from './errors.js';
|
|
31
|
-
/**
|
|
32
|
-
* Resolve the ab-api base URL the same way for authenticated and device-flow calls.
|
|
33
|
-
*
|
|
34
|
-
* `MM_BACKEND_API_URL` is honored because several Python skills already fall back
|
|
35
|
-
* to it (registry_loader, render_job_client, _vod_polling). Since runner.ts now
|
|
36
|
-
* injects the resolved base URL into those child processes, leaving it out here
|
|
37
|
-
* would let a prod default silently override an ab-agent-injected backend.
|
|
38
|
-
*/
|
|
39
|
-
export function resolveApiBaseUrl(flag) {
|
|
40
|
-
const fromEnv = (process.env.MM_API_BASE_URL ?? '').trim() || (process.env.MM_BACKEND_API_URL ?? '').trim();
|
|
41
|
-
return ((flag ?? '').trim() || fromEnv || DEFAULT_API_BASE_URL).replace(/\/+$/, '');
|
|
42
|
-
}
|
|
43
|
-
export async function resolveHttpContext(skillName, opts = {}) {
|
|
44
|
-
const apiBaseUrl = resolveApiBaseUrl(opts.apiBaseUrl);
|
|
45
|
-
// An explicit --token flag still wins over the preflight result.
|
|
46
|
-
let resolved = await resolvePrivToken({
|
|
47
|
-
flag: opts.privateToken ?? opts.preflightToken,
|
|
48
|
-
apiBaseUrl,
|
|
49
|
-
});
|
|
50
|
-
// Only when flag/env/store are all empty do we try automatic browser auth.
|
|
51
|
-
// attemptAutoLogin self-gates (cloud env / headless / disabled → returns null
|
|
52
|
-
// immediately), so the env-injected path never reaches it (zero regression).
|
|
53
|
-
if (!resolved) {
|
|
54
|
-
resolved = await attemptAutoLogin(apiBaseUrl);
|
|
55
|
-
}
|
|
56
|
-
if (!resolved) {
|
|
57
|
-
throw new SkillError(NOT_AUTHENTICATED_HINT, EXIT.NOT_AUTHENTICATED);
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
apiBaseUrl,
|
|
61
|
-
privateToken: resolved.privToken,
|
|
62
|
-
skillName,
|
|
63
|
-
agentName: process.env.AGENT_NAME || undefined,
|
|
64
|
-
conversationId: process.env.CONVERSATION_ID || undefined,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function buildHeaders(ctx, extra) {
|
|
68
|
-
const headers = {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
'X-Priv-Token': ctx.privateToken,
|
|
71
|
-
'x-invoke-skill': ctx.skillName,
|
|
72
|
-
};
|
|
73
|
-
if (ctx.agentName)
|
|
74
|
-
headers['x-invoke-agent'] = ctx.agentName;
|
|
75
|
-
if (ctx.conversationId)
|
|
76
|
-
headers['x-conversation-id'] = ctx.conversationId;
|
|
77
|
-
return { ...headers, ...extra, ...traceHeaders() };
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* POST JSON to ab-api and return the parsed business payload.
|
|
81
|
-
* Throws SkillError on HTTP errors or business code != 0.
|
|
82
|
-
*/
|
|
83
|
-
export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
|
|
84
|
-
const url = pathOrUrl.startsWith('http') ? pathOrUrl : `${ctx.apiBaseUrl}${pathOrUrl}`;
|
|
85
|
-
const controller = new AbortController();
|
|
86
|
-
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 120_000);
|
|
87
|
-
let resp;
|
|
88
|
-
try {
|
|
89
|
-
resp = await fetch(url, {
|
|
90
|
-
method: 'POST',
|
|
91
|
-
headers: buildHeaders(ctx),
|
|
92
|
-
body: JSON.stringify(body),
|
|
93
|
-
signal: controller.signal,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
98
|
-
throw new SkillError(`❌ request timed out: ${url}`, EXIT.BACKEND_UNREACHABLE);
|
|
99
|
-
}
|
|
100
|
-
throw new SkillError(`❌ network error: ${err.message}`, EXIT.BACKEND_UNREACHABLE);
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
clearTimeout(timeout);
|
|
104
|
-
}
|
|
105
|
-
const text = await resp.text();
|
|
106
|
-
if (!resp.ok) {
|
|
107
|
-
// 401/403 is not a generic API failure — it means the credential is missing,
|
|
108
|
-
// expired or revoked. Surfacing it as such (with exit code 4) is what lets a
|
|
109
|
-
// host offer to re-authorize instead of reporting an opaque backend error.
|
|
110
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
111
|
-
throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 HTTP ${resp.status})`, EXIT.NOT_AUTHENTICATED);
|
|
112
|
-
}
|
|
113
|
-
if (resp.status >= 500) {
|
|
114
|
-
throw new SkillError(`❌ API request failed (HTTP ${resp.status}): [response omitted]`, EXIT.BACKEND_UNREACHABLE);
|
|
115
|
-
}
|
|
116
|
-
throw new SkillError(`❌ API request failed (HTTP ${resp.status}): [response omitted]`);
|
|
117
|
-
}
|
|
118
|
-
let parsed;
|
|
119
|
-
try {
|
|
120
|
-
parsed = JSON.parse(text);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
throw new SkillError(`❌ failed to parse response, body is not JSON: [response omitted]`);
|
|
124
|
-
}
|
|
125
|
-
// Before the code check: a charged response is always code=0 today, but a
|
|
126
|
-
// partial-failure envelope that still billed must not lose its billing line.
|
|
127
|
-
recordBilling(parsed.billing);
|
|
128
|
-
if (parsed.code !== 0) {
|
|
129
|
-
// ab-api reports auth failures in the envelope (HTTP 200 + code=401), so the
|
|
130
|
-
// business-code path needs the same 401 → "re-authorize" mapping as above.
|
|
131
|
-
if (parsed.code === 401 || parsed.code === 403) {
|
|
132
|
-
throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 code=${parsed.code}${parsed.msg ? `: ${parsed.msg}` : ''})`, EXIT.NOT_AUTHENTICATED);
|
|
133
|
-
}
|
|
134
|
-
throw new SkillError(`❌ API returned a business error: ${cleanDiagnosticText(parsed.msg ?? 'unknown error')} (code=${parsed.code})`);
|
|
135
|
-
}
|
|
136
|
-
return parsed.data ?? undefined;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Generic polling helper. Invokes `probe()` immediately, then every
|
|
140
|
-
* `intervalMs`, until it returns a non-null value or `timeoutMs` elapses.
|
|
141
|
-
* Probing first (rather than sleeping first) avoids a wasted initial interval
|
|
142
|
-
* when the backend job is already complete.
|
|
143
|
-
*/
|
|
144
|
-
export async function pollUntil(probe, opts = {}) {
|
|
145
|
-
const intervalMs = opts.intervalMs ?? 5_000;
|
|
146
|
-
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
147
|
-
const start = Date.now();
|
|
148
|
-
while (Date.now() - start < timeoutMs) {
|
|
149
|
-
const result = await probe();
|
|
150
|
-
if (result !== null)
|
|
151
|
-
return result;
|
|
152
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
153
|
-
}
|
|
154
|
-
throw new SkillError(`❌ polling timed out (${Math.round(timeoutMs / 1000)}s)`);
|
|
155
|
-
}
|
|
1
|
+
var f=Object.defineProperty;var a=(e,t)=>f(e,"name",{value:t,configurable:!0});import{traceHeaders as A,cleanDiagnosticText as w}from"./diagnostics.js";import{recordBilling as E}from"./billing.js";import{resolvePrivToken as N,NOT_AUTHENTICATED_HINT as d}from"./auth/resolve.js";import{attemptAutoLogin as v}from"./auth/auto-login.js";import{EXIT as l,SkillError as n}from"./errors.js";const _="https://api.remixmate.ai/api";import{SkillError as S,EXIT as b}from"./errors.js";function h(e){const t=(process.env.MM_API_BASE_URL??"").trim()||(process.env.MM_BACKEND_API_URL??"").trim();return((e??"").trim()||t||_).replace(/\/+$/,"")}a(h,"resolveApiBaseUrl");async function P(e,t={}){const o=h(t.apiBaseUrl);let i=await N({flag:t.privateToken??t.preflightToken,apiBaseUrl:o});if(i||(i=await v(o)),!i)throw new n(d,l.NOT_AUTHENTICATED);return{apiBaseUrl:o,privateToken:i.privToken,skillName:e,agentName:process.env.AGENT_NAME||void 0,conversationId:process.env.CONVERSATION_ID||void 0}}a(P,"resolveHttpContext");function g(e,t){const o={"Content-Type":"application/json","X-Priv-Token":e.privateToken,"x-invoke-skill":e.skillName};return e.agentName&&(o["x-invoke-agent"]=e.agentName),e.conversationId&&(o["x-conversation-id"]=e.conversationId),{...o,...t,...A()}}a(g,"buildHeaders");async function U(e,t,o,i={}){const m=t.startsWith("http")?t:`${e.apiBaseUrl}${t}`,c=new AbortController,T=setTimeout(()=>c.abort(),i.timeoutMs??12e4);let s;try{s=await fetch(m,{method:"POST",headers:g(e),body:JSON.stringify(o),signal:c.signal})}catch(u){throw u instanceof Error&&u.name==="AbortError"?new n(`❌ request timed out: ${m}`,l.BACKEND_UNREACHABLE):new n(`❌ network error: ${u.message}`,l.BACKEND_UNREACHABLE)}finally{clearTimeout(T)}const p=await s.text();if(!s.ok)throw s.status===401||s.status===403?new n(`${d}
|
|
2
|
+
(后端返回 HTTP ${s.status})`,l.NOT_AUTHENTICATED):s.status>=500?new n(`❌ API request failed (HTTP ${s.status}): [response omitted]`,l.BACKEND_UNREACHABLE):new n(`❌ API request failed (HTTP ${s.status}): [response omitted]`);let r;try{r=JSON.parse(p)}catch{throw new n("❌ failed to parse response, body is not JSON: [response omitted]")}if(E(r.billing),r.code!==0)throw r.code===401||r.code===403?new n(`${d}
|
|
3
|
+
(后端返回 code=${r.code}${r.msg?`: ${r.msg}`:""})`,l.NOT_AUTHENTICATED):new n(`❌ API returned a business error: ${w(r.msg??"unknown error")} (code=${r.code})`);return r.data??void 0}a(U,"mmPost");async function D(e,t={}){const o=t.intervalMs??5e3,i=t.timeoutMs??12e4,m=Date.now();for(;Date.now()-m<i;){const c=await e();if(c!==null)return c;await new Promise(T=>setTimeout(T,o))}throw new n(`❌ polling timed out (${Math.round(i/1e3)}s)`)}a(D,"pollUntil");export{b as EXIT,S as SkillError,U as mmPost,D as pollUntil,h as resolveApiBaseUrl,P as resolveHttpContext};
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* remixmate — public library exports.
|
|
3
|
-
*
|
|
4
|
-
* The CLI entrypoint lives in `cli.ts` (`bin: remixmate`). This file is the
|
|
5
|
-
* library surface for type-sharing consumers; the published npm package
|
|
6
|
-
* exposes it via `main: dist/index.js` / `types: dist/index.d.ts`.
|
|
7
|
-
*/
|
|
8
|
-
export { loadSkills, findSkill, SKILLS_DIR } from './registry.js';
|
|
1
|
+
import{loadSkills as i,findSkill as o,SKILLS_DIR as d}from"./registry.js";export{d as SKILLS_DIR,o as findSkill,i as loadSkills};
|