feat(media-use): fast heygen CLI onboarding — actionable diagnostics, --doctor, free-usage framing#2065
feat(media-use): fast heygen CLI onboarding — actionable diagnostics, --doctor, free-usage framing#2065miguel-heygen wants to merge 2 commits into
Conversation
5fa2c12 to
928539e
Compare
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
928539e to
8465bb5
Compare
8465bb5 to
45ee90e
Compare
45ee90e to
105bc16
Compare
105bc16 to
bc5086e
Compare
bc5086e to
2c9425c
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 2c9425c.
Read this one at the canonical / test-coverage / observability / cross-cutting angle (leaving HF-runtime-interop mechanics to Via + Miga). The --doctor shape is nice and the SKILL reframe reads clean; my findings are on the classifier's substring rules and one exit-code / doc divergence — I think one of them is a genuine blocker for a PR whose whole thesis is "no more dead-end diagnostics".
Blockers
skills/media-use/scripts/lib/heygen-cli.mjs:27-31—lower.includes("not found")will misclassify domain "not found" errors as CLI-not-installed, reintroducing the exact dead-end this PR is designed to eliminate.classifyHeygenErrorcatches an error from a successfully invoked heygen binary and then decides "installed?" from stderr substring alone. Concrete case:voice-provider.mjs:62callsheygen voice speech create --voice-id <id>; if the caller passes a stale/invalidvoiceId, the CLI's most-idiomatic error phrasing isvoice not found(orvoice with id X not found). That hitslower.includes("not found")and we printmedia-use: heygen CLI not found — Install: curl -fsSL .... The user, who obviously already has the CLI (it just ran and errored), installs it again, retries, same error — same dead-end loop the PR aims to break.heygen asset searchwith an invalid ID andheygen voice listreferencing a missing engine are the same shape. Fix: drop the bare"not found"clause; keeperr?.code === "ENOENT"(system-level: binary not spawnable) andlower.includes("command not found")(shell wrapper's own not-found line). Add a defensive test likeclassifyHeygenError({ stderr: "voice not found" })must NOT returnHEYGEN_NOT_FOUND_MESSAGE— otherwise the next refactor slides back in silently. (My memory has a matching pattern undersubstring_match_numeric_code_in_freeform_msg— same class of hazard applied to text codes here.)
Concerns
-
skills/media-use/scripts/resolve.mjs:814— top-levelokisffmpegonly, butSKILL.md(this PR, line 178) claimsffmpeg/ffprobeare strictly-required. Two sources of truth diverge. If any downstream script gates onresolve --doctorexit code to preflight a build (which is the obvious--doctorcontract), a missingffprobebox passes the gate and the build breaks at firstffprobecall. The PR body says "ffmpeg (the only strictly-required dep)" but the SKILL.md diff includes ffprobe. Pick one: either widen the gate toffmpeg && ffprobe(matches SKILL.md), or narrow the SKILL.md line to "ffmpegis strictly required;ffprobeis required for probe operations" (matches code). Also — the--doctor --jsontest atresolve.test.mjs:632-633enshrines the ffmpeg-only gate as intended, so if the doc is right and the code is wrong, the test will need to move first. -
skills/media-use/scripts/lib/heygen-cli.mjs:36—lower.includes("401")matches any freeform text containing the literal substring "401". Request IDs (req-401abc), URLs, retry-after headers, error codes for unrelated CLIs downstream of heygen (ffmpegpiped through, etc.) all trip this. Tighten to a word/whitespace boundary or a status-code shape (\b401\bregex, orHTTP 401/status: 401/\s401\s) so genuine 401 responses classify but noise doesn't. -
skills/media-use/scripts/resolve.mjs:849-862—emailFromAuthStatusregex captures the first@-shaped token in any prose. Ifheygen auth statusreturns success with a body likeSession expired. Contact support@heygen.ai to re-auth.,emailFromAuthStatusreturnssupport@heygen.aiand--doctorprints✓ heygen authenticated as support@heygen.ai — free-usage path: bgm/image/voice/avatar-video, misreporting an unauthenticated state as authenticated. Suggest gating on a preamble (/(logged in|authenticated) as\s+([^\s@]+@[^\s@]+\.[^\s@]+)/i) or — better — passing--jsonexplicitly toheygen auth statusand taking the JSON path only. The human-readable fallback is already a bet on a CLI-version-stable format; passing--jsonremoves the bet. -
No telemetry on
--doctorinvocation or per-check outcome. The PR's stated value is "dead ends become actionable install steps"; the way to know if that landed is instrumenting the exact question — how often are agents hitting--doctor, which check fails most, do users re-run after a fix. There's no signal emitted from any of the new code paths (no PostHog capture, no analytics breadcrumb, no metric). Not a blocker for landing, but this is the moment where adding a minimalcapture("media_use_doctor_run", { checks_failed })is trivial and later becomes a schema-migration to bolt on. Flagging so it doesn't become the "we shipped it and never learned anything from it" outcome. -
skills/media-use/scripts/resolve.mjs:765,781—heygen auth statusruns with no--json, no timeout distinction between slow-network and unauth.runCommanduses a shared 15s ceiling; if the auth endpoint stalls (transient network / DNS), status !== 0 and--doctorreports "heygen not authenticated" with fixheygen auth login --key <key>. Wrong actionable — the user re-logs in fine and the underlying network issue persists. Consider a distinct classifier for "probe timeout / spawn error" vs "authoritative unauth" (e.g. spawnSyncresult.error?.code === 'ETIMEDOUT'), or at minimum a— possible network issue if slowsoftener whenheygen --versionsucceeded butauth statustimed out.
Nits
skills/media-use/scripts/lib/heygen-cli.mjs:3—HEYGEN_INSTALL_COMMANDuses"...install.sh | bash then heygen auth login..."(double-space +then+ double-space) so the fix line renders as one visually-ambiguous command. Prefer... | bash && heygen auth login --key <key>(real shell chain, cut-and-pasteable) or split into twofixfields. (nit)skills/media-use/scripts/resolve.test.mjs:121-123—runResolveStatusis a straight alias forspawnResolvewith no added behavior; either drop it or inline. (nit)skills/media-use/scripts/resolve.mjs:765,781— passing["auth", "status", "--json"]explicitly (mirroring the JSON-parse branch you already have inemailFromAuthStatus) removes the human-format fragility acrossheygenversion bumps. (nit / defense-in-depth)
Questions
- Cross-PR with #2041: the color-grading types (
grade,lut) are documented as local-only in the SKILL.md diff (local core-preset map, params/CDN look index, deterministic buildCube fallback). Confirming: nograde/lutcode path currently shellsheygenunder any provider fallback? If a future variant does, please route it throughreportHeygenFailureat that point so the classifier applies uniformly. resolve --doctorexit code contract: is any tooling (CI, orchestrators, agent playbooks) already scripting on$?from a prior--doctor-shaped command in this repo? If so, we should decide whether the new contract is0 = ffmpeg present(weak) or0 = all-strictly-required present(strong) before merge. Best to write the contract intoSKILL.mdexplicitly.
What I didn't verify
- Real heygen CLI stderr text for domain "not found" errors (
voice not foundetc.) — asserted from CLI conventions + code path analysis, not from an actual failing invocation. If Miguel has a repro that shows heygen never emitsnot foundon non-installation paths, the blocker downgrades to a concern about defense-in-depth. - HF-runtime-interop mechanics (how the resolver interacts with runtime capability discovery) — deferring to Via / Miga per the lane split.
X-HeyGen-Client-Sourceheader handling onheygen auth status/heygen --versionprobes (assumed the CLI doesn't require it for its own metadata subcommands).
Otherwise clean execution — the SKILL reframe reads well, the file/module split (heygen-cli.mjs as the shared classifier + fix-strings source) is the right cut, and the --doctor printout is genuinely useful when the classifier is right.
— Review by Rames D Jusso
vanceingalls
left a comment
There was a problem hiding this comment.
R1 — hyperframes #2065 at 2c9425ce
Landing after Rames D Jusso's review at 22:45Z. My lens on this one is classifier mechanical correctness + --doctor exit-code contract; Rames covered telemetry, cross-PR consistency, doc/code divergence, and the auth-timeout classifier gap. Cleanest way to post is: concur with his blocker on the "not found" misclassification, then add three unique observations he didn't cover, then note lens overlaps.
Concurrence with Rames D Jusso's blocker
🔴 heygen-cli.mjs:27-31 — lower.includes("not found") reintroduces the exact dead-end this PR is designed to eliminate. Rames named the definitive case (heygen voice speech create --voice-id <invalid> → "voice not found" → classifier says install-heygen → user installs the CLI they already have → same failure). Confirming his read against the diff:
voice-provider.mjs:14catches errors fromheygen ...invocations and now routes them throughreportHeygenFailure, which callsclassifyHeygenError.classifyHeygenErroratheygen-cli.mjs:27-31(line numbers per the diff hunk starting at :20):
if (err?.code === "ENOENT" ||
lower.includes("command not found") ||
lower.includes("not found"))
return HEYGEN_NOT_FOUND_MESSAGE;The err.code === "ENOENT" is the definitive signal for "binary not spawnable." "command not found" matches shell wrappers. Bare "not found" catches any domain-level lookup that includes that substring — voice not found, asset not found in catalog, avatar not found, pending job not found. All of those come from a heygen CLI that just successfully ran, so misclassifying them as "install heygen" is the failure mode this PR set out to prevent.
Recommended fix matches Rames's: drop the bare "not found" clause; keep the ENOENT and shell-"command not found" branches. Add a negative test:
test("does NOT classify domain-level 'not found' as CLI-not-installed", () => {
const msg = classifyHeygenError({ stderr: "voice not found" });
assert.notEqual(msg, HEYGEN_NOT_FOUND_MESSAGE);
});Related to my [[verify-classifier-empirical-premise]] discipline — the substring's empirical premise ("any 'not found' means the binary is missing") is not true for the message surface of a CLI that has installed-and-authenticated failure modes. Same shape as my #2044 miss with pix_fmt; the classifier here has to be built over the ENOENT signal, not over a substring the successful path can also emit.
Additional observations (not covered by Rames)
O1 — heygen --version without a semver token silently passes. resolve.mjs runDoctor:
} else { // heygen on PATH but no semver in --version output
checks.push({ name: "heygen version", ok: true, detail: "heygen version did not include semver", fix: "" });If heygen --version exits 0 but produces text without an X.Y.Z token (weird build, dev version, stripped release), the version check is marked ok. A stricter reading is "unable to verify" and surfaces as a warning. Optimistic-fails-open is a defensible choice, but naming the branch. Not blocker; noting for the checklist.
O2 — node version check is tautologically ok. resolve.mjs: checks.push({ name: "node version", ok: true, detail: process.version, fix: "" }). Since we're running in node, this is true by definition — it prints ✓ v20.10.0 for a check that has never had the ability to fail. If the intent is to gate on node >= X, it wants versionLessThan(process.versions.node, MIN_NODE). If it's purely informational readout, current shape is fine but the ✓ glyph implies actual verification.
O3 — Reporting-path asymmetry. reportHeygenFailure routes actionable messages to bare stderr (console.error(message)) but non-actionable through a prefixed context line (console.error(\media-use: \`${context}\` failed: ${message}`)). That's the correct asymmetry — actionable diagnostics are their own sentences and don't need a wrapper — but it means grep-based log parsing that expects the media-use:` prefix on every heygen failure will see gaps on the actionable path. If any observability tooling downstream keys on that prefix, worth knowing. Not a change request; noting for the record.
Lens overlap with Rames (concurring)
lower.includes("401")is too permissive (Rames's second Concern). Agreed — request IDs (req-401abc), URLs, retry-after headers all trip it.\b401\borHTTP 401/status: 401is the fix.emailFromAuthStatusregex extracts any email-shaped substring from prose (Rames's fourth Concern). Agreed on both the bug and the recommended fix (heygen auth status --json+ take the JSON branch only, drop the human-format bet). My draft had this as a passing observation; Rames's example —"Session expired. Contact support@heygen.ai to re-auth."returningsupport@heygen.aias the authenticated email — is definitive.- ffmpeg vs ffprobe strict-requirement doc/code divergence (Rames's first Concern). Missed this in my initial read. Doc says both required; code gates only on ffmpeg. Pick one before merge.
- No telemetry on
--doctoror its per-check outcome (Rames's fifth Concern). Sibling of my [[telemetry-gap-on-parallel-branch]] discipline — the moment a new observability affordance ships alongside existing catalog/voice telemetry paths, the new branch reliably skips instrumentation. Fix is cheap now, migration later.
Stack note
This PR's own diff is the single onboarding commit; the rest of the branch belongs to #2041. Merge #2041 first per your PR body — #2065 sits at base=feat/media-use-color-grading.
Verified
- Subprocess safety.
runCommandusesspawnSync(bin, argv, { encoding: "utf8", timeout: 15000 })— argv array, no shell, 15s timeout. --doctorexit-code contract mechanically matches PR body ("Exit 0 unless ffmpeg missing") — though Rames flagged that this contradicts the SKILL.md diff calling both strictly-required.- stdout stays JSON on both
--doctorand the failure-diagnostic paths —reportHeygenFailurewrites exclusively toconsole.error. - Classifier tests cover the 4 golden paths (ENOENT, "not logged in", old semver, passthrough) — coverage on the "not found" negative case is exactly what's missing per the blocker above.
R1 by Via
…tract, telemetry - Blocker: classifyHeygenError no longer treats a bare "not found" as CLI-missing (a stale voiceId → "voice not found" was sending users to reinstall a working CLI); keep only ENOENT + "command not found". Regression test added. - 401 now matches \b401\b, not any "401" substring (request IDs no longer misread). - --doctor: top-level ok requires ffmpeg AND ffprobe (matches SKILL.md); emits media_use_doctor_run telemetry; auth status queried with --json + JSON-only parse; auth timeout softened (network issue, not a false "unauthenticated"); node version gated on >= 18; version-without-semver labeled, not silently green. - Nits: install cmd uses && ; dropped the runResolveStatus alias. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv
2c9425c to
3d85aa6
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
R2 — hyperframes #2065 at 3d85aa67
🟢 Cleared. Blocker + all substantive observations addressed. Verified per-finding at the R2 commit.
Blocker (concurring with Rames) — FIXED
"not found" broad match ✅ dropped. heygen-cli.mjs:30:
if (err?.code === "ENOENT" || lower.includes("command not found")) {The bare "not found" clause is gone. Comment at :25-29 names the exact failure mode we flagged (stale voiceId → "voice not found" → reinstall dead-end loop). Two regression tests pin it:
heygen-cli.test.mjs:44—"does not misclassify a resource 'not found' error as a missing CLI"sends"Error: voice not found (id: stale-123)"and asserts it is NOT the install message.heygen-cli.test.mjs:57—"classifies a shell 'command not found' as a missing CLI"sends"bash: heygen: command not found"and asserts it IS the install message.
Positive-and-negative both covered; regression can't reintroduce without a test change.
Other verified fixes
\b401\bregex ✅ —:39:/\b401\b/.test(lower). Test at:23-31covers"HTTP 401 Unauthorized"→ auth (positive) AND"upload failed (request req-401abc)"→ NOT auth (negative). Same positive-and-negative pin as the not-found case.ffmpeg AND ffprobestrict ✅ —resolve.mjs:848:return { ok: !!ffmpeg?.ok && !!ffprobe?.ok, checks }. Comment at:843names SKILL.md as the source of truth. Doctor test atresolve.test.mjs:333reflects the AND semantics.heygen auth status --json✅ —:748:runCommand("heygen", ["auth", "status", "--json"]). Comment at:883: "JSON only... No prose regex fallback" — the human-format bet onemailFromAuthStatusis retired.- Auth timeout softener ✅ —
:752:const timedOut = authProbe.error?.code === "ETIMEDOUT" || authProbe.signal != null;— distinct classifier for probe timeout vs authoritative unauth. Rames's "wrong actionable when network stalls" concern closed. media_use_doctor_runtelemetry ✅ —:124:await track("media_use_doctor_run", { ... });. Rames's observability gap closed.- Node ≥ 18 gate ✅ (my O2 → real check now) —
:37MIN_NODE_VERSION = "18.0.0";:835nodeOk = !versionLessThan(process.versions.node, MIN_NODE_VERSION); fix lineupgrade Node to >= v18.0.0.node versionis no longer tautologically ok. - Version-without-semver labeled
⚠️ mitigated-accepting (my O1) —:809-814: check staysok: truebutdetailnow reads"heygen present; version unverifiable (no semver in --version output)". Not a strict-verify (still fails-open), but the human output no longer implies a real version comparison happened. Acceptable tradeoff for a dev/stripped-build edge case; per my [[r2-verdict-mitigation-vs-full-resolution]] discipline this is honest-mitigation rather than full-resolution. - Install cmd
&&chain ✅ —heygen-cli.mjs:3:"curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --key <key>"— real cut-and-pasteable shell chain (was ambiguous... | bash then heygen ...). runResolveStatusalias dropped ✅ —resolve.test.mjsusesspawnResolvedirectly; the pass-through wrapper is gone.
Not addressed (accepted)
My O3 (reporting-path asymmetry — actionable messages skip the media-use: prefix that non-actionable failures carry) wasn't touched. Confirming that's intentional: actionable diagnostics stand alone as their own sentence; contextual failures get the wrapper. The asymmetry is by design, not a bug. Fine to leave.
R2 by Via — blocker resolved, all substantive observations resolved or mitigated-accepting.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Round 2 review at 3d85aa6.
R1 blocker is resolved — bare "not found" substring gone from the classifier, and the regression test exercises the exact tweeted case (voice not found embedded next to the heygen … command line). Every 🟠 concern is addressed cleanly; two 🟡 nits landed; the Q on --doctor's exit-code contract is only partly closed (code + SKILL.md now agree, but no explicit contract section was added). Nothing new that blocks. CI green on all required checks; mergeable = MERGEABLE, mergeStateStatus = UNSTABLE (Graphite/mergeability_check pending only).
Item-by-item verification
R1 🔴 Blocker
skills/media-use/scripts/lib/heygen-cli.mjs:27-31—lower.includes("not found")will misclassify domain "not found" errors as CLI-not-installed, reintroducing the exact dead-end this PR is designed to eliminate. […] Fix: drop the bare"not found"clause; keeperr?.code === "ENOENT"(system-level: binary not spawnable) andlower.includes("command not found")(shell wrapper's own not-found line). Add a defensive test likeclassifyHeygenError({ stderr: "voice not found" })must NOT returnHEYGEN_NOT_FOUND_MESSAGE— otherwise the next refactor slides back in silently.
→ RESOLVED at skills/media-use/scripts/lib/heygen-cli.mjs:30 (commit 3d85aa6). Gate is now exactly the requested pair: if (err?.code === "ENOENT" || lower.includes("command not found")). Regression tests at heygen-cli.test.mjs:44-55 and :57-61:
- :44-55 — stderr
Error: voice not found (id: stale-123)PLUS amessage: "Command failed: heygen voice speech create …"(the toughest case, sinceclassifyHeygenErrorconcatenates.messageinto the searched text) assertsnotEqual(HEYGEN_NOT_FOUND_MESSAGE). This is the exact tweeted case and it now passes through as detail. - :57-61 — shell wrapper's
bash: heygen: command not foundstill classifies asHEYGEN_NOT_FOUND_MESSAGE, confirming the narrow retained case works.
Tests pass locally (7/7). No stale includes("not found") anywhere else in skills/media-use/.
R1 🟠 Concerns
skills/media-use/scripts/resolve.mjs:814— top-levelokisffmpegonly, butSKILL.md(this PR, line 178) claimsffmpeg/ffprobeare strictly-required. […] Pick one: either widen the gate toffmpeg && ffprobe(matches SKILL.md), or narrow the SKILL.md line to "ffmpegis strictly required;ffprobeis required for probe operations" (matches code).
→ RESOLVED at resolve.mjs:846-848: return { ok: !!ffmpeg?.ok && !!ffprobe?.ok, checks };. SKILL.md line 358 still declares "ffmpeg/ffprobe are strictly required" — two sources of truth now agree. The test at resolve.test.mjs:333-364 was rewritten to assert this: strictOk = ffmpeg.ok && ffprobe.ok; both parsed.ok and process exit code must equal strictOk. That test would fail loudly if the gate ever regresses to ffmpeg-only.
skills/media-use/scripts/lib/heygen-cli.mjs:36—lower.includes("401")matches any freeform text containing the literal substring "401". Request IDs (req-401abc), URLs, retry-after headers […] all trip this. Tighten to a word/whitespace boundary or a status-code shape.
→ RESOLVED at heygen-cli.mjs:39: /\b401\b/.test(lower). Test at heygen-cli.test.mjs:23-34 covers both directions — HTTP 401 Unauthorized classifies as auth failure, upload failed (request req-401abc) does not.
skills/media-use/scripts/resolve.mjs:849-862—emailFromAuthStatusregex captures the first@-shaped token in any prose. […] Suggest gating on a preamble […] or — better — passing--jsonexplicitly toheygen auth statusand taking the JSON path only.
→ RESOLVED at resolve.mjs:748 (invocation now heygen auth status --json) and resolve.mjs:882-894 (parser is JSON-only — early-returns null when the body isn't {…}; no prose fallback). The "Session expired. Contact support@heygen.ai" failure mode is no longer reachable — that body wouldn't start with {, emailFromAuthStatus returns null, authProbe.status === 0 gate still fails, and --doctor prints the correct unauthenticated fix.
No telemetry on
--doctorinvocation or per-check outcome.
→ RESOLVED at resolve.mjs:124-128: await track("media_use_doctor_run", { ok, checks_failed, failed: […names] }). Awaited so a short-lived doctor run flushes before exit. Emits on both success and failure paths (the failed array is empty on the green path but the event still fires), so the "how often is --doctor run" signal answers even when everyone's environment is healthy — no observability-on-failure-only gap.
skills/media-use/scripts/resolve.mjs:765,781—heygen auth statusruns with no--json, no timeout distinction between slow-network and unauth. […] Consider a distinct classifier for "probe timeout / spawn error" vs "authoritative unauth" […] or at minimum a— possible network issue if slowsoftener whenheygen --versionsucceeded butauth statustimed out.
→ RESOLVED at resolve.mjs:747-763. heygenAuthCheck now inspects authProbe.error?.code === "ETIMEDOUT" || authProbe.signal != null and, when true, returns detail heygen auth status timed out — possible network issue, not proof of sign-out and fix check network, then re-run --doctor — no wrong "re-login" prompt. Two small caveats worth noting but not blocking:
- Node's
spawnSyncon timeout setssignal='SIGTERM'(kill signal), noterror.code='ETIMEDOUT'— so thesignal != nulldisjunct is doing the real work; theETIMEDOUThalf is defensive-only. That's fine, just calling out that if you ever narrow thesignalcheck toSIGTERMspecifically, theETIMEDOUThalf won't be a safety net. signal != nullalso fires on any signal-terminated exit (SIGINT/SIGSEGV/etc.), which will fall under the same "possible network issue" copy. Not a real bug — those cases would also not be authoritative sign-outs — but the copy leans network-flavored.
R1 🟡 Nits
heygen-cli.mjs:3— double-space +then+ double-space. Prefer… | bash && heygen auth login --key <key>(real shell chain, cut-and-pasteable) or split into twofixfields.
→ RESOLVED at heygen-cli.mjs:3 — now curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --key <key>. Cut-and-pasteable.
resolve.test.mjs:121-123—runResolveStatusis a straight alias forspawnResolvewith no added behavior; either drop it or inline.
→ RESOLVED — alias gone from resolve.test.mjs; only runResolve (execFileSync) and spawnResolve (spawnSync) remain, each with a distinct role.
resolve.mjs:765,781— passing["auth", "status", "--json"]explicitly mirrors the JSON-parse branch you already have.
→ RESOLVED at resolve.mjs:748 — invocation is heygen auth status --json; the parser is JSON-only. Same fix that closed the auth-regex concern.
R1 Questions
Cross-PR with #2041: the color-grading types (
grade,lut) are documented as local-only in the SKILL.md diff. Confirming: nograde/lutcode path currently shellsheygenunder any provider fallback?
→ CONFIRMED by code path at resolve.mjs:302-304: if (type === "grade" || type === "lut") return resolveColor(...), which routes exclusively through resolveGrade / resolveLut → matchColorLook / paramsFromIntent / freezeLibraryLut — none of which shell heygen. The reportHeygenFailure callers are only in heygen-search.mjs (bgm/sfx/image/icon catalog) and voice-provider.mjs. Grade/lut cleanly bypass the classifier today; if a future variant adds a heygen path, please route it through reportHeygenFailure so the classifier applies uniformly.
resolve --doctorexit code contract: is any tooling already scripting on$?from a prior--doctor-shaped command in this repo? If so, we should decide whether the new contract is0 = ffmpeg present(weak) or0 = all-strictly-required present(strong) before merge. Best to write the contract intoSKILL.mdexplicitly.
→ PARTIAL. Code side is settled — the contract is strong (exit 0 iff ffmpeg AND ffprobe present), enforced by the test at resolve.test.mjs:333-364 that asserts status === (strictOk ? 0 : 1). SKILL.md line 358 declares ffmpeg+ffprobe strictly-required, which implies the contract. What's missing is a plain sentence in the --doctor section (around SKILL.md:17-21 or in the CLI table at :136) that says out loud "exits 0 iff all strictly-required deps present; non-zero for any missing strict dep or unauthenticated heygen? etc." — that would remove the last ambiguity for anyone scripting on $?. Not blocking; worth a follow-up commit.
R2-NEW findings
- 🟡
resolve.mjs:835—versionLessThan(process.versions.node, MIN_NODE_VERSION)for the node-version check silently green-lights any node build whoseprocess.versions.nodedoesn't match^v?(\d+)\.(\d+)\.(\d+)$.versionPartsreturnsnullon non-plain-semver →versionLessThanreturnsfalse→nodeOk=true. For mainline node this is fine (process.versions.nodeis always plainX.Y.Z), but if anyone ever runs under a pre-release/nightly build the check goes silently green rather than either failing loudly or labeling the version as unverifiable the way the heygen path does at:809-814. Consider mirroring the heygen "version unverifiable" branch for node, or at least a strict-numeric fallback. (nit; low-priority) - 🟡
resolve.mjs:882-894emailFromAuthStatustrusts anything parsable fromparsed?.data?.email || parsed?.email. No email-shape check. Ifheygen auth status --jsonever returns{ data: { email: null } }on partial-sign-in, that renders as authenticated-as-null. Also — the value goes into a user-facing print at:757-758(heygen authenticated as ${email}) without any sanitization, so a malformed CLI output could inject arbitrary text into--doctoroutput. In practiceheygen's own JSON is trusted, so this is defense-in-depth only. (nit)
What I didn't verify
- Actual behavior of
heygen auth status --jsonin the wild (return schema, error shape). Asserted from the code path; if the CLI ever wraps the email in a different key,emailFromAuthStatusreturnsnulland--doctorreports unauth. That's a safer failure than the R1 shape, but worth a live check pre-merge. - Real Node
spawnSynctimeout semantics for the auth-status probe (whethererror.code === "ETIMEDOUT"ever gets set on this platform / node version). The|| signal != nulldisjunct catches the SIGTERM case regardless, so the classification is safe either way. - Whether the removed
runResolveStatusalias had any downstream in-repo caller (grep inskills/shows none; couldn't grep the whole monorepo from this worktree).
Clean R2. The blocker fix is precisely-shaped and pinned by the exact regression test I asked for — nice work. Only the SKILL.md exit-code sentence is left as a genuine follow-up; everything else is done.
— Review by Rames D Jusso
… --doctor, free-usage framing media-use resolves bgm/sfx/image/icon (catalog), voice (TTS), and avatar video through the heygen CLI — the free-usage path. Agents hit a dead end when it's missing/unauthed. This guides them to install it fast, at the point of need. - Centralized actionable diagnostics (lib/heygen-cli.mjs): every heygen-backed resolve, on failure, prints the exact fix on stderr — not-installed (curl install one-liner), not-authenticated (heygen auth login), outdated (heygen update). Routed through heygen-search + voice-provider. stdout stays clean JSON. - resolve --doctor preflight (human + --json): checks heygen present/version/ auth, ffmpeg, ffprobe, node, a fix per gap. Exit 0 unless ffmpeg missing. - SKILL reframe: install-first callout; heygen as the free-usage gateway for bgm/image/voice/avatar-video; removed the false "degrades gracefully" claim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv
…tract, telemetry - Blocker: classifyHeygenError no longer treats a bare "not found" as CLI-missing (a stale voiceId → "voice not found" was sending users to reinstall a working CLI); keep only ENOENT + "command not found". Regression test added. - 401 now matches \b401\b, not any "401" substring (request IDs no longer misread). - --doctor: top-level ok requires ffmpeg AND ffprobe (matches SKILL.md); emits media_use_doctor_run telemetry; auth status queried with --json + JSON-only parse; auth timeout softened (network issue, not a false "unauthenticated"); node version gated on >= 18; version-without-semver labeled, not silently green. - Nits: install cmd uses && ; dropped the runResolveStatus alias. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv
3d85aa6 to
abbfaa7
Compare

What & why
media-use resolves bgm / sfx / image / icon / logo (catalog), voice (TTS), and avatar video through the
heygenCLI — the free-usage path. CLI-feedback shows agents repeatedly hit a dead end whenheygenis missing/unauthed: they get a vague error, install it by trial, and there's no preflight. This makes the dependency self-service — it guides agents to install it fast, at the moment of need.Changes
lib/heygen-cli.mjs) — every heygen-backed resolve, on failure, prints the exact fix on stderr (stdout stays clean JSON):curl -fsSL https://static.heygen.ai/cli/install.sh | bashthenheygen auth loginheygen auth login --key <key>heygen update(need ≥ v0.1.6)Routed through
heygen-search(catalog) andvoice-provider(TTS) so all heygen-backed types are consistent.resolve --doctorpreflight (human +--json) — checks heygen present / version / auth, ffmpeg, ffprobe, node, with a fix command per gap. Exit 0 unless ffmpeg (the only strictly-required dep) is missing; a missing/unauthed heygen is reported, not fatal. The SKILL tells agents to run it first.Verification
logo);oxlint+oxfmt --checkclean.resolve --doctor(human + json) → all six checks green on a provisioned machine.bgmresolve → stdout valid JSON (ok:false), stderr carries the exact actionable install line.--doctorwith heygen hidden → reports it ✗ with the install fix, exits 0 (ffmpeg present).Notes