* refactor(agy): make sweepAgyHookCarriers async via fs/promises Convert the carrier sweep's directory scan (readdir/lstat/readFile/rm) from sync fs calls to fs/promises, so it no longer blocks the event loop while it runs. The call site in runAgy.ts still awaits it in place, so this preserves the current blocking-before-hook-server ordering exactly -- only the mechanism changes. Also adds racing-safety tests proving a carrier created after the readdir() snapshot was taken (or written by this session's own prepareAgyHookCarrier() while a sweep is in flight) is never examined, since decoupling the call site (next commit) makes that interleaving possible for the first time. * perf(agy): decouple carrier sweep from session startup sweepAgyHookCarriers is a backup path -- normal teardown already removes a carrier via cleanupAgyHookCarrier, so sweep only matters after a crash. There is no reason for it to delay this session's own startup (hook server, carrier prep, PTY spawn). Now that it is async (previous commit), fire it without awaiting it instead of blocking on it before startHookServer. It runs concurrently with this session's own prepareAgyHookCarrier() call further down; the previous commit's racing-safety tests prove that interleaving is safe. Still guaranteed to never throw or leave an unhandled rejection. * refactor(agy): add a process-lifetime cache for carrier scope The upcoming macOS/Windows identity probes are async child-process calls (ioreg/sysctl, reg query), but writeOwnerMetadata is called synchronously from prepareAgyHookCarrier -- including from agyPtyLauncher.ts's respawn path, which must stay synchronous per its fail-closed contract. A warm cache lets that synchronous call read a value computed ahead of time instead of needing to await a probe. Adds computeLocalCarrierScopeAsync (platform dispatcher, Linux-only for now -- delegates to the exact same sync read computeLocalCarrierScope already does, so no platform's observable output changes here), warmCarrierScope (populates the cache once, including caching a failed probe so it is never retried), and resolveLocalCarrierScope (used by sweepAgyHookCarriers; bypasses the cache for any non-default probe so the many existing custom-probe tests keep forcing a fresh computation per call). writeOwnerMetadata reads the cache first and falls back to the existing synchronous Linux computation when the cache was never warmed -- unchanged behavior wherever nothing calls warmCarrierScope yet. * feat(agy): enable carrier sweep on macOS computeLocalCarrierScope was Linux-only, so sweepAgyHookCarriers was a no-op on macOS -- only crash leftovers there ever accumulated (normal teardown still works via cleanupAgyHookCarrier), but they accumulated forever. Adds a strong-identity scope for macOS: IOPlatformUUID (ioreg) + kern.bootsessionuuid (sysctl), the macOS analogue of Linux's boot_id -- NOT kern.boottime, which a live re-measurement showed drifts by over a second across 8 days on the same boot (recomputed from NTP-adjusted wall clock time under the hood). The probe runs by absolute path via execFile (never a shell, never PATH-dependent) with a 2s timeout; a failed or timed-out probe still resolves to undefined, preserving sweepAgyHookCarriers's existing "cannot identify -> preserve everything" contract unchanged. Windows is deliberately not enabled. MachineGuid -- the obvious candidate for a win32 scope -- is a machine identifier, not a PID-space identifier: it is written once at OS install time and is NOT regenerated by cloning a disk image (that is exactly what sysprep exists to fix). Two clones of the same image sharing a HAPI_HOME (SMB share, sync folder, shared VM folder) would compute the identical win32:<guid> scope while having completely independent PID spaces, so sweep could delete a live session's carrier and its --dangerously-skip-permissions approval bridge with it. No cheap per-boot alternative exists on Windows: no boot-id registry key, Get-CimInstance's LastBootUpTime measured 1.4-2.5s per call (an order of magnitude too slow for identity plumbing), and net statistics/systeminfo output is locale-dependent. computeLocalCarrierScopeAsync's docstring records the full reasoning and the bar for re-enabling it (a boot-scoped, not machine-scoped, identifier cheaper than CIM). Windows carriers keep accumulating exactly as before this change -- this is a purely additive capability for macOS. Wires warmCarrierScope into runAgy.ts's PTY setup: fired without awaiting it right after the sweep call (so the probe cost -- two child processes on macOS -- overlaps with hook server startup instead of adding to it), then awaited immediately before prepareAgyHookCarrier() so its synchronous owner.json write reads a warm cache. agyPtyLauncher.ts's respawn path needs no equivalent wiring: it always runs in the same, by-then-warm process.
hapi CLI
Run Claude Code, Codex, Cursor Agent, Grok Build, or OpenCode sessions from your terminal and control them remotely through the hapi hub.
What it does
- Starts Claude Code sessions and registers them with hapi-hub.
- Starts Codex mode for OpenAI-based sessions.
- Starts Cursor Agent mode for Cursor CLI sessions.
- Starts Grok Build locally or via ACP for remote sessions.
- Starts OpenCode mode via ACP and its plugin hook system.
- Provides an MCP stdio bridge for external tools.
- Manages a background runner for long-running sessions.
- Includes diagnostics and auth helpers.
Typical flow
- Start the hub and set env vars (see ../hub/README.md).
- Set the same CLI_API_TOKEN on this machine or run
hapi auth login. - Run
hapito start a session. - Use the web app or Telegram Mini App to monitor and control.
Commands
Session commands
hapi- Start a Claude Code session (passes through Claude CLI flags). Seesrc/index.ts.hapi codex- Start Codex mode. Seesrc/codex/runCodex.ts.hapi codex resume <sessionId>- Resume existing Codex session.hapi cursor- Start Cursor Agent mode. Seesrc/cursor/runCursor.ts. Supportshapi cursor resume <chatId>,hapi cursor --continue,--mode plan|ask,--yolo,--model. Local and remote modes supported; remote usesagent -pwith stream-json.hapi grok- Start Grok Build mode. Seesrc/grok/runGrok.ts.hapi opencode- Start OpenCode mode via ACP. Seesrc/opencode/runOpencode.ts. Note: OpenCode supports local and remote modes; local mode streams via OpenCode plugins.hapi resume [sessionId]- List resumable sessions for this machine or resume one locally.hapi ping-peer <session-id-prefix> <message>- Resume (if needed) and message another session. Prefer this or MCPping_peer/list_peersover reinventing JWT+curl. Also--message-file/--list.hapi inspect-peer <session-id-or-prefix>- Read-only peer metadata + recent message text (no resume). Prefer this or MCPinspect_peerwhen a user cites[title](/sessions/<id>)or Copy-referenceSee session "…" (/sessions/<id>) for context./sessions/<id>is a hub path, not a local file. Optional--limit.
Resume a remote session locally
hapi resume
hapi resume <session-id>
hapi resume lists resumable sessions for the current machine. hapi resume <session-id> hands off an active remote session and opens the same HAPI session in the local terminal.
Authentication
hapi auth status- Show authentication configuration and token source.hapi auth login- Interactively enter and save CLI_API_TOKEN.hapi auth logout- Clear saved credentials.
See src/commands/auth.ts.
Runner management
hapi runner start- Start runner as detached process.hapi runner stop- Stop runner gracefully.hapi runner status- Show runner diagnostics.hapi runner list- List active sessions managed by runner.hapi runner stop-session <sessionId>- Terminate specific session.hapi runner logs- Print path to latest runner log file.
Both start and start-sync accept repeatable --workspace-root <path> (or --workspace-root=<path>). When set:
- The web
/browsepage surfaces scoped file trees rooted at those paths. - The runner refuses
list-directoryandspawn-sessionrequests for paths outside the configured roots. ~and~/fooare expanded.
Omitting the flag keeps the legacy behavior: no scoping, no /browse feature.
See src/runner/run.ts.
Diagnostics
hapi doctor- Show full diagnostics (version, runner status, logs, processes).hapi doctor clean- Kill runaway HAPI processes.
See src/ui/doctor.ts.
Other
hapi mcp- Start MCP stdio bridge. Seesrc/codex/happyMcpStdioBridge.ts.hapi hub- Start the bundled hub (single binary workflow).hapi server- Alias forhapi hub.
Configuration
See src/configuration.ts for all options.
Required
CLI_API_TOKEN- Shared secret; must match the hub. Can be set via env or~/.hapi/settings.json(env wins).HAPI_API_URL- Hub base URL (default: http://localhost:3006).
Optional
HAPI_HOME- Config/data directory (default: ~/.hapi).HAPI_EXPERIMENTAL- Enable experimental features (true/1/yes).HAPI_EXTRA_HEADERS_JSON- JSON object of extra headers to send on CLI → hub requests, e.g.{"Cookie":"CF_Authorization=..."}. Can also be set as theextraHeadersobject in~/.hapi/settings.json(environment variable wins).HAPI_CLAUDE_PATH- Path to a specificclaudeexecutable.HAPI_HTTP_MCP_URL- Default MCP target forhapi mcp.
Runner
HAPI_RUNNER_HEARTBEAT_INTERVAL- Heartbeat interval in ms (default: 60000).HAPI_RUNNER_HTTP_TIMEOUT- HTTP timeout for runner control in ms (default: 10000).
Worktree (set by runner)
HAPI_WORKTREE_BASE_PATH- Base repository path.HAPI_WORKTREE_BRANCH- Current branch name.HAPI_WORKTREE_NAME- Worktree name.HAPI_WORKTREE_PATH- Full worktree path.HAPI_WORKTREE_CREATED_AT- Creation timestamp (ms).
Set for the wrapped agent
-
HAPI_SESSION_ID- The hub session id for the current run, exported into the wrapped agent/CLI child environment at spawn for every flavor (claude / codex / copilot / cursor / gemini / opencode / kimi / grok / pi), both runner-spawned and locally started sessions. Agents can read it to self-target "this chat" over the hub REST API or shell helpers without listing/api/sessions. Prefer the MCPdisplay_imagetool for inline media when it is available; useHAPI_SESSION_IDfor hub REST / shell tooling where MCP is not. To list peers on the same hub/namespace, prefer MCPlist_peers(works from runner-spawned sessions without sitting on the hub host; excludes the calling session). To read another session, prefer MCPinspect_peerorhapi inspect-peer. To message another session, prefer MCPping_peerorhapi ping-peer— do not reinvent JWT+curl. User citations look like[title](/sessions/<id>)or Copy-referenceSee session "…" (/sessions/<id>) for context; pass that<id>assessionIdPrefix. Do not Grep/Glob/sessions/<id>as a local filesystem path. On a remote runner, configure matchingHAPI_API_URL+CLI_API_TOKEN(orhapi auth login/~/.hapi/settings.json) on the runner host so shellhapi ping-peer --listworks; session CLI may export an explicit non-default hub URL into child env, but never mirrorsCLI_API_TOKENinto wrapped agents.Lazy Codex (terminal) sessions export the id only after the hub row is materialized, which happens when the MCP bridge starts — before the agent process is spawned — so path-only self-targeting does not race a missing hub row.
Example (shell fallback when MCP is unavailable) — path-only, self-targets the current session:
bun scripts/tooling/hapi-display-image.mjs /absolute/path/to/image.png "optional title"Explicit other session (prefix or full uuid) still works; that path may list sessions.
Storage
Data is stored in ~/.hapi/ (or $HAPI_HOME):
settings.json- User settings (machineId, token, onboarding flag). Seesrc/persistence.ts.runner.state.json- Runner state (pid, port, version, heartbeat).logs/- Log files.
Requirements
- Claude CLI installed and logged in (
claudeon PATH). - Cursor Agent CLI installed (
agenton PATH) forhapi cursor. Install:curl https://cursor.com/install -fsS | bash(macOS/Linux),irm 'https://cursor.com/install?win32=true' | iex(Windows). - Grok Build CLI installed (
grokon PATH) forhapi grok. Authenticate withgrok login --device-authon headless runner machines, or setXAI_API_KEY. - OpenCode CLI installed (
opencodeon PATH). - Bun for building from source.
Build from source
From the repo root:
bun install
bun run build:cli
bun run build:cli:exe
For an all-in-one binary that also embeds the web app:
bun run build:single-exe
Source structure
src/api/- Bot communication (Socket.IO + REST).src/claude/- Claude Code integration.src/codex/- Codex mode integration.src/cursor/- Cursor Agent integration.src/grok/- Grok Build native TUI + ACP integration.src/agent/- Shared support for ACP-compatible agents.src/opencode/- OpenCode ACP + hook integration.src/runner/- Background service.src/commands/- CLI command handlers.src/ui/- User interface and diagnostics.src/modules/- Tool implementations (ripgrep, difftastic, git).
Related docs
../hub/README.md../web/README.md