happy-discord-bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +270 -0
  2. package/dist/bridge.d.ts +104 -0
  3. package/dist/bridge.js +870 -0
  4. package/dist/bridge.js.map +1 -0
  5. package/dist/cli/daemon.d.ts +10 -0
  6. package/dist/cli/daemon.js +125 -0
  7. package/dist/cli/daemon.js.map +1 -0
  8. package/dist/cli/init.d.ts +12 -0
  9. package/dist/cli/init.js +66 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/update.d.ts +3 -0
  12. package/dist/cli/update.js +127 -0
  13. package/dist/cli/update.js.map +1 -0
  14. package/dist/cli.d.ts +6 -0
  15. package/dist/cli.js +59 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/config.d.ts +14 -0
  18. package/dist/config.js +56 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/discord/bot.d.ts +71 -0
  21. package/dist/discord/bot.js +213 -0
  22. package/dist/discord/bot.js.map +1 -0
  23. package/dist/discord/buttons.d.ts +73 -0
  24. package/dist/discord/buttons.js +303 -0
  25. package/dist/discord/buttons.js.map +1 -0
  26. package/dist/discord/commands.d.ts +7 -0
  27. package/dist/discord/commands.js +448 -0
  28. package/dist/discord/commands.js.map +1 -0
  29. package/dist/discord/deploy-commands.d.ts +3 -0
  30. package/dist/discord/deploy-commands.js +41 -0
  31. package/dist/discord/deploy-commands.js.map +1 -0
  32. package/dist/discord/formatter.d.ts +27 -0
  33. package/dist/discord/formatter.js +210 -0
  34. package/dist/discord/formatter.js.map +1 -0
  35. package/dist/discord/interactions.d.ts +12 -0
  36. package/dist/discord/interactions.js +204 -0
  37. package/dist/discord/interactions.js.map +1 -0
  38. package/dist/happy/client.d.ts +34 -0
  39. package/dist/happy/client.js +127 -0
  40. package/dist/happy/client.js.map +1 -0
  41. package/dist/happy/permission-cache.d.ts +39 -0
  42. package/dist/happy/permission-cache.js +188 -0
  43. package/dist/happy/permission-cache.js.map +1 -0
  44. package/dist/happy/session-metadata.d.ts +8 -0
  45. package/dist/happy/session-metadata.js +45 -0
  46. package/dist/happy/session-metadata.js.map +1 -0
  47. package/dist/happy/skill-registry.d.ts +25 -0
  48. package/dist/happy/skill-registry.js +205 -0
  49. package/dist/happy/skill-registry.js.map +1 -0
  50. package/dist/happy/state-tracker.d.ts +13 -0
  51. package/dist/happy/state-tracker.js +41 -0
  52. package/dist/happy/state-tracker.js.map +1 -0
  53. package/dist/happy/types.d.ts +125 -0
  54. package/dist/happy/types.js +7 -0
  55. package/dist/happy/types.js.map +1 -0
  56. package/dist/happy/usage.d.ts +29 -0
  57. package/dist/happy/usage.js +21 -0
  58. package/dist/happy/usage.js.map +1 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +538 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/state-dir.d.ts +1 -0
  63. package/dist/state-dir.js +7 -0
  64. package/dist/state-dir.js.map +1 -0
  65. package/dist/store.d.ts +18 -0
  66. package/dist/store.js +26 -0
  67. package/dist/store.js.map +1 -0
  68. package/dist/vendor/api.d.ts +64 -0
  69. package/dist/vendor/api.js +142 -0
  70. package/dist/vendor/api.js.map +1 -0
  71. package/dist/vendor/config.d.ts +6 -0
  72. package/dist/vendor/config.js +9 -0
  73. package/dist/vendor/config.js.map +1 -0
  74. package/dist/vendor/credentials.d.ts +15 -0
  75. package/dist/vendor/credentials.js +56 -0
  76. package/dist/vendor/credentials.js.map +1 -0
  77. package/dist/vendor/encryption.d.ts +30 -0
  78. package/dist/vendor/encryption.js +167 -0
  79. package/dist/vendor/encryption.js.map +1 -0
  80. package/dist/version.d.ts +1 -0
  81. package/dist/version.js +23 -0
  82. package/dist/version.js.map +1 -0
  83. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # Happy Discord Bot
2
+
3
+ Discord Bot that controls Claude Code sessions via Happy Coder's relay server. Single-user private bot with per-operation HITL permission approval.
4
+
5
+ ## Features
6
+
7
+ - **Message forwarding** — Discord messages auto-forward to Claude Code, replies stream back
8
+ - **Permission HITL** — per-operation approval buttons (Yes / Allow All Edits / For This Tool / No)
9
+ - **Thread per session** — each Claude Code session maps to a Discord thread for parallel conversations
10
+ - **AskUserQuestion** — Claude's questions rendered as option buttons, answers sent as structured data
11
+ - **ExitPlanMode** — plan review with Approve / Reject (with feedback) buttons
12
+ - **TodoWrite progress** — task list displayed and updated as a pinned message
13
+ - **Typing & emoji signals** — typing indicator + thinking/tool-call emoji reactions
14
+ - **Attachment upload** — Discord file attachments written to CLI working directory via RPC
15
+ - **Permission persistence** — permission state saved per session, restored on bot restart
16
+ - **Permission replay** — pending permissions re-displayed on bot startup
17
+ - **Session management** — create, archive, delete sessions; batch cleanup of stale sessions
18
+ - **Usage tracking** — query token usage and cost per session or across all sessions
19
+
20
+ ## Installation
21
+
22
+ ### Global install (recommended)
23
+
24
+ ```bash
25
+ npm install -g happy-discord-bot
26
+ happy-discord-bot init # Interactive config setup (~/.happy-discord-bot/.env)
27
+ happy-discord-bot deploy-commands # Register Discord slash commands
28
+ happy-discord-bot daemon start # Run as background daemon
29
+ ```
30
+
31
+ `init` prompts for Discord token, channel ID, user ID, application ID, and optional Happy credentials. Config is saved to `~/.happy-discord-bot/.env` with restrictive file permissions (0600).
32
+
33
+ If you already have `~/.happy/agent.key` from `happy-agent auth login`, you can skip the Happy credential prompts during init.
34
+
35
+ ### From source (development)
36
+
37
+ ```bash
38
+ git clone https://github.com/nicholasgriffintn/happy-discord-bot
39
+ cd happy-discord-bot
40
+ npm install
41
+ cp .env.example .env # Edit with your credentials
42
+ npm run deploy-commands
43
+ npm run dev
44
+ ```
45
+
46
+ ## Prerequisites
47
+
48
+ - Node.js 20+
49
+ - A Discord Bot token
50
+ - A Happy Coder account with paired agent credentials
51
+ - `happy` CLI daemon running (`happy daemon start`) for `/new` session creation
52
+
53
+ ## Setup (from source)
54
+
55
+ ### 1. Install dependencies
56
+
57
+ ```bash
58
+ npm install
59
+ ```
60
+
61
+ ### 2. Create Discord Bot
62
+
63
+ 1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
64
+ 2. Create a new application → Bot
65
+ 3. Enable **Privileged Gateway Intents** → **Message Content Intent**
66
+ 4. Copy the Bot token
67
+
68
+ **Invite the bot** to your server:
69
+ - OAuth2 → URL Generator
70
+ - Scopes: `bot`, `applications.commands`
71
+ - Permissions: `Send Messages`, `Read Message History`, `View Channels`, `Manage Messages` (for pin/unpin), `Create Public Threads`, `Send Messages in Threads`, `Manage Threads`, `Add Reactions`
72
+
73
+ ### 3. Get Happy Coder credentials
74
+
75
+ **Option A: File-based (local development)**
76
+
77
+ ```bash
78
+ npm install -g @slopus/agent
79
+ happy-agent auth login
80
+ ```
81
+
82
+ This displays a QR code in the terminal. Scan it with the Happy Coder app:
83
+ **Settings → Account → Link New Device**. The app is required for this step — it performs a device pairing that transfers your encrypted account secret to the CLI.
84
+
85
+ Once paired, `~/.happy/agent.key` is created with your token and secret.
86
+
87
+ **Option B: Environment variables (deployment)**
88
+
89
+ Extract token and secret from an existing `agent.key`:
90
+
91
+ ```bash
92
+ cat ~/.happy/agent.key
93
+ # Output: {"token":"...","secret":"..."}
94
+ ```
95
+
96
+ Set them as `HAPPY_TOKEN` and `HAPPY_SECRET` in your environment.
97
+
98
+ ### 4. Configure environment
99
+
100
+ ```bash
101
+ cp .env.example .env
102
+ ```
103
+
104
+ Fill in:
105
+
106
+ | Variable | Required | Description |
107
+ |----------|----------|-------------|
108
+ | `DISCORD_TOKEN` | Yes | Discord Bot token |
109
+ | `DISCORD_CHANNEL_ID` | Yes | Target channel ID (right-click → Copy Channel ID) |
110
+ | `DISCORD_USER_ID` | Yes | Your Discord user ID (right-click → Copy User ID) |
111
+ | `DISCORD_APPLICATION_ID` | Yes* | Application ID (Developer Portal → General Information) |
112
+ | `DISCORD_GUILD_ID` | No | Guild ID for fast command deployment (dev only) |
113
+ | `DISCORD_REQUIRE_MENTION` | No | Require @bot mention to forward messages (default: `false`) |
114
+ | `HAPPY_TOKEN` | No** | Happy API token (env-based auth) |
115
+ | `HAPPY_SECRET` | No** | Happy account secret, base64 (env-based auth) |
116
+ | `HAPPY_SERVER_URL` | No | Override relay URL (default: `https://api.cluster-fluster.com`) |
117
+ | `BOT_STATE_DIR` | No | Directory for state.json persistence (default: `~/.happy-discord-bot`) |
118
+
119
+ \* Required for `npm run deploy-commands`.
120
+ \*\* Either set `HAPPY_TOKEN` + `HAPPY_SECRET`, or have `~/.happy/agent.key` present.
121
+
122
+ ### 5. Deploy slash commands
123
+
124
+ Register bot commands with Discord (run once, or after adding new commands):
125
+
126
+ ```bash
127
+ npm run deploy-commands
128
+ ```
129
+
130
+ If `DISCORD_GUILD_ID` is set, commands deploy to that guild instantly. Otherwise they deploy globally (up to 1 hour propagation).
131
+
132
+ ## Run
133
+
134
+ ```bash
135
+ # Development
136
+ npm run dev
137
+
138
+ # Production
139
+ npm run build
140
+ npm start
141
+ ```
142
+
143
+ ## Starting a Claude Code Session
144
+
145
+ The bot monitors Claude Code sessions running through the Happy relay. You need at least one active session for the bot to interact with.
146
+
147
+ ### Using `happy` CLI
148
+
149
+ ```bash
150
+ # Install happy CLI (if not already installed)
151
+ brew install slopus/tap/happy
152
+
153
+ # Authenticate (first time only)
154
+ happy auth
155
+
156
+ # Start a Claude Code session in any project directory
157
+ cd ~/your-project
158
+ happy
159
+ ```
160
+
161
+ The `happy auth` command will prompt you to authenticate via the Happy Coder mobile app (scan QR code) or web browser. Once authenticated, `happy` starts a Claude Code session connected through the relay.
162
+
163
+ ### How the bot discovers sessions
164
+
165
+ On startup, the bot:
166
+ 1. Lists all active sessions from the relay API
167
+ 2. Creates a Discord thread for each session (named `{directory} @ {host}`)
168
+ 3. Replays any pending permission requests as buttons
169
+ 4. Auto-selects the most recently active session
170
+
171
+ Messages sent in a thread auto-route to the bound session. Messages in the main channel go to the active session.
172
+
173
+ ## Slash Commands
174
+
175
+ | Command | Description |
176
+ |---------|-------------|
177
+ | `/sessions` | List active sessions (active / archived), with thread links |
178
+ | `/stop` | Abort the current operation (thread-aware) |
179
+ | `/compact` | Compact session context (thread-aware) |
180
+ | `/mode <mode>` | Set permission mode (default / acceptEdits / bypass / plan) |
181
+ | `/new` | Create new session — pick directory or enter custom path |
182
+ | `/archive [session]` | Kill session process, preserve data, archive thread |
183
+ | `/delete [session]` | Permanently delete session + data + thread (with confirmation) |
184
+ | `/cleanup` | Batch delete archived sessions + orphan threads (with confirmation) |
185
+ | `/usage [period]` | Token usage & cost — session-scoped in threads, account-wide in channel |
186
+ | `/skills [name] [args]` | List, search, or invoke Claude Code skills/commands (with autocomplete) |
187
+ | `/loop <args>` | Run a prompt or skill on a recurring interval (e.g. `5m /compact`) |
188
+ | `/update` | Check for updates and upgrade the bot (safe dual-process handoff) |
189
+
190
+ Commands in a thread automatically resolve to that thread's session.
191
+
192
+ ## CLI Commands
193
+
194
+ When installed globally (`npm install -g happy-discord-bot`):
195
+
196
+ ```
197
+ happy-discord-bot start # Run bot (foreground, default)
198
+ happy-discord-bot daemon start # Run as background daemon
199
+ happy-discord-bot daemon stop # Stop daemon
200
+ happy-discord-bot daemon status # Show daemon status
201
+ happy-discord-bot update # Check for updates and upgrade
202
+ happy-discord-bot init # Interactive config setup
203
+ happy-discord-bot deploy-commands # Register Discord slash commands
204
+ happy-discord-bot version # Show version
205
+ ```
206
+
207
+ ### Daemon mode
208
+
209
+ The daemon runs the bot as a detached background process. State is tracked in `~/.happy-discord-bot/daemon.state.json`.
210
+
211
+ ```bash
212
+ happy-discord-bot daemon start # Spawn detached process, print PID
213
+ happy-discord-bot daemon status # Show PID, version, start time
214
+ happy-discord-bot daemon stop # Send SIGTERM, wait for exit, clean up state
215
+ ```
216
+
217
+ ### Updating
218
+
219
+ Two ways to update to the latest version:
220
+
221
+ **From the CLI:**
222
+
223
+ ```bash
224
+ happy-discord-bot update
225
+ ```
226
+
227
+ Checks npm registry, installs the new version globally, and restarts the daemon if running.
228
+
229
+ **From Discord:**
230
+
231
+ Use the `/update` slash command. The bot performs a safe dual-process handoff:
232
+ 1. Installs new version via `npm install -g`
233
+ 2. Spawns new process with `--update-handoff`
234
+ 3. New process connects to Discord + Happy relay
235
+ 4. New process signals ready via file
236
+ 5. Old process exits gracefully
237
+
238
+ Zero downtime — the new process is fully connected before the old one shuts down.
239
+
240
+ ### Config resolution
241
+
242
+ The bot loads `.env` from the first location found:
243
+ 1. `.env` in the current working directory
244
+ 2. `~/.happy-discord-bot/.env` (created by `init`)
245
+
246
+ Override the state directory with `BOT_STATE_DIR` env var.
247
+
248
+ ## npm Scripts
249
+
250
+ ```bash
251
+ npm run dev # Development with tsx
252
+ npm run build # Compile TypeScript to dist/
253
+ npm start # Run compiled JS
254
+ npm run deploy-commands # Register Discord slash commands (run once)
255
+ npm run lint # ESLint
256
+ npm run lint:fix # ESLint auto-fix
257
+ npm test # Run tests (vitest)
258
+ npm run test:watch # Run tests in watch mode
259
+ npm run test:e2e # E2E smoke tests (requires .env.e2e, real services)
260
+ ```
261
+
262
+ ## Testing
263
+
264
+ Unit/integration tests use Vitest with all external dependencies mocked:
265
+
266
+ ```bash
267
+ npm test
268
+ ```
269
+
270
+ E2E smoke tests require a second Discord bot, a dedicated test channel, and a running `happy` daemon. See `e2e/` directory and `.env.e2e.example` for setup.
@@ -0,0 +1,104 @@
1
+ import type { HappyClient } from './happy/client.js';
2
+ import type { DiscordBot } from './discord/bot.js';
3
+ import type { BotConfig } from './config.js';
4
+ import { type DecryptedSession } from './vendor/api.js';
5
+ import type { StateTracker } from './happy/state-tracker.js';
6
+ import type { PermissionCache } from './happy/permission-cache.js';
7
+ import type { Store } from './store.js';
8
+ import type { PermissionMode } from './happy/types.js';
9
+ import { SkillRegistry } from './happy/skill-registry.js';
10
+ export interface DiscordAttachment {
11
+ url: string;
12
+ name: string;
13
+ contentType: string | null;
14
+ size: number;
15
+ }
16
+ export declare class Bridge {
17
+ private readonly happy;
18
+ private readonly discord;
19
+ private readonly config;
20
+ private readonly stateTracker;
21
+ private readonly permissionCache;
22
+ private readonly multiSelectState;
23
+ private readonly sessionToThread;
24
+ private readonly threadToSession;
25
+ private readonly sessionDirMap;
26
+ readonly skillRegistry: SkillRegistry;
27
+ private activeSessionId;
28
+ private store;
29
+ private disconnectTimer;
30
+ private initialConnectDone;
31
+ private readonly sessionLastMsg;
32
+ private readonly sessionTyping;
33
+ private readonly sessionTodoMsgId;
34
+ private readonly sessionCompactReply;
35
+ private readonly pendingResponseTimers;
36
+ constructor(happy: HappyClient, discord: DiscordBot, config: BotConfig, stateTracker: StateTracker, permissionCache: PermissionCache);
37
+ setActiveSession(sessionId: string): void;
38
+ get activeSession(): string | null;
39
+ isSessionAvailable(sessionId: string): boolean;
40
+ get happyClient(): HappyClient;
41
+ get permissions(): PermissionCache;
42
+ setStore(store: Store): void;
43
+ persistModes(): void;
44
+ setLastUserMessageId(messageId: string, threadId?: string, sessionId?: string): void;
45
+ /** Handle ephemeral activity update (session-alive keepalive). */
46
+ handleEphemeralActivity(sessionId: string, thinking: boolean): void;
47
+ sendMessage(text: string, sessionId: string): Promise<void>;
48
+ uploadAttachments(attachments: readonly DiscordAttachment[]): Promise<string[]>;
49
+ start(): Promise<void>;
50
+ /** Create threads for sessions that don't have one. Call after Discord bot is online. */
51
+ ensureThreadsForSessions(): Promise<void>;
52
+ /** Replay pending permission requests from all active sessions. Call after Discord bot is online. */
53
+ replayPendingPermissions(): Promise<void>;
54
+ listSessions(): Promise<DecryptedSession[]>;
55
+ listAllSessions(): Promise<DecryptedSession[]>;
56
+ createNewSession(machineId: string, directory: string): Promise<string>;
57
+ archiveSession(sessionId?: string): Promise<string>;
58
+ deleteSession(sessionId?: string): Promise<string>;
59
+ cleanupArchivedSessions(): Promise<{
60
+ sessions: number;
61
+ threads: number;
62
+ }>;
63
+ /** Process a raw update from the Happy relay. Public for testing. */
64
+ processUpdate(data: unknown): void;
65
+ stopSession(sessionId?: string): Promise<void>;
66
+ compactSession(sessionId: string, messageId: string): Promise<void>;
67
+ approvePermission(sessionId: string, requestId: string, mode?: PermissionMode, allowTools?: string[], answers?: Record<string, string>): Promise<void>;
68
+ denyPermission(sessionId: string, requestId: string, reason?: string): Promise<void>;
69
+ handleAskUserAnswer(sessionId: string, requestId: string, answers: Record<string, string>): Promise<void>;
70
+ toggleMultiSelect(key: string, optionIndex: number): ReadonlySet<number>;
71
+ getMultiSelectState(key: string): ReadonlySet<number>;
72
+ clearMultiSelectState(key: string): void;
73
+ setThread(sessionId: string, threadId: string): void;
74
+ removeThread(sessionId: string): void;
75
+ getThreadId(sessionId: string): string | null;
76
+ getSessionByThread(threadId: string): string | null;
77
+ getSessionProjectDir(sessionId: string): string | undefined;
78
+ getProjectDirForThread(threadId: string): string | undefined;
79
+ getAllProjectDirs(): string[];
80
+ getThreadMap(): Record<string, string>;
81
+ loadThreadMap(map: Record<string, string>): void;
82
+ ensureThread(sessionId: string, metadata: unknown): Promise<string>;
83
+ private cancelDisconnectTimer;
84
+ private getSessionTypingState;
85
+ private startTypingLoop;
86
+ private stopTypingLoop;
87
+ private updateThinkingEmoji;
88
+ private updateToolUseEmoji;
89
+ private sendToSession;
90
+ private sendWithButtonsToSession;
91
+ private sendWithAttachmentToSession;
92
+ private handleSessionUpdate;
93
+ private handlePermissionRequest;
94
+ private loadSessions;
95
+ private waitForSession;
96
+ private handleNewMessage;
97
+ private handleCompactionComplete;
98
+ private startResponseTimer;
99
+ private clearResponseTimer;
100
+ private handleTodoWrite;
101
+ /** Resolve a session ID prefix to a full session ID via the thread map. */
102
+ private resolveFullSessionId;
103
+ private requireActiveSession;
104
+ }