Skip to content

feat(media-use): fast heygen CLI onboarding — actionable diagnostics, --doctor, free-usage framing#2065

Open
miguel-heygen wants to merge 2 commits into
mainfrom
feat/media-use-heygen-onboarding
Open

feat(media-use): fast heygen CLI onboarding — actionable diagnostics, --doctor, free-usage framing#2065
miguel-heygen wants to merge 2 commits into
mainfrom
feat/media-use-heygen-onboarding

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

📚 Stack (top): stacked on #2041 (color grading). This PR's own diff is the single heygen-onboarding commit; the rest belongs to #2041. Merge #2041 first.

What & why

media-use resolves bgm / sfx / image / icon / logo (catalog), voice (TTS), and avatar video through the heygen CLI — the free-usage path. CLI-feedback shows agents repeatedly hit a dead end when heygen is 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

  • Actionable failure diagnostics (lib/heygen-cli.mjs) — every heygen-backed resolve, on failure, prints the exact fix on stderr (stdout stays clean JSON):
    • not installed → curl -fsSL https://static.heygen.ai/cli/install.sh | bash then heygen auth login
    • not authenticated → heygen auth login --key <key>
    • outdated → heygen update (need ≥ v0.1.6)
      Routed through heygen-search (catalog) and voice-provider (TTS) so all heygen-backed types are consistent.
  • resolve --doctor preflight (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.
  • SKILL reframe — install-first callout up top; heygen positioned as the free-usage gateway for bgm/image/voice/avatar-video (not "upsell"); removed the false "degrades gracefully to the free/cloud path" claim (for those types, heygen is the path).

Verification

  • media-use suite 135/135 (on the stacked base — includes color grading + logo); oxlint + oxfmt --check clean.
  • resolve --doctor (human + json) → all six checks green on a provisioned machine.
  • Missing-heygen bgm resolve → stdout valid JSON (ok:false), stderr carries the exact actionable install line.
  • --doctor with heygen hidden → reports it ✗ with the install fix, exits 0 (ffmpeg present).

Notes

  • Fallback is guidance-only by design (installing heygen is free) — no bundled audio / licensing surface.
  • Pairs with the in-flight free-usage avatar video + TTS work: this is the onboarding layer that makes that path land without install friction.

@miguel-heygen miguel-heygen force-pushed the feat/media-use-heygen-onboarding branch from 5fa2c12 to 928539e Compare July 8, 2026 18:29
@miguel-heygen miguel-heygen changed the base branch from main to feat/media-use-color-grading July 8, 2026 18:29
@mintlify

mintlify Bot commented Jul 8, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Jul 8, 2026, 6:52 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

miguel-heygen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-31lower.includes("not found") will misclassify domain "not found" errors as CLI-not-installed, reintroducing the exact dead-end this PR is designed to eliminate. classifyHeygenError catches an error from a successfully invoked heygen binary and then decides "installed?" from stderr substring alone. Concrete case: voice-provider.mjs:62 calls heygen voice speech create --voice-id <id>; if the caller passes a stale/invalid voiceId, the CLI's most-idiomatic error phrasing is voice not found (or voice with id X not found). That hits lower.includes("not found") and we print media-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 search with an invalid ID and heygen voice list referencing a missing engine are the same shape. Fix: drop the bare "not found" clause; keep err?.code === "ENOENT" (system-level: binary not spawnable) and lower.includes("command not found") (shell wrapper's own not-found line). Add a defensive test like classifyHeygenError({ stderr: "voice not found" }) must NOT return HEYGEN_NOT_FOUND_MESSAGE — otherwise the next refactor slides back in silently. (My memory has a matching pattern under substring_match_numeric_code_in_freeform_msg — same class of hazard applied to text codes here.)

Concerns

  • skills/media-use/scripts/resolve.mjs:814 — top-level ok is ffmpeg only, but SKILL.md (this PR, line 178) claims ffmpeg/ffprobe are strictly-required. Two sources of truth diverge. If any downstream script gates on resolve --doctor exit code to preflight a build (which is the obvious --doctor contract), a missing ffprobe box passes the gate and the build breaks at first ffprobe call. The PR body says "ffmpeg (the only strictly-required dep)" but the SKILL.md diff includes ffprobe. Pick one: either widen the gate to ffmpeg && ffprobe (matches SKILL.md), or narrow the SKILL.md line to "ffmpeg is strictly required; ffprobe is required for probe operations" (matches code). Also — the --doctor --json test at resolve.test.mjs:632-633 enshrines 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:36lower.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 (ffmpeg piped through, etc.) all trip this. Tighten to a word/whitespace boundary or a status-code shape (\b401\b regex, or HTTP 401 / status: 401 / \s401\s) so genuine 401 responses classify but noise doesn't.

  • skills/media-use/scripts/resolve.mjs:849-862emailFromAuthStatus regex captures the first @-shaped token in any prose. If heygen auth status returns success with a body like Session expired. Contact support@heygen.ai to re-auth., emailFromAuthStatus returns support@heygen.ai and --doctor prints ✓ 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 --json explicitly to heygen auth status and taking the JSON path only. The human-readable fallback is already a bet on a CLI-version-stable format; passing --json removes the bet.

  • No telemetry on --doctor invocation 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 minimal capture("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,781heygen auth status runs with no --json, no timeout distinction between slow-network and unauth. runCommand uses a shared 15s ceiling; if the auth endpoint stalls (transient network / DNS), status !== 0 and --doctor reports "heygen not authenticated" with fix heygen 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. spawnSync result.error?.code === 'ETIMEDOUT'), or at minimum a — possible network issue if slow softener when heygen --version succeeded but auth status timed out.

Nits

  • skills/media-use/scripts/lib/heygen-cli.mjs:3HEYGEN_INSTALL_COMMAND uses "...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 two fix fields. (nit)
  • skills/media-use/scripts/resolve.test.mjs:121-123runResolveStatus is a straight alias for spawnResolve with 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 in emailFromAuthStatus) removes the human-format fragility across heygen version 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: no grade/lut code path currently shells heygen under any provider fallback? If a future variant does, please route it through reportHeygenFailure at that point so the classifier applies uniformly.
  • resolve --doctor exit 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 is 0 = ffmpeg present (weak) or 0 = all-strictly-required present (strong) before merge. Best to write the contract into SKILL.md explicitly.

What I didn't verify

  • Real heygen CLI stderr text for domain "not found" errors (voice not found etc.) — asserted from CLI conventions + code path analysis, not from an actual failing invocation. If Miguel has a repro that shows heygen never emits not found on 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-Source header handling on heygen auth status / heygen --version probes (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 vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-31lower.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:14 catches errors from heygen ... invocations and now routes them through reportHeygenFailure, which calls classifyHeygenError.
  • classifyHeygenError at heygen-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\b or HTTP 401 / status: 401 is the fix.
  • emailFromAuthStatus regex 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." returning support@heygen.ai as 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 --doctor or 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. runCommand uses spawnSync(bin, argv, { encoding: "utf8", timeout: 15000 }) — argv array, no shell, 15s timeout.
  • --doctor exit-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 --doctor and the failure-diagnostic paths — reportHeygenFailure writes exclusively to console.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

miguel-heygen added a commit that referenced this pull request Jul 9, 2026
…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
@miguel-heygen miguel-heygen force-pushed the feat/media-use-heygen-onboarding branch from 2c9425c to 3d85aa6 Compare July 9, 2026 00:07

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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\b regex ✅:39: /\b401\b/.test(lower). Test at :23-31 covers "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 ffprobe strict ✅resolve.mjs:848: return { ok: !!ffmpeg?.ok && !!ffprobe?.ok, checks }. Comment at :843 names SKILL.md as the source of truth. Doctor test at resolve.test.mjs:333 reflects 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 on emailFromAuthStatus is 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_run telemetry ✅:124: await track("media_use_doctor_run", { ... });. Rames's observability gap closed.
  • Node ≥ 18 gate ✅ (my O2 → real check now) — :37 MIN_NODE_VERSION = "18.0.0"; :835 nodeOk = !versionLessThan(process.versions.node, MIN_NODE_VERSION); fix line upgrade Node to >= v18.0.0. node version is no longer tautologically ok.
  • Version-without-semver labeled ⚠️ mitigated-accepting (my O1) — :809-814: check stays ok: true but detail now 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 ...).
  • runResolveStatus alias dropped ✅resolve.test.mjs uses spawnResolve directly; 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 james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-31lower.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; keep err?.code === "ENOENT" (system-level: binary not spawnable) and lower.includes("command not found") (shell wrapper's own not-found line). Add a defensive test like classifyHeygenError({ stderr: "voice not found" }) must NOT return HEYGEN_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 a message: "Command failed: heygen voice speech create …" (the toughest case, since classifyHeygenError concatenates .message into the searched text) asserts notEqual(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 found still classifies as HEYGEN_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-level ok is ffmpeg only, but SKILL.md (this PR, line 178) claims ffmpeg/ffprobe are strictly-required. […] Pick one: either widen the gate to ffmpeg && ffprobe (matches SKILL.md), or narrow the SKILL.md line to "ffmpeg is strictly required; ffprobe is 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:36lower.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-862emailFromAuthStatus regex captures the first @-shaped token in any prose. […] Suggest gating on a preamble […] or — better — passing --json explicitly to heygen auth status and 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 --doctor invocation 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,781heygen auth status runs 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 slow softener when heygen --version succeeded but auth status timed 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 spawnSync on timeout sets signal='SIGTERM' (kill signal), not error.code='ETIMEDOUT' — so the signal != null disjunct is doing the real work; the ETIMEDOUT half is defensive-only. That's fine, just calling out that if you ever narrow the signal check to SIGTERM specifically, the ETIMEDOUT half won't be a safety net.
  • signal != null also 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 two fix fields.

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-123runResolveStatus is a straight alias for spawnResolve with 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: no grade/lut code path currently shells heygen under any provider fallback?

CONFIRMED by code path at resolve.mjs:302-304: if (type === "grade" || type === "lut") return resolveColor(...), which routes exclusively through resolveGrade / resolveLutmatchColorLook / 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 --doctor exit 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 is 0 = ffmpeg present (weak) or 0 = all-strictly-required present (strong) before merge. Best to write the contract into SKILL.md explicitly.

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:835versionLessThan(process.versions.node, MIN_NODE_VERSION) for the node-version check silently green-lights any node build whose process.versions.node doesn't match ^v?(\d+)\.(\d+)\.(\d+)$. versionParts returns null on non-plain-semver → versionLessThan returns falsenodeOk = true. For mainline node this is fine (process.versions.node is always plain X.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-894 emailFromAuthStatus trusts anything parsable from parsed?.data?.email || parsed?.email. No email-shape check. If heygen auth status --json ever 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 --doctor output. In practice heygen's own JSON is trusted, so this is defense-in-depth only. (nit)

What I didn't verify

  • Actual behavior of heygen auth status --json in the wild (return schema, error shape). Asserted from the code path; if the CLI ever wraps the email in a different key, emailFromAuthStatus returns null and --doctor reports unauth. That's a safer failure than the R1 shape, but worth a live check pre-merge.
  • Real Node spawnSync timeout semantics for the auth-status probe (whether error.code === "ETIMEDOUT" ever gets set on this platform / node version). The || signal != null disjunct catches the SIGTERM case regardless, so the classification is safe either way.
  • Whether the removed runResolveStatus alias had any downstream in-repo caller (grep in skills/ 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

Base automatically changed from feat/media-use-color-grading to main July 9, 2026 02:20
miguel-heygen and others added 2 commits July 8, 2026 22:21
… --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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants