Replace custom git clone + copy logic with `npx skills add` from the
vercel-labs/skills ecosystem. This is the standard used by GSAP,
Remotion, and other skill providers.
- Primary path: `npx skills add heygen-com/hyperframes` and
`npx skills add greensock/gsap-skills` with `-g -y -a <agent>` flags
- Git clone + copy kept as fallback if npx is unavailable
- Added `skillsAgent` field to targets mapping CLI flags to the correct
agent names (e.g., gemini → gemini-cli)
- Removes ~80 lines of custom git/copy infrastructure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New HyperFrames projects created via `hyperframes init` now include:
- CLAUDE.md + AGENTS.md — teaches AI agents about skills, commands,
project structure, and framework rules (class="clip", timeline
registration, determinism). Agents know to invoke /compose-video
before writing compositions.
- .claude/skills/{compose-video,captions} — project-level skills for
immediate availability in the current agent session (global skills
require a session restart to discover).
- Updated next-steps output with `hyperframes docs <topic>` and a
link to hyperframes.heygen.com.
- Updated README with "AI Agent Skills" section documenting
`npx hyperframes skills` and `npx skills add` install paths.
- Repo-level CLAUDE.md for framework contributors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What
Enhanced timeline collection to discover GSAP-animated scene elements and persistent overlays through runtime introspection.
## Why
The existing timeline collection only captured elements with explicit timing attributes (`data-start`, `data-track-index`) or media elements, missing scene elements that are animated purely through GSAP tweens and persistent overlay elements that should appear for the full composition duration.
## How
Added two new discovery mechanisms to `collectRuntimeTimelinePayload`:
1. **GSAP Timeline Introspection**: Walks the master timeline's tweens using `getChildren()` to find animated elements, calculates absolute time ranges by traversing parent timelines, and bubbles child tween ranges up to their nearest scene-level ancestors (direct children of root with IDs).
2. **Persistent Overlay Detection**: Identifies direct children of the root composition that weren't captured by DOM queries or GSAP introspection, treating them as full-duration overlay elements while filtering out non-visual elements (script, style, meta tags) and hidden elements.
Both mechanisms respect existing track assignments and create new tracks when needed to avoid conflicts.
## Test plan
- [x] Unit tests added/updated
- [x] Manual testing performed
- [ ] Documentation updated (if applicable)
Added comprehensive test coverage for:
- GSAP-animated scene element discovery via timeline introspection
- Time range bubbling from child elements to scene ancestors
- Persistent overlay inclusion as full-duration clips
- Proper filtering of non-visual elements (script/style tags)


The 3-workflow chain (release.yml → release-tag.yml → publish.yml) was
broken by design: tags created by GITHUB_TOKEN don't trigger other
workflows, so merging a release PR never actually published.
Consolidate into a single publish.yml that triggers on both tag push
and release PR merge. Delete the redundant prepare-release and
tag-release workflows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What
Updated the GitHub Actions regression workflow to monitor specific package directories instead of the entire packages folder.
## Why
This change provides more granular control over when regression tests are triggered, allowing the workflow to run only when changes are made to the core, producer, or engine packages rather than any package in the repository.
## How
Modified the path filters in the regression workflow to explicitly list the three critical package directories (`packages/core/**`, `packages/producer/**`, `packages/engine/**`) instead of using the broad `packages/**` pattern.
## Test plan
How was this tested?
- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated (if applicable)
The blank template was a bare `<div>` fragment without `<!DOCTYPE>`,
`<head>`, or `<body>`. This caused:
- Blank preview (bundler/runtime can't initialize from a fragment)
- "Failed to run lint" (parsing errors on the malformed document)
All other templates (swiss-grid, vignelli, warm-grain, play-mode) are
proper HTML documents — blank was the only outlier.
Also improves lint error reporting to show the actual error message
instead of the generic "Failed to run lint."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SIGINT handlers that were meant to do graceful cleanup (close server,
remove symlinks) override Node's default exit behavior. If the cleanup
hangs (e.g. server.close() blocked by open connections), the process is
stuck and Ctrl+C does nothing.
Fix: don't intercept SIGINT at all. Node's default behavior exits the
process immediately on Ctrl+C. The OS reclaims the port and file handles.
Use process.on("exit") for best-effort symlink cleanup instead.
Also fixes running the CLI via `tsx` in dev mode — __CLI_VERSION__ is a
tsup build-time define that crashes at runtime without a fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- Fix silent failure when `hyperframes dev` port (default 3002) is already in use (e.g., by Cursor IDE)
- Replace TOCTOU-prone `isPortAvailable()` probe with `serveWithPortFallback()` that binds the real server directly
- Auto-increment to next available port with a visible yellow warning, or show a clear error when all ports (range of 10) are exhausted
## What changed
The old approach used a throwaway `net.createServer()` to test port availability, closed it, then opened the real Hono server — a classic TOCTOU race. The fallback also silently returned the original port when all 10 were taken.
Now we use `createAdaptorServer()` (creates the Hono HTTP server without binding) and manually call `.listen(port)`, catching `EADDRINUSE` to try the next port. This eliminates the race entirely.
**Before:** `hyperframes dev` says "Studio running at http://localhost:3002" even when port 3002 belongs to another process.
**After:**
- Port available: works as before
- Port taken: `Port 3002 is in use, using 3003 instead` (yellow warning)
- All ports taken: `Ports 3002–3011 are all in use. Use --port to specify a different port.` (error + exit)
## Testing
- Verified TypeScript compiles cleanly (`tsc --noEmit`)
- All pre-commit hooks (lint, format, commitlint) pass
- Manual test: run another server on 3002, then `hyperframes dev` → confirms auto-increment message appears
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The embedded dev server (hyperframes dev) silently reported success even
when the requested port was already in use (e.g., by Cursor IDE on 3002).
This happened because the old isPortAvailable() probe had a TOCTOU race
and findAvailablePort() silently fell back to the original port on exhaustion.
Replace with serveWithPortFallback() that binds the real Hono server
directly via createAdaptorServer + manual listen(), retrying on EADDRINUSE.
Shows a yellow warning when auto-incrementing and a clear error when all
ports are exhausted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use static import for copyFileSync (was unnecessary dynamic import)
- Shallow-copy config before mutating forceScreenshot (prevents
caller-provided config from being permanently modified)
- Consolidate isWebm/isWebmRender/outputFormat into single early
declaration in renderOrchestrator
- Fix debug output extension for WebM (was hardcoded .mp4)
- Log unexpected audio extraction errors instead of silently swallowing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Changes
- Add optional `entryFile` parameter to render API endpoints (`/v1/render` and `/v1/render-stream`)
- Enable rendering individual sub-compositions by extracting them from index.html context when the entry file is a `<template>` wrapper
- Change base64 audio/video linting from detecting "fabricated" media to prohibiting all inline base64 media
- Add manifest path resolution for bundled producer deployments
## API Changes
- `RenderConfig` — new optional `entryFile` field for specifying HTML file to render
- `server.ts` — parses `entryFile` from request body, validates file exists in project directory
- `executeRenderJob` — uses `entryFile` instead of hardcoded `"index.html"`
## Template Extraction
- `extractStandaloneEntryFromIndex` — extracts sub-composition hosts from index.html and creates standalone render context
- Handles `<template>` entry files by finding matching `data-composition-src` in index.html and isolating that host
- Resets `data-start` to 0 for standalone rendering
## Linting Updates
- Change rule #3.7 from detecting "fabricated" base64 media to prohibiting all inline base64 audio/video
- Lower detection threshold from 100+ to 20+ base64 characters
- All base64 media now triggers error severity with clearer messaging about file size bloat
## Usage
```json
POST /v1/render-stream
{ "projectDir": "/path/to/project", "entryFile": "compositions/intro.html" }
```
Omit `entryFile` for default behavior (renders `index.html`).
- Engine: document getEncoderPreset() for MP4/WebM, VP9 alpha flags,
Opus audio in mux step
- Producer: document format field in RenderConfig, WebM usage example,
pipeline steps updated for WebM
- CLI: add render command examples in --help (including WebM overlay)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the --format webm flag, VP9 alpha output, overlay workflow
with FFmpeg, and transparent background requirement for compositions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The VP9 alpha encoding works correctly — verified by overlaying on a
green background with explicit VP9 decoder. FFmpeg's default decoder
doesn't expose VP9 alpha through ffprobe, but browsers and VP9-aware
decoders read it correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Force screenshot capture mode for WebM (beginFrame doesn't support
alpha channel in chrome-headless-shell)
- Set Emulation.setDefaultBackgroundColorOverride once during session
creation (matching experiment-framework's approach)
- Fix acquireBrowser to pass executable path in screenshot mode on
Linux (was setting undefined, causing puppeteer-core to fail)
- Add Page.captureScreenshot params: fromSurface, captureBeyondViewport,
optimizeForSpeed (matching experiment-framework)
- Fix regression harness extractMonoPcm16 for videos without audio
- Add getEncoderPreset unit tests
- Add webm-transparency regression test with golden baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New regression test `webm-transparency`: minimal transparent composition
rendered to WebM, validates VP9 codec and visual quality at 100
checkpoints against golden baseline
- Extend regression harness: `renderConfig.format` field ("mp4" | "webm"),
format-aware output paths and snapshot filenames
- Fix `extractMonoPcm16` to gracefully handle videos without audio
streams (WebM without audio was throwing instead of returning empty)
- Unit tests for `getEncoderPreset()`: VP9/yuva420p for WebM,
h264/yuv420p for MP4, preset mapping, quality preservation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support rendering compositions with transparent backgrounds via
`--format webm`. VP9+alpha is the standard format for overlayable
video (captions, lower thirds, overlays).
Changes by layer:
- CLI: `--format mp4|webm` flag on render command
- Producer: threads format through RenderConfig, switches to PNG
capture and VP9 encoding when webm
- Engine: getEncoderPreset() returns VP9 config with yuva420p;
transparent page background via CDP when capturing PNG;
mux uses Opus audio for WebM; VP9 flags from production:
-row-mt 1, -auto-alt-ref 0, alpha_mode=1 metadata
- Frame capture: Emulation.setDefaultBackgroundColorOverride a=0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What
When a composition has an empty GSAP timeline (no animations), `window.__hf.duration` was always 0, causing `hyperframes render` to time out after 45 seconds waiting for `duration > 0`.
## Why
The HF bridge script reads duration from `window.__player.getDuration()`, which returns the GSAP timeline duration. An empty timeline has duration 0. The render engine waits for `window.__hf.duration > 0` to confirm the runtime is ready — so compositions with no animations would always deadlock.
This hits the `--template blank` scaffold immediately: it generates an empty GSAP timeline and relies solely on `data-duration="10"` for composition timing.
## How
One-line change to the bridge script: when `getDuration()` returns 0, fall back to reading `data-duration` from the root `[data-composition-id]` element. This is the same value the static compiler already extracted — so we get a correct duration without any extra browser round-trips.
```js
get duration() {
var d = p.getDuration();
return d > 0 ? d : getDeclaredDuration(); // reads data-duration from root element
}
```
## Test plan
- [x] `hyperframes init my-video --template blank && hyperframes render my-video` completes successfully
- [x] Output: `output.mp4` — 10s, 1920×1080, 30fps ✓
- [x] Compositions with actual GSAP animations unaffected (fallback only triggers when timeline duration is 0)
- [x] Build passes, lint/format clean
- Document the three dev server modes (embedded/local studio/monorepo)
- Add --port flag to dev command
- Document _meta envelope on all --json commands
- Document upgrade --check --json for agent consumption
- Document passive update notices and HYPERFRAMES_NO_UPDATE_CHECK
- Update doctor output example with Version check row
- Fix README default port from 3000 to 3002
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New `updateCheck.ts` utility: cached npm registry check (24h TTL),
sync `getUpdateMeta()` for _meta envelope, `printUpdateNotice()` for
passive stderr banner
- `upgrade --check --json`: machine-readable version check for AI agents
Returns { current, latest, updateAvailable }
- `_meta` envelope on all --json commands (info, lint, benchmark,
compositions): includes version, latestVersion, updateAvailable
- `doctor` shows version check as first row
- Passive update notice on stderr after command completes (skipped in
CI, non-TTY, --json, --quiet)
- Background check fires on startup (non-blocking, populates cache)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- version.ts: replace hardcoded "0.1.0" with __CLI_VERSION__ injected by
tsup at build time from package.json — fixes version mismatch where
`hyperframes --version` reported 0.1.0 while package was 0.1.4
- tsup.config.ts: add define.__CLI_VERSION__ using package.json version
- render.ts: renderDocker error handler showed "Try --docker" even when
already using --docker — changed to "Check Docker is running: docker info"
- dev.ts: add missing --port arg to embedded mode; findAvailablePort now
starts from the user-supplied port instead of hardcoded 3002
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add nullish coalescing for regex match groups that TypeScript flags as
possibly undefined.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up the "Export MP4" button in the studio UI. The render runs
async in the same process using @hyperframes/producer's executeRenderJob,
with SSE progress streaming and MP4 download on completion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When installed via npx, `hyperframes dev` now starts a standalone
Hono HTTP server that serves the pre-built studio SPA and implements
the project API (file listing, read/write, preview bundling,
sub-composition rendering, runtime serving, SSE file watching).
Three modes are auto-detected:
1. Monorepo dev (running from .ts source) → spawn Vite (existing)
2. Local @hyperframes/studio installed → spawn Vite via package (new)
3. Default → embedded Hono server (new, zero extra deps needed)
Also patches the studio SPA to use EventSource SSE fallback when
Vite HMR is unavailable (production/embedded builds).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- copy data-start from the host sub-composition node to the inlined inner composition root
- preserve the correct runtime offset lookup for nested compositions after producer compilation
- avoid nested GSAP timelines snapping to their end state when the host starts later than t=0
Reverting the package rename — Vance needs @hyperframes/cli for local
dev workflow. Instead, rewrite the name to "hyperframes" in the publish
workflow just before npm publish, so the monorepo name stays intact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CLI is published to npm as unscoped `hyperframes` but the
package.json had `@hyperframes/cli`, causing ENEEDAUTH on publish
(wrong scope for the npm token).
Also replace per-step continue-on-error with a single publish script
that skips already-published versions and fails on real errors, making
re-runs safe.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The generated font data is a pure function of the generator script +
@fontsource package versions — no reason to store 566KB of base64
blobs in git. Generate it during the Docker test image build instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The regression CI runs the producer source directly via tsx (not the
bundled CLI), so it needs fontData.generated.ts to exist at import
time. Remove from .gitignore and commit the generated file.
Mark as linguist-generated in .gitattributes so GitHub collapses it
in PR diffs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous approach called loadHyperframeRuntimeSource() to regenerate
the runtime IIFE, but its output lacked the trailing newline present in
the pre-built artifact, causing a SHA256 checksum mismatch with the
manifest. Copy the pre-built files from core/dist directly instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Empty src="" on <audio> elements causes the browser to fetch the
current page URL as the audio source, producing 404 errors in the
console during rendering. These are placeholder sound effect slots
whose src should be set dynamically when audio is configured.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CLI's ensureBrowser() finds Chrome via env var, cached download,
or system paths (including /Applications/Google Chrome.app on macOS).
But the result was discarded — the engine's acquireBrowser() only
checked the puppeteer cache for chrome-headless-shell, passing
undefined executablePath on macOS without headless-shell installed.
Bridge the two by setting PRODUCER_HEADLESS_SHELL_PATH from the CLI's
resolved browser path before creating the render job, so the engine's
resolveConfig() picks it up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hyperframeRuntimeLoader uses import.meta.url to resolve
hyperframe.manifest.json as a sibling file. When bundled into the CLI
via tsup, import.meta.url points to dist/cli.js, so the sibling lookup
checks dist/hyperframe.manifest.json — which wasn't being shipped.
Copy the manifest and the canonical-named runtime IIFE into dist/ during
the build-runtime step so the loader's sibling-path resolution works in
published npm packages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>