Files
hapi/cli/README.md
T
7f5d6b17f3 feat(peer): spawn-peer remit + rename-proof Parent UUID stamps (#1922)
* feat(cli): spawn-peer with required remit and MCP spawn_peer

Machine spawn creates an empty composer; stuffing message into that
JSON is stripped. Add hapi spawn-peer and MCP spawn_peer that spawn,
rename, deliver via the ping-peer path, and fail closed unless the
remit is on the new session. Reject message/prompt/text on machine
spawn with 400. Do not auto-approve spawn_peer in default/read-only.

Fixes #1509

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): reject overlong spawn-peer names before spawn

Hub rename max is 255; continuing past PATCH 400 left a live session under a different title. MCP/steer copy now says the hub 400s a spawn-body remit instead of stripping it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): wait for fresh spawn-peer child instead of resuming it

Machine spawn can return while the hub row is still inactive. ping-peer then POSTed /resume, which can launch a second agent because the original spawn has no existingSessionId for runner dedupe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): reject flavor-illegal permissionMode before spawn-peer

Codex + bypassPermissions (and Pi + any mode) used to reach the runner and fail after a child existed, or silently drop the mode. Check isPermissionModeAllowedForFlavor when both agent and mode are explicit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): treat omitted spawn-peer agent as Claude for permissionMode

Hub and runner default agent to claude when spawn omits it. Validate permissionMode against that effective flavor so yolo/read-only fail closed before a child starts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): resolve spawn-peer dir locally and archive failed children

Relative --dir values were forwarded unchanged, so the runner resolved them against daemon cwd. Remit delivery failures also left the new session running, so a retry spawned a duplicate. Resolve in the caller and best-effort archive the child before rethrowing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): verify remit before archiving a failed spawn-peer child

A timed-out ping-peer POST can still land the remit. Check the transcript before archive, and only say the child was archived when the hub returns ok.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): swallow spawn-peer transcript GET errors and skip unapplied names

A thrown messages GET after spawn skipped archive and leaked a live child. Treat transport errors as a miss so the verify loop can retry, then clean up. Also do not return a requested name the hub never applied.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(spawn-peer): hub peerSpawnDefaults + model/effort spawn surface

Add hub settings peerSpawnDefaults (agent, permissionMode, per-flavor models)
with stock claude/yolo/sonnet fallbacks. spawnPeer resolves explicit args →
hub settings → stock, maps yolo to flavor equivalents, and forwards model/effort
into machine spawn. Settings UI under General → Agents; MCP/CLI optional overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(hub): use cursor-valid permission mode in peer spawn defaults test

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): teach spawn_peer that omit permissionMode means yolo

MCP copy said "pass permissionMode only when the operator asked", so agents
omitted it and got hub/stock whatever was stored — often approval-gated
default. Clarify omit = yolo via peerSpawnDefaults/stock; pass only to tighten.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub,web): apply peerSpawnDefaults to machine spawn and New Session

POST /api/machines/:id/spawn now resolves omitted agent/permissionMode/model
from hub Settings, and New Session seeds from the same peerSpawnDefaults so
UI create and peer spawn share one authoritative default (stock yolo).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): NewSession hub seed respects sticky prefs + QueryClient in tests

Sticky localStorage last-used agent wins over hub seed; machine spawn still
applies peerSpawnDefaults server-side. Tests provide QueryClientProvider.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(web): mock CREATABLE_AGENT_FLAVORS for settings general peerSpawn UI

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(web): stabilize NewSession tests after hub peerSpawnDefaults seed

Wrap remaining mounts in QueryClientProvider, pre-seed hub settings so a
late useQuery resolve cannot wipe in-test model picks, and use neutral
stock permission defaults in the fixture.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): address tip Codex Majors for peerSpawnDefaults

Preserve explicit yolo booleans on machine spawn, fail closed when hub
defaults cannot load, validate permission modes against the resolved
agent, remap auto-approval across flavors, and stop late hub settings
from wiping New Session edits. Reject retired agents in spawn defaults
and restore the CLI executable build docs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): address follow-up tip Codex Majors

Stop absent Yolo localStorage from beating hub defaults, track New Session
edits before late hub seed, reject incompatible machine-spawn permissions,
resolve MCP spawn_peer paths against the session working directory, and
hide unsupported Codex safe-yolo from Settings spawn defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): clear tip-2 CI and follow-up Codex Majors

Omit permissionMode for Pi/DSH, preserve explicit Yolo-off across delayed
hub seed, fix settings test mocks for launch permission helpers, and
restore unrelated CLI README lifecycle/ACP docs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): tip-3 Majors for seed edits and launchable modes

Skip hub agent seed after permission edits, resolve inherited modes via
the launch catalog (AGY request-review fallback), preserve hub Cursor
models through preference restore, and restore Codex auth README nuance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): tip Minors for Cursor/Grok hub permission seeding

Give Cursor the native permission select so Plan/Ask survive machine
spawn, and re-apply Grok hub permissions after agent-init resets.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): tip Minors for Cursor select and hub seed remap

Map hub permission through resolvePermissionModeForFlavor, keep native
Default when agent is explicit without a model, and assert Cursor Plan/Ask Mode labels.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): tip Minors for delayed hub seed and Default model

Apply hub permission/model to unset fields even with a sticky agent,
preserve explicitly saved auto models, and seed OpenCode/AGY selectors
from hub defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): tip Minors for model edit sticky and clear override

Mark OpenCode/AGY model picks as edited so delayed hub seed cannot
overwrite them, and treat empty hub model values as clear sentinels.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): tip Majors for models patch typing and remit page scan

Type the models patch as NonNullable and paginate transcript verification
oldest-ward so a busy child is not archived when the remit leaves page 1.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): tip Minor — keep agent after explicit model pick

Do not apply a late hub default agent when the operator already chose a
model; otherwise preference restore drops that selection.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spawn-peer): tip Cursor YOLO migrate and Codex effort map

Bridge Cursor's saved YOLO toggle (incl. false) into native Default over
hub yolo, and send Codex/OpenCode --effort as modelReasoningEffort.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): tip Majors — scoped YOLO bridge and catalog fallback

Keep unsupported saved permission modes at Default instead of hub yolo,
and apply delayed YOLO migration only when the sticky bridge agent matches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): drop rebase conflict markers in startHappyServer

Both sessionDisplayRename and spawnPeer imports are required after
rebasing onto upstream rename work; markers had slipped into tip.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): wrap Kimi NewSession tests in QueryClientProvider

Hub settings useQuery requires a provider; upstream Kimi catalog tests
rendered bare and broke after the spawn-peer rebase onto main.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(peer): rename-proof Parent UUID stamp on spawn remits

Orchestrated spawn_peer / spawn-peer remits now auto-stamp a durable
[title](/sessions/<uuid>) Parent chip (fail-closed in-session). MCP copy
forbids inventing titles; UUID stays identity after parent rename.

Refs heavygee/hapi#175

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(peer): plain hyphen in Parent-stamp warning and test comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): reject blank remit before Parent stamp

Codex review on #1922: stamping first turned empty remits non-empty and
bypassed the idle-session guard. Also only treat ## Parent blocks as
already-present so bare /sessions citations still get the chip.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): gate New Session Create on hub defaults; require Parent chip

Codex tip review on #1922: block Create while getHubSettings is pending so
stock mount defaults cannot bypass restrictive hub peerSpawnDefaults; skip
Parent stamp only when ## Parent already has a markdown session chip; clarify
codex resume takes a native thread id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): resolve MCP spawn cwd; Agents settings section

Codex Major on #1922: spawn_peer relative directories now prefer the
launcher cwd (Codex --cd), then skillLookup, then session metadata.path.
Move peerSpawnDefaults UI into the unused Agents settings section.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): pass session cwd to MCP spawn_peer; Agents settings section

Codex tip review on #1922: resolve relative spawn_peer directories from the
launcher working directory (Codex --cd), skillLookup, then metadata.path;
move peerSpawnDefaults UI into the Agents settings section.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): skip archive on unread transcript; validate startingMode after hub agent

Codex tip review: do not archive a child when remit verification cannot read
the transcript; run AGY/DSH remote-only startingMode checks against the
resolved hub agent; update MCP bridge test expectations for workingDirectory.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): treat exhausted remit pagination as unverifiable

Hitting the messages page cap with hasMore still true must not count as an
observed-empty transcript — that would archive a child whose remit may still
exist on older pages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(cli): list HAPI_API_URL as optional with localhost default

Codex tip review: the README had moved HAPI_API_URL under Required even
though configuration defaults it to http://localhost:3006 (and settings can
supply a URL). Also drop trailing whitespace flagged by git diff --check.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): re-verify empty transcript before archiving failed spawn

An earlier observed-absent page plus later unread GETs must not archive a
child whose remit may have landed after the first empty read.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): never auto-archive on spawnPeer failure path

Timed-out remit POSTs and empty-read/archive TOCTOU races can still land
work on the child. Leave the session running with its id for inspection
instead of terminating an in-flight peer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): classify empty_session from final remit verify

An earlier empty transcript read must not stick as empty_session once
later message GETs become unavailable — report verify_failed instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): recognize escaped brackets in Parent chip labels

Titles with ] were escaped in the chip label but the idempotency regex
stopped at the first raw bracket and re-stamped a second Parent block.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): avoid Parent chip regex backtracking on backslashes

Exclude \\ from the single-char label alternative so escaped pairs have
one parse path — long nonmatching backslash runs no longer explode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli): MCP/CLI spawn_peer --machine for cross-host remits

Wire optional machine (UUID or hostname via GET /api/machines) through
MCP spawn_peer and hapi spawn-peer into existing spawnPeer({ machineId }).
Skip local directory existence checks for cross-host targets to match the
estate wrapper; keep fail-closed remit verify.

Fixes tiann/hapi#1931

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): reject ambiguous spawn --machine label matches

Tip bot Major on #1922: hostname/displayName selectors that match more
than one online runner now fail closed with ambiguous + UUID guidance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): preserve cross-OS absolute dirs for spawn --machine

path.resolve on the caller OS was rewriting foreign absolute roots
(C:\… on Linux, /srv/… on Windows) before the hub spawn POST. Detect
POSIX or Windows absolute paths and only resolve relative directories.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(peer): resolve spawn dirs after local vs remote is known

Defer directory resolution until the target machine is selected. Remote
spawns still preserve foreign absolute roots; local spawns use native
path.resolve so Windows drive-less paths like \repo bind to the caller
drive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): apply delayed DSH model defaults without replacing edits

* fix(web): preserve hub permission defaults across form remounts

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-11-02 13:17:22 +08:00

405 lines
22 KiB
Markdown

# hapi CLI
Choose a supported coding agent from your terminal and control its sessions remotely through the hapi hub. See the [supported agents](../docs/guide/agents.md).
## 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
1. Start the hub and set env vars (see ../hub/README.md).
2. Set the same CLI_API_TOKEN on this machine or run `hapi auth login`.
3. Run `hapi` to choose an agent, or `hapi <agent>` to start one directly.
4. Use the web app or Telegram Mini App to monitor and control.
## Commands
### Session commands
- `hapi` - Choose an agent interactively. Unavailable agents are shown with a reason and cannot be selected.
- `hapi claude` - Start a Claude Code session (passes through Claude CLI flags).
- `hapi codex` - Start Codex mode. See `src/codex/runCodex.ts`.
- `hapi codex resume <native-thread-id>` - Resume a Codex conversation by its native thread ID. For a HAPI session ID, use `hapi resume <id>`.
- `hapi cursor` - Start Cursor Agent mode. See `src/cursor/runCursor.ts`.
Supports `hapi cursor resume <chatId>`, `hapi cursor --continue`, `--mode plan|ask`, `--yolo`, `--model`.
Local and remote modes supported; new remote sessions use `agent acp`. Pre-ACP sessions retain the legacy `agent -p` stream-json resume path.
- `hapi grok` - Start Grok Build mode. See `src/grok/runGrok.ts`.
- `hapi copilot` - Start GitHub Copilot mode.
- `hapi kimi` - Start Kimi mode.
- `hapi agy` - Start Antigravity mode (remote-only).
- `hapi pi` - Start Pi mode (remote-only).
- `hapi opencode` - Start OpenCode mode via ACP. See `src/opencode/runOpencode.ts`.
Note: OpenCode supports local and remote modes; local mode streams via OpenCode plugins.
- `hapi dsh` - Start DeepSeek Harness through ACP. See `src/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 MCP `ping_peer` / `list_peers` over reinventing JWT+curl. Also `--message-file` / `--list`.
- `hapi spawn-peer --dir PATH --name TITLE --message-file -` - Spawn a session and deliver a required first message. Optional `--machine ID|hostname` targets another runner (resolved via GET /api/machines); `--dir` must exist on the target. Machine spawn HTTP 200 is not a working peer; this command fails if the remit does not land. Prefer MCP `spawn_peer`.
- `hapi usage [--since 24h|7d|<ISO>|<ms>] [--until …] [--json]` - When agents were working across every machine on the hub, from `GET /api/activity`. `--json` prints one `hapi.activity.v1` report for other tools (time trackers); errors go to stderr with exit codes 2 (arguments), 3 (no token or refused), 4 (hub too old), 1 (other).
- `hapi inspect-peer <session-id-or-prefix>` - Read-only peer metadata + recent message text (no resume). Prefer this or MCP `inspect_peer` when a user cites `[title](/sessions/<id>)` or Copy-reference `See session "…" (/sessions/<id>) for context`. `/sessions/<id>` is a hub path, not a local file. Optional `--limit`.
- `hapi set-mode <session-id-or-prefix> <mode>` - Change the permission mode of an existing session, like the web permission picker. Active sessions switch immediately; inactive ones use it on next resume. An invalid mode prints the modes allowed for that agent.
Peer messages default to `auto`: use the target's live state to steer an active
Codex turn when steering is available; otherwise queue, resuming an inactive
session if needed. Auto currently queues for all other agent flavors. Prefer
auto for routine collaboration when queueing is an acceptable fallback.
Use explicit `queue` for non-urgent updates that can wait. Use `steer` for
important, time-sensitive findings the peer needs to know now, not after its
current turn: a correction that invalidates its assumptions, a blocker, or a
result that changes its next action. State why it matters now and what the peer
should adjust. Steer requires insertion into an active Claude (remote), Pi, Codex, or Cursor ACP
turn; it does not abort that turn or guarantee an immediate response. MCP `ping_peer` accepts
the same optional `deliveryMode: "auto" | "queue" | "steer"` field. Auto is
resolved by the peer sender; ordinary web/native chat delivery is unchanged.
```bash
hapi ping-peer <session-id> 'The dependency is ready' --delivery-mode auto
hapi ping-peer <session-id> 'Run the next check' --delivery-mode queue
hapi ping-peer <session-id> 'Use the corrected input path' --delivery-mode steer
```
Queue success reports `accepted` with a `ping-peer-` localId, not confirmed
delivery. Delivery is shown in the recipient chat only after its runtime
acknowledges consumption. Steer messages retain the same peer identity prefix.
Steer rejects unsupported agents, idle targets, and inactive targets before
sending. It reuses the queued-message Steer action: success reports `steered`,
or `invoked` if the target consumed the message before the action arrived.
Auto uses Codex's `steeringActive` state and control capabilities, not just
`active` or `thinking`. If the turn changes after selection, auto uses the same
safe error handling as explicit steer; it does not resend or silently retry.
If steering fails after the message was accepted, the error includes its
`localId`; it may still be queued or already consumed. Inspect the target
before retrying; do not automatically resend the prompt. Steering does not
abort the current turn.
The picker lists agents alphabetically by command name. Use Up/Down and Enter
to choose; Esc or Ctrl-C cancels. It appears on every bare invocation, even
when only one agent is available. No default agent or selection is saved.
Scripts and non-interactive shells must specify an agent. Old implicit-Claude
commands such as `hapi --yolo`, `hapi --resume`, and `hapi "prompt"` now report
an error; use `hapi claude --yolo`, `hapi claude --resume`, or
`hapi claude "prompt"` instead.
`hapi --help`, `hapi -h`, and `hapi help` show only HAPI's help, without starting
a session or requiring an installed agent. `hapi --version` / `hapi -v` show
HAPI's version. Flags after an agent name are handled by that agent's existing
integration; supported flags vary by agent. HAPI does not translate or append
agent help text.
### Resume a remote session locally
```bash
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.
For Codex, the terminal and Web stay usable at the same time. Closing the
original terminal stops a terminal-started run, but its history remains
resumable. Sessions started from the Web run under the Runner; closing a
terminal attached later does not stop them. **End session** archives the
selected conversation. See [Codex usage and limits](../docs/guide/codex-shared-sessions.md)
for details.
### Answer local Claude prompts from HAPI
In a local Claude session started by `hapi claude`, main-session `AskUserQuestion`
questions and tool permission prompts can also be answered from the web app.
The terminal dialog stays usable; answering does not restart Claude or switch
the session to remote mode. Claude arbitrates terminal/web races, and HAPI
records the native result rather than assuming the web response won.
The bridge uses Claude's `PermissionRequest` hook, not a blocking
`PreToolUse` approval gate. An unanswered remote request expires after one
hour; expiration, disconnection, or bridge failure leaves the native prompt
available. Local answers/cancellation, session changes, and mode switches
withdraw stale web controls. Cancellation detection may wait for the next
transcript scan.
Verified with Claude Code **2.1.221**. Background subagents, `ExitPlanMode`,
and requests that cannot be unambiguously matched to a native tool call remain
terminal-only. Earlier Claude versions have not been verified.
### 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` - Replace any existing runner and start a detached process with the supplied flags/environment.
- `hapi runner stop` - Stop runner gracefully; agent sessions stay alive.
- `hapi runner start-sync` - Run in the foreground (for a process supervisor).
- `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 `/browse` page surfaces scoped file trees rooted at those paths.
- The runner refuses `list-directory` and `spawn-session` requests for paths outside the configured roots.
- The runner can serve read-only file, Git, and ripgrep requests for inactive
sessions through its machine-scoped `workspace-file-access` capability.
- `~` and `~/foo` are 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. See `src/codex/happyMcpStdioBridge.ts`.
- `hapi hub` - Start the bundled hub (single binary workflow).
- `hapi server` - Alias for `hapi hub`.
### Codex round timing
Completed live Codex turns persist their duration in the message stream, so Web
and PWA response details keep the same Round duration after refresh or a cached
tail reload. Local transcript timing uses the original start and completion
timestamps; imported history and turns without an observed live start do not
receive a new duration.
### 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. When a runner spawn supplies a Codex auth token, it copies only
`config.toml` into a temporary `CODEX_HOME` and writes the supplied `auth.json`,
preserving MCP settings without copying unrelated authentication state.
Without a supplied token, Codex uses the runner's normal Codex home/auth.
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 the official `dsh` CLI with `--profile acp` by default
(the ACP profile ships with `@deepseek-ai/dsh` since 0.2.0). Override the
executable or its arguments without shell parsing:
```bash
export HAPI_DSH_ACP_COMMAND=dsh-acp-demo
export HAPI_DSH_ACP_CONFIG=/path/to/deepseek-harness/examples/acp-agent/cordis.yml
hapi dsh
```
`HAPI_DSH_ACP_CONFIG` maps to `--config` for the configured server. Without
`HAPI_DSH_ACP_COMMAND` it instead applies to the official CLI, which has no
`--config` flag — the path is passed as a `--patch` overlay on top of
`--profile acp`.
For a source checkout, use JSON arguments:
```bash
export HAPI_DSH_ACP_COMMAND=pnpm
export HAPI_DSH_ACP_ARGS_JSON='["--dir", "/path/to/deepseek-harness", "run", "demo:acp"]'
```
The official ACP entrypoint is fresh-session-only and does not support native
resume, MCP injection, or live tool/reasoning telemetry. Model selection is
advertised for DSH: the composition's catalog is listed via the `ListDshModels`
session RPC and applied per turn with ACP `session/set_config_option`. HAPI
uses the standard chat and pending one-shot permission surfaces; the ACP
composition owns the overall permission policy.
### Required
- `CLI_API_TOKEN` - Shared secret; must match the hub. Can be set via env or `~/.hapi/settings.json` (env wins).
### Optional
- `HAPI_API_URL` - Hub base URL (default: http://localhost:3006; also configurable as `apiUrl` in settings).
- `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 the `extraHeaders` object in `~/.hapi/settings.json` (environment variable wins).
- `HAPI_CLAUDE_PATH` - Path to a specific `claude` executable.
- `HAPI_DSH_ACP_COMMAND` - ACP server executable for `hapi dsh` (default: `dsh` with `--profile acp`).
- `HAPI_DSH_ACP_CONFIG` - Optional config path for the configured DSH ACP server (`--config` with `HAPI_DSH_ACP_COMMAND`, `--patch` overlay on the default official CLI).
- `HAPI_DSH_ACP_ARGS_JSON` - Optional JSON array of ACP server arguments.
- `HAPI_HTTP_MCP_URL` - Default MCP target for `hapi 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).
- `HAPI_RUNNER_WEBHOOK_TIMEOUT_MS` - Session-start webhook timeout in ms (default: 15000); raise for slow agent startup/resume.
- `HAPI_DISABLE_VERSION_HANDOFF` - Set to `1` to disable automatic runner replacement on CLI binary changes.
- `HAPI_RUNNER_SUPERVISED` - Set to `1` only when a supervisor restarts the runner after exit; enables the web Restart control's supervised path.
### 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 current HAPI session ID, available inside agent shells. Use it in scripts that target the current conversation without listing sessions.
- An explicitly configured `HAPI_API_URL` is also made available to agent shells. HAPI does not copy settings-backed `CLI_API_TOKEN` secrets into the agent environment; credentials already present in the parent environment may still be inherited. Web Terminal PTYs (session Terminal button) strip hub secrets and agent-context markers (`HAPI_AGENT_CONTEXT`, `CURSOR_AGENT`, …) and set `HAPI_USER_TERMINAL=1` so operator tools treat that shell as human-facing.
For peer discovery and messaging, use the session's MCP `list_peers`,
`inspect_peer`, and `ping_peer` tools, or the corresponding CLI commands.
To create a new peer with work attached, use MCP `spawn_peer` or `hapi spawn-peer`.
The first message is required and verified in the transcript before success;
machine-spawn HTTP 200 alone does not prove the task was delivered. In-session
spawns add a Parent `[title](/sessions/<uuid>)` citation so parent renames do not
change its identity.
On a remote runner host, configure the matching hub URL and token so shell
commands reach the same hub (`hapi auth login` saves the token).
For example, this source-checkout helper displays an image in the current
session when MCP is unavailable:
```bash
bun scripts/tooling/hapi-display-image.mjs /absolute/path/to/image.png "optional title"
```
## Session lifecycle invariants
- Runner waits up to 30 seconds for Codex startup (15 seconds for other agents),
overridable with `HAPI_RUNNER_WEBHOOK_TIMEOUT_MS`. A Codex startup deadline
returns an unconfirmed result and keeps its process tracked; a late webhook
completes the same session. Wait for that row or explicitly stop it rather
than creating duplicates. Process exit and explicit stop still clean up the
child. The Hub gives Codex spawn RPCs 60 seconds for preflight plus startup.
When changing agent bootstrap, handoff, or shared-session plumbing:
- Handoff-capable integrations use `local` (terminal) and `remote` (web-controlled) ownership modes. Codex instead supports concurrent clients without ownership switching; see [Codex shared sessions](../docs/guide/codex-shared-sessions.md).
- Ordinary wrappers export `HAPI_SESSION_ID` after bootstrap. Shared Codex uses a per-root MCP bridge and `shell_environment_policy.set.HAPI_SESSION_ID`; never put one root's ID into the shared app-server environment. Implementation: `src/codex/shared/root.ts`, `src/codex/shared/runtime.ts`.
- Gemini remains a historical wire flavor, not a launchable integration. Use the [supported-agent guide](../docs/guide/agents.md) for the launchable set.
## Storage
Data is stored in `~/.hapi/` (or `$HAPI_HOME`):
- `settings.json` - User settings (machineId, token, onboarding flag). See `src/persistence.ts`.
- `runner.state.json` - Runner state (pid, port, version, heartbeat).
- `logs/` - Log files.
## Requirements
- Install and authenticate the agent you want to use. Claude CLI (`claude` on PATH) is required only for `hapi claude`.
- Cursor Agent CLI installed (`agent` on PATH) for `hapi cursor`. Install: `curl https://cursor.com/install -fsS | bash` (macOS/Linux), `irm 'https://cursor.com/install?win32=true' | iex` (Windows).
- Grok Build CLI installed (`grok` on PATH) for `hapi grok`. Authenticate with `grok login --device-auth` on headless runner machines, or set `XAI_API_KEY`.
- OpenCode CLI installed (`opencode` on PATH).
- Bun 1.4.4 for building from source.
## Build from source
From the repo root:
```bash
bun install
bun run build:cli # Type-check the CLI; no executable output
bun run --cwd cli build:exe # Host-platform executable in cli/dist-exe/<target>/
```
For an all-in-one binary that also embeds the web app:
```bash
bun run build:single-exe
```
## Large Codex thread recovery
Cold resumes and established internal subscriptions exclude inline turns; HAPI
reads history separately. The initial root subscription retains the existing
response behavior so empty native threads can initialize their history source.
This does not truncate the native rollout or reduce the context restored by Codex.
For exceptionally large threads, set `HAPI_CODEX_LAZY_HISTORY_IDS` to a
comma-separated list of native Codex thread IDs in the Runner environment before
starting the Runner. Only these roots replay their newest turn when reading
paginated history and skip child history replay. Other threads retain full replay;
legacy native threads without paginated history still use the full-history path.
Descendant discovery and subscriptions to loaded children are retained, including
after reconnect, so pending questions and live child events can still reach HAPI.
This option favors recovery latency over Hub history completeness. It does not
implement UI-driven pagination or asynchronous backfill. Older turns and child
traces missing from the Hub may remain missing after reconnect; messages with an
uncertain delivery state may need manual inspection if their acceptance evidence
is in an older turn. Native rollout files remain intact. Historical fork checks
still read complete history when the message-to-turn mapping is already known;
older messages without that mapping cannot be forked through the Hub. A single
turn can still be large, so this is a turn limit rather than a byte limit.
## Source structure
- `src/api/` - Hub 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).
## Releasing
From the repository root, `bun run release-all <version>` updates the CLI package,
shared app version, iOS `MARKETING_VERSION` (app and notification extension, all
configurations), and Android's default `versionName`, then commits, tags `v<version>`,
and pushes. It requires a clean working tree on `main`. Use `--dry-run` to preview
without changing files. Native build numbers (`CURRENT_PROJECT_VERSION` / `versionCode`)
remain managed separately; Android's `-PhapiVersionName` override is preserved.
The pushed tag triggers `.github/workflows/release.yml`, which builds all binaries,
runs `bun run --cwd cli publish-npm`, and creates the GitHub Release. npm publishing
uses [Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC), so no npm
token is involved; each `@twsxtd/hapi*` package must list `tiann/hapi` /
`release.yml` as its trusted publisher on npmjs.com. Packages already on npm are
skipped, so a failed release can be retried by re-running the workflow.
`setup-bun` reads the build version from the root `package.json`'s `packageManager`.
Keep local release builds on that version too. Bun 1.4.0 produces invalid
darwin-arm64 signatures that macOS 27 rejects with `SIGKILL`; Bun 1.4.4 includes
the [upstream signing fix](https://github.com/oven-sh/bun/pull/39837).
## Related docs
- `../hub/README.md`
- `../web/README.md`