* fix(cli): billing and Desktop handoff read their hosts from settings
* fix(cli): OAuth client and API hosts read from settings
* test(cli): host and client settings tests run the same on any machine
* chore(cli): format and refresh the skills manifest
* chore(cli): trim a comment
* chore(cli): keep the stylesheet as it was
* fix(core): motion blur preview no longer slows down later in the timeline
With data-motion-blur-events="on", which Studio's Subtle and Strong write,
both blur tiers ran timeline callbacks at every per-frame pose read. Each
frame's reads step back from half a shutter ahead to the frame, so a
callback that keeps state and replays from zero on a backward seek (a
ripple simulation, for example) redid its whole history most frames, and
the cost grew with the playhead.
The mover scan still runs callbacks, so motion set from a render(t)
callback is still found. With events on, it also scans once without them,
and the per-frame reads run callbacks only when the two scans disagree.
The eventful scan runs first, so tweens take their start values after the
callbacks before them and a one-off call fires as often as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): keep the motion blur comments to the invariant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): motion blur reads run callbacks when a callback moves any box
Comparing which elements each scan finds missed a callback that pans a
mover's ancestor while a tween also changes that ancestor: both scans
found the same movers and the same changing elements, so the reads left
the pan out. The scans now compare every changing element's box at each
step, and the reads run callbacks when any step differs.
Raises @hyperframes/core's unpacked size budget to the measured size plus
the usual 5%: main already sat 0.98% over the old budget, and this change
adds 1,232 bytes, which tipped it past the 1% tolerance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): silent motion blur reads land back on the frame as its own seek left it
After silent pose reads, the restore seek back to the frame left GSAP in a
different state than the frame's own eventful seek did:
- A step exactly on the frame (a .call() or set) was re-armed, so the next
frame fired it again. Calls fired 2-5 times per crossing, and a toggle
could end inverted.
- In renders, which keep no GSAP structure, landing back on the frame from
later skipped same-time steps' authored order, so the frame showed the
first of two sets instead of the last.
Events-off blur always read silently and had both. This PR moves events-on
roots without callback-driven motion (the Studio default) onto that path,
so both are fixed here.
The host's new holdFrame snapshots every landing mark on the root and scene
timelines before the reads, and its restore seeks back silently, redraws in
authored order where there is no structure, and puts the marks back. Each
call now fires once per frame crossing, as without blur, and same-time sets
land as authored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): a held render frame draws its scenes first, so the root lands last
holdFrame redrew the root and then the scenes, the reverse of the frame's
own seek. When a scene and the root tween one property, the frame landed
on the scene's value instead of the root's.
The hold and landing move into the GSAP adapter as holdFrameMarks, which
takes the scenes and the root separately, so the order lives in one place
a real-GSAP test can pin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli,skills): say a step's credits are its total, and keep cost out of retries
An out-of-credits refusal named what the step needs, and an agent read
that as the shortfall. It now says the number is the step's total.
After its last try, an agent told the person what was spent and what
another try would cost. The HeyGen reference now says not to.
* fix(cli): plan avatar credits from the HyperFrames price rows
An avatar's credits came from its old API price, about five times what
the plan charges, so --max-credits refused calls that fit and a top-up
asked for too much. They now use the Avatar IV photo row plus the
general avatar video row, 950 credits a minute.
* fix(cli): pin the avatar price test and keep pricing under the comment share
* fix(cli): plan avatar seconds on the Avatar IV row alone
The general avatar row is only checked at admission and billed on
seconds left after Avatar IV, so adding it to every second made a 15 s
script plan at 238 instead of 200 and a 220 limit refuse a call that fits.
* fix(cli): the credits link opens the web plan page, which is live
* test(cli): pin the production credits link too
* chore(skills): regenerate the skills manifest after merging main
* docs(cli): keep the person measure's doc comments to four lines
* docs(cli): keep the measure doc comments to four lines
* docs(cli): keep the measure comment lines to 120 characters
---------
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
Make a video, Studio app and Developers move into a Docs menu (each keeps its
own sidebar), with Free lessons, Community and the app changelog under them;
Catalog stays a tab. The top-bar copy of Ask Assistant goes (Ask stays in
search and the page's bottom bar); the logo opens the homepage; the button
reads Download.
* docs(catalog): put each item page's title, description and Docs text in the server HTML
Mintlify renders snippet components in the browser only, so everything inside
CatalogDetail (title, standfirst, the Docs tab) was missing from the HTML search
engines read: 448 catalog pages served Googlebot no body text. The generator now
writes the H1 and standfirst as page markup above the component and the Docs tab
as page markup after it, shown with that tab (docs/custom.css). The component no
longer draws its own title. Pages patched structurally only; registry drift on 14
pages is left for a regeneration of its own.
* docs(catalog): keep the title's top spacing the component gave it
* fix(docs): catalog titles and descriptions keep markdown markers literal
MDX reads text inside the static <h1> and <p> as inline markdown, so @_blue_sweater_
rendered as italic blue_sweater. mdxText now writes backslash, backtick, * _ [ ] ~ as
character references too, with a unit test; the four affected pages are re-escaped.
* feat(lint): warn when a url() in a linked stylesheet does not resolve beside it
* fix(lint): a percent-encoded url() in a linked stylesheet resolves like the browser does
* feat(cli): render --start and --end render only that span of the film as its own MP4
The span reuses the chunked render path, so frames are captured at their absolute times and sub-composition timing matches a full render.
* chore(producer): shorten the renderSpan comment
* fix(producer): a cancelled span render stops, and its real-render test runs in the integration lane
renderSpan checks the abort signal before each chunk part and before the stitch. The real-render tests move to their own file listed in the integration lane.
* chore(producer): put the renderSpan doc comment back above renderSpan
* revert: feat(skills): reverse any video (#6754)
Keep the reverse-any-video skill and its CLI billing commands out of main until
web subscriptions are live (/pricing and /account/plan return 404 today).
Re-land together with #6833 once those pages ship.
This reverts commit cb2389cf35.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(scripts): allow the #6754 revert deletions
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): reverse any video on the HyperFrames plan
Reverse rebuilds a video as an editable HyperFrames project and re-shoots its
people with HeyGen. It runs for a person signed in on HyperFrames Pro; Free is
told it is a Pro feature and given the upgrade step.
- skills: reverse-any-video; media-use is the one gate for HeyGen calls
(sign-in, credits, refusals)
- cli: media-use generate (image, video shot, avatar, voice, music) with --plan
pricing and --max-credits; measure, diff and analyze for the rebuild; the
HyperFrames plan in auth status; billing subscribe and top-up wait on the
plans or credits page
- remotion-to-hyperframes compares with `hyperframes diff` instead of its own
shell scripts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(skills): satisfy CI gates for reverse-any-video
- docs: list diff, measure, analyze and billing in "Find your command" in
docs/packages/cli.mdx, each with its own section.
- skills: bring media-use/SKILL.md under its 5120 B budget by pointing setup,
direct-recipe and treatment detail at the references that already hold it;
the sign-in, credits, --max-credits and billing rules stay in SKILL.md.
- comments: register transcript.json and REVERSE.md as project files for the
citation checker, and drop a history note from face.test.ts.
- comments: cut restating prose or move it into names so every changed file is
at or under its comment share. Allowlist the two deleted remotion scripts in
check-no-main-deletions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): terminate measure's ffprobe options with --
probeVideo passed the input path straight after its options, so a path
starting with "-" would be read as a flag; the producer's ffprobe argv
contract test failed on it. runFfmpeg's fixed flags move to a constant
so the contract no longer mistakes that ffmpeg wrapper for a probe.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): split diff's video tests into three files under CI's per-file limit
The file took ~131 s on CI against the flake gate's 120 s, and its heaviest test (four
comparisons) hit the 20 s per-test limit. The video tests now live in three files sharing
diff.test-helpers.ts, and that test is two tests of two comparisons each.
analyze reads each part once and checks the bytes it read, instead of a stat then a read
(CodeQL js/file-system-race).
* fix(skills,cli): one credits rule; ask before dropping a step that won't fit
On dev with a 100-credit limit the agent priced a re-shoot with --plan and dropped it without
asking (J5), and in J6 it told the person a price. SKILL.md's Credits rule now covers both:
--plan is for the agent, and when a step won't fit (a --plan over what is left, or a refused
call) the agent says what doesn't fit and asks before dropping or shrinking it.
setup-providers.md's older confirm-before-paying rule points to it.
--plan now runs the same checks as the real call (checkRequest), so it no longer prices a
request that would be refused. The re-shoot recipe in people.md gains the --prompt it needs.
heygen.md: a sign-in also makes images, and --plan's usd is the API charge rounded to $1/60
while its credits come from the list price.
* fix(cli): auth status drops 'balance unavailable' above the plan's credits; sign-in page title
* test(cli): keep diff's video tests inside the flake gate's limits
Under the gate's CPU contention every diff test needing face detection or
optical flow paid opencv.js's 11 MB download and WebAssembly start-up, and
every diffVideos call wrote four worst-moment pictures (about 28 ffmpeg runs)
no assertion read. Tests ran 21-28 s against the 20 s limit.
- loadOpenCvFirst() stages opencv.js in a beforeAll (from this machine's
cache, a temp copy kept between runs, else the network).
- diffVerdicts() runs diffVideos with worst: 0 for tests that only read
verdicts; the worst-moment tests keep the pictures.
- The carried-text test is split into its dirty and clean halves.
Locally with 8 CPU burners on 8 cores: slowest test 7.7 s, files 24-30 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): generate --plan refuses what the real call refuses
checkRequest now holds every pre-send check (the --out extension and
--mask without --motion joined the route body and guide checks), and
generate() runs it first, so --plan and the real call share one function.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): plans link uses from=cli and follows HYPERFRAMES_ENV; no balance notice beside the plan
- pageUrl sets from=cli, the parameter the pricing page reads to send a
CLI buyer back to the terminal.
- The plans and credits pages are on dev.hyperframes.dev with
HYPERFRAMES_ENV=dev, hyperframes.studio otherwise.
- auth status --json omits the 'HyperFrames balance is unavailable' notice
when the plan was read.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): reverse-any-video re-checks sign-in on resume; REVERSE.md holds no prices
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): split diff-people's whole-frame reuse test into three
It ran five diff comparisons in one test and took 20.2-20.6 s under the flake
gate's contention, over the 20 s limit. Each piece now runs at most two
comparisons against a shared ledger: copy and burned-in title, footage cut
whole, and cuts that only fed a generation. Under 24 CPU burners locally the
slowest of the three takes 3.0 s, where the combined test took 4.9 s under 8.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli,skills): reverse resume asks after one failed retry; out-of-credits names the step's price
- media-use generate: when HeyGen refuses for credits without saying how many, the refusal
names the CLI's own price for the call (what --plan prints) as "needs about N" and
"billing top-up --needed N", instead of "needs more".
- reverse-any-video: never repeat auth status's plan/credits line; a step an earlier session
gave up on gets one more try, then ask; a command stored in REVERSE.md is a note, rebuilt
from its recipe and checked with --plan, never run as written.
- people.md / heygen.md: a refused or failed job gets one more try, test calls included,
then stop and ask.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli,skills): send a failed HeyGen job again as new; resume asks if the limit still applies
- runJob: a key whose job failed or was refused is recorded in the config dir
(heygen-failed-keys); the next run with the same inputs sends key-1, key-2, ... so HeyGen
starts a new job. A finished or still-running job keeps its key and is picked up, so
nothing is paid twice. "fix the request it names, then run again" now holds.
- reverse-any-video: on resume after a run under a credit limit, just ask whether it still
applies, naming no costs or numbers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(cli,skills): drop what reverse-any-video no longer uses
The skill rebuilds from measure probe, cuts, frames, events and face, and checks with
diff. analyze, diff --explain and eight measure commands (trail, box, ink, rhythm,
objects, track, flow, continuity) have no caller, so they go, with their tests and docs.
Examples now use original.mp4 instead of the old .reverse/source.mp4 evidence folder,
and diff's --segments is described as a file rather than REVERSE.md.
* fix(cli): check no longer copies VP9 and AV1 videos before it starts
Chrome plays both, alpha included, and the proxy is made on demand if a browser ever
asks. Copying remove-background's VP9 cutouts took minutes on projects that have them.
* fix(cli,skills): keep paid retries, credit limits and lip sync honest
Retries keep their key after 401, 403, 408, 409, 425 and 429, which say
nothing about the job, and failed keys are scoped to the API and payer.
Credits come from the plan's video rows instead of list dollars, and
reference pictures past the first two count toward --max-credits.
Lip sync reads sound on its own timestamps, so a delayed track fails
the gate. Look and energy call two near-zero values the same.
--plan refuses a missing input as the real call does. The skills teach
the direct sign-in, its credential path and the billing commands.
* chore(cli): keep the new pricing and energy files under the comment share
* fix(cli): scope failed HeyGen keys to the signed-in account
Failed keys were scoped by the local user block, which is absent for a
host's token and display-only otherwise, so two accounts could share a
scope and one could start a second paid job.
The scope now comes from the account /v3/users/me reports for the token
in use. When that cannot be told, no failure is recorded and the key
stays the same.
* fix(cli): fix a job's sender when it starts
Failure scope was looked up again after polling, so a sign-in during a
job could record one account's failure under another and rotate its
finished key. The credential is now read once per job: it signs the POST
and owns any failure, even if someone else signs in meanwhile.
Prefer username to email, since /v3/users/me returns no user id.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: James <james.russo@heygen.com>
* feat(cli): use dedicated HyperFrames OAuth clients
* docs(cli): shorten credential store contract comment
* fix(cli): respect Windows modes in OAuth media test
* fix(cli): keep media workflows on HyperFrames OAuth
* test(cli): scope billing status assertions to wallet rows
* test(cli): align telemetry fixtures and CI auth inputs
* fix(cli): a project .env only sets the CLI's own settings
The .env in the working directory could set any environment variable for every
command except capture. It now sets only the keys the CLI reads from a project:
the HeyGen, ElevenLabs and Figma credentials and the vision keys, models, Vertex
settings and timeout. Other keys are ignored, and one stderr line names them.
* test(cli): the startup test and comment budget follow the single project .env key list
* fix(cli): a project .env can still turn telemetry off
* docs(telemetry): say signed-in usage is linked to your account, not anonymous
After hyperframes auth login the CLI sends a PostHog $identify with the
account email (username as fallback) and merges the install's earlier
anonymous usage; media-use does the same when it finds a signed-in
account. Several places still called all telemetry anonymous. Say it is
anonymous until you sign in, in the CLI reference (media-use, --skill,
feedback, events, telemetry), the hyperframes-cli skill references, and
the CLI help and notice strings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(telemetry): identify signed-in CLI users by their HyperFrames user id
The CLI and media-use identified a signed-in install by account email, so
CLI usage landed on a different PostHog person than the same account in
the HyperFrames web app, which keys on toUUID(username). Identify by that
same sha256-derived id instead, and $set email plus heygen_username on the
person. heygen_username is the cross-project join column for the Snowflake
event table (HeyGen's own project stores it as `name`). An account with no
username still falls back to the lowercased email.
Notices and docs now say usage is keyed on an id derived from the username
with email and username stored on it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(telemetry): trim comments to the package's comment share
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(telemetry): put heygen_username on every event while signed in
The Snowflake event sync may not carry PostHog person properties, so a
$set alone does not let an event row join across projects. The CLI now
adds the signed-in HeyGen username (raw, from ~/.heygen/credentials) to
every event and drops it once the credentials hold no user; media-use
does the same on each event it sends.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci: count auth/paths.ts as an edit-accuracy bench input
Telemetry now imports it to read the signed-in username.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(telemetry): stat and read the credentials through one file handle
CodeQL flagged the stat-then-read as a file system race.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(telemetry): key signed-in events on the account id; fix stale anonymity copy
Genesis review on #6703:
- telemetry --help listed 'Any personally identifiable information' as
never collected, and media-use's meta.md still said anonymous.
- Installs already identified by email under an older CLI never merge
into the new person, so send signed-in events with distinct_id = the
account id (back to the install id after logout), in the CLI and in
media-use, and stop claiming earlier usage always merges.
- A user block with no credential no longer counts as signed in.
- A UUID-shaped username passes through unhashed, as on the web.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An inert <hf-color-group> holds a lane's data-color-grading; <video>, <img>
and <canvas> join with data-color-group. A member's own grading overrides
its group's. The runtime discovers members, regrades them when the group's
grading or a member's data-color-group changes, and ungrades them when the
group element or its grading goes, drawing only members on screen.
Render keeps group-graded video at full size like directly graded video,
and lint accepts (and still validates) grading on hf-color-group.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(audio): a sound is voice only with a voice-range pitch, judged on the whole clip
Two ways the kind guess called a non-voice sound voice:
- A short hit, click or whoosh can have a speech-shaped envelope. classify() now also
measures the share of 40 ms frames with a 70-500 Hz fundamental (YIN, moved from the
CLI into core) and calls a sound voice only when that share reaches 0.15. Speech
measured 0.22-0.43; impacts, clicks and whooshes 0.
- A range query judged a clip's kind on the slice it covered, so 1.7 s of a music bed
read as voice. A film clip's kind is now judged on all of the clip that plays.
Even-paced speech (a 64 s TTS take) moves less at syllable rate and was called music;
with no beat and a voiced share of 0.25 or more it is now voice.
* chore(audio): keep each touched file's comment share at or under its base
The Comments check failed on run.ts, targets.ts and audioAnalysis.ts because the
PR added comment lines to files whose share may only go down. Drops the added
doc comments; no code change.
— Jerrai
* fix(audio): preserve clip evidence and stereo pitch during analysis
* fix(audio): bound and cache whole-clip kind analysis
* fix(audio): retain native-rate kind evidence when available
---------
Co-authored-by: Jerrai <jerrai@heygen.com>
* fix(cli): every --json failure answers with one envelope, and --json only where supported
Under --json, failures came back as a bare string (`"error": "Command failed"`
from lint), a command-specific shape, or nothing at all on stdout (info,
compositions, render, snapshot, cloud get). Now every failure is
`{"ok": false, "error": {"code", "message", "hint"?}}`:
- jsonFailure() in utils/jsonError.ts builds it. The commands that printed
`{ok: false, error: "<text>"}` use it, keeping their extra fields (lint's
findings, validate's errors). A refused timeline edit now writes it to stdout
instead of stderr, and keeps its `reason` and `fix` fields.
- When a --json run fails without writing anything to stdout, cli.ts writes the
envelope itself: usage errors carry their own code (unknown_flag,
unknown_command, json_unsupported, confirmation_required), anything else is
runtime_error with the message the command printed on stderr.
--json is no longer accepted by every command. A command supports it by
declaring a `json` arg; the others reject it with exit 2 and the code
json_unsupported, instead of accepting the flag and printing prose.
The contract test now checks every command's --json path, and packages/cli
gets an AGENTS.md with the rules: stdout is data, stderr is messages, never
prompt without a TTY, exit 2 for usage errors, --json always returns an
envelope.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): put the real failure in the --json envelope, never the generic text or a notice
lint --json and check --json in a directory with no project answered
"message": "Command failed". The project check reports the problem, then
throws a presented "Command failed" with the real error as its cause, and the
command's own catch serialized that generic text. failureMessage() now returns
the cause, or else what the command reported through errorBox, and every
command catch that writes a --json failure uses it (lint, check, validate,
layout, snapshot, compare, grade-compare, media-treatment, tts,
remove-background, add, skills, lambda policies). compare, grade-compare and
add share failCaught() for that catch.
The boundary envelope follows the same order: the error's own message unless
it is the generic one, then its cause, then the errorBox title and hint, and
only then the stderr text. Notices (the telemetry disclosure, deprecation
lines, diag progress) are no longer captured, so validate, layout and inspect
no longer put "is deprecated" in the error message.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): render --json keeps human output off stdout so a failure answers with the envelope
Under --json a single render printed its header, lint findings and progress on
stdout, so a later failure such as `render --variables '{bad' --json` ended
with no JSON envelope. --json now silences that output the way --batch --json
already did, and --variables is parsed before the browser check so a bad value
fails without a Chrome download.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): a --strict lint abort says why, even under --json
Under --json the render is quiet, so a --strict lint abort printed nothing and
the envelope said only "Command failed". The abort is now an error box on
stderr in every mode, naming the error and warning counts and the first
finding, and the --json envelope carries the same message. Lint now runs
before the browser check, so the abort never waits on a Chrome download.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): time the render setup cancel from the wrapper's exit
The 1 s budget started when the command did. With lint now running before the
browser check, its subprocess (about 250 ms here) sat inside that budget, and
the test failed once under load. The budget now starts when the wrapper exits,
which is the latency the test is about.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(inspector): live queries see every composition, wherever the playhead is
The Studio preview parks scenes away from the playhead, so a live inspect, timeline or
element-map query answered missing for every element in a parked composition. Each live
query now holds every scene mounted under its own holder, then hands the scene window back.
__hfHoldScenes keeps holds per holder, so the inspector letting go never drops a scene
Studio holds.
* fix(inspector): isolate concurrent live query scene holds
* fix(cli): add help [cmd], suggest the closest command or flag, and quiet the nags
`hyperframes help` printed "Unknown command help" with raw ANSI codes and no
suggestion. It is now an alias: `help [cmd...]` is `[cmd...] --help`.
An unknown command (at the root or inside a group like `cloud`) prints
"Unknown command: rendr", "Did you mean 'hyperframes render'?" and the group's
usage, all on stderr, and exits 2. An unknown flag suggests the flag it most
likely means ("Did you mean --output?"). Usage text printed for a usage error
now goes to stderr instead of stdout, and citty's colors are stripped from
help whenever this CLI's own colors are off.
No update, skills or app-history notice is appended to a usage error. The
skills notice shows at most once a day per project (timestamps in
~/.hyperframes/skills-notice.json) and, in an unattended run, only when a core
skill is missing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): reject an unknown top-level command without loading every command
citty looks for an alias on every registered command before it reports an
unknown name, which loads all of them. From a source checkout without the
studio packages built, that meant `hyperframes rendr` failed in preview's
build check with exit 1 instead of suggesting `render`. The name is now
checked against the registry first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): help with a mistyped subcommand reports it like the command itself
`hyperframes help cloud rendr` printed cloud's help and exited 0, while
`hyperframes cloud rendr` exits 2 with a suggestion. Both now exit 2 with the
suggestion. A group that also takes positional arguments (timeline takes a
project directory) still shows its help for a word it does not know.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): never prompt or auto-install when nobody is at the terminal
publish and upgrade called the confirmation prompt with no terminal check.
With stdin from /dev/null, publish exited 13; with an open pipe it hung.
Unattended and without --yes, both now exit 2 before doing any work, and
stderr names --yes (upgrade also names --check).
All prompts and spinners now go through ui/prompts.ts, the only module allowed
to import @clack/prompts (a lint rule and a contract test enforce it). Its
confirm, select, text and password refuse to prompt in an unattended run, and
its spinner prints only the final line, on stderr. init, feedback, catalog and
cloud delete decide whether to ask with isUnattended() instead of a TTY check
of their own.
The background auto-install no longer starts in an unattended run, and the
update notice ("Run: npm install -g ...") is not shown while an install for
that version is pending. CI now counts as set for any value but "false" or "0".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): neutralize agent detection in the catalog offer tests through isAgentDriven
Attendance now reads isAgentDriven(), so that is what the catalog tests stub to
stay independent of the agent running the suite.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(cli): define every command through one factory and test them all as an agent
Every command now comes from defineCommand in utils/defineCommand.ts. A lint
rule bans importing defineCommand from citty anywhere else, and a new contract
test walks the command registry in cli.ts, checks each command was built by the
factory, and runs it the way a coding agent does: no terminal, stdin closed,
CI=1, CLAUDECODE=1 and a home folder of its own.
Usage errors now exit 2: unknown flags, extra positionals, and citty's own
parse errors (missing argument, bad value, unknown or missing subcommand).
An unknown flag names the help command instead of printing nothing else.
isUnattended() in utils/attendedTerminal.ts is the one answer to "can a person
respond here". It is true without a TTY on stdin or stdout, under CI (any value
except "false" or "0"), when the existing agent detection finds an agent, or
with HYPERFRAMES_AGENT=1. Color is off when it is true.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): spawn preview in the agent-mode test only when the studio packages are built
From source, preview refuses to load until the studio workspace packages are
built, and the CLI test jobs do not build them. preview is still checked for
being defined with the shared factory.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): count only agent-run shells as unattended, and exit 2 for missing arguments
isUnattended() used the telemetry fingerprint detectAgentRuntime(), which also
matches a person typing in Cursor's or Windsurf's terminal (TERM_PROGRAM) and
any Replit shell (REPL_ID, REPLIT_USER). It now uses isAgentDriven(), which
keeps only markers an agent sets on the shells it runs: CURSOR_AGENT counts,
TERM_PROGRAM=cursor, TERM_PROGRAM=windsurf and the Replit variables do not.
The hand-written argument checks in lambda, cloudrun, lambda policies,
capture, remove-background, init (--resolution), tts (--lang) and cloud render
(--idempotency-key, --dry-run) exited 1. They are usage errors and now exit 2.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>