Files
hapi/cli
Junmo KimandGitHub 229766dd21 fix(agy): make conversation discovery deterministic at session start (#1369)
* refactor(agy): extract brain UUID adoption from the PreToolUse hook handler

Pull the first-wins UUID-adoption block out of onPreToolUse into a
standalone adoptBrainUuidIfUnset() helper so it can be shared with the
upcoming PreInvocation hook handler without duplicating the guard logic.
No behavior change.

* feat(agy): discover the brain UUID from agy's PreInvocation hook

PreToolUse only fires once a tool actually runs, so a tool-free turn
(e.g. a plain "hi") never gets a brain UUID from it. Register agy's
PreInvocation hook alongside PreToolUse: it fires before every model
call regardless of tool use, carries the same conversationId, and lets
discovery resolve deterministically instead of depending on a tool
being invoked.

PreInvocation uses agy's flat hook schema (distinct from PreToolUse's
grouped {matcher,hooks} shape) and a short 5s timeout, since it blocks
the agent loop synchronously on every model call. The forwarder gains
an explicit --event flag (default pre-tool-use, unchanged) to route to
a new /hook/agy-invocation endpoint; that path is fail-open (always
responds 200 / stdout "{}") since a lost discovery signal must never
block a model call, unlike a permission decision.

Both hooks funnel into the same first-wins UUID adoption guard, so a
resume-seeded sessionId is never overwritten by either.

* refactor(agy): drop transcript content-matching now that the hook is authoritative

The scanner's content-match discovery was the fallback for turns where
the PreToolUse hook never fired (no tool used). Now that PreInvocation
covers exactly that case, the fallback never actually gets a chance to
run in practice: carrier hook loading fails all-or-nothing (both events
live in the same hooks.json), and a failed carrier already aborts the
PTY session before discovery matters. Keeping unreachable code around
just keeps the risk it was flagged for — attaching to an unrelated agy
session that happens to share the same first prompt.

Removes the scan-window heuristics, the wrapped-USER_REQUEST content
matcher, and the ambiguity-reporting path entirely. The scanner is now
purely reactive: it watches nothing until onNewSession() (driven by a
hook) tells it which brain to watch. extractUserRequest/
normalizeUserInput and the launcher's userRequestMatches are untouched
— they answer a different question (did the web-submitted message echo
back into the PTY), which hook payloads carry no text to answer.

* feat(agy): drop the PreInvocation discovery hook once the conversation is identified

PreInvocation fires on every model call (~424ms round trip measured), but the
brain UUID only needs to be discovered once. agy re-reads hooks.json before
every model call, so the carrier's hooks.json can be rewritten in place (via
a temp-file-plus-rename atomic write) to drop the PreInvocation block the
moment handleSessionFound confirms the UUID, leaving PreToolUse untouched.

PreInvocation is restored before every respawn, since a resume that silently
fails would otherwise leave no way to discover the replacement conversation's
UUID. If the carrier itself has vanished (e.g. /tmp's tmpfiles.d sweep on a
long-lived session), it is rebuilt from scratch and hookCarrierDir is
repointed for the next agy spawn.

* refactor(cli): extract resolveHapiHomeDir from Configuration's constructor

Configuration.happyHomeDir is a singleton computed once at process
startup, which the upcoming agy carrier relocation can't reuse directly
without breaking per-test HAPI_HOME isolation. Extract the priority
logic into a standalone, env-injectable function with no behavior
change.

* feat(agy): relocate the hook carrier under HAPI_HOME and sweep dead ones

Carriers used to live under mkdtempSync(join(tmpdir(), 'hapi-agy-
carrier-')). On this machine /tmp is swept by tmpfiles.d after 30 days,
and agy re-reads hooks.json on every model call (not just at spawn), so
a long-lived session's carrier could be deleted out from under it,
silently killing both the permission bridge and discovery at once.

Move carriers to <HAPI_HOME>/agy-carriers/<random>/, record owner
metadata (pid, startedAt) at the carrier root (outside .agents/, which
agy itself reads), and sweep carriers whose owner process has
confirmed-died at session start. Liveness is judged strictly by
process.kill(pid, 0): ESRCH means dead and safe to remove, EPERM means
alive but not ours and must be preserved, anything else is unknown and
also preserved. Carriers with unreadable or missing owner metadata
(pre-existing or corrupted) are only swept once old enough to rule out
a carrier still mid-creation. Every ambiguous case defaults to
preservation, since deleting a live session's carrier is far more
costly than leaving an inert directory on disk.

* fix(agy): abort respawn instead of spawning agy without a permission bridge

syncPreInvocationHookForLaunch used to log-and-return when the hook
carrier could not be recreated before a respawn, letting launchOnce
spawn agy anyway with --dangerously-skip-permissions and no PreToolUse
hook wired up — every tool call would auto-approve with nobody in the
loop. Throw instead, matching runAgy.ts's existing fail-closed contract
for the initial carrier, and notify the web chat via sendSessionEvent
so the abort isn't silent.

* fix(agy): sweep carriers only when the owner is positively identified

An unreadable owner.json is not evidence of staleness: a live session
whose metadata cannot be parsed would have its carrier removed once it
aged past the threshold, taking the PreToolUse approval bridge with it.
Hostname is not an identity either — containers sharing a HAPI_HOME can
share a hostname while their PIDs live in unrelated namespaces, so a
liveness probe there reports ESRCH for a process that is very much alive.

Scope the owner record to the boot id and PID namespace on Linux, fall
back to a distinguishable hostname-only scope elsewhere, and delete only
when the scope matches and the pid is confirmed dead. Carriers whose
owner cannot be identified are now kept.

* fix(agy): drop the hostname scope fallback rather than guess ownership

Hostname is not an identity. Where /proc is unavailable, two machines or
containers sharing a HAPI_HOME and a hostname compute the same scope, so a
pid that is live on the owning system reads as ESRCH here and its carrier
is deleted — taking the PreToolUse approval bridge with it while agy runs
with --dangerously-skip-permissions.

Without a strong boot and PID-namespace identity the scope is now
undefined, which makes the sweep preserve everything. Orphaned carriers
accumulate on those platforms instead, which is the cheaper failure:
ordinary teardown still removes carriers, so only crash leftovers remain.

* fix(agy): point carrier failures at HAPI_HOME instead of the temp dir

The carrier moved under HAPI_HOME/agy-carriers earlier in this branch,
but the abort messages still told users to check the temporary directory.
On a custom or quota-limited HAPI_HOME that sends remediation to a
filesystem that has nothing to do with the failure.

Both the initial-preparation path and the respawn-recreation path carried
the stale hint, so both are updated — otherwise the same failure would
suggest two different places to look.
2026-08-05 07:56:40 +08:00
..
2025-12-16 15:03:50 +08:00
2025-12-16 15:03:50 +08:00
2025-12-16 15:03:50 +08:00
2026-01-03 22:22:45 +08:00
2025-12-16 15:03:50 +08:00
2026-01-04 20:45:15 +08:00
2026-08-04 08:33:19 +08:00

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

  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 start a session.
  4. 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). See src/index.ts.
  • hapi codex - Start Codex mode. See src/codex/runCodex.ts.
  • hapi codex resume <sessionId> - Resume existing Codex session.
  • 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; remote uses agent -p with stream-json.
  • hapi grok - Start Grok Build mode. See src/grok/runGrok.ts.
  • 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 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 over 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 MCP inspect_peer when a user cites [title](/sessions/<id>). 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 /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.
  • ~ and ~/foo are 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. See src/codex/happyMcpStdioBridge.ts.
  • hapi hub - Start the bundled hub (single binary workflow).
  • hapi server - Alias for hapi 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 the extraHeaders object in ~/.hapi/settings.json (environment variable wins).
  • HAPI_CLAUDE_PATH - Path to a specific claude executable.
  • 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).

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 MCP display_image tool for inline media when it is available; use HAPI_SESSION_ID for hub REST / shell tooling where MCP is not. To read another session, prefer MCP inspect_peer or hapi inspect-peer. To message another session, prefer MCP ping_peer or hapi ping-peer — do not reinvent JWT+curl. User citations look like [title](/sessions/<id>); pass that <id> as sessionIdPrefix.

    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). See src/persistence.ts.
  • runner.state.json - Runner state (pid, port, version, heartbeat).
  • logs/ - Log files.

Requirements

  • Claude CLI installed and logged in (claude on PATH).
  • 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 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).
  • ../hub/README.md
  • ../web/README.md