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;