* fix(agy): say what the model picker is actually waiting on The spinner in the New Session AGY picker read "Checking Antigravity authentication…", but nothing at that point checks authentication — the machine is running `agy models`, and the sign-in prompt is a separate branch below it, shown only when agy reports the failure. Name the wait after the work: "Fetching available models…", the same words agy prints while it fetches. * refactor(agy): describe a probe by its outcome, not by its response The probe function returned a finished `AgyModelsResponse`, so "agy could not be reached" and "agy listed no models" both arrived as a successful response carrying the hardcoded mirror, and the caller could no longer tell which had happened. Every policy decision about that answer has to live inside the probe as a result. Hand back what the probe observed — a live catalog, an auth failure, or nothing usable — and let the caller turn it into a response. Same behaviour: the mirror still stands in for both failure modes, and the 60s cache still holds whatever came out. * fix(agy): serve the model catalog stale-while-revalidate The `agy models` probe is a whole agy invocation — around 3s on a good day, 15s when it times out — and the 60s window meant the New Session picker paid that again a minute after the last look. Keep the last listing agy actually returned and answer from it: fresh for ten minutes, then still answered while a probe refreshes behind it, until the entry is a day old and stops standing in for the machine at all. A probe that times out or loses auth leaves that entry alone, so a blip no longer empties a working picker, and the hardcoded mirror is no longer recorded as if the machine had reported it. Three things fall out of that and are handled here. A machine whose sign-in has actually gone bad would otherwise look healthy for a day, so an auth failure rides along with the catalog it can still serve — and, because nothing else would re-probe a catalog that is still fresh, a warning riding on the answer is itself a reason to look again. A failed probe is not repeated on the very next request either, or a machine where agy hangs would spawn it once per poll. An explicit refresh always costs a probe, and never rides one that was already running when it was asked for. * fix(agy): let Retry force a fresh model catalog probe With the catalog held for ten minutes, Retry would otherwise hand back the answer it was pressed to replace, so the intent travels to the machine: `?refresh=true` on the machine route, an optional RPC param, and a one-shot flag on the query so ordinary mount and focus refetches stay cheap. Every hop is optional, so a hub and a runner on different versions still talk — the older side ignores it and answers from its cache. Retry also has to be reachable, and honest, in the state that needs it. The machine now answers with both a usable catalog and the sign-in failure behind it, so the picker keeps the list and says why it may be out of date, with the button right there rather than only once there is nothing left to show. Pressing it runs agy, which can take tens of seconds, so the button says so while it does. The client contract covers both: `agy-models` is the one catalog route that can carry an `error` on a successful response, and the one that takes a refresh parameter. * fix(agy): use the machine catalog in the in-session model picker New Session already asks the machine what `agy models` lists, but a session that is already open offered the built-in list in `shared/src/models.ts`. That list is a hand-maintained mirror, so a model agy started offering after the last release could be picked for a new session and not for the one already running. Point the composer at the same machine catalog. The mirror stays as the fallback for the moment before the machine answers, and a model the session is already on is kept selectable — and readable, when it is one of the known presets — even after the catalog moves on without it. * fix(agy): announce a model catalog re-check that changed the answer Serving the last known catalog answers the picker instantly, but a picker that was already open kept showing that answer until the user closed and reopened it — the machine had no way to say it had found something newer. Say it on the stream that already carries machine changes. The machine daemon — the only process that answers `<machineId>:listAgyModels` — emits it, and the hub forwards it as `machine-agy-models-updated` with nothing but the machineId. Namespace resolution, per-machine delivery and reconnect replay all come from the existing path. What counts as a change is what the route would answer, not what sits in the cache. That distinction carries the cases: a sign-in that lapsed or came back changes no models yet changes what the user is told; a machine whose agy was signed out has been answering from the hardcoded mirror, and its first real listing is the largest change there is, for every client except the one awaiting it. * fix(agy): re-read the announced machine's model catalog On `machine-agy-models-updated`, cancel and refetch that one machine's catalog query — the app's global connection is always subscribed, so an open picker redraws wherever it is. Cancelling first is what makes it correct rather than merely likely. query-core cancels an in-flight fetch only when the query already holds data, so a picker opening for the first time would otherwise join the request already on its way and settle on the listing the announcement replaced. The refetch is answered from the machine's cache, so it starts no probe and cannot bounce another announcement back. A reconnect the hub could not replay takes the resync path, which clears the agy catalogs the same way — that path has no announcement to fall back on, so it is the one that can least afford to join a stale request. * fix(agy): keep a model the user picked when the catalog moves under them The catalog can now change while the New Session form is open, and the form dropped any selection the machine no longer advertised — including one the user had just made. Keep that one, and list it as no longer listed so the form does not imply agy is still offering it. A model restored from a draft or a saved preference is still dropped: it may never have been runnable here. * fix(agy): announce uncached authentication changes
hapi CLI
Run Claude Code, Codex, Cursor Agent, Grok Build, OpenCode, or DeepSeek Harness 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.
- Starts DeepSeek Harness through an external ACP stdio server.
- 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 dsh- Start DeepSeek Harness through ACP. Seesrc/dsh/runDsh.ts. DSH is remote-only and its ACP server must be configured separately.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 manual session spawning unrestricted and leaves the
web /browse feature disabled. Machine directory lookups used by session
autocomplete and native pickers are still available, but are limited to the
runner's home directory.
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.
Codex MCP servers
Codex sessions keep the MCP servers configured in the user's Codex
config.toml. HAPI adds its own hapi bridge without replacing other user
servers. Runner-spawned Codex sessions copy only config.toml into their
temporary CODEX_HOME, so MCP settings are preserved while authentication
state remains isolated. The hapi server name is reserved by HAPI.
On Windows, known package-manager shims (uvx, npx, npm, pnpm, yarn,
bunx, and .cmd/.bat commands) use a short-lived HAPI stdio compatibility
proxy before reaching the configured MCP server. The proxy keeps the original
command and arguments in a session-temporary file and forwards MCP JSON-RPC
bytes without putting environment-variable values into arguments or that file.
Secret and network variables still need to be listed in the MCP entry's
env_vars (or supplied through env); HAPI does not forward the whole host
environment automatically.
Configuration
See src/configuration.ts for all options.
DeepSeek Harness ACP uses dsh-acp-demo by default. Override the executable or
its arguments without shell parsing:
export HAPI_DSH_ACP_COMMAND=dsh-acp-demo
export HAPI_DSH_ACP_CONFIG=/path/to/deepseek-harness/examples/acp-agent/cordis.yml
hapi dsh
For a source checkout, use JSON arguments:
export HAPI_DSH_ACP_COMMAND=pnpm
export HAPI_DSH_ACP_ARGS_JSON='["--dir", "/path/to/deepseek-harness", "run", "demo:acp"]'
The official ACP demo is fresh-session-only and does not support native resume, model switching, MCP injection, or live tool/reasoning telemetry. HAPI uses the standard chat and pending one-shot permission surfaces; the ACP composition owns the overall permission policy and HAPI does not advertise resume or model controls for DSH.
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_DSH_ACP_COMMAND- ACP server executable forhapi dsh(default:dsh-acp-demo).HAPI_DSH_ACP_CONFIG- Optionaldsh-acp-demo --configpath.HAPI_DSH_ACP_ARGS_JSON- Optional JSON array of ACP server arguments.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 1.4.0 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
Codex Luna Reserve
Remote sessions read Codex's account usage through app-server. Luna Reserve is shown only while the task is using the backend-authorized Reserve route; an unused Reserve allowance never adds a row or a model option. Ordinary and Reserve windows remain separate, and unavailable percentages stay unknown.
HAPI applies the official fallback with thread/settings/update and restores
the task's saved model and reasoning effort after a fresh account read permits
ordinary usage. It does not replay the blocked turn. Resume first reconciles
Codex's task settings; the task-local return record uses Codex's
CODEX_HOME/tui-luna-reserve/<thread-id>.json format for TUI handoff.
This requires the newer app-server protocol with ordinaryUsageAllowed,
supportsLunaReserve, the hidden Reserve catalog entry, and thread settings
updates. Verified against official source
ac192cd7937.
Codex 0.153.4 does not expose the required usage capability; no minimum released
version is claimed. Older servers keep their ordinary usage display without
advertising Reserve activation. Real eligible-account exhaustion, Reserve
exhaustion, and recovery still require account-level validation.