@remixmate/cli 0.9.28 → 0.9.29
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 +4 -0
- package/dist/diagnostics.js +39 -0
- package/dist/http.js +6 -5
- package/dist/manifest.json +2 -2
- package/dist/runner.js +14 -6
- package/package.json +1 -1
- package/skills/render-video/scripts/remote_renderer_client.py +4 -0
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/scripts/log_diagnostics.py +32 -0
- package/skills/template-registry/scripts/render_job_client.py +2 -0
- package/skills/template-registry/version.json +1 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function traceHeaders(): Record<string, string>;
|
|
2
|
+
export declare function cleanDiagnosticText(value: string): string;
|
|
3
|
+
/** Opt-in line framing for hosts; stdout is exclusively the existing skill protocol. */
|
|
4
|
+
export declare function diagnostic(exitCode: number, durationMs: number, error?: unknown): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
export function traceHeaders() {
|
|
3
|
+
const parent = process.env.AB_TRACEPARENT ?? '';
|
|
4
|
+
const match = /^00-([a-f0-9]{32})-([a-f0-9]{16})-[a-f0-9]{2}$/.exec(parent);
|
|
5
|
+
const trace = match && !/^0+$/.test(match[1]) && !/^0+$/.test(match[2]) ? match[1] : randomBytes(16).toString('hex');
|
|
6
|
+
const header = `00-${trace}-${randomBytes(8).toString('hex')}-01`;
|
|
7
|
+
process.env.AB_TRACEPARENT = header;
|
|
8
|
+
process.env.AB_OPERATION_ID ||= randomUUID();
|
|
9
|
+
return { traceparent: header };
|
|
10
|
+
}
|
|
11
|
+
export function cleanDiagnosticText(value) {
|
|
12
|
+
let text = value.replace(/https?:\/\/[^\s"'<>]+/g, raw => {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(raw);
|
|
15
|
+
return url.origin + url.pathname;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return '[url]';
|
|
19
|
+
}
|
|
20
|
+
}).replace(/(Bearer\s+|(?:token|secret|password|api[_-]?key)\s*[=:]\s*)[^\s,;}]+/gi, '$1[redacted]');
|
|
21
|
+
for (const [key, secret] of Object.entries(process.env)) {
|
|
22
|
+
if (/TOKEN|SECRET|PASSWORD|API_KEY/i.test(key) && secret && secret.length >= 6)
|
|
23
|
+
text = text.split(secret).join('[redacted]');
|
|
24
|
+
}
|
|
25
|
+
return Buffer.from(text).subarray(0, 2048).toString('utf8');
|
|
26
|
+
}
|
|
27
|
+
/** Opt-in line framing for hosts; stdout is exclusively the existing skill protocol. */
|
|
28
|
+
export function diagnostic(exitCode, durationMs, error) {
|
|
29
|
+
if (process.env.AB_DIAGNOSTICS !== '1')
|
|
30
|
+
return;
|
|
31
|
+
traceHeaders();
|
|
32
|
+
const outcome = exitCode === 0 ? 'success' : exitCode === 130 ? 'cancelled' : [2, 3, 4].includes(exitCode) ? 'rejected' : 'failure';
|
|
33
|
+
const record = { schema_version: 1, timestamp: new Date().toISOString(), service: 'remixmate-cli', environment: process.env.APP_ENV ?? 'dev',
|
|
34
|
+
level: outcome === 'failure' ? 'error' : 'info', event: 'cli.execution.completed', message: 'CLI execution completed', outcome,
|
|
35
|
+
trace_id: process.env.AB_TRACEPARENT?.split('-')[1], operation_id: process.env.AB_OPERATION_ID,
|
|
36
|
+
duration_ms: durationMs, attributes: { exit_code: exitCode },
|
|
37
|
+
error: error instanceof Error ? { type: error.name, message: cleanDiagnosticText(error.message), stack: cleanDiagnosticText(error.stack ?? '') } : undefined };
|
|
38
|
+
process.stderr.write('__diagnostic_v1__ ' + JSON.stringify(record) + '\n');
|
|
39
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* 3. process.env.MM_BACKEND_API_URL — ab-agent's convention (back-compat)
|
|
19
19
|
* 4. https://api.remixmate.com/api — production default (zero-config)
|
|
20
20
|
*/
|
|
21
|
+
import { traceHeaders, cleanDiagnosticText } from './diagnostics.js';
|
|
21
22
|
import { recordBilling } from './billing.js';
|
|
22
23
|
import { resolvePrivToken, NOT_AUTHENTICATED_HINT } from './auth/resolve.js';
|
|
23
24
|
import { attemptAutoLogin } from './auth/auto-login.js';
|
|
@@ -73,7 +74,7 @@ function buildHeaders(ctx, extra) {
|
|
|
73
74
|
headers['x-invoke-agent'] = ctx.agentName;
|
|
74
75
|
if (ctx.conversationId)
|
|
75
76
|
headers['x-conversation-id'] = ctx.conversationId;
|
|
76
|
-
return { ...headers, ...extra };
|
|
77
|
+
return { ...headers, ...extra, ...traceHeaders() };
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
80
|
* POST JSON to ab-api and return the parsed business payload.
|
|
@@ -110,16 +111,16 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
|
|
|
110
111
|
throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 HTTP ${resp.status})`, EXIT.NOT_AUTHENTICATED);
|
|
111
112
|
}
|
|
112
113
|
if (resp.status >= 500) {
|
|
113
|
-
throw new SkillError(`❌ API request failed (HTTP ${resp.status}):
|
|
114
|
+
throw new SkillError(`❌ API request failed (HTTP ${resp.status}): [response omitted]`, EXIT.BACKEND_UNREACHABLE);
|
|
114
115
|
}
|
|
115
|
-
throw new SkillError(`❌ API request failed (HTTP ${resp.status}):
|
|
116
|
+
throw new SkillError(`❌ API request failed (HTTP ${resp.status}): [response omitted]`);
|
|
116
117
|
}
|
|
117
118
|
let parsed;
|
|
118
119
|
try {
|
|
119
120
|
parsed = JSON.parse(text);
|
|
120
121
|
}
|
|
121
122
|
catch {
|
|
122
|
-
throw new SkillError(`❌ failed to parse response, body is not JSON:
|
|
123
|
+
throw new SkillError(`❌ failed to parse response, body is not JSON: [response omitted]`);
|
|
123
124
|
}
|
|
124
125
|
// Before the code check: a charged response is always code=0 today, but a
|
|
125
126
|
// partial-failure envelope that still billed must not lose its billing line.
|
|
@@ -130,7 +131,7 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
|
|
|
130
131
|
if (parsed.code === 401 || parsed.code === 403) {
|
|
131
132
|
throw new SkillError(`${NOT_AUTHENTICATED_HINT}\n (后端返回 code=${parsed.code}${parsed.msg ? `: ${parsed.msg}` : ''})`, EXIT.NOT_AUTHENTICATED);
|
|
132
133
|
}
|
|
133
|
-
throw new SkillError(`❌ API returned a business error: ${parsed.msg ?? 'unknown error'} (code=${parsed.code})`);
|
|
134
|
+
throw new SkillError(`❌ API returned a business error: ${cleanDiagnosticText(parsed.msg ?? 'unknown error')} (code=${parsed.code})`);
|
|
134
135
|
}
|
|
135
136
|
return parsed.data ?? undefined;
|
|
136
137
|
}
|
package/dist/manifest.json
CHANGED
package/dist/runner.js
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 { diagnostic, cleanDiagnosticText, traceHeaders } from './diagnostics.js';
|
|
12
13
|
import { spawn } from 'node:child_process';
|
|
13
14
|
import path from 'node:path';
|
|
14
15
|
import { findSkill, SKILLS_DIR } from './registry.js';
|
|
@@ -24,9 +25,14 @@ function tokenFlag(args) {
|
|
|
24
25
|
return typeof value === 'string' ? value : undefined;
|
|
25
26
|
}
|
|
26
27
|
export async function runSkill(skillName, opts) {
|
|
28
|
+
const started = performance.now();
|
|
29
|
+
let exitCode = EXIT.ERROR;
|
|
30
|
+
let failure;
|
|
31
|
+
traceHeaders();
|
|
27
32
|
const skill = findSkill(skillName, opts.baseDir ?? SKILLS_DIR);
|
|
28
33
|
if (!skill) {
|
|
29
34
|
process.stderr.write(`❌ skill not found: ${skillName}\n`);
|
|
35
|
+
diagnostic(EXIT.USAGE, performance.now() - started);
|
|
30
36
|
return EXIT.USAGE;
|
|
31
37
|
}
|
|
32
38
|
try {
|
|
@@ -40,24 +46,26 @@ export async function runSkill(skillName, opts) {
|
|
|
40
46
|
case 'python':
|
|
41
47
|
// Python children talk to ab-api themselves and print their own billing
|
|
42
48
|
// footer; nothing was charged through this process.
|
|
43
|
-
return await runPython(skill, opts.rawArgs, auth);
|
|
49
|
+
return exitCode = await runPython(skill, opts.rawArgs, auth);
|
|
44
50
|
case 'http':
|
|
45
51
|
case 'builtin':
|
|
46
|
-
return await runHandler(skill, opts.parsedArgs, auth);
|
|
52
|
+
return exitCode = await runHandler(skill, opts.parsedArgs, auth);
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
catch (err) {
|
|
56
|
+
failure = err;
|
|
50
57
|
if (err instanceof SkillError) {
|
|
51
|
-
process.stderr.write(err.message + '\n');
|
|
52
|
-
return err.exitCode;
|
|
58
|
+
process.stderr.write(cleanDiagnosticText(err.message) + '\n');
|
|
59
|
+
return exitCode = err.exitCode;
|
|
53
60
|
}
|
|
54
|
-
process.stderr.write(`❌ unexpected error: ${err.message}\n`);
|
|
61
|
+
process.stderr.write(`❌ unexpected error: ${cleanDiagnosticText(err.stack ?? err.message)}\n`);
|
|
55
62
|
return EXIT.ERROR;
|
|
56
63
|
}
|
|
57
64
|
finally {
|
|
58
65
|
// In `finally` because a run can fail *after* a billed step (e.g. the image
|
|
59
66
|
// generated and was charged, then the upload timed out) — spend gets
|
|
60
67
|
// reported either way. No-ops when nothing was charged.
|
|
68
|
+
diagnostic(exitCode, performance.now() - started, failure);
|
|
61
69
|
emitBillingFooter();
|
|
62
70
|
}
|
|
63
71
|
}
|
|
@@ -140,7 +148,7 @@ async function applyTakeContext(skill, opts, auth) {
|
|
|
140
148
|
`(project ${take.projectId}${take.attempt ? `, attempt ${take.attempt}` : ''})\n`);
|
|
141
149
|
}
|
|
142
150
|
catch (err) {
|
|
143
|
-
process.stderr.write(`⚠️ 无法归属本次产物(内容仍会正常生成,但不会出现在项目里): ${err.message}\n`);
|
|
151
|
+
process.stderr.write(`⚠️ 无法归属本次产物(内容仍会正常生成,但不会出现在项目里): ${cleanDiagnosticText(err.message)}\n`);
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
async function runPython(skill, rawArgs, auth) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.29",
|
|
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",
|
|
@@ -34,6 +34,9 @@ import urllib.error
|
|
|
34
34
|
import urllib.parse
|
|
35
35
|
import urllib.request
|
|
36
36
|
from typing import Callable, Optional
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "template-registry" / "scripts"))
|
|
39
|
+
from log_diagnostics import trace_headers
|
|
37
40
|
|
|
38
41
|
DEFAULT_API_BASE_URL = "https://api-render.remixmate.com"
|
|
39
42
|
API_BASE_URL = (
|
|
@@ -87,6 +90,7 @@ def _build_headers(private_token: str, content_type: str = "application/json", c
|
|
|
87
90
|
headers["x-invoke-agent"] = AGENT_NAME
|
|
88
91
|
if conversation_id:
|
|
89
92
|
headers["x-conversation-id"] = conversation_id
|
|
93
|
+
headers.update(trace_headers())
|
|
90
94
|
return headers
|
|
91
95
|
|
|
92
96
|
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V21",
|
|
6
6
|
"skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
|
|
7
7
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Portable stdlib-only context and opt-in stderr diagnostics; never writes stdout."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import secrets
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
_started = time.monotonic()
|
|
12
|
+
|
|
13
|
+
def trace_headers():
|
|
14
|
+
match = re.fullmatch(r'00-([a-f0-9]{32})-([a-f0-9]{16})-[a-f0-9]{2}', os.getenv('AB_TRACEPARENT', ''))
|
|
15
|
+
tid = match[1] if match and match[1] != '0'*32 and match[2] != '0'*16 else secrets.token_hex(16)
|
|
16
|
+
parent = f'00-{tid}-{secrets.token_hex(8)}-01'
|
|
17
|
+
os.environ['AB_TRACEPARENT'] = parent
|
|
18
|
+
os.environ.setdefault('AB_OPERATION_ID', str(uuid.uuid4()))
|
|
19
|
+
return {'traceparent': parent}
|
|
20
|
+
|
|
21
|
+
def diagnostic(exit_code, kind='none', service='remixmate-studio-cli'):
|
|
22
|
+
if os.getenv('AB_DIAGNOSTICS') != '1':
|
|
23
|
+
return
|
|
24
|
+
trace_headers()
|
|
25
|
+
outcome = 'success' if exit_code == 0 else 'failure' if exit_code == 2 else 'rejected'
|
|
26
|
+
record = dict(schema_version=1, timestamp=datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
|
27
|
+
level='error' if outcome == 'failure' else 'info', service=service, environment=os.getenv('APP_ENV', 'dev'),
|
|
28
|
+
event='cli.execution.completed', message='CLI execution completed', outcome=outcome,
|
|
29
|
+
duration_ms=(time.monotonic()-_started)*1000, trace_id=os.environ['AB_TRACEPARENT'].split('-')[1],
|
|
30
|
+
operation_id=os.environ['AB_OPERATION_ID'], attributes={'exit_code': exit_code, 'error_kind': kind if re.fullmatch('[a-z_]{1,80}', str(kind)) else 'unknown'})
|
|
31
|
+
sys.stderr.write('__diagnostic_v1__ ' + json.dumps(record, ensure_ascii=False) + '\n')
|
|
32
|
+
sys.stderr.flush()
|
|
@@ -11,6 +11,7 @@ gen_jianying_draft.py 使用。
|
|
|
11
11
|
两个变量语义相同,保留回退是为兼容只配了其中一个的部署环境。
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
from log_diagnostics import trace_headers
|
|
14
15
|
import json
|
|
15
16
|
import os
|
|
16
17
|
import sys
|
|
@@ -34,6 +35,7 @@ def _make_headers(priv_token: str) -> dict:
|
|
|
34
35
|
agent_name = os.environ.get("AGENT_NAME", "")
|
|
35
36
|
if agent_name:
|
|
36
37
|
headers["x-invoke-agent"] = agent_name
|
|
38
|
+
headers.update(trace_headers())
|
|
37
39
|
return headers
|
|
38
40
|
|
|
39
41
|
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "template-registry",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "475",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V14",
|
|
6
6
|
"skillDescription": "Video-template registry skill. Stores every video-template definition and lists the available templates (templateId / name / aspect ratio / style tags).\n\nUse this skill as soon as the user mentions any of these intents:\n- View available templates / list every template\n\nNote: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally."
|
|
7
7
|
}
|