Adom Desktop
Public Made by Adomby adom
Installs the adom-desktop CLI and its Claude skills INTO A CLOUD CONTAINER so an AI assistant there can understand and drive Adom Desktop over the relay. This is the container side, NOT the app itself. The Adom Desktop app is the signed Windows installer under Download below: run THAT on the PC you want the AI to control. Rule of thumb: pkg install here (in your Linux container), Download there (on your Windows PC). Once installed, the AI can drive file transfer, screenshots, notifications, KiCa
Hydrogen Desktop verbs
Build, ship, screenshot, and drive the embedded Hydrogen Desktop.
Part of the Adom Desktop verb reference. Invoke as adom-desktop <verb> '<json>'.
Note: v1.8.15+: hd_* verbs proxy to Hydrogen Desktop's local API on 127.0.0.1:9001. HD is a sibling Tauri v2 app that runs alongside adom-desktop on the user's machine. These are built-in proxy handlers (NOT a separate bridge process) — when HD isn't running, hd_status returns {ok:false, error:'HD not running'} cleanly instead of crashing.
hd_api
Generic HTTP pass-through to HD's control API. The canonical way to call ANY HD endpoint from a script/AI without shelling out to curl (which the relay mangles). Auto-discovers HD's control port from %APPDATA%/hydrogen-desktop/ports.json (key 'control'); falls back to 47084. Public-build denylist refuses destructive paths (e.g. /wsl/unregister, /setup/virgin-reset) — use the hd-dev internal build for those.
Args:
body— optional object — JSON body for POST/PUT/PATCH. Passed through verbatim (no escaping).method— optional string (default 'GET') — 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'path— required string starting with '/' — e.g. '/setup/run-step', '/wsl/status', '/claude/start-auth'port— optional int — override discovered port; for testing
Returns: {success:bool, ok:bool, status:int, body:any, method:str, path:str, port:int} — body is the parsed JSON HD returned (or a string if non-JSON)
Example: adom-desktop hd_api '{"method":"POST","path":"/setup/run-step","body":{"id":"welcome"}}'
hd_browser_profiles
Enumerate browsers + profiles HD detects on the user's machine (GET /browser-profiles). Use the returned profileDir values when calling hd_open_url.
Returns: HD's /browser-profiles response — typically {browsers:[{name, profiles:[{dir, name, gaia?}, ...]}]}
hd_build
Full HD rebuild (async): git pull → pnpm build → cargo build (debug). Returns immediately with {pid, logPath}; the build runs detached so the relay's 30s HTTP timeout doesn't kill it. Each step appends to %TEMP%\hd-build.log (overwritten at start); the script writes BUILD_OK or BUILD_FAILED: as its final line so callers can detect completion without scraping tool-specific output. v1.8.16+.
Args:
lingerSecs— optional int (default 30 when show=true, 0 when show=false) — v1.8.23+. After the build's final BUILD_OK/BUILD_FAILED line, the visible window stays open for this many seconds before auto-closing. 30s default lets the user read the error; pass 0 to close immediately; pass a larger number for more reading time. Capped at 3600s. Ignored entirely when show=false (no window to linger).show— optional bool (default false) — true opens a visible console window the user can watch the build scroll in; false runs hidden
Returns: {ok, pid, logPath, flavor:'full', show, lingerSecs, _hint}
Note: DO NOT call hd_launch until hd_build_status shows succeeded=true. Poll hd_build_status or stream output with hd_build_tail {offset:N}. Build PID is written to %TEMP%\hd-build.pid by the PowerShell script itself so it's authoritative regardless of the wrapper process tree. v1.8.23+: visible build windows now auto-close after lingerSecs (default 30) so build-heavy sessions don't accumulate orphan consoles.
hd_build_frontend
Frontend-only HD build (async): pnpm build from the repo root. Same log/PID/sentinel conventions as hd_build. Use when only Svelte/TypeScript changed and the Rust binary is unchanged — much faster than a full build.
Args:
lingerSecs— optional int (default 30 when show=true) — see hd_build for details. v1.8.23+.show— optional bool (default false)
Returns: On dispatch success: {ok, pid, logPath, flavor:'frontend', show, lingerSecs, builtSha, exeMtimeBefore, exeSizeBefore, _hint}. On lock contention (v1.8.57+): {ok:false, refused:true, failedAt:'lock', verb, holderPid, holderVerb, sha, elapsedSec, narrative, next} — another hd_build*/hd_ship is already running. NO QUEUE: just refused immediately. The lock auto-releases when the holder's PowerShell PID dies.
Note: Skip when the Rust src-tauri/ side changed — those need hd_build or hd_build_rust to rebuild the binary. v1.8.57+ adds a global build lock — two concurrent hd_build_*/hd_ship calls cannot race.
hd_build_log
Return the FULL build log from %TEMP%\hd-build.log. Use this after hd_build_status reports failed to read the complete output and find the error line.
Returns: {ok, log:'', lines, logPath, _hint}
hd_build_rust
Rust-only HD build (async): cargo build from src-tauri/. Same log/PID/sentinel conventions as hd_build. Use when only Rust code changed (no frontend changes). v1.8.52+: dispatch response also carries builtSha + exeMtimeBefore + exeSizeBefore (captured at spawn time into %TEMP%\hd-build-meta.json) so the subsequent hd_build_status / hd_launch don't need a separate git rev-parse + stat call.
Args:
lingerSecs— optional int (default 30 when show=true) — see hd_build for details. v1.8.23+.show— optional bool (default false)
Returns: {ok, pid, logPath, flavor:'rust', show, lingerSecs, builtSha, exeMtimeBefore, exeSizeBefore, _hint}
Note: Builds the debug binary at target/debug/hydrogen-desktop.exe. The frontend's build/ output is NOT regenerated — re-use whatever was last built by hd_build / hd_build_frontend.
hd_build_status
Synchronous build-state probe. v1.8.52+ enriched: returns compiledCrates (parsed from 'Compiling X' lines so you KNOW your crate recompiled vs cargo found nothing to do — eliminates the most common stale-relink trap), relinkOnly (true when succeeded but compiledCrates is empty), exeMtimeBefore/After + exeSizeBefore/After + exeChanged (proves the binary actually got rewritten), exeLocked + holderPids (when cargo failed because hydrogen-desktop.exe is held by another process — most often a still-running HD instance), builtSha (the SHA hd_build_rust / hd_build was dispatched at — compare to /buildinfo.build_sha after launch), elapsedSecs (from dispatch to now). State machine: no log → idle; log present without sentinel + PID alive → building; log ends with BUILD_OK → succeeded; log ends with BUILD_FAILED → failed; log present without sentinel + PID dead → treated as failed (orphaned). v1.8.16+.
Returns: {ok:bool, building:bool, succeeded:bool, failed:bool, lastLines:[string] (last ~40 lines of stderr), logPath:string, pid:int (cargo PID or 0), pidAlive:bool, failureReason:string (when failed:true — e.g. 'compile_error', 'exe_locked', 'orphaned_no_sentinel'), compiledCrates:[string] (names parsed from 'Compiling X v...' lines — EMPTY means relink-only, your code didn't actually rebuild), relinkOnly:bool (true iff succeeded:true && compiledCrates empty), exeChanged:bool (mtime OR size after differs from before — definitive proof a fresh binary was written), exeMtimeBefore:int (unix seconds), exeMtimeAfter:int (unix seconds), exeSizeBefore:int (bytes), exeSizeAfter:int (bytes), exeLocked:bool (true when build failed because hydrogen-desktop.exe was held by another PID), holderPids:[int] (the PIDs holding the exe — taskkill these), builtSha:string|null (the git SHA hd_build was dispatched at — compare to /buildinfo.build_sha after launch), elapsedSecs:int, flavor:string ('rust'|'frontend'|'full'), _hint:string}
Note: relinkOnly:true on success is the #1 trap to look for — your push didn't actually change the binary because cargo found nothing to recompile. Usually means your source changes weren't pulled, or you changed a file the workspace doesn't track. Use hd_ship with clean:true to force a fresh build. exeLocked:true on failure plus holderPids tells you exactly which PID to taskkill; or just use hd_ship which kills HD before building.
hd_build_tail
Streaming tail of the build log. Pass offset=0 on first call; the response includes newOffset which you pass back on the next call to get just the new bytes. Lines are split on \n; a partial trailing line (no newline yet) is held back and re-included on the next call. Use this to follow a running build like tail -f without re-fetching the whole log every poll. v1.8.16+.
Args:
offset— optional int (default 0) — byte offset into the log file from the previous call's newOffset
Returns: {ok, lines:[], newOffset, totalBytes, done:bool, succeeded:bool, failureReason, _hint}
Note: done=true means BUILD_OK or BUILD_FAILED has appeared in the log — stop polling. done=false means more output expected; sleep 2-3s and call again with newOffset.
hd_container_exec
[MOVED to hd-dev in v1.8.72] Run a command inside HD's Docker container. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{"method":"POST","path":"/container-exec","body":{"command":"..."}}'.
Args:
command— required string
Returns: Public build: refusal payload. hd-dev internal build: HD's /container-exec response.
hd_eval
[MOVED to hd-dev in v1.8.72] Evaluate JS in HD's main webview. Public Adom Desktop returns {error:'verb_moved_to_hd_dev'} with a _hint pointing at the internal build. Workaround: hd_api '{"method":"POST","path":"/eval","body":{"js":"..."}}'.
Args:
js— required string — JS expression or statement
Returns: Public build: refusal payload. hd-dev internal build: HD's /eval response.
hd_iframe_eval
[MOVED to hd-dev in v1.8.72] Evaluate JS inside HD's code-server iframe via CDP. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{"method":"POST","path":"/iframe-eval","body":{"js":"..."}}'.
Args:
contextIndex— optional intjs— required string
Returns: Public build: refusal payload. hd-dev internal build: HD's /iframe-eval response.
hd_launch
Start HD's debug binary detached. v1.8.52+ returns structured launch state so you NEVER mistake 'attached to a stale running process' for 'launched fresh code'. Three guards refuse the launch: (1) build_in_progress — a hd_build/hd_build_frontend/hd_build_rust is still running; (2) build_failed — last build ended with BUILD_FAILED; (3) already_running — hydrogen-desktop.exe is in the process list. The already_running guard can be overridden with killExisting:true (taskkills the existing instance then spawns fresh; killedFirst=true in the response). On success, sleeps briefly then reports HD's actual PID (looked up via tasklist) since the cmd-wrapper PID is gone immediately. v1.8.16+.
Args:
killExisting— optional bool (default false). When false and HD is already running, refuse with reason=already_running so the caller doesn't accidentally attach to a stale process. When true, taskkill the existing instance with a 600ms grace period before launching fresh; killedFirst=true in the response.
Returns: On launch: {ok:true, launched:true, wasAlreadyRunning:bool, pid:int (HD's actual PID, not the cmd-wrapper), killedFirst:bool (true iff killExisting was honored), exePath:string, builtSha:string|null (the SHA the binary was built at — compare to /buildinfo.build_sha to confirm fresh code), _hint}. On refusal: {ok:false, launched:false, wasAlreadyRunning:bool, pid:int|null, killedFirst:false, reason:string, _hint}. reason ∈ {'build_in_progress' (a hd_build/hd_build_rust/hd_build_frontend is still running — wait for hd_build_status.building:false), 'build_failed' (last build ended with BUILD_FAILED — fix and retry hd_build), 'already_running' (HD is up — pass killExisting:true to force-kill+relaunch, or use hd_restart), 'binary_missing' (target/debug/hydrogen-desktop.exe not on disk — run hd_build first)}.
Note: The launched + wasAlreadyRunning + killedFirst combination is the proof you need — launched:true, wasAlreadyRunning:false, killedFirst:false means truly fresh code is running; any other combination requires you to think about whether your changes actually took effect. builtSha is included so you can immediately compare to /buildinfo.build_sha without a separate call.
hd_log
Read the tail of HD's log file at %APPDATA%\hydrogen-desktop\hydrogen-desktop.log. Doesn't require HD to be running — reads the file directly from disk. Use this when HD seems stuck or has crashed to see what it said last.
Args:
tail— optional int (default 30) — number of trailing lines to return
Returns: {ok, lines:[...], path, totalLines, returnedLines}
hd_open_url
Open a URL in a specific browser profile via HD's /open-in-profile endpoint. HD knows which browsers + profiles are installed (see hd_browser_profiles) and dispatches to the right one with the right --profile-directory / -P flag.
Args:
browser— optional (default 'chrome') — 'chrome', 'edge', 'firefox', 'brave'profileDir— optional (default 'Default') — profile directory name from hd_browser_profilesurl— required string
Returns: HD's /open-in-profile response (typically {ok, pid, profile})
hd_reload_vscode
[MOVED to hd-dev in v1.8.72] Reload the VS Code iframe inside HD. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{"method":"POST","path":"/reload-vscode"}' (allowed by the public denylist).
Returns: Public build: refusal payload. hd-dev internal build: HD's /reload-vscode response.
hd_restart
Stop + launch in one call. Same guards as hd_launch (build_in_progress / build_failed / binary_missing all refuse). If HD is running it's taskkilled with a 600ms grace period before relaunch to release the exe lock. v1.8.16+.
Returns: {ok, killedPid?, pid, exePath, _hint}
Note: Use after hd_build_status shows succeeded=true to pick up the new binary. Don't call when build is in progress — the launch will fail.
hd_screenshot
Capture the Hydrogen Desktop window as a lossless PNG. Finds the window by title ('Hydrogen Desktop') via the same Win32 enum the desktop bridge uses, then captures via PrintWindow.
Returns: {success, savedTo, sizeKB, hwnd, data:{image (base64), format:'png', encoding, sizeBytes}}
Note: Requires HD to be running AND its window to be visible (PrintWindow can't capture a hidden window). If HD isn't found, returns 'Hydrogen Desktop window not found' — verify HD is up via hd_status.
hd_send_files
Send local files DIRECTLY into Hydrogen Desktop's WSL2 project. Relays the bytes to HD's control API, which writes them (as adom) into / (default 'downloads', created on first use). Lands in the WSL2 ext4 filesystem so the AI sees them IMMEDIATELY — avoids the plain send_files path (→ Windows Downloads → /mnt/c mount) whose ~5s WSL2 listing-cache makes freshly-dropped files look missing. HD owns the write (it knows its container/bind-mount layout + runs as adom), so ownership + visibility are correct. Requires HD running (hd_launch) and an HD build exposing POST /files/inject.
Args:
filePaths— required array of absolute local file paths to send (same as send_files)subdir— optional string (default 'downloads') — subfolder under HD's project dir to write into; sits alongside the project's screenshots/ folder
Returns: send_files-style result: {success:bool, filesReceived:int, destinationPaths:[ '//', ... ], error}. destinationPaths are the WSL2 paths HD actually wrote, so the AI can find them straight away. HD down or endpoint missing → success:false + a clear error.
Note: Use this instead of send_files whenever the destination is HD's WSL2 project (it's the cure for the /mnt/c cache-lag 'file isn't there' confusion).
hd_ship
v1.8.49+: atomic 'rebuild from the exact SHA I just pushed + relaunch + verify it's actually running' sequence. v1.8.57+ adds three guardrails BEFORE doing destructive work: (a) expectSha pre-validation — git fetch origin main then git cat-file -t <sha> must be commit and git merge-base --is-ancestor <sha> origin/main must succeed; (b) global build lock at %TEMP%\hd-build.lock — refuses immediately if another hd_build_*/hd_ship is in flight (no queue); (c) single-flight lifecycle mutex — refuses if hd_launch/hd_stop/hd_restart/hd_ship is already running. Pipeline after guards pass: (1) taskkill HD + poll until zero (frees exe lock); (2) git fetch + reset --hard origin/main + assert HEAD == expectSha; (3) optional cargo clean; (4) cargo build; (5) launch + poll http://127.0.0.1:47084/buildinfo until SHA matches.
Args:
clean— optional bool, default false. true forcescargo clean -p hydrogen-desktopbefore build to defeat the stale-relink trap — use when build said 'Finished' but the running SHA doesn't match expectSha on a prior hd_ship attempt.expectSha— REQUIRED string — full or prefix SHA of the commit you want running. Matching is bidirectional-prefix (so 'abc123' matches the full SHA starting with abc123 and vice versa). v1.8.57+ pre-validates against origin/main; hallucinated/typo'd/not-yet-pushed SHAs are refused at zero cost before any destructive work.
Returns: On success: {ok:true, builtSha (== expectSha), runningSha (== expectSha per /buildinfo), compiled:bool, elapsedSecs, _hint}. On pipeline failure: {ok:false, failedAt: 'args'|'kill'|'git'|'clean'|'build'|'launch'|'verify', error, builtSha|null, runningSha|null, compiled|null, elapsedSecs, _hint}. On v1.8.57+ guard refusal: {ok:false, refused:true, failedAt: 'sha_validate'|'git_fetch'|'lock'|'lifecycle_lock', error, narrative, _youProbablyMeant|_next}. Hallucinated SHA → 'sha_validate' + _youProbablyMeant tells you what real SHAs look like. Lock held → 'lock' + holderPid/holderVerb/elapsedSec tells you who's holding it.
Note: Long-running: cold cargo build can take 2-3 min. CLI WS timeout is 12 min — call as a single sync invocation and wait. v1.8.57+: arg validation, SHA validation, and lock acquisition all happen BEFORE any destructive action — if you get refused:true at the top, the running HD was not touched. compiled:false on success means cargo found nothing to recompile (either cached or HEAD didn't actually change). The verify step requires HD to expose http://127.0.0.1:47084/buildinfo returning JSON with build_sha (or buildSha or sha).
hd_shot
v1.8.54+: Capture a named region of Hydrogen Desktop, produce BOTH a full-res PNG and a Claude-safe-resized copy, and return rich metadata so the AI never has to chain screenshot→pull→convert→crop→resize→Read manually. Regions are deterministic (no AI-supplied pixel coords) — they resolve via HD window bounds + CDP element rects queried through HD's /eval endpoint. Each shot is self-identifying: it includes the running build's SHA + local build time read from HD's /buildinfo so you can confirm you're looking at the build you expect before trusting what you see. v1.8.61+ universal screenshot delivery contract: the CLI auto-pulls BOTH PNGs to /tmp/ad-shots/, verifies each landed + is non-empty, and enriches the response with localFullPath + localSafePath + localFullExists + localSafeExists + fullBytes + safeBytes + source:'hd_shot' + narrative telling you which path to Read for analysis (the safe one).
Args:
format— optional string, default 'png' (only PNG supported in v1).region— REQUIRED string. One of: 'full' (whole HD window), 'titlebar' (top ~40px strip — build SHA / timestamp readout), 'vscode' (VS Code iframe pane via CDP element rect), 'setup-panel' (Setup Steps panel via .setup-panel or .setup-panel-output), 'wiki' (wiki/browser tab pane), 'claude' (Claude Code panel). If unknown, response is {ok:false, error, knownRegions:[...]}.resizeMax— optional int, default 1400. Longest side of the safe-resized copy. Use lower (≈800) if you want to fit more shots in one Claude turn; higher (1568) for max fidelity.
Returns: {ok, region, fullPath, safePath, fullSize:[w,h], safeSize:[w,h], clipRect:[x,y,w,h], capturedAtLocal, runningBuildSha, runningBuildLocal, hdWindowFound:bool, regionFound:bool, windowOuter:[w,h], capturedSize:[w,h], _hints:{pull, fullForArchive, staleWindow, regionMiss}}
Note: Always pull_file the SAFE path and Read THAT — it's already sized for Claude's image analysis. The FULL path is meant for shotlog / disk archival, NOT direct Read (may exceed Claude's per-image budget). regionFound:false when the element rect wasn't in HD's DOM — the panel/iframe may not be mounted yet, OR HD's markup changed and region_selectors() in hd_bridge.rs is out of date. Either way, the shot is still produced (falls back to the full window) so you have something to look at. runningBuildSha can be null if HD's /buildinfo isn't exposed yet — same SHA HD's launch path uses, so you can compare to hd_launch's builtSha. All child spawns use CREATE_NO_WINDOW (no cmd flashes). v1.8.54+.
hd_status
v1.8.58+ COMPOSED + RELAYED snapshot of HD's state; v1.8.129+ churn-honest. AD owns host/process/repo facts; HD owns runtime state (relayed verbatim from HD's control API, port discovered from %APPDATA%/hydrogen-desktop/ports.json key 'control', fallback 47084 — NOT a hardcoded port). v1.8.129 reports THREE orthogonal signals instead of one boolean so you can reason about HD's constant build-relaunch churn: processPresent (hydrogen-desktop.exe exists, sampled with retry to ride the ~1s kill→respawn gap), controlApiReachable (control port actually listening), and embedded (AD's OWN mode — answers 'is AD embedded under HD?'). running = processPresent||controlApiReachable.
Returns: {ok:true, running:bool (process||control), processPresent:bool, controlApiReachable:bool, controlPort:int (the dynamically-discovered port), embedded:bool (AD's own mode), embeddedOwner:string|null, embeddedVia:string|null ('LaunchFlag'|'RuntimeAdopt'), laptopHeadSha:string, originMainSha:string, shaMatchesOrigin:bool, runningSha:string|null, workspaceHealth:object|null (HD's /workspace/health relayed verbatim), buildinfo:object|null (HD's /buildinfo relayed verbatim), narrative:string (honest about up-serving vs up-churning vs down, plus AD's embedded mode), _next:string}
Note: v1.8.129 fixes the misleading 'HD is NOT running' that fired when a single tasklist snapshot landed mid-relaunch. If you see running:true + controlApiReachable:false, HD is mid-build/relaunch — retry in a few seconds, don't treat it as down. Use this as the entry point for 'what's HD's state?' — it tells you which downstream verb to run next (hd_ship, hd_launch, hd_shot, etc.).
hd_stop
Kill hydrogen-desktop.exe. wasRunning distinguishes 'killed it' from 'nothing to do'. v1.8.16+.
Returns: {ok, wasRunning:bool, killedPid?, _hint}
# Hydrogen Desktop verbs
Build, ship, screenshot, and drive the embedded Hydrogen Desktop.
_Part of the [Adom Desktop verb reference](VERB-REFERENCE.md). Invoke as `adom-desktop <verb> '<json>'`._
> **Note:** v1.8.15+: hd_* verbs proxy to Hydrogen Desktop's local API on 127.0.0.1:9001. HD is a sibling Tauri v2 app that runs alongside adom-desktop on the user's machine. These are built-in proxy handlers (NOT a separate bridge process) — when HD isn't running, hd_status returns {ok:false, error:'HD not running'} cleanly instead of crashing.
### `hd_api`
Generic HTTP pass-through to HD's control API. The canonical way to call ANY HD endpoint from a script/AI without shelling out to curl (which the relay mangles). Auto-discovers HD's control port from %APPDATA%/hydrogen-desktop/ports.json (key 'control'); falls back to 47084. Public-build denylist refuses destructive paths (e.g. /wsl/unregister, /setup/virgin-reset) — use the hd-dev internal build for those.
**Args:**
- `body` — optional object — JSON body for POST/PUT/PATCH. Passed through verbatim (no escaping).
- `method` — optional string (default 'GET') — 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
- `path` — required string starting with '/' — e.g. '/setup/run-step', '/wsl/status', '/claude/start-auth'
- `port` — optional int — override discovered port; for testing
**Returns:** {success:bool, ok:bool, status:int, body:any, method:str, path:str, port:int} — `body` is the parsed JSON HD returned (or a string if non-JSON)
**Example:** `adom-desktop hd_api '{"method":"POST","path":"/setup/run-step","body":{"id":"welcome"}}'`
### `hd_browser_profiles`
Enumerate browsers + profiles HD detects on the user's machine (GET /browser-profiles). Use the returned profileDir values when calling hd_open_url.
**Returns:** HD's /browser-profiles response — typically {browsers:[{name, profiles:[{dir, name, gaia?}, ...]}]}
### `hd_build`
Full HD rebuild (async): git pull → pnpm build → cargo build (debug). Returns immediately with {pid, logPath}; the build runs detached so the relay's 30s HTTP timeout doesn't kill it. Each step appends to %TEMP%\hd-build.log (overwritten at start); the script writes BUILD_OK or BUILD_FAILED:<reason> as its final line so callers can detect completion without scraping tool-specific output. v1.8.16+.
**Args:**
- `lingerSecs` — optional int (default 30 when show=true, 0 when show=false) — v1.8.23+. After the build's final BUILD_OK/BUILD_FAILED line, the visible window stays open for this many seconds before auto-closing. 30s default lets the user read the error; pass 0 to close immediately; pass a larger number for more reading time. Capped at 3600s. Ignored entirely when show=false (no window to linger).
- `show` — optional bool (default false) — true opens a visible console window the user can watch the build scroll in; false runs hidden
**Returns:** {ok, pid, logPath, flavor:'full', show, lingerSecs, _hint}
**Note:** DO NOT call hd_launch until hd_build_status shows succeeded=true. Poll hd_build_status or stream output with hd_build_tail {offset:N}. Build PID is written to %TEMP%\hd-build.pid by the PowerShell script itself so it's authoritative regardless of the wrapper process tree. v1.8.23+: visible build windows now auto-close after lingerSecs (default 30) so build-heavy sessions don't accumulate orphan consoles.
### `hd_build_frontend`
Frontend-only HD build (async): `pnpm build` from the repo root. Same log/PID/sentinel conventions as hd_build. Use when only Svelte/TypeScript changed and the Rust binary is unchanged — much faster than a full build.
**Args:**
- `lingerSecs` — optional int (default 30 when show=true) — see hd_build for details. v1.8.23+.
- `show` — optional bool (default false)
**Returns:** On dispatch success: {ok, pid, logPath, flavor:'frontend', show, lingerSecs, builtSha, exeMtimeBefore, exeSizeBefore, _hint}. On lock contention (v1.8.57+): {ok:false, refused:true, failedAt:'lock', verb, holderPid, holderVerb, sha, elapsedSec, narrative, _next} — another hd_build_*/hd_ship is already running. NO QUEUE: just refused immediately. The lock auto-releases when the holder's PowerShell PID dies.
**Note:** Skip when the Rust src-tauri/ side changed — those need hd_build or hd_build_rust to rebuild the binary. v1.8.57+ adds a global build lock — two concurrent hd_build_*/hd_ship calls cannot race.
### `hd_build_log`
Return the FULL build log from %TEMP%\hd-build.log. Use this after hd_build_status reports failed to read the complete output and find the error line.
**Returns:** {ok, log:'<full text>', lines, logPath, _hint}
### `hd_build_rust`
Rust-only HD build (async): `cargo build` from src-tauri/. Same log/PID/sentinel conventions as hd_build. Use when only Rust code changed (no frontend changes). v1.8.52+: dispatch response also carries builtSha + exeMtimeBefore + exeSizeBefore (captured at spawn time into %TEMP%\hd-build-meta.json) so the subsequent hd_build_status / hd_launch don't need a separate git rev-parse + stat call.
**Args:**
- `lingerSecs` — optional int (default 30 when show=true) — see hd_build for details. v1.8.23+.
- `show` — optional bool (default false)
**Returns:** {ok, pid, logPath, flavor:'rust', show, lingerSecs, builtSha, exeMtimeBefore, exeSizeBefore, _hint}
**Note:** Builds the debug binary at target/debug/hydrogen-desktop.exe. The frontend's build/ output is NOT regenerated — re-use whatever was last built by hd_build / hd_build_frontend.
### `hd_build_status`
Synchronous build-state probe. v1.8.52+ enriched: returns compiledCrates (parsed from 'Compiling X' lines so you KNOW your crate recompiled vs cargo found nothing to do — eliminates the most common stale-relink trap), relinkOnly (true when succeeded but compiledCrates is empty), exeMtimeBefore/After + exeSizeBefore/After + exeChanged (proves the binary actually got rewritten), exeLocked + holderPids (when cargo failed because hydrogen-desktop.exe is held by another process — most often a still-running HD instance), builtSha (the SHA hd_build_rust / hd_build was dispatched at — compare to /buildinfo.build_sha after launch), elapsedSecs (from dispatch to now). State machine: no log → idle; log present without sentinel + PID alive → building; log ends with BUILD_OK → succeeded; log ends with BUILD_FAILED → failed; log present without sentinel + PID dead → treated as failed (orphaned). v1.8.16+.
**Returns:** {ok:bool, building:bool, succeeded:bool, failed:bool, lastLines:[string] (last ~40 lines of stderr), logPath:string, pid:int (cargo PID or 0), pidAlive:bool, failureReason:string (when failed:true — e.g. 'compile_error', 'exe_locked', 'orphaned_no_sentinel'), compiledCrates:[string] (names parsed from 'Compiling X v...' lines — EMPTY means relink-only, your code didn't actually rebuild), relinkOnly:bool (true iff succeeded:true && compiledCrates empty), exeChanged:bool (mtime OR size after differs from before — definitive proof a fresh binary was written), exeMtimeBefore:int (unix seconds), exeMtimeAfter:int (unix seconds), exeSizeBefore:int (bytes), exeSizeAfter:int (bytes), exeLocked:bool (true when build failed because hydrogen-desktop.exe was held by another PID), holderPids:[int] (the PIDs holding the exe — taskkill these), builtSha:string|null (the git SHA hd_build was dispatched at — compare to /buildinfo.build_sha after launch), elapsedSecs:int, flavor:string ('rust'|'frontend'|'full'), _hint:string}
**Note:** **relinkOnly:true on success is the #1 trap to look for** — your push didn't actually change the binary because cargo found nothing to recompile. Usually means your source changes weren't pulled, or you changed a file the workspace doesn't track. Use hd_ship with clean:true to force a fresh build. **exeLocked:true on failure** plus holderPids tells you exactly which PID to taskkill; or just use hd_ship which kills HD before building.
### `hd_build_tail`
Streaming tail of the build log. Pass offset=0 on first call; the response includes newOffset which you pass back on the next call to get just the new bytes. Lines are split on \n; a partial trailing line (no newline yet) is held back and re-included on the next call. Use this to follow a running build like `tail -f` without re-fetching the whole log every poll. v1.8.16+.
**Args:**
- `offset` — optional int (default 0) — byte offset into the log file from the previous call's newOffset
**Returns:** {ok, lines:[], newOffset, totalBytes, done:bool, succeeded:bool, failureReason, _hint}
**Note:** done=true means BUILD_OK or BUILD_FAILED has appeared in the log — stop polling. done=false means more output expected; sleep 2-3s and call again with newOffset.
### `hd_container_exec`
[MOVED to hd-dev in v1.8.72] Run a command inside HD's Docker container. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{"method":"POST","path":"/container-exec","body":{"command":"..."}}'.
**Args:**
- `command` — required string
**Returns:** Public build: refusal payload. hd-dev internal build: HD's /container-exec response.
### `hd_eval`
[MOVED to hd-dev in v1.8.72] Evaluate JS in HD's main webview. Public Adom Desktop returns {error:'verb_moved_to_hd_dev'} with a _hint pointing at the internal build. Workaround: hd_api '{"method":"POST","path":"/eval","body":{"js":"..."}}'.
**Args:**
- `js` — required string — JS expression or statement
**Returns:** Public build: refusal payload. hd-dev internal build: HD's /eval response.
### `hd_iframe_eval`
[MOVED to hd-dev in v1.8.72] Evaluate JS inside HD's code-server iframe via CDP. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{"method":"POST","path":"/iframe-eval","body":{"js":"..."}}'.
**Args:**
- `contextIndex` — optional int
- `js` — required string
**Returns:** Public build: refusal payload. hd-dev internal build: HD's /iframe-eval response.
### `hd_launch`
Start HD's debug binary detached. v1.8.52+ returns structured launch state so you NEVER mistake 'attached to a stale running process' for 'launched fresh code'. Three guards refuse the launch: (1) build_in_progress — a hd_build/hd_build_frontend/hd_build_rust is still running; (2) build_failed — last build ended with BUILD_FAILED; (3) already_running — hydrogen-desktop.exe is in the process list. The already_running guard can be overridden with `killExisting:true` (taskkills the existing instance then spawns fresh; killedFirst=true in the response). On success, sleeps briefly then reports HD's actual PID (looked up via tasklist) since the cmd-wrapper PID is gone immediately. v1.8.16+.
**Args:**
- `killExisting` — optional bool (default false). When false and HD is already running, refuse with reason=already_running so the caller doesn't accidentally attach to a stale process. When true, taskkill the existing instance with a 600ms grace period before launching fresh; killedFirst=true in the response.
**Returns:** On launch: {ok:true, launched:true, wasAlreadyRunning:bool, pid:int (HD's actual PID, not the cmd-wrapper), killedFirst:bool (true iff killExisting was honored), exePath:string, builtSha:string|null (the SHA the binary was built at — compare to /buildinfo.build_sha to confirm fresh code), _hint}. On refusal: {ok:false, launched:false, wasAlreadyRunning:bool, pid:int|null, killedFirst:false, reason:string, _hint}. reason ∈ {'build_in_progress' (a hd_build/hd_build_rust/hd_build_frontend is still running — wait for hd_build_status.building:false), 'build_failed' (last build ended with BUILD_FAILED — fix and retry hd_build), 'already_running' (HD is up — pass killExisting:true to force-kill+relaunch, or use hd_restart), 'binary_missing' (target/debug/hydrogen-desktop.exe not on disk — run hd_build first)}.
**Note:** **The launched + wasAlreadyRunning + killedFirst combination is the proof you need** — `launched:true, wasAlreadyRunning:false, killedFirst:false` means truly fresh code is running; any other combination requires you to think about whether your changes actually took effect. builtSha is included so you can immediately compare to /buildinfo.build_sha without a separate call.
### `hd_log`
Read the tail of HD's log file at %APPDATA%\hydrogen-desktop\hydrogen-desktop.log. Doesn't require HD to be running — reads the file directly from disk. Use this when HD seems stuck or has crashed to see what it said last.
**Args:**
- `tail` — optional int (default 30) — number of trailing lines to return
**Returns:** {ok, lines:[...], path, totalLines, returnedLines}
### `hd_open_url`
Open a URL in a specific browser profile via HD's /open-in-profile endpoint. HD knows which browsers + profiles are installed (see hd_browser_profiles) and dispatches to the right one with the right --profile-directory / -P flag.
**Args:**
- `browser` — optional (default 'chrome') — 'chrome', 'edge', 'firefox', 'brave'
- `profileDir` — optional (default 'Default') — profile directory name from hd_browser_profiles
- `url` — required string
**Returns:** HD's /open-in-profile response (typically {ok, pid, profile})
### `hd_reload_vscode`
[MOVED to hd-dev in v1.8.72] Reload the VS Code iframe inside HD. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{"method":"POST","path":"/reload-vscode"}' (allowed by the public denylist).
**Returns:** Public build: refusal payload. hd-dev internal build: HD's /reload-vscode response.
### `hd_restart`
Stop + launch in one call. Same guards as hd_launch (build_in_progress / build_failed / binary_missing all refuse). If HD is running it's taskkilled with a 600ms grace period before relaunch to release the exe lock. v1.8.16+.
**Returns:** {ok, killedPid?, pid, exePath, _hint}
**Note:** Use after `hd_build_status` shows succeeded=true to pick up the new binary. Don't call when build is in progress — the launch will fail.
### `hd_screenshot`
Capture the Hydrogen Desktop window as a lossless PNG. Finds the window by title ('Hydrogen Desktop') via the same Win32 enum the desktop bridge uses, then captures via PrintWindow.
**Returns:** {success, savedTo, sizeKB, hwnd, data:{image (base64), format:'png', encoding, sizeBytes}}
**Note:** Requires HD to be running AND its window to be visible (PrintWindow can't capture a hidden window). If HD isn't found, returns 'Hydrogen Desktop window not found' — verify HD is up via hd_status.
### `hd_send_files`
Send local files DIRECTLY into Hydrogen Desktop's WSL2 project. Relays the bytes to HD's control API, which writes them (as adom) into <project>/<subdir> (default 'downloads', created on first use). Lands in the WSL2 ext4 filesystem so the AI sees them IMMEDIATELY — avoids the plain send_files path (→ Windows Downloads → /mnt/c mount) whose ~5s WSL2 listing-cache makes freshly-dropped files look missing. HD owns the write (it knows its container/bind-mount layout + runs as adom), so ownership + visibility are correct. Requires HD running (hd_launch) and an HD build exposing POST /files/inject.
**Args:**
- `filePaths` — required array of absolute local file paths to send (same as send_files)
- `subdir` — optional string (default 'downloads') — subfolder under HD's project dir to write into; sits alongside the project's screenshots/ folder
**Returns:** send_files-style result: {success:bool, filesReceived:int, destinationPaths:[ '<project>/<subdir>/<name>', ... ], error}. destinationPaths are the WSL2 paths HD actually wrote, so the AI can find them straight away. HD down or endpoint missing → success:false + a clear error.
**Note:** Use this instead of send_files whenever the destination is HD's WSL2 project (it's the cure for the /mnt/c cache-lag 'file isn't there' confusion).
### `hd_ship`
v1.8.49+: atomic 'rebuild from the exact SHA I just pushed + relaunch + verify it's actually running' sequence. v1.8.57+ adds three guardrails BEFORE doing destructive work: (a) expectSha pre-validation — `git fetch origin main` then `git cat-file -t <sha>` must be `commit` and `git merge-base --is-ancestor <sha> origin/main` must succeed; (b) global build lock at `%TEMP%\hd-build.lock` — refuses immediately if another hd_build_*/hd_ship is in flight (no queue); (c) single-flight lifecycle mutex — refuses if hd_launch/hd_stop/hd_restart/hd_ship is already running. Pipeline after guards pass: (1) taskkill HD + poll until zero (frees exe lock); (2) git fetch + reset --hard origin/main + assert HEAD == expectSha; (3) optional cargo clean; (4) cargo build; (5) launch + poll http://127.0.0.1:47084/buildinfo until SHA matches.
**Args:**
- `clean` — optional bool, default false. true forces `cargo clean -p hydrogen-desktop` before build to defeat the stale-relink trap — use when build said 'Finished' but the running SHA doesn't match expectSha on a prior hd_ship attempt.
- `expectSha` — REQUIRED string — full or prefix SHA of the commit you want running. Matching is bidirectional-prefix (so 'abc123' matches the full SHA starting with abc123 and vice versa). v1.8.57+ pre-validates against origin/main; hallucinated/typo'd/not-yet-pushed SHAs are refused at zero cost before any destructive work.
**Returns:** On success: {ok:true, builtSha (== expectSha), runningSha (== expectSha per /buildinfo), compiled:bool, elapsedSecs, _hint}. On pipeline failure: {ok:false, failedAt: 'args'|'kill'|'git'|'clean'|'build'|'launch'|'verify', error, builtSha|null, runningSha|null, compiled|null, elapsedSecs, _hint}. On v1.8.57+ guard refusal: {ok:false, refused:true, failedAt: 'sha_validate'|'git_fetch'|'lock'|'lifecycle_lock', error, narrative, _youProbablyMeant|_next}. Hallucinated SHA → 'sha_validate' + _youProbablyMeant tells you what real SHAs look like. Lock held → 'lock' + holderPid/holderVerb/elapsedSec tells you who's holding it.
**Note:** Long-running: cold cargo build can take 2-3 min. CLI WS timeout is 12 min — call as a single sync invocation and wait. v1.8.57+: arg validation, SHA validation, and lock acquisition all happen BEFORE any destructive action — if you get `refused:true` at the top, the running HD was not touched. `compiled:false` on success means cargo found nothing to recompile (either cached or HEAD didn't actually change). The verify step requires HD to expose http://127.0.0.1:47084/buildinfo returning JSON with `build_sha` (or `buildSha` or `sha`).
### `hd_shot`
v1.8.54+: Capture a named region of Hydrogen Desktop, produce BOTH a full-res PNG and a Claude-safe-resized copy, and return rich metadata so the AI never has to chain screenshot→pull→convert→crop→resize→Read manually. Regions are deterministic (no AI-supplied pixel coords) — they resolve via HD window bounds + CDP element rects queried through HD's /eval endpoint. Each shot is self-identifying: it includes the running build's SHA + local build time read from HD's /buildinfo so you can confirm you're looking at the build you expect before trusting what you see. v1.8.61+ universal screenshot delivery contract: the CLI auto-pulls BOTH PNGs to /tmp/ad-shots/<basename>, verifies each landed + is non-empty, and enriches the response with `localFullPath` + `localSafePath` + `localFullExists` + `localSafeExists` + `fullBytes` + `safeBytes` + `source:'hd_shot'` + `narrative` telling you which path to Read for analysis (the safe one).
**Args:**
- `format` — optional string, default 'png' (only PNG supported in v1).
- `region` — REQUIRED string. One of: 'full' (whole HD window), 'titlebar' (top ~40px strip — build SHA / timestamp readout), 'vscode' (VS Code iframe pane via CDP element rect), 'setup-panel' (Setup Steps panel via .setup-panel or .setup-panel-output), 'wiki' (wiki/browser tab pane), 'claude' (Claude Code panel). If unknown, response is {ok:false, error, knownRegions:[...]}.
- `resizeMax` — optional int, default 1400. Longest side of the safe-resized copy. Use lower (≈800) if you want to fit more shots in one Claude turn; higher (1568) for max fidelity.
**Returns:** {ok, region, fullPath, safePath, fullSize:[w,h], safeSize:[w,h], clipRect:[x,y,w,h], capturedAtLocal, runningBuildSha, runningBuildLocal, hdWindowFound:bool, regionFound:bool, windowOuter:[w,h], capturedSize:[w,h], _hints:{pull, fullForArchive, staleWindow, regionMiss}}
**Note:** Always pull_file the SAFE path and Read THAT — it's already sized for Claude's image analysis. The FULL path is meant for shotlog / disk archival, NOT direct Read (may exceed Claude's per-image budget). **regionFound:false** when the element rect wasn't in HD's DOM — the panel/iframe may not be mounted yet, OR HD's markup changed and region_selectors() in hd_bridge.rs is out of date. Either way, the shot is still produced (falls back to the full window) so you have something to look at. **runningBuildSha** can be null if HD's /buildinfo isn't exposed yet — same SHA HD's launch path uses, so you can compare to hd_launch's builtSha. All child spawns use CREATE_NO_WINDOW (no cmd flashes). v1.8.54+.
### `hd_status`
v1.8.58+ COMPOSED + RELAYED snapshot of HD's state; v1.8.129+ churn-honest. AD owns host/process/repo facts; HD owns runtime state (relayed verbatim from HD's control API, port discovered from %APPDATA%/hydrogen-desktop/ports.json key 'control', fallback 47084 — NOT a hardcoded port). v1.8.129 reports THREE orthogonal signals instead of one boolean so you can reason about HD's constant build-relaunch churn: processPresent (hydrogen-desktop.exe exists, sampled with retry to ride the ~1s kill→respawn gap), controlApiReachable (control port actually listening), and embedded (AD's OWN mode — answers 'is AD embedded under HD?'). `running` = processPresent||controlApiReachable.
**Returns:** {ok:true, running:bool (process||control), processPresent:bool, controlApiReachable:bool, controlPort:int (the dynamically-discovered port), embedded:bool (AD's own mode), embeddedOwner:string|null, embeddedVia:string|null ('LaunchFlag'|'RuntimeAdopt'), laptopHeadSha:string, originMainSha:string, shaMatchesOrigin:bool, runningSha:string|null, workspaceHealth:object|null (HD's /workspace/health relayed verbatim), buildinfo:object|null (HD's /buildinfo relayed verbatim), narrative:string (honest about up-serving vs up-churning vs down, plus AD's embedded mode), _next:string}
**Note:** v1.8.129 fixes the misleading 'HD is NOT running' that fired when a single tasklist snapshot landed mid-relaunch. If you see running:true + controlApiReachable:false, HD is mid-build/relaunch — retry in a few seconds, don't treat it as down. Use this as the entry point for 'what's HD's state?' — it tells you which downstream verb to run next (hd_ship, hd_launch, hd_shot, etc.).
### `hd_stop`
Kill hydrogen-desktop.exe. `wasRunning` distinguishes 'killed it' from 'nothing to do'. v1.8.16+.
**Returns:** {ok, wasRunning:bool, killedPid?, _hint}