From 4f03f29ac313dfb6c420fee78b8bf6b73dc231f3 Mon Sep 17 00:00:00 2001 From: weishu Date: Sun, 21 Dec 2025 18:49:04 +0800 Subject: [PATCH] docs: rebrand Happy to HAPI and add component documentation This commit rebrands the project from "Happy" to "HAPI" throughout the codebase, including documentation, comments, logs, and tool references. It also adds comprehensive README files for the server and web components, clarifies the monorepo structure in AGENTS.md and root README.md, and removes the outdated roadmap.md file. Changes include: - Rebrand references from Happy to HAPI in CLI, server, and web components - MCP tool names updated from mcp__happy__ to mcp__hapi__ - Process/service names updated consistently - New server/README.md with deployment and configuration guide - New web/README.md with stack and development instructions - Updated root README.md with quickstart guide - Updated AGENTS.md with cleaner structure documentation - Removed cli/roadmap.md (now superseded by documentation) --- AGENTS.md | 108 ++---- README.md | 81 ++++- cli/CLAUDE.md | 30 +- cli/README.md | 106 +++--- cli/package.json | 2 +- cli/roadmap.md | 353 ------------------- cli/src/api/apiMachine.ts | 2 +- cli/src/claude/claudeLocal.ts | 2 +- cli/src/claude/registerKillSessionHandler.ts | 2 +- cli/src/claude/runClaude.ts | 16 +- cli/src/claude/sdk/index.ts | 4 +- cli/src/claude/sdk/utils.ts | 14 +- cli/src/claude/utils/startHappyServer.ts | 12 +- cli/src/claude/utils/systemPrompt.ts | 10 +- cli/src/codex/codexMcpClient.ts | 2 +- cli/src/codex/happyMcpStdioBridge.ts | 16 +- cli/src/codex/runCodex.ts | 8 +- cli/src/commands/auth.ts | 2 +- cli/src/configuration.ts | 6 +- cli/src/daemon/CLAUDE.md | 38 +- cli/src/daemon/controlClient.ts | 10 +- cli/src/daemon/daemon.integration.test.ts | 22 +- cli/src/daemon/doctor.ts | 8 +- cli/src/daemon/install.ts | 4 +- cli/src/daemon/mac/install.ts | 22 +- cli/src/daemon/mac/uninstall.ts | 6 +- cli/src/daemon/run.ts | 18 +- cli/src/daemon/uninstall.ts | 4 +- cli/src/persistence.ts | 5 +- cli/src/ui/doctor.ts | 6 +- cli/src/ui/logger.ts | 4 +- cli/src/utils/spawnHappyCLI.ts | 24 +- server/README.md | 75 ++++ server/package.json | 2 +- server/src/configuration.ts | 4 +- server/src/index.ts | 8 +- server/src/sync/syncEngine.ts | 6 +- server/src/telegram/bot.ts | 32 +- server/src/telegram/renderer.ts | 2 +- web/README.md | 37 ++ web/src/chat/normalize.ts | 2 +- web/src/chat/reducer.ts | 4 +- web/src/components/ToolCard/knownTools.tsx | 2 +- web/src/index.css | 4 +- 44 files changed, 442 insertions(+), 683 deletions(-) delete mode 100644 cli/roadmap.md create mode 100644 server/README.md create mode 100644 web/README.md diff --git a/AGENTS.md b/AGENTS.md index 44af2544..7f806235 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,87 +1,35 @@ # AGENTS.md -Guidelines for AI agents working in this workspace. +Short guide for AI agents in this repo. Prefer progressive loading: start with the root README, then package READMEs as needed. -## Monorepo Structure +## Repo layout +- `cli/` - hapi CLI, daemon, Codex/MCP tooling +- `server/` - Telegram bot + HTTP API + Socket.IO + SSE +- `web/` - React Mini App / PWA -Bun workspaces monorepo with three packages: +## Reference docs +- `README.md` (user overview) +- `cli/README.md` (CLI behavior and config) +- `server/README.md` (server setup and architecture) +- `web/README.md` (web app behavior and dev workflow) +- `localdocs/` (optional deep dives) -| Package | Path | Purpose | -|---------|------|---------| -| `hapi` | `cli/` | CLI tool (single executable) - `hapi` / `hapi mcp` | -| `hapi-server` | `server/` | API server + Telegram bot (serves `web/dist/`) | -| `hapi-web` | `web/` | Web frontend / Mini App / PWA (Vite → `dist/`) | +## Shared rules +- TypeScript strict; no untyped code. +- Bun workspaces; run `bun` commands from repo root. +- Path alias `@/*` maps to `./src/*` per package. +- No backward compatibility: breaking format changes are allowed. +- Prefer 4-space indentation. -``` -hapi/ -├── package.json # Workspace root -├── tsconfig.base.json # Shared TS config -├── cli/ -│ ├── package.json # npm: hapi -│ ├── src/ # CLI source -│ └── bin/ # Executables -├── server/ -│ ├── package.json # private -│ └── src/ # Hono + Socket.IO + Grammy -└── web/ - ├── package.json # private - ├── vite.config.ts - └── src/ # React + Tailwind -``` +## Common commands (repo root) + bun run build + bun run build:single-exe + bun run typecheck + bun run dev:server + bun run dev:web + bun run test -## Shared Rules - -- **TypeScript strict** - No untyped code -- **Bun workspaces** - Run `bun install` / `bun run ...` from repo root -- **Path aliases** - `@/*` → `./src/*` (per-package tsconfig) -- **No backward compatibility** - Break old formats freely -- **4-space indent** - Prefer 4 spaces - -## Commands - -Run from repo root: - -```bash -# Build -bun run build # Build all packages -bun run build:cli # CLI only (pkgroll → CJS + ESM) -bun run build:server # Server only (bun build) -bun run build:web # Web only (Vite) - -# Type check -bun run typecheck # All packages - -# Development -bun run dev:server # Server with --watch -bun run dev:web # Vite dev server - -# Test -bun run test # CLI tests (Vitest) -``` - -## Package Details - -### cli/ (hapi) -- **Entry**: `src/index.ts` → `bin/happy.mjs` -- **Build**: `pkgroll` generates CJS + ESM bundles -- **Test**: `vitest` with `.env.integration-test` -- **Publish**: `npm publish` (see `cli/package.json`) - -### server/ (hapi-server) -- **Entry**: `src/index.ts` -- **Runtime**: Bun -- **Stack**: Hono (HTTP) + Socket.IO + Grammy (Telegram) -- **Static**: Serves `../web/dist/` via `src/web/server.ts` - -### web/ (hapi-web) -- **Entry**: `src/main.tsx` -- **Stack**: React 19 + Tailwind + Vite -- **Output**: `dist/` (served by server) - -## Key Source Directories - -| Package | Key Paths | -|---------|-----------| -| cli | `src/api/`, `src/claude/`, `src/commands/`, `src/codex/` | -| server | `src/web/`, `src/socket/`, `src/telegram/`, `src/sync/` | -| web | `src/components/`, `src/api/`, `src/hooks/` | +## Key source dirs +- `cli/src/api/`, `cli/src/claude/`, `cli/src/commands/`, `cli/src/codex/` +- `server/src/web/`, `server/src/socket/`, `server/src/telegram/`, `server/src/sync/` +- `web/src/components/`, `web/src/api/`, `web/src/hooks/` diff --git a/README.md b/README.md index 682e7b1a..027c4c96 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,78 @@ # hapi -Monorepo with three Bun workspace packages: +HAPI means "哈皮," a Chinese transliteration of [happy](https://github.com/slopus/happy), great credit to the original Happy project. -- `cli/` (`hapi`): CLI (single executable) exposing `hapi` / `hapi mcp` -- `server/` (`hapi-server`): backend API + Telegram bot server, serves `web/dist/` -- `web/` (`hapi-web`): web frontend (Vite) building to `web/dist/` +Run Claude Code / Codex / Coding Agent sessions locally and control them remotely through a Web / PWA / Telegram mini App. -## Commands +## Quickstart (single executable) -Run from the repo root: +1. Download the prebuilt `hapi` binary for your platform and put it on your PATH. + +2. Start the server on a machine you control: ```bash -bun install -bun run typecheck -bun run build -bun run build:cli:exe -bun run build:cli:exe -- --target bun-darwin-x64 +export TELEGRAM_BOT_TOKEN="..." +export ALLOWED_CHAT_IDS="12345678" +export CLI_API_TOKEN="shared-secret" +export WEBAPP_URL="https://your-domain.example" # required for Telegram Mini App + +hapi server ``` + +3. If the server has no public IP, expose it over HTTPS: +- Cloudflare Tunnel docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/ +- Tailscale docs: https://tailscale.com/kb/ +- Telegram Mini Apps require HTTPS. + +4. Run the CLI on the machine where you want sessions: + +```bash +# If the server is not on localhost:3006 +export HAPI_BOT_URL="https://your-domain.example" + +hapi +``` + +The CLI will prompt for `CLI_API_TOKEN` and save it locally. + +5. Open the UI: +- In Telegram, run `/app` in the bot chat. +- In a browser, open `WEBAPP_URL` and log in with `CLI_API_TOKEN`. + +## CLI config file +You can store the token in `~/.config/hapi/settings.json` instead of an env var. +Environment variables take priority over the file. + +Example: +```json +{ + "cliApiToken": "shared-secret" +} +``` + +## Requirements +- Claude CLI installed and logged in (`claude` on PATH). +- A Telegram bot token from @BotFather (for Mini App access). +- Bun if building from source. + +## Build from source +```bash +bun install +bun run build +``` + +Build a single binary with embedded web assets: +```bash +bun run build:single-exe +``` + +Build CLI-only binaries: +```bash +bun run build:cli:exe +bun run build:cli:exe:all +``` + +## Docs +- `cli/README.md` - CLI usage and config +- `server/README.md` - server setup and architecture +- `web/README.md` - web app behavior and dev workflow diff --git a/cli/CLAUDE.md b/cli/CLAUDE.md index b60feada..211aec37 100644 --- a/cli/CLAUDE.md +++ b/cli/CLAUDE.md @@ -1,11 +1,11 @@ -# Happy CLI Codebase Overview +# HAPI CLI Codebase Overview ## Project Overview -Happy CLI (`happy-cli`) is a command-line tool that wraps Claude Code to enable remote control and session sharing via `happy-bot` (Telegram Bot + Mini App). It's part of a two-component system: +HAPI CLI (`hapi`) is a command-line tool that wraps Claude Code to enable remote control and session sharing via `hapi-server` (Telegram Bot + Mini App). It's part of a two-component system: -1. **happy-cli** (this project) - CLI wrapper for Claude Code -2. **happy-bot** - Public server (Socket.IO + REST + SQLite) + Telegram Mini App +1. **hapi** (this project) - CLI wrapper for Claude Code +2. **hapi-server** - Public server (Socket.IO + REST + SQLite) + Telegram Mini App ## Code Style Preferences @@ -96,29 +96,29 @@ User interface components. ## Data Flow 1. **Authentication**: - - Use `CLI_API_TOKEN` to authenticate to `happy-bot` (REST + Socket.IO) + - Use `CLI_API_TOKEN` to authenticate to `hapi-server` (REST + Socket.IO) 2. **Session Creation**: - Create/load session via `POST /cli/sessions` → Establish Socket.IO `/cli` connection 3. **Message Flow**: - - Local mode: terminal/SDK → happy-cli → happy-bot → Telegram Mini App + - Local mode: terminal/SDK → hapi CLI → hapi-server → Telegram Mini App 4. **Permission Handling**: - - Claude requests permission → happy-cli exposes RPC handlers → Mini App calls REST → happy-bot relays RPC to happy-cli + - Claude requests permission → hapi CLI exposes RPC handlers → Mini App calls REST → hapi-server relays RPC to hapi CLI ## Key Design Decisions 1. **File-based logging**: Prevents interference with Claude's terminal UI 2. **Dual Claude integration**: Process spawning for interactive, SDK for remote -3. **No E2E encryption**: Use HTTPS/TLS for `happy-bot` deployments +3. **No E2E encryption**: Use HTTPS/TLS for `hapi-server` deployments 4. **Session persistence**: Allows resuming sessions across restarts 5. **Optimistic concurrency**: Handles distributed state updates gracefully ## Security Considerations - `CLI_API_TOKEN` is a shared secret; treat it like a password. -- No end-to-end encryption: use HTTPS/TLS for `happy-bot` deployments. +- No end-to-end encryption: use HTTPS/TLS for `hapi-server` deployments. - Session isolation through unique session IDs. ## Dependencies @@ -135,21 +135,21 @@ User interface components. ## Starting the Daemon ```bash -# From the happy-cli directory: -./bin/happy.mjs daemon start +# From the hapi CLI directory: +hapi daemon start # With custom bot URL (for local development): -HAPPY_BOT_URL=http://localhost:3006 CLI_API_TOKEN=your_token ./bin/happy.mjs daemon start +HAPI_BOT_URL=http://localhost:3006 CLI_API_TOKEN=your_token hapi daemon start # Stop the daemon: -./bin/happy.mjs daemon stop +hapi daemon stop # Check daemon status: -./bin/happy.mjs daemon status +hapi daemon status ``` ## Daemon Logs -- Daemon logs are stored in `~/.happy-dev/logs/` (or `$HAPPY_HOME_DIR/logs/`) +- Daemon logs are stored in `~/.hapi-dev/logs/` (or `$HAPI_HOME_DIR/logs/`) - Named with format: `YYYY-MM-DD-HH-MM-SS-daemon.log` # Session Forking `claude` and sdk behavior diff --git a/cli/README.md b/cli/README.md index f7fc4492..dd6d446a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,75 +1,69 @@ -# hapi +# hapi CLI -Code on the go controlling Claude Code from your mobile device. +Run Claude Code or Codex sessions from your terminal and control them remotely through the hapi server. -Free. Open source. Code anywhere. +## What it does +- Starts Claude Code sessions and registers them with hapi-server. +- Starts Codex mode for OpenAI-based sessions. +- Provides an MCP stdio bridge for external tools. +- Manages a background daemon for long-running sessions. +- Includes diagnostics and auth helpers. -## Installation +## Typical flow +1. Start the server and set env vars (see ../server/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. +## Quickstart ```bash -# Download the prebuilt hapi binary for your platform -# (macOS/Linux/Windows x64/arm64) and place it on your PATH. +# Point to the server if it is not on localhost:3006 +export HAPI_BOT_URL="https://your-server-domain" + +hapi # prompts for CLI_API_TOKEN and saves it locally ``` -## Usage - -```bash -hapi -``` - -This will: -1. Start a Claude Code session -2. Register the session with your `hapi-server` instance -3. Allow real-time session control from the Telegram Mini App - ## Commands +- `hapi` - start a Claude Code session (passes through Claude CLI flags) +- `hapi codex` - start Codex mode +- `hapi mcp` - start MCP stdio bridge +- `hapi auth` - login/status/logout for CLI_API_TOKEN +- `hapi server` - start the bundled server (single binary workflow) +- `hapi daemon` - manage background service +- `hapi doctor` - diagnostics and cleanup -- `hapi auth` – Manage authentication -- `hapi codex` – Start Codex mode -- `hapi mcp` – Start MCP stdio bridge -- `hapi connect` – Not available in direct-connect mode -- `hapi notify` – Not available in direct-connect mode -- `hapi daemon` – Manage background service -- `hapi doctor` – System diagnostics & troubleshooting +## Configuration +Required: +- `CLI_API_TOKEN` - shared secret; must match the server +- `HAPI_BOT_URL` - server base URL (default: http://localhost:3006) -## Options +`CLI_API_TOKEN` can be set via env or stored in `~/.config/hapi/settings.json` (env wins). -- `-h, --help` - Show help -- `-v, --version` - Show version -- `-m, --model ` - Claude model to use (default: sonnet) -- `-p, --permission-mode ` - Permission mode: auto, default, or plan -- `--claude-env KEY=VALUE` - Set environment variable for Claude Code -- `--claude-arg ARG` - Pass additional argument to Claude CLI - -## Environment Variables - -- `HAPPY_BOT_URL` - Bot URL (default: http://localhost:3006) -- `CLI_API_TOKEN` - Shared secret for bot authentication (required) -- `HAPI_HOME_DIR` - Custom home directory for hapi data (default: ~/.config/hapi) -- `HAPPY_EXPERIMENTAL` - Enable experimental features (set to `true`, `1`, or `yes`) +Optional: +- `HAPI_HOME_DIR` - config/data directory (default: ~/.config/hapi) +- `HAPI_EXPERIMENTAL` - enable experimental features (true/1/yes) +- `HAPI_HTTP_MCP_URL` - default MCP target for `hapi mcp` +- `HAPI_CLAUDE_PATH` - path to a specific `claude` executable +- `HAPI_USE_BUNDLED_CLAUDE` - set to 1 to prefer node_modules claude +- `HAPI_USE_GLOBAL_CLAUDE` - set to 1 to prefer global claude ## Requirements +- Claude CLI installed and logged in (`claude` on PATH). +- Bun for building from source. -- Prebuilt hapi binary (no Bun or Node required at runtime) -- Claude CLI installed & logged in (`claude` command available in PATH) -- Bun (for building from source) - -## Building the executable - +## Build from source +From the repo root: ```bash -# From repo root +bun install +bun run build:cli bun run build:cli:exe -bun run build:cli:exe -- --target bun-darwin-x64 - -# Platform-only target uses the host arch -bun run build:cli:exe -- --target bun-linux - -# Build all targets -bun run build:cli:exe:all ``` -Note: Windows arm64 builds require arm64 tool archives in `cli/tools/archives`. +For an all-in-one binary that also embeds the web app: +```bash +bun run build:single-exe +``` -## License - -MIT +## Related docs +- `../server/README.md` +- `../web/README.md` diff --git a/cli/package.json b/cli/package.json index c2d1839d..72aa4c22 100644 --- a/cli/package.json +++ b/cli/package.json @@ -65,7 +65,7 @@ "package.json" ], "scripts": { - "why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started happy", + "why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started HAPI", "typecheck": "tsc --noEmit", "build": "shx rm -rf dist && tsc --noEmit && pkgroll", "build:exe": "bun run scripts/build-executable.ts", diff --git a/cli/roadmap.md b/cli/roadmap.md deleted file mode 100644 index c44d3741..00000000 --- a/cli/roadmap.md +++ /dev/null @@ -1,353 +0,0 @@ -# APi eeror? - -API Error: 500 {"type":"error","error":{"type":"api_error","message":"Overloaded"}} - -Not showng -Session -213d643d-fc52-4d43-83cd-d4d1e1b45fc6 -logs /Users/kirilldubovitskiy/.happy/logs/2025-07-20-20-48-12.log - - - - -# July 20 - -3) permission cancelling -5) nice to have - live activity -6) possible crash when disconnects? - -Push notification on result -Test how permission mode is exited from when in remote - -Expired permission requests? - -How do we know that Claude is waiting for us in interactive session - -On decline - what happens? -On timeout? - -Cli version report - -Not kirill -4) permission ui - -updateMetadata - update with usage - -- Permission request times out - -Failed to get or create session - crashes the cli -Ideally we want remote mode to be optional. -Should do this with lazy initialization - - -show we are stuck - likely waiting for permissions? - -### Edge cases: -- When we are stuck in permissions - we are unable to text & get a response - -- User write lol.txt -- Permission blocks for 5 minutes -- If they don't respond within 4.5 minutes, - - we should abort ourselves, keep the permission up on the client - - Lets look at async generator to abort - - When they approve permission next time - - We should continue -- We should cache permissions previously approved - so on repeat requests - we will auto approve? - later - -? Where is the timeout actually coming from? -- Is it claude or mcp server configuration? -- If MCP has defualt timeouts - - Trying from MCP debug tool - timeout maxx start at 2:35 - - -- Calling interrupt & blocking on mcp server does not yield results. MIght have to do it at the same time - -- - -[14:09:13.746] [MessageQueue] waitForNext() adding waiter. Total waiters: 1 -[14:13:40.716] [claudeRemote] Received message from SDK: user -[14:13:40.717] [CLAUDE] Message from non interactive & remote mode: - { - "type": "user", - "message": { - "role": "user", - "content": [ - { - "type": "tool_result", - "content": "Error calling tool", - "is_error": true, - "tool_use_id": "toolu_01BHgTzQcaoa7KMq8sMe1HU1" - } - ] - }, - "parent_tool_use_id": null, - "session_id": "8226266e-9953-4ea2-b6f1-17384bb4d469" -} - - -Anoterh sample - -[14:13:45.662] [CLAUDE] Message from non interactive & remote mode: - -14:18:46.683 - error - -Basically exactly 5 minutes. - -? How do we get the expiration? -? We should reset the permissions? - -Or we should allow them to - - -Now it fails after 5 minutes and tries again with a different permissions? or same - -We want to - -## - -[15:01:29.481] [SOCKET] Sending message through socket: - { - "role": "agent", - "content": { - "type": "output", - "data": { - "cwd": "/Users/kirilldubovitskiy/projects/happy/handy-cli", - "sessionId": "329df624-b37c-4849-ab83-65722d321c29", - "version": "1.0.51", - "uuid": "072e0a05-5480-4a83-9f63-802da615b66b", - "timestamp": "2025-07-20T22:01:29.464Z", - "type": "assistant", - "message": { - "id": "msg_01H9en68JmbGQJb3Mfob2rs8", - "type": "message", - "role": "assistant", - "model": "claude-opus-4-20250514", - "content": [ - { - "type": "text", - "text": "I'll create the file `lol.txt` in the parent directory." - } - ], - "stop_reason": null, - "stop_sequence": null - }, - "requestId": "req_011CRK2MXhzGmZ255atcGVzF" - } - } -} - -[15:39:05.950] [SOCKET] Sending message through socket: - { - "role": "agent", - "content": { - "type": "output", - "data": { - "cwd": "/Users/kirilldubovitskiy/projects/happy/handy-cli", - "sessionId": "329df624-b37c-4849-ab83-65722d321c29", - "version": "1.0.51", - "uuid": "8b1593e4-c56e-4785-9dd0-741587021c95", - "timestamp": "2025-07-20T22:39:03.317Z", - "type": "assistant", - "message": { - "id": "msg_01H9en68JmbGQJb3Mfob2rs8", - "type": "message", - "role": "assistant", - "model": "claude-opus-4-20250514", - "content": [ - { - "type": "text", - "text": "I'll create the file `lol.txt` in the parent directory." - } - ], - "stop_reason": null, - "stop_sequence": null - } - } - } -} - - -- 'result' message type is not sent to history. Its only for sdk - -- Remote is maintaned - Starting claudeRemote with messages: - - - I found the MCP permission tool call timeout configuration! - - Key findings: - - 1. Timeout Configuration Location: The timeout is configured via the RC6() function which reads from the environment variable MCP_TOOL_TIMEOUT: - function RC6() { return parseInt(process.env.MCP_TOOL_TIMEOUT || "", 10) || 1e8 } - 2. Default Timeout: If MCP_TOOL_TIMEOUT is not set, it defaults to 1e8 milliseconds (100,000,000 ms = ~27.8 hours) - 3. Where it's used: This timeout is passed to the MCP client's callTool method in the Uq2 function: - let G = await A.callTool({ name: Q, arguments: D }, vm, { signal: I, timeout: RC6() }); - 4. Connection Timeout: There's also a separate connection timeout configured via Kq2(): - function Kq2() { return parseInt(process.env.MCP_TIMEOUT || "", 10) || 30000 } - 4. This defaults to 30 seconds and is used for establishing the initial MCP server connection. - - To configure the timeout, you can set the environment variable: - - MCP_TOOL_TIMEOUT - for individual tool/permission calls (defaults to ~27.8 hours) - - MCP_TIMEOUT - for initial connection timeout (defaults to 30 seconds) - -- interrupt - -# July 19 - - -- Lazy initialize our shit - immediatelly drop to claude. If we are ofline - still do it -- Passthrough claude parameters -- Server diying test - - lsof -ti tcp:3005 | xargs kill -9 - - This kills the app :D and cli -- - -- Interruptions add UI to stop (stop button) -- Show that its doing someting, fix thingking - -# UI -- Permission request - - Bash - - Edit / Create (show too much info) -- Tools - - MultiEdit - - Task - - - -## Nice to have -- Chat titles - big ux boost -- Proxy to show the token count so we know its doing something - -## Nice nice to have -- Embed amphetamine into it?? -> adderal - - -# July 18 - -# CLI - -- Permissions fix -- Test what happens when we timeout the response, how - -- Test end to end & rollout new version - -CLI dies with -error Command failed with exit code 137. -info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. -kirilldubovitskiy@MacBookPro handy-cli % node:events:496 - throw er; // Unhandled 'error' event - ^ - -Error: read EIO - at TTY.onStreamRead (node:internal/stream_base_commons:216:20) -Emitted 'error' event on ReadStream instance at: - at emitErrorNT (node:internal/streams/destroy:170:8) - at emitErrorCloseNT (node:internal/streams/destroy:129:3) - at process.processTicksAndRejections (node:internal/process/task_queues:90:21) { - errno: -5, - code: 'EIO', - syscall: 'read' -} - -- Dogfood for issues -- Refactor existing code -- Make it feel nicer - -- Embed amphetamine into it? - -- Permissions - fix mcp server integration - -- Deep link to a website - -# App -- Logout - make a full reload -- Make scroll work nicely - -# Big ideas -- Coordinator agent - will ensure claude keeps working at max token usage - juice the most out of it -- Social component -- Notifications -- Real time voice - -# Archive July 18 - -# Roadmap - -## App -- Make key messages render -- [later] Wrapping claude in an http proxy, allows us to snoop on token usage to show its doing something in the ui when running in remote mode -- For local mode, same approach will work - -- Distribution - - Website - happyinc.ai? - - App Store - - Google Play - -- Deep link to download app from cli link - -## Server - -- Session management - - Keep track of who is controlling the session - remote or local - - -## CLI -- Make it stable to be a drop in replacement for claude -- Fix snooping on existing conversation bug, after switching back and forth stops watching the session file for new messages -- [later] Test it works on linux, windows, lower node version - -Conversation continuity -- Some things will not expect as you would want such as /clear ing the conversation, or forking (press 2 escape on empty input) -- We might want to be better at switching between sessions for full compatibility with claude - -MCP -- Permissions - - I think we should reuse the format from .claude/settings.local.json, so interactive & our checking will be similar - - Impelement checking logic - - Implement blessing command logic () -- Implement conversation naming - - I wonder if the server can initiate an llm call on its own accord? - -Permission automatic checking -- Pull antropic token from secrets - - -Blocking -- Permission checking [steve] - - use mcp - - see if it has a timeout or we can block forever (ideally) - - copy cc system (deterministic splitting, prefix checking, injection detection, prefix whitelist suggest) - - use cc settings local file & format for compatibility - - figure out extra path permissions - -- CLI dies if server disconnects :D - -- Need to make agent state work. Most important state - permissions -- Try logging out of Claude and see how to handle that case -- Make sure to use Claude from our package. Kill other Claudes -- Make sure interruption of remote controlled session works - -### Nice to have -- UX final touches - onboarding make sure terminal, add session icons or something catchy -- See if I can simplify / get rid of a likely race condition in pty related code -- Pass --local-installation to setup .happy folder locally and avoid clashing with global installation - -# Distribution - -- Post on hacker news -- Send to friends to try -- Send to influencers who reviewed similar products -- Mass email people who have starred claudecodeui - - -# Later, low priority - - -- Permissions callout: - - permission checking will not be visible on the client nor will we be aware of it - - ✻ Enchanting… (5s · ↑ 27 tokens · esc to interrupt) - - We can parse the terminal output - -- e2e single tests - - Would be nice to be able to run the whole thing - including pty to emulate a simple scenario and make sure a single multi step happy path works fine - diff --git a/cli/src/api/apiMachine.ts b/cli/src/api/apiMachine.ts index 51a075d0..61bffd48 100644 --- a/cli/src/api/apiMachine.ts +++ b/cli/src/api/apiMachine.ts @@ -1,5 +1,5 @@ /** - * WebSocket client for machine/daemon communication with happy-bot + * WebSocket client for machine/daemon communication with hapi-server */ import { io, type Socket } from 'socket.io-client' diff --git a/cli/src/claude/claudeLocal.ts b/cli/src/claude/claudeLocal.ts index 3823c421..f2c9fc03 100644 --- a/cli/src/claude/claudeLocal.ts +++ b/cli/src/claude/claudeLocal.ts @@ -95,7 +95,7 @@ export async function claudeLocal(opts: { } if (!claudeCliPath || !existsSync(claudeCliPath)) { - throw new Error('Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.'); + throw new Error('Claude local launcher not found. Please ensure HAPI_PROJECT_ROOT is set correctly for development.'); } // Prepare environment variables diff --git a/cli/src/claude/registerKillSessionHandler.ts b/cli/src/claude/registerKillSessionHandler.ts index e62ba7a5..02788d46 100644 --- a/cli/src/claude/registerKillSessionHandler.ts +++ b/cli/src/claude/registerKillSessionHandler.ts @@ -25,7 +25,7 @@ export function registerKillSessionHandler( // should optimistically assume the session is dead. return { success: true, - message: 'Killing happy-cli process' + message: 'Killing hapi CLI process' }; }); } diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index 305dffd8..b7506474 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -36,7 +36,7 @@ export async function runClaude(options: StartOptions = {}): Promise { const sessionTag = randomUUID(); // Log environment info at startup - logger.debugLargeJson('[START] Happy process started', getEnvironmentInfo()); + logger.debugLargeJson('[START] HAPI process started', getEnvironmentInfo()); logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`); // Validate daemon spawn requirements @@ -121,9 +121,9 @@ export async function runClaude(options: StartOptions = {}): Promise { // Create realtime session const session = api.sessionSyncClient(response); - // Start Happy MCP server + // Start HAPI MCP server const happyServer = await startHappyServer(session); - logger.debug(`[START] Happy MCP server started at ${happyServer.url}`); + logger.debug(`[START] HAPI MCP server started at ${happyServer.url}`); // Print log file path const logPath = logger.logFilePath; @@ -303,7 +303,7 @@ export async function runClaude(options: StartOptions = {}): Promise { await session.close(); } - // Stop Happy MCP server + // Stop HAPI MCP server happyServer.stop(); logger.debug('[START] Cleanup complete, exiting'); @@ -339,7 +339,7 @@ export async function runClaude(options: StartOptions = {}): Promise { startingMode: options.startingMode, messageQueue, api, - allowedTools: happyServer.toolNames.map(toolName => `mcp__happy__${toolName}`), + allowedTools: happyServer.toolNames.map(toolName => `mcp__hapi__${toolName}`), onModeChange: (newMode) => { session.sendSessionEvent({ type: 'switch', mode: newMode }); session.updateAgentState((currentState) => ({ @@ -351,7 +351,7 @@ export async function runClaude(options: StartOptions = {}): Promise { // Intentionally unused }, mcpServers: { - 'happy': { + 'hapi': { type: 'http' as const, url: happyServer.url, } @@ -372,9 +372,9 @@ export async function runClaude(options: StartOptions = {}): Promise { logger.debug('Closing session...'); await session.close(); - // Stop Happy MCP server + // Stop HAPI MCP server happyServer.stop(); - logger.debug('Stopped Happy MCP server'); + logger.debug('Stopped HAPI MCP server'); // Exit process.exit(0); diff --git a/cli/src/claude/sdk/index.ts b/cli/src/claude/sdk/index.ts index 226aa8dc..2c8f4ad3 100644 --- a/cli/src/claude/sdk/index.ts +++ b/cli/src/claude/sdk/index.ts @@ -1,5 +1,5 @@ /** - * Claude Code SDK integration for Happy CLI + * Claude Code SDK integration for HAPI CLI * Provides clean TypeScript implementation without Bun support */ @@ -19,4 +19,4 @@ export type { SDKControlRequest, CanCallToolCallback, PermissionResult -} from './types' \ No newline at end of file +} from './types' diff --git a/cli/src/claude/sdk/utils.ts b/cli/src/claude/sdk/utils.ts index 773ceb6a..8b525dc7 100644 --- a/cli/src/claude/sdk/utils.ts +++ b/cli/src/claude/sdk/utils.ts @@ -118,21 +118,21 @@ function findGlobalClaudePath(): string | null { * Compares global and bundled versions, uses the newer one * * Environment variables: - * - HAPPY_CLAUDE_PATH: Force a specific path to claude executable - * - HAPPY_USE_BUNDLED_CLAUDE=1: Force use of node_modules version (skip global search) - * - HAPPY_USE_GLOBAL_CLAUDE=1: Force use of global version (if available) + * - HAPI_CLAUDE_PATH: Force a specific path to claude executable + * - HAPI_USE_BUNDLED_CLAUDE=1: Force use of node_modules version (skip global search) + * - HAPI_USE_GLOBAL_CLAUDE=1: Force use of global version (if available) */ export function getDefaultClaudeCodePath(): string { const nodeModulesPath = join(__dirname, '..', '..', '..', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js') // Allow explicit override via env var - if (process.env.HAPPY_CLAUDE_PATH) { - logger.debug(`[Claude SDK] Using HAPPY_CLAUDE_PATH: ${process.env.HAPPY_CLAUDE_PATH}`) - return process.env.HAPPY_CLAUDE_PATH + if (process.env.HAPI_CLAUDE_PATH) { + logger.debug(`[Claude SDK] Using HAPI_CLAUDE_PATH: ${process.env.HAPI_CLAUDE_PATH}`) + return process.env.HAPI_CLAUDE_PATH } // Force bundled version if requested - if (process.env.HAPPY_USE_BUNDLED_CLAUDE === '1') { + if (process.env.HAPI_USE_BUNDLED_CLAUDE === '1') { logger.debug(`[Claude SDK] Forced bundled version: ${nodeModulesPath}`) return nodeModulesPath } diff --git a/cli/src/claude/utils/startHappyServer.ts b/cli/src/claude/utils/startHappyServer.ts index 9a1bb21b..1ddc33ec 100644 --- a/cli/src/claude/utils/startHappyServer.ts +++ b/cli/src/claude/utils/startHappyServer.ts @@ -1,6 +1,6 @@ /** - * Happy MCP server - * Provides Happy CLI specific tools including chat session title management + * HAPI MCP server + * Provides HAPI CLI specific tools including chat session title management */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -15,7 +15,7 @@ import { randomUUID } from "node:crypto"; export async function startHappyServer(client: ApiSessionClient) { // Handler that sends title updates via the client const handler = async (title: string) => { - logger.debug('[happyMCP] Changing title to:', title); + logger.debug('[hapiMCP] Changing title to:', title); try { // Send title as a summary message, similar to title generator client.sendClaudeSessionMessage({ @@ -35,7 +35,7 @@ export async function startHappyServer(client: ApiSessionClient) { // const mcp = new McpServer({ - name: "Happy MCP", + name: "HAPI MCP", version: "1.0.0", }); @@ -47,7 +47,7 @@ export async function startHappyServer(client: ApiSessionClient) { }, }, async (args) => { const response = await handler(args.title); - logger.debug('[happyMCP] Response:', response); + logger.debug('[hapiMCP] Response:', response); if (response.success) { return { @@ -105,7 +105,7 @@ export async function startHappyServer(client: ApiSessionClient) { url: baseUrl.toString(), toolNames: ['change_title'], stop: () => { - logger.debug('[happyMCP] Stopping server'); + logger.debug('[hapiMCP] Stopping server'); mcp.close(); server.close(); } diff --git a/cli/src/claude/utils/systemPrompt.ts b/cli/src/claude/utils/systemPrompt.ts index 78a59b78..723b6c43 100644 --- a/cli/src/claude/utils/systemPrompt.ts +++ b/cli/src/claude/utils/systemPrompt.ts @@ -5,22 +5,22 @@ import { shouldIncludeCoAuthoredBy } from "./claudeSettings"; * Base system prompt shared across all configurations */ const BASE_SYSTEM_PROMPT = (() => trimIdent(` - ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human. + ALWAYS when you start a new chat - you must call a tool "mcp__hapi__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human. `))(); /** * Co-authored-by credits to append when enabled */ const CO_AUTHORED_CREDITS = (() => trimIdent(` - When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so: + When making commit messages, instead of just giving co-credit to Claude, also give credit to HAPI like so:
Generated with [Claude Code](https://claude.ai/code) - via [Happy](https://happy.engineering) + via [HAPI](https://happy.engineering) Co-Authored-By: Claude - Co-Authored-By: Happy + Co-Authored-By: HAPI `))(); /** @@ -35,4 +35,4 @@ export const systemPrompt = (() => { } else { return BASE_SYSTEM_PROMPT; } -})(); \ No newline at end of file +})(); diff --git a/cli/src/codex/codexMcpClient.ts b/cli/src/codex/codexMcpClient.ts index 0c7097d1..4c9e1012 100644 --- a/cli/src/codex/codexMcpClient.ts +++ b/cli/src/codex/codexMcpClient.ts @@ -54,7 +54,7 @@ export class CodexMcpClient { constructor() { this.client = new Client( - { name: 'happy-codex-client', version: '1.0.0' }, + { name: 'hapi-codex-client', version: '1.0.0' }, { capabilities: { elicitation: {} } } ); diff --git a/cli/src/codex/happyMcpStdioBridge.ts b/cli/src/codex/happyMcpStdioBridge.ts index 62796305..d6d41355 100644 --- a/cli/src/codex/happyMcpStdioBridge.ts +++ b/cli/src/codex/happyMcpStdioBridge.ts @@ -1,11 +1,11 @@ /** - * Happy MCP STDIO Bridge + * HAPI MCP STDIO Bridge * * Minimal STDIO MCP server exposing a single tool `change_title`. - * On invocation it forwards the tool call to an existing Happy HTTP MCP server + * On invocation it forwards the tool call to an existing HAPI HTTP MCP server * using the StreamableHTTPClientTransport. * - * Configure the target HTTP MCP URL via env var `HAPPY_HTTP_MCP_URL` or + * Configure the target HTTP MCP URL via env var `HAPI_HTTP_MCP_URL` or * via CLI flag `--url `. * * Note: This process must not print to stdout as it would break MCP STDIO. @@ -33,12 +33,12 @@ export async function runHappyMcpStdioBridge(argv: string[]): Promise { try { // Resolve target HTTP MCP URL const { url: urlFromArgs } = parseArgs(argv); - const baseUrl = urlFromArgs || process.env.HAPPY_HTTP_MCP_URL || ''; + const baseUrl = urlFromArgs || process.env.HAPI_HTTP_MCP_URL || ''; if (!baseUrl) { // Write to stderr; never stdout. process.stderr.write( - '[happy-mcp] Missing target URL. Set HAPPY_HTTP_MCP_URL or pass --url \n' + '[hapi-mcp] Missing target URL. Set HAPI_HTTP_MCP_URL or pass --url \n' ); process.exit(2); } @@ -48,7 +48,7 @@ export async function runHappyMcpStdioBridge(argv: string[]): Promise { async function ensureHttpClient(): Promise { if (httpClient) return httpClient; const client = new Client( - { name: 'happy-stdio-bridge', version: '1.0.0' }, + { name: 'hapi-stdio-bridge', version: '1.0.0' }, { capabilities: {} } ); @@ -60,7 +60,7 @@ export async function runHappyMcpStdioBridge(argv: string[]): Promise { // Create STDIO MCP server const server = new McpServer({ - name: 'Happy MCP Bridge', + name: 'HAPI MCP Bridge', version: '1.0.0', }); @@ -96,7 +96,7 @@ export async function runHappyMcpStdioBridge(argv: string[]): Promise { await server.connect(stdio); } catch (err) { try { - process.stderr.write(`[happy-mcp] Fatal: ${err instanceof Error ? err.message : String(err)}\n`); + process.stderr.write(`[hapi-mcp] Fatal: ${err instanceof Error ? err.message : String(err)}\n`); } finally { process.exit(1); } diff --git a/cli/src/codex/runCodex.ts b/cli/src/codex/runCodex.ts index 5ce225f0..3d656259 100644 --- a/cli/src/codex/runCodex.ts +++ b/cli/src/codex/runCodex.ts @@ -271,7 +271,7 @@ export async function runCodex(opts: { await session.close(); } - // Stop Happy MCP server + // Stop HAPI MCP server happyServer.stop(); logger.debug('[Codex] Session termination complete, exiting'); @@ -520,11 +520,11 @@ export async function runCodex(opts: { } }); - // Start Happy MCP server (HTTP) and prepare STDIO bridge config for Codex + // Start HAPI MCP server (HTTP) and prepare STDIO bridge config for Codex const happyServer = await startHappyServer(session); const bridgeCommand = getHappyCliCommand(['mcp', '--url', happyServer.url]); const mcpServers = { - happy: { + hapi: { command: bridgeCommand.command, args: bridgeCommand.args } @@ -727,7 +727,7 @@ export async function runCodex(opts: { logger.debug('[codex]: client.disconnect begin'); await client.disconnect(); logger.debug('[codex]: client.disconnect done'); - // Stop Happy MCP server + // Stop HAPI MCP server logger.debug('[codex]: happyServer.stop'); happyServer.stop(); diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts index 38234a1b..4c0f7bb3 100644 --- a/cli/src/commands/auth.ts +++ b/cli/src/commands/auth.ts @@ -20,7 +20,7 @@ export async function handleAuthCommand(args: string[]): Promise { const hasToken = Boolean(envToken || settingsToken) const tokenSource = envToken ? 'environment' : (settingsToken ? 'settings file' : 'none') console.log(chalk.bold('\nDirect Connect Status\n')) - console.log(chalk.gray(` HAPPY_BOT_URL: ${configuration.serverUrl}`)) + console.log(chalk.gray(` HAPI_BOT_URL: ${configuration.serverUrl}`)) console.log(chalk.gray(` CLI_API_TOKEN: ${hasToken ? 'set' : 'missing'}`)) console.log(chalk.gray(` Token Source: ${tokenSource}`)) console.log(chalk.gray(` Machine ID: ${settings.machineId ?? 'not set'}`)) diff --git a/cli/src/configuration.ts b/cli/src/configuration.ts index 63d04a6f..0588e872 100644 --- a/cli/src/configuration.ts +++ b/cli/src/configuration.ts @@ -1,5 +1,5 @@ /** - * Global configuration for happy CLI + * Global configuration for HAPI CLI * * Centralizes all configuration including environment variables and paths * Environment files should be loaded using Node's --env-file flag @@ -28,7 +28,7 @@ class Configuration { constructor() { // Bot server configuration - this.serverUrl = process.env.HAPPY_BOT_URL || 'http://localhost:3006' + this.serverUrl = process.env.HAPI_BOT_URL || 'http://localhost:3006' this._cliApiToken = process.env.CLI_API_TOKEN || '' // Check if we're running as daemon based on process args @@ -50,7 +50,7 @@ class Configuration { this.daemonStateFile = join(this.happyHomeDir, 'daemon.state.json') this.daemonLockFile = join(this.happyHomeDir, 'daemon.state.json.lock') - this.isExperimentalEnabled = ['true', '1', 'yes'].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || '') + this.isExperimentalEnabled = ['true', '1', 'yes'].includes(process.env.HAPI_EXPERIMENTAL?.toLowerCase() || '') this.currentCliVersion = packageJson.version diff --git a/cli/src/daemon/CLAUDE.md b/cli/src/daemon/CLAUDE.md index 59f1e5a6..c2564e40 100644 --- a/cli/src/daemon/CLAUDE.md +++ b/cli/src/daemon/CLAUDE.md @@ -1,12 +1,12 @@ -# Happy CLI Daemon: Control Flow and Lifecycle +# HAPI CLI Daemon: Control Flow and Lifecycle -The daemon is a persistent background process that manages Happy sessions, enables remote control from the mobile app, and handles auto-updates when the CLI version changes. +The daemon is a persistent background process that manages HAPI sessions, enables remote control from the mobile app, and handles auto-updates when the CLI version changes. ## 1. Daemon Lifecycle ### Starting the Daemon -Command: `happy daemon start` +Command: `hapi daemon start` Control Flow: 1. `src/index.ts` receives `daemon start` command @@ -23,7 +23,7 @@ Control Flow: - HTTP server: starts on random port for local CLI control (list, stop, spawn) - WebSocket: establishes persistent connection to backend via `ApiMachineClient` - RPC registration: exposes `spawn-happy-session`, `stop-session`, `requestShutdown` handlers - - Heartbeat loop: every 60s (or HAPPY_DAEMON_HEARTBEAT_INTERVAL) checks for version updates and prunes dead sessions + - Heartbeat loop: every 60s (or HAPI_DAEMON_HEARTBEAT_INTERVAL) checks for version updates and prunes dead sessions 5. Awaits shutdown promise which resolves when: - OS signal received (SIGINT/SIGTERM) - HTTP `/stop` endpoint called @@ -40,7 +40,7 @@ Control Flow: ### Version Mismatch Auto-Update -The daemon detects when `npm upgrade happy-coder` occurs: +The daemon detects when `npm upgrade hapi` occurs: 1. Heartbeat reads package.json from disk 2. Compares `JSON.parse(package.json).version` with compiled `configuration.currentCliVersion` 3. If mismatch detected: @@ -52,7 +52,7 @@ The daemon detects when `npm upgrade happy-coder` occurs: ### Stopping the Daemon -Command: `happy daemon stop` +Command: `hapi daemon stop` Control Flow: 1. `stopDaemon()` in `controlClient.ts` reads daemon.state.json @@ -74,10 +74,10 @@ Initiated by mobile app via backend RPC: 2. `ApiMachineClient` invokes `spawnSession()` handler 3. `spawnSession()`: - Creates directory if needed - - Spawns detached Happy process with `--hapi-starting-mode remote --started-by daemon` + - Spawns detached HAPI process with `--hapi-starting-mode remote --started-by daemon` - Adds to `pidToTrackedSession` map - Sets up 10-second awaiter for session webhook -4. New Happy process: +4. New HAPI process: - Creates session with backend, receives `happySessionId` - Calls `notifyDaemonSessionStarted()` to POST to daemon's `/session-started` 5. Daemon updates tracking with `happySessionId`, resolves awaiter @@ -85,10 +85,10 @@ Initiated by mobile app via backend RPC: ### Terminal-Spawned Sessions -User runs `happy` directly: +User runs `hapi` directly: 1. CLI auto-starts daemon if configured -2. Happy process calls `notifyDaemonSessionStarted()` -3. Daemon receives webhook, creates `TrackedSession` with `startedBy: 'happy directly...'` +2. HAPI process calls `notifyDaemonSessionStarted()` +3. Daemon receives webhook, creates `TrackedSession` with `startedBy: 'hapi directly...'` 4. Session tracked for health monitoring ### Session Termination @@ -111,14 +111,14 @@ Local HTTP server (127.0.0.1 only) provides: ### Doctor Command -`happy doctor` uses `ps aux | grep` to find all Happy processes: +`hapi doctor` uses `ps aux | grep` to find all HAPI processes: - Production: matches `happy.mjs`, `happy-coder`, `dist/index.mjs` - Development: matches `tsx.*src/index.ts` - Categorizes by command args: daemon, daemon-spawned, user-session, doctor ### Clean Runaway Processes -`happy doctor clean`: +`hapi doctor clean`: 1. `findRunawayHappyProcesses()` filters for likely orphans 2. `killRunawayHappyProcesses()`: - Sends SIGTERM @@ -178,7 +178,7 @@ I do not like how # Machine Sync Architecture - Separated Metadata & Daemon State -> Direct-connect note: the “server” is `happy-bot` (not `happy-server`), payloads are plain JSON (no base64/encryption), +> Direct-connect note: the “server” is `hapi-server`, payloads are plain JSON (no base64/encryption), > and authentication uses `CLI_API_TOKEN` (REST `Authorization: Bearer ...` + Socket.IO `handshake.auth.token`). ## Data Structure (Similar to Session's metadata + agentState) @@ -222,7 +222,7 @@ Checks if machine ID exists in settings: "platform": "darwin", "happyCliVersion": "1.0.0", "homeDir": "/Users/john", - "happyHomeDir": "/Users/john/.happy" + "happyHomeDir": "/Users/john/.config/hapi" }, "daemonState": { "status": "running", @@ -318,16 +318,16 @@ socket.emit('machine-update-metadata', { "platform": "darwin", "happyCliVersion": "1.0.1", "homeDir": "/Users/john", - "happyHomeDir": "/Users/john/.happy" + "happyHomeDir": "/Users/john/.config/hapi" }, "expectedVersion": 1 }, callback) ``` -## 5. Mini App RPC Calls (via happy-bot) +## 5. Mini App RPC Calls (via hapi-server) -The Telegram Mini App calls REST endpoints on `happy-bot` (for example `POST /api/machines/:id/spawn`). -`happy-bot` then relays those requests to the daemon via Socket.IO `rpc-request` on the `/cli` namespace. +The Telegram Mini App calls REST endpoints on `hapi-server` (for example `POST /api/machines/:id/spawn`). +`hapi-server` then relays those requests to the daemon via Socket.IO `rpc-request` on the `/cli` namespace. RPC method naming (machine-scoped) uses a `${machineId}:` prefix, for example: - `${machineId}:spawn-happy-session` diff --git a/cli/src/daemon/controlClient.ts b/cli/src/daemon/controlClient.ts index 2d0e529c..d061753e 100644 --- a/cli/src/daemon/controlClient.ts +++ b/cli/src/daemon/controlClient.ts @@ -32,7 +32,7 @@ async function daemonPost(path: string, body?: any): Promise<{ error?: string } } try { - const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 10_000; + const timeout = process.env.HAPI_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPI_DAEMON_HTTP_TIMEOUT) : 10_000; const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -90,7 +90,7 @@ export async function stopDaemonHttp(): Promise { /** * The version check is still quite naive. - * For instance we are not handling the case where we upgraded happy, + * For instance we are not handling the case where we upgraded hapi, * the daemon is still running, and it recieves a new message to spawn a new session. * This is a tough case - we need to somehow figure out to restart ourselves, * yet still handle the original request. @@ -113,7 +113,7 @@ export async function stopDaemonHttp(): Promise { * Not just a boolean. * * We can destructure the response on the caller for richer output. - * For instance when running `happy daemon status` we can show more information. + * For instance when running `hapi daemon status` we can show more information. */ export async function checkIfDaemonRunningAndCleanupStaleState(): Promise { const state = await readDaemonState(); @@ -164,7 +164,7 @@ export async function isDaemonRunningCurrentlyInstalledHappyVersion(): Promise } } throw new Error('Process did not die within timeout'); -} \ No newline at end of file +} diff --git a/cli/src/daemon/daemon.integration.test.ts b/cli/src/daemon/daemon.integration.test.ts index 3dfb9d42..c26c3cf2 100644 --- a/cli/src/daemon/daemon.integration.test.ts +++ b/cli/src/daemon/daemon.integration.test.ts @@ -10,9 +10,9 @@ * and the daemon will not work properly! * * The integration test environment uses .env.integration-test which sets: - * - HAPPY_HOME_DIR=~/.happy-dev-test (DIFFERENT from dev's ~/.happy-dev!) - * - HAPPY_BOT_URL=http://localhost:3006 (local happy-bot) - * - CLI_API_TOKEN=... (must match the bot) + * - HAPI_HOME_DIR=~/.hapi-dev-test (DIFFERENT from dev's ~/.hapi-dev!) + * - HAPI_BOT_URL=http://localhost:3006 (local hapi-server) + * - CLI_API_TOKEN=... (must match the server) */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; @@ -136,7 +136,7 @@ describe.skipIf(!await isServerHealthy())('Daemon Integration Tests', { timeout: expect(sessions).toHaveLength(1); const tracked = sessions[0]; - expect(tracked.startedBy).toBe('happy directly - likely by user from terminal'); + expect(tracked.startedBy).toBe('hapi directly - likely by user from terminal'); expect(tracked.happySessionId).toBe('test-session-123'); expect(tracked.pid).toBe(99999); }); @@ -192,7 +192,7 @@ describe.skipIf(!await isServerHealthy())('Daemon Integration Tests', { timeout: }); it('should track both daemon-spawned and terminal sessions', async () => { - // Spawn a real happy process that looks like it was started from terminal + // Spawn a real hapi process that looks like it was started from terminal const terminalHappyProcess = spawnHappyCLI([ '--hapi-starting-mode', 'remote', '--started-by', 'terminal' @@ -202,7 +202,7 @@ describe.skipIf(!await isServerHealthy())('Daemon Integration Tests', { timeout: stdio: 'ignore' }); if (!terminalHappyProcess || !terminalHappyProcess.pid) { - throw new Error('Failed to spawn terminal happy process'); + throw new Error('Failed to spawn terminal hapi process'); } // Give time to start & report itself await new Promise(resolve => setTimeout(resolve, 5_000)); @@ -223,7 +223,7 @@ describe.skipIf(!await isServerHealthy())('Daemon Integration Tests', { timeout: ); expect(terminalSession).toBeDefined(); - expect(terminalSession.startedBy).toBe('happy directly - likely by user from terminal'); + expect(terminalSession.startedBy).toBe('hapi directly - likely by user from terminal'); expect(daemonSession).toBeDefined(); expect(daemonSession.startedBy).toBe('daemon'); @@ -397,9 +397,9 @@ describe.skipIf(!await isServerHealthy())('Daemon Integration Tests', { timeout: * 7. New daemon starts, reads daemon.state.json, sees old version != its compiled version * 8. New daemon calls stopDaemon() to kill old daemon, then takes over * - * This simulates what happens during `npm upgrade happy-coder`: + * This simulates what happens during `npm upgrade hapi`: * - Running daemon has OLD version loaded in memory (configuration.currentCliVersion) - * - npm replaces node_modules/happy-coder/ with NEW version files + * - npm replaces node_modules/hapi/ with NEW version files * - package.json on disk now has NEW version * - Daemon reads package.json, detects mismatch, triggers self-update * - Key difference: npm atomically replaces the entire module directory, while @@ -451,7 +451,7 @@ describe.skipIf(!await isServerHealthy())('Daemon Integration Tests', { timeout: // The daemon should automatically detect the version mismatch and restart itself // We check once per minute, wait for a little longer than that - await new Promise(resolve => setTimeout(resolve, parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || '30000') + 10_000)); + await new Promise(resolve => setTimeout(resolve, parseInt(process.env.HAPI_DAEMON_HEARTBEAT_INTERVAL || '30000') + 10_000)); // Check that the daemon is running with the new version const finalState = await readDaemonState(); @@ -471,7 +471,7 @@ describe.skipIf(!await isServerHealthy())('Daemon Integration Tests', { timeout: // TODO: Add a test to see if a corrupted file will work - // TODO: Test npm uninstall scenario - daemon should gracefully handle when happy-coder is uninstalled + // TODO: Test npm uninstall scenario - daemon should gracefully handle when hapi is uninstalled // Current behavior: daemon tries to spawn new daemon on version mismatch but dist/index.mjs is gone // Expected: daemon should detect missing entrypoint and either exit cleanly or at minimum not respawn infinitely }); diff --git a/cli/src/daemon/doctor.ts b/cli/src/daemon/doctor.ts index bbac388c..38558b3f 100644 --- a/cli/src/daemon/doctor.ts +++ b/cli/src/daemon/doctor.ts @@ -9,7 +9,7 @@ import psList from 'ps-list'; import spawn from 'cross-spawn'; /** - * Find all Happy CLI processes (including current process) + * Find all HAPI CLI processes (including current process) */ export async function findAllHappyProcesses(): Promise> { try { @@ -20,7 +20,7 @@ export async function findAllHappyProcesses(): Promise> { const allProcesses = await findAllHappyProcesses(); @@ -80,7 +80,7 @@ export async function findRunawayHappyProcesses(): Promise }> { const runawayProcesses = await findRunawayHappyProcesses(); diff --git a/cli/src/daemon/install.ts b/cli/src/daemon/install.ts index b20654a0..ffd2af3d 100644 --- a/cli/src/daemon/install.ts +++ b/cli/src/daemon/install.ts @@ -10,6 +10,6 @@ export async function install(): Promise { throw new Error('Daemon installation requires sudo privileges. Please run with sudo.'); } - logger.info('Installing Happy CLI daemon for macOS...'); + logger.info('Installing HAPI CLI daemon for macOS...'); await installMac(); -} \ No newline at end of file +} diff --git a/cli/src/daemon/mac/install.ts b/cli/src/daemon/mac/install.ts index edd3d394..3722d860 100644 --- a/cli/src/daemon/mac/install.ts +++ b/cli/src/daemon/mac/install.ts @@ -1,12 +1,12 @@ /** - * Installation script for Happy daemon using macOS LaunchDaemons + * Installation script for HAPI daemon using macOS LaunchDaemons * * NOTE: This installation method is currently NOT USED in favor of auto-starting - * the daemon when the user runs the happy command. + * the daemon when the user runs the hapi command. * * Why we're not using this approach: * 1. Installing a LaunchDaemon requires sudo permissions, which users might not be comfortable with - * 2. We assume users will run happy frequently (every time they open their laptop) + * 2. We assume users will run hapi frequently (every time they open their laptop) * 3. The auto-start approach provides the same functionality without requiring elevated permissions * * This code is kept for potential future use if we decide to offer system-level installation as an option. @@ -18,7 +18,7 @@ import { logger } from '@/ui/logger'; import { trimIdent } from '@/utils/trimIdent'; import os from 'os'; -const PLIST_LABEL = 'com.happy-cli.daemon'; +const PLIST_LABEL = 'com.hapi-cli.daemon'; const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`; // NOTE: Local installation like --local does not make too much sense I feel like @@ -31,7 +31,7 @@ export async function install(): Promise { execSync(`launchctl unload ${PLIST_FILE}`, { stdio: 'inherit' }); } - // Get the path to the happy CLI executable + // Get the path to the hapi CLI executable const happyPath = process.argv[0]; // Node.js executable const scriptPath = process.argv[1]; // Script path @@ -48,12 +48,12 @@ export async function install(): Promise { ${happyPath} ${scriptPath} - happy-daemon + hapi-daemon EnvironmentVariables - HAPPY_DAEMON_MODE + HAPI_DAEMON_MODE true @@ -64,10 +64,10 @@ export async function install(): Promise { StandardErrorPath - ${os.homedir()}/.happy/daemon.err + ${os.homedir()}/.hapi/daemon.err StandardOutPath - ${os.homedir()}/.happy/daemon.log + ${os.homedir()}/.hapi/daemon.log WorkingDirectory /tmp @@ -85,10 +85,10 @@ export async function install(): Promise { execSync(`launchctl load ${PLIST_FILE}`, { stdio: 'inherit' }); logger.info('Daemon installed and started successfully'); - logger.info('Check logs at ~/.happy/daemon.log'); + logger.info('Check logs at ~/.hapi/daemon.log'); } catch (error) { logger.debug('Failed to install daemon:', error); throw error; } -} \ No newline at end of file +} diff --git a/cli/src/daemon/mac/uninstall.ts b/cli/src/daemon/mac/uninstall.ts index 54179816..ce2aebdb 100644 --- a/cli/src/daemon/mac/uninstall.ts +++ b/cli/src/daemon/mac/uninstall.ts @@ -1,5 +1,5 @@ /** - * Uninstallation script for Happy daemon LaunchDaemon + * Uninstallation script for HAPI daemon LaunchDaemon * * NOTE: This uninstallation method is currently NOT USED since we moved away from * system-level daemon installation. See install.ts for the full explanation. @@ -12,7 +12,7 @@ import { existsSync, unlinkSync } from 'fs'; import { execSync } from 'child_process'; import { logger } from '@/ui/logger'; -const PLIST_LABEL = 'com.happy-cli.daemon'; +const PLIST_LABEL = 'com.hapi-cli.daemon'; const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`; export async function uninstall(): Promise { @@ -42,4 +42,4 @@ export async function uninstall(): Promise { logger.debug('Failed to uninstall daemon:', error); throw error; } -} \ No newline at end of file +} diff --git a/cli/src/daemon/run.ts b/cli/src/daemon/run.ts index 448d0320..d444f2e1 100644 --- a/cli/src/daemon/run.ts +++ b/cli/src/daemon/run.ts @@ -39,8 +39,8 @@ export async function startDaemon(): Promise { // // In case the setup malfunctions - our signal handlers will not properly // shut down. We will force exit the process with code 1. - let requestShutdown: (source: 'happy-app' | 'happy-cli' | 'os-signal' | 'exception', errorMessage?: string) => void; - let resolvesWhenShutdownRequested = new Promise<({ source: 'happy-app' | 'happy-cli' | 'os-signal' | 'exception', errorMessage?: string })>((resolve) => { + let requestShutdown: (source: 'hapi-app' | 'hapi-cli' | 'os-signal' | 'exception', errorMessage?: string) => void; + let resolvesWhenShutdownRequested = new Promise<({ source: 'hapi-app' | 'hapi-cli' | 'os-signal' | 'exception', errorMessage?: string })>((resolve) => { requestShutdown = (source, errorMessage) => { logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`); @@ -132,7 +132,7 @@ export async function startDaemon(): Promise { // Helper functions const getCurrentChildren = () => Array.from(pidToTrackedSession.values()); - // Handle webhook from happy session reporting itself + // Handle webhook from HAPI session reporting itself const onHappySessionWebhook = (sessionId: string, sessionMetadata: Metadata) => { logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata); @@ -164,7 +164,7 @@ export async function startDaemon(): Promise { } else if (!existingSession) { // New session started externally const trackedSession: TrackedSession = { - startedBy: 'happy directly - likely by user from terminal', + startedBy: 'hapi directly - likely by user from terminal', happySessionId: sessionId, happySessionMetadataFromLocalWebhook: sessionMetadata, pid @@ -286,7 +286,7 @@ export async function startDaemon(): Promise { logger.debug('[DAEMON RUN] Failed to spawn process - no PID returned'); return { type: 'error', - errorMessage: 'Failed to spawn Happy process - no PID returned' + errorMessage: 'Failed to spawn HAPI process - no PID returned' }; } @@ -399,7 +399,7 @@ export async function startDaemon(): Promise { getChildren: getCurrentChildren, stopSession, spawnSession, - requestShutdown: () => requestShutdown('happy-cli'), + requestShutdown: () => requestShutdown('hapi-cli'), onHappySessionWebhook }); @@ -440,7 +440,7 @@ export async function startDaemon(): Promise { apiMachine.setRPCHandlers({ spawnSession, stopSession, - requestShutdown: () => requestShutdown('happy-app') + requestShutdown: () => requestShutdown('hapi-app') }); // Connect to server @@ -451,7 +451,7 @@ export async function startDaemon(): Promise { // 2. Check if daemon needs update // 3. If outdated, restart with latest version // 4. Write heartbeat - const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || '60000'); + const heartbeatIntervalMs = parseInt(process.env.HAPI_DAEMON_HEARTBEAT_INTERVAL || '60000'); let heartbeatRunning = false const restartOnStaleVersionAndHeartbeat = setInterval(async () => { if (heartbeatRunning) { @@ -536,7 +536,7 @@ export async function startDaemon(): Promise { }, heartbeatIntervalMs); // Every 60 seconds in production // Setup signal handlers - const cleanupAndShutdown = async (source: 'happy-app' | 'happy-cli' | 'os-signal' | 'exception', errorMessage?: string) => { + const cleanupAndShutdown = async (source: 'hapi-app' | 'hapi-cli' | 'os-signal' | 'exception', errorMessage?: string) => { logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`); // Clear health check interval diff --git a/cli/src/daemon/uninstall.ts b/cli/src/daemon/uninstall.ts index fea46da0..49467a18 100644 --- a/cli/src/daemon/uninstall.ts +++ b/cli/src/daemon/uninstall.ts @@ -10,6 +10,6 @@ export async function uninstall(): Promise { throw new Error('Daemon uninstallation requires sudo privileges. Please run with sudo.'); } - logger.info('Uninstalling Happy CLI daemon for macOS...'); + logger.info('Uninstalling HAPI CLI daemon for macOS...'); await uninstallMac(); -} \ No newline at end of file +} diff --git a/cli/src/persistence.ts b/cli/src/persistence.ts index 17e91908..2af38a77 100644 --- a/cli/src/persistence.ts +++ b/cli/src/persistence.ts @@ -1,7 +1,7 @@ /** - * Minimal persistence functions for happy CLI + * Minimal persistence functions for HAPI CLI * - * Handles settings and private key storage in ~/.happy/ or local .happy/ + * Handles settings and private key storage in ~/.config/hapi/ (or HAPI_HOME_DIR override) */ import { FileHandle } from 'node:fs/promises' @@ -320,4 +320,3 @@ export async function releaseDaemonLock(lockHandle: FileHandle): Promise { } } catch { } } - diff --git a/cli/src/ui/doctor.ts b/cli/src/ui/doctor.ts index 156a56ae..0cb520f0 100644 --- a/cli/src/ui/doctor.ts +++ b/cli/src/ui/doctor.ts @@ -24,8 +24,8 @@ export function getEnvironmentInfo(): Record { return { PWD: process.env.PWD, HAPI_HOME_DIR: process.env.HAPI_HOME_DIR, - HAPPY_BOT_URL: process.env.HAPPY_BOT_URL, - HAPPY_PROJECT_ROOT: process.env.HAPPY_PROJECT_ROOT, + HAPI_BOT_URL: process.env.HAPI_BOT_URL, + HAPI_PROJECT_ROOT: process.env.HAPI_PROJECT_ROOT, CLI_API_TOKEN_SET: Boolean(process.env.CLI_API_TOKEN), DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING, NODE_ENV: process.env.NODE_ENV, @@ -117,7 +117,7 @@ export async function runDoctorCommand(filter?: 'all' | 'daemon'): Promise console.log(chalk.bold('\n🌍 Environment Variables')); const env = getEnvironmentInfo(); console.log(`HAPI_HOME_DIR: ${env.HAPI_HOME_DIR ? chalk.green(env.HAPI_HOME_DIR) : chalk.gray('not set')}`); - console.log(`HAPPY_BOT_URL: ${env.HAPPY_BOT_URL ? chalk.green(env.HAPPY_BOT_URL) : chalk.gray('not set')}`); + console.log(`HAPI_BOT_URL: ${env.HAPI_BOT_URL ? chalk.green(env.HAPI_BOT_URL) : chalk.gray('not set')}`); console.log(`CLI_API_TOKEN: ${env.CLI_API_TOKEN_SET ? chalk.green('set') : chalk.gray('not set')}`); console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow('ENABLED') : chalk.gray('not set')}`); console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray('not set')}`); diff --git a/cli/src/ui/logger.ts b/cli/src/ui/logger.ts index e1c0442a..e1cb83bd 100644 --- a/cli/src/ui/logger.ts +++ b/cli/src/ui/logger.ts @@ -52,8 +52,8 @@ class Logger { ) { // Remote logging enabled only when explicitly set with server URL if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING - && process.env.HAPPY_BOT_URL) { - this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPPY_BOT_URL + && process.env.HAPI_BOT_URL) { + this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPI_BOT_URL console.log(chalk.yellow('[REMOTE LOGGING] Sending logs to server for AI debugging')) } } diff --git a/cli/src/utils/spawnHappyCLI.ts b/cli/src/utils/spawnHappyCLI.ts index c470c78c..3cd1cfd3 100644 --- a/cli/src/utils/spawnHappyCLI.ts +++ b/cli/src/utils/spawnHappyCLI.ts @@ -1,5 +1,5 @@ /** - * Cross-platform Happy CLI spawning utility + * Cross-platform HAPI CLI spawning utility * * ## Background * @@ -9,13 +9,13 @@ * noise from end users by passing specific flags: `--no-warnings --no-deprecation`. * * Users don't care about these technical details - they just want a clean experience - * with no warning output when using Happy. + * with no warning output when using HAPI. * * ## The Wrapper Strategy * * We created a wrapper script `bin/happy.mjs` with a shebang `#!/usr/bin/env node`. * This allows direct execution on Unix systems and NPM automatically generates - * Windows-specific wrapper scripts (`happy.cmd` and `happy.ps1`) when it sees + * Windows-specific wrapper scripts (`hapi.cmd` and `hapi.ps1`) when it sees * the `bin` field in package.json pointing to a JavaScript file with a shebang. * * The wrapper script either directly execs `dist/index.mjs` with the flags we want, @@ -24,18 +24,18 @@ * ## Execution Chains * * **Unix/Linux/macOS:** - * 1. User runs `happy` command + * 1. User runs `hapi` command * 2. Shell directly executes `bin/happy.mjs` (shebang: `#!/usr/bin/env node`) * 3. `bin/happy.mjs` either execs `node --no-warnings --no-deprecation dist/index.mjs` or imports `dist/index.mjs` directly * * **Windows:** - * 1. User runs `happy` command - * 2. NPM wrapper (`happy.cmd`) calls `node bin/happy.mjs` + * 1. User runs `hapi` command + * 2. NPM wrapper (`hapi.cmd`) calls `node bin/happy.mjs` * 3. `bin/happy.mjs` either execs `node --no-warnings --no-deprecation dist/index.mjs` or imports `dist/index.mjs` directly * * ## The Spawning Problem * - * When our code needs to spawn Happy cli as a subprocess (for daemon processes), + * When our code needs to spawn HAPI CLI as a subprocess (for daemon processes), * we were trying to execute `bin/happy.mjs` directly. This fails on Windows * because Windows doesn't understand shebangs - you get an `EFTYPE` error. * @@ -60,13 +60,13 @@ import { logger } from '@/ui/logger'; import { existsSync } from 'node:fs'; /** - * Spawn the Happy CLI with the given arguments in a cross-platform way. + * Spawn the HAPI CLI with the given arguments in a cross-platform way. * * This function bypasses the wrapper script (bin/happy.mjs) and spawns the * actual CLI entrypoint (dist/index.mjs) directly with the current runtime * (Node.js or Bun), ensuring compatibility across all platforms including Windows. * - * @param args - Arguments to pass to the Happy CLI + * @param args - Arguments to pass to the HAPI CLI * @param options - Spawn options (same as child_process.spawn) * @returns ChildProcess instance */ @@ -124,9 +124,9 @@ export function spawnHappyCLI(args: string[], options: SpawnOptions = {}): Child directory = process.cwd() } // Note: We're executing the current runtime with the calculated entrypoint path below, - // bypassing the 'happy' wrapper that would normally be found in the shell's PATH. - // However, we log it as 'happy' here because other engineers are typically looking - // for when "happy" was started and don't care about the underlying node process + // bypassing the 'hapi' wrapper that would normally be found in the shell's PATH. + // However, we log it as 'hapi' here because other engineers are typically looking + // for when "hapi" was started and don't care about the underlying node process // details and flags we use to achieve the same result. const fullCommand = `hapi ${args.join(' ')}`; logger.debug(`[SPAWN HAPI CLI] Spawning: ${fullCommand} in ${directory}`); diff --git a/server/README.md b/server/README.md new file mode 100644 index 00000000..8770f469 --- /dev/null +++ b/server/README.md @@ -0,0 +1,75 @@ +# hapi-server + +Telegram bot + HTTP API + realtime updates for hapi. + +## What it does +- Telegram bot for notifications and the Mini App entrypoint. +- HTTP API for sessions, messages, permissions, machines, and files. +- Server-Sent Events stream for live updates in the web app. +- Socket.IO channel for CLI connections. +- Serves the web app from `web/dist` or embedded assets in the single binary. +- Persists state in SQLite. + +## Typical deployment flow +1. Configure env vars. +2. Expose the server to the internet (HTTPS) if you need Telegram Mini App access. +3. Run the server. +4. Point the CLI to the server and open the web app. + +## Configuration +Required: +- `TELEGRAM_BOT_TOKEN` - token from @BotFather. +- `ALLOWED_CHAT_IDS` - comma-separated chat IDs allowed to use the bot. +- `CLI_API_TOKEN` - shared secret used by CLI and web login. + +Optional: +- `WEBAPP_PORT` - HTTP port (default: 3006). +- `WEBAPP_URL` - public URL for Telegram Mini App button. +- `CORS_ORIGINS` - comma-separated origins, or `*`. +- `HAPI_BOT_DATA_DIR` - data directory (default: ~/.hapi-server). +- `DB_PATH` - SQLite database path. + +## Running +Binary (single executable): +```bash +export TELEGRAM_BOT_TOKEN="..." +export ALLOWED_CHAT_IDS="12345678" +export CLI_API_TOKEN="shared-secret" +export WEBAPP_URL="https://your-domain.example" + +hapi server +``` + +From source: +```bash +bun install +bun run dev:server +``` + +Or inside `server/`: +```bash +bun run start +``` + +## Build for deployment +From the repo root: +```bash +bun run build:server +bun run build:web +``` + +The server build output is `server/dist/index.js`, and the web assets are in `web/dist`. + +## Networking notes +- Telegram Mini Apps require HTTPS and a public URL. If the server has no public IP, use Cloudflare Tunnel or Tailscale and set `WEBAPP_URL` to the HTTPS endpoint. +- If the web app is hosted on a different origin, set `CORS_ORIGINS` accordingly. + +## Architecture overview +The server is the hub for direct-connect mode. It accepts CLI connections over Socket.IO, exposes HTTP endpoints for the web UI, and publishes live updates over SSE. A Telegram bot provides notifications and a Mini App entrypoint. Session and machine state are stored in a local SQLite database. + +## Security model +Access is controlled by: +- Telegram chat ID allowlist. +- `CLI_API_TOKEN` shared secret for CLI and browser access. + +Transport security depends on HTTPS in front of the server. diff --git a/server/package.json b/server/package.json index 270f8c02..afd64eca 100644 --- a/server/package.json +++ b/server/package.json @@ -2,7 +2,7 @@ "name": "hapi-server", "private": true, "version": "0.1.0", - "description": "Telegram Bot client for Happy - control Claude Code sessions", + "description": "Telegram Bot client for HAPI - control Claude Code sessions", "author": "Kirill Dubovitskiy", "license": "MIT", "type": "module", diff --git a/server/src/configuration.ts b/server/src/configuration.ts index 721f9e23..c52f835e 100644 --- a/server/src/configuration.ts +++ b/server/src/configuration.ts @@ -110,8 +110,8 @@ class Configuration { } // Data directory - if (process.env.HAPPY_BOT_DATA_DIR) { - const expandedPath = process.env.HAPPY_BOT_DATA_DIR.replace(/^~/, homedir()) + if (process.env.HAPI_BOT_DATA_DIR) { + const expandedPath = process.env.HAPI_BOT_DATA_DIR.replace(/^~/, homedir()) this.dataDir = expandedPath } else { this.dataDir = join(homedir(), '.hapi-server') diff --git a/server/src/index.ts b/server/src/index.ts index 79d3bae4..4f22dda8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,7 +1,7 @@ /** - * Happy Telegram Bot - Main Entry Point + * HAPI Telegram Bot - Main Entry Point * - * This is a Telegram Bot client for Happy that provides: + * This is a Telegram Bot client for HAPI that provides: * - Session list and detail views * - Message viewing and sending * - Permission approval workflows @@ -26,7 +26,7 @@ let webServer: BunServer | null = null let sseManager: SSEManager | null = null async function main() { - console.log('Happy Bot starting...') + console.log('HAPI Bot starting...') // Load configuration (will throw if required env vars missing) const config = getConfiguration() @@ -62,7 +62,7 @@ async function main() { // Start the bot await happyBot.start() - console.log('\nHappy Bot is ready!') + console.log('\nHAPI Bot is ready!') // Handle shutdown const shutdown = async () => { diff --git a/server/src/sync/syncEngine.ts b/server/src/sync/syncEngine.ts index cbb25c18..8e78ec36 100644 --- a/server/src/sync/syncEngine.ts +++ b/server/src/sync/syncEngine.ts @@ -1,9 +1,9 @@ /** - * Sync Engine for Happy Telegram Bot (Direct Connect) + * Sync Engine for HAPI Telegram Bot (Direct Connect) * * In the direct-connect architecture: - * - happy-bot is the server (Socket.IO + REST) - * - happy-cli connects directly to the bot (no happy-server relay) + * - hapi-server is the server (Socket.IO + REST) + * - hapi CLI connects directly to the server (no relay) * - No E2E encryption; data is stored as JSON in SQLite */ diff --git a/server/src/telegram/bot.ts b/server/src/telegram/bot.ts index 95fb25a7..90ef5be2 100644 --- a/server/src/telegram/bot.ts +++ b/server/src/telegram/bot.ts @@ -1,5 +1,5 @@ /** - * Telegram Bot for Happy + * Telegram Bot for HAPI * * Main bot class that initializes grammy, applies middleware, * and sets up command handlers. @@ -27,7 +27,7 @@ export interface HappyBotConfig { } /** - * Happy Telegram Bot + * HAPI Telegram Bot */ export class HappyBot { private bot: Bot @@ -92,13 +92,13 @@ export class HappyBot { async start(): Promise { if (this.isRunning) return - console.log('[HappyBot] Starting Telegram bot...') + console.log('[HAPIBot] Starting Telegram bot...') this.isRunning = true // Start polling this.bot.start({ onStart: (botInfo) => { - console.log(`[HappyBot] Bot @${botInfo.username} started`) + console.log(`[HAPIBot] Bot @${botInfo.username} started`) } }) } @@ -109,7 +109,7 @@ export class HappyBot { async stop(): Promise { if (!this.isRunning) return - console.log('[HappyBot] Stopping Telegram bot...') + console.log('[HAPIBot] Stopping Telegram bot...') // Unsubscribe from sync events if (this.unsubscribeSyncEvents) { @@ -135,7 +135,7 @@ export class HappyBot { this.bot.use(async (ctx: BotContext, next: NextFunction) => { const chatId = ctx.chat?.id if (!chatId || !configuration.allowedChatIds.includes(chatId)) { - console.log(`[HappyBot] Rejected message from unauthorized chat: ${chatId}`) + console.log(`[HAPIBot] Rejected message from unauthorized chat: ${chatId}`) return // Silently ignore unauthorized users } await next() @@ -143,7 +143,7 @@ export class HappyBot { // Error handling middleware this.bot.catch((err) => { - console.error('[HappyBot] Error:', err.message) + console.error('[HAPIBot] Error:', err.message) }) } @@ -157,7 +157,7 @@ export class HappyBot { const machineCount = this.syncEngine?.getOnlineMachines().length ?? 0 await ctx.reply( - `Welcome to Happy Bot!\n\n` + + `Welcome to HAPI Bot!\n\n` + `Active Sessions: ${sessionCount}\n` + `Online Machines: ${machineCount}\n\n` + `Commands:\n` + @@ -169,8 +169,8 @@ export class HappyBot { // /help - Show help information this.bot.command('help', async (ctx) => { await ctx.reply( - `Happy Bot Help\n\n` + - `Happy Bot is a notification layer for Happy sessions.\n\n` + + `HAPI Bot Help\n\n` + + `HAPI Bot is a notification layer for HAPI sessions.\n\n` + `Commands:\n` + `/start - Start the bot or show status\n` + `/app - Open the Mini App\n` + @@ -186,7 +186,7 @@ export class HappyBot { // /app - Open Telegram Mini App this.bot.command('app', async (ctx) => { const keyboard = new InlineKeyboard().webApp('📱 Open App', configuration.miniAppUrl) - await ctx.reply('Open Happy Mini App:', { reply_markup: keyboard }) + await ctx.reply('Open HAPI Mini App:', { reply_markup: keyboard }) }) } @@ -265,7 +265,7 @@ export class HappyBot { if (eventType === 'ready') { this.sendReadyNotification(event.sessionId).catch((error) => { - console.error('[HappyBot] Failed to send ready notification:', error) + console.error('[HAPIBot] Failed to send ready notification:', error) }) return } @@ -273,7 +273,7 @@ export class HappyBot { if (eventType === 'switch') { const mode = messageContent?.data?.mode === 'local' ? 'local' : 'remote' this.sendSwitchNotification(event.sessionId, mode).catch((error) => { - console.error('[HappyBot] Failed to send switch notification:', error) + console.error('[HAPIBot] Failed to send switch notification:', error) }) return } @@ -289,7 +289,7 @@ export class HappyBot { } this.sendMessageNotification(event.sessionId, preview).catch((error) => { - console.error('[HappyBot] Failed to send message notification:', error) + console.error('[HAPIBot] Failed to send message notification:', error) }) } } @@ -450,7 +450,7 @@ export class HappyBot { const timer = setTimeout(() => { this.notificationDebounce.delete(currentSession.id) this.sendPermissionNotification(currentSession.id).catch(err => { - console.error('[HappyBot] Failed to send notification:', err) + console.error('[HAPIBot] Failed to send notification:', err) }) }, 500) @@ -476,7 +476,7 @@ export class HappyBot { reply_markup: keyboard }) } catch (error) { - console.error(`[HappyBot] Failed to send notification to chat ${chatId}:`, error) + console.error(`[HAPIBot] Failed to send notification to chat ${chatId}:`, error) } } } diff --git a/server/src/telegram/renderer.ts b/server/src/telegram/renderer.ts index cb4146bc..281a20be 100644 --- a/server/src/telegram/renderer.ts +++ b/server/src/telegram/renderer.ts @@ -249,7 +249,7 @@ function formatMessage(msg: DecryptedMessage): string { */ export function formatMachineList(machines: Machine[]): string { if (machines.length === 0) { - return 'No machines online.\n\nMake sure you have the Happy daemon running on your machines.' + return 'No machines online.\n\nMake sure you have the HAPI daemon running on your machines.' } let message = `Online Machines (${machines.length}):\n\n` diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..b2807c20 --- /dev/null +++ b/web/README.md @@ -0,0 +1,37 @@ +# hapi-web + +React Mini App / PWA for monitoring and controlling hapi sessions. + +## What it does +- Session list with status, pending approvals, and summaries. +- Chat view with streaming updates and message sending. +- Permission approval and denial workflows. +- Machine list and remote session spawn. +- File browser and git status/diff views. +- PWA install prompt and offline banner. + +## Runtime behavior +- When opened inside Telegram, auth uses Telegram WebApp init data. +- When opened in a normal browser, you can log in with the shared `CLI_API_TOKEN`. +- Live updates come from the server via SSE. + +## Development +From the repo root: +```bash +bun install +bun run dev:web +``` + +If testing in Telegram, set: +- `WEBAPP_URL` to the public HTTPS URL of the dev server. +- `CORS_ORIGINS` to include the dev server origin. + +## Build +```bash +bun run build:web +``` + +The built assets land in `web/dist` and are served by hapi-server. The single executable can embed these assets. + +## Stack +React 19 + Vite + TanStack Router/Query + Tailwind. diff --git a/web/src/chat/normalize.ts b/web/src/chat/normalize.ts index 1489ea27..c882c655 100644 --- a/web/src/chat/normalize.ts +++ b/web/src/chat/normalize.ts @@ -235,7 +235,7 @@ function normalizeAgentRecord( const data = isObject(content.data) ? content.data : null if (!data || typeof data.type !== 'string') return null - // Skip meta/compact-summary messages (parity with happy-app) + // Skip meta/compact-summary messages (parity with hapi-app) if (data.isMeta) return null if (data.isCompactSummary) return null diff --git a/web/src/chat/reducer.ts b/web/src/chat/reducer.ts index 4715cf28..83650786 100644 --- a/web/src/chat/reducer.ts +++ b/web/src/chat/reducer.ts @@ -47,7 +47,7 @@ function collectTitleChanges(messages: NormalizedMessage[]): Map if (msg.role !== 'agent') continue for (const content of msg.content) { if (content.type !== 'tool-call') continue - if (content.name !== 'mcp__happy__change_title') continue + if (content.name !== 'mcp__hapi__change_title') continue const title = extractTitleFromChangeTitleInput(content.input) if (!title) continue map.set(content.id, title) @@ -432,7 +432,7 @@ function reduceTimeline( } if (c.type === 'tool-call') { - if (c.name === 'mcp__happy__change_title') { + if (c.name === 'mcp__hapi__change_title') { const title = context.titleChangesByToolUseId.get(c.id) ?? extractTitleFromChangeTitleInput(c.input) if (title && !context.emittedTitleChangeToolUseIds.has(c.id)) { context.emittedTitleChangeToolUseIds.add(c.id) diff --git a/web/src/components/ToolCard/knownTools.tsx b/web/src/components/ToolCard/knownTools.tsx index b92aedbe..8f9dbde0 100644 --- a/web/src/components/ToolCard/knownTools.tsx +++ b/web/src/components/ToolCard/knownTools.tsx @@ -4,7 +4,7 @@ import { BulbIcon, ClipboardIcon, EyeIcon, FileDiffIcon, GlobeIcon, PuzzleIcon, import { basename, resolveDisplayPath } from '@/components/ToolCard/path' const DEFAULT_ICON_CLASS = 'h-3.5 w-3.5' -// Tool presentation registry for `hapi/web` (aligned with `happy-app`). +// Tool presentation registry for `hapi/web` (aligned with `hapi-app`). export type ToolPresentation = { icon: ReactNode diff --git a/web/src/index.css b/web/src/index.css index 9a39ceee..8cc0c7b8 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -18,7 +18,7 @@ --app-code-bg: #f6f8fa; --app-inline-code-bg: rgba(0, 0, 0, 0.08); - /* Diff colors (light) - from happy-app */ + /* Diff colors (light) - from hapi-app */ --app-diff-added-bg: #e6ffed; --app-diff-added-text: #24292e; --app-diff-removed-bg: #ffeef0; @@ -59,7 +59,7 @@ --app-code-bg: #282c34; --app-inline-code-bg: rgba(255, 255, 255, 0.1); - /* Diff colors (dark) - from happy-app */ + /* Diff colors (dark) - from hapi-app */ --app-diff-added-bg: #0d2e1f; --app-diff-added-text: #c9d1d9; --app-diff-removed-bg: #3f1b23;