11 KiB
AGENTS.md
Work style: telegraph; noun-phrases ok; drop grammar;
Short guide for AI agents in this repo. Prefer progressive loading: start with the root README, then package READMEs as needed.
What is HAPI?
Local-first platform for running AI coding agents with remote control via web/phone. CLI wraps agents and connects to hub; hub serves web app and handles real-time sync. See docs/guide/agents.md for launchable agents; Gemini remains a historical wire flavor, not a launchable integration.
Repo layout
cli/ - CLI binary, agent wrappers, runner daemon
hub/ - HTTP API + Socket.IO + SSE + Telegram bot
relay/ - Standalone encrypted native push relay (APNs + FCM)
web/ - React PWA for remote control
ios/ - Native SwiftUI app (in development)
android/ - Native Kotlin Compose app (in development)
shared/ - Common types, schemas, utilities
shared/fixtures/ - Golden chat fixtures, generated from web pipeline (never hand-edit)
docs/ - VitePress documentation site
website/ - Marketing site
Bun workspaces: cli, shared, hub, web, website, docs, relay. shared consumed by cli, hub, web as @hapi/protocol. ios/android outside workspaces (Xcode / Gradle toolchains).
Architecture overview
┌─────────┐ Socket.IO ┌─────────┐ SSE/REST ┌─────────┐
│ CLI │ ──────────── │ Hub │ ──────────── │ Web │
│ (agent) │ │ (server)│ │ (PWA) │
└─────────┘ └─────────┘ └─────────┘
│ │ │
├─ Wraps Claude/Codex ├─ SQLite persistence ├─ TanStack Query
├─ Socket.IO client ├─ Session cache ├─ SSE for updates
└─ RPC handlers ├─ RPC gateway └─ assistant-ui
└─ Telegram bot
Data flow:
- CLI starts the selected agent integration, connects to hub via Socket.IO
- Agent events → CLI → hub (socket
messageevent) → DB + SSE broadcast - Web subscribes to SSE
/api/events, receives live updates - User actions → Web → hub REST API → RPC to CLI → agent
Web terminals use a separate JWT-authenticated Socket.IO /terminal namespace; /cli uses the CLI access token.
Reference docs
README.md- User overview, quick startcli/README.md- CLI commands, config, runnerhub/README.md- Hub config, HTTP API, Socket.IO eventsweb/README.md- Routes, components, hooksdocs/guide/- User guides (installation, how-it-works, FAQ)
Shared rules
- No backward compatibility: breaking old formats freely
- Prioritize Pragmatism, and Avoid Overengineering.
- Write necessary tests ONLY.
- TypeScript strict; no untyped code
- Bun workspaces; run
buncommands from repo root - Path alias
@/*maps to./src/*per package - Prefer 4-space indentation
- Zod for runtime validation (schemas in
shared/src/schemas.ts)
Common commands (repo root)
bun typecheck # cli + hub + web + relay (shared checked through consumers)
bun run test # cli + hub + web + shared + relay tests
bun run dev # hub + web concurrently
bun run build:single-exe # All-in-one binary
bun run gen:fixtures # Regenerate shared/fixtures/ from web pipeline
cd android && ./gradlew :core:protocol:test # Android protocol conformance
iOS tests run in CI (ios.yml: macOS swift test); no local Xcode/Swift toolchain assumed.
Key source dirs
CLI (cli/src/)
api/- Hub connection (Socket.IO client, auth)claude/- Claude Code integration (wrapper, hooks)codex/- Codex mode integrationagent/- Shared session/bootstrap support and ACP transportrunner/- Background daemon for remote spawncommands/- CLI subcommands (auth, runner, doctor)modules/- Tool implementations (ripgrep, difftastic, git)ui/- Terminal UI (Ink components)
Hub (hub/src/)
web/routes/- REST API endpointssocket/- Socket.IO setupsocket/handlers/cli/- CLI event handlers (session, terminal, machine, RPC)sync/- Core logic (sessionCache, messageService, rpcGateway)store/- SQLite persistence (bun:sqlite)sse/- Server-Sent Events managertelegram/- Bot commands, callbacksnotifications/- Notification dispatch and payload compositionpush/,fcm/,push-ios/- Web Push, Android, and iOS deliverypush-native/- Shared encrypted envelope and push relay clientconfig/- Settings loading, token generationvisibility/- Client visibility tracking
Web (web/src/)
routes/- TanStack Router pagesroutes/sessions/- Session views (chat, files, terminal)components/- Reusable UI (SessionList, SessionChat, NewSession/)hooks/queries/- TanStack Query hookshooks/mutations/- Mutation hookshooks/useSSE.ts- SSE subscriptionapi/client.ts- API client wrapper
Shared (shared/src/)
types.ts- Core types (Session, Message, Machine)schemas.ts- Zod schemas for validationsocket.ts- Socket.IO event typesmessages.ts- Message parsing utilitiesmodes.ts- Permission/model mode definitions
iOS (ios/)
Packages/HapiKit/- local SPM package:HapiProtocol(wire models + chat pipeline, fixtures-verified),HapiClient(API/auth/SSE/stores),HapiUI(rendering)Hapi/+Hapi.xcodeproj- SwiftUI app, native transcript, feature screens, push extension
Android (android/)
:core:protocol- pure JVM wire types + chat pipeline (fixtures-verified):core:data- transport (OkHttp/SSE), auth, stores:app- Compose UI, navigation, deep links, FCM
Protocol conformance (native apps)
shared/fixtures/**machine-generated from the web chat pipeline (source of truth). NEVER hand-edit; editweb/scripts/fixtures/cases/+ regenerate.- Changing
web/src/chat/**,web/src/lib/message-window-store.ts, orweb/src/lib/sessionPatch.ts: runbun run gen:fixtures, commit the diff. CI enforces (.github/workflows/fixtures.yml); fixture diffs auto-trigger iOS/Android conformance suites (ios.yml/android.yml). - Native client contract docs:
docs/api/client-contract/(auth, rest, sse, pagination, messages, errors). - Tracks:
ios/(SwiftUI, iOS 17+) +android/(Kotlin Compose, minSdk 26) — independent codebases, share only contract + fixtures. Plan:~/.claude/plans/web-pwa-abundant-yeti.md.
Pre-push self-review (agents)
Before commit/push/PR, run the repository checks directly:
- Mechanical:
bun typecheck && bun run test(typecheck/unit-test portion of.github/workflows/test.yml; CI also runs selected Playwright and CLI integration tests) - Logic: skim
git diff origin/main...HEAD; apply.github/prompts/codex-pr-review.mdas a local Major checklist (no Codex required) - Style: optional
Testing
- Test frameworks: Vitest for CLI/Web; Bun test for Hub/Shared/Relay (via
bun run test) - Test files:
*.test.tsnext to source - Run:
bun run test(from root) orbun run test(from package) - Hub tests:
hub/src/**/*.test.ts - CLI tests:
cli/src/**/*.test.ts - Web tests:
web/src/**/*.test.{ts,tsx}(fixtures self-check:web/src/chat/fixtures.test.ts)
Common tasks
| Task | Key files |
|---|---|
| Add CLI command | cli/src/commands/, register in cli/src/commands/registry.ts; public help in help.ts |
| Add API endpoint | hub/src/web/routes/, register in hub/src/web/server.ts |
| Add Socket.IO event | hub/src/socket/handlers/cli/, shared/src/socket.ts |
| Add web route | web/src/routes/, web/src/router.tsx |
| Add web component | web/src/components/ |
| Modify session logic | hub/src/sync/sessionCache.ts, hub/src/sync/syncEngine.ts |
| Modify message handling | hub/src/sync/messageService.ts |
| Add notification type | hub/src/notifications/ |
| Add shared type | shared/src/types.ts, shared/src/schemas.ts |
Important patterns
- RPC: CLI registers handlers (
rpc-register), hub routes requests viarpcGateway.ts - Versioned updates: CLI sends
update-metadata/update-statewith version; hub rejects stale - Session modes:
local(terminal) vsremote(web-controlled) for handoff-capable integrations; Codex uses concurrent clients without ownership switching. Seedocs/guide/codex-shared-sessions.md. - Session identity: 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 (cli/src/codex/shared/root.ts,runtime.ts). - Permission modes: Per-flavor catalogs in
shared/src/modes.ts; session capabilities further constrain available controls - Namespaces: Multi-user isolation via
CLI_API_TOKEN:<namespace>suffix
Adding new web features — consider an FUE
When you ship a non-essential feature (the 20% of sessions, not the 80%), consider wrapping its affordance in the generic First-User-Experience primitive so existing users discover it without a giant always-visible UI block.
- Hook:
web/src/lib/use-fue.ts—useFue(featureId)returns{ status, engage, dismiss }. Storage namespacehapi.fue.v1.<featureId>(one localStorage key per feature, isolated from any upstream onboarding flow). - Components:
web/src/components/Fue.tsx—<FueDot>(small pulsing badge for the affordance) and<FueCallout>(portal-rendered popover with title/body + "Got it" affirmative-action dismiss).
Pattern (~10 lines around the affordance):
const fue = useFue('my-feature')
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<>
<button ref={buttonRef} onClick={() => { fue.engage(); doThing() }}>
<Icon />
{fue.status !== 'acknowledged' ? <FueDot pulsing={fue.status === 'unseen'} /> : null}
</button>
{fue.status === 'engaging' ? (
<FueCallout
title={t('myFeature.fueTitle')}
body={t('myFeature.fueBody')}
onDismiss={fue.dismiss}
anchorRef={buttonRef}
/>
) : null}
</>
)
Rules:
- Affirmative action only: there is no auto-timeout — user dismisses by clicking "Got it" (reading speed varies).
- The FUE dot and any feature-specific badge (e.g. an entry counter) should be mutually exclusive: onboarding signal beats inventory signal until acknowledged.
- Storage is opt-in per-feature; if upstream ships its own onboarding for a feature, just don't wrap that affordance.
Canonical example: scratchlist toggle in web/src/components/AssistantChat/ComposerButtons.tsx (ScratchlistToggleButton).
Critical Thinking
- Fix root cause (not band-aid).
- Unsure: read more code; if still stuck, ask w/ short options.
- Conflicts: call out; pick safer path.
- Unrecognized changes: assume other agent; keep going; focus your changes. If it causes issues, stop + ask user.