@delt/claude-alarm 0.2.1 → 0.3.1

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.
@@ -25,6 +25,9 @@ var logger = {
25
25
  }
26
26
  };
27
27
 
28
+ // src/hub/server.ts
29
+ import { randomUUID } from "crypto";
30
+
28
31
  // src/shared/constants.ts
29
32
  import path from "path";
30
33
  import os from "os";
@@ -34,6 +37,7 @@ var CONFIG_DIR = path.join(os.homedir(), ".claude-alarm");
34
37
  var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
35
38
  var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
36
39
  var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
40
+ var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
37
41
  var WS_PATH_CHANNEL = "/ws/channel";
38
42
  var WS_PATH_DASHBOARD = "/ws/dashboard";
39
43
 
@@ -185,6 +189,8 @@ var HubServer = class {
185
189
  startTime = Date.now();
186
190
  // Map sessionId -> channel WebSocket
187
191
  channelSockets = /* @__PURE__ */ new Map();
192
+ // Track which channel connections are local
193
+ localChannels = /* @__PURE__ */ new Set();
188
194
  // All connected dashboard WebSockets
189
195
  dashboardSockets = /* @__PURE__ */ new Set();
190
196
  host;
@@ -206,7 +212,7 @@ var HubServer = class {
206
212
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
207
213
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
208
214
  this.wssChannel = new WebSocketServer({ noServer: true });
209
- this.wssChannel.on("connection", (ws) => this.handleChannelConnection(ws));
215
+ this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
210
216
  this.wssDashboard = new WebSocketServer({ noServer: true });
211
217
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
212
218
  this.httpServer.on("upgrade", (req, socket, head) => {
@@ -233,6 +239,7 @@ var HubServer = class {
233
239
  });
234
240
  }
235
241
  async start() {
242
+ this.cleanupUploads();
236
243
  return new Promise((resolve, reject) => {
237
244
  this.httpServer.on("error", reject);
238
245
  this.httpServer.listen(this.port, this.host, () => {
@@ -366,12 +373,13 @@ var HubServer = class {
366
373
  this.jsonResponse(res, 200, { ok: true });
367
374
  }
368
375
  // --- Channel WebSocket ---
369
- handleChannelConnection(ws) {
370
- logger.info("Channel server connected");
376
+ handleChannelConnection(ws, req) {
377
+ const isLocal = this.isLocalRequest(req);
378
+ logger.info(`Channel server connected (local: ${isLocal})`);
371
379
  ws.on("message", (data) => {
372
380
  try {
373
381
  const msg = JSON.parse(data.toString());
374
- this.handleChannelMessage(ws, msg);
382
+ this.handleChannelMessage(ws, isLocal, msg);
375
383
  } catch {
376
384
  logger.warn("Invalid message from channel");
377
385
  }
@@ -381,6 +389,7 @@ var HubServer = class {
381
389
  if (sock === ws) {
382
390
  const session = this.sessions.unregister(sessionId);
383
391
  this.channelSockets.delete(sessionId);
392
+ this.localChannels.delete(sessionId);
384
393
  logger.info(`Channel disconnected: ${sessionId}`);
385
394
  this.broadcastToDashboards({
386
395
  type: "session_disconnected",
@@ -391,13 +400,15 @@ var HubServer = class {
391
400
  }
392
401
  });
393
402
  }
394
- handleChannelMessage(ws, msg) {
403
+ handleChannelMessage(ws, isLocal, msg) {
395
404
  switch (msg.type) {
396
405
  case "register": {
397
406
  const session = msg.session;
407
+ session.isLocal = isLocal;
398
408
  const isReregister = !!this.sessions.get(session.id);
399
409
  this.sessions.register(session);
400
410
  this.channelSockets.set(session.id, ws);
411
+ if (isLocal) this.localChannels.add(session.id);
401
412
  logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);
402
413
  this.broadcastToDashboards({
403
414
  type: isReregister ? "session_updated" : "session_connected",
@@ -459,6 +470,8 @@ var HubServer = class {
459
470
  if (channelWs?.readyState === WebSocket.OPEN) {
460
471
  channelWs.send(JSON.stringify(msg));
461
472
  }
473
+ } else if (msg.type === "image_upload") {
474
+ this.handleImageUpload(msg);
462
475
  }
463
476
  } catch {
464
477
  logger.warn("Invalid message from dashboard");
@@ -478,6 +491,57 @@ var HubServer = class {
478
491
  }
479
492
  }
480
493
  }
494
+ handleImageUpload(msg) {
495
+ const { sessionId, imageData, mimeType, originalName } = msg;
496
+ if (!this.localChannels.has(sessionId)) {
497
+ logger.warn(`Image upload rejected: session ${sessionId} is not local`);
498
+ return;
499
+ }
500
+ const channelWs = this.channelSockets.get(sessionId);
501
+ if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
502
+ const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
503
+ if (!allowedTypes.includes(mimeType)) return;
504
+ const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
505
+ const buffer = Buffer.from(base64Data, "base64");
506
+ if (buffer.length > 10 * 1024 * 1024) {
507
+ logger.warn("Image upload rejected: exceeds 10MB");
508
+ return;
509
+ }
510
+ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
511
+ const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
512
+ const filename = `${randomUUID()}.${ext}`;
513
+ const filePath = path2.join(UPLOADS_DIR, filename);
514
+ fs.writeFileSync(filePath, buffer);
515
+ const forwardMsg = {
516
+ type: "image_to_session",
517
+ sessionId,
518
+ imagePath: filePath,
519
+ mimeType,
520
+ originalName
521
+ };
522
+ channelWs.send(JSON.stringify(forwardMsg));
523
+ logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
524
+ setTimeout(() => {
525
+ try {
526
+ fs.unlinkSync(filePath);
527
+ } catch {
528
+ }
529
+ }, 5 * 60 * 1e3);
530
+ }
531
+ cleanupUploads() {
532
+ try {
533
+ if (!fs.existsSync(UPLOADS_DIR)) return;
534
+ const files = fs.readdirSync(UPLOADS_DIR);
535
+ for (const file of files) {
536
+ try {
537
+ fs.unlinkSync(path2.join(UPLOADS_DIR, file));
538
+ } catch {
539
+ }
540
+ }
541
+ if (files.length > 0) logger.info(`Cleaned up ${files.length} leftover upload(s)`);
542
+ } catch {
543
+ }
544
+ }
481
545
  getSessionLabel(session) {
482
546
  if (!session) return "unknown";
483
547
  return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hub/server.ts","../../src/shared/logger.ts","../../src/shared/constants.ts","../../src/hub/session-manager.ts","../../src/hub/notifier.ts"],"sourcesContent":["import http from 'node:http';\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { fileURLToPath } from 'node:url';\r\nimport { WebSocketServer, WebSocket } from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\nimport {\r\n DEFAULT_HUB_HOST,\r\n DEFAULT_HUB_PORT,\r\n WS_PATH_CHANNEL,\r\n WS_PATH_DASHBOARD,\r\n} from '../shared/constants.js';\r\nimport { SessionManager } from './session-manager.js';\r\nimport { Notifier } from './notifier.js';\r\nimport type { ChannelMessage, AppConfig, SessionInfo } from '../shared/types.js';\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nexport class HubServer {\r\n private httpServer: http.Server;\r\n private wssChannel: WebSocketServer;\r\n private wssDashboard: WebSocketServer;\r\n private sessions = new SessionManager();\r\n private notifier = new Notifier();\r\n private startTime = Date.now();\r\n\r\n // Map sessionId -> channel WebSocket\r\n private channelSockets = new Map<string, WebSocket>();\r\n // All connected dashboard WebSockets\r\n private dashboardSockets = new Set<WebSocket>();\r\n\r\n private host: string;\r\n private port: number;\r\n private token?: string;\r\n\r\n constructor(config?: Partial<AppConfig>) {\r\n this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;\r\n this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;\r\n this.token = config?.hub?.token;\r\n\r\n if (config?.notifications) {\r\n this.notifier.configure({\r\n desktop: config.notifications.desktop,\r\n });\r\n }\r\n if (config?.webhooks) {\r\n this.notifier.configure({ webhooks: config.webhooks });\r\n }\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });\r\n\r\n // HTTP Server\r\n this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));\r\n\r\n // WebSocket for channel servers\r\n this.wssChannel = new WebSocketServer({ noServer: true });\r\n this.wssChannel.on('connection', (ws) => this.handleChannelConnection(ws));\r\n\r\n // WebSocket for dashboard\r\n this.wssDashboard = new WebSocketServer({ noServer: true });\r\n this.wssDashboard.on('connection', (ws) => this.handleDashboardConnection(ws));\r\n\r\n // Route WebSocket upgrade requests\r\n this.httpServer.on('upgrade', (req, socket, head) => {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n const pathname = url.pathname;\r\n\r\n // Token auth for WebSocket connections (skip for local requests)\r\n if (this.token && !this.isLocalRequest(req)) {\r\n const wsToken = url.searchParams.get('token');\r\n if (wsToken !== this.token) {\r\n socket.destroy();\r\n return;\r\n }\r\n }\r\n\r\n if (pathname === WS_PATH_CHANNEL) {\r\n this.wssChannel.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssChannel.emit('connection', ws, req);\r\n });\r\n } else if (pathname === WS_PATH_DASHBOARD) {\r\n this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssDashboard.emit('connection', ws, req);\r\n });\r\n } else {\r\n socket.destroy();\r\n }\r\n });\r\n }\r\n\r\n async start(): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n this.httpServer.on('error', reject);\r\n this.httpServer.listen(this.port, this.host, () => {\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n logger.info(`Hub server listening on http://${displayHost}:${this.port}`);\r\n resolve();\r\n });\r\n });\r\n }\r\n\r\n stop(): Promise<void> {\r\n return new Promise((resolve) => {\r\n // Force-close all WebSocket connections\r\n for (const ws of this.channelSockets.values()) ws.terminate();\r\n for (const ws of this.dashboardSockets) ws.terminate();\r\n this.channelSockets.clear();\r\n this.dashboardSockets.clear();\r\n\r\n this.wssChannel.close();\r\n this.wssDashboard.close();\r\n this.httpServer.close(() => {\r\n logger.info('Hub server stopped');\r\n resolve();\r\n });\r\n\r\n // Force resolve after 3 seconds if server won't close\r\n setTimeout(() => {\r\n logger.warn('Force shutting down');\r\n resolve();\r\n }, 3000);\r\n });\r\n }\r\n\r\n // --- HTTP Handler ---\r\n\r\n private handleHttp(req: http.IncomingMessage, res: http.ServerResponse): void {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n\r\n // CORS headers - restrict to same origin\r\n const origin = req.headers.origin;\r\n if (origin && (origin.includes('127.0.0.1') || origin.includes('localhost'))) {\r\n res.setHeader('Access-Control-Allow-Origin', origin);\r\n }\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.writeHead(204);\r\n res.end();\r\n return;\r\n }\r\n\r\n // Token auth for API endpoints (skip dashboard HTML serving)\r\n if (url.pathname !== '/' && this.token) {\r\n if (!this.isLocalRequest(req)) {\r\n const authHeader = req.headers['authorization'];\r\n const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;\r\n if (bearerToken !== this.token) {\r\n this.jsonResponse(res, 401, { error: 'Unauthorized' });\r\n return;\r\n }\r\n }\r\n }\r\n\r\n // Route\r\n if (url.pathname === '/' && req.method === 'GET') {\r\n this.serveDashboard(res);\r\n } else if (url.pathname === '/api/sessions' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });\r\n } else if (url.pathname === '/api/status' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, {\r\n running: true,\r\n pid: process.pid,\r\n port: this.port,\r\n sessions: this.sessions.count(),\r\n uptime: Date.now() - this.startTime,\r\n });\r\n } else if (url.pathname === '/api/send' && req.method === 'POST') {\r\n this.handleApiSend(req, res);\r\n } else if (url.pathname === '/api/notify' && req.method === 'POST') {\r\n this.handleApiNotify(req, res);\r\n } else {\r\n this.jsonResponse(res, 404, { error: 'Not found' });\r\n }\r\n }\r\n\r\n private serveDashboard(res: http.ServerResponse): void {\r\n // Look for dashboard HTML relative to this file (dist) or source\r\n const candidates = [\r\n path.join(__dirname, '..', 'dashboard', 'index.html'), // from dist/hub/\r\n path.join(__dirname, 'dashboard', 'index.html'), // from dist/ (bundled index.js)\r\n path.join(__dirname, '..', '..', 'src', 'dashboard', 'index.html'), // from dist/hub/ -> src/\r\n path.join(__dirname, '..', 'src', 'dashboard', 'index.html'), // from dist/ -> src/\r\n path.join(process.cwd(), 'dist', 'dashboard', 'index.html'), // from cwd\r\n path.join(process.cwd(), 'src', 'dashboard', 'index.html'), // from cwd/src\r\n ];\r\n logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);\r\n\r\n for (const candidate of candidates) {\r\n if (fs.existsSync(candidate)) {\r\n const html = fs.readFileSync(candidate, 'utf-8');\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end(html);\r\n return;\r\n }\r\n }\r\n\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end('<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>');\r\n }\r\n\r\n private async handleApiSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { sessionId, content } = body as { sessionId?: string; content?: string };\r\n if (!sessionId || !content) {\r\n this.jsonResponse(res, 400, { error: 'sessionId and content are required' });\r\n return;\r\n }\r\n\r\n const ws = this.channelSockets.get(sessionId);\r\n if (!ws || ws.readyState !== WebSocket.OPEN) {\r\n this.jsonResponse(res, 404, { error: 'Session not connected' });\r\n return;\r\n }\r\n\r\n const msg: ChannelMessage = { type: 'message_to_session', sessionId, content };\r\n ws.send(JSON.stringify(msg));\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n private async handleApiNotify(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { title, message, level } = body as { title?: string; message?: string; level?: string };\r\n if (!title || !message) {\r\n this.jsonResponse(res, 400, { error: 'title and message are required' });\r\n return;\r\n }\r\n\r\n await this.notifier.notify(title, message, (level as any) ?? 'info');\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n // --- Channel WebSocket ---\r\n\r\n private handleChannelConnection(ws: WebSocket): void {\r\n logger.info('Channel server connected');\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n this.handleChannelMessage(ws, msg);\r\n } catch {\r\n logger.warn('Invalid message from channel');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n // Find and remove the session for this socket\r\n for (const [sessionId, sock] of this.channelSockets) {\r\n if (sock === ws) {\r\n const session = this.sessions.unregister(sessionId);\r\n this.channelSockets.delete(sessionId);\r\n logger.info(`Channel disconnected: ${sessionId}`);\r\n this.broadcastToDashboards({\r\n type: 'session_disconnected',\r\n sessionId,\r\n });\r\n break;\r\n }\r\n }\r\n });\r\n }\r\n\r\n private handleChannelMessage(ws: WebSocket, msg: ChannelMessage): void {\r\n switch (msg.type) {\r\n case 'register': {\r\n const session = msg.session;\r\n const isReregister = !!this.sessions.get(session.id);\r\n this.sessions.register(session);\r\n this.channelSockets.set(session.id, ws);\r\n logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);\r\n this.broadcastToDashboards({\r\n type: isReregister ? 'session_updated' : 'session_connected',\r\n session,\r\n });\r\n break;\r\n }\r\n\r\n case 'status': {\r\n const updated = this.sessions.updateStatus(msg.sessionId, msg.status);\r\n if (updated) {\r\n this.broadcastToDashboards({ type: 'session_updated', session: updated });\r\n }\r\n break;\r\n }\r\n\r\n case 'notify': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const notifySession = this.sessions.get(msg.sessionId);\r\n const notifyLabel = this.getSessionLabel(notifySession);\r\n this.notifier.notify(`[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? 'info');\r\n this.broadcastToDashboards({\r\n type: 'notification',\r\n sessionId: msg.sessionId,\r\n title: msg.title,\r\n message: msg.message,\r\n level: msg.level,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n\r\n case 'reply': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const replySession = this.sessions.get(msg.sessionId);\r\n const replyLabel = this.getSessionLabel(replySession);\r\n this.notifier.notify(`[${replyLabel}] Reply`, msg.content.slice(0, 200), 'info');\r\n this.broadcastToDashboards({\r\n type: 'reply_from_session',\r\n sessionId: msg.sessionId,\r\n content: msg.content,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // --- Dashboard WebSocket ---\r\n\r\n private handleDashboardConnection(ws: WebSocket): void {\r\n this.dashboardSockets.add(ws);\r\n logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);\r\n\r\n // Send current session list\r\n const sessionsMsg: ChannelMessage = {\r\n type: 'sessions_list',\r\n sessions: this.sessions.getAll(),\r\n };\r\n ws.send(JSON.stringify(sessionsMsg));\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n if (msg.type === 'message_to_session') {\r\n const channelWs = this.channelSockets.get(msg.sessionId);\r\n if (channelWs?.readyState === WebSocket.OPEN) {\r\n channelWs.send(JSON.stringify(msg));\r\n }\r\n }\r\n } catch {\r\n logger.warn('Invalid message from dashboard');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n this.dashboardSockets.delete(ws);\r\n logger.info(`Dashboard disconnected (total: ${this.dashboardSockets.size})`);\r\n });\r\n }\r\n\r\n // --- Helpers ---\r\n\r\n private broadcastToDashboards(msg: ChannelMessage): void {\r\n const payload = JSON.stringify(msg);\r\n for (const ws of this.dashboardSockets) {\r\n if (ws.readyState === WebSocket.OPEN) {\r\n ws.send(payload);\r\n }\r\n }\r\n }\r\n\r\n private getSessionLabel(session?: SessionInfo): string {\r\n if (!session) return 'unknown';\r\n return session.cwd?.replace(/^.*[/\\\\]/, '') || session.name;\r\n }\r\n\r\n private jsonResponse(res: http.ServerResponse, status: number, body: unknown): void {\r\n res.writeHead(status, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify(body));\r\n }\r\n\r\n private isLocalRequest(req: http.IncomingMessage): boolean {\r\n const addr = req.socket.remoteAddress;\r\n return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';\r\n }\r\n\r\n private readBody(req: http.IncomingMessage, maxSize = 1024 * 1024): Promise<unknown | null> {\r\n return new Promise((resolve) => {\r\n let data = '';\r\n let size = 0;\r\n req.on('data', (chunk) => {\r\n size += chunk.length;\r\n if (size > maxSize) {\r\n req.destroy();\r\n resolve(null);\r\n return;\r\n }\r\n data += chunk;\r\n });\r\n req.on('end', () => {\r\n try {\r\n resolve(JSON.parse(data));\r\n } catch {\r\n resolve(null);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n\r\n// Direct execution support\r\nif (process.argv[1] && (\r\n process.argv[1].endsWith('hub/server.js') ||\r\n process.argv[1].endsWith('hub/server.ts')\r\n)) {\r\n const hub = new HubServer();\r\n hub.start().catch((err) => {\r\n logger.error('Failed to start hub:', err);\r\n process.exit(1);\r\n });\r\n\r\n const shutdown = () => {\r\n hub.stop().then(() => process.exit(0));\r\n };\r\n process.on('SIGINT', shutdown);\r\n process.on('SIGTERM', shutdown);\r\n}\r\n","/**\r\n * Logger that writes to stderr only.\r\n * CRITICAL: In MCP channel servers, stdout is used for the stdio protocol.\r\n * Any console.log() would corrupt the protocol. Always use this logger.\r\n */\r\nexport const logger = {\r\n info(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm] ${msg}`, ...args);\r\n },\r\n warn(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm WARN] ${msg}`, ...args);\r\n },\r\n error(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm ERROR] ${msg}`, ...args);\r\n },\r\n debug(msg: string, ...args: unknown[]) {\r\n if (process.env.CLAUDE_ALARM_DEBUG) {\r\n console.error(`[claude-alarm DEBUG] ${msg}`, ...args);\r\n }\r\n },\r\n};\r\n","import path from 'node:path';\r\nimport os from 'node:os';\r\n\r\nexport const DEFAULT_HUB_HOST = '127.0.0.1';\r\nexport const DEFAULT_HUB_PORT = 7900;\r\n\r\nexport const CONFIG_DIR = path.join(os.homedir(), '.claude-alarm');\r\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\r\nexport const PID_FILE = path.join(CONFIG_DIR, 'hub.pid');\r\nexport const LOG_FILE = path.join(CONFIG_DIR, 'hub.log');\r\n\r\nexport const WS_PATH_CHANNEL = '/ws/channel';\r\nexport const WS_PATH_DASHBOARD = '/ws/dashboard';\r\n\r\nexport const CHANNEL_SERVER_NAME = 'claude-alarm';\r\nexport const CHANNEL_SERVER_VERSION = '0.1.0';\r\n","import type { SessionInfo, SessionStatus } from '../shared/types.js';\r\n\r\nexport class SessionManager {\r\n private sessions = new Map<string, SessionInfo>();\r\n\r\n register(session: SessionInfo): void {\r\n this.sessions.set(session.id, { ...session });\r\n }\r\n\r\n unregister(sessionId: string): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n this.sessions.delete(sessionId);\r\n return session;\r\n }\r\n\r\n updateStatus(sessionId: string, status: SessionStatus): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.status = status;\r\n session.lastActivity = Date.now();\r\n }\r\n return session;\r\n }\r\n\r\n updateActivity(sessionId: string): void {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.lastActivity = Date.now();\r\n }\r\n }\r\n\r\n get(sessionId: string): SessionInfo | undefined {\r\n return this.sessions.get(sessionId);\r\n }\r\n\r\n getAll(): SessionInfo[] {\r\n return Array.from(this.sessions.values());\r\n }\r\n\r\n count(): number {\r\n return this.sessions.size;\r\n }\r\n}\r\n","import notifier from 'node-notifier';\r\nimport { execFile } from 'node:child_process';\r\nimport { logger } from '../shared/logger.js';\r\nimport type { NotifyLevel, WebhookConfig } from '../shared/types.js';\r\n\r\nexport class Notifier {\r\n private webhooks: WebhookConfig[] = [];\r\n private desktopEnabled = true;\r\n private notificationSettingsOpened = false;\r\n private dashboardUrl?: string;\r\n\r\n configure(options: { desktop?: boolean; webhooks?: WebhookConfig[]; dashboardUrl?: string }): void {\r\n if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;\r\n if (options.desktop !== undefined) this.desktopEnabled = options.desktop;\r\n if (options.webhooks) this.webhooks = options.webhooks;\r\n }\r\n\r\n async notify(title: string, message: string, level: NotifyLevel = 'info'): Promise<void> {\r\n const promises: Promise<void>[] = [];\r\n\r\n if (this.desktopEnabled) {\r\n promises.push(this.sendDesktop(title, message, level));\r\n }\r\n\r\n for (const webhook of this.webhooks) {\r\n promises.push(this.sendWebhook(webhook, title, message, level));\r\n }\r\n\r\n await Promise.allSettled(promises);\r\n }\r\n\r\n private async sendDesktop(title: string, message: string, _level: NotifyLevel): Promise<void> {\r\n if (process.platform === 'win32') {\r\n // Check if notifications are enabled by running snoretoast directly\r\n const enabled = await this.checkWindowsNotifications();\r\n if (!enabled) {\r\n this.openNotificationSettings();\r\n return;\r\n }\r\n }\r\n\r\n return new Promise((resolve) => {\r\n const notification = (notifier as any).notify(\r\n {\r\n title: `Claude Alarm: ${title}`,\r\n message,\r\n sound: true,\r\n wait: true,\r\n },\r\n (err: Error | null) => {\r\n if (err) {\r\n logger.warn(`Desktop notification failed: ${err.message}`);\r\n }\r\n resolve();\r\n },\r\n );\r\n\r\n // No click handler - dashboard is already open and notifications\r\n // can be clicked there to focus the relevant session\r\n });\r\n }\r\n\r\n private checkWindowsNotifications(): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n execFile(\r\n 'powershell',\r\n ['-Command', '(Get-ItemProperty -Path \"HKCU:\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\PushNotifications\" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],\r\n (err, stdout) => {\r\n if (err) { resolve(true); return; } // assume enabled on error\r\n const value = stdout.trim();\r\n resolve(value !== '0');\r\n },\r\n );\r\n });\r\n }\r\n\r\n private openNotificationSettings(): void {\r\n if (this.notificationSettingsOpened) return;\r\n this.notificationSettingsOpened = true;\r\n\r\n logger.warn('Windows notifications are disabled. Opening notification settings...');\r\n logger.warn('Please enable notifications for this app, then try again.');\r\n\r\n if (process.platform === 'win32') {\r\n execFile('powershell', ['-Command', 'Start-Process ms-settings:notifications']);\r\n }\r\n\r\n // Allow re-opening after 5 minutes\r\n setTimeout(() => { this.notificationSettingsOpened = false; }, 5 * 60 * 1000);\r\n }\r\n\r\n private async sendWebhook(\r\n webhook: WebhookConfig,\r\n title: string,\r\n message: string,\r\n level: NotifyLevel,\r\n ): Promise<void> {\r\n try {\r\n const response = await fetch(webhook.url, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...webhook.headers,\r\n },\r\n body: JSON.stringify({\r\n title,\r\n message,\r\n level,\r\n timestamp: Date.now(),\r\n source: 'claude-alarm',\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n logger.warn(`Webhook ${webhook.url} returned ${response.status}`);\r\n }\r\n } catch (err) {\r\n logger.warn(`Webhook ${webhook.url} failed: ${(err as Error).message}`);\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["path","path"]}
1
+ {"version":3,"sources":["../../src/hub/server.ts","../../src/shared/logger.ts","../../src/shared/constants.ts","../../src/hub/session-manager.ts","../../src/hub/notifier.ts"],"sourcesContent":["import http from 'node:http';\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { fileURLToPath } from 'node:url';\r\nimport { WebSocketServer, WebSocket } from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\nimport { randomUUID } from 'node:crypto';\r\nimport {\r\n DEFAULT_HUB_HOST,\r\n DEFAULT_HUB_PORT,\r\n WS_PATH_CHANNEL,\r\n WS_PATH_DASHBOARD,\r\n UPLOADS_DIR,\r\n} from '../shared/constants.js';\r\nimport { SessionManager } from './session-manager.js';\r\nimport { Notifier } from './notifier.js';\r\nimport type { ChannelMessage, AppConfig, SessionInfo } from '../shared/types.js';\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nexport class HubServer {\r\n private httpServer: http.Server;\r\n private wssChannel: WebSocketServer;\r\n private wssDashboard: WebSocketServer;\r\n private sessions = new SessionManager();\r\n private notifier = new Notifier();\r\n private startTime = Date.now();\r\n\r\n // Map sessionId -> channel WebSocket\r\n private channelSockets = new Map<string, WebSocket>();\r\n // Track which channel connections are local\r\n private localChannels = new Set<string>();\r\n // All connected dashboard WebSockets\r\n private dashboardSockets = new Set<WebSocket>();\r\n\r\n private host: string;\r\n private port: number;\r\n private token?: string;\r\n\r\n constructor(config?: Partial<AppConfig>) {\r\n this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;\r\n this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;\r\n this.token = config?.hub?.token;\r\n\r\n if (config?.notifications) {\r\n this.notifier.configure({\r\n desktop: config.notifications.desktop,\r\n });\r\n }\r\n if (config?.webhooks) {\r\n this.notifier.configure({ webhooks: config.webhooks });\r\n }\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });\r\n\r\n // HTTP Server\r\n this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));\r\n\r\n // WebSocket for channel servers\r\n this.wssChannel = new WebSocketServer({ noServer: true });\r\n this.wssChannel.on('connection', (ws: WebSocket, req: http.IncomingMessage) => this.handleChannelConnection(ws, req));\r\n\r\n // WebSocket for dashboard\r\n this.wssDashboard = new WebSocketServer({ noServer: true });\r\n this.wssDashboard.on('connection', (ws) => this.handleDashboardConnection(ws));\r\n\r\n // Route WebSocket upgrade requests\r\n this.httpServer.on('upgrade', (req, socket, head) => {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n const pathname = url.pathname;\r\n\r\n // Token auth for WebSocket connections (skip for local requests)\r\n if (this.token && !this.isLocalRequest(req)) {\r\n const wsToken = url.searchParams.get('token');\r\n if (wsToken !== this.token) {\r\n socket.destroy();\r\n return;\r\n }\r\n }\r\n\r\n if (pathname === WS_PATH_CHANNEL) {\r\n this.wssChannel.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssChannel.emit('connection', ws, req);\r\n });\r\n } else if (pathname === WS_PATH_DASHBOARD) {\r\n this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssDashboard.emit('connection', ws, req);\r\n });\r\n } else {\r\n socket.destroy();\r\n }\r\n });\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.cleanupUploads();\r\n return new Promise((resolve, reject) => {\r\n this.httpServer.on('error', reject);\r\n this.httpServer.listen(this.port, this.host, () => {\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n logger.info(`Hub server listening on http://${displayHost}:${this.port}`);\r\n resolve();\r\n });\r\n });\r\n }\r\n\r\n stop(): Promise<void> {\r\n return new Promise((resolve) => {\r\n // Force-close all WebSocket connections\r\n for (const ws of this.channelSockets.values()) ws.terminate();\r\n for (const ws of this.dashboardSockets) ws.terminate();\r\n this.channelSockets.clear();\r\n this.dashboardSockets.clear();\r\n\r\n this.wssChannel.close();\r\n this.wssDashboard.close();\r\n this.httpServer.close(() => {\r\n logger.info('Hub server stopped');\r\n resolve();\r\n });\r\n\r\n // Force resolve after 3 seconds if server won't close\r\n setTimeout(() => {\r\n logger.warn('Force shutting down');\r\n resolve();\r\n }, 3000);\r\n });\r\n }\r\n\r\n // --- HTTP Handler ---\r\n\r\n private handleHttp(req: http.IncomingMessage, res: http.ServerResponse): void {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n\r\n // CORS headers - restrict to same origin\r\n const origin = req.headers.origin;\r\n if (origin && (origin.includes('127.0.0.1') || origin.includes('localhost'))) {\r\n res.setHeader('Access-Control-Allow-Origin', origin);\r\n }\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.writeHead(204);\r\n res.end();\r\n return;\r\n }\r\n\r\n // Token auth for API endpoints (skip dashboard HTML serving)\r\n if (url.pathname !== '/' && this.token) {\r\n if (!this.isLocalRequest(req)) {\r\n const authHeader = req.headers['authorization'];\r\n const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;\r\n if (bearerToken !== this.token) {\r\n this.jsonResponse(res, 401, { error: 'Unauthorized' });\r\n return;\r\n }\r\n }\r\n }\r\n\r\n // Route\r\n if (url.pathname === '/' && req.method === 'GET') {\r\n this.serveDashboard(res);\r\n } else if (url.pathname === '/api/sessions' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });\r\n } else if (url.pathname === '/api/status' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, {\r\n running: true,\r\n pid: process.pid,\r\n port: this.port,\r\n sessions: this.sessions.count(),\r\n uptime: Date.now() - this.startTime,\r\n });\r\n } else if (url.pathname === '/api/send' && req.method === 'POST') {\r\n this.handleApiSend(req, res);\r\n } else if (url.pathname === '/api/notify' && req.method === 'POST') {\r\n this.handleApiNotify(req, res);\r\n } else {\r\n this.jsonResponse(res, 404, { error: 'Not found' });\r\n }\r\n }\r\n\r\n private serveDashboard(res: http.ServerResponse): void {\r\n // Look for dashboard HTML relative to this file (dist) or source\r\n const candidates = [\r\n path.join(__dirname, '..', 'dashboard', 'index.html'), // from dist/hub/\r\n path.join(__dirname, 'dashboard', 'index.html'), // from dist/ (bundled index.js)\r\n path.join(__dirname, '..', '..', 'src', 'dashboard', 'index.html'), // from dist/hub/ -> src/\r\n path.join(__dirname, '..', 'src', 'dashboard', 'index.html'), // from dist/ -> src/\r\n path.join(process.cwd(), 'dist', 'dashboard', 'index.html'), // from cwd\r\n path.join(process.cwd(), 'src', 'dashboard', 'index.html'), // from cwd/src\r\n ];\r\n logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);\r\n\r\n for (const candidate of candidates) {\r\n if (fs.existsSync(candidate)) {\r\n const html = fs.readFileSync(candidate, 'utf-8');\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end(html);\r\n return;\r\n }\r\n }\r\n\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end('<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>');\r\n }\r\n\r\n private async handleApiSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { sessionId, content } = body as { sessionId?: string; content?: string };\r\n if (!sessionId || !content) {\r\n this.jsonResponse(res, 400, { error: 'sessionId and content are required' });\r\n return;\r\n }\r\n\r\n const ws = this.channelSockets.get(sessionId);\r\n if (!ws || ws.readyState !== WebSocket.OPEN) {\r\n this.jsonResponse(res, 404, { error: 'Session not connected' });\r\n return;\r\n }\r\n\r\n const msg: ChannelMessage = { type: 'message_to_session', sessionId, content };\r\n ws.send(JSON.stringify(msg));\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n private async handleApiNotify(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { title, message, level } = body as { title?: string; message?: string; level?: string };\r\n if (!title || !message) {\r\n this.jsonResponse(res, 400, { error: 'title and message are required' });\r\n return;\r\n }\r\n\r\n await this.notifier.notify(title, message, (level as any) ?? 'info');\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n // --- Channel WebSocket ---\r\n\r\n private handleChannelConnection(ws: WebSocket, req: http.IncomingMessage): void {\r\n const isLocal = this.isLocalRequest(req);\r\n logger.info(`Channel server connected (local: ${isLocal})`);\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n this.handleChannelMessage(ws, isLocal, msg);\r\n } catch {\r\n logger.warn('Invalid message from channel');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n // Find and remove the session for this socket\r\n for (const [sessionId, sock] of this.channelSockets) {\r\n if (sock === ws) {\r\n const session = this.sessions.unregister(sessionId);\r\n this.channelSockets.delete(sessionId);\r\n this.localChannels.delete(sessionId);\r\n logger.info(`Channel disconnected: ${sessionId}`);\r\n this.broadcastToDashboards({\r\n type: 'session_disconnected',\r\n sessionId,\r\n });\r\n break;\r\n }\r\n }\r\n });\r\n }\r\n\r\n private handleChannelMessage(ws: WebSocket, isLocal: boolean, msg: ChannelMessage): void {\r\n switch (msg.type) {\r\n case 'register': {\r\n const session = msg.session;\r\n session.isLocal = isLocal;\r\n const isReregister = !!this.sessions.get(session.id);\r\n this.sessions.register(session);\r\n this.channelSockets.set(session.id, ws);\r\n if (isLocal) this.localChannels.add(session.id);\r\n logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);\r\n this.broadcastToDashboards({\r\n type: isReregister ? 'session_updated' : 'session_connected',\r\n session,\r\n });\r\n break;\r\n }\r\n\r\n case 'status': {\r\n const updated = this.sessions.updateStatus(msg.sessionId, msg.status);\r\n if (updated) {\r\n this.broadcastToDashboards({ type: 'session_updated', session: updated });\r\n }\r\n break;\r\n }\r\n\r\n case 'notify': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const notifySession = this.sessions.get(msg.sessionId);\r\n const notifyLabel = this.getSessionLabel(notifySession);\r\n this.notifier.notify(`[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? 'info');\r\n this.broadcastToDashboards({\r\n type: 'notification',\r\n sessionId: msg.sessionId,\r\n title: msg.title,\r\n message: msg.message,\r\n level: msg.level,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n\r\n case 'reply': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const replySession = this.sessions.get(msg.sessionId);\r\n const replyLabel = this.getSessionLabel(replySession);\r\n this.notifier.notify(`[${replyLabel}] Reply`, msg.content.slice(0, 200), 'info');\r\n this.broadcastToDashboards({\r\n type: 'reply_from_session',\r\n sessionId: msg.sessionId,\r\n content: msg.content,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // --- Dashboard WebSocket ---\r\n\r\n private handleDashboardConnection(ws: WebSocket): void {\r\n this.dashboardSockets.add(ws);\r\n logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);\r\n\r\n // Send current session list\r\n const sessionsMsg: ChannelMessage = {\r\n type: 'sessions_list',\r\n sessions: this.sessions.getAll(),\r\n };\r\n ws.send(JSON.stringify(sessionsMsg));\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n if (msg.type === 'message_to_session') {\r\n const channelWs = this.channelSockets.get(msg.sessionId);\r\n if (channelWs?.readyState === WebSocket.OPEN) {\r\n channelWs.send(JSON.stringify(msg));\r\n }\r\n } else if (msg.type === 'image_upload') {\r\n this.handleImageUpload(msg);\r\n }\r\n } catch {\r\n logger.warn('Invalid message from dashboard');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n this.dashboardSockets.delete(ws);\r\n logger.info(`Dashboard disconnected (total: ${this.dashboardSockets.size})`);\r\n });\r\n }\r\n\r\n // --- Helpers ---\r\n\r\n private broadcastToDashboards(msg: ChannelMessage): void {\r\n const payload = JSON.stringify(msg);\r\n for (const ws of this.dashboardSockets) {\r\n if (ws.readyState === WebSocket.OPEN) {\r\n ws.send(payload);\r\n }\r\n }\r\n }\r\n\r\n private handleImageUpload(msg: ChannelMessage & { type: 'image_upload' }): void {\r\n const { sessionId, imageData, mimeType, originalName } = msg;\r\n\r\n // Only allow for local sessions\r\n if (!this.localChannels.has(sessionId)) {\r\n logger.warn(`Image upload rejected: session ${sessionId} is not local`);\r\n return;\r\n }\r\n\r\n const channelWs = this.channelSockets.get(sessionId);\r\n if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;\r\n\r\n // Validate mime type\r\n const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];\r\n if (!allowedTypes.includes(mimeType)) return;\r\n\r\n // Extract base64 data (remove data URL prefix if present)\r\n const base64Data = imageData.replace(/^data:image\\/\\w+;base64,/, '');\r\n const buffer = Buffer.from(base64Data, 'base64');\r\n\r\n // Validate size (10MB max)\r\n if (buffer.length > 10 * 1024 * 1024) {\r\n logger.warn('Image upload rejected: exceeds 10MB');\r\n return;\r\n }\r\n\r\n // Save to uploads dir\r\n fs.mkdirSync(UPLOADS_DIR, { recursive: true });\r\n const ext = mimeType.split('/')[1] === 'jpeg' ? 'jpg' : mimeType.split('/')[1];\r\n const filename = `${randomUUID()}.${ext}`;\r\n const filePath = path.join(UPLOADS_DIR, filename);\r\n fs.writeFileSync(filePath, buffer);\r\n\r\n // Forward file path to channel\r\n const forwardMsg: ChannelMessage = {\r\n type: 'image_to_session',\r\n sessionId,\r\n imagePath: filePath,\r\n mimeType,\r\n originalName,\r\n };\r\n channelWs.send(JSON.stringify(forwardMsg));\r\n logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);\r\n\r\n // Cleanup after 5 minutes\r\n setTimeout(() => {\r\n try { fs.unlinkSync(filePath); } catch {}\r\n }, 5 * 60 * 1000);\r\n }\r\n\r\n private cleanupUploads(): void {\r\n try {\r\n if (!fs.existsSync(UPLOADS_DIR)) return;\r\n const files = fs.readdirSync(UPLOADS_DIR);\r\n for (const file of files) {\r\n try { fs.unlinkSync(path.join(UPLOADS_DIR, file)); } catch {}\r\n }\r\n if (files.length > 0) logger.info(`Cleaned up ${files.length} leftover upload(s)`);\r\n } catch {}\r\n }\r\n\r\n private getSessionLabel(session?: SessionInfo): string {\r\n if (!session) return 'unknown';\r\n return session.cwd?.replace(/^.*[/\\\\]/, '') || session.name;\r\n }\r\n\r\n private jsonResponse(res: http.ServerResponse, status: number, body: unknown): void {\r\n res.writeHead(status, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify(body));\r\n }\r\n\r\n private isLocalRequest(req: http.IncomingMessage): boolean {\r\n const addr = req.socket.remoteAddress;\r\n return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';\r\n }\r\n\r\n private readBody(req: http.IncomingMessage, maxSize = 1024 * 1024): Promise<unknown | null> {\r\n return new Promise((resolve) => {\r\n let data = '';\r\n let size = 0;\r\n req.on('data', (chunk) => {\r\n size += chunk.length;\r\n if (size > maxSize) {\r\n req.destroy();\r\n resolve(null);\r\n return;\r\n }\r\n data += chunk;\r\n });\r\n req.on('end', () => {\r\n try {\r\n resolve(JSON.parse(data));\r\n } catch {\r\n resolve(null);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n\r\n// Direct execution support\r\nif (process.argv[1] && (\r\n process.argv[1].endsWith('hub/server.js') ||\r\n process.argv[1].endsWith('hub/server.ts')\r\n)) {\r\n const hub = new HubServer();\r\n hub.start().catch((err) => {\r\n logger.error('Failed to start hub:', err);\r\n process.exit(1);\r\n });\r\n\r\n const shutdown = () => {\r\n hub.stop().then(() => process.exit(0));\r\n };\r\n process.on('SIGINT', shutdown);\r\n process.on('SIGTERM', shutdown);\r\n}\r\n","/**\r\n * Logger that writes to stderr only.\r\n * CRITICAL: In MCP channel servers, stdout is used for the stdio protocol.\r\n * Any console.log() would corrupt the protocol. Always use this logger.\r\n */\r\nexport const logger = {\r\n info(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm] ${msg}`, ...args);\r\n },\r\n warn(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm WARN] ${msg}`, ...args);\r\n },\r\n error(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm ERROR] ${msg}`, ...args);\r\n },\r\n debug(msg: string, ...args: unknown[]) {\r\n if (process.env.CLAUDE_ALARM_DEBUG) {\r\n console.error(`[claude-alarm DEBUG] ${msg}`, ...args);\r\n }\r\n },\r\n};\r\n","import path from 'node:path';\r\nimport os from 'node:os';\r\n\r\nexport const DEFAULT_HUB_HOST = '127.0.0.1';\r\nexport const DEFAULT_HUB_PORT = 7900;\r\n\r\nexport const CONFIG_DIR = path.join(os.homedir(), '.claude-alarm');\r\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\r\nexport const PID_FILE = path.join(CONFIG_DIR, 'hub.pid');\r\nexport const LOG_FILE = path.join(CONFIG_DIR, 'hub.log');\r\nexport const UPLOADS_DIR = path.join(CONFIG_DIR, 'uploads');\r\n\r\nexport const WS_PATH_CHANNEL = '/ws/channel';\r\nexport const WS_PATH_DASHBOARD = '/ws/dashboard';\r\n\r\nexport const CHANNEL_SERVER_NAME = 'claude-alarm';\r\nexport const CHANNEL_SERVER_VERSION = '0.1.0';\r\n","import type { SessionInfo, SessionStatus } from '../shared/types.js';\r\n\r\nexport class SessionManager {\r\n private sessions = new Map<string, SessionInfo>();\r\n\r\n register(session: SessionInfo): void {\r\n this.sessions.set(session.id, { ...session });\r\n }\r\n\r\n unregister(sessionId: string): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n this.sessions.delete(sessionId);\r\n return session;\r\n }\r\n\r\n updateStatus(sessionId: string, status: SessionStatus): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.status = status;\r\n session.lastActivity = Date.now();\r\n }\r\n return session;\r\n }\r\n\r\n updateActivity(sessionId: string): void {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.lastActivity = Date.now();\r\n }\r\n }\r\n\r\n get(sessionId: string): SessionInfo | undefined {\r\n return this.sessions.get(sessionId);\r\n }\r\n\r\n getAll(): SessionInfo[] {\r\n return Array.from(this.sessions.values());\r\n }\r\n\r\n count(): number {\r\n return this.sessions.size;\r\n }\r\n}\r\n","import notifier from 'node-notifier';\r\nimport { execFile } from 'node:child_process';\r\nimport { logger } from '../shared/logger.js';\r\nimport type { NotifyLevel, WebhookConfig } from '../shared/types.js';\r\n\r\nexport class Notifier {\r\n private webhooks: WebhookConfig[] = [];\r\n private desktopEnabled = true;\r\n private notificationSettingsOpened = false;\r\n private dashboardUrl?: string;\r\n\r\n configure(options: { desktop?: boolean; webhooks?: WebhookConfig[]; dashboardUrl?: string }): void {\r\n if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;\r\n if (options.desktop !== undefined) this.desktopEnabled = options.desktop;\r\n if (options.webhooks) this.webhooks = options.webhooks;\r\n }\r\n\r\n async notify(title: string, message: string, level: NotifyLevel = 'info'): Promise<void> {\r\n const promises: Promise<void>[] = [];\r\n\r\n if (this.desktopEnabled) {\r\n promises.push(this.sendDesktop(title, message, level));\r\n }\r\n\r\n for (const webhook of this.webhooks) {\r\n promises.push(this.sendWebhook(webhook, title, message, level));\r\n }\r\n\r\n await Promise.allSettled(promises);\r\n }\r\n\r\n private async sendDesktop(title: string, message: string, _level: NotifyLevel): Promise<void> {\r\n if (process.platform === 'win32') {\r\n // Check if notifications are enabled by running snoretoast directly\r\n const enabled = await this.checkWindowsNotifications();\r\n if (!enabled) {\r\n this.openNotificationSettings();\r\n return;\r\n }\r\n }\r\n\r\n return new Promise((resolve) => {\r\n const notification = (notifier as any).notify(\r\n {\r\n title: `Claude Alarm: ${title}`,\r\n message,\r\n sound: true,\r\n wait: true,\r\n },\r\n (err: Error | null) => {\r\n if (err) {\r\n logger.warn(`Desktop notification failed: ${err.message}`);\r\n }\r\n resolve();\r\n },\r\n );\r\n\r\n // No click handler - dashboard is already open and notifications\r\n // can be clicked there to focus the relevant session\r\n });\r\n }\r\n\r\n private checkWindowsNotifications(): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n execFile(\r\n 'powershell',\r\n ['-Command', '(Get-ItemProperty -Path \"HKCU:\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\PushNotifications\" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],\r\n (err, stdout) => {\r\n if (err) { resolve(true); return; } // assume enabled on error\r\n const value = stdout.trim();\r\n resolve(value !== '0');\r\n },\r\n );\r\n });\r\n }\r\n\r\n private openNotificationSettings(): void {\r\n if (this.notificationSettingsOpened) return;\r\n this.notificationSettingsOpened = true;\r\n\r\n logger.warn('Windows notifications are disabled. Opening notification settings...');\r\n logger.warn('Please enable notifications for this app, then try again.');\r\n\r\n if (process.platform === 'win32') {\r\n execFile('powershell', ['-Command', 'Start-Process ms-settings:notifications']);\r\n }\r\n\r\n // Allow re-opening after 5 minutes\r\n setTimeout(() => { this.notificationSettingsOpened = false; }, 5 * 60 * 1000);\r\n }\r\n\r\n private async sendWebhook(\r\n webhook: WebhookConfig,\r\n title: string,\r\n message: string,\r\n level: NotifyLevel,\r\n ): Promise<void> {\r\n try {\r\n const response = await fetch(webhook.url, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...webhook.headers,\r\n },\r\n body: JSON.stringify({\r\n title,\r\n message,\r\n level,\r\n timestamp: Date.now(),\r\n source: 'claude-alarm',\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n logger.warn(`Webhook ${webhook.url} returned ${response.status}`);\r\n }\r\n } catch (err) {\r\n logger.warn(`Webhook ${webhook.url} failed: ${(err as Error).message}`);\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["path","path"]}
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ interface SessionInfo {
9
9
  lastActivity: number;
10
10
  cwd?: string;
11
11
  channelEnabled?: boolean;
12
+ isLocal?: boolean;
12
13
  }
13
14
  /** Messages sent between channel server and hub */
14
15
  type ChannelMessage = {
@@ -32,6 +33,18 @@ type ChannelMessage = {
32
33
  type: 'message_to_session';
33
34
  sessionId: string;
34
35
  content: string;
36
+ } | {
37
+ type: 'image_upload';
38
+ sessionId: string;
39
+ imageData: string;
40
+ mimeType: string;
41
+ originalName?: string;
42
+ } | {
43
+ type: 'image_to_session';
44
+ sessionId: string;
45
+ imagePath: string;
46
+ mimeType: string;
47
+ originalName?: string;
35
48
  } | {
36
49
  type: 'sessions_list';
37
50
  sessions: SessionInfo[];
@@ -97,6 +110,7 @@ declare class HubServer {
97
110
  private notifier;
98
111
  private startTime;
99
112
  private channelSockets;
113
+ private localChannels;
100
114
  private dashboardSockets;
101
115
  private host;
102
116
  private port;
@@ -112,6 +126,8 @@ declare class HubServer {
112
126
  private handleChannelMessage;
113
127
  private handleDashboardConnection;
114
128
  private broadcastToDashboards;
129
+ private handleImageUpload;
130
+ private cleanupUploads;
115
131
  private getSessionLabel;
116
132
  private jsonResponse;
117
133
  private isLocalRequest;
@@ -190,9 +206,10 @@ declare const CONFIG_DIR: string;
190
206
  declare const CONFIG_FILE: string;
191
207
  declare const PID_FILE: string;
192
208
  declare const LOG_FILE: string;
209
+ declare const UPLOADS_DIR: string;
193
210
  declare const WS_PATH_CHANNEL = "/ws/channel";
194
211
  declare const WS_PATH_DASHBOARD = "/ws/dashboard";
195
212
  declare const CHANNEL_SERVER_NAME = "claude-alarm";
196
213
  declare const CHANNEL_SERVER_VERSION = "0.1.0";
197
214
 
198
- export { type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, Notifier, type NotifyLevel, PID_FILE, type SessionInfo, SessionManager, type SessionStatus, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
215
+ export { type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, Notifier, type NotifyLevel, PID_FILE, type SessionInfo, SessionManager, type SessionStatus, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
package/dist/index.js CHANGED
@@ -23,6 +23,9 @@ var logger = {
23
23
  }
24
24
  };
25
25
 
26
+ // src/hub/server.ts
27
+ import { randomUUID } from "crypto";
28
+
26
29
  // src/shared/constants.ts
27
30
  import path from "path";
28
31
  import os from "os";
@@ -32,6 +35,7 @@ var CONFIG_DIR = path.join(os.homedir(), ".claude-alarm");
32
35
  var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
33
36
  var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
34
37
  var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
38
+ var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
35
39
  var WS_PATH_CHANNEL = "/ws/channel";
36
40
  var WS_PATH_DASHBOARD = "/ws/dashboard";
37
41
  var CHANNEL_SERVER_NAME = "claude-alarm";
@@ -185,6 +189,8 @@ var HubServer = class {
185
189
  startTime = Date.now();
186
190
  // Map sessionId -> channel WebSocket
187
191
  channelSockets = /* @__PURE__ */ new Map();
192
+ // Track which channel connections are local
193
+ localChannels = /* @__PURE__ */ new Set();
188
194
  // All connected dashboard WebSockets
189
195
  dashboardSockets = /* @__PURE__ */ new Set();
190
196
  host;
@@ -206,7 +212,7 @@ var HubServer = class {
206
212
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
207
213
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
208
214
  this.wssChannel = new WebSocketServer({ noServer: true });
209
- this.wssChannel.on("connection", (ws) => this.handleChannelConnection(ws));
215
+ this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
210
216
  this.wssDashboard = new WebSocketServer({ noServer: true });
211
217
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
212
218
  this.httpServer.on("upgrade", (req, socket, head) => {
@@ -233,6 +239,7 @@ var HubServer = class {
233
239
  });
234
240
  }
235
241
  async start() {
242
+ this.cleanupUploads();
236
243
  return new Promise((resolve, reject) => {
237
244
  this.httpServer.on("error", reject);
238
245
  this.httpServer.listen(this.port, this.host, () => {
@@ -366,12 +373,13 @@ var HubServer = class {
366
373
  this.jsonResponse(res, 200, { ok: true });
367
374
  }
368
375
  // --- Channel WebSocket ---
369
- handleChannelConnection(ws) {
370
- logger.info("Channel server connected");
376
+ handleChannelConnection(ws, req) {
377
+ const isLocal = this.isLocalRequest(req);
378
+ logger.info(`Channel server connected (local: ${isLocal})`);
371
379
  ws.on("message", (data) => {
372
380
  try {
373
381
  const msg = JSON.parse(data.toString());
374
- this.handleChannelMessage(ws, msg);
382
+ this.handleChannelMessage(ws, isLocal, msg);
375
383
  } catch {
376
384
  logger.warn("Invalid message from channel");
377
385
  }
@@ -381,6 +389,7 @@ var HubServer = class {
381
389
  if (sock === ws) {
382
390
  const session = this.sessions.unregister(sessionId);
383
391
  this.channelSockets.delete(sessionId);
392
+ this.localChannels.delete(sessionId);
384
393
  logger.info(`Channel disconnected: ${sessionId}`);
385
394
  this.broadcastToDashboards({
386
395
  type: "session_disconnected",
@@ -391,13 +400,15 @@ var HubServer = class {
391
400
  }
392
401
  });
393
402
  }
394
- handleChannelMessage(ws, msg) {
403
+ handleChannelMessage(ws, isLocal, msg) {
395
404
  switch (msg.type) {
396
405
  case "register": {
397
406
  const session = msg.session;
407
+ session.isLocal = isLocal;
398
408
  const isReregister = !!this.sessions.get(session.id);
399
409
  this.sessions.register(session);
400
410
  this.channelSockets.set(session.id, ws);
411
+ if (isLocal) this.localChannels.add(session.id);
401
412
  logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);
402
413
  this.broadcastToDashboards({
403
414
  type: isReregister ? "session_updated" : "session_connected",
@@ -459,6 +470,8 @@ var HubServer = class {
459
470
  if (channelWs?.readyState === WebSocket.OPEN) {
460
471
  channelWs.send(JSON.stringify(msg));
461
472
  }
473
+ } else if (msg.type === "image_upload") {
474
+ this.handleImageUpload(msg);
462
475
  }
463
476
  } catch {
464
477
  logger.warn("Invalid message from dashboard");
@@ -478,6 +491,57 @@ var HubServer = class {
478
491
  }
479
492
  }
480
493
  }
494
+ handleImageUpload(msg) {
495
+ const { sessionId, imageData, mimeType, originalName } = msg;
496
+ if (!this.localChannels.has(sessionId)) {
497
+ logger.warn(`Image upload rejected: session ${sessionId} is not local`);
498
+ return;
499
+ }
500
+ const channelWs = this.channelSockets.get(sessionId);
501
+ if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
502
+ const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
503
+ if (!allowedTypes.includes(mimeType)) return;
504
+ const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
505
+ const buffer = Buffer.from(base64Data, "base64");
506
+ if (buffer.length > 10 * 1024 * 1024) {
507
+ logger.warn("Image upload rejected: exceeds 10MB");
508
+ return;
509
+ }
510
+ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
511
+ const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
512
+ const filename = `${randomUUID()}.${ext}`;
513
+ const filePath = path2.join(UPLOADS_DIR, filename);
514
+ fs.writeFileSync(filePath, buffer);
515
+ const forwardMsg = {
516
+ type: "image_to_session",
517
+ sessionId,
518
+ imagePath: filePath,
519
+ mimeType,
520
+ originalName
521
+ };
522
+ channelWs.send(JSON.stringify(forwardMsg));
523
+ logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
524
+ setTimeout(() => {
525
+ try {
526
+ fs.unlinkSync(filePath);
527
+ } catch {
528
+ }
529
+ }, 5 * 60 * 1e3);
530
+ }
531
+ cleanupUploads() {
532
+ try {
533
+ if (!fs.existsSync(UPLOADS_DIR)) return;
534
+ const files = fs.readdirSync(UPLOADS_DIR);
535
+ for (const file of files) {
536
+ try {
537
+ fs.unlinkSync(path2.join(UPLOADS_DIR, file));
538
+ } catch {
539
+ }
540
+ }
541
+ if (files.length > 0) logger.info(`Cleaned up ${files.length} leftover upload(s)`);
542
+ } catch {
543
+ }
544
+ }
481
545
  getSessionLabel(session) {
482
546
  if (!session) return "unknown";
483
547
  return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
@@ -628,7 +692,7 @@ var HubClient = class {
628
692
  // src/shared/config.ts
629
693
  import fs2 from "fs";
630
694
  import path3 from "path";
631
- import { randomUUID } from "crypto";
695
+ import { randomUUID as randomUUID2 } from "crypto";
632
696
  var DEFAULT_CONFIG = {
633
697
  hub: {
634
698
  host: DEFAULT_HUB_HOST,
@@ -660,7 +724,7 @@ function loadConfig() {
660
724
  }
661
725
  }
662
726
  if (!config.hub.token) {
663
- config.hub.token = randomUUID();
727
+ config.hub.token = randomUUID2();
664
728
  saveConfig(config);
665
729
  }
666
730
  return config;
@@ -706,6 +770,7 @@ export {
706
770
  Notifier,
707
771
  PID_FILE,
708
772
  SessionManager,
773
+ UPLOADS_DIR,
709
774
  WS_PATH_CHANNEL,
710
775
  WS_PATH_DASHBOARD,
711
776
  loadConfig,