Hypagon Gateway

Media download integration

Status: complete. status, info, preview, transcript, download, jobs, cancel and list work end to end; the downloader updates itself; downloads can use the browser session; ffmpeg can be detected, configured or installed; and the save folder is configurable. Cortex indexing of transcripts is the obvious next step.

Read this before changing anything under src/main/media/.

Where it lives

Media is a tool group in hb-browser, not a third MCP server. Execution happens in the main process behind /media/* bridge routes, for four reasons:

If the surface grows into a full media pipeline, hb-media can be added later as a thin stdio faΓ§ade over the same routes, exactly like hb-rest.

The JavaScript runtime is Electron

Full YouTube support needs an external JavaScript runtime. The app does not ship Deno; it hands the downloader its own binary in Node mode:

env.ELECTRON_RUN_AS_NODE = '1';
args.push('--js-runtimes', `node:${process.execPath}`);

process.execPath reports v24.21.0 on Electron 44.7.2 when that variable is set, which satisfies the Node β‰₯ 22 requirement. Verified end to end: the same video returns 25 formats with the runtime and 24 without it, and without it the downloader warns that some formats may be missing.

The runtime helper returns no runtime rather than guessing when it cannot prove Node β‰₯ 22. The app then behaves like a plain downloader install.

Binaries

scripts/media-binaries.json pins one downloader release and a SHA-256 per target. scripts/fetch-media.mjs downloads the asset, verifies the hash, makes it executable, runs --version, and fails the build on any problem. It never writes a placeholder: a build that cannot fetch the binary is not releaseable.

bin/media/<platform>-<arch>/<downloader>        seed, shipped, asar-unpacked
<userData>/media/bin/<downloader>               working copy, writable, updated at runtime

The seed is read-only on macOS and on AppImage mounts, so the runnable copy lives in userData. It is only refreshed from the seed when the seed is newer (an app update shipped a newer binary), so a runtime update is never rolled back by the next launch.

npm run build fetches the host binary; npm run fetch:media -- --all fetches every target for a cross-platform build.

Tool surface

Action State Behaviour
status shipped Binary version/source, runtime kind, capability flags, job counts
info shipped Metadata without downloading: title, duration, formats, subtitle languages, warnings
download shipped Enqueue a job and return its id immediately
jobs shipped Progress, bytes, speed, ETA, stage and output files for every job
cancel shipped Kill the process tree and remove the job directory
list shipped Completed downloads with files and sizes
transcript shipped Captions as text (paged), without downloading
preview shipped Metadata + transcript excerpt + poster image, without downloading

Downloads never block a tool call: the bridge timeout is 30 s, a download is minutes. Jobs are polled, the same way record_start returns when capture is live.

Jobs

Session cookies

Off by default. A site that needs a signed-in session returns LOGIN_REQUIRED, and the app shows a desktop notification; a human then enables Settings β†’ Media β†’ "Always use the browser session" and retries. Nothing an agent calls can turn it on.

Previewing without downloading

media preview and media transcript run the downloader with --skip-download: they fetch the caption track and the poster image, never the media.

Security

Updates

The downloader is the one binary that must keep up with the sites it reads, so it updates at runtime:

Output folder

Settings β†’ Media has a save-folder picker. Empty means <userData>/media; any absolute path is accepted. Every job still gets its own <jobId> subfolder under it, so cancelling a job removes only its own partial output, never another download. A folder that cannot be created fails the job with OUTPUT_DIR instead of leaving it queued.

ffmpeg

Needed for merging separate video and audio streams, for transcoding, and for audio extraction to an arbitrary codec. Without it the downloader still fetches a single-file MP4 and an audio-only stream; media status reports merge/transcode accordingly.

Three modes, saved in Settings β†’ Media:

Mode Resolution order
auto (default) configured path β†’ managed copy β†’ ffmpeg on PATH
path the configured binary only; no silent fallback
managed the copy downloaded into <userData>/media/bin/ffmpeg only

Decision log