claude-code-discord-status 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ <p align="center">
2
+ <img src="./assets/hero-banner.svg" alt="claude-code-discord-status" width="900" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/claude-code-discord-status"><img src="https://img.shields.io/npm/v/claude-code-discord-status?color=5865f2&style=flat-square" alt="npm version" /></a>
7
+ <a href="https://github.com/BrunoJurkovic/claude-code-discord-status/actions"><img src="https://img.shields.io/github/actions/workflow/status/BrunoJurkovic/claude-code-discord-status/ci.yml?style=flat-square" alt="CI" /></a>
8
+ <a href="https://github.com/BrunoJurkovic/claude-code-discord-status/blob/main/LICENSE"><img src="https://img.shields.io/github/license/BrunoJurkovic/claude-code-discord-status?style=flat-square" alt="License" /></a>
9
+ <img src="https://img.shields.io/node/v/claude-code-discord-status?style=flat-square" alt="Node version" />
10
+ </p>
11
+
12
+ <p align="center">
13
+ Show what Claude Code is doing as a live Discord Rich Presence card.<br/>
14
+ Hooks into Claude Code's lifecycle events and updates your Discord status in real time.
15
+ </p>
16
+
17
+ ---
18
+
19
+ ## Preview
20
+
21
+ <p align="center">
22
+ <img src="./assets/card-single.svg" alt="Single session card" width="340" />
23
+ &nbsp;&nbsp;
24
+ <img src="./assets/card-multi.svg" alt="Multi-session card" width="340" />
25
+ </p>
26
+
27
+ <p align="center">
28
+ <b>Single session</b> — shows current action + project name &nbsp;·&nbsp; <b>Multiple sessions</b> — quirky messages + aggregate stats
29
+ </p>
30
+
31
+ ## Features
32
+
33
+ - **Live activity updates** — Your Discord card reflects what Claude is doing right now (editing, searching, running commands, thinking)
34
+ - **MCP-powered custom messages** — Claude can set its own status via `set_discord_status`, with a 30-second priority window over hook updates
35
+ - **Multi-session support** — Running multiple Claude Code instances? The card escalates with quirky messages and aggregated stats
36
+ - **Activity mode detection** — Dominant activity type (coding, terminal, searching, thinking) changes the card icon
37
+ - **Rotating tooltips** — Hidden easter eggs on hover, rotating every 5 minutes
38
+ - **Auto-reconnect** — Daemon handles Discord RPC disconnects gracefully
39
+
40
+ ## Quick Start
41
+
42
+ ### Prerequisites
43
+
44
+ - Node.js >= 18
45
+ - [jq](https://jqlang.github.io/jq/) (`brew install jq` / `apt install jq`)
46
+ - Discord desktop app running
47
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI installed
48
+
49
+ ### Setup
50
+
51
+ ```bash
52
+ npx claude-code-discord-status setup
53
+ ```
54
+
55
+ This will:
56
+
57
+ 1. Create a config at `~/.claude-discord-status/config.json`
58
+ 2. Register the MCP server with Claude Code
59
+ 3. Add lifecycle hooks to `~/.claude/settings.json`
60
+ 4. Start the daemon in the background
61
+
62
+ That's it. Your Discord status updates automatically whenever you use Claude Code.
63
+
64
+ ## How It Works
65
+
66
+ <p align="center">
67
+ <img src="./assets/architecture.svg" alt="Architecture diagram" width="800" />
68
+ </p>
69
+
70
+ Three components work together:
71
+
72
+ 1. **Hooks** — Bash scripts fired by Claude Code lifecycle events (session start/end, tool use, prompt submit). They POST updates to the daemon's HTTP API.
73
+ 2. **MCP Server** — An MCP tool (`set_discord_status`) that Claude can call to set a custom, contextual status message — these take priority for 30 seconds.
74
+ 3. **Daemon** — Background process that holds the Discord RPC connection, tracks all sessions, resolves what to show, and pushes it to Discord.
75
+
76
+ > See [docs/architecture.md](./docs/architecture.md) for the full deep dive.
77
+
78
+ ## CLI
79
+
80
+ ```bash
81
+ npx claude-code-discord-status setup # Interactive setup wizard
82
+ npx claude-code-discord-status status # Check daemon status and active sessions
83
+ npx claude-code-discord-status start -d # Start daemon in background
84
+ npx claude-code-discord-status stop # Stop the daemon
85
+ npx claude-code-discord-status uninstall # Remove everything
86
+ ```
87
+
88
+ ## Configuration
89
+
90
+ Config file: `~/.claude-discord-status/config.json`
91
+
92
+ | Key | Env Override | Default | Description |
93
+ | --- | --- | --- | --- |
94
+ | `discordClientId` | `CLAUDE_DISCORD_CLIENT_ID` | `1472915568930848829` | Discord Application Client ID |
95
+ | `daemonPort` | `CLAUDE_DISCORD_PORT` | `19452` | Local HTTP server port |
96
+
97
+ The default client ID works out of the box — it's a public app identifier, not a secret.
98
+
99
+ > See [docs/setup.md](./docs/setup.md) for all config options, timeouts, and how to use a custom Discord application.
100
+
101
+ ## Multi-Session Fun
102
+
103
+ When you're running multiple Claude Code sessions, the card gets quirky:
104
+
105
+ - **2 sessions** — _"Dual-wielding codebases"_, _"Pair programming with myself"_
106
+ - **3 sessions** — _"Triple threat detected"_, _"Three-ring circus"_
107
+ - **4 sessions** — _"4 parallel universes deep"_, _"One for each brain cell"_
108
+ - **5+ sessions** — _"Send help (5 projects)"_, _"Gone feral (6 projects)"_
109
+
110
+ Plus aggregate stats like `23 edits · 8 cmds · 2h 15m deep` and rotating hover tooltips like _"Technically I'm one Claude in a trenchcoat"_.
111
+
112
+ > See [docs/multi-session.md](./docs/multi-session.md) for the full message pool and how the resolver works.
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ git clone https://github.com/BrunoJurkovic/claude-code-discord-status.git
118
+ cd claude-code-discord-status
119
+ npm install
120
+ npm run build
121
+ npm test
122
+ ```
123
+
124
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
125
+
126
+ ## License
127
+
128
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, unlinkSync } from "fs";
5
+ import { spawn, execSync } from "child_process";
6
+ import { join as join2, dirname, resolve } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import * as p from "@clack/prompts";
9
+
10
+ // src/shared/constants.ts
11
+ import { join } from "path";
12
+ import { homedir } from "os";
13
+ var DEFAULT_PORT = 19452;
14
+ var CONFIG_DIR = join(homedir(), ".claude-discord-status");
15
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
16
+ var PID_FILE = join(CONFIG_DIR, "daemon.pid");
17
+ var LOG_FILE = join(CONFIG_DIR, "daemon.log");
18
+ var DEFAULT_DISCORD_CLIENT_ID = "1472915568930848829";
19
+ var STALE_CHECK_INTERVAL = 3e4;
20
+ var IDLE_TIMEOUT = 6e5;
21
+ var REMOVE_TIMEOUT = 18e5;
22
+
23
+ // src/shared/config.ts
24
+ import { readFileSync, existsSync } from "fs";
25
+ function loadConfig() {
26
+ let fileConfig = {};
27
+ if (existsSync(CONFIG_FILE)) {
28
+ try {
29
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
30
+ fileConfig = JSON.parse(raw);
31
+ } catch {
32
+ }
33
+ }
34
+ return {
35
+ discordClientId: process.env.CLAUDE_DISCORD_CLIENT_ID ?? fileConfig.discordClientId ?? DEFAULT_DISCORD_CLIENT_ID,
36
+ daemonPort: process.env.CLAUDE_DISCORD_PORT ? parseInt(process.env.CLAUDE_DISCORD_PORT, 10) : fileConfig.daemonPort ?? DEFAULT_PORT,
37
+ staleCheckInterval: fileConfig.staleCheckInterval ?? STALE_CHECK_INTERVAL,
38
+ idleTimeout: fileConfig.idleTimeout ?? IDLE_TIMEOUT,
39
+ removeTimeout: fileConfig.removeTimeout ?? REMOVE_TIMEOUT
40
+ };
41
+ }
42
+
43
+ // src/cli-utils.ts
44
+ var green = (s) => `\x1B[32m${s}\x1B[39m`;
45
+ var yellow = (s) => `\x1B[33m${s}\x1B[39m`;
46
+ var dim = (s) => `\x1B[2m${s}\x1B[22m`;
47
+ function formatDuration(ms) {
48
+ const seconds = Math.floor(ms / 1e3);
49
+ if (seconds < 60) return `${seconds}s`;
50
+ const minutes = Math.floor(seconds / 60);
51
+ const remainingSeconds = seconds % 60;
52
+ if (minutes < 60) {
53
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
54
+ }
55
+ const hours = Math.floor(minutes / 60);
56
+ const remainingMinutes = minutes % 60;
57
+ return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
58
+ }
59
+ function statusBadge(status) {
60
+ if (status === "active") return green("active");
61
+ if (status === "idle") return yellow("idle");
62
+ return dim(status);
63
+ }
64
+ function connectionBadge(connected) {
65
+ return connected ? green("Connected") : yellow("Connecting...");
66
+ }
67
+
68
+ // src/cli.ts
69
+ var __filename = fileURLToPath(import.meta.url);
70
+ var __dirname = dirname(__filename);
71
+ var args = process.argv.slice(2);
72
+ var command = args[0];
73
+ function getDaemonPid() {
74
+ try {
75
+ if (existsSync2(PID_FILE)) {
76
+ const pid = parseInt(readFileSync2(PID_FILE, "utf-8").trim(), 10);
77
+ process.kill(pid, 0);
78
+ return pid;
79
+ }
80
+ } catch {
81
+ try {
82
+ unlinkSync(PID_FILE);
83
+ } catch {
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ async function checkHealth() {
89
+ const config = loadConfig();
90
+ try {
91
+ const res = await fetch(`http://127.0.0.1:${config.daemonPort}/health`);
92
+ if (res.ok) {
93
+ return await res.json();
94
+ }
95
+ } catch {
96
+ }
97
+ return null;
98
+ }
99
+ async function startDaemon(background) {
100
+ p.intro("claude-discord-status");
101
+ const existing = getDaemonPid();
102
+ if (existing) {
103
+ p.log.warn(`Daemon is already running (PID ${existing})`);
104
+ p.outro();
105
+ return;
106
+ }
107
+ const daemonPath = resolve(__dirname, "daemon", "index.js");
108
+ if (!existsSync2(daemonPath)) {
109
+ p.log.error(`Daemon entry point not found at ${daemonPath}`);
110
+ p.log.info("Run `npm run build` first.");
111
+ p.outro();
112
+ process.exit(1);
113
+ }
114
+ if (background) {
115
+ mkdirSync(CONFIG_DIR, { recursive: true });
116
+ const { openSync } = await import("fs");
117
+ const logFd = openSync(LOG_FILE, "a");
118
+ const child = spawn("node", [daemonPath], {
119
+ detached: true,
120
+ stdio: ["ignore", logFd, logFd],
121
+ env: { ...process.env }
122
+ });
123
+ child.unref();
124
+ p.log.success(`Daemon started in background (PID ${child.pid})`);
125
+ p.log.info(`Log file: ${LOG_FILE}`);
126
+ p.outro();
127
+ } else {
128
+ p.log.info("Starting daemon in foreground...");
129
+ p.outro();
130
+ const child = spawn("node", [daemonPath], {
131
+ stdio: "inherit",
132
+ env: { ...process.env }
133
+ });
134
+ child.on("exit", (code) => {
135
+ process.exit(code ?? 0);
136
+ });
137
+ }
138
+ }
139
+ async function stopDaemon() {
140
+ p.intro("claude-discord-status");
141
+ const pid = getDaemonPid();
142
+ if (!pid) {
143
+ p.log.info("Daemon is not running.");
144
+ p.outro();
145
+ return;
146
+ }
147
+ try {
148
+ process.kill(pid, "SIGTERM");
149
+ p.log.success(`Daemon stopped (PID ${pid})`);
150
+ } catch {
151
+ p.log.info("Daemon process not found, cleaning up PID file.");
152
+ }
153
+ try {
154
+ unlinkSync(PID_FILE);
155
+ } catch {
156
+ }
157
+ p.outro();
158
+ }
159
+ async function showStatus() {
160
+ p.intro("claude-discord-status");
161
+ const pid = getDaemonPid();
162
+ const health = await checkHealth();
163
+ if (!pid && !health) {
164
+ p.log.info("Daemon is not running.");
165
+ p.outro();
166
+ return;
167
+ }
168
+ const lines = [];
169
+ lines.push(`PID ${pid ?? "unknown"}`);
170
+ if (health) {
171
+ lines.push(`Discord ${connectionBadge(health.connected)}`);
172
+ lines.push(`Sessions ${health.sessions} active`);
173
+ lines.push(`Uptime ${formatDuration(health.uptime * 1e3)}`);
174
+ } else {
175
+ lines.push(`Health Could not reach daemon`);
176
+ }
177
+ p.note(lines.join("\n"), "Daemon Status");
178
+ const config = loadConfig();
179
+ try {
180
+ const res = await fetch(`http://127.0.0.1:${config.daemonPort}/sessions`);
181
+ if (res.ok) {
182
+ const sessions = await res.json();
183
+ if (sessions.length > 0) {
184
+ for (const s of sessions) {
185
+ const elapsed = s.startedAt ? formatDuration(Date.now() - new Date(s.startedAt).getTime()) : "";
186
+ const badge = statusBadge(s.status);
187
+ p.log.step(`${s.projectName}
188
+ ${s.details} \u2014 ${badge}${elapsed ? ` \u2014 ${elapsed}` : ""}`);
189
+ }
190
+ }
191
+ }
192
+ } catch {
193
+ }
194
+ p.outro();
195
+ }
196
+ async function setup() {
197
+ p.intro("claude-discord-status");
198
+ const nodeVersion = process.versions.node;
199
+ const nodeMajor = parseInt(nodeVersion.split(".")[0], 10);
200
+ if (nodeMajor < 18) {
201
+ p.log.error(`Node.js >= 18 required (found ${nodeVersion})`);
202
+ p.outro();
203
+ process.exit(1);
204
+ }
205
+ let jqVersion = "";
206
+ try {
207
+ jqVersion = execSync("jq --version", { stdio: "pipe" }).toString().trim();
208
+ } catch {
209
+ p.log.error("jq is required but not found.");
210
+ p.log.info(" macOS: brew install jq");
211
+ p.log.info(" Ubuntu: sudo apt install jq");
212
+ p.outro();
213
+ process.exit(1);
214
+ }
215
+ p.log.success(`Node.js ${nodeVersion}`);
216
+ p.log.success(`jq ${jqVersion}`);
217
+ let resolvedClientId = DEFAULT_DISCORD_CLIENT_ID;
218
+ const existingConfig = existsSync2(CONFIG_FILE);
219
+ if (existingConfig) {
220
+ try {
221
+ const current = JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
222
+ if (current.discordClientId) {
223
+ resolvedClientId = current.discordClientId;
224
+ }
225
+ } catch {
226
+ }
227
+ }
228
+ const useCustomApp = await p.confirm({
229
+ message: 'Use a custom Discord app? (default shows as "Claude Code")',
230
+ initialValue: false
231
+ });
232
+ if (p.isCancel(useCustomApp)) {
233
+ p.cancel("Setup cancelled.");
234
+ process.exit(0);
235
+ }
236
+ if (useCustomApp) {
237
+ const clientId = await p.text({
238
+ message: "Discord Client ID",
239
+ placeholder: DEFAULT_DISCORD_CLIENT_ID,
240
+ validate: (value = "") => {
241
+ if (!value.trim()) return "Client ID is required";
242
+ if (!/^\d+$/.test(value.trim())) return "Client ID must be numeric";
243
+ }
244
+ });
245
+ if (p.isCancel(clientId)) {
246
+ p.cancel("Setup cancelled.");
247
+ process.exit(0);
248
+ }
249
+ resolvedClientId = clientId.trim();
250
+ }
251
+ if (resolvedClientId === DEFAULT_DISCORD_CLIENT_ID) {
252
+ p.log.info("Using default Client ID");
253
+ } else {
254
+ p.log.info(`Using custom Client ID: ${resolvedClientId}`);
255
+ }
256
+ mkdirSync(CONFIG_DIR, { recursive: true });
257
+ const config = {
258
+ discordClientId: resolvedClientId,
259
+ daemonPort: DEFAULT_PORT
260
+ };
261
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
262
+ p.log.success(`Config written to ${CONFIG_FILE}`);
263
+ let hasClaude = false;
264
+ try {
265
+ execSync("which claude", { stdio: "pipe" });
266
+ hasClaude = true;
267
+ } catch {
268
+ }
269
+ const mcpPath = resolve(__dirname, "mcp", "index.js");
270
+ if (hasClaude) {
271
+ try {
272
+ execSync(`claude mcp add --transport stdio --scope user discord-status -- node ${mcpPath}`, {
273
+ stdio: "pipe"
274
+ });
275
+ p.log.success("MCP server registered");
276
+ } catch {
277
+ p.log.warn("Could not register MCP server automatically.");
278
+ p.log.info(
279
+ ` Run: claude mcp add --transport stdio --scope user discord-status -- node ${mcpPath}`
280
+ );
281
+ }
282
+ } else {
283
+ p.log.warn("claude CLI not found \u2014 skipping MCP registration.");
284
+ p.log.info(
285
+ ` Run: claude mcp add --transport stdio --scope user discord-status -- node ${mcpPath}`
286
+ );
287
+ }
288
+ const hookScriptPath = resolve(__dirname, "..", "src", "hooks", "claude-hook.sh");
289
+ const hookCommand = existsSync2(hookScriptPath) ? hookScriptPath : "claude-hook.sh";
290
+ const claudeSettingsPath = join2(
291
+ process.env.HOME ?? process.env.USERPROFILE ?? "~",
292
+ ".claude",
293
+ "settings.json"
294
+ );
295
+ const hookConfig = createHookConfig(hookCommand);
296
+ try {
297
+ let existingSettings = {};
298
+ if (existsSync2(claudeSettingsPath)) {
299
+ existingSettings = JSON.parse(readFileSync2(claudeSettingsPath, "utf-8"));
300
+ }
301
+ const existingHooks = existingSettings.hooks ?? {};
302
+ const newHooks = hookConfig.hooks;
303
+ let hooksAdded = 0;
304
+ let hooksSkipped = 0;
305
+ for (const [event, entries] of Object.entries(newHooks)) {
306
+ if (!existingHooks[event]) {
307
+ existingHooks[event] = [];
308
+ }
309
+ for (const entry of entries) {
310
+ const entryStr = JSON.stringify(entry);
311
+ const alreadyExists = existingHooks[event].some((e) => JSON.stringify(e) === entryStr);
312
+ if (!alreadyExists) {
313
+ existingHooks[event].push(entry);
314
+ hooksAdded++;
315
+ } else {
316
+ hooksSkipped++;
317
+ }
318
+ }
319
+ }
320
+ existingSettings.hooks = existingHooks;
321
+ mkdirSync(dirname(claudeSettingsPath), { recursive: true });
322
+ writeFileSync(claudeSettingsPath, JSON.stringify(existingSettings, null, 2), "utf-8");
323
+ if (hooksAdded > 0 && hooksSkipped > 0) {
324
+ p.log.success(`Hooks configured (${hooksAdded} added, ${hooksSkipped} already present)`);
325
+ } else if (hooksAdded > 0) {
326
+ p.log.success(`Hooks configured (${hooksAdded} lifecycle events)`);
327
+ } else {
328
+ p.log.success("Hooks already configured (no changes)");
329
+ }
330
+ } catch (err) {
331
+ p.log.warn(`Could not configure hooks: ${err.message}`);
332
+ p.log.info(` Manually add hooks to ${claudeSettingsPath}`);
333
+ }
334
+ const existingPid = getDaemonPid();
335
+ if (existingPid) {
336
+ p.log.success(`Daemon already running (PID ${existingPid})`);
337
+ } else {
338
+ const daemonPath = resolve(__dirname, "daemon", "index.js");
339
+ mkdirSync(CONFIG_DIR, { recursive: true });
340
+ const { openSync } = await import("fs");
341
+ const logFd = openSync(LOG_FILE, "a");
342
+ const child = spawn("node", [daemonPath], {
343
+ detached: true,
344
+ stdio: ["ignore", logFd, logFd],
345
+ env: { ...process.env }
346
+ });
347
+ child.unref();
348
+ p.log.success(`Daemon started (PID ${child.pid})`);
349
+ }
350
+ const s = p.spinner();
351
+ s.start("Verifying Discord connection...");
352
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
353
+ const health = await checkHealth();
354
+ if (health) {
355
+ if (health.connected) {
356
+ s.stop("Discord connected");
357
+ } else {
358
+ s.stop("Discord is connecting (open Discord if not running)");
359
+ }
360
+ } else {
361
+ s.stop("Could not reach daemon \u2014 check logs");
362
+ p.log.info(` cat ${LOG_FILE}`);
363
+ }
364
+ p.note(
365
+ 'Open Discord and check your profile \u2014 you\nshould see "Using Claude Code" as activity.',
366
+ "Next steps"
367
+ );
368
+ p.outro("Setup complete!");
369
+ }
370
+ function createHookConfig(hookCommand) {
371
+ const syncHook = {
372
+ matcher: "",
373
+ hooks: [
374
+ {
375
+ type: "command",
376
+ command: hookCommand,
377
+ timeout: 5
378
+ }
379
+ ]
380
+ };
381
+ const asyncHook = (matcher) => ({
382
+ ...matcher ? { matcher } : {},
383
+ hooks: [
384
+ {
385
+ type: "command",
386
+ command: hookCommand,
387
+ timeout: 5,
388
+ async: true
389
+ }
390
+ ]
391
+ });
392
+ return {
393
+ hooks: {
394
+ SessionStart: [syncHook],
395
+ UserPromptSubmit: [asyncHook()],
396
+ PreToolUse: [asyncHook("Write|Edit|Bash|Read|Grep|Glob|WebSearch|WebFetch|Task")],
397
+ Stop: [asyncHook()],
398
+ Notification: [asyncHook()],
399
+ SessionEnd: [asyncHook()]
400
+ }
401
+ };
402
+ }
403
+ async function uninstall() {
404
+ p.intro("claude-discord-status");
405
+ const shouldContinue = await p.confirm({
406
+ message: "This will remove all hooks, MCP registration, and config. Continue?",
407
+ initialValue: false
408
+ });
409
+ if (p.isCancel(shouldContinue) || !shouldContinue) {
410
+ p.cancel("Uninstall cancelled.");
411
+ process.exit(0);
412
+ }
413
+ const pid = getDaemonPid();
414
+ if (pid) {
415
+ try {
416
+ process.kill(pid, "SIGTERM");
417
+ p.log.success(`Daemon stopped (PID ${pid})`);
418
+ } catch {
419
+ p.log.info("Daemon process not found, cleaning up PID file.");
420
+ }
421
+ try {
422
+ unlinkSync(PID_FILE);
423
+ } catch {
424
+ }
425
+ } else {
426
+ p.log.info("Daemon was not running");
427
+ }
428
+ try {
429
+ execSync("claude mcp remove discord-status", { stdio: "pipe" });
430
+ p.log.success("MCP server removed");
431
+ } catch {
432
+ p.log.warn("Could not remove MCP server (may not have been registered)");
433
+ }
434
+ const claudeSettingsPath = join2(
435
+ process.env.HOME ?? process.env.USERPROFILE ?? "~",
436
+ ".claude",
437
+ "settings.json"
438
+ );
439
+ try {
440
+ if (existsSync2(claudeSettingsPath)) {
441
+ const settings = JSON.parse(readFileSync2(claudeSettingsPath, "utf-8"));
442
+ if (settings.hooks) {
443
+ for (const event of Object.keys(settings.hooks)) {
444
+ settings.hooks[event] = settings.hooks[event].filter((entry) => {
445
+ const str = JSON.stringify(entry);
446
+ return !str.includes("claude-hook.sh");
447
+ });
448
+ if (settings.hooks[event].length === 0) {
449
+ delete settings.hooks[event];
450
+ }
451
+ }
452
+ if (Object.keys(settings.hooks).length === 0) {
453
+ delete settings.hooks;
454
+ }
455
+ writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2), "utf-8");
456
+ p.log.success("Hooks removed");
457
+ }
458
+ }
459
+ } catch {
460
+ p.log.warn("Could not clean up hooks");
461
+ }
462
+ try {
463
+ const { rmSync } = await import("fs");
464
+ rmSync(CONFIG_DIR, { recursive: true, force: true });
465
+ p.log.success("Config removed");
466
+ } catch {
467
+ p.log.warn("Could not remove config directory");
468
+ }
469
+ p.outro("Uninstall complete.");
470
+ }
471
+ function showHelp() {
472
+ p.intro("claude-discord-status");
473
+ p.note(
474
+ [
475
+ "setup Interactive setup",
476
+ "start [-d] Start the daemon (-d for background)",
477
+ "stop Stop the daemon",
478
+ "status Show daemon status and sessions",
479
+ "uninstall Remove all hooks, MCP, and config"
480
+ ].join("\n"),
481
+ "Commands"
482
+ );
483
+ p.outro("Discord Rich Presence for Claude Code");
484
+ }
485
+ switch (command) {
486
+ case "start":
487
+ await startDaemon(args.includes("-d") || args.includes("--daemon"));
488
+ break;
489
+ case "stop":
490
+ await stopDaemon();
491
+ break;
492
+ case "status":
493
+ await showStatus();
494
+ break;
495
+ case "setup":
496
+ await setup();
497
+ break;
498
+ case "uninstall":
499
+ await uninstall();
500
+ break;
501
+ default:
502
+ showHelp();
503
+ break;
504
+ }
505
+ //# sourceMappingURL=cli.js.map