@remixmate/cli 0.9.16 → 0.9.18
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/dist/cli.js +12 -0
- package/dist/doctor.js +21 -0
- package/dist/manifest.json +2 -2
- package/dist/project/commands.d.ts +19 -0
- package/dist/project/commands.js +156 -0
- package/dist/project/host.d.ts +77 -0
- package/dist/project/host.js +106 -0
- package/dist/project/resolve.d.ts +43 -0
- package/dist/project/resolve.js +65 -0
- package/dist/project/store.d.ts +31 -0
- package/dist/project/store.js +97 -0
- package/dist/project/take.d.ts +46 -0
- package/dist/project/take.js +102 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +3 -0
- package/dist/runner.d.ts +18 -0
- package/dist/runner.js +85 -0
- package/dist/skill-schema.d.ts +23 -0
- package/dist/skill-schema.js +31 -0
- package/package.json +1 -1
- package/skills/gen-digital-human/skill.json +78 -17
- package/skills/gen-image/skill.json +66 -15
- package/skills/gen-video/skill.json +73 -17
- package/skills/gen-voice/skill.json +34 -8
- package/skills/prepare-video-assets/skill.json +43 -9
- package/skills/render-video/SKILL.md +11 -1
- package/skills/render-video/scripts/render_video.py +20 -0
- package/skills/render-video/skill.json +44 -8
- package/skills/video-parser/skill.json +29 -7
- package/skills/web-record/skill.json +137 -33
- package/skills/web-screenshot/skill.json +66 -16
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent project binding — which Project should this machine's CLI output
|
|
3
|
+
* belong to?
|
|
4
|
+
*
|
|
5
|
+
* Stored at ~/.config/remixmate/projects.json, NOT in credentials.json: that
|
|
6
|
+
* file is a 0600 secret store keyed by API base URL, with different lifetime
|
|
7
|
+
* and different blast radius on corruption.
|
|
8
|
+
*
|
|
9
|
+
* Two layers, because host cwd semantics are not uniform:
|
|
10
|
+
* - `byPath` — keyed by absolute cwd. Hosts that run inside the user's real
|
|
11
|
+
* repo (Codex, Claude Code) get per-repo bindings.
|
|
12
|
+
* - `default` — a single fallback. Hosts that spawn a throwaway working
|
|
13
|
+
* directory per conversation (WorkBuddy uses
|
|
14
|
+
* ~/WorkBuddy/<timestamp>) would never hit a byPath entry, so a
|
|
15
|
+
* cwd-only design silently degrades to nothing for them.
|
|
16
|
+
*
|
|
17
|
+
* See docs/cli-project-binding-design.md §3.
|
|
18
|
+
*/
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { promises as fs } from 'node:fs';
|
|
22
|
+
export const CONFIG_DIR = join(homedir(), '.config', 'remixmate');
|
|
23
|
+
export const PROJECTS_FILE = join(CONFIG_DIR, 'projects.json');
|
|
24
|
+
/**
|
|
25
|
+
* Must build a NEW object every call, not spread a shared constant: `backends`
|
|
26
|
+
* would then be one object aliased by every "empty" read, and writeBinding
|
|
27
|
+
* mutates it — so a later read of a missing/corrupt file would hand back
|
|
28
|
+
* bindings accumulated earlier in the process instead of nothing.
|
|
29
|
+
*/
|
|
30
|
+
function emptyFile() {
|
|
31
|
+
return { version: 1, backends: {} };
|
|
32
|
+
}
|
|
33
|
+
async function readFile() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = await fs.readFile(PROJECTS_FILE, 'utf-8');
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.backends)
|
|
38
|
+
return emptyFile();
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Missing or corrupt → behave as unbound. A broken binding file must never
|
|
43
|
+
// block a render; the resolution ladder just falls through to the default project.
|
|
44
|
+
return emptyFile();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function writeFile(data) {
|
|
48
|
+
await fs.mkdir(dirname(PROJECTS_FILE), { recursive: true, mode: 0o700 });
|
|
49
|
+
await fs.writeFile(PROJECTS_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
50
|
+
}
|
|
51
|
+
/** Look up the binding for a cwd: exact byPath match first, then the default. */
|
|
52
|
+
export async function readBinding(apiBaseUrl, cwd = process.cwd()) {
|
|
53
|
+
const file = await readFile();
|
|
54
|
+
const entry = file.backends[apiBaseUrl];
|
|
55
|
+
if (!entry)
|
|
56
|
+
return null;
|
|
57
|
+
const byPath = entry.byPath?.[resolve(cwd)];
|
|
58
|
+
if (byPath)
|
|
59
|
+
return { projectId: byPath, scope: 'path' };
|
|
60
|
+
if (entry.default)
|
|
61
|
+
return { projectId: entry.default, scope: 'default' };
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** Bind a project to this cwd (`scope: 'path'`) or as the fallback (`'default'`). */
|
|
65
|
+
export async function writeBinding(apiBaseUrl, projectId, scope, cwd = process.cwd()) {
|
|
66
|
+
const file = await readFile();
|
|
67
|
+
const entry = file.backends[apiBaseUrl] ?? {};
|
|
68
|
+
if (scope === 'default') {
|
|
69
|
+
entry.default = projectId;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
entry.byPath = { ...(entry.byPath ?? {}), [resolve(cwd)]: projectId };
|
|
73
|
+
}
|
|
74
|
+
file.backends[apiBaseUrl] = entry;
|
|
75
|
+
await writeFile(file);
|
|
76
|
+
}
|
|
77
|
+
/** Drop a binding. Returns true when something was actually removed. */
|
|
78
|
+
export async function clearBinding(apiBaseUrl, scope, cwd = process.cwd()) {
|
|
79
|
+
const file = await readFile();
|
|
80
|
+
const entry = file.backends[apiBaseUrl];
|
|
81
|
+
if (!entry)
|
|
82
|
+
return false;
|
|
83
|
+
let removed = false;
|
|
84
|
+
if (scope === 'default') {
|
|
85
|
+
removed = entry.default != null;
|
|
86
|
+
delete entry.default;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const key = resolve(cwd);
|
|
90
|
+
removed = entry.byPath?.[key] != null;
|
|
91
|
+
if (entry.byPath)
|
|
92
|
+
delete entry.byPath[key];
|
|
93
|
+
}
|
|
94
|
+
if (removed)
|
|
95
|
+
await writeFile(file);
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Take creation — turn a render invocation into a VideoProject the web app can
|
|
3
|
+
* show, and hand its conversation id down to the skill.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists at all: ab-web derives a video's project from
|
|
6
|
+
* `file.conversation_id → media_video_project → media_project`. A render whose
|
|
7
|
+
* upload carries no conversation id is stored correctly but belongs to nothing,
|
|
8
|
+
* so it shows up only in the raw asset list. The cloud agent injects
|
|
9
|
+
* CONVERSATION_ID for its own runs; locally, nobody did — that is the bug this
|
|
10
|
+
* module fixes.
|
|
11
|
+
*
|
|
12
|
+
* Idempotency lives on the server (`/video-project/ensure` is get-or-create on
|
|
13
|
+
* `(user_id, client_session_key)`), not in a local state file: a pipeline is N
|
|
14
|
+
* separate CLI processes, sometimes concurrent, and a local cache would have to
|
|
15
|
+
* solve staleness and write races that a unique index solves for free.
|
|
16
|
+
*
|
|
17
|
+
* See docs/cli-project-binding-design.md §5.
|
|
18
|
+
*/
|
|
19
|
+
import { type HttpContext } from '../http.js';
|
|
20
|
+
export interface EnsuredTake {
|
|
21
|
+
conversationId: string;
|
|
22
|
+
videoProjectId: string;
|
|
23
|
+
projectId: string;
|
|
24
|
+
created: boolean;
|
|
25
|
+
attempt: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Look up the take for the current grouping key without creating anything.
|
|
29
|
+
*
|
|
30
|
+
* Used by pipeline skills that should attach to a take when one exists but must
|
|
31
|
+
* never bring one into being: someone running `remixmate gen-image` on its own
|
|
32
|
+
* isn't making a video, and an empty VideoProject is worse clutter than an
|
|
33
|
+
* unattributed image.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately does NOT resolve a project — resolveProject()'s last rung
|
|
36
|
+
* lazily *creates* 「未分类」, which a read-only lookup has no business doing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findTake(ctx: HttpContext): Promise<EnsuredTake | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve project + grouping key, then get-or-create the take.
|
|
41
|
+
* Returns null when this invocation shouldn't own a take.
|
|
42
|
+
*/
|
|
43
|
+
export declare function ensureTake(ctx: HttpContext, opts: {
|
|
44
|
+
jobId?: number;
|
|
45
|
+
flagProjectId?: string;
|
|
46
|
+
}): Promise<EnsuredTake | null>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Take creation — turn a render invocation into a VideoProject the web app can
|
|
3
|
+
* show, and hand its conversation id down to the skill.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists at all: ab-web derives a video's project from
|
|
6
|
+
* `file.conversation_id → media_video_project → media_project`. A render whose
|
|
7
|
+
* upload carries no conversation id is stored correctly but belongs to nothing,
|
|
8
|
+
* so it shows up only in the raw asset list. The cloud agent injects
|
|
9
|
+
* CONVERSATION_ID for its own runs; locally, nobody did — that is the bug this
|
|
10
|
+
* module fixes.
|
|
11
|
+
*
|
|
12
|
+
* Idempotency lives on the server (`/video-project/ensure` is get-or-create on
|
|
13
|
+
* `(user_id, client_session_key)`), not in a local state file: a pipeline is N
|
|
14
|
+
* separate CLI processes, sometimes concurrent, and a local cache would have to
|
|
15
|
+
* solve staleness and write races that a unique index solves for free.
|
|
16
|
+
*
|
|
17
|
+
* See docs/cli-project-binding-design.md §5.
|
|
18
|
+
*/
|
|
19
|
+
import { mmPost } from '../http.js';
|
|
20
|
+
import { resolveTakeKey } from './host.js';
|
|
21
|
+
import { resolveProject } from './resolve.js';
|
|
22
|
+
/**
|
|
23
|
+
* Pull title / aspectRatio / templateId off the persisted render job.
|
|
24
|
+
*
|
|
25
|
+
* The VideoProject row needs them at creation time, but this runs in Node
|
|
26
|
+
* before the Python renderer is spawned, so the DSL itself isn't in reach —
|
|
27
|
+
* `render_jobs.dsl_meta` is the summary of exactly those fields. Best-effort:
|
|
28
|
+
* a take with a placeholder title beats no take at all.
|
|
29
|
+
*/
|
|
30
|
+
async function fetchJobMeta(ctx, jobId) {
|
|
31
|
+
try {
|
|
32
|
+
const detail = await mmPost(ctx, '/renderJob/get', { jobId });
|
|
33
|
+
return detail?.dslMeta ?? {};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Look up the take for the current grouping key without creating anything.
|
|
41
|
+
*
|
|
42
|
+
* Used by pipeline skills that should attach to a take when one exists but must
|
|
43
|
+
* never bring one into being: someone running `remixmate gen-image` on its own
|
|
44
|
+
* isn't making a video, and an empty VideoProject is worse clutter than an
|
|
45
|
+
* unattributed image.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately does NOT resolve a project — resolveProject()'s last rung
|
|
48
|
+
* lazily *creates* 「未分类」, which a read-only lookup has no business doing.
|
|
49
|
+
*/
|
|
50
|
+
export async function findTake(ctx) {
|
|
51
|
+
const takeKey = resolveTakeKey(undefined);
|
|
52
|
+
if (!takeKey.key)
|
|
53
|
+
return null;
|
|
54
|
+
const res = await mmPost(ctx, '/video-project/find-by-session', {
|
|
55
|
+
clientSessionKey: takeKey.key,
|
|
56
|
+
});
|
|
57
|
+
if (!res?.conversationId)
|
|
58
|
+
return null;
|
|
59
|
+
return {
|
|
60
|
+
conversationId: res.conversationId,
|
|
61
|
+
videoProjectId: res.id,
|
|
62
|
+
projectId: res.projectId ?? '',
|
|
63
|
+
created: false,
|
|
64
|
+
attempt: res.attempt ?? 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve project + grouping key, then get-or-create the take.
|
|
69
|
+
* Returns null when this invocation shouldn't own a take.
|
|
70
|
+
*/
|
|
71
|
+
export async function ensureTake(ctx, opts) {
|
|
72
|
+
const jobId = opts.jobId && opts.jobId > 0 ? opts.jobId : undefined;
|
|
73
|
+
const takeKey = resolveTakeKey(jobId);
|
|
74
|
+
// No grouping key at all → don't create. Only reachable for a take-creating
|
|
75
|
+
// skill that has no jobId (prepare-video-assets) on a host that exposes no
|
|
76
|
+
// session id. Creating here would produce an ungrouped take that the later
|
|
77
|
+
// render-video call — which *does* have a jobId, hence the `job:N` key —
|
|
78
|
+
// could not join, leaving two takes for one video.
|
|
79
|
+
if (!takeKey.key)
|
|
80
|
+
return null;
|
|
81
|
+
const { projectId } = await resolveProject(ctx, { flagProjectId: opts.flagProjectId });
|
|
82
|
+
const meta = jobId ? await fetchJobMeta(ctx, jobId) : {};
|
|
83
|
+
const res = await mmPost(ctx, '/video-project/ensure', {
|
|
84
|
+
projectId,
|
|
85
|
+
clientSessionKey: takeKey.key,
|
|
86
|
+
clientHost: takeKey.host,
|
|
87
|
+
jobId: jobId ?? 0,
|
|
88
|
+
title: meta?.title ?? '',
|
|
89
|
+
archetype: meta?.archetype ?? '',
|
|
90
|
+
aspectRatio: meta?.aspectRatio ?? '',
|
|
91
|
+
templateId: meta?.templateId ?? '',
|
|
92
|
+
});
|
|
93
|
+
if (!res?.conversationId)
|
|
94
|
+
return null;
|
|
95
|
+
return {
|
|
96
|
+
conversationId: res.conversationId,
|
|
97
|
+
videoProjectId: res.id,
|
|
98
|
+
projectId,
|
|
99
|
+
created: res.created,
|
|
100
|
+
attempt: res.attempt,
|
|
101
|
+
};
|
|
102
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface SkillDef {
|
|
|
16
16
|
entry: SkillEntry;
|
|
17
17
|
/** Authorization requirement enforced by the dispatcher before invocation. */
|
|
18
18
|
auth: SkillAuthMode;
|
|
19
|
+
/** Ensure a take (VideoProject + conversation) before running — see skill-schema.ts. */
|
|
20
|
+
createsTake: boolean;
|
|
21
|
+
/** Attach to an existing take, never create one — see skill-schema.ts. */
|
|
22
|
+
joinsTake: boolean;
|
|
23
|
+
/** Argv keys that downgrade createsTake to joinsTake — see skill-schema.ts. */
|
|
24
|
+
createsTakeUnless: string[];
|
|
19
25
|
skillDir: string;
|
|
20
26
|
/** Absolute path to the python entry script (only when entry.type === 'python'). */
|
|
21
27
|
scriptAbsolutePath?: string;
|
package/dist/registry.js
CHANGED
|
@@ -38,6 +38,9 @@ function loadOne(skillDir) {
|
|
|
38
38
|
parameters: raw.parameters ?? {},
|
|
39
39
|
entry,
|
|
40
40
|
auth: resolveAuthMode(raw),
|
|
41
|
+
createsTake: raw.createsTake === true,
|
|
42
|
+
joinsTake: raw.joinsTake === true,
|
|
43
|
+
createsTakeUnless: Array.isArray(raw.createsTakeUnless) ? raw.createsTakeUnless : [],
|
|
41
44
|
skillDir,
|
|
42
45
|
};
|
|
43
46
|
if (entry.type === 'python') {
|
package/dist/runner.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Resolves to the child process's exit code so cli.ts can propagate it.
|
|
11
11
|
*/
|
|
12
|
+
import { type SkillDef } from './registry.js';
|
|
12
13
|
import type { ParsedArgs } from './argv.js';
|
|
13
14
|
export interface RunOptions {
|
|
14
15
|
/** Override the skills directory (defaults to the package's bundled skills/). */
|
|
@@ -19,3 +20,20 @@ export interface RunOptions {
|
|
|
19
20
|
parsedArgs: ParsedArgs;
|
|
20
21
|
}
|
|
21
22
|
export declare function runSkill(skillName: string, opts: RunOptions): Promise<number>;
|
|
23
|
+
/** What this invocation is allowed to do about attribution. */
|
|
24
|
+
export type TakeMode = 'create' | 'join' | 'none';
|
|
25
|
+
/**
|
|
26
|
+
* Decide the take mode from the skill's declaration *and* what this particular
|
|
27
|
+
* invocation was asked to do.
|
|
28
|
+
*
|
|
29
|
+
* The manifest flags alone are not enough, because they describe the skill, not
|
|
30
|
+
* the call: `render-video --help` prints usage and exits, and
|
|
31
|
+
* `render-video --resolve-only` stops after asset resolution. Both used to run
|
|
32
|
+
* the full createsTake path, so a help query was enough to mint an empty take
|
|
33
|
+
* and lazily create 「未分类」 — the exact clutter project/take.ts sets out to
|
|
34
|
+
* avoid ("an empty VideoProject is worse clutter than an unattributed image").
|
|
35
|
+
*
|
|
36
|
+
* Note that a downgraded run still *joins*: when its own pipeline already
|
|
37
|
+
* started a take under the same grouping key, assets it generates stay attached.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveTakeMode(skill: Pick<SkillDef, 'createsTake' | 'joinsTake' | 'createsTakeUnless'>, args: ParsedArgs): TakeMode;
|
package/dist/runner.js
CHANGED
|
@@ -16,6 +16,8 @@ import { HANDLERS } from './handlers/index.js';
|
|
|
16
16
|
import { EXIT, SkillError } from './errors.js';
|
|
17
17
|
import { authChildEnv, ensureAuth } from './auth/ensure.js';
|
|
18
18
|
import { emitBillingFooter } from './billing.js';
|
|
19
|
+
import { resolveHttpContext } from './http.js';
|
|
20
|
+
import { ensureTake, findTake } from './project/take.js';
|
|
19
21
|
/** Read the token override accepted by both the TS handlers and the Python skills. */
|
|
20
22
|
function tokenFlag(args) {
|
|
21
23
|
const value = args.token ?? args.priv_token;
|
|
@@ -33,6 +35,7 @@ export async function runSkill(skillName, opts) {
|
|
|
33
35
|
apiBaseUrl: typeof opts.parsedArgs.api_base_url === 'string' ? opts.parsedArgs.api_base_url : undefined,
|
|
34
36
|
flagToken: tokenFlag(opts.parsedArgs),
|
|
35
37
|
});
|
|
38
|
+
await applyTakeContext(skill, opts, auth);
|
|
36
39
|
switch (skill.entry.type) {
|
|
37
40
|
case 'python':
|
|
38
41
|
// Python children talk to ab-api themselves and print their own billing
|
|
@@ -58,6 +61,88 @@ export async function runSkill(skillName, opts) {
|
|
|
58
61
|
emitBillingFooter();
|
|
59
62
|
}
|
|
60
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Decide the take mode from the skill's declaration *and* what this particular
|
|
66
|
+
* invocation was asked to do.
|
|
67
|
+
*
|
|
68
|
+
* The manifest flags alone are not enough, because they describe the skill, not
|
|
69
|
+
* the call: `render-video --help` prints usage and exits, and
|
|
70
|
+
* `render-video --resolve-only` stops after asset resolution. Both used to run
|
|
71
|
+
* the full createsTake path, so a help query was enough to mint an empty take
|
|
72
|
+
* and lazily create 「未分类」 — the exact clutter project/take.ts sets out to
|
|
73
|
+
* avoid ("an empty VideoProject is worse clutter than an unattributed image").
|
|
74
|
+
*
|
|
75
|
+
* Note that a downgraded run still *joins*: when its own pipeline already
|
|
76
|
+
* started a take under the same grouping key, assets it generates stay attached.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveTakeMode(skill, args) {
|
|
79
|
+
// Introspection — the skill prints its usage and exits without producing
|
|
80
|
+
// anything, so it must not touch the backend at all.
|
|
81
|
+
if (args.help === true || args.h === true)
|
|
82
|
+
return 'none';
|
|
83
|
+
if (skill.createsTake) {
|
|
84
|
+
const downgraded = (skill.createsTakeUnless ?? []).some((key) => args[key] === true);
|
|
85
|
+
return downgraded ? 'join' : 'create';
|
|
86
|
+
}
|
|
87
|
+
return skill.joinsTake ? 'join' : 'none';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Attach this invocation to a take, publishing the result as `CONVERSATION_ID`.
|
|
91
|
+
* That variable is the only thing that makes an uploaded asset reachable from
|
|
92
|
+
* the web app — it rides the upload as `x-conversation-id` and lands in
|
|
93
|
+
* `file.conversation_id`.
|
|
94
|
+
*
|
|
95
|
+
* Writes `process.env` rather than returning a child-env patch, because the two
|
|
96
|
+
* dispatch paths consume it differently: spawned Python inherits the process
|
|
97
|
+
* environment, while http/builtin handlers run in-process and read it via
|
|
98
|
+
* resolveHttpContext(). Four of the seven take-joining skills are in-process
|
|
99
|
+
* handlers, so a child-env-only patch would silently do nothing for them.
|
|
100
|
+
* Safe here because the CLI runs exactly one skill per process.
|
|
101
|
+
*
|
|
102
|
+
* Two deliberate non-behaviours:
|
|
103
|
+
* - An already-set CONVERSATION_ID is left alone. The cloud agent injects its
|
|
104
|
+
* own, and so does our own parent when prepare-video-assets shells out to
|
|
105
|
+
* gen-image / gen-voice. Re-deriving would fork the conversation.
|
|
106
|
+
* - Any failure is downgraded to a warning. Attribution is a convenience;
|
|
107
|
+
* losing it must never cost the user a render they already paid for. The
|
|
108
|
+
* asset still uploads, just unattributed — exactly the old behaviour.
|
|
109
|
+
*/
|
|
110
|
+
async function applyTakeContext(skill, opts, auth) {
|
|
111
|
+
const mode = resolveTakeMode(skill, opts.parsedArgs);
|
|
112
|
+
if (mode === 'none')
|
|
113
|
+
return;
|
|
114
|
+
if ((process.env.CONVERSATION_ID ?? '').trim())
|
|
115
|
+
return;
|
|
116
|
+
if (!auth.token)
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
const ctx = await resolveHttpContext(skill.name, {
|
|
120
|
+
apiBaseUrl: auth.apiBaseUrl,
|
|
121
|
+
preflightToken: auth.token,
|
|
122
|
+
});
|
|
123
|
+
let take;
|
|
124
|
+
if (mode === 'create') {
|
|
125
|
+
const jobIdArg = opts.parsedArgs.job_id;
|
|
126
|
+
const jobId = typeof jobIdArg === 'number' ? jobIdArg : Number(jobIdArg);
|
|
127
|
+
const flagProject = opts.parsedArgs.project;
|
|
128
|
+
take = await ensureTake(ctx, {
|
|
129
|
+
jobId: Number.isFinite(jobId) ? jobId : undefined,
|
|
130
|
+
flagProjectId: typeof flagProject === 'string' ? flagProject : undefined,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
take = await findTake(ctx);
|
|
135
|
+
}
|
|
136
|
+
if (!take)
|
|
137
|
+
return;
|
|
138
|
+
process.env.CONVERSATION_ID = take.conversationId;
|
|
139
|
+
process.stdout.write(`📁 take ${take.created ? 'created' : 'reused'}: ${take.videoProjectId} ` +
|
|
140
|
+
`(project ${take.projectId}${take.attempt ? `, attempt ${take.attempt}` : ''})\n`);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
process.stderr.write(`⚠️ 无法归属本次产物(内容仍会正常生成,但不会出现在项目里): ${err.message}\n`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
61
146
|
async function runPython(skill, rawArgs, auth) {
|
|
62
147
|
if (!skill.scriptAbsolutePath) {
|
|
63
148
|
throw new SkillError(`skill ${skill.name} has entry.type=python but no scriptAbsolutePath`);
|
package/dist/skill-schema.d.ts
CHANGED
|
@@ -58,6 +58,29 @@ export interface RawSkillJson {
|
|
|
58
58
|
scriptPath?: string;
|
|
59
59
|
entry?: SkillEntry;
|
|
60
60
|
envVars?: string[];
|
|
61
|
+
/**
|
|
62
|
+
* Declares that this skill produces a finished video, so the dispatcher should
|
|
63
|
+
* ensure a "take" (VideoProject + conversation) before running it and inject the
|
|
64
|
+
* resulting CONVERSATION_ID. Declarative rather than a hardcoded skill name in
|
|
65
|
+
* runner.ts, so the pipeline's shape stays visible in the skill's own manifest.
|
|
66
|
+
*/
|
|
67
|
+
createsTake?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Declares that this skill contributes to a video but must never *start* one:
|
|
70
|
+
* attach to an existing take if there is one, otherwise carry on unattributed.
|
|
71
|
+
* A standalone `gen-image` run isn't a video, and an empty VideoProject is
|
|
72
|
+
* worse clutter than an unattributed asset.
|
|
73
|
+
*/
|
|
74
|
+
joinsTake?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Parsed-argv keys (snake_case, as produced by argv.ts) whose presence means
|
|
77
|
+
* *this* invocation won't finish a video — `render-video --resolve-only`
|
|
78
|
+
* resolves assets and stops. Such a run degrades from createsTake to
|
|
79
|
+
* joinsTake: it still attaches to a take its own pipeline already started,
|
|
80
|
+
* but never brings one — nor the lazily created 「未分类」 — into being for a
|
|
81
|
+
* pass that may end in a contract error and no video at all.
|
|
82
|
+
*/
|
|
83
|
+
createsTakeUnless?: string[];
|
|
61
84
|
}
|
|
62
85
|
/**
|
|
63
86
|
* Resolve a skill's auth mode, defaulting for records that predate the field.
|
package/dist/skill-schema.js
CHANGED
|
@@ -61,6 +61,37 @@ export function validateSkillJson(raw, skillId) {
|
|
|
61
61
|
if (raw.auth != null && !AUTH_VALUES.includes(raw.auth)) {
|
|
62
62
|
errors.push(`skill.json.auth must be one of ${AUTH_VALUES.join(' | ')}, got '${raw.auth}'`);
|
|
63
63
|
}
|
|
64
|
+
if (raw.createsTake != null && typeof raw.createsTake !== 'boolean') {
|
|
65
|
+
errors.push(`skill.json.createsTake must be a boolean, got '${String(raw.createsTake)}'`);
|
|
66
|
+
}
|
|
67
|
+
if (raw.joinsTake != null && typeof raw.joinsTake !== 'boolean') {
|
|
68
|
+
errors.push(`skill.json.joinsTake must be a boolean, got '${String(raw.joinsTake)}'`);
|
|
69
|
+
}
|
|
70
|
+
if (raw.createsTakeUnless != null) {
|
|
71
|
+
if (!Array.isArray(raw.createsTakeUnless) ||
|
|
72
|
+
raw.createsTakeUnless.some((f) => typeof f !== 'string' || f === '')) {
|
|
73
|
+
errors.push("skill.json.createsTakeUnless must be an array of non-empty argv keys");
|
|
74
|
+
}
|
|
75
|
+
else if (raw.createsTake !== true) {
|
|
76
|
+
// It only ever downgrades createsTake; on any other skill it would read as
|
|
77
|
+
// working while doing nothing.
|
|
78
|
+
errors.push("skill.json.createsTakeUnless requires createsTake=true");
|
|
79
|
+
}
|
|
80
|
+
else if (raw.createsTakeUnless.some((f) => f.includes('-'))) {
|
|
81
|
+
// argv.ts hands the dispatcher snake_case keys, so `--resolve-only`
|
|
82
|
+
// arrives as `resolve_only`. A kebab entry would silently never match.
|
|
83
|
+
errors.push("skill.json.createsTakeUnless entries must be snake_case argv keys (e.g. 'resolve_only', not 'resolve-only')");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (raw.createsTake === true && raw.joinsTake === true) {
|
|
87
|
+
errors.push("skill.json cannot set both 'createsTake' and 'joinsTake' — createsTake already implies joining");
|
|
88
|
+
}
|
|
89
|
+
// Attribution means calling ab-api as a specific user. An `auth: none` skill
|
|
90
|
+
// never touches the backend and produces no server-side file, so there is
|
|
91
|
+
// nothing to attribute — the flag would be dead config that reads as working.
|
|
92
|
+
if ((raw.createsTake === true || raw.joinsTake === true) && resolveAuthMode(raw) === 'none') {
|
|
93
|
+
errors.push("skill.json declares createsTake/joinsTake but auth='none' — a purely local skill has nothing to attribute");
|
|
94
|
+
}
|
|
64
95
|
if (raw.name && raw.name !== skillId) {
|
|
65
96
|
errors.push(`skill.json.name='${raw.name}' does not match directory name '${skillId}'`);
|
|
66
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.18",
|
|
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",
|
|
@@ -6,26 +6,87 @@
|
|
|
6
6
|
"title": "Digital-Human Talking-Head",
|
|
7
7
|
"description": "Digital-human video: list available avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL.",
|
|
8
8
|
"auth": "required",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"joinsTake": true,
|
|
10
|
+
"envVars": [
|
|
11
|
+
"PRIV_TOKEN",
|
|
12
|
+
"MM_API_BASE_URL",
|
|
13
|
+
"AGENT_NAME"
|
|
14
|
+
],
|
|
15
|
+
"entry": {
|
|
16
|
+
"type": "http",
|
|
17
|
+
"handler": "gen-digital-human"
|
|
18
|
+
},
|
|
11
19
|
"parameters": {
|
|
12
20
|
"type": "object",
|
|
13
21
|
"properties": {
|
|
14
|
-
"list_avatars": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
"list_avatars": {
|
|
23
|
+
"type": "boolean",
|
|
24
|
+
"description": "List available digital-human avatars"
|
|
25
|
+
},
|
|
26
|
+
"mine": {
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"description": "With list_avatars=true: list the caller's own custom avatars instead of the public catalog. Custom avatars do not appear without this."
|
|
29
|
+
},
|
|
30
|
+
"name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "With list_avatars=true: fuzzy-filter avatars by name"
|
|
33
|
+
},
|
|
34
|
+
"source": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": [
|
|
37
|
+
"jimeng",
|
|
38
|
+
"hifly"
|
|
39
|
+
],
|
|
40
|
+
"description": "Provider: jimeng is image-driven, hifly is video-driven. Usually inferred from the avatar; pass it explicitly when the avatar declares no source."
|
|
41
|
+
},
|
|
42
|
+
"gender": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"enum": [
|
|
45
|
+
"male",
|
|
46
|
+
"female"
|
|
47
|
+
],
|
|
48
|
+
"description": "Filter by gender"
|
|
49
|
+
},
|
|
50
|
+
"avatar_id": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"description": "Avatar id"
|
|
53
|
+
},
|
|
54
|
+
"text": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Narration text (TTS mode)"
|
|
57
|
+
},
|
|
58
|
+
"audio_url": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Audio URL (audio-driven mode)"
|
|
61
|
+
},
|
|
62
|
+
"voice_id": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Voice id (TTS mode). Shares the Minimax catalog with gen-voice — call gen_voice with list_voices=true to see available ids rather than inventing one."
|
|
65
|
+
},
|
|
66
|
+
"voice_name": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Voice display name, recorded alongside voice_id for bookkeeping. Does not affect synthesis."
|
|
69
|
+
},
|
|
70
|
+
"aspect_ratio": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Aspect ratio: 9:16 / 16:9 / 3:4 / 1:1. Defaults to the avatar's own ratio."
|
|
73
|
+
},
|
|
74
|
+
"prompt": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Action prompt describing how the avatar should perform, e.g. 'more hand gestures'"
|
|
77
|
+
},
|
|
78
|
+
"check_status": {
|
|
79
|
+
"type": "boolean",
|
|
80
|
+
"description": "Status-check mode: poll an earlier job instead of starting a new one. Requires generation_id. Use this when a generate call timed out."
|
|
81
|
+
},
|
|
82
|
+
"generation_id": {
|
|
83
|
+
"type": "number",
|
|
84
|
+
"description": "Job id to poll (required when check_status=true)"
|
|
85
|
+
},
|
|
86
|
+
"json_output": {
|
|
87
|
+
"type": "boolean",
|
|
88
|
+
"description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output"
|
|
89
|
+
}
|
|
29
90
|
},
|
|
30
91
|
"required": []
|
|
31
92
|
}
|