Skip to content

Commit d570af8

Browse files
miguel-heygenclaude
andcommitted
fix(media-use): require OAuth-capable heygen CLI (v0.3.0), fix auth-status probe
E2E against the live free-usage backend surfaced three issues: - HEYGEN_MIN_VERSION was 0.1.6, but that CLI can't use OAuth ("heygen-cli can't use OAuth yet") — free usage needs >= v0.3.0. Bumped the floor; --doctor now also nudges `heygen update` when a newer stable exists (always-latest). - Onboarding pointed at `heygen auth login --key` (API credits / billing); the free path is `--oauth` (subscription/free credits). Fixed install + auth guidance and SKILL.md accordingly. - `heygen auth status --json` is an unknown flag on v0.3.0 (JSON is the default output) — the added --json broke auth detection. Dropped it; verified --doctor reports authenticated on a real free (OAuth) account. Tests assert against the exported message constants instead of brittle literals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv
1 parent abbfaa7 commit d570af8

7 files changed

Lines changed: 61 additions & 37 deletions

File tree

skills-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"files": 10
4747
},
4848
"media-use": {
49-
"hash": "4507353b2551d1a5",
49+
"hash": "8676f1777f3d14ab",
5050
"files": 115
5151
},
5252
"motion-graphics": {

skills/media-use/SKILL.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ The media OS for HyperFrames: resolve · generate · operate · remember, every
1111

1212
```bash
1313
curl -fsSL https://static.heygen.ai/cli/install.sh | bash
14-
heygen auth login --key <key>
14+
heygen update # free usage needs the OAuth-capable CLI (v0.3.0+)
15+
heygen auth login --oauth # OAuth = free subscription credits; --api-key bills API credits
1516
```
1617

17-
This unlocks the FREE path for bgm/sfx/image/icon catalog search, TTS (voice), and avatar videos. Before resolving anything, verify setup with:
18+
This unlocks the FREE path for bgm/sfx/image/icon catalog search, TTS (voice), and avatar videos. Sign in with `--oauth` — the free allowance rides on the OAuth session (an API key bills API credits instead). Before resolving anything, verify setup with:
1819

1920
```bash
2021
node <SKILL_DIR>/scripts/resolve.mjs --doctor
@@ -357,15 +358,15 @@ tools are OPT-IN alternatives where they exist; install one to unlock its free,
357358
private, on-device path instead of or ahead of HeyGen for that type. Only
358359
`ffmpeg`/`ffprobe` are strictly required for the tool to run at all.
359360

360-
| Tool | Serves | Install |
361-
| ------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
362-
| `ffmpeg`/`ffprobe` | adopt probing, smart-grade signalstats, cut, duck bake, loudnorm | system package (`brew install ffmpeg`) |
363-
| `heygen` | catalog (bgm/sfx/image/icon) + TTS (voice) + avatar video — the free-usage path | `curl -fsSL https://static.heygen.ai/cli/install.sh \| bash` then `heygen auth login --key <key>` (needs >= v0.1.6) |
364-
| `mflux-generate` | local image gen (FLUX), best-for-RAM | `uv venv ~/.venvs/mflux && VIRTUAL_ENV=~/.venvs/mflux uv pip install mflux==0.9.6` |
365-
| `codex` | image gen upsell (ChatGPT sub) | Codex CLI, logged in via ChatGPT (owns its own auth) |
366-
| `parakeet-mlx` | local transcription (default ASR, best) | `uv venv ~/.venvs/parakeet && VIRTUAL_ENV=~/.venvs/parakeet uv pip install parakeet-mlx` |
367-
| `ltx-2-mlx` | local video gen | `git clone https://github.com/dgrauet/ltx-2-mlx && cd ltx-2-mlx && uv sync --all-extras` |
368-
| `npx hyperframes` | Kokoro TTS (voice), whisper.cpp (transcribe fallback), remove-background | bundled with the hyperframes CLI |
361+
| Tool | Serves | Install |
362+
| ------------------ | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
363+
| `ffmpeg`/`ffprobe` | adopt probing, smart-grade signalstats, cut, duck bake, loudnorm | system package (`brew install ffmpeg`) |
364+
| `heygen` | catalog (bgm/sfx/image/icon) + TTS (voice) + avatar video — the free-usage path | `curl -fsSL https://static.heygen.ai/cli/install.sh \| bash` then `heygen auth login --oauth` (needs >= v0.3.0) |
365+
| `mflux-generate` | local image gen (FLUX), best-for-RAM | `uv venv ~/.venvs/mflux && VIRTUAL_ENV=~/.venvs/mflux uv pip install mflux==0.9.6` |
366+
| `codex` | image gen upsell (ChatGPT sub) | Codex CLI, logged in via ChatGPT (owns its own auth) |
367+
| `parakeet-mlx` | local transcription (default ASR, best) | `uv venv ~/.venvs/parakeet && VIRTUAL_ENV=~/.venvs/parakeet uv pip install parakeet-mlx` |
368+
| `ltx-2-mlx` | local video gen | `git clone https://github.com/dgrauet/ltx-2-mlx && cd ltx-2-mlx && uv sync --all-extras` |
369+
| `npx hyperframes` | Kokoro TTS (voice), whisper.cpp (transcribe fallback), remove-background | bundled with the hyperframes CLI |
369370

370371
The RAM-graded local-model shortlist + exact per-tier install/invoke lives in
371372
`scripts/lib/local-models.mjs` (the agent can read `describeModelLadder(cap, specs)`
@@ -375,7 +376,7 @@ provider exists (e.g. no `mflux` -> codex image upsell; no `parakeet-mlx` -> whi
375376

376377
`heygen asset search` is a pre-launch command hidden from `heygen --help`, but it
377378
runs; providers tag requests with the allowlisted `X-HeyGen-Client-Source` header
378-
(v0.1.6+).
379+
(v0.3.0+).
379380

380381
## Telemetry
381382

skills/media-use/scripts/lib/heygen-cli.mjs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
export const HEYGEN_MIN_VERSION = "0.1.6";
1+
// v0.3.0 is the first CLI that can use an OAuth session; v0.1.x/0.2.x reject it
2+
// ("heygen-cli can't use OAuth yet"), and OAuth is what the free-usage path
3+
// needs — so anything below this can't authenticate for free usage at all.
4+
export const HEYGEN_MIN_VERSION = "0.3.0";
5+
// Free-usage path is OAuth (`--oauth` → subscription/free credits); `--api-key`
6+
// bills API credits, so the onboarding steers to OAuth.
27
export const HEYGEN_INSTALL_COMMAND =
3-
"curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --key <key>";
4-
export const HEYGEN_AUTH_COMMAND = "heygen auth login --key <key>";
8+
"curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --oauth";
9+
export const HEYGEN_AUTH_COMMAND = "heygen auth login --oauth";
510
export const HEYGEN_UPDATE_COMMAND = "heygen update";
611

712
export const HEYGEN_NOT_FOUND_MESSAGE = `media-use: heygen CLI not found — it's the free path for bgm/image/voice/avatar-video. Install: ${HEYGEN_INSTALL_COMMAND}`;

skills/media-use/scripts/lib/heygen-cli.test.mjs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,40 @@
11
import { strict as assert } from "node:assert";
22
import { test } from "node:test";
3-
import { classifyHeygenError, HEYGEN_NOT_FOUND_MESSAGE } from "./heygen-cli.mjs";
3+
import {
4+
classifyHeygenError,
5+
HEYGEN_NOT_AUTHENTICATED_MESSAGE,
6+
HEYGEN_NOT_FOUND_MESSAGE,
7+
HEYGEN_OUTDATED_MESSAGE,
8+
} from "./heygen-cli.mjs";
49

510
test("classifies ENOENT-style missing heygen errors with install instructions", () => {
611
const message = classifyHeygenError({ code: "ENOENT", message: "spawn heygen ENOENT" });
712

8-
assert.equal(
9-
message,
10-
"media-use: heygen CLI not found — it's the free path for bgm/image/voice/avatar-video. Install: curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --key <key>",
11-
);
13+
assert.equal(message, HEYGEN_NOT_FOUND_MESSAGE);
1214
});
1315

1416
test("classifies auth failures with login instructions", () => {
1517
const message = classifyHeygenError({ stderr: Buffer.from("Error: not logged in") });
1618

17-
assert.equal(
18-
message,
19-
"media-use: heygen CLI not authenticated (free usage) — run: heygen auth login --key <key>",
20-
);
19+
assert.equal(message, HEYGEN_NOT_AUTHENTICATED_MESSAGE);
2120
});
2221

2322
test("classifies a real 401 as auth, but not a bare 401 substring in prose", () => {
2423
assert.equal(
2524
classifyHeygenError({ stderr: Buffer.from("HTTP 401 Unauthorized") }),
26-
"media-use: heygen CLI not authenticated (free usage) — run: heygen auth login --key <key>",
25+
HEYGEN_NOT_AUTHENTICATED_MESSAGE,
2726
);
2827
// A request id that merely contains "401" must NOT read as an auth failure.
2928
const noise = classifyHeygenError({ stderr: Buffer.from("upload failed (request req-401abc)") });
30-
assert.notEqual(
31-
noise,
32-
"media-use: heygen CLI not authenticated (free usage) — run: heygen auth login --key <key>",
33-
);
29+
assert.notEqual(noise, HEYGEN_NOT_AUTHENTICATED_MESSAGE);
3430
});
3531

3632
test("classifies old heygen versions with update instructions", () => {
3733
const message = classifyHeygenError({
3834
stderr: Buffer.from("heygen v0.1.5 does not support --headers"),
3935
});
4036

41-
assert.equal(message, "media-use: heygen CLI is outdated — run: heygen update (need >= v0.1.6)");
37+
assert.equal(message, HEYGEN_OUTDATED_MESSAGE);
4238
});
4339

4440
test("does not misclassify a resource 'not found' error as a missing CLI", () => {

skills/media-use/scripts/lib/heygen-search.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function heygenSearch(subcommand, query, { type, limit = 5, minScore } =
55
// execFileSync with an argv array (no shell), so query/type/etc. are passed as
66
// literal arguments — no quoting tricks, no command injection. subcommand is a
77
// hardcoded multi-word string (e.g. "audio sounds list"), split into tokens.
8-
// Tag the caller via the CLI's allowlisted attribution header (heygen >= v0.1.6).
8+
// Tag the caller via the CLI's allowlisted attribution header (heygen >= v0.3.0).
99
const args = [
1010
"--headers",
1111
"X-HeyGen-Client-Source: media-use",

skills/media-use/scripts/lib/voice-provider.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { reportHeygenFailure } from "./heygen-cli.mjs";
33

44
// Voice / TTS generation via the HeyGen CLI — the only external CLI media-use
55
// shells (CLI-only invariant: media-use holds no keys; the CLI owns auth).
6-
// Flags verified against `heygen voice speech create --help` (v0.1.6).
6+
// Flags verified against `heygen voice speech create --help` (v0.3.0).
77

88
function runJson(bin, argv, label) {
99
let out;

skills/media-use/scripts/resolve.mjs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -744,8 +744,23 @@ async function showCandidates() {
744744
}
745745
}
746746

747+
// Best-effort latest stable CLI tag from the CDN (the install script's source of
748+
// truth). null on any failure (offline, no curl) — treated as "unknown", never fatal.
749+
function latestHeygenStable() {
750+
const probe = runCommand("curl", [
751+
"-fsSL",
752+
"--max-time",
753+
"4",
754+
"https://static.heygen.ai/cli/stable",
755+
]);
756+
return probe.status === 0 ? firstSemver(commandText(probe)) : null;
757+
}
758+
747759
function heygenAuthCheck() {
748-
const authProbe = runCommand("heygen", ["auth", "status", "--json"]);
760+
// `heygen auth status` already emits JSON by default (only `--human` opts out
761+
// to a table) — there is no `--json`/`--output` flag; passing one errors with
762+
// "unknown flag". emailFromAuthStatus parses that default JSON.
763+
const authProbe = runCommand("heygen", ["auth", "status"]);
749764
// spawnSync sets .error/.signal on a timeout or spawn failure (status then
750765
// null). A stalled auth endpoint (transient network/DNS) must not be reported
751766
// as an authoritative "not authenticated" with a re-login fix.
@@ -794,11 +809,18 @@ function runDoctor() {
794809
});
795810
} else if (heygenVersion) {
796811
const versionOk = !versionLessThan(heygenVersion, HEYGEN_MIN_VERSION);
812+
// Keep it latest: even when the installed version clears the floor, nudge
813+
// `heygen update` if a newer stable exists. Best-effort — silently skipped
814+
// when the CDN is unreachable, so it never blocks the check.
815+
const latest = versionOk ? latestHeygenStable() : null;
816+
const behind = latest && versionLessThan(heygenVersion, latest);
797817
checks.push({
798818
name: "heygen version",
799819
ok: versionOk,
800-
detail: `heygen v${heygenVersion} (need >= v${HEYGEN_MIN_VERSION})`,
801-
fix: versionOk ? "" : HEYGEN_UPDATE_COMMAND,
820+
detail: versionOk
821+
? `heygen v${heygenVersion}${behind ? ` (latest v${latest} available)` : ""}`
822+
: `heygen v${heygenVersion} (need >= v${HEYGEN_MIN_VERSION})`,
823+
fix: versionOk ? (behind ? HEYGEN_UPDATE_COMMAND : "") : HEYGEN_UPDATE_COMMAND,
802824
});
803825

804826
checks.push(heygenAuthCheck());
@@ -880,7 +902,7 @@ function firstLine(text) {
880902
}
881903

882904
function emailFromAuthStatus(text) {
883-
// JSON only (auth status is queried with --json). No prose regex fallback: a
905+
// JSON only (auth status emits JSON by default). No prose regex fallback: a
884906
// human-format body like "Session expired. Contact support@heygen.ai" would
885907
// otherwise report the user as authenticated as support@heygen.ai.
886908
const trimmed = String(text || "").trim();

0 commit comments

Comments
 (0)