@remixmate/cli 0.9.34 → 0.9.36
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/diagnostics.d.ts +21 -1
- package/dist/diagnostics.js +36 -5
- package/dist/http.d.ts +1 -1
- package/dist/http.js +2 -2
- package/dist/install.d.ts +1 -0
- package/dist/install.js +1 -1
- package/dist/manifest.json +2 -2
- package/dist/project/take.d.ts +4 -0
- package/dist/project/take.js +4 -0
- package/dist/project/trace.d.ts +98 -0
- package/dist/project/trace.js +278 -0
- package/dist/runner.js +58 -13
- package/package.json +1 -1
- package/skills/export-jianying/SKILL.md +1 -1
- package/skills/export-jianying/scripts/gen_jianying_draft.py +2 -2
- package/skills/gen-digital-human/SKILL.md +1 -1
- package/skills/gen-image/SKILL.md +1 -1
- package/skills/gen-video/SKILL.md +1 -1
- package/skills/gen-voice/SKILL.md +1 -1
- package/skills/prepare-video-assets/SKILL.md +2 -2
- package/skills/render-video/SKILL.md +3 -3
- package/skills/render-video/scripts/_video_probe.py +1 -1
- package/skills/render-video/scripts/_vod_polling.py +1 -1
- package/skills/render-video/scripts/remote_renderer_client.py +2 -2
- package/skills/render-video/scripts/upload_video.py +3 -3
- package/skills/template-registry/README.md +1 -1
- package/skills/template-registry/SKILL.md +2 -2
- package/skills/template-registry/scripts/registry_loader.py +3 -3
- package/skills/template-registry/scripts/render_job_client.py +2 -2
- package/skills/video-parser/SKILL.md +1 -1
- package/skills/video-parser/scripts/deconstruct_video.py +2 -2
- package/skills/video-parser/scripts/parse_via_render.py +2 -2
- package/skills/web-screenshot/scripts/_media_upload.py +1 -1
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
export declare function traceHeaders(): Record<string, string>;
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Redact a string for anything that leaves this process (diagnostic lines,
|
|
4
|
+
* execution traces).
|
|
5
|
+
*
|
|
6
|
+
* Kept in lockstep with ab-agent's `lib/telemetry.cleanText` — the two feed the
|
|
7
|
+
* same `media_chat_turn_trace` table, and a table whose rows were sanitized by
|
|
8
|
+
* two different rulesets is a table nobody can reason about. ANSI stripping,
|
|
9
|
+
* e-mail and phone redaction were added here to close that gap.
|
|
10
|
+
*
|
|
11
|
+
* `max` caps the result in BYTES, keeping the head. Callers that need the tail
|
|
12
|
+
* (a command's decisive line is its last one) slice before calling.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cleanDiagnosticText(value: string, max?: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Exit code → the outcome vocabulary of the logging contract.
|
|
17
|
+
*
|
|
18
|
+
* Exported because the execution trace (project/trace.ts) reports the same
|
|
19
|
+
* verdict for the same run: two mappings would let a run be `failure` in the
|
|
20
|
+
* diagnostic line and `success` in the database.
|
|
21
|
+
*/
|
|
22
|
+
export declare function outcomeOf(exitCode: number): 'success' | 'cancelled' | 'rejected' | 'failure';
|
|
3
23
|
/** Opt-in line framing for hosts; stdout is exclusively the existing skill protocol. */
|
|
4
24
|
export declare function diagnostic(exitCode: number, durationMs: number, error?: unknown): void;
|
package/dist/diagnostics.js
CHANGED
|
@@ -8,8 +8,22 @@ export function traceHeaders() {
|
|
|
8
8
|
process.env.AB_OPERATION_ID ||= randomUUID();
|
|
9
9
|
return { traceparent: header };
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Redact a string for anything that leaves this process (diagnostic lines,
|
|
13
|
+
* execution traces).
|
|
14
|
+
*
|
|
15
|
+
* Kept in lockstep with ab-agent's `lib/telemetry.cleanText` — the two feed the
|
|
16
|
+
* same `media_chat_turn_trace` table, and a table whose rows were sanitized by
|
|
17
|
+
* two different rulesets is a table nobody can reason about. ANSI stripping,
|
|
18
|
+
* e-mail and phone redaction were added here to close that gap.
|
|
19
|
+
*
|
|
20
|
+
* `max` caps the result in BYTES, keeping the head. Callers that need the tail
|
|
21
|
+
* (a command's decisive line is its last one) slice before calling.
|
|
22
|
+
*/
|
|
23
|
+
export function cleanDiagnosticText(value, max = 2048) {
|
|
24
|
+
let text = value
|
|
25
|
+
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
26
|
+
.replace(/https?:\/\/[^\s"'<>]+/g, raw => {
|
|
13
27
|
try {
|
|
14
28
|
const url = new URL(raw);
|
|
15
29
|
return url.origin + url.pathname;
|
|
@@ -17,19 +31,36 @@ export function cleanDiagnosticText(value) {
|
|
|
17
31
|
catch {
|
|
18
32
|
return '[url]';
|
|
19
33
|
}
|
|
20
|
-
})
|
|
34
|
+
})
|
|
35
|
+
.replace(/(Bearer\s+|(?:token|secret|password|api[_-]?key)\s*[=:]\s*)[^\s,;}]+/gi, '$1[redacted]')
|
|
36
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[email]')
|
|
37
|
+
.replace(/\b1[3-9]\d{9}\b/g, '[phone]');
|
|
21
38
|
for (const [key, secret] of Object.entries(process.env)) {
|
|
22
39
|
if (/TOKEN|SECRET|PASSWORD|API_KEY/i.test(key) && secret && secret.length >= 6)
|
|
23
40
|
text = text.split(secret).join('[redacted]');
|
|
24
41
|
}
|
|
25
|
-
return Buffer.from(text).subarray(0,
|
|
42
|
+
return Buffer.from(text).subarray(0, max).toString('utf8');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Exit code → the outcome vocabulary of the logging contract.
|
|
46
|
+
*
|
|
47
|
+
* Exported because the execution trace (project/trace.ts) reports the same
|
|
48
|
+
* verdict for the same run: two mappings would let a run be `failure` in the
|
|
49
|
+
* diagnostic line and `success` in the database.
|
|
50
|
+
*/
|
|
51
|
+
export function outcomeOf(exitCode) {
|
|
52
|
+
if (exitCode === 0)
|
|
53
|
+
return 'success';
|
|
54
|
+
if (exitCode === 130)
|
|
55
|
+
return 'cancelled';
|
|
56
|
+
return [2, 3, 4].includes(exitCode) ? 'rejected' : 'failure';
|
|
26
57
|
}
|
|
27
58
|
/** Opt-in line framing for hosts; stdout is exclusively the existing skill protocol. */
|
|
28
59
|
export function diagnostic(exitCode, durationMs, error) {
|
|
29
60
|
if (process.env.AB_DIAGNOSTICS !== '1')
|
|
30
61
|
return;
|
|
31
62
|
traceHeaders();
|
|
32
|
-
const outcome =
|
|
63
|
+
const outcome = outcomeOf(exitCode);
|
|
33
64
|
const record = { schema_version: 1, timestamp: new Date().toISOString(), service: 'remixmate-cli', environment: process.env.APP_ENV ?? 'dev',
|
|
34
65
|
level: outcome === 'failure' ? 'error' : 'info', event: 'cli.execution.completed', message: 'CLI execution completed', outcome,
|
|
35
66
|
trace_id: process.env.AB_TRACEPARENT?.split('-')[1], operation_id: process.env.AB_OPERATION_ID,
|
package/dist/http.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 1. opts.apiBaseUrl — caller override
|
|
17
17
|
* 2. process.env.MM_API_BASE_URL — process env (point at local/staging here)
|
|
18
18
|
* 3. process.env.MM_BACKEND_API_URL — ab-agent's convention (back-compat)
|
|
19
|
-
* 4. https://api.remixmate.
|
|
19
|
+
* 4. https://api.remixmate.ai/api — production default (zero-config)
|
|
20
20
|
*/
|
|
21
21
|
import { type Billing } from './billing.js';
|
|
22
22
|
export { SkillError, EXIT } from './errors.js';
|
package/dist/http.js
CHANGED
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
* 1. opts.apiBaseUrl — caller override
|
|
17
17
|
* 2. process.env.MM_API_BASE_URL — process env (point at local/staging here)
|
|
18
18
|
* 3. process.env.MM_BACKEND_API_URL — ab-agent's convention (back-compat)
|
|
19
|
-
* 4. https://api.remixmate.
|
|
19
|
+
* 4. https://api.remixmate.ai/api — production default (zero-config)
|
|
20
20
|
*/
|
|
21
21
|
import { traceHeaders, cleanDiagnosticText } from './diagnostics.js';
|
|
22
22
|
import { recordBilling } from './billing.js';
|
|
23
23
|
import { resolvePrivToken, NOT_AUTHENTICATED_HINT } from './auth/resolve.js';
|
|
24
24
|
import { attemptAutoLogin } from './auth/auto-login.js';
|
|
25
25
|
import { EXIT, SkillError } from './errors.js';
|
|
26
|
-
const DEFAULT_API_BASE_URL = 'https://api.remixmate.
|
|
26
|
+
const DEFAULT_API_BASE_URL = 'https://api.remixmate.ai/api';
|
|
27
27
|
// Re-exported so the many `import { SkillError } from '../http.js'` call sites
|
|
28
28
|
// stay valid; the definition moved to errors.ts to break an import cycle with
|
|
29
29
|
// the auth gateway (see errors.ts).
|
package/dist/install.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare function detectHosts(): Host[];
|
|
|
37
37
|
* 完全看不到它,只会以为这个 CLI 不支持 —— 正是这次 WorkBuddy 的翻车方式。
|
|
38
38
|
*/
|
|
39
39
|
export declare function projectOnlyHosts(): HostId[];
|
|
40
|
+
export declare function packageVersion(): string;
|
|
40
41
|
export declare function installUsage(): void;
|
|
41
42
|
export declare function uninstallUsage(): void;
|
|
42
43
|
export declare function runInstall(argv: string[]): number;
|
package/dist/install.js
CHANGED
|
@@ -147,7 +147,7 @@ export function detectHosts() {
|
|
|
147
147
|
export function projectOnlyHosts() {
|
|
148
148
|
return HOST_IDS.filter((id) => isInstalled(id) && !spec(id).userDirName);
|
|
149
149
|
}
|
|
150
|
-
function packageVersion() {
|
|
150
|
+
export function packageVersion() {
|
|
151
151
|
try {
|
|
152
152
|
const raw = readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf-8');
|
|
153
153
|
return JSON.parse(raw).version ?? 'unknown';
|
package/dist/manifest.json
CHANGED
package/dist/project/take.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface EnsuredTake {
|
|
|
23
23
|
projectId: string;
|
|
24
24
|
created: boolean;
|
|
25
25
|
attempt: number;
|
|
26
|
+
/** The grouping key this take was resolved under — reused as the trace's session id. */
|
|
27
|
+
sessionKey: string;
|
|
28
|
+
/** Provenance label of the host we're running inside ('' when unknown). */
|
|
29
|
+
host: string;
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* Look up the take for the current grouping key without creating anything.
|
package/dist/project/take.js
CHANGED
|
@@ -62,6 +62,8 @@ export async function findTake(ctx) {
|
|
|
62
62
|
projectId: res.projectId ?? '',
|
|
63
63
|
created: false,
|
|
64
64
|
attempt: res.attempt ?? 0,
|
|
65
|
+
sessionKey: takeKey.key,
|
|
66
|
+
host: takeKey.host,
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
/**
|
|
@@ -98,5 +100,7 @@ export async function ensureTake(ctx, opts) {
|
|
|
98
100
|
projectId,
|
|
99
101
|
created: res.created,
|
|
100
102
|
attempt: res.attempt,
|
|
103
|
+
sessionKey: takeKey.key,
|
|
104
|
+
host: takeKey.host,
|
|
101
105
|
};
|
|
102
106
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution-trace self-reporting — tell the backend what this invocation
|
|
3
|
+
* actually did, so a locally-run pipeline is not a blind spot.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: the cloud agent records every tool call, its arguments AND
|
|
6
|
+
* its result into `media_chat_turn_trace` (ab-agent/src/trace-persist.ts). The
|
|
7
|
+
* local path records nothing — a take created from codex / claude code has an
|
|
8
|
+
* empty conversation, zero tool calls and zero credits in the analytics tables,
|
|
9
|
+
* so every session-level metric silently excludes it.
|
|
10
|
+
*
|
|
11
|
+
* What we can honestly report and what we cannot: the CLI is a child process
|
|
12
|
+
* spawned once per skill call. It never sees the user's prompt or the host's
|
|
13
|
+
* reply — those live in the host's own transcript. So this module reports the
|
|
14
|
+
* one thing it genuinely observes: the invocation itself.
|
|
15
|
+
*
|
|
16
|
+
* See docs/cli-session-record-design.md §5.
|
|
17
|
+
*/
|
|
18
|
+
import { type HttpContext } from '../http.js';
|
|
19
|
+
import type { ParsedArgs } from '../argv.js';
|
|
20
|
+
import type { EnsuredTake } from './take.js';
|
|
21
|
+
export interface TraceOptions {
|
|
22
|
+
skillName: string;
|
|
23
|
+
args: ParsedArgs;
|
|
24
|
+
take: EnsuredTake;
|
|
25
|
+
ctx: HttpContext;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether this invocation should report at all.
|
|
29
|
+
*
|
|
30
|
+
* The rule is deliberately narrow: report only when THIS process resolved the
|
|
31
|
+
* take. Everything else is already covered by someone else —
|
|
32
|
+
* - under the cloud agent, CONVERSATION_ID arrives pre-set and ab-agent's own
|
|
33
|
+
* TurnTracer already recorded the very same call;
|
|
34
|
+
* - a nested `gen-image` spawned by prepare-video-assets inherits its
|
|
35
|
+
* parent's CONVERSATION_ID, and the parent's row already covers it.
|
|
36
|
+
* Since runner.ts only builds a tracer after resolving a take itself, this
|
|
37
|
+
* function just honours the kill switch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function tracingEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Collects one invocation's evidence and posts it as a single turn.
|
|
42
|
+
*
|
|
43
|
+
* Lifecycle (runner.ts): `begin()` → output flows through the tap → `finish()`
|
|
44
|
+
* in a finally block. Every failure mode ends in a warning on stderr: a lost
|
|
45
|
+
* trace row must never cost the user a render they already paid for.
|
|
46
|
+
*/
|
|
47
|
+
export declare class InvocationTrace {
|
|
48
|
+
private readonly opts;
|
|
49
|
+
private chunks;
|
|
50
|
+
private restore;
|
|
51
|
+
private flushed;
|
|
52
|
+
private constructor();
|
|
53
|
+
/** Returns null when tracing is off — callers can then skip output capture entirely. */
|
|
54
|
+
static begin(opts: TraceOptions): InvocationTrace | null;
|
|
55
|
+
/**
|
|
56
|
+
* Tap both output streams.
|
|
57
|
+
*
|
|
58
|
+
* One tap covers both dispatch paths: in-process handlers write here
|
|
59
|
+
* directly, and the Python child's piped output is re-emitted through the
|
|
60
|
+
* same `write` (see runner.ts). Restored in finish().
|
|
61
|
+
*/
|
|
62
|
+
installCapture(): void;
|
|
63
|
+
/** Feed captured output. Keeps a bounded tail — the decisive line is the last one. */
|
|
64
|
+
absorb(chunk: unknown): void;
|
|
65
|
+
/**
|
|
66
|
+
* Emit the turn. Idempotent; never throws.
|
|
67
|
+
*
|
|
68
|
+
* `error` is the dispatcher's caught exception when there is one — a Python
|
|
69
|
+
* child that exits non-zero gives us no exception, only its output, which is
|
|
70
|
+
* exactly why the tail buffer exists.
|
|
71
|
+
*/
|
|
72
|
+
finish(exitCode: number, durationMs: number, error?: unknown): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Flags → a trace-safe object.
|
|
76
|
+
*
|
|
77
|
+
* Credentials are dropped outright; long strings are cut to their opening
|
|
78
|
+
* because an argument like `--prompt` is *content*, and this table is evidence,
|
|
79
|
+
* not an archive of what the user wrote.
|
|
80
|
+
*/
|
|
81
|
+
export declare function sanitiseArgs(args: ParsedArgs): Record<string, unknown>;
|
|
82
|
+
/**
|
|
83
|
+
* Captured output → the tail we store.
|
|
84
|
+
*
|
|
85
|
+
* Two transformations before truncating, both about spending the 16KB on
|
|
86
|
+
* things a human would read:
|
|
87
|
+
* - progress lines are the machine protocol — one fat JSON object per phase,
|
|
88
|
+
* marked by a `__progress__` field (progress.ts, and the same shape from the
|
|
89
|
+
* Python skills). Their payload is noise here, but WHICH PHASE a failed run
|
|
90
|
+
* reached is the single most useful fact about it, so the line collapses to
|
|
91
|
+
* its phase name instead of being dropped.
|
|
92
|
+
* - `__diagnostic_v1__` lines are our own structured log of this very run;
|
|
93
|
+
* keeping them would store the same verdict twice.
|
|
94
|
+
*
|
|
95
|
+
* Then keeps the TAIL, unlike ab-agent's head-first cleanText: a command's
|
|
96
|
+
* verdict — the error, or the URL it produced — is its last line.
|
|
97
|
+
*/
|
|
98
|
+
export declare function condense(raw: string): string;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution-trace self-reporting — tell the backend what this invocation
|
|
3
|
+
* actually did, so a locally-run pipeline is not a blind spot.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: the cloud agent records every tool call, its arguments AND
|
|
6
|
+
* its result into `media_chat_turn_trace` (ab-agent/src/trace-persist.ts). The
|
|
7
|
+
* local path records nothing — a take created from codex / claude code has an
|
|
8
|
+
* empty conversation, zero tool calls and zero credits in the analytics tables,
|
|
9
|
+
* so every session-level metric silently excludes it.
|
|
10
|
+
*
|
|
11
|
+
* What we can honestly report and what we cannot: the CLI is a child process
|
|
12
|
+
* spawned once per skill call. It never sees the user's prompt or the host's
|
|
13
|
+
* reply — those live in the host's own transcript. So this module reports the
|
|
14
|
+
* one thing it genuinely observes: the invocation itself.
|
|
15
|
+
*
|
|
16
|
+
* See docs/cli-session-record-design.md §5.
|
|
17
|
+
*/
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import { cleanDiagnosticText, outcomeOf } from '../diagnostics.js';
|
|
20
|
+
import { billingSummary } from '../billing.js';
|
|
21
|
+
import { packageVersion } from '../install.js';
|
|
22
|
+
import { mmPost } from '../http.js';
|
|
23
|
+
/** Same ceiling ab-agent uses for a tool result — one table, one limit. */
|
|
24
|
+
const TOOL_RESULT_MAX = 16 * 1024;
|
|
25
|
+
/** Raw bytes held before trimming. Generous: trimming is the tail, cleaning is cheap. */
|
|
26
|
+
const RAW_CAPTURE_MAX = 64 * 1024;
|
|
27
|
+
/** Per-argument ceiling. Prompts and narration bodies are content, not evidence. */
|
|
28
|
+
const ARG_VALUE_MAX = 512;
|
|
29
|
+
/** `error_summary` is varchar(1024). */
|
|
30
|
+
const ERROR_SUMMARY_MAX = 1024;
|
|
31
|
+
const FLUSH_TIMEOUT_MS = 10_000;
|
|
32
|
+
const SENSITIVE_KEY = /token|secret|password|api[_-]?key|authorization/i;
|
|
33
|
+
/**
|
|
34
|
+
* Whether this invocation should report at all.
|
|
35
|
+
*
|
|
36
|
+
* The rule is deliberately narrow: report only when THIS process resolved the
|
|
37
|
+
* take. Everything else is already covered by someone else —
|
|
38
|
+
* - under the cloud agent, CONVERSATION_ID arrives pre-set and ab-agent's own
|
|
39
|
+
* TurnTracer already recorded the very same call;
|
|
40
|
+
* - a nested `gen-image` spawned by prepare-video-assets inherits its
|
|
41
|
+
* parent's CONVERSATION_ID, and the parent's row already covers it.
|
|
42
|
+
* Since runner.ts only builds a tracer after resolving a take itself, this
|
|
43
|
+
* function just honours the kill switch.
|
|
44
|
+
*/
|
|
45
|
+
export function tracingEnabled(env = process.env) {
|
|
46
|
+
return (env.REMIXMATE_TRACE ?? '').trim() !== '0';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Collects one invocation's evidence and posts it as a single turn.
|
|
50
|
+
*
|
|
51
|
+
* Lifecycle (runner.ts): `begin()` → output flows through the tap → `finish()`
|
|
52
|
+
* in a finally block. Every failure mode ends in a warning on stderr: a lost
|
|
53
|
+
* trace row must never cost the user a render they already paid for.
|
|
54
|
+
*/
|
|
55
|
+
export class InvocationTrace {
|
|
56
|
+
opts;
|
|
57
|
+
chunks = '';
|
|
58
|
+
restore = null;
|
|
59
|
+
flushed = false;
|
|
60
|
+
constructor(opts) {
|
|
61
|
+
this.opts = opts;
|
|
62
|
+
}
|
|
63
|
+
/** Returns null when tracing is off — callers can then skip output capture entirely. */
|
|
64
|
+
static begin(opts) {
|
|
65
|
+
if (!tracingEnabled())
|
|
66
|
+
return null;
|
|
67
|
+
if (!opts.take.conversationId)
|
|
68
|
+
return null;
|
|
69
|
+
return new InvocationTrace(opts);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Tap both output streams.
|
|
73
|
+
*
|
|
74
|
+
* One tap covers both dispatch paths: in-process handlers write here
|
|
75
|
+
* directly, and the Python child's piped output is re-emitted through the
|
|
76
|
+
* same `write` (see runner.ts). Restored in finish().
|
|
77
|
+
*/
|
|
78
|
+
installCapture() {
|
|
79
|
+
if (this.restore)
|
|
80
|
+
return;
|
|
81
|
+
const out = process.stdout.write.bind(process.stdout);
|
|
82
|
+
const err = process.stderr.write.bind(process.stderr);
|
|
83
|
+
const tap = (original) => ((chunk, ...rest) => {
|
|
84
|
+
this.absorb(chunk);
|
|
85
|
+
return original(chunk, ...rest);
|
|
86
|
+
});
|
|
87
|
+
process.stdout.write = tap(out);
|
|
88
|
+
process.stderr.write = tap(err);
|
|
89
|
+
this.restore = () => {
|
|
90
|
+
process.stdout.write = out;
|
|
91
|
+
process.stderr.write = err;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Feed captured output. Keeps a bounded tail — the decisive line is the last one. */
|
|
95
|
+
absorb(chunk) {
|
|
96
|
+
try {
|
|
97
|
+
const text = typeof chunk === 'string'
|
|
98
|
+
? chunk
|
|
99
|
+
: chunk instanceof Uint8Array
|
|
100
|
+
? Buffer.from(chunk).toString('utf8')
|
|
101
|
+
: '';
|
|
102
|
+
if (!text)
|
|
103
|
+
return;
|
|
104
|
+
this.chunks += text;
|
|
105
|
+
if (this.chunks.length > RAW_CAPTURE_MAX * 2) {
|
|
106
|
+
this.chunks = this.chunks.slice(-RAW_CAPTURE_MAX);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Capturing must never break the thing being captured.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Emit the turn. Idempotent; never throws.
|
|
115
|
+
*
|
|
116
|
+
* `error` is the dispatcher's caught exception when there is one — a Python
|
|
117
|
+
* child that exits non-zero gives us no exception, only its output, which is
|
|
118
|
+
* exactly why the tail buffer exists.
|
|
119
|
+
*/
|
|
120
|
+
async finish(exitCode, durationMs, error) {
|
|
121
|
+
this.restore?.();
|
|
122
|
+
this.restore = null;
|
|
123
|
+
if (this.flushed)
|
|
124
|
+
return;
|
|
125
|
+
this.flushed = true;
|
|
126
|
+
const { skillName, args, take, ctx } = this.opts;
|
|
127
|
+
const output = condense(this.chunks);
|
|
128
|
+
const outcome = outcomeOf(exitCode);
|
|
129
|
+
const isError = exitCode !== 0;
|
|
130
|
+
const rows = [];
|
|
131
|
+
let stepSeq = 0;
|
|
132
|
+
// One env row per take, not per call: it describes the toolchain, and the
|
|
133
|
+
// toolchain does not change between two calls of the same pipeline.
|
|
134
|
+
if (take.created) {
|
|
135
|
+
rows.push({ sessionId: take.sessionKey, stepSeq: stepSeq++, kind: 'env', env: envSnapshot(take) });
|
|
136
|
+
}
|
|
137
|
+
rows.push({
|
|
138
|
+
sessionId: take.sessionKey,
|
|
139
|
+
stepSeq: stepSeq++,
|
|
140
|
+
kind: 'tool_call',
|
|
141
|
+
toolName: `cli__${skillName.replace(/-/g, '_')}`,
|
|
142
|
+
toolUseId: (process.env.AB_OPERATION_ID ?? '').slice(0, 64),
|
|
143
|
+
toolInput: sanitiseArgs(args),
|
|
144
|
+
toolResult: output || '[no output captured]',
|
|
145
|
+
isError,
|
|
146
|
+
durationMs: Math.round(durationMs),
|
|
147
|
+
});
|
|
148
|
+
rows.push({
|
|
149
|
+
sessionId: take.sessionKey,
|
|
150
|
+
stepSeq: stepSeq++,
|
|
151
|
+
kind: 'turn_result',
|
|
152
|
+
outcome,
|
|
153
|
+
errorSummary: isError ? summariseError(error, output) : '',
|
|
154
|
+
credits: billingSummary()?.credits ?? 0,
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
await mmPost(ctx, `/chat/conversation/${encodeURIComponent(take.conversationId)}/trace`,
|
|
158
|
+
// appendTurn: the server allocates turn_seq. N unrelated processes
|
|
159
|
+
// cannot number themselves, and cli-project-binding §5.1 already ruled
|
|
160
|
+
// out keeping local state. Older backends ignore the field and every
|
|
161
|
+
// row lands on turn 0 — still ordered, just ungrouped.
|
|
162
|
+
{ appendTurn: true, traces: rows }, { timeoutMs: FLUSH_TIMEOUT_MS });
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
process.stderr.write(`⚠️ 执行轨迹未能上报(不影响本次产物): ${cleanDiagnosticText(err.message, 256)}\n`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Which rung of the grouping ladder produced this key (cli-project-binding §5.1). */
|
|
170
|
+
function keySource(key) {
|
|
171
|
+
if (key.startsWith('custom:'))
|
|
172
|
+
return 'env-override';
|
|
173
|
+
if (key.startsWith('job:'))
|
|
174
|
+
return 'render-job';
|
|
175
|
+
return 'host';
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* What this take was produced by. Mirrors ab-agent's envSnapshot() in purpose:
|
|
179
|
+
* answer "which version started failing" without storing anything large.
|
|
180
|
+
*/
|
|
181
|
+
function envSnapshot(take) {
|
|
182
|
+
return {
|
|
183
|
+
// The discriminator every cross-channel metric needs — a CLI "turn" is one
|
|
184
|
+
// skill call, a cloud turn is a whole user request. Never compare blindly.
|
|
185
|
+
channel: 'cli',
|
|
186
|
+
cli_version: packageVersion(),
|
|
187
|
+
host: take.host || 'unknown',
|
|
188
|
+
take_key_source: keySource(take.sessionKey),
|
|
189
|
+
node: process.version,
|
|
190
|
+
platform: `${os.platform()}-${os.arch()}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Flags → a trace-safe object.
|
|
195
|
+
*
|
|
196
|
+
* Credentials are dropped outright; long strings are cut to their opening
|
|
197
|
+
* because an argument like `--prompt` is *content*, and this table is evidence,
|
|
198
|
+
* not an archive of what the user wrote.
|
|
199
|
+
*/
|
|
200
|
+
export function sanitiseArgs(args) {
|
|
201
|
+
const out = {};
|
|
202
|
+
for (const [key, value] of Object.entries(args).slice(0, 40)) {
|
|
203
|
+
if (SENSITIVE_KEY.test(key)) {
|
|
204
|
+
out[key] = '[redacted]';
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === 'boolean') {
|
|
208
|
+
out[key] = value;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
out[key] = value.slice(0, 20).map((v) => cleanDiagnosticText(String(v), ARG_VALUE_MAX));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
out[key] = cleanDiagnosticText(String(value), ARG_VALUE_MAX);
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Captured output → the tail we store.
|
|
221
|
+
*
|
|
222
|
+
* Two transformations before truncating, both about spending the 16KB on
|
|
223
|
+
* things a human would read:
|
|
224
|
+
* - progress lines are the machine protocol — one fat JSON object per phase,
|
|
225
|
+
* marked by a `__progress__` field (progress.ts, and the same shape from the
|
|
226
|
+
* Python skills). Their payload is noise here, but WHICH PHASE a failed run
|
|
227
|
+
* reached is the single most useful fact about it, so the line collapses to
|
|
228
|
+
* its phase name instead of being dropped.
|
|
229
|
+
* - `__diagnostic_v1__` lines are our own structured log of this very run;
|
|
230
|
+
* keeping them would store the same verdict twice.
|
|
231
|
+
*
|
|
232
|
+
* Then keeps the TAIL, unlike ab-agent's head-first cleanText: a command's
|
|
233
|
+
* verdict — the error, or the URL it produced — is its last line.
|
|
234
|
+
*/
|
|
235
|
+
export function condense(raw) {
|
|
236
|
+
if (!raw)
|
|
237
|
+
return '';
|
|
238
|
+
const compacted = raw
|
|
239
|
+
.split('\n')
|
|
240
|
+
.map((line) => {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (trimmed.startsWith('__diagnostic_v1__'))
|
|
243
|
+
return null;
|
|
244
|
+
if (!trimmed.startsWith('{') || !trimmed.includes('"__progress__"'))
|
|
245
|
+
return line;
|
|
246
|
+
try {
|
|
247
|
+
const phase = JSON.parse(trimmed).phase;
|
|
248
|
+
return typeof phase === 'string' ? `· phase=${phase}` : '· progress';
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return '· progress';
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
.filter((line) => line !== null)
|
|
255
|
+
.join('\n')
|
|
256
|
+
.trim();
|
|
257
|
+
// Slice the tail before cleaning: cleanDiagnosticText truncates head-first.
|
|
258
|
+
const tail = compacted.length > TOOL_RESULT_MAX ? compacted.slice(-TOOL_RESULT_MAX) : compacted;
|
|
259
|
+
return cleanDiagnosticText(tail, TOOL_RESULT_MAX);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The one-line verdict.
|
|
263
|
+
*
|
|
264
|
+
* A thrown SkillError carries it directly. A Python child does not throw — it
|
|
265
|
+
* just exits non-zero, so fall back to the last line it printed, which is where
|
|
266
|
+
* argparse errors, tracebacks and our own ❌ messages end up.
|
|
267
|
+
*/
|
|
268
|
+
function summariseError(error, output) {
|
|
269
|
+
if (error instanceof Error && error.message) {
|
|
270
|
+
return cleanDiagnosticText(error.message, ERROR_SUMMARY_MAX);
|
|
271
|
+
}
|
|
272
|
+
const lastLine = output
|
|
273
|
+
.split('\n')
|
|
274
|
+
.map((l) => l.trim())
|
|
275
|
+
.filter((l) => l && !l.startsWith('· phase='))
|
|
276
|
+
.pop();
|
|
277
|
+
return lastLine ? cleanDiagnosticText(lastLine, ERROR_SUMMARY_MAX) : '';
|
|
278
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -19,6 +19,7 @@ import { authChildEnv, ensureAuth } from './auth/ensure.js';
|
|
|
19
19
|
import { emitBillingFooter } from './billing.js';
|
|
20
20
|
import { resolveHttpContext } from './http.js';
|
|
21
21
|
import { ensureTake, findTake } from './project/take.js';
|
|
22
|
+
import { InvocationTrace } from './project/trace.js';
|
|
22
23
|
/** Read the token override accepted by both the TS handlers and the Python skills. */
|
|
23
24
|
function tokenFlag(args) {
|
|
24
25
|
const value = args.token ?? args.priv_token;
|
|
@@ -28,6 +29,7 @@ export async function runSkill(skillName, opts) {
|
|
|
28
29
|
const started = performance.now();
|
|
29
30
|
let exitCode = EXIT.ERROR;
|
|
30
31
|
let failure;
|
|
32
|
+
let trace = null;
|
|
31
33
|
traceHeaders();
|
|
32
34
|
const skill = findSkill(skillName, opts.baseDir ?? SKILLS_DIR);
|
|
33
35
|
if (!skill) {
|
|
@@ -41,25 +43,43 @@ export async function runSkill(skillName, opts) {
|
|
|
41
43
|
apiBaseUrl: typeof opts.parsedArgs.api_base_url === 'string' ? opts.parsedArgs.api_base_url : undefined,
|
|
42
44
|
flagToken: tokenFlag(opts.parsedArgs),
|
|
43
45
|
});
|
|
44
|
-
await applyTakeContext(skill, opts, auth);
|
|
46
|
+
const attached = await applyTakeContext(skill, opts, auth);
|
|
47
|
+
if (attached) {
|
|
48
|
+
// Only a take THIS process resolved gets reported — see trace.ts for why
|
|
49
|
+
// the cloud path and nested pipeline children must stay silent.
|
|
50
|
+
trace = InvocationTrace.begin({
|
|
51
|
+
skillName: skill.name,
|
|
52
|
+
args: opts.parsedArgs,
|
|
53
|
+
take: attached.take,
|
|
54
|
+
ctx: attached.ctx,
|
|
55
|
+
});
|
|
56
|
+
trace?.installCapture();
|
|
57
|
+
}
|
|
45
58
|
switch (skill.entry.type) {
|
|
46
59
|
case 'python':
|
|
47
60
|
// Python children talk to ab-api themselves and print their own billing
|
|
48
61
|
// footer; nothing was charged through this process.
|
|
49
|
-
|
|
62
|
+
exitCode = await runPython(skill, opts.rawArgs, auth, trace);
|
|
63
|
+
break;
|
|
50
64
|
case 'http':
|
|
51
65
|
case 'builtin':
|
|
52
|
-
|
|
66
|
+
exitCode = await runHandler(skill, opts.parsedArgs, auth);
|
|
67
|
+
break;
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
70
|
catch (err) {
|
|
56
71
|
failure = err;
|
|
57
72
|
if (err instanceof SkillError) {
|
|
58
73
|
process.stderr.write(cleanDiagnosticText(err.message) + '\n');
|
|
59
|
-
|
|
74
|
+
exitCode = err.exitCode;
|
|
60
75
|
}
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
else {
|
|
77
|
+
process.stderr.write(`❌ unexpected error: ${cleanDiagnosticText(err.stack ?? err.message)}\n`);
|
|
78
|
+
exitCode = EXIT.ERROR;
|
|
79
|
+
}
|
|
80
|
+
// Single exit point below: the trace flush in `finally` must run before the
|
|
81
|
+
// value leaves, and an `await` there defeats TS's reachability analysis on
|
|
82
|
+
// returns made from inside the try/catch.
|
|
63
83
|
}
|
|
64
84
|
finally {
|
|
65
85
|
// In `finally` because a run can fail *after* a billed step (e.g. the image
|
|
@@ -67,7 +87,12 @@ export async function runSkill(skillName, opts) {
|
|
|
67
87
|
// reported either way. No-ops when nothing was charged.
|
|
68
88
|
diagnostic(exitCode, performance.now() - started, failure);
|
|
69
89
|
emitBillingFooter();
|
|
90
|
+
// Last, so the billing footer is still inside the captured tail. Never
|
|
91
|
+
// throws and never changes the exit code — a lost trace row must not cost
|
|
92
|
+
// the user a render they already paid for.
|
|
93
|
+
await trace?.finish(exitCode, performance.now() - started, failure);
|
|
70
94
|
}
|
|
95
|
+
return exitCode;
|
|
71
96
|
}
|
|
72
97
|
/**
|
|
73
98
|
* Decide the take mode from the skill's declaration *and* what this particular
|
|
@@ -118,11 +143,11 @@ export function resolveTakeMode(skill, args) {
|
|
|
118
143
|
async function applyTakeContext(skill, opts, auth) {
|
|
119
144
|
const mode = resolveTakeMode(skill, opts.parsedArgs);
|
|
120
145
|
if (mode === 'none')
|
|
121
|
-
return;
|
|
146
|
+
return null;
|
|
122
147
|
if ((process.env.CONVERSATION_ID ?? '').trim())
|
|
123
|
-
return;
|
|
148
|
+
return null;
|
|
124
149
|
if (!auth.token)
|
|
125
|
-
return;
|
|
150
|
+
return null;
|
|
126
151
|
try {
|
|
127
152
|
const ctx = await resolveHttpContext(skill.name, {
|
|
128
153
|
apiBaseUrl: auth.apiBaseUrl,
|
|
@@ -142,16 +167,32 @@ async function applyTakeContext(skill, opts, auth) {
|
|
|
142
167
|
take = await findTake(ctx);
|
|
143
168
|
}
|
|
144
169
|
if (!take)
|
|
145
|
-
return;
|
|
170
|
+
return null;
|
|
146
171
|
process.env.CONVERSATION_ID = take.conversationId;
|
|
147
172
|
process.stdout.write(`📁 take ${take.created ? 'created' : 'reused'}: ${take.videoProjectId} ` +
|
|
148
173
|
`(project ${take.projectId}${take.attempt ? `, attempt ${take.attempt}` : ''})\n`);
|
|
174
|
+
return { take, ctx };
|
|
149
175
|
}
|
|
150
176
|
catch (err) {
|
|
151
177
|
process.stderr.write(`⚠️ 无法归属本次产物(内容仍会正常生成,但不会出现在项目里): ${cleanDiagnosticText(err.message)}\n`);
|
|
178
|
+
return null;
|
|
152
179
|
}
|
|
153
180
|
}
|
|
154
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Spawn the Python skill.
|
|
183
|
+
*
|
|
184
|
+
* `stdio: 'inherit'` is the default and stays the default: the child writes
|
|
185
|
+
* straight to the terminal, which is exactly what a CLI should do. It does mean
|
|
186
|
+
* the parent sees nothing, so a failed render can only ever be reported as
|
|
187
|
+
* "exit code 1" — for the most valuable step in the pipeline.
|
|
188
|
+
*
|
|
189
|
+
* So when (and only when) we are tracing, stdout/stderr are piped and re-emitted
|
|
190
|
+
* verbatim, which routes them through the capture tap. stdin stays inherited so
|
|
191
|
+
* interactive prompts still work. Resolving on `close` rather than `exit` in
|
|
192
|
+
* that mode matters: `exit` can fire while the last chunk is still in the pipe,
|
|
193
|
+
* which is precisely the chunk holding the error message.
|
|
194
|
+
*/
|
|
195
|
+
async function runPython(skill, rawArgs, auth, trace) {
|
|
155
196
|
if (!skill.scriptAbsolutePath) {
|
|
156
197
|
throw new SkillError(`skill ${skill.name} has entry.type=python but no scriptAbsolutePath`);
|
|
157
198
|
}
|
|
@@ -159,10 +200,14 @@ async function runPython(skill, rawArgs, auth) {
|
|
|
159
200
|
return await new Promise((resolve) => {
|
|
160
201
|
const proc = spawn('python3', [skill.scriptAbsolutePath, ...rawArgs], {
|
|
161
202
|
cwd,
|
|
162
|
-
stdio: 'inherit',
|
|
203
|
+
stdio: trace ? ['inherit', 'pipe', 'pipe'] : 'inherit',
|
|
163
204
|
env: { ...process.env, ...authChildEnv(auth), PYTHONUNBUFFERED: '1' },
|
|
164
205
|
});
|
|
165
|
-
|
|
206
|
+
if (trace) {
|
|
207
|
+
proc.stdout?.on('data', (chunk) => process.stdout.write(chunk));
|
|
208
|
+
proc.stderr?.on('data', (chunk) => process.stderr.write(chunk));
|
|
209
|
+
}
|
|
210
|
+
proc.on(trace ? 'close' : 'exit', (code) => resolve(code ?? EXIT.ERROR));
|
|
166
211
|
proc.on('error', (err) => {
|
|
167
212
|
process.stderr.write(`❌ failed to spawn python3: ${err.message}\n`);
|
|
168
213
|
resolve(EXIT.SPAWN_FAILED);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.36",
|
|
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",
|
|
@@ -37,7 +37,7 @@ There is no dedicated env file; the **process environment** variables of the cal
|
|
|
37
37
|
| Env var | Description | Default |
|
|
38
38
|
|---------|-------------|---------|
|
|
39
39
|
| `PRIV_TOKEN` | Tianyan token; overridable with `--priv-token`. | (none) |
|
|
40
|
-
| `MM_API_BASE_URL` | API root URL. | `https://api-agent.remixmate.
|
|
40
|
+
| `MM_API_BASE_URL` | API root URL. | `https://api-agent.remixmate.ai/api` |
|
|
41
41
|
| `AGENT_NAME` | Optional, sent as the `x-invoke-agent` request header. | (none) |
|
|
42
42
|
|
|
43
43
|
## Steps
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
python gen_jianying_draft.py --title "测试" --scenes scenes.json --download -o draft.zip
|
|
14
14
|
|
|
15
15
|
环境变量:
|
|
16
|
-
MM_API_BASE_URL - 后端 API 地址(默认: https://api-agent.remixmate.
|
|
16
|
+
MM_API_BASE_URL - 后端 API 地址(默认: https://api-agent.remixmate.ai/api)
|
|
17
17
|
PRIV_TOKEN - PrivToken(优先读取;未配置时提示手动输入)
|
|
18
18
|
"""
|
|
19
19
|
|
|
@@ -30,7 +30,7 @@ import urllib.request
|
|
|
30
30
|
import uuid
|
|
31
31
|
import zipfile
|
|
32
32
|
|
|
33
|
-
API_BASE_URL = os.environ.get("MM_API_BASE_URL", "https://api-agent.remixmate.
|
|
33
|
+
API_BASE_URL = os.environ.get("MM_API_BASE_URL", "https://api-agent.remixmate.ai/api")
|
|
34
34
|
PRIVATE_TOKEN = "" # 在 main() 中通过 resolve_token() 初始化
|
|
35
35
|
SKILL_NAME = "export-jianying"
|
|
36
36
|
AGENT_NAME = os.environ.get("AGENT_NAME", "")
|
|
@@ -44,7 +44,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
44
44
|
| Env var | Description | Default |
|
|
45
45
|
|---------|-------------|---------|
|
|
46
46
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
47
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.
|
|
47
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.ai/api` |
|
|
48
48
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
49
49
|
|
|
50
50
|
## Operations
|
|
@@ -52,7 +52,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
52
52
|
|---------|-------------|---------|
|
|
53
53
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
54
54
|
| `MM_IMAGE_MODEL` | Default model, as a `--model` value | backend default (`seedream`) |
|
|
55
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.
|
|
55
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.ai/api` |
|
|
56
56
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
57
57
|
|
|
58
58
|
## Operations
|
|
@@ -93,7 +93,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
93
93
|
|---------|-------------|---------|
|
|
94
94
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
95
95
|
| `MM_VIDEO_MODEL` | Default model id or shortcut | catalog default (`seedance-mini`) |
|
|
96
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.
|
|
96
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.ai/api` |
|
|
97
97
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
98
98
|
|
|
99
99
|
## Operations
|
|
@@ -29,7 +29,7 @@ There is no skill-local env file — the executing process inherits the system e
|
|
|
29
29
|
| Env var | Description | Default |
|
|
30
30
|
|---------|-------------|---------|
|
|
31
31
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
32
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.
|
|
32
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.ai/api` |
|
|
33
33
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
34
34
|
|
|
35
35
|
## Operations
|
|
@@ -64,8 +64,8 @@ The asset resolver invokes the following skills based on `AssetRef` declarations
|
|
|
64
64
|
| Env var | Description | Default |
|
|
65
65
|
|---------|-------------|---------|
|
|
66
66
|
| `PRIV_TOKEN` | Tianyan token (needed for asset generation + DB persist). | (none) |
|
|
67
|
-
| `MM_API_BASE_URL` | Asset-generation API root (ab-agent proxy). | `https://api-agent.remixmate.
|
|
68
|
-
| `MM_BACKEND_API_URL` | Backend API root (RenderPlan persistence). | `https://api.remixmate.
|
|
67
|
+
| `MM_API_BASE_URL` | Asset-generation API root (ab-agent proxy). | `https://api-agent.remixmate.ai/api` |
|
|
68
|
+
| `MM_BACKEND_API_URL` | Backend API root (RenderPlan persistence). | `https://api.remixmate.ai/api` |
|
|
69
69
|
| `ASSET_CACHE_DIR` | Asset cache directory. | `./.asset-cache/` |
|
|
70
70
|
|
|
71
71
|
## Canonical usage (inline DSL JSON, no temp files)
|
|
@@ -62,8 +62,8 @@ Rendering supports two modes:
|
|
|
62
62
|
| Env var | Description | Default |
|
|
63
63
|
|---------|-------------|---------|
|
|
64
64
|
| `PRIV_TOKEN` | Tianyan token (needed to load the RenderPlan from the DB and to upload). | (none) |
|
|
65
|
-
| `MM_BACKEND_API_URL` | Backend API root URL (RenderPlan / VOD lookups). | `https://api.remixmate.
|
|
66
|
-
| `REMOTION_RENDER_API_URL` | Remote renderer base URL (ab-render). Falls back to `RENDER_API_URL` (same value across skills). | `https://api-render.remixmate.
|
|
65
|
+
| `MM_BACKEND_API_URL` | Backend API root URL (RenderPlan / VOD lookups). | `https://api.remixmate.ai/api` |
|
|
66
|
+
| `REMOTION_RENDER_API_URL` | Remote renderer base URL (ab-render). Falls back to `RENDER_API_URL` (same value across skills). | `https://api-render.remixmate.ai` |
|
|
67
67
|
| `REMOTION_RENDER_MODE` | Default render mode (`remote` / `local`); CLI `--renderer` overrides it. | `remote` |
|
|
68
68
|
| `REMOTION_OUTPUT_DIR` | Render output directory. | `./output/` |
|
|
69
69
|
| `REMOTION_CONCURRENCY` | Local Remotion render concurrency. | `2` |
|
|
@@ -217,7 +217,7 @@ If the remote service is unreachable or auth fails, the script exits with a deta
|
|
|
217
217
|
The Remotion render project lives at the monorepo root under `<monorepo-root>/remotion-renderer/` and is deployed as a **standalone HTTP service**. The client script in this skill only sends POST requests and does not participate in server-side code sync.
|
|
218
218
|
|
|
219
219
|
- Server code + deployment docs: `<monorepo-root>/remotion-renderer/README.md`.
|
|
220
|
-
- Production (default): the client uses `https://api-render.remixmate.
|
|
220
|
+
- Production (default): the client uses `https://api-render.remixmate.ai` when `REMOTION_RENDER_API_URL` is unset — no config needed.
|
|
221
221
|
- Local debugging: in the renderer directory run `npm install && npm run server:dev`, then point the client at it with `REMOTION_RENDER_API_URL=http://localhost:3000`.
|
|
222
222
|
|
|
223
223
|
## Credits
|
|
@@ -61,7 +61,7 @@ def _query_duration_from_api(url: str) -> Optional[float]:
|
|
|
61
61
|
|
|
62
62
|
api_base = os.environ.get(
|
|
63
63
|
"MM_API_BASE_URL",
|
|
64
|
-
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.
|
|
64
|
+
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.ai/api"),
|
|
65
65
|
).rstrip("/")
|
|
66
66
|
if not api_base:
|
|
67
67
|
return None
|
|
@@ -42,7 +42,7 @@ def poll_vod_playback_url(
|
|
|
42
42
|
"""
|
|
43
43
|
api_base = os.environ.get(
|
|
44
44
|
"MM_API_BASE_URL",
|
|
45
|
-
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.
|
|
45
|
+
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.ai/api"),
|
|
46
46
|
).rstrip("/")
|
|
47
47
|
url = f"{api_base}/file/get"
|
|
48
48
|
headers = {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
Base URL 优先级:
|
|
20
20
|
1. 调用方显式传入 base_url
|
|
21
21
|
2. 环境变量 REMOTION_RENDER_API_URL(指向本地/staging 时在此覆盖)
|
|
22
|
-
3. 默认 https://api-render.remixmate.
|
|
22
|
+
3. 默认 https://api-render.remixmate.ai(生产,零配置可用)
|
|
23
23
|
|
|
24
24
|
本模块只负责 HTTP 层;调用方(render_video.py)负责把结果写回 render_plan。
|
|
25
25
|
"""
|
|
@@ -38,7 +38,7 @@ from pathlib import Path
|
|
|
38
38
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "template-registry" / "scripts"))
|
|
39
39
|
from log_diagnostics import trace_headers
|
|
40
40
|
|
|
41
|
-
DEFAULT_API_BASE_URL = "https://api-render.remixmate.
|
|
41
|
+
DEFAULT_API_BASE_URL = "https://api-render.remixmate.ai"
|
|
42
42
|
API_BASE_URL = (
|
|
43
43
|
os.environ.get("REMOTION_RENDER_API_URL")
|
|
44
44
|
or os.environ.get("RENDER_API_URL")
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
python3 upload_video.py --file output/video.mp4 --json-output
|
|
14
14
|
|
|
15
15
|
环境变量:
|
|
16
|
-
MM_API_BASE_URL - API 根地址(默认 MM_API_BASE_URL 环境变量或 https://api-agent.remixmate.
|
|
16
|
+
MM_API_BASE_URL - API 根地址(默认 MM_API_BASE_URL 环境变量或 https://api-agent.remixmate.ai/api)
|
|
17
17
|
|
|
18
18
|
注意:
|
|
19
19
|
- `uploadMediaFile` 接口视频大小限制 ≤ 50MB,音频 ≤ 15MB
|
|
@@ -31,7 +31,7 @@ import urllib.parse
|
|
|
31
31
|
import urllib.request
|
|
32
32
|
import uuid
|
|
33
33
|
|
|
34
|
-
DEFAULT_API_BASE_URL = "https://api-agent.remixmate.
|
|
34
|
+
DEFAULT_API_BASE_URL = "https://api-agent.remixmate.ai/api"
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _guess_content_type(file_path: str) -> str:
|
|
@@ -164,7 +164,7 @@ def main():
|
|
|
164
164
|
parser.add_argument(
|
|
165
165
|
"--api-base-url",
|
|
166
166
|
default=None,
|
|
167
|
-
help="API root URL (defaults to MM_API_BASE_URL env var or https://api-agent.remixmate.
|
|
167
|
+
help="API root URL (defaults to MM_API_BASE_URL env var or https://api-agent.remixmate.ai/api)",
|
|
168
168
|
)
|
|
169
169
|
parser.add_argument("--timeout", type=int, default=600, help="Upload timeout in seconds (default 600)")
|
|
170
170
|
parser.add_argument("--json-output", action="store_true", help="Emit the full response as JSON on stdout")
|
|
@@ -9,7 +9,7 @@ CLI 暴露面是「列出可用模板」(`--list-templates`)。把 Video DSL 场
|
|
|
9
9
|
| 来源 | 触发条件 | 适用场景 |
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| 显式 `VIDEO_TEMPLATE_REGISTRY_URL`(ab-api 接口) | 环境变量非空 + `PRIV_TOKEN` 有效 | 指定后端 |
|
|
12
|
-
| 派生默认 URL `<MM_API_BASE_URL>/remotionTemplate/registry` | `VIDEO_TEMPLATE_REGISTRY_URL` 未设置 | 默认(含独立安装 codex / `npm i -g`,默认 `https://api.remixmate.
|
|
12
|
+
| 派生默认 URL `<MM_API_BASE_URL>/remotionTemplate/registry` | `VIDEO_TEMPLATE_REGISTRY_URL` 未设置 | 默认(含独立安装 codex / `npm i -g`,默认 `https://api.remixmate.ai/api`) |
|
|
13
13
|
|
|
14
14
|
只从 ab-api 取数,是为了避免「本地文件 / monorepo 源码 / 数据库」多源并存导致的不一致——私有 / 多租户模板只存在于 ab-api,本地源永远不全。默认 URL **内置在 remixmate-cli 自身**(按 CLI 后端约定 `MM_API_BASE_URL` 推导),不再依赖 ab-agent 等宿主在 spawn 时注入,独立运行即可找到 registry。指向你自己的 ab-api 只需设 `MM_API_BASE_URL`(或直接设 `VIDEO_TEMPLATE_REGISTRY_URL`)并配置 `PRIV_TOKEN`。
|
|
15
15
|
|
|
@@ -63,12 +63,12 @@ The skill itself only reads the registry; whether a token is needed depends on t
|
|
|
63
63
|
| Env var | Description | Default |
|
|
64
64
|
|---------|-------------|---------|
|
|
65
65
|
| `VIDEO_TEMPLATE_REGISTRY_URL` | ab-api endpoint returning the registry (the single source of truth). | derived from `MM_API_BASE_URL` |
|
|
66
|
-
| `MM_API_BASE_URL` | ab-api base URL the CLI talks to. When `VIDEO_TEMPLATE_REGISTRY_URL` is unset, the registry endpoint is derived as `<base>/remotionTemplate/registry`. | `https://api.remixmate.
|
|
66
|
+
| `MM_API_BASE_URL` | ab-api base URL the CLI talks to. When `VIDEO_TEMPLATE_REGISTRY_URL` is unset, the registry endpoint is derived as `<base>/remotionTemplate/registry`. | `https://api.remixmate.ai/api` |
|
|
67
67
|
| `PRIV_TOKEN` | Sent as `X-Priv-Token` when hitting the registry endpoint. Falls back to `~/.config/remixmate/credentials.json`. | unset |
|
|
68
68
|
| `VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD` | `POST` (default) or `GET`. POST shape matches ab-api `{code,msg,data}`. | `POST` |
|
|
69
69
|
|
|
70
70
|
Resolution (single source — see `scripts/registry_loader.py` for the canonical implementation):
|
|
71
|
-
the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDEO_TEMPLATE_REGISTRY_URL`, otherwise `<MM_API_BASE_URL>/remotionTemplate/registry` (default `https://api.remixmate.
|
|
71
|
+
the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDEO_TEMPLATE_REGISTRY_URL`, otherwise `<MM_API_BASE_URL>/remotionTemplate/registry` (default `https://api.remixmate.ai/api/...`). There is no local-file / monorepo / PREFER_LOCAL fallback: those multi-source paths were removed to avoid registry skew (private/multi-tenant templates only exist on ab-api). On a transient HTTP failure the loader degrades to the on-disk cache of the same URL; with no cache it fails loud. A standalone install (Codex / `npm i -g`) just needs `MM_API_BASE_URL` (or `VIDEO_TEMPLATE_REGISTRY_URL`) pointed at your ab-api plus a valid `PRIV_TOKEN`.
|
|
72
72
|
|
|
73
73
|
## Steps
|
|
74
74
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
1. 显式 VIDEO_TEMPLATE_REGISTRY_URL(若设置则直接用)
|
|
16
16
|
2. 否则按 CLI 后端约定推导:
|
|
17
17
|
<MM_API_BASE_URL>/remotionTemplate/registry
|
|
18
|
-
MM_API_BASE_URL 默认 https://api.remixmate.
|
|
18
|
+
MM_API_BASE_URL 默认 https://api.remixmate.ai/api(与 src/http.ts 一致),
|
|
19
19
|
兼容 ab-agent 注入的 MM_BACKEND_API_URL。默认值内置在 CLI 自身,独立安装
|
|
20
20
|
(codex / npm i -g)开箱即用,无需宿主在 spawn 时注入。
|
|
21
21
|
|
|
@@ -92,7 +92,7 @@ _CACHE_DIR = os.environ.get(
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
# CLI 默认后端 base,与 remixmate-cli/src/http.ts 的 DEFAULT_API_BASE_URL 保持一致。
|
|
95
|
-
_DEFAULT_API_BASE_URL = "https://api.remixmate.
|
|
95
|
+
_DEFAULT_API_BASE_URL = "https://api.remixmate.ai/api"
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _default_registry_url() -> str:
|
|
@@ -104,7 +104,7 @@ def _default_registry_url() -> str:
|
|
|
104
104
|
base 解析顺序与 CLI / ab-agent 对齐:
|
|
105
105
|
MM_API_BASE_URL(CLI ``src/http.ts`` 约定)
|
|
106
106
|
→ MM_BACKEND_API_URL(ab-agent 约定,向后兼容)
|
|
107
|
-
→ https://api.remixmate.
|
|
107
|
+
→ https://api.remixmate.ai/api(生产默认,零配置)
|
|
108
108
|
路径段固定为 ``/remotionTemplate/registry``(POST,返回 ``{code,msg,data}``)。
|
|
109
109
|
"""
|
|
110
110
|
base = (
|
|
@@ -7,7 +7,7 @@ gen_jianying_draft.py 使用。
|
|
|
7
7
|
|
|
8
8
|
认证方式: X-Priv-Token header(与其他 skill 脚本一致)
|
|
9
9
|
接口地址: 优先读 MM_API_BASE_URL(executor 显式转发、全链路标准名),
|
|
10
|
-
回退 MM_BACKEND_API_URL,再回退默认 https://api.remixmate.
|
|
10
|
+
回退 MM_BACKEND_API_URL,再回退默认 https://api.remixmate.ai/api。
|
|
11
11
|
两个变量语义相同,保留回退是为兼容只配了其中一个的部署环境。
|
|
12
12
|
"""
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ import urllib.request
|
|
|
22
22
|
def _api_base() -> str:
|
|
23
23
|
return os.environ.get(
|
|
24
24
|
"MM_API_BASE_URL",
|
|
25
|
-
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.
|
|
25
|
+
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.ai/api"),
|
|
26
26
|
).rstrip("/")
|
|
27
27
|
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
30
30
|
|
|
31
31
|
| Env var | Description | Default |
|
|
32
32
|
|---------|-------------|---------|
|
|
33
|
-
| `RENDER_API_URL` | ab-render service base URL. Falls back to `REMOTION_RENDER_API_URL` (same value across skills), then the default. | `https://api-render.remixmate.
|
|
33
|
+
| `RENDER_API_URL` | ab-render service base URL. Falls back to `REMOTION_RENDER_API_URL` (same value across skills), then the default. | `https://api-render.remixmate.ai` |
|
|
34
34
|
| `PRIV_TOKEN` | Tianyan token, sent as `X-Priv-Token`. | (none) |
|
|
35
35
|
| `CONVERSATION_ID` | Optional, sent as `x-conversation-id` for file association. | (none) |
|
|
36
36
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
python deconstruct_video.py --url "https://example.com/video.mp4" --json-output
|
|
14
14
|
|
|
15
15
|
环境变量:
|
|
16
|
-
MM_API_BASE_URL - 后端 API 地址(默认: https://api-agent.remixmate.
|
|
16
|
+
MM_API_BASE_URL - 后端 API 地址(默认: https://api-agent.remixmate.ai/api)
|
|
17
17
|
PRIV_TOKEN - PrivToken 认证令牌
|
|
18
18
|
"""
|
|
19
19
|
|
|
@@ -27,7 +27,7 @@ import time
|
|
|
27
27
|
import urllib.error
|
|
28
28
|
import urllib.request
|
|
29
29
|
|
|
30
|
-
API_BASE_URL = os.environ.get("MM_API_BASE_URL", "https://api-agent.remixmate.
|
|
30
|
+
API_BASE_URL = os.environ.get("MM_API_BASE_URL", "https://api-agent.remixmate.ai/api")
|
|
31
31
|
|
|
32
32
|
PRIVATE_TOKEN = "" # 在 main() 中通过 resolve_token() 初始化
|
|
33
33
|
SKILL_NAME = "video-parser"
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
POST $RENDER_API_URL/parseStatus → { status, progress, result?, error? }
|
|
8
8
|
|
|
9
9
|
必要环境变量(由 ab-agent 自动注入):
|
|
10
|
-
RENDER_API_URL ab-render 服务地址(默认 https://api-render.remixmate.
|
|
10
|
+
RENDER_API_URL ab-render 服务地址(默认 https://api-render.remixmate.ai;指向本地/staging 时覆盖)
|
|
11
11
|
PRIV_TOKEN ab-api 私有 token
|
|
12
12
|
|
|
13
13
|
可选环境变量:
|
|
@@ -27,7 +27,7 @@ import urllib.request
|
|
|
27
27
|
RENDER_API_URL = (
|
|
28
28
|
os.environ.get("RENDER_API_URL")
|
|
29
29
|
or os.environ.get("REMOTION_RENDER_API_URL")
|
|
30
|
-
or "https://api-render.remixmate.
|
|
30
|
+
or "https://api-render.remixmate.ai"
|
|
31
31
|
).rstrip("/")
|
|
32
32
|
PRIV_TOKEN = os.environ.get("PRIV_TOKEN", "")
|
|
33
33
|
CONVERSATION_ID = os.environ.get("CONVERSATION_ID", "")
|
|
@@ -26,7 +26,7 @@ import urllib.request
|
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
from typing import Callable, Optional
|
|
28
28
|
|
|
29
|
-
DEFAULT_API_BASE_URL = "https://api-render.remixmate.
|
|
29
|
+
DEFAULT_API_BASE_URL = "https://api-render.remixmate.ai"
|
|
30
30
|
SKILL_NAME = "web-record"
|
|
31
31
|
|
|
32
32
|
TERMINAL_SUCCESS = {"succeeded", "succeed", "completed"}
|