@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.
@@ -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}): ${text}`, EXIT.BACKEND_UNREACHABLE);
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}): ${text}`);
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: ${text.slice(0, 200)}`);
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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.28",
4
- "generatedAt": "2026-09-13T13:40:09.148Z",
3
+ "version": "0.9.29",
4
+ "generatedAt": "2026-09-14T08:49:05.207Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "export-jianying",
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.28",
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": "V20",
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": "V13",
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
  }