* fix(cli): cap and back off consecutive remote launch failures claudeRemoteLauncher's respawn loop retried claudeRemote() immediately on every throw with no backoff or limit. A deterministic launch failure (bad auth, invalid model/args, spawn failure) respawned in a tight loop instead of giving up, hammering the same failure forever. Track whether onReady() fired at least once per attempt to tell an immediate/deterministic failure apart from a failure after real progress, back off between immediate-failure retries, and after 3 consecutive immediate failures drop the message that keeps triggering them and reset the streak, instead of respawning forever. The session keeps running so a later, unrelated message still gets its own budget. The streak reset on a non-throwing attempt is itself gated on having reached onReady, not applied unconditionally -- otherwise a message that keeps getting parked and re-picked-up on alternating attempts (e.g. an isolated command hitting the same deterministic failure) would reset the streak every other attempt and the cap would never fire. * fix(cli): restore queued message when remote launch fails before delivery MessageQueue2.collectBatch() acks a message (fires onBatchConsumed, which the hub uses to mark it consumed) at dequeue time, before the message ever reaches the SDK. If claudeRemote() then throws before onReady -- e.g. the process dies right after picking up the message -- the catch block only logged and retried, so the message vanished: the hub already thinks it was delivered, but the CLI never acted on it. Track the message returned from nextMessage() (whether freshly dequeued or held in `pending` across a mode change) as in-flight until the next onReady confirms it was handled, and restore it to the front of the queue (preserving isolation via unshiftIsolated when needed) if the attempt throws and will be retried. Restoring happens even if the throw races with a user-initiated switch/exit, so a message is not silently dropped by that unrelated shutdown either. When the immediate-failure cap from the previous commit is reached, the in-flight message is dropped instead of restored: unshifting it back would just feed it into another immediate failure on the very next attempt, storming again. This mirrors cursorLegacyRemoteLauncher's existing drop-and-reset policy on its own consecutive-failure cap. * fix(cli): preserve localId when restoring a failed message batch MessageQueue2.collectBatch() already collects each queue item's localId (it fires onBatchConsumed with the full list to ack them), but only exposed the joined `message` string to callers, discarding the per-item localIds and their original boundaries in the process. When claudeRemoteLauncher restores a dequeued-but-undelivered batch after a launch failure, it re-added the joined string as a single new queue item with no localId, orphaning the retried prompt from the hub row(s) it originated from (and from cancel-by-localId). Expose the pre-join `items` breakdown (message + localId per item) alongside the existing joined `message` field on collectBatch()/waitForMessagesAndGetAsString() -- purely additive, so the other callers of waitForMessagesAndGetAsString() (grok, kimi, opencode, cursor, codex, runAgentSession) are unaffected. On restore, unshift each original item individually in reverse order, so the localId and relative order of a multi-message batch are both preserved instead of just the first item's. * fix(cli): reset immediate-failure streak on a delivered non-onReady success claudeRemote.ts's /clear handling delivers the queued message to the SDK, then calls onSessionReset()/onCompletionEvent() and returns successfully without ever calling onReady(). The success-path streak reset only cleared on reachedReadyThisAttempt, so a successful /clear between two unrelated immediate launch failures did not reset the streak: an unrelated message's very next failure could hit the 3-in-a-row cap after just 1 failure, and the resulting banner would misreport "3 times in a row". Track whether nextMessage() actually handed a message to the SDK this attempt (deliveredMessageThisAttempt), separately from whether the attempt reached onReady, and reset the streak on either signal. The livelock-prone case this guards against (a message parked into `pending` and the attempt returning without ever delivering anything) leaves both flags false, so it still does not reset the streak.
HAPI
Run official Claude Code / Codex / Cursor Agent / Grok Build / OpenCode sessions locally and control them remotely through a Web / PWA / Telegram Mini App.
Why HAPI? HAPI is a local-first alternative to Happy. See Why Not Happy? for the key differences.
Features
- Seamless Handoff - Work locally, switch to remote when needed, switch back anytime. No context loss, no session restart.
- Native First - HAPI wraps your AI agent instead of replacing it. Same terminal, same experience, same muscle memory.
- AFK Without Stopping - Step away from your desk? Approve AI requests from your phone with one tap.
- Your AI, Your Choice - Claude Code, Codex, Cursor Agent, Grok Build, OpenCode—different agents, one unified workflow.
- Terminal Anywhere - Run commands from your phone or browser, directly connected to the working machine.
- Voice Control - Talk to your AI agent hands-free using the built-in voice assistant.
- Workspace Browser - Opt-in via one or more
hapi runner start --workspace-root <path>flags: browse scoped file trees from the web and start sessions in allowed subdirectories.
Demo
https://github.com/user-attachments/assets/38230353-94c6-4dbe-9c29-b2a2cc457546
Getting Started
npx @twsxtd/hapi hub --relay # start hub with E2E encrypted relay
npx @twsxtd/hapi # run claude code
hapi server remains supported as an alias.
The terminal will display a URL and QR code. Scan the QR code with your phone or open the URL to access.
The relay uses WireGuard + TLS for end-to-end encryption. Your data is encrypted from your device to your machine.
For self-hosted options (Cloudflare Tunnel, Tailscale), see Installation
Docs
Build from source
bun install
bun run build:single-exe
Credits
HAPI means "哈皮" a Chinese transliteration of Happy. Great credit to the original project.