* feat(cli): add diff-shape tool input canonicalization helper
* fix(cli): canonicalize ACP diff-shaped tool inputs for the Edit/Write views
* fix(opencode): normalize local-mode edit/write tool parts to the diff view shapes
* fix(opencode): preserve replaceAll and fix id-less hook pairing
- canonicalizeDiffToolInput now carries OpenCode's optional replaceAll through
to the canonical Edit input as replace_all, so persisted history keeps
whether an edit replaced one occurrence or all.
- opencodeLocalLauncher computes the execute-hook pairing signature from the
raw name/input BEFORE canonicalizing, so an id-less 'before' with empty or
partial args shares a queue key with the full 'after'.
* fix(opencode): preserve snake_case replace_all in edit canonicalization
canonicalizeDiffToolInput accepts already-canonical snake_case edit inputs
(file_path/old_string/new_string), but only read camelCase replaceAll. Read
replace_all too so the execution argument survives every call site.
* fix(opencode): upgrade partial local edit/write calls when full args arrive
A partial native before ({filePath}) was emitted as-is and recorded as sent,
so the later canonical full input was deduped and the web Edit/Write view never
received the complete arguments. Track the emitted input per callId and, when
a partial native edit/write call is superseded by a canonical full input, emit
a replacement tool-call (same callId) before the result. Applies to both the
message.part.updated and tool.execute.* local event paths.
* fix(opencode): release completed tool inputs from upgrade tracking
emittedToolInputs retained the full emitted Write content and Edit old/new
strings for the whole launcher lifetime. Once a call is completed
(sentToolResults), later upgrades are already blocked, so drop the stored
payload in both result paths. Add a lifecycle test that a stale part for a
completed callId neither re-emits nor emits a duplicate result.
* style(opencode): restore 4-space indentation on local launcher tracking decls
The per-session tracking declarations (sentTextParts, sentToolCalls,
sentToolResults, emittedToolInputs) were flush-left inside opencodeLocalLauncher,
violating the repo's 4-space indentation rule.
* fix(opencode): gate diff canonicalization on edit/write semantics
canonicalizeDiffToolInput now takes the tool's semantic name/kind and only
normalizes known edit/write aliases (edit, write, write_file, replace, modify,
file_edit). The Edit shape (path + both old/new) is unambiguous on its own, but
Write is just {path, content} — a shape many non-edit MCP tools also accept —
so without the gate those tools were renamed to Write and their extra args
dropped. OpenCode maps both edit and write to kind 'edit', which stays in the
allow-list, so the native-shape cases keep working. ACP callers pass the update
kind (falling back to the derived name); local callers pass the tool name.
* fix(opencode): re-emit canonical edit/write when the input changes
shouldEmit only handled the null->canonical transition, so a shape-complete
Write/Edit that already canonicalizes (e.g. {content: ""}) was suppressed
when the final content arrived, leaving the web diff with stale input. Emit on
first usable input and re-emit whenever the canonical input value changes,
across both message.part.updated and tool.execute.* paths.
* test(opencode): cover changed-input re-emit on the message.part.updated path
The execute-hook path already had a regression for canonical inputs changing
from empty to final content; add the matching case for message.part.updated so
both local emit paths are covered.
* fix(web): render opencode native read output as a file content code block
OpenCode's lowercase read tool fell through to the generic markdown
result view, collapsing the whole file into a soft-wrapped paragraph
and dropping the <path>/<type>/<content> markup. Register the read
alias, unwrap the opencode payload (including the trailing
"(End of file ...)" trailer), parse its 1-indexed line numbering into
the code block gutter, and resolve the camelCase filePath key in the
card title.
* fix(web): only unwrap complete opencode read envelopes
Anchor the envelope match to the full <path>/<type>file/<content>
structure so plain Read results whose file text contains a literal
<content> element are left untouched, capture the body greedily so an
embedded </content> survives, and stop trimming real trailing
whitespace.
hapi CLI
Choose a supported coding agent from your terminal and control its sessions remotely through the hapi hub. See the supported agents.
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 choose an agent, orhapi <agent>to start one directly. - 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. Seesrc/codex/runCodex.ts.hapi codex resume <native-thread-id>- Resume a Codex conversation by its native thread ID. For a HAPI session ID, usehapi resume <id>.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; new remote sessions useagent acp. Pre-ACP sessions retain the legacyagent -pstream-json resume path.hapi grok- Start Grok Build mode. Seesrc/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. 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.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.
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
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 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
/browsepage surfaces scoped file trees rooted at those paths. - The runner refuses
list-directoryandspawn-sessionrequests 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-accesscapability. ~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 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:
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:
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 asapiUrlin 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 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:dshwith--profile acp).HAPI_DSH_ACP_CONFIG- Optional config path for the configured DSH ACP server (--configwithHAPI_DSH_ACP_COMMAND,--patchoverlay on the default official CLI).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).HAPI_RUNNER_WEBHOOK_TIMEOUT_MS- Session-start webhook timeout in ms (default: 15000); raise for slow agent startup/resume.HAPI_DISABLE_VERSION_HANDOFF- Set to1to disable automatic runner replacement on CLI binary changes.HAPI_RUNNER_SUPERVISED- Set to1only 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_URLis also made available to agent shells. HAPI does not copy settings-backedCLI_API_TOKENsecrets 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 setHAPI_USER_TERMINAL=1so 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.
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:
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) andremote(web-controlled) ownership modes. Codex instead supports concurrent clients without ownership switching; see Codex shared sessions. - Ordinary wrappers export
HAPI_SESSION_IDafter bootstrap. Shared Codex uses a per-root MCP bridge andshell_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 for the launchable set.
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
- Install and authenticate the agent you want to use. Claude CLI (
claudeon PATH) is required only forhapi claude. - 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 # 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:
bun run build:single-exe
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 (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.
Related docs
../hub/README.md../web/README.md