* fix(web): filter Pi effort options by model thinkingLevelMap in new session form The create-session EffortField called getPiThinkingLevelOptions without the selected model's thinkingLevelMap, so the Pi effort select always showed the static off..high list: levels the model marks unsupported stayed visible and xhigh/max never appeared even for models that opt in. Pass the map through piSelectedModel (mirrors HappyComposer), and extend the stale-effort reset effect so a level unsupported by the newly selected model falls back to auto. * feat(cli): probe machine Pi models over RPC to carry thinkingLevelMap The machine-level Pi model probe parsed the `pi --list-models` text table, which only exposes provider/model/thinking-yes-no — thinkingLevelMap (and name/contextWindow) never reached the create-session form, so model-accurate thinking levels could not render there (xhigh/max are map-opt-in and were permanently hidden; see the companion web commit). Replace the table probe with a short-lived `pi --mode rpc` child (--no-session --no-extensions --no-skills --no-prompt-templates --no-tools) that issues get_available_models and reuses the session path's parsePiModels schema, so machine and session catalogs share one wire contract. The lightweight probe also measures faster than the table probe (~0.8s vs ~1.6-2.4s) and drops the whitespace-table parsing entirely. Cache/inflight dedupe/timeout structure is unchanged. * fix: address review findings on the Pi model probe and effort reset Three review findings on the RPC probe / effort-map change: - (high) Probe teardown killed only the direct child PID: with shell:true on Windows that is the shell, orphaning the interactive pi RPC process on every successful probe and on timeout; finish() also resolved before the process was confirmed gone. Use killProcessByChildProcess (taskkill /T on Windows, Unix tree kill, SIGKILL escalation) and settle only after the tree teardown completes. - (medium) An explicit get_available_models success:false response was discarded, so Pi's own error text was lost and the interactive child hung until the generic 15s timeout. parsePiModelsProbeLine now returns a three-way result (unrelated/models/error), also validating the response id, and the probe rejects immediately with Pi's error. - (medium) Switching an xhigh/max-capable model back to Default left the now-hidden effort in state and submitted it while the select visually fell back to auto. The reset effect now also covers model === 'auto' (undefined map) while still not resetting mid-resolve for a concrete model. Tests: probe-line failure/foreign-id cases; NewSession restore-to-Default reset and map-opt-in retention (the latter guards the former against a false green from the restore path). * fix(web): type the Pi model test mock as PiModelSummary The inline mock element type omitted thinkingLevelMap, so the new capability-driven tests failed typecheck (TS2353) and the required test job stopped before the unit tests ran. Use the shared PiModelSummary type so the mock cannot drift from the wire contract again. * fix(web): reconcile hidden Pi effort when model discovery fails A restored explicit Pi model never resolves when the machine catalog request fails, so piSelectedModel stays null for good. The reset effect required a resolved model, so it skipped reconciliation, while EffortField rendered with an undefined map and hid xhigh/max. Creation is only gated on the loading state, not on the error, so handleCreate could still forward the stale hidden level for Pi to reject or clamp. Treat a failed catalog as a settled selection (alongside Default and a resolved model) and reconcile against the undefined map; keep skipping the reset while a concrete model is still resolving without an error, so a restored xhigh/max survives until the map can prove it valid. Test asserts the spawn payload carries no effort after a failed catalog with a restored xhigh; verified it fails when the error branch is reverted. * fix(cli): honor a failed probe process-tree teardown killProcessByChildProcess reports survivors by resolving false, but the probe discarded that result and settled anyway. The Windows graceful path is taskkill /T without /F and escalates nothing on its own, so a probe child that refuses the signal would be reported as cleaned up and the catalog cached, letting interactive Pi processes accumulate across refreshes. Escalate to the forced teardown when the graceful one reports survivors, and reject (caching nothing, so the next call re-probes) when even that fails. Skip the check when the child has no pid: nothing can leak, and replacing a spawn ENOENT with a teardown error would only obscure the real failure. Adds probe lifecycle tests (spawn + teardown helper mocked) for the escalation path, the reject-and-do-not-cache path, the no-pid spawn-failure path, and the timeout path; verified they fail when the escalation is reverted. * fix(cli): keep extensions enabled for the Pi model probe --no-extensions silently dropped providers contributed through pi.registerProvider, which the old `pi --list-models` probe did list. Users with such an extension would have lost those models in the create-session form only. Verified with a project-local .pi/extensions provider in the same cwd: the probe with --no-extensions returned 29 models, while both the default run and the old table probe returned 30 including the extension's model (and its thinkingLevelMap). Dropping the flag restores parity; the extension provider now comes back with its map intact. Discovery that cannot contribute models stays disabled (--no-session, --no-skills, --no-prompt-templates, --no-tools). Cost: the probe now measures ~1.4-2.0s instead of ~0.65s, still at or below the old table probe (~1.6-2.4s) and fronted by the existing 60s cache. * fix(cli): probe Pi models from the home directory, not the runner cwd Under launchd/systemd the runner cwd is `/`, and starting Pi there is pathological: project discovery plus extensions that scan from the working directory walk the whole filesystem root. With extensions enabled (required so pi.registerProvider models still surface) the RPC probe took 16.8s at cwd=/, past the 15s timeout, so machine-level model discovery failed 100% of the time and the create-session form showed only 'Pi model discovery timed out'. Measured on macOS with 9 global extensions: cwd=/ extensions on 16.8s -> timeout cwd=/ extensions off 0.6s -> would lose extension providers cwd=$HOME extensions on 1.4s -> 29 models, complete The catalog is machine-scoped and does not depend on cwd, so probing from the home directory is both safe and representative. Verified from cwd=/ under the runner's exact launchd environment: 1.3-1.7s, 29 models. The old `pi --list-models` probe was immune because it never initialized a session; this regression arrived with the RPC probe and was missed because every earlier verification ran from a project directory. Tests assert the spawn cwd is homedir() and that --no-extensions stays absent; verified the cwd test fails when the option is removed. * fix(cli): keep the probe in the runner cwd, fall back to home only at a root Forcing every probe to the home directory fixed the launchd timeout but broke project-local discovery: a runner started inside a project stopped seeing that project's .pi/extensions providers, which the replaced --list-models probe did surface. Verified with a project-local provider: present from the project dir (30 models), absent from home (29). Use the runner cwd normally and fall back to home only when cwd is a filesystem root -- the launchd/systemd case where Pi startup walks the whole tree (16.8s, past PROBE_TIMEOUT_MS) and where there is no project to lose anyway. process.cwd() can also throw for a deleted directory, so that falls back to home too. Verified under the runner's launchd environment: from / -> home, 1.6s, 29 models; from a project dir -> that dir, 1.4s, 30 models including the project-local provider. Tests cover all three branches and were checked in both directions: pinning to home fails the project-cwd test, pinning to cwd fails the root test.
HAPI
Run official Claude Code / Codex / Cursor Agent / Grok Build / OpenCode / Kimi / Copilot / Antigravity / Pi 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, Kimi, Copilot, Antigravity, Pi—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
Native apps (iOS / Android)
Fully native SwiftUI and Kotlin Compose clients are in development under ios/ and android/. They pair with your hub by scanning the same terminal QR code as the web app, and follow the same protocol — see the client contract 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.