@delt/claude-alarm 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/dist/cli.js ADDED
@@ -0,0 +1,921 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/shared/constants.ts
13
+ import path from "path";
14
+ import os from "os";
15
+ var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, WS_PATH_CHANNEL, WS_PATH_DASHBOARD;
16
+ var init_constants = __esm({
17
+ "src/shared/constants.ts"() {
18
+ "use strict";
19
+ DEFAULT_HUB_HOST = "127.0.0.1";
20
+ DEFAULT_HUB_PORT = 7890;
21
+ CONFIG_DIR = path.join(os.homedir(), ".claude-alarm");
22
+ CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
23
+ PID_FILE = path.join(CONFIG_DIR, "hub.pid");
24
+ LOG_FILE = path.join(CONFIG_DIR, "hub.log");
25
+ WS_PATH_CHANNEL = "/ws/channel";
26
+ WS_PATH_DASHBOARD = "/ws/dashboard";
27
+ }
28
+ });
29
+
30
+ // src/shared/logger.ts
31
+ var logger;
32
+ var init_logger = __esm({
33
+ "src/shared/logger.ts"() {
34
+ "use strict";
35
+ logger = {
36
+ info(msg, ...args) {
37
+ console.error(`[claude-alarm] ${msg}`, ...args);
38
+ },
39
+ warn(msg, ...args) {
40
+ console.error(`[claude-alarm WARN] ${msg}`, ...args);
41
+ },
42
+ error(msg, ...args) {
43
+ console.error(`[claude-alarm ERROR] ${msg}`, ...args);
44
+ },
45
+ debug(msg, ...args) {
46
+ if (process.env.CLAUDE_ALARM_DEBUG) {
47
+ console.error(`[claude-alarm DEBUG] ${msg}`, ...args);
48
+ }
49
+ }
50
+ };
51
+ }
52
+ });
53
+
54
+ // src/hub/session-manager.ts
55
+ var SessionManager;
56
+ var init_session_manager = __esm({
57
+ "src/hub/session-manager.ts"() {
58
+ "use strict";
59
+ SessionManager = class {
60
+ sessions = /* @__PURE__ */ new Map();
61
+ register(session) {
62
+ this.sessions.set(session.id, { ...session });
63
+ }
64
+ unregister(sessionId) {
65
+ const session = this.sessions.get(sessionId);
66
+ this.sessions.delete(sessionId);
67
+ return session;
68
+ }
69
+ updateStatus(sessionId, status) {
70
+ const session = this.sessions.get(sessionId);
71
+ if (session) {
72
+ session.status = status;
73
+ session.lastActivity = Date.now();
74
+ }
75
+ return session;
76
+ }
77
+ updateActivity(sessionId) {
78
+ const session = this.sessions.get(sessionId);
79
+ if (session) {
80
+ session.lastActivity = Date.now();
81
+ }
82
+ }
83
+ get(sessionId) {
84
+ return this.sessions.get(sessionId);
85
+ }
86
+ getAll() {
87
+ return Array.from(this.sessions.values());
88
+ }
89
+ count() {
90
+ return this.sessions.size;
91
+ }
92
+ };
93
+ }
94
+ });
95
+
96
+ // src/hub/notifier.ts
97
+ import notifier from "node-notifier";
98
+ import { execFile } from "child_process";
99
+ var Notifier;
100
+ var init_notifier = __esm({
101
+ "src/hub/notifier.ts"() {
102
+ "use strict";
103
+ init_logger();
104
+ Notifier = class {
105
+ webhooks = [];
106
+ desktopEnabled = true;
107
+ notificationSettingsOpened = false;
108
+ dashboardUrl;
109
+ configure(options) {
110
+ if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;
111
+ if (options.desktop !== void 0) this.desktopEnabled = options.desktop;
112
+ if (options.webhooks) this.webhooks = options.webhooks;
113
+ }
114
+ async notify(title, message, level = "info") {
115
+ const promises = [];
116
+ if (this.desktopEnabled) {
117
+ promises.push(this.sendDesktop(title, message, level));
118
+ }
119
+ for (const webhook of this.webhooks) {
120
+ promises.push(this.sendWebhook(webhook, title, message, level));
121
+ }
122
+ await Promise.allSettled(promises);
123
+ }
124
+ async sendDesktop(title, message, _level) {
125
+ if (process.platform === "win32") {
126
+ const enabled = await this.checkWindowsNotifications();
127
+ if (!enabled) {
128
+ this.openNotificationSettings();
129
+ return;
130
+ }
131
+ }
132
+ return new Promise((resolve) => {
133
+ const notification = notifier.notify(
134
+ {
135
+ title: `Claude Alarm: ${title}`,
136
+ message,
137
+ sound: true,
138
+ wait: true
139
+ },
140
+ (err) => {
141
+ if (err) {
142
+ logger.warn(`Desktop notification failed: ${err.message}`);
143
+ }
144
+ resolve();
145
+ }
146
+ );
147
+ if (this.dashboardUrl && notification) {
148
+ const url = this.dashboardUrl;
149
+ notification.on("click", () => {
150
+ execFile("powershell", ["-Command", `Start-Process "${url}"`]);
151
+ });
152
+ }
153
+ });
154
+ }
155
+ checkWindowsNotifications() {
156
+ return new Promise((resolve) => {
157
+ execFile(
158
+ "powershell",
159
+ ["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
160
+ (err, stdout) => {
161
+ if (err) {
162
+ resolve(true);
163
+ return;
164
+ }
165
+ const value = stdout.trim();
166
+ resolve(value !== "0");
167
+ }
168
+ );
169
+ });
170
+ }
171
+ openNotificationSettings() {
172
+ if (this.notificationSettingsOpened) return;
173
+ this.notificationSettingsOpened = true;
174
+ logger.warn("Windows notifications are disabled. Opening notification settings...");
175
+ logger.warn("Please enable notifications for this app, then try again.");
176
+ if (process.platform === "win32") {
177
+ execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"]);
178
+ }
179
+ setTimeout(() => {
180
+ this.notificationSettingsOpened = false;
181
+ }, 5 * 60 * 1e3);
182
+ }
183
+ async sendWebhook(webhook, title, message, level) {
184
+ try {
185
+ const response = await fetch(webhook.url, {
186
+ method: "POST",
187
+ headers: {
188
+ "Content-Type": "application/json",
189
+ ...webhook.headers
190
+ },
191
+ body: JSON.stringify({
192
+ title,
193
+ message,
194
+ level,
195
+ timestamp: Date.now(),
196
+ source: "claude-alarm"
197
+ })
198
+ });
199
+ if (!response.ok) {
200
+ logger.warn(`Webhook ${webhook.url} returned ${response.status}`);
201
+ }
202
+ } catch (err) {
203
+ logger.warn(`Webhook ${webhook.url} failed: ${err.message}`);
204
+ }
205
+ }
206
+ };
207
+ }
208
+ });
209
+
210
+ // src/hub/server.ts
211
+ var server_exports = {};
212
+ __export(server_exports, {
213
+ HubServer: () => HubServer
214
+ });
215
+ import http from "http";
216
+ import fs2 from "fs";
217
+ import path3 from "path";
218
+ import { fileURLToPath } from "url";
219
+ import { WebSocketServer, WebSocket } from "ws";
220
+ var __dirname, HubServer;
221
+ var init_server = __esm({
222
+ "src/hub/server.ts"() {
223
+ "use strict";
224
+ init_logger();
225
+ init_constants();
226
+ init_session_manager();
227
+ init_notifier();
228
+ __dirname = path3.dirname(fileURLToPath(import.meta.url));
229
+ HubServer = class {
230
+ httpServer;
231
+ wssChannel;
232
+ wssDashboard;
233
+ sessions = new SessionManager();
234
+ notifier = new Notifier();
235
+ startTime = Date.now();
236
+ // Map sessionId -> channel WebSocket
237
+ channelSockets = /* @__PURE__ */ new Map();
238
+ // All connected dashboard WebSockets
239
+ dashboardSockets = /* @__PURE__ */ new Set();
240
+ host;
241
+ port;
242
+ token;
243
+ constructor(config) {
244
+ this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
245
+ this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
246
+ this.token = config?.hub?.token;
247
+ if (config?.notifications) {
248
+ this.notifier.configure({
249
+ desktop: config.notifications.desktop
250
+ });
251
+ }
252
+ if (config?.webhooks) {
253
+ this.notifier.configure({ webhooks: config.webhooks });
254
+ }
255
+ this.notifier.configure({ dashboardUrl: `http://${this.host}:${this.port}` });
256
+ this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
257
+ this.wssChannel = new WebSocketServer({ noServer: true });
258
+ this.wssChannel.on("connection", (ws) => this.handleChannelConnection(ws));
259
+ this.wssDashboard = new WebSocketServer({ noServer: true });
260
+ this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
261
+ this.httpServer.on("upgrade", (req, socket, head) => {
262
+ const url = new URL(req.url, `http://${req.headers.host}`);
263
+ const pathname = url.pathname;
264
+ if (this.token && !this.isLocalRequest(req)) {
265
+ const wsToken = url.searchParams.get("token");
266
+ if (wsToken !== this.token) {
267
+ socket.destroy();
268
+ return;
269
+ }
270
+ }
271
+ if (pathname === WS_PATH_CHANNEL) {
272
+ this.wssChannel.handleUpgrade(req, socket, head, (ws) => {
273
+ this.wssChannel.emit("connection", ws, req);
274
+ });
275
+ } else if (pathname === WS_PATH_DASHBOARD) {
276
+ this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
277
+ this.wssDashboard.emit("connection", ws, req);
278
+ });
279
+ } else {
280
+ socket.destroy();
281
+ }
282
+ });
283
+ }
284
+ async start() {
285
+ return new Promise((resolve, reject) => {
286
+ this.httpServer.on("error", reject);
287
+ this.httpServer.listen(this.port, this.host, () => {
288
+ logger.info(`Hub server listening on http://${this.host}:${this.port}`);
289
+ resolve();
290
+ });
291
+ });
292
+ }
293
+ stop() {
294
+ return new Promise((resolve) => {
295
+ for (const ws of this.channelSockets.values()) ws.close();
296
+ for (const ws of this.dashboardSockets) ws.close();
297
+ this.wssChannel.close();
298
+ this.wssDashboard.close();
299
+ this.httpServer.close(() => {
300
+ logger.info("Hub server stopped");
301
+ resolve();
302
+ });
303
+ });
304
+ }
305
+ // --- HTTP Handler ---
306
+ handleHttp(req, res) {
307
+ const url = new URL(req.url, `http://${req.headers.host}`);
308
+ const origin = req.headers.origin;
309
+ if (origin && (origin.includes("127.0.0.1") || origin.includes("localhost"))) {
310
+ res.setHeader("Access-Control-Allow-Origin", origin);
311
+ }
312
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
313
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
314
+ if (req.method === "OPTIONS") {
315
+ res.writeHead(204);
316
+ res.end();
317
+ return;
318
+ }
319
+ if (url.pathname !== "/" && this.token) {
320
+ if (!this.isLocalRequest(req)) {
321
+ const authHeader = req.headers["authorization"];
322
+ const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
323
+ if (bearerToken !== this.token) {
324
+ this.jsonResponse(res, 401, { error: "Unauthorized" });
325
+ return;
326
+ }
327
+ }
328
+ }
329
+ if (url.pathname === "/" && req.method === "GET") {
330
+ this.serveDashboard(res);
331
+ } else if (url.pathname === "/api/sessions" && req.method === "GET") {
332
+ this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });
333
+ } else if (url.pathname === "/api/status" && req.method === "GET") {
334
+ this.jsonResponse(res, 200, {
335
+ running: true,
336
+ pid: process.pid,
337
+ port: this.port,
338
+ sessions: this.sessions.count(),
339
+ uptime: Date.now() - this.startTime
340
+ });
341
+ } else if (url.pathname === "/api/send" && req.method === "POST") {
342
+ this.handleApiSend(req, res);
343
+ } else if (url.pathname === "/api/notify" && req.method === "POST") {
344
+ this.handleApiNotify(req, res);
345
+ } else {
346
+ this.jsonResponse(res, 404, { error: "Not found" });
347
+ }
348
+ }
349
+ serveDashboard(res) {
350
+ const candidates = [
351
+ path3.join(__dirname, "..", "dashboard", "index.html"),
352
+ // from dist/hub/
353
+ path3.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
354
+ // from dist/hub/ -> src/
355
+ path3.join(process.cwd(), "dist", "dashboard", "index.html"),
356
+ // from cwd
357
+ path3.join(process.cwd(), "src", "dashboard", "index.html")
358
+ // from cwd/src
359
+ ];
360
+ logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
361
+ for (const candidate of candidates) {
362
+ if (fs2.existsSync(candidate)) {
363
+ const html = fs2.readFileSync(candidate, "utf-8");
364
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
365
+ res.end(html);
366
+ return;
367
+ }
368
+ }
369
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
370
+ res.end("<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>");
371
+ }
372
+ async handleApiSend(req, res) {
373
+ const body = await this.readBody(req);
374
+ if (!body) {
375
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
376
+ return;
377
+ }
378
+ const { sessionId, content } = body;
379
+ if (!sessionId || !content) {
380
+ this.jsonResponse(res, 400, { error: "sessionId and content are required" });
381
+ return;
382
+ }
383
+ const ws = this.channelSockets.get(sessionId);
384
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
385
+ this.jsonResponse(res, 404, { error: "Session not connected" });
386
+ return;
387
+ }
388
+ const msg = { type: "message_to_session", sessionId, content };
389
+ ws.send(JSON.stringify(msg));
390
+ this.jsonResponse(res, 200, { ok: true });
391
+ }
392
+ async handleApiNotify(req, res) {
393
+ const body = await this.readBody(req);
394
+ if (!body) {
395
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
396
+ return;
397
+ }
398
+ const { title, message, level } = body;
399
+ if (!title || !message) {
400
+ this.jsonResponse(res, 400, { error: "title and message are required" });
401
+ return;
402
+ }
403
+ await this.notifier.notify(title, message, level ?? "info");
404
+ this.jsonResponse(res, 200, { ok: true });
405
+ }
406
+ // --- Channel WebSocket ---
407
+ handleChannelConnection(ws) {
408
+ logger.info("Channel server connected");
409
+ ws.on("message", (data) => {
410
+ try {
411
+ const msg = JSON.parse(data.toString());
412
+ this.handleChannelMessage(ws, msg);
413
+ } catch {
414
+ logger.warn("Invalid message from channel");
415
+ }
416
+ });
417
+ ws.on("close", () => {
418
+ for (const [sessionId, sock] of this.channelSockets) {
419
+ if (sock === ws) {
420
+ const session = this.sessions.unregister(sessionId);
421
+ this.channelSockets.delete(sessionId);
422
+ logger.info(`Channel disconnected: ${sessionId}`);
423
+ this.broadcastToDashboards({
424
+ type: "session_disconnected",
425
+ sessionId
426
+ });
427
+ break;
428
+ }
429
+ }
430
+ });
431
+ }
432
+ handleChannelMessage(ws, msg) {
433
+ switch (msg.type) {
434
+ case "register": {
435
+ const session = msg.session;
436
+ this.sessions.register(session);
437
+ this.channelSockets.set(session.id, ws);
438
+ logger.info(`Session registered: ${session.id} (${session.name})`);
439
+ this.broadcastToDashboards({ type: "session_connected", session });
440
+ break;
441
+ }
442
+ case "status": {
443
+ const updated = this.sessions.updateStatus(msg.sessionId, msg.status);
444
+ if (updated) {
445
+ this.broadcastToDashboards({ type: "session_updated", session: updated });
446
+ }
447
+ break;
448
+ }
449
+ case "notify": {
450
+ this.sessions.updateActivity(msg.sessionId);
451
+ this.notifier.notify(msg.title, msg.message, msg.level ?? "info");
452
+ this.broadcastToDashboards({
453
+ type: "notification",
454
+ sessionId: msg.sessionId,
455
+ title: msg.title,
456
+ message: msg.message,
457
+ level: msg.level,
458
+ timestamp: Date.now()
459
+ });
460
+ break;
461
+ }
462
+ case "reply": {
463
+ this.sessions.updateActivity(msg.sessionId);
464
+ this.broadcastToDashboards({
465
+ type: "reply_from_session",
466
+ sessionId: msg.sessionId,
467
+ content: msg.content,
468
+ timestamp: Date.now()
469
+ });
470
+ break;
471
+ }
472
+ }
473
+ }
474
+ // --- Dashboard WebSocket ---
475
+ handleDashboardConnection(ws) {
476
+ this.dashboardSockets.add(ws);
477
+ logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);
478
+ const sessionsMsg = {
479
+ type: "sessions_list",
480
+ sessions: this.sessions.getAll()
481
+ };
482
+ ws.send(JSON.stringify(sessionsMsg));
483
+ ws.on("message", (data) => {
484
+ try {
485
+ const msg = JSON.parse(data.toString());
486
+ if (msg.type === "message_to_session") {
487
+ const channelWs = this.channelSockets.get(msg.sessionId);
488
+ if (channelWs?.readyState === WebSocket.OPEN) {
489
+ channelWs.send(JSON.stringify(msg));
490
+ }
491
+ }
492
+ } catch {
493
+ logger.warn("Invalid message from dashboard");
494
+ }
495
+ });
496
+ ws.on("close", () => {
497
+ this.dashboardSockets.delete(ws);
498
+ logger.info(`Dashboard disconnected (total: ${this.dashboardSockets.size})`);
499
+ });
500
+ }
501
+ // --- Helpers ---
502
+ broadcastToDashboards(msg) {
503
+ const payload = JSON.stringify(msg);
504
+ for (const ws of this.dashboardSockets) {
505
+ if (ws.readyState === WebSocket.OPEN) {
506
+ ws.send(payload);
507
+ }
508
+ }
509
+ }
510
+ jsonResponse(res, status, body) {
511
+ res.writeHead(status, { "Content-Type": "application/json" });
512
+ res.end(JSON.stringify(body));
513
+ }
514
+ isLocalRequest(req) {
515
+ const addr = req.socket.remoteAddress;
516
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
517
+ }
518
+ readBody(req, maxSize = 1024 * 1024) {
519
+ return new Promise((resolve) => {
520
+ let data = "";
521
+ let size = 0;
522
+ req.on("data", (chunk) => {
523
+ size += chunk.length;
524
+ if (size > maxSize) {
525
+ req.destroy();
526
+ resolve(null);
527
+ return;
528
+ }
529
+ data += chunk;
530
+ });
531
+ req.on("end", () => {
532
+ try {
533
+ resolve(JSON.parse(data));
534
+ } catch {
535
+ resolve(null);
536
+ }
537
+ });
538
+ });
539
+ }
540
+ };
541
+ if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
542
+ const hub = new HubServer();
543
+ hub.start().catch((err) => {
544
+ logger.error("Failed to start hub:", err);
545
+ process.exit(1);
546
+ });
547
+ const shutdown = () => {
548
+ hub.stop().then(() => process.exit(0));
549
+ };
550
+ process.on("SIGINT", shutdown);
551
+ process.on("SIGTERM", shutdown);
552
+ }
553
+ }
554
+ });
555
+
556
+ // src/cli.ts
557
+ import { spawn } from "child_process";
558
+ import fs3 from "fs";
559
+ import path4 from "path";
560
+ import readline from "readline";
561
+ import { fileURLToPath as fileURLToPath2 } from "url";
562
+
563
+ // src/shared/config.ts
564
+ init_constants();
565
+ import fs from "fs";
566
+ import path2 from "path";
567
+ import { randomUUID } from "crypto";
568
+ var DEFAULT_CONFIG = {
569
+ hub: {
570
+ host: DEFAULT_HUB_HOST,
571
+ port: DEFAULT_HUB_PORT
572
+ },
573
+ notifications: {
574
+ desktop: true,
575
+ sound: true
576
+ },
577
+ webhooks: []
578
+ };
579
+ function ensureConfigDir() {
580
+ if (!fs.existsSync(CONFIG_DIR)) {
581
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
582
+ }
583
+ }
584
+ function loadConfig() {
585
+ ensureConfigDir();
586
+ let config;
587
+ if (!fs.existsSync(CONFIG_FILE)) {
588
+ config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
589
+ } else {
590
+ try {
591
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
592
+ const parsed = JSON.parse(raw);
593
+ config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };
594
+ } catch {
595
+ config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
596
+ }
597
+ }
598
+ if (!config.hub.token) {
599
+ config.hub.token = randomUUID();
600
+ saveConfig(config);
601
+ }
602
+ return config;
603
+ }
604
+ function getOrCreateToken() {
605
+ const config = loadConfig();
606
+ return config.hub.token;
607
+ }
608
+ function saveConfig(config) {
609
+ ensureConfigDir();
610
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
611
+ }
612
+ function setupMcpConfig(targetDir) {
613
+ const dir = targetDir ?? process.cwd();
614
+ const mcpPath = path2.join(dir, ".mcp.json");
615
+ let mcpConfig = {};
616
+ if (fs.existsSync(mcpPath)) {
617
+ try {
618
+ mcpConfig = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
619
+ } catch {
620
+ mcpConfig = {};
621
+ }
622
+ }
623
+ if (!mcpConfig.mcpServers) {
624
+ mcpConfig.mcpServers = {};
625
+ }
626
+ mcpConfig.mcpServers["claude-alarm"] = {
627
+ command: "npx",
628
+ args: ["-y", "@delt/claude-alarm"],
629
+ env: {
630
+ CLAUDE_ALARM_SESSION_NAME: path2.basename(dir)
631
+ }
632
+ };
633
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
634
+ return mcpPath;
635
+ }
636
+
637
+ // src/cli.ts
638
+ init_constants();
639
+ init_logger();
640
+ var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
641
+ function printUsage() {
642
+ console.log(`
643
+ claude-alarm - Monitor Claude Code sessions with notifications
644
+
645
+ Usage:
646
+ claude-alarm init Setup everything and show next steps
647
+ claude-alarm hub start [-d] Start the hub server (-d for daemon)
648
+ claude-alarm hub stop Stop the hub daemon
649
+ claude-alarm hub status Show hub status
650
+ claude-alarm setup [dir] Add claude-alarm to .mcp.json
651
+ claude-alarm test Send a test notification
652
+ claude-alarm token Show current auth token
653
+ claude-alarm help Show this help
654
+
655
+ Quick start:
656
+ claude-alarm init
657
+ `);
658
+ }
659
+ async function hubStart(daemon) {
660
+ const config = loadConfig();
661
+ const host = config.hub.host ?? DEFAULT_HUB_HOST;
662
+ const port = config.hub.port ?? DEFAULT_HUB_PORT;
663
+ if (fs3.existsSync(PID_FILE)) {
664
+ const pid = parseInt(fs3.readFileSync(PID_FILE, "utf-8").trim(), 10);
665
+ if (isProcessRunning(pid)) {
666
+ console.log(`Hub is already running (PID: ${pid}) on http://${host}:${port}`);
667
+ return;
668
+ }
669
+ fs3.unlinkSync(PID_FILE);
670
+ }
671
+ if (daemon) {
672
+ ensureConfigDir();
673
+ const logFd = fs3.openSync(LOG_FILE, "a");
674
+ const hubScript = path4.join(__dirname2, "hub", "server.js");
675
+ const child = spawn(process.execPath, [hubScript], {
676
+ detached: true,
677
+ stdio: ["ignore", logFd, logFd],
678
+ env: { ...process.env }
679
+ });
680
+ if (child.pid) {
681
+ fs3.writeFileSync(PID_FILE, String(child.pid), "utf-8");
682
+ child.unref();
683
+ console.log(`Hub started as daemon (PID: ${child.pid})`);
684
+ console.log(`Dashboard: http://${host}:${port}`);
685
+ console.log(`Token: ${config.hub.token}`);
686
+ console.log(`Logs: ${LOG_FILE}`);
687
+ } else {
688
+ console.error("Failed to start hub daemon");
689
+ process.exit(1);
690
+ }
691
+ } else {
692
+ console.log(`Starting hub on http://${host}:${port} (press Ctrl+C to stop)`);
693
+ console.log(`Token: ${config.hub.token}`);
694
+ const { HubServer: HubServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
695
+ const hub = new HubServer2(config);
696
+ await hub.start();
697
+ ensureConfigDir();
698
+ fs3.writeFileSync(PID_FILE, String(process.pid), "utf-8");
699
+ const shutdown = async () => {
700
+ console.log("\nShutting down...");
701
+ await hub.stop();
702
+ if (fs3.existsSync(PID_FILE)) fs3.unlinkSync(PID_FILE);
703
+ process.exit(0);
704
+ };
705
+ process.on("SIGINT", shutdown);
706
+ process.on("SIGTERM", shutdown);
707
+ }
708
+ }
709
+ function hubStop() {
710
+ if (!fs3.existsSync(PID_FILE)) {
711
+ console.log("Hub is not running (no PID file found)");
712
+ return;
713
+ }
714
+ const pid = parseInt(fs3.readFileSync(PID_FILE, "utf-8").trim(), 10);
715
+ try {
716
+ process.kill(pid, "SIGTERM");
717
+ console.log(`Hub stopped (PID: ${pid})`);
718
+ } catch {
719
+ console.log("Hub process not found (may have already stopped)");
720
+ }
721
+ fs3.unlinkSync(PID_FILE);
722
+ }
723
+ async function hubStatus() {
724
+ const config = loadConfig();
725
+ const host = config.hub.host ?? DEFAULT_HUB_HOST;
726
+ const port = config.hub.port ?? DEFAULT_HUB_PORT;
727
+ let pidInfo = "not running";
728
+ if (fs3.existsSync(PID_FILE)) {
729
+ const pid = parseInt(fs3.readFileSync(PID_FILE, "utf-8").trim(), 10);
730
+ if (isProcessRunning(pid)) {
731
+ pidInfo = `running (PID: ${pid})`;
732
+ } else {
733
+ pidInfo = "not running (stale PID file)";
734
+ }
735
+ }
736
+ try {
737
+ const res = await fetch(`http://${host}:${port}/api/status`);
738
+ if (res.ok) {
739
+ const data = await res.json();
740
+ console.log(`Hub: running (PID: ${data.pid})`);
741
+ console.log(`Port: ${data.port}`);
742
+ console.log(`Sessions: ${data.sessions}`);
743
+ console.log(`Uptime: ${Math.round(data.uptime / 1e3)}s`);
744
+ console.log(`Dashboard: http://${host}:${port}`);
745
+ const token = config.hub.token;
746
+ if (token) {
747
+ console.log(`Token: ${token.slice(0, 8)}...(masked)`);
748
+ }
749
+ return;
750
+ }
751
+ } catch {
752
+ }
753
+ console.log(`Hub: ${pidInfo}`);
754
+ console.log(`Configured: http://${host}:${port}`);
755
+ }
756
+ function setup(targetDir) {
757
+ const mcpPath = setupMcpConfig(targetDir);
758
+ console.log(`Added claude-alarm to ${mcpPath}`);
759
+ console.log("\nTo use with Claude Code:");
760
+ console.log(" 1. Start the hub: claude-alarm hub start -d");
761
+ console.log(" 2. Run Claude Code: claude --dangerously-load-development-channels server:claude-alarm");
762
+ }
763
+ async function test() {
764
+ const config = loadConfig();
765
+ const host = config.hub.host ?? DEFAULT_HUB_HOST;
766
+ const port = config.hub.port ?? DEFAULT_HUB_PORT;
767
+ try {
768
+ const headers = { "Content-Type": "application/json" };
769
+ if (config.hub.token) {
770
+ headers["Authorization"] = `Bearer ${config.hub.token}`;
771
+ }
772
+ const res = await fetch(`http://${host}:${port}/api/notify`, {
773
+ method: "POST",
774
+ headers,
775
+ body: JSON.stringify({
776
+ title: "Test Notification",
777
+ message: "Claude Alarm is working! This is a test notification.",
778
+ level: "success"
779
+ })
780
+ });
781
+ if (res.ok) {
782
+ console.log("Test notification sent! Check your desktop for the toast.");
783
+ } else {
784
+ console.error(`Hub returned ${res.status}. Is the hub running?`);
785
+ }
786
+ } catch {
787
+ console.error("Could not reach hub. Start it first: claude-alarm hub start");
788
+ }
789
+ }
790
+ function ask(question) {
791
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
792
+ return new Promise((resolve) => {
793
+ rl.question(question, (answer) => {
794
+ rl.close();
795
+ resolve(answer.trim());
796
+ });
797
+ });
798
+ }
799
+ async function init() {
800
+ const dir = process.cwd();
801
+ const projectName = path4.basename(dir);
802
+ console.log(`
803
+ claude-alarm init for "${projectName}"
804
+ `);
805
+ const remote = await ask("Connect to a remote hub? (y/N): ");
806
+ let env = {
807
+ CLAUDE_ALARM_SESSION_NAME: projectName
808
+ };
809
+ if (remote.toLowerCase() === "y") {
810
+ const host = await ask("Hub host (e.g. 192.168.1.100): ");
811
+ const port = await ask("Hub port (default: 7890): ");
812
+ const token = await ask("Hub token: ");
813
+ if (!host) {
814
+ console.error("Host is required.");
815
+ process.exit(1);
816
+ }
817
+ env.CLAUDE_ALARM_HUB_HOST = host;
818
+ if (port) env.CLAUDE_ALARM_HUB_PORT = port;
819
+ if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
820
+ }
821
+ const mcpPath = path4.join(dir, ".mcp.json");
822
+ let mcpConfig = {};
823
+ if (fs3.existsSync(mcpPath)) {
824
+ try {
825
+ mcpConfig = JSON.parse(fs3.readFileSync(mcpPath, "utf-8"));
826
+ } catch {
827
+ mcpConfig = {};
828
+ }
829
+ }
830
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
831
+ mcpConfig.mcpServers["claude-alarm"] = {
832
+ command: "npx",
833
+ args: ["-y", "@delt/claude-alarm"],
834
+ env
835
+ };
836
+ fs3.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
837
+ console.log(`
838
+ \u2713 Created ${mcpPath}`);
839
+ if (remote.toLowerCase() !== "y") {
840
+ const config = loadConfig();
841
+ const host = config.hub.host ?? DEFAULT_HUB_HOST;
842
+ const port = config.hub.port ?? DEFAULT_HUB_PORT;
843
+ let hubRunning = false;
844
+ try {
845
+ const res = await fetch(`http://${host}:${port}/api/status`);
846
+ hubRunning = res.ok;
847
+ } catch {
848
+ }
849
+ if (hubRunning) {
850
+ console.log("\u2713 Hub is running");
851
+ } else {
852
+ console.log("\u2717 Hub is not running. Start it with:");
853
+ console.log(` claude-alarm hub start`);
854
+ }
855
+ console.log(` Dashboard: http://${host}:${port}`);
856
+ }
857
+ console.log(`
858
+ Next step:`);
859
+ console.log(` claude --dangerously-load-development-channels server:claude-alarm
860
+ `);
861
+ }
862
+ function showToken() {
863
+ const token = getOrCreateToken();
864
+ console.log(`Token: ${token}`);
865
+ }
866
+ function isProcessRunning(pid) {
867
+ try {
868
+ process.kill(pid, 0);
869
+ return true;
870
+ } catch {
871
+ return false;
872
+ }
873
+ }
874
+ async function main() {
875
+ const args = process.argv.slice(2);
876
+ const cmd = args[0];
877
+ const sub = args[1];
878
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
879
+ printUsage();
880
+ return;
881
+ }
882
+ if (cmd === "init") {
883
+ await init();
884
+ return;
885
+ }
886
+ if (cmd === "hub") {
887
+ if (sub === "start") {
888
+ const daemon = args.includes("-d") || args.includes("--daemon");
889
+ await hubStart(daemon);
890
+ } else if (sub === "stop") {
891
+ hubStop();
892
+ } else if (sub === "status") {
893
+ await hubStatus();
894
+ } else {
895
+ console.error(`Unknown hub command: ${sub}`);
896
+ printUsage();
897
+ process.exit(1);
898
+ }
899
+ return;
900
+ }
901
+ if (cmd === "setup") {
902
+ setup(args[1]);
903
+ return;
904
+ }
905
+ if (cmd === "test") {
906
+ await test();
907
+ return;
908
+ }
909
+ if (cmd === "token") {
910
+ showToken();
911
+ return;
912
+ }
913
+ console.error(`Unknown command: ${cmd}`);
914
+ printUsage();
915
+ process.exit(1);
916
+ }
917
+ main().catch((err) => {
918
+ logger.error("CLI error:", err);
919
+ process.exit(1);
920
+ });
921
+ //# sourceMappingURL=cli.js.map