Files
wehub-resource-sync 85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

1168 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { existsSync, statSync, writeFileSync, renameSync, rmSync } from "node:fs";
import { resolve, join, extname, basename } from "node:path";
import { parseArgs } from "node:util";
import { appendRecord, findByPrompt, findByEntity, nextId, allocateId } from "./lib/manifest.mjs";
import { regenerateIndex } from "./lib/index-gen.mjs";
import { cacheGet, cacheGetByEntity, importFromCache, cachePut } from "./lib/cache.mjs";
import { runCapability, listTypes, providerMatches, providerNamesFor } from "./lib/registry.mjs";
import { freezeUrl, freezeLocalFile, isDirectMediaUrl } from "./lib/freeze.mjs";
import { findExistingAsset } from "./lib/adopt.mjs";
import { track } from "./lib/telemetry.mjs";
import { recordMiss } from "./lib/misses.mjs";
import { buildStats } from "./lib/stats.mjs";
import { typesMatch } from "./lib/match.mjs";
import { listCandidates, formatCandidates, CANDIDATE_CAP } from "./lib/candidates.mjs";
import { findGlobalBySha } from "./lib/cache.mjs";
import { heygenAuthMethod } from "../audio/scripts/lib/heygen.mjs";
import { buildCube, paramsFromIntent } from "./lib/cube-build.mjs";
import { validateCubeFile } from "./lib/cube-validate.mjs";
import { analyzeMediaGrade, formatMeasuredNote } from "./lib/grade-analyzer.mjs";
import {
freezeLibraryLut,
isLibraryLutOfflineMiss,
matchColorLook,
} from "./lib/lut-preset-provider.mjs";
import {
HEYGEN_AUTH_COMMAND,
HEYGEN_INSTALL_COMMAND,
HEYGEN_MIN_VERSION,
HEYGEN_UPDATE_COMMAND,
consumeHeygenRemediation,
firstSemver,
flushHeygenFailureTracking,
versionLessThan,
} from "./lib/heygen-cli.mjs";
const INGEST_TYPES = [...listTypes(), "video"];
// resolve shells `fetch`/`freezeUrl` and modern ESM; 18 is the floor where those
// exist without flags. Named so the --doctor node check verifies something real
// (O2). Declared before the top-level `--doctor` branch that calls runDoctor().
const MIN_NODE_VERSION = "18.0.0";
const { values: args } = parseArgs({
options: {
type: { type: "string", short: "t" },
intent: { type: "string", short: "i" },
entity: { type: "string", short: "e" },
project: { type: "string", short: "p", default: "." },
adopt: { type: "boolean", default: false },
candidates: { type: "boolean", default: false },
doctor: { type: "boolean", default: false },
stats: { type: "boolean", default: false },
days: { type: "string" },
"dry-run": { type: "boolean", default: false },
reuse: { type: "string" },
from: { type: "string" },
params: { type: "string" },
for: { type: "string" },
"local-only": { type: "boolean", default: false },
provider: { type: "string" },
json: { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
strict: true,
});
if (args.help) {
console.log(`media-use resolve — turn a media need into a frozen local file
Usage:
node resolve.mjs --type <type> --intent "<description>" [--project <dir>]
Types: ${listTypes().join(", ")}
Options:
--type, -t Media type (required)
--intent, -i What you need (required)
--entity, -e Entity name for cache matching (optional)
--project, -p Project directory (default: .)
--adopt Adopt all existing assets/ files into the manifest
--candidates List reusable assets (project + global cache) for --type; no
download, no mutation. Read them and decide reuse yourself.
--doctor Check local CLI dependencies; no manifest changes.
--stats Print local usage stats from .media and ~/.media; no mutation.
--days <N> Limit --stats to records/misses from the last N days when
timestamps are available.
--reuse <sha> Import a specific global-cache asset (by content sha/prefix,
from --candidates) into this project
--from <file> Freeze a local file or direct public URL (ingest)
--params <json> Build an explicit parametric LUT (lut/grade only)
--for <media> Analyze a local image/video and add measured grade adjust
suggestions (grade only)
--local-only Offline: skip every network provider
--provider Force one generator (e.g. codex, mflux, kokoro, heygen)
--json Output JSON instead of one-line result
--help, -h Show this help`);
process.exit(0);
}
const projectDir = resolve(args.project);
const type = args.type;
const intent = args.intent;
const entity = args.entity || null;
if (args.adopt) {
const { adoptExistingAssets } = await import("./lib/adopt.mjs");
const adopted = adoptExistingAssets(projectDir);
if (args.json) {
console.log(JSON.stringify({ ok: true, adopted: adopted.length, assets: adopted }));
} else if (adopted.length === 0) {
console.log("no new assets to adopt (assets/ empty or already registered)");
} else {
console.log(`adopted ${adopted.length} asset${adopted.length === 1 ? "" : "s"} from assets/`);
for (const r of adopted) console.log(` ${r.id}${r.path} (${r.type})`);
}
process.exit(0);
}
// Candidates: side-effect-free listing of reusable assets (project + global
// cache) for --type. No download, no provider, no mutation. The agent reads
// these and decides semantic fit itself.
if (args.candidates || args["dry-run"]) {
await showCandidates();
process.exit(0);
}
if (args.doctor) {
const doctor = runDoctor();
const failed = doctor.checks.filter((check) => !check.ok);
// Non-PII: instrument the exact question the feature exists to answer — how
// often is --doctor run and which check fails most. Awaited so a short-lived
// run flushes before exit.
await track("media_use_doctor_run", {
ok: doctor.ok,
checks_failed: failed.length,
failed: failed.map((check) => check.name),
});
if (args.json) {
console.log(JSON.stringify({ ok: doctor.ok, checks: doctor.checks }));
} else {
printDoctor(doctor.checks);
}
process.exit(doctor.ok ? 0 : 1);
}
if (args.stats) {
const report = buildStats({
projectDir,
days: args.days ? Number(args.days) : undefined,
});
if (args.json) {
console.log(JSON.stringify(report));
} else {
printStats(report);
}
process.exit(0);
}
// Reuse: import a specific global-cache asset (by content sha/prefix, taken
// from --candidates) into this project. `!== undefined` so an empty --reuse ""
// still routes here (and gets a clear empty-sha error) instead of falling
// through to the misleading "--type and --intent are required".
if (args.reuse !== undefined) {
await reuseGlobal(args.reuse);
process.exit(0);
}
// Ingest: freeze a user-supplied local file or direct public URL (no search).
if (args.from) {
await ingest(args.from);
process.exit(0);
}
if (args.params !== undefined) {
if (type !== "lut" && type !== "grade") {
exitError(
type
? `--params only supports --type lut or grade (got ${type})`
: "--params requires --type lut or grade",
2,
);
}
try {
await runParams();
process.exit(0);
} catch (err) {
exitError(err.message, 1);
}
}
if (!args.type || !args.intent || !args.intent.trim()) {
console.error("error: --type and a non-empty --intent are required");
process.exit(2);
}
if (!listTypes().includes(args.type)) {
console.error(`error: unknown media type: ${args.type} (known: ${listTypes().join(", ")})`);
process.exit(2);
}
// Forced-provider validation: reject an unknown/unavailable provider name up
// front so a typo reads as a typo, not a catalog miss (`no provider could
// resolve`). Match rule mirrors runProviders (full name or dotted prefix).
if (args.provider && !providerMatches(args.type, args.provider)) {
console.error(
`error: unknown provider "${args.provider}" for type ${args.type} (available: ${providerNamesFor(args.type).join(", ")})`,
);
process.exit(2);
}
function recordAvailable(projectDir, record) {
if (!record) return false;
if (record.path) return existsSync(join(projectDir, record.path));
return record.type === "grade" && record.grading;
}
// Sparse `{ authMethod }` for a heygen-family provider name (e.g. "heygen.tts"),
// else `{}` — keeps auth_method telemetry absent for every non-heygen resolve
// instead of implying an auth method that doesn't apply.
function heygenAuthMethodFor(provider) {
if (!provider || !provider.startsWith("heygen.")) return {};
const authMethod = heygenAuthMethod();
return authMethod ? { authMethod } : {};
}
function localizeImportedRecord(record, localPath) {
if (record?.type === "grade" && record.grading?.lut) {
record.grading = {
...record.grading,
lut: { ...record.grading.lut, src: localPath },
};
}
return record;
}
async function run() {
// A forced --provider means "(re)generate with THIS provider" — it bypasses
// every reuse rung (project/entity/assets/global cache) so it can't silently
// hand back an asset from a different provider. The floor only applies to the
// default (unforced) cascade.
const forced = !!args.provider;
// 1. project manifest — exact-prompt match
const projectHit = forced ? null : findByPrompt(projectDir, intent, type);
if (recordAvailable(projectDir, projectHit)) {
return result(projectHit, "cached");
}
// 1b. entity match in project. icon and image are interchangeable for
// entity hits — both live in images/, and figma-imported brand marks are
// always recorded as type image while agents ask for logos as type icon.
if (!forced && entity) {
const entityHit = findByEntity(projectDir, entity);
if (entityHit && typesMatch(entityHit.type, type) && recordAvailable(projectDir, entityHit)) {
return result(entityHit, "cached");
}
}
// 1c. scan existing assets/ directory for unregistered matches
const existingAsset =
forced || type === "grade" || type === "lut"
? null
: findExistingAsset(projectDir, intent, type);
if (existingAsset) {
const id = nextId(projectDir, type);
const record = {
id,
type: existingAsset.type,
path: existingAsset.relativePath,
source: "existing",
description: existingAsset.name.replace(/[-_]/g, " "),
provenance: { provider: "local", adopted: true, prompt: intent },
};
appendRecord(projectDir, record);
regenerateIndex(projectDir);
return result(record, "existing");
}
// 2. global cache — exact-prompt or entity match
const cacheHit = forced ? null : cacheGet(intent, type);
if (cacheHit) {
const ext = extname(cacheHit.cached_path);
const { id, localPath } = allocateId(projectDir, type, ext);
const imported = localizeImportedRecord(
importFromCache(cacheHit, projectDir, id, localPath),
localPath,
);
if (imported) {
appendRecord(projectDir, imported);
regenerateIndex(projectDir);
return result(imported, "reused");
}
}
if (!forced && entity) {
const entityCacheHit = cacheGetByEntity(entity);
if (entityCacheHit && typesMatch(entityCacheHit.type, type)) {
const ext = extname(entityCacheHit.cached_path);
const { id, localPath } = allocateId(projectDir, type, ext);
const imported = localizeImportedRecord(
importFromCache(entityCacheHit, projectDir, id, localPath),
localPath,
);
if (imported) {
appendRecord(projectDir, imported);
regenerateIndex(projectDir);
return result(imported, "reused");
}
}
}
// Offline guard: --local-only skips every remote provider (HeyGen catalog),
// leaving the project + global cache and any local provider.
const localOnly = args["local-only"];
const ctx = { entity, projectDir, localOnly, provider: args.provider };
// Adherence nudge (offline, no auto-reuse): the exact-cache floor missed and
// we're about to fetch/generate. If lexically-similar assets already exist,
// point the agent at --candidates so it can reuse instead of fetching. Only a
// fuzzy match ever reaches the agent this way — never auto-applied. Goes to
// stderr so it reaches --json callers without corrupting stdout. Best-effort.
try {
const { similar } = listCandidates({ projectDir, type, intent, cap: CANDIDATE_CAP });
if (similar > 0) {
console.error(
`media-use: ${similar} similar cached asset${similar === 1 ? "" : "s"} already ${similar === 1 ? "exists" : "exist"} — run \`resolve --candidates --type ${type} --intent "${intent}"\` to review and reuse instead of fetching.`,
);
}
} catch {
// hint is best-effort; never block a resolve
}
if (type === "grade" || type === "lut") {
return resolveColor(type, intent, { projectDir });
}
// 3. provider search — registry tries providers in order (heygen-CLI first)
let searchResult = null;
try {
searchResult = await runCapability(type, "search", intent, ctx);
} catch {
// search failed, try generate
}
// 4. generate fallback — same ordered cascade for the generate capability
if (!searchResult) {
try {
searchResult = await runCapability(type, "generate", intent, ctx);
} catch {
// generate failed too
}
}
// A search/generate attempt against heygen may have fired a fire-and-forget
// media_use_provider_error track (reportHeygenFailure — heygen-search.mjs /
// voice-provider.mjs are sync call sites several layers below here and can't
// await it themselves). Join it now, before any process.exit() below can
// race it: both it and the miss/success telemetry below are separate,
// non-keepalive HTTP connections with no ordering guarantee otherwise.
await flushHeygenFailureTracking();
if (!searchResult) {
await track("media_use_resolve_miss", {
type,
local_only: !!localOnly,
provider_override: !!args.provider,
});
recordMiss({
type,
intent,
provider_override: !!args.provider,
local_only: !!args["local-only"],
});
// brand stays local: no frame.md/design.md -> upsell the HyperFrames design
// flow rather than reporting a generic miss (B5).
const msg =
type === "brand"
? "no brand spec found — add a frame.md or design.md (colors/font/logo) to this project. Run the HyperFrames design flow to create one; brand tokens are read locally for deterministic rendering."
: args.provider
? `provider "${args.provider}" could not resolve ${type}: "${intent}"${localOnly ? " (--local-only skips network providers; drop it or the --provider override)" : ""}`
: `no provider could resolve ${type}: "${intent}"`;
if (args.json) {
console.log(JSON.stringify({ ok: false, error: msg }));
} else {
console.error(`error: ${msg}`);
}
process.exit(1);
}
// 5. freeze + register (atomic id+file reservation so concurrent resolves
// can't collide on an id during the download — MU-23)
const ext = searchResult.ext || extFromUrl(searchResult.url || "") || defaultExt(type);
const { id, localPath } = allocateId(projectDir, type, ext);
const fullPath = join(projectDir, localPath);
if (searchResult.localPath) {
freezeLocalFile(searchResult.localPath, fullPath);
} else if (searchResult.url) {
await freezeUrl(searchResult.url, fullPath);
} else {
console.error("error: provider returned no url or localPath");
process.exit(1);
}
const record = {
id,
type,
path: localPath,
source: searchResult.source || "search",
description: searchResult.metadata?.description || intent,
...(searchResult.metadata?.duration != null && {
duration: Math.round(searchResult.metadata.duration * 10) / 10, // round to 0.1s like probe (voice bypassed it)
}),
...(searchResult.metadata?.width != null && { width: searchResult.metadata.width }),
...(searchResult.metadata?.height != null && { height: searchResult.metadata.height }),
...(searchResult.metadata?.transparent != null && {
transparent: searchResult.metadata.transparent,
}),
...(entity && { entity }),
provenance: {
provider: searchResult.metadata?.provider || "unknown",
prompt: intent,
// heygenAuthMethodFor spreads first so an explicit authMethod on a
// future provider's own metadata.provenance can still override it below
// -- safe today (no provider sets authMethod itself), but keep this
// ordering if that ever changes.
...heygenAuthMethodFor(searchResult.metadata?.provider),
...searchResult.metadata?.provenance,
},
};
const heygenRemediation = consumeHeygenRemediation();
if (
searchResult.metadata?.provider === "bundled.sfx" &&
!localOnly &&
!args.provider &&
heygenRemediation
) {
record.advisory = heygenRemediation;
}
appendRecord(projectDir, record);
regenerateIndex(projectDir);
// Auto-promote: surface every fetched asset in the global cache so it's
// reusable across all hyperframes projects (B3). Non-fatal; dedup by sha.
// ponytail: promotes search/generate/ingest assets (the ones media-use
// fetched), not bulk --adopt imports — add those if cross-project reuse of
// pre-existing project assets is wanted.
try {
cachePut(fullPath, record);
} catch {
// promotion is best-effort; a resolve still succeeds locally
}
return result(record, searchResult.source || "search");
}
function mergeSmartAdjust(block) {
if (!args.for) return block;
const mediaPath = resolve(args.for);
// Clear upfront error beats an ffmpeg "No such file" stack on a typo'd path.
if (!existsSync(mediaPath)) throw new Error(`--for file not found: ${mediaPath}`);
const analysis = analyzeMediaGrade(mediaPath);
console.error(formatMeasuredNote(mediaPath, analysis.measured));
return {
...block,
adjust: {
...(block.adjust || {}),
...analysis.adjust,
},
};
}
function freezeGeneratedLut(
params,
{
projectDir,
type,
description = "parametric color grade",
validationErrorPrefix = "generated LUT failed validation",
},
) {
const { id, localPath } = allocateId(projectDir, type, ".cube");
const fullPath = join(projectDir, localPath);
const tmpPath = `${fullPath}.tmp`;
try {
// Write + validate at .tmp, then atomic rename, so a crash between write and
// validate can't leave an invalid .cube at the final path.
writeFileSync(tmpPath, buildCube(params));
const check = validateCubeFile(tmpPath);
if (!check.ok) throw new Error(check.error);
renameSync(tmpPath, fullPath);
} catch (err) {
rmSync(tmpPath, { force: true });
throw new Error(`${validationErrorPrefix}: ${err.message}`);
}
return {
id,
localPath,
fullPath,
lut: { src: localPath, intensity: 1 },
source: "generated",
description,
metadata: {
provider: "cube_lut.builder",
provenance: { params },
},
};
}
function exitError(message, status = 1) {
if (args.json) {
console.log(JSON.stringify({ ok: false, error: message }));
} else {
console.error(`error: ${message}`);
}
process.exit(status);
}
function parseExplicitParams() {
try {
return JSON.parse(args.params);
} catch (err) {
throw new Error(`invalid --params JSON: ${err.message}`);
}
}
async function runParams() {
if (type === "lut" && args.for) {
throw new Error("--for is only supported with --type grade");
}
const params = parseExplicitParams();
const description =
typeof intent === "string" && intent.trim()
? intent.trim()
: `custom parametric ${type === "lut" ? "lut" : "grade"}`;
const frozen = freezeGeneratedLut(params, {
projectDir,
type,
description,
validationErrorPrefix: "--params produced an invalid LUT",
});
const record = {
id: frozen.id,
type,
path: frozen.localPath,
source: frozen.source,
description: frozen.description,
...(type === "grade" && { grading: mergeSmartAdjust({ intensity: 1, lut: frozen.lut }) }),
provenance: {
provider: frozen.metadata.provider,
...frozen.metadata.provenance,
},
};
return finalizeColorRecord(record, frozen.source, frozen.fullPath);
}
async function finalizeColorRecord(record, source, fullPath = null) {
appendRecord(projectDir, record);
regenerateIndex(projectDir);
if (fullPath) {
try {
cachePut(fullPath, record);
} catch {
// promotion is best-effort
}
}
return result(record, source);
}
async function colorMiss(type, intent) {
await track("media_use_resolve_miss", {
type,
local_only: !!args["local-only"],
provider_override: !!args.provider,
});
recordMiss({
type,
intent,
provider_override: !!args.provider,
local_only: !!args["local-only"],
});
const msg = `no local color grade could resolve ${type}: "${intent}"`;
if (args.json) {
console.log(JSON.stringify({ ok: false, error: msg }));
} else {
console.error(`error: ${msg}`);
}
process.exit(1);
}
async function resolveGrade(intent, { projectDir }) {
const match = matchColorLook(intent);
if (match?.kind === "preset") {
const id = nextId(projectDir, "grade");
const grading = mergeSmartAdjust({ preset: match.preset, intensity: 1 });
const record = {
id,
type: "grade",
source: "preset",
description: intent,
grading,
provenance: {
provider: "color_grade.local",
prompt: intent,
preset: match.preset,
},
};
return finalizeColorRecord(record, "preset");
}
if (match?.kind === "library") {
let frozen;
try {
frozen = await freezeLibraryLut(match, {
projectDir,
type: "grade",
localOnly: args["local-only"],
});
} catch (err) {
if (isLibraryLutOfflineMiss(err)) return colorMiss("grade", intent);
throw err;
}
const grading = mergeSmartAdjust({ intensity: 1, lut: frozen.lut });
const record = {
id: frozen.id,
type: "grade",
path: frozen.localPath,
source: frozen.source,
description: frozen.description,
grading,
provenance: {
provider: frozen.metadata.provider,
prompt: intent,
...frozen.metadata.provenance,
},
};
return finalizeColorRecord(record, frozen.source, frozen.fullPath);
}
const params = paramsFromIntent(intent);
if (!params) {
// No creative look matched. With --for, the measured adjust block is a
// valid grade on its own (footage auto-correction); only a true miss
// (no look AND no analysis) aborts.
if (args.for) {
const grading = mergeSmartAdjust({ intensity: 1 });
const record = {
id: nextId(projectDir, "grade"),
type: "grade",
source: "measured",
description: intent,
grading,
provenance: { provider: "color_grade.local", prompt: intent, measured: true },
};
return finalizeColorRecord(record, "measured");
}
return colorMiss("grade", intent);
}
const frozen = freezeGeneratedLut(params, { projectDir, type: "grade" });
const grading = mergeSmartAdjust({ intensity: 1, lut: frozen.lut });
const record = {
id: frozen.id,
type: "grade",
path: frozen.localPath,
source: frozen.source,
description: intent,
grading,
provenance: {
provider: frozen.metadata.provider,
prompt: intent,
...frozen.metadata.provenance,
},
};
return finalizeColorRecord(record, frozen.source, frozen.fullPath);
}
async function resolveLut(intent, { projectDir }) {
if (args.for) {
throw new Error("--for is only supported with --type grade");
}
const match = matchColorLook(intent);
if (match?.kind === "library") {
let frozen;
try {
frozen = await freezeLibraryLut(match, {
projectDir,
type: "lut",
localOnly: args["local-only"],
});
} catch (err) {
if (isLibraryLutOfflineMiss(err)) return colorMiss("lut", intent);
throw err;
}
const record = {
id: frozen.id,
type: "lut",
path: frozen.localPath,
source: frozen.source,
description: frozen.description,
provenance: {
provider: frozen.metadata.provider,
prompt: intent,
...frozen.metadata.provenance,
},
};
return finalizeColorRecord(record, frozen.source, frozen.fullPath);
}
const params = paramsFromIntent(intent);
if (!params) return colorMiss("lut", intent);
const frozen = freezeGeneratedLut(params, { projectDir, type: "lut" });
const record = {
id: frozen.id,
type: "lut",
path: frozen.localPath,
source: frozen.source,
description: intent,
provenance: {
provider: frozen.metadata.provider,
prompt: intent,
...frozen.metadata.provenance,
},
};
return finalizeColorRecord(record, frozen.source, frozen.fullPath);
}
async function resolveColor(type, intent, options) {
if (type === "grade") return resolveGrade(intent, options);
return resolveLut(intent, options);
}
async function ingest(src) {
if (!type || !INGEST_TYPES.includes(type)) {
console.error(`error: --from requires --type (one of: ${INGEST_TYPES.join(", ")})`);
process.exit(2);
}
const isUrl = /^https?:\/\//i.test(src);
if (isUrl && !isDirectMediaUrl(src)) {
console.error(
`error: --from takes a direct public media URL or a local file; "${src}" is not a direct media link (no platform pages / yt-dlp)`,
);
process.exit(2);
}
if (!isUrl && !existsSync(resolve(src))) {
console.error(`error: file not found: ${src}`);
process.exit(2);
}
// Refuse 0-byte input: an empty asset would register clean but fail at render
// (freezeUrl already rejects empty responses; this covers local files).
if (!isUrl && statSync(resolve(src)).size === 0) {
console.error(`error: refusing to ingest a 0-byte file: ${src}`);
process.exit(2);
}
const ext = extname(isUrl ? new URL(src).pathname : src) || defaultExt(type);
const { id, localPath } = allocateId(projectDir, type, ext);
const fullPath = join(projectDir, localPath);
if (isUrl) await freezeUrl(src, fullPath);
else freezeLocalFile(resolve(src), fullPath);
if (type === "lut" || type === "grade") {
try {
const check = validateCubeFile(fullPath);
if (!check.ok) throw new Error(check.error);
} catch (err) {
rmSync(fullPath, { force: true });
exitError(`ingested LUT is invalid: ${err.message}`, 1);
}
}
const record = {
id,
type,
path: localPath,
source: "ingested",
description: basename(src.split("?")[0]),
provenance: { provider: "local", from: src },
};
appendRecord(projectDir, record);
regenerateIndex(projectDir);
try {
cachePut(fullPath, record); // surface ingested assets globally too (B3)
} catch {
// best-effort
}
await result(record, "ingested");
}
async function showCandidates() {
const projectDir = resolve(args.project);
const type = args.type;
if (!type || !listTypes().includes(type)) {
console.error(`error: --candidates requires --type (one of: ${listTypes().join(", ")})`);
process.exit(2);
}
const intent = args.intent || "";
const { candidates, truncated, total, similar } = listCandidates({
projectDir,
type,
intent,
cap: CANDIDATE_CAP,
});
await track("media_use_candidates", {
type,
project_n: total.project,
global_n: total.global,
local_only: !!args["local-only"],
});
if (args.json) {
console.log(JSON.stringify({ ok: true, candidates, truncated, total, similar }));
} else {
console.log(formatCandidates(candidates, { truncated, total }));
}
}
// Best-effort latest stable CLI tag from the CDN (the install script's source of
// truth). null on any failure (offline, no curl) — treated as "unknown", never fatal.
function latestHeygenStable() {
const probe = runCommand("curl", [
"-fsSL",
"--max-time",
"4",
"https://static.heygen.ai/cli/stable",
]);
return probe.status === 0 ? firstSemver(commandText(probe)) : null;
}
function heygenAuthCheck() {
// `heygen auth status` already emits JSON by default (only `--human` opts out
// to a table) — there is no `--json`/`--output` flag; passing one errors with
// "unknown flag". emailFromAuthStatus parses that default JSON.
// NOTE: JSON-by-default is a v0.3.0 behavior — this probe assumes it, which
// HEYGEN_MIN_VERSION >= 0.3.0 (+ the version gate above) guarantees. If that
// floor is ever lowered, auth detection on an older CLI would silently break.
const authProbe = runCommand("heygen", ["auth", "status"]);
// spawnSync sets .error/.signal on a timeout or spawn failure (status then
// null). A stalled auth endpoint (transient network/DNS) must not be reported
// as an authoritative "not authenticated" with a re-login fix.
const timedOut = authProbe.error?.code === "ETIMEDOUT" || authProbe.signal != null;
const email = authProbe.status === 0 ? emailFromAuthStatus(commandText(authProbe)) : null;
return {
name: "heygen authenticated",
ok: !!email,
detail: email
? `heygen authenticated as ${email}`
: timedOut
? "heygen auth status timed out — possible network issue, not proof of sign-out"
: "heygen not authenticated",
fix: email ? "" : timedOut ? "check network, then re-run --doctor" : HEYGEN_AUTH_COMMAND,
};
}
function runDoctor() {
const checks = [];
const heygenVersionProbe = runCommand("heygen", ["--version"]);
const heygenOnPath = heygenVersionProbe.status === 0;
const heygenVersionText = commandText(heygenVersionProbe);
const heygenVersion = firstSemver(heygenVersionText);
checks.push({
name: "heygen on PATH",
ok: heygenOnPath,
// Just "is the binary here" — the version row below owns the version string,
// so this row must not also render `heygen v0.3.0` (two byte-identical lines).
detail: heygenOnPath ? "heygen found on PATH" : "heygen not found",
fix: heygenOnPath ? "" : HEYGEN_INSTALL_COMMAND,
});
if (!heygenOnPath) {
checks.push({
name: "heygen version",
ok: false,
detail: "heygen version unavailable",
fix: HEYGEN_INSTALL_COMMAND,
});
checks.push({
name: "heygen authenticated",
ok: false,
detail: "heygen auth status unavailable",
fix: HEYGEN_INSTALL_COMMAND,
});
} else if (heygenVersion) {
const versionOk = !versionLessThan(heygenVersion, HEYGEN_MIN_VERSION);
// Keep it latest: even when the installed version clears the floor, nudge
// `heygen update` if a newer stable exists. Best-effort — silently skipped
// when the CDN is unreachable, so it never blocks the check.
const latest = versionOk ? latestHeygenStable() : null;
const behind = latest && versionLessThan(heygenVersion, latest);
checks.push({
name: "heygen version",
ok: versionOk,
detail: versionOk
? `heygen v${heygenVersion}${behind ? ` (latest v${latest} available)` : ""}`
: `heygen v${heygenVersion} (need >= v${HEYGEN_MIN_VERSION})`,
fix: versionOk ? (behind ? HEYGEN_UPDATE_COMMAND : "") : HEYGEN_UPDATE_COMMAND,
});
// Below the OAuth-capable floor the auth probe fails for the SAME root cause
// (an old CLI can't OAuth and doesn't emit JSON auth status), which would
// read as a confusing second "not authenticated" error. Skip it — one root
// cause, one fix.
checks.push(
versionOk
? heygenAuthCheck()
: {
name: "heygen authenticated",
ok: false,
detail: "skipped — update heygen first",
fix: HEYGEN_UPDATE_COMMAND,
},
);
} else {
// Fail-open: heygen ran but printed no semver (dev/stripped build). We can't
// verify the version, so we don't block on it — but say so rather than a bare
// green check that implies a real version comparison happened.
checks.push({
name: "heygen version",
ok: true,
detail: "heygen present; version unverifiable (no semver in --version output)",
fix: "",
});
checks.push(heygenAuthCheck());
}
const ffmpegProbe = runCommand("ffmpeg", ["-version"]);
checks.push({
name: "ffmpeg on PATH",
ok: ffmpegProbe.status === 0,
detail: ffmpegProbe.status === 0 ? firstLine(ffmpegProbe.stdout) : "ffmpeg not found",
fix: ffmpegProbe.status === 0 ? "" : "brew install ffmpeg",
});
const ffprobeProbe = runCommand("ffprobe", ["-version"]);
checks.push({
name: "ffprobe on PATH",
ok: ffprobeProbe.status === 0,
detail: ffprobeProbe.status === 0 ? firstLine(ffprobeProbe.stdout) : "ffprobe not found",
fix: ffprobeProbe.status === 0 ? "" : "brew install ffmpeg",
});
const nodeOk = !versionLessThan(process.versions.node, MIN_NODE_VERSION);
checks.push({
name: "node version",
ok: nodeOk,
detail: `${process.version} (need >= v${MIN_NODE_VERSION})`,
fix: nodeOk ? "" : `upgrade Node to >= v${MIN_NODE_VERSION}`,
});
// ffmpeg AND ffprobe are both strictly required (see SKILL.md); the exit code
// must reflect that so a script gating on `--doctor` doesn't pass with ffprobe
// missing and then break at the first probe call.
const ffmpeg = checks.find((check) => check.name === "ffmpeg on PATH");
const ffprobe = checks.find((check) => check.name === "ffprobe on PATH");
return { ok: !!ffmpeg?.ok && !!ffprobe?.ok, checks };
}
function printDoctor(checks) {
const heygenChecks = new Set(["heygen on PATH", "heygen version", "heygen authenticated"]);
for (const check of checks) {
const prefix = check.ok ? "✓" : "✗";
const freePath = heygenChecks.has(check.name)
? " — free-usage path: bgm/image/voice/avatar-video"
: "";
const fix = check.ok || !check.fix ? "" : ` — fix: ${check.fix}`;
console.log(`${prefix} ${check.detail}${freePath}${fix}`);
}
}
function printStats(report) {
console.log("media-use stats");
console.log(`total resolves: ${report.total_resolves}`);
console.log(`misses: ${report.misses}`);
console.log(
`hit rate: ${report.hit_rate == null ? "n/a" : `${Math.round(report.hit_rate * 100)}%`}`,
);
printMap("by type", report.by_type);
printMap("by source", report.by_source);
printMap("by provider", report.by_provider);
printMap("by via", report.by_via);
console.log(`global cache assets: ${report.global_cache_assets}`);
console.log(`global cache disk: ${report.global_cache_disk_bytes} bytes`);
console.log(`cross-project reuse: ${report.cross_project_reuse}`);
console.log("top missed intents:");
const entries = Object.entries(report.top_missed_intents);
if (entries.length === 0) {
console.log(" none");
return;
}
for (const [type, misses] of entries) {
console.log(` ${type}:`);
for (const miss of misses) console.log(` ${miss.count} ${miss.intent}`);
}
}
function printMap(label, values) {
const entries = Object.entries(values);
console.log(`${label}:`);
if (entries.length === 0) {
console.log(" none");
return;
}
for (const [key, value] of entries) console.log(` ${key}: ${value}`);
}
function runCommand(bin, argv) {
return spawnSync(bin, argv, {
encoding: "utf8",
timeout: 15000,
});
}
function commandText(result) {
return [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
}
function firstLine(text) {
return (
String(text || "")
.trim()
.split(/\r?\n/)[0] || ""
);
}
function emailFromAuthStatus(text) {
// JSON only (auth status emits JSON by default). No prose regex fallback: a
// human-format body like "Session expired. Contact support@heygen.ai" would
// otherwise report the user as authenticated as support@heygen.ai.
const trimmed = String(text || "").trim();
if (!trimmed.startsWith("{")) return null;
try {
const parsed = JSON.parse(trimmed);
return parsed?.data?.email || parsed?.email || null;
} catch {
return null;
}
}
async function reuseGlobal(shaArg) {
const projectDir = resolve(args.project);
const type = args.type;
if (!type || !listTypes().includes(type)) {
console.error(`error: --reuse requires --type (one of: ${listTypes().join(", ")})`);
process.exit(2);
}
if (!shaArg || !shaArg.trim()) {
console.error("error: --reuse needs a content sha/prefix (from `resolve --candidates`)");
process.exit(2);
}
const rec = findGlobalBySha(shaArg);
if (rec && rec.ambiguous) {
console.error(
`error: sha prefix "${shaArg}" is ambiguous (${rec.count} matches) — use more characters`,
);
process.exit(2);
}
if (!rec) {
console.error(`error: no reusable global asset matches sha "${shaArg}"`);
process.exit(1);
}
// Type guard: don't import a bgm asset as an image (audio under images/).
// icon<->image are interchangeable; everything else must match --type.
if (!typesMatch(rec.type, type)) {
console.error(`error: sha "${shaArg}" is a ${rec.type} asset, not ${type}`);
process.exit(2);
}
const ext = extname(rec.cached_path || "") || defaultExt(type);
const { id, localPath } = allocateId(projectDir, type, ext);
const imported = localizeImportedRecord(
importFromCache(rec, projectDir, id, localPath),
localPath,
);
if (!imported) {
console.error(`error: cache entry for "${shaArg}" is incomplete or missing on disk`);
process.exit(1);
}
// Distinguish an explicit agent reuse from an automatic normalize-exact hit.
imported.source = "reused-explicit";
imported.provenance = { ...imported.provenance, reused_by: "agent" };
appendRecord(projectDir, imported);
regenerateIndex(projectDir);
await result(imported, "reused-explicit");
}
async function result(record, source) {
// Non-PII usage event: which media type, how it resolved, which provider won.
// Never the intent text or paths. Awaited so a short-lived run flushes it.
await track("media_use_resolve", {
type: record.type,
source,
provider: record.provenance?.provider,
// How a library LUT resolved: "url" (CDN), "params-fallback" (CDN failed →
// parametric), or "params" (offline). Surfaces silent CDN→params downgrades
// in prod, which --doctor can't (it only answers "reachable now?").
via: record.provenance?.via,
// Free (OAuth) vs. paid (API-key) heygen path — sparse: absent for every
// non-heygen provider (see heygenAuthMethodFor at construction time). On a
// cache/reuse hit this reports how the asset was ORIGINALLY fetched, not
// this resolve's own credential state — intentional: it's a conversion
// signal about the fetch that actually consumed a heygen credit, not
// about the (free, no-credential) act of copying a cached file.
auth_method: record.provenance?.authMethod,
local_only: !!args["local-only"],
provider_override: !!args.provider,
});
if (args.json) {
const grading = record.type === "grade" && record.grading ? record.grading : null;
console.log(
JSON.stringify({
ok: true,
...record,
...(grading || {}),
...(grading && { grading }),
_source: source,
}),
);
} else {
const meta = formatMeta(record, source);
console.log(`resolved ${record.id}${record.path || "inline"} (${meta})`);
}
}
function formatMeta(record, source) {
const parts = [record.type];
if (record.grading?.preset) parts.push(`preset ${record.grading.preset}`);
if (record.grading?.lut) parts.push("lut");
if (record.duration != null) parts.push(`${record.duration}s`);
if (record.width && record.height) parts.push(`${record.width}×${record.height}`);
if (record.transparent) parts.push("transparent");
if (source === "reused" || source === "reused-explicit") parts.push("reused");
if (source === "generated") parts.push("generated");
return parts.join(", ");
}
function extFromUrl(url) {
try {
return extname(new URL(url).pathname) || null;
} catch {
return null;
}
}
const DEFAULT_EXT = {
bgm: ".wav",
sfx: ".mp3",
voice: ".wav",
image: ".jpg",
icon: ".svg",
logo: ".svg",
brand: ".png",
video: ".mp4",
grade: ".cube",
lut: ".cube",
};
function defaultExt(type) {
return DEFAULT_EXT[type] || ".bin";
}
run().catch((err) => {
if (args.json) {
console.log(JSON.stringify({ ok: false, error: err.message }));
} else {
console.error(`error: ${err.message}`);
}
process.exit(1);
});