@delt/claude-alarm 0.7.1 → 0.8.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.
@@ -203,7 +203,20 @@ var server = new Server(
203
203
  },
204
204
  tools: {}
205
205
  },
206
- instructions: 'Messages from the claude-alarm dashboard arrive as <channel source="claude-alarm" sender="...">. Read the message and act on it. Reply with the same detail and depth as you normally would \u2014 do not shorten your response. IMPORTANT: The dashboard user can ONLY see messages sent via the reply tool. Your terminal output is NOT visible on the dashboard. Therefore, when responding to a dashboard message, you MUST call the reply tool with your response so the dashboard user can see it. Use the notify tool to send desktop notifications for key events: task completion, errors, or when user input is needed. Do NOT notify for intermediate steps or simple acknowledgments. Use the status tool to update your session status.'
206
+ instructions: `Messages from the claude-alarm dashboard arrive as <channel source="claude-alarm" sender="...">. Read the message and act on it.
207
+
208
+ REPLYING:
209
+ - The dashboard user ONLY sees messages sent via the reply tool \u2014 your terminal output is invisible to them. You MUST call reply with your response.
210
+ - The dashboard renders Markdown (headings, lists, tables, code blocks). Provide a thorough, complete answer; do not apply CLI-style brevity.
211
+ - Reply in the user's language (match the language they wrote in).
212
+
213
+ IMAGES: If a channel message contains "[Image: ...] Read the file to view it: <path>", use the Read tool on that path before responding \u2014 otherwise the image is invisible to you.
214
+
215
+ STATUS: Call status("working") before starting a long task, status("waiting_input") when blocked on user input, status("idle") when finished responding.
216
+
217
+ NOTIFICATIONS:
218
+ - Use notify only for key events: task completion, errors, or when user attention is needed. Not for intermediate steps or simple acknowledgments.
219
+ - Pick level: success=completion, error=failure, warning=attention needed, info=neutral status.`
207
220
  }
208
221
  );
209
222
  var config = loadConfig();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/channel/server.ts","../../src/shared/logger.ts","../../src/shared/constants.ts","../../src/shared/config.ts","../../src/channel/hub-client.ts"],"sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\r\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\r\nimport {\r\n CallToolRequestSchema,\r\n ListToolsRequestSchema,\r\n} from '@modelcontextprotocol/sdk/types.js';\r\nimport { z } from 'zod';\r\nimport { randomUUID } from 'node:crypto';\r\nimport path from 'node:path';\r\nimport { logger } from '../shared/logger.js';\r\nimport { CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION } from '../shared/constants.js';\r\nimport { loadConfig } from '../shared/config.js';\r\nimport { HubClient } from './hub-client.js';\r\nimport type { SessionStatus, NotifyLevel } from '../shared/types.js';\r\n\r\nconst sessionId = randomUUID();\r\nconst sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path.basename(process.cwd());\r\n\r\nconst server = new Server(\r\n {\r\n name: CHANNEL_SERVER_NAME,\r\n version: CHANNEL_SERVER_VERSION,\r\n },\r\n {\r\n capabilities: {\r\n experimental: {\r\n 'claude/channel': {},\r\n 'claude/channel/permission': {},\r\n },\r\n tools: {},\r\n },\r\n instructions:\r\n 'Messages from the claude-alarm dashboard arrive as <channel source=\"claude-alarm\" sender=\"...\">. ' +\r\n 'Read the message and act on it. Reply with the same detail and depth as you normally would — do not shorten your response. ' +\r\n 'IMPORTANT: The dashboard user can ONLY see messages sent via the reply tool. Your terminal output is NOT visible on the dashboard. ' +\r\n 'Therefore, when responding to a dashboard message, you MUST call the reply tool with your response so the dashboard user can see it. ' +\r\n 'Use the notify tool to send desktop notifications for key events: task completion, errors, or when user input is needed. ' +\r\n 'Do NOT notify for intermediate steps or simple acknowledgments. ' +\r\n 'Use the status tool to update your session status.',\r\n },\r\n);\r\n\r\n// Load config for hub connection (env vars take priority)\r\nconst config = loadConfig();\r\nconst hubHost = process.env.CLAUDE_ALARM_HUB_HOST ?? config.hub.host;\r\nconst hubPort = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;\r\nconst hubToken = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;\r\n\r\n// Hub client for forwarding to central hub\r\nconst hubClient = new HubClient(\r\n sessionId,\r\n sessionName,\r\n hubHost,\r\n hubPort,\r\n hubToken,\r\n);\r\n\r\n// --- Tools ---\r\n\r\nserver.setRequestHandler(ListToolsRequestSchema, async () => ({\r\n tools: [\r\n {\r\n name: 'notify',\r\n description:\r\n 'Send a desktop notification to the user. Use this when you complete a task, encounter an error, or need user attention. The notification will appear as a system toast/popup.',\r\n inputSchema: {\r\n type: 'object' as const,\r\n properties: {\r\n title: { type: 'string', description: 'Notification title (short)' },\r\n message: { type: 'string', description: 'Notification body text' },\r\n level: {\r\n type: 'string',\r\n enum: ['info', 'warning', 'error', 'success'],\r\n description: 'Notification level (default: info)',\r\n },\r\n },\r\n required: ['title', 'message'],\r\n },\r\n },\r\n {\r\n name: 'reply',\r\n description:\r\n 'Send a message to the web dashboard. Use this to communicate status updates, results, or any information the user should see in the monitoring dashboard.',\r\n inputSchema: {\r\n type: 'object' as const,\r\n properties: {\r\n content: { type: 'string', description: 'Message content to display on the dashboard' },\r\n },\r\n required: ['content'],\r\n },\r\n },\r\n {\r\n name: 'status',\r\n description:\r\n 'Update your session status displayed on the dashboard. Set to \"working\" when actively processing, \"waiting_input\" when you need user input, or \"idle\" when done.',\r\n inputSchema: {\r\n type: 'object' as const,\r\n properties: {\r\n status: {\r\n type: 'string',\r\n enum: ['idle', 'working', 'waiting_input'],\r\n description: 'Current session status',\r\n },\r\n },\r\n required: ['status'],\r\n },\r\n },\r\n ],\r\n}));\r\n\r\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\r\n const { name, arguments: args } = request.params;\r\n\r\n switch (name) {\r\n case 'notify': {\r\n const title = args?.title as string;\r\n const message = args?.message as string;\r\n const level = (args?.level as NotifyLevel) ?? 'info';\r\n logger.info(`Notify [${level}]: ${title} - ${message}`);\r\n hubClient.send({\r\n type: 'notify',\r\n sessionId,\r\n title,\r\n message,\r\n level,\r\n });\r\n return {\r\n content: [{ type: 'text', text: `Notification sent: \"${title}\"` }],\r\n };\r\n }\r\n\r\n case 'reply': {\r\n const content = args?.content as string;\r\n logger.info(`Reply: ${content.slice(0, 100)}...`);\r\n hubClient.send({\r\n type: 'reply',\r\n sessionId,\r\n content,\r\n });\r\n return {\r\n content: [{ type: 'text', text: 'Message sent to dashboard.' }],\r\n };\r\n }\r\n\r\n case 'status': {\r\n const status = args?.status as SessionStatus;\r\n logger.info(`Status update: ${status}`);\r\n hubClient.send({\r\n type: 'status',\r\n sessionId,\r\n status,\r\n });\r\n return {\r\n content: [{ type: 'text', text: `Status updated to \"${status}\".` }],\r\n };\r\n }\r\n\r\n default:\r\n return {\r\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\r\n isError: true,\r\n };\r\n }\r\n});\r\n\r\n// --- Permission Relay ---\r\n\r\nconst PermissionRequestSchema = z.object({\r\n method: z.literal('notifications/claude/channel/permission_request'),\r\n params: z.object({\r\n request_id: z.string(),\r\n tool_name: z.string(),\r\n description: z.string(),\r\n input_preview: z.string(),\r\n }),\r\n});\r\n\r\n// Handle permission_request from Claude Code and forward to hub\r\nserver.setNotificationHandler(\r\n PermissionRequestSchema,\r\n async (notification) => {\r\n const { request_id, tool_name, description, input_preview } = notification.params;\r\n logger.info(`Permission request [${request_id}]: ${tool_name} - ${description}`);\r\n hubClient.send({\r\n type: 'permission_request',\r\n sessionId,\r\n requestId: request_id,\r\n toolName: tool_name,\r\n description,\r\n inputPreview: input_preview,\r\n timestamp: Date.now(),\r\n });\r\n },\r\n);\r\n\r\n// --- Startup ---\r\n\r\nasync function main() {\r\n logger.info(`Starting MCP channel server (session: ${sessionId})`);\r\n\r\n // Connect to hub (non-blocking, will retry)\r\n hubClient.connect();\r\n\r\n // Listen for messages from hub and forward to Claude via channel notification\r\n hubClient.onMessage(async (msg) => {\r\n if (msg.type === 'message_to_session' && msg.sessionId === sessionId) {\r\n logger.info(`Message from dashboard: ${msg.content}`);\r\n await server.notification({\r\n method: 'notifications/claude/channel',\r\n params: {\r\n content: msg.content,\r\n meta: { sender: 'dashboard', timestamp: String(Date.now()) },\r\n },\r\n });\r\n } else if (msg.type === 'image_to_session' && msg.sessionId === sessionId) {\r\n logger.info(`Image from dashboard: ${msg.imagePath}`);\r\n const textPart = msg.content ? `\\n${msg.content}` : '';\r\n await server.notification({\r\n method: 'notifications/claude/channel',\r\n params: {\r\n content: `[Image: ${msg.originalName || 'image'}] The user sent an image. Read the file to view it: ${msg.imagePath}${textPart}`,\r\n meta: { sender: 'dashboard', timestamp: String(Date.now()), imagePath: msg.imagePath, mimeType: msg.mimeType },\r\n },\r\n });\r\n } else if (msg.type === 'permission_response' && msg.sessionId === sessionId) {\r\n logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior}`);\r\n await server.notification({\r\n method: 'notifications/claude/channel/permission',\r\n params: {\r\n request_id: msg.requestId,\r\n behavior: msg.behavior,\r\n },\r\n });\r\n }\r\n });\r\n\r\n const transport = new StdioServerTransport();\r\n await server.connect(transport);\r\n logger.info('MCP channel server running on stdio');\r\n\r\n // Exit when stdin closes (Claude Code session ended)\r\n process.stdin.on('end', () => {\r\n logger.info('stdin closed, exiting');\r\n hubClient.disconnect();\r\n process.exit(0);\r\n });\r\n process.stdin.on('close', () => {\r\n logger.info('stdin closed, exiting');\r\n hubClient.disconnect();\r\n process.exit(0);\r\n });\r\n}\r\n\r\nmain().catch((err) => {\r\n logger.error('Fatal error:', err);\r\n process.exit(1);\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 fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { randomUUID } from 'node:crypto';\r\nimport { CONFIG_DIR, CONFIG_FILE, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT } from './constants.js';\r\nimport type { AppConfig } from './types.js';\r\n\r\nconst DEFAULT_CONFIG: AppConfig = {\r\n hub: {\r\n host: DEFAULT_HUB_HOST,\r\n port: DEFAULT_HUB_PORT,\r\n },\r\n notifications: {\r\n desktop: true,\r\n sound: true,\r\n },\r\n webhooks: [],\r\n};\r\n\r\nexport function ensureConfigDir(): void {\r\n if (!fs.existsSync(CONFIG_DIR)) {\r\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\r\n }\r\n}\r\n\r\nexport function loadConfig(): AppConfig {\r\n ensureConfigDir();\r\n let config: AppConfig;\r\n if (!fs.existsSync(CONFIG_FILE)) {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n } else {\r\n try {\r\n const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');\r\n const parsed = JSON.parse(raw);\r\n config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...(parsed.telegram ? { telegram: parsed.telegram } : {}) };\r\n } catch {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n }\r\n }\r\n\r\n // Auto-generate token if missing\r\n if (!config.hub.token) {\r\n config.hub.token = randomUUID();\r\n saveConfig(config);\r\n }\r\n\r\n return config;\r\n}\r\n\r\n/** Get the current token, generating one if needed */\r\nexport function getOrCreateToken(): string {\r\n const config = loadConfig();\r\n return config.hub.token!;\r\n}\r\n\r\nexport function saveConfig(config: AppConfig): void {\r\n ensureConfigDir();\r\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n}\r\n\r\n/**\r\n * Add claude-alarm as an MCP channel server to .mcp.json\r\n */\r\nexport function setupMcpConfig(targetDir?: string): string {\r\n const dir = targetDir ?? process.cwd();\r\n const mcpPath = path.join(dir, '.mcp.json');\r\n\r\n let mcpConfig: Record<string, any> = {};\r\n if (fs.existsSync(mcpPath)) {\r\n try {\r\n mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));\r\n } catch {\r\n mcpConfig = {};\r\n }\r\n }\r\n\r\n if (!mcpConfig.mcpServers) {\r\n mcpConfig.mcpServers = {};\r\n }\r\n\r\n mcpConfig.mcpServers['claude-alarm'] = {\r\n command: 'npx',\r\n args: ['-y', '@delt/claude-alarm', 'serve'],\r\n env: {\r\n CLAUDE_ALARM_SESSION_NAME: path.basename(dir),\r\n },\r\n };\r\n\r\n fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');\r\n return mcpPath;\r\n}\r\n","import WebSocket from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\nimport { DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, WS_PATH_CHANNEL } from '../shared/constants.js';\r\nimport type { ChannelMessage, SessionInfo } from '../shared/types.js';\r\n\r\nexport class HubClient {\r\n private ws: WebSocket | null = null;\r\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n private messageHandlers: Array<(msg: ChannelMessage) => void> = [];\r\n private queue: ChannelMessage[] = [];\r\n private connected = false;\r\n\r\n constructor(\r\n private sessionId: string,\r\n private sessionName: string,\r\n private hubHost = DEFAULT_HUB_HOST,\r\n private hubPort = DEFAULT_HUB_PORT,\r\n private token?: string,\r\n ) {}\r\n\r\n connect(): void {\r\n const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : '';\r\n const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;\r\n logger.debug(`Connecting to hub at ${url}`);\r\n\r\n try {\r\n this.ws = new WebSocket(url);\r\n\r\n this.ws.on('open', () => {\r\n logger.info('Connected to hub');\r\n this.connected = true;\r\n\r\n // Register this session\r\n const registration: ChannelMessage = {\r\n type: 'register',\r\n session: {\r\n id: this.sessionId,\r\n name: this.sessionName,\r\n status: 'idle',\r\n connectedAt: Date.now(),\r\n lastActivity: Date.now(),\r\n cwd: process.cwd(),\r\n channelEnabled: true,\r\n },\r\n };\r\n this.ws!.send(JSON.stringify(registration));\r\n\r\n // Flush queued messages\r\n for (const msg of this.queue) {\r\n this.ws!.send(JSON.stringify(msg));\r\n }\r\n this.queue = [];\r\n });\r\n\r\n this.ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n for (const handler of this.messageHandlers) {\r\n handler(msg);\r\n }\r\n } catch (err) {\r\n logger.warn('Failed to parse hub message:', err);\r\n }\r\n });\r\n\r\n this.ws.on('close', () => {\r\n logger.info('Disconnected from hub');\r\n this.connected = false;\r\n this.scheduleReconnect();\r\n });\r\n\r\n this.ws.on('error', (err) => {\r\n logger.debug(`Hub connection error: ${err.message}`);\r\n this.connected = false;\r\n });\r\n } catch {\r\n logger.debug('Failed to connect to hub, will retry');\r\n this.scheduleReconnect();\r\n }\r\n }\r\n\r\n send(msg: ChannelMessage): void {\r\n if (this.connected && this.ws?.readyState === WebSocket.OPEN) {\r\n this.ws.send(JSON.stringify(msg));\r\n } else {\r\n if (this.queue.length < 100) {\r\n this.queue.push(msg);\r\n }\r\n logger.debug('Hub not connected, message queued');\r\n }\r\n }\r\n\r\n onMessage(handler: (msg: ChannelMessage) => void): void {\r\n this.messageHandlers.push(handler);\r\n }\r\n\r\n disconnect(): void {\r\n if (this.reconnectTimer) {\r\n clearTimeout(this.reconnectTimer);\r\n this.reconnectTimer = null;\r\n }\r\n if (this.ws) {\r\n this.ws.close();\r\n this.ws = null;\r\n }\r\n this.connected = false;\r\n }\r\n\r\n private scheduleReconnect(): void {\r\n if (this.reconnectTimer) return;\r\n this.reconnectTimer = setTimeout(() => {\r\n this.reconnectTimer = null;\r\n this.connect();\r\n }, 5000);\r\n }\r\n}\r\n"],"mappings":"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","names":["randomUUID","path","path","config","config","sessionId","sessionName","hubHost","hubPort","randomUUID","path"]}
1
+ {"version":3,"sources":["../../src/channel/server.ts","../../src/shared/logger.ts","../../src/shared/constants.ts","../../src/shared/config.ts","../../src/channel/hub-client.ts"],"sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\r\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\r\nimport {\r\n CallToolRequestSchema,\r\n ListToolsRequestSchema,\r\n} from '@modelcontextprotocol/sdk/types.js';\r\nimport { z } from 'zod';\r\nimport { randomUUID } from 'node:crypto';\r\nimport path from 'node:path';\r\nimport { logger } from '../shared/logger.js';\r\nimport { CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION } from '../shared/constants.js';\r\nimport { loadConfig } from '../shared/config.js';\r\nimport { HubClient } from './hub-client.js';\r\nimport type { SessionStatus, NotifyLevel } from '../shared/types.js';\r\n\r\nconst sessionId = randomUUID();\r\nconst sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path.basename(process.cwd());\r\n\r\nconst server = new Server(\r\n {\r\n name: CHANNEL_SERVER_NAME,\r\n version: CHANNEL_SERVER_VERSION,\r\n },\r\n {\r\n capabilities: {\r\n experimental: {\r\n 'claude/channel': {},\r\n 'claude/channel/permission': {},\r\n },\r\n tools: {},\r\n },\r\n instructions: `Messages from the claude-alarm dashboard arrive as <channel source=\"claude-alarm\" sender=\"...\">. Read the message and act on it.\r\n\r\nREPLYING:\r\n- The dashboard user ONLY sees messages sent via the reply tool — your terminal output is invisible to them. You MUST call reply with your response.\r\n- The dashboard renders Markdown (headings, lists, tables, code blocks). Provide a thorough, complete answer; do not apply CLI-style brevity.\r\n- Reply in the user's language (match the language they wrote in).\r\n\r\nIMAGES: If a channel message contains \"[Image: ...] Read the file to view it: <path>\", use the Read tool on that path before responding — otherwise the image is invisible to you.\r\n\r\nSTATUS: Call status(\"working\") before starting a long task, status(\"waiting_input\") when blocked on user input, status(\"idle\") when finished responding.\r\n\r\nNOTIFICATIONS:\r\n- Use notify only for key events: task completion, errors, or when user attention is needed. Not for intermediate steps or simple acknowledgments.\r\n- Pick level: success=completion, error=failure, warning=attention needed, info=neutral status.`,\r\n },\r\n);\r\n\r\n// Load config for hub connection (env vars take priority)\r\nconst config = loadConfig();\r\nconst hubHost = process.env.CLAUDE_ALARM_HUB_HOST ?? config.hub.host;\r\nconst hubPort = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;\r\nconst hubToken = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;\r\n\r\n// Hub client for forwarding to central hub\r\nconst hubClient = new HubClient(\r\n sessionId,\r\n sessionName,\r\n hubHost,\r\n hubPort,\r\n hubToken,\r\n);\r\n\r\n// --- Tools ---\r\n\r\nserver.setRequestHandler(ListToolsRequestSchema, async () => ({\r\n tools: [\r\n {\r\n name: 'notify',\r\n description:\r\n 'Send a desktop notification to the user. Use this when you complete a task, encounter an error, or need user attention. The notification will appear as a system toast/popup.',\r\n inputSchema: {\r\n type: 'object' as const,\r\n properties: {\r\n title: { type: 'string', description: 'Notification title (short)' },\r\n message: { type: 'string', description: 'Notification body text' },\r\n level: {\r\n type: 'string',\r\n enum: ['info', 'warning', 'error', 'success'],\r\n description: 'Notification level (default: info)',\r\n },\r\n },\r\n required: ['title', 'message'],\r\n },\r\n },\r\n {\r\n name: 'reply',\r\n description:\r\n 'Send a message to the web dashboard. Use this to communicate status updates, results, or any information the user should see in the monitoring dashboard.',\r\n inputSchema: {\r\n type: 'object' as const,\r\n properties: {\r\n content: { type: 'string', description: 'Message content to display on the dashboard' },\r\n },\r\n required: ['content'],\r\n },\r\n },\r\n {\r\n name: 'status',\r\n description:\r\n 'Update your session status displayed on the dashboard. Set to \"working\" when actively processing, \"waiting_input\" when you need user input, or \"idle\" when done.',\r\n inputSchema: {\r\n type: 'object' as const,\r\n properties: {\r\n status: {\r\n type: 'string',\r\n enum: ['idle', 'working', 'waiting_input'],\r\n description: 'Current session status',\r\n },\r\n },\r\n required: ['status'],\r\n },\r\n },\r\n ],\r\n}));\r\n\r\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\r\n const { name, arguments: args } = request.params;\r\n\r\n switch (name) {\r\n case 'notify': {\r\n const title = args?.title as string;\r\n const message = args?.message as string;\r\n const level = (args?.level as NotifyLevel) ?? 'info';\r\n logger.info(`Notify [${level}]: ${title} - ${message}`);\r\n hubClient.send({\r\n type: 'notify',\r\n sessionId,\r\n title,\r\n message,\r\n level,\r\n });\r\n return {\r\n content: [{ type: 'text', text: `Notification sent: \"${title}\"` }],\r\n };\r\n }\r\n\r\n case 'reply': {\r\n const content = args?.content as string;\r\n logger.info(`Reply: ${content.slice(0, 100)}...`);\r\n hubClient.send({\r\n type: 'reply',\r\n sessionId,\r\n content,\r\n });\r\n return {\r\n content: [{ type: 'text', text: 'Message sent to dashboard.' }],\r\n };\r\n }\r\n\r\n case 'status': {\r\n const status = args?.status as SessionStatus;\r\n logger.info(`Status update: ${status}`);\r\n hubClient.send({\r\n type: 'status',\r\n sessionId,\r\n status,\r\n });\r\n return {\r\n content: [{ type: 'text', text: `Status updated to \"${status}\".` }],\r\n };\r\n }\r\n\r\n default:\r\n return {\r\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\r\n isError: true,\r\n };\r\n }\r\n});\r\n\r\n// --- Permission Relay ---\r\n\r\nconst PermissionRequestSchema = z.object({\r\n method: z.literal('notifications/claude/channel/permission_request'),\r\n params: z.object({\r\n request_id: z.string(),\r\n tool_name: z.string(),\r\n description: z.string(),\r\n input_preview: z.string(),\r\n }),\r\n});\r\n\r\n// Handle permission_request from Claude Code and forward to hub\r\nserver.setNotificationHandler(\r\n PermissionRequestSchema,\r\n async (notification) => {\r\n const { request_id, tool_name, description, input_preview } = notification.params;\r\n logger.info(`Permission request [${request_id}]: ${tool_name} - ${description}`);\r\n hubClient.send({\r\n type: 'permission_request',\r\n sessionId,\r\n requestId: request_id,\r\n toolName: tool_name,\r\n description,\r\n inputPreview: input_preview,\r\n timestamp: Date.now(),\r\n });\r\n },\r\n);\r\n\r\n// --- Startup ---\r\n\r\nasync function main() {\r\n logger.info(`Starting MCP channel server (session: ${sessionId})`);\r\n\r\n // Connect to hub (non-blocking, will retry)\r\n hubClient.connect();\r\n\r\n // Listen for messages from hub and forward to Claude via channel notification\r\n hubClient.onMessage(async (msg) => {\r\n if (msg.type === 'message_to_session' && msg.sessionId === sessionId) {\r\n logger.info(`Message from dashboard: ${msg.content}`);\r\n await server.notification({\r\n method: 'notifications/claude/channel',\r\n params: {\r\n content: msg.content,\r\n meta: { sender: 'dashboard', timestamp: String(Date.now()) },\r\n },\r\n });\r\n } else if (msg.type === 'image_to_session' && msg.sessionId === sessionId) {\r\n logger.info(`Image from dashboard: ${msg.imagePath}`);\r\n const textPart = msg.content ? `\\n${msg.content}` : '';\r\n await server.notification({\r\n method: 'notifications/claude/channel',\r\n params: {\r\n content: `[Image: ${msg.originalName || 'image'}] The user sent an image. Read the file to view it: ${msg.imagePath}${textPart}`,\r\n meta: { sender: 'dashboard', timestamp: String(Date.now()), imagePath: msg.imagePath, mimeType: msg.mimeType },\r\n },\r\n });\r\n } else if (msg.type === 'permission_response' && msg.sessionId === sessionId) {\r\n logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior}`);\r\n await server.notification({\r\n method: 'notifications/claude/channel/permission',\r\n params: {\r\n request_id: msg.requestId,\r\n behavior: msg.behavior,\r\n },\r\n });\r\n }\r\n });\r\n\r\n const transport = new StdioServerTransport();\r\n await server.connect(transport);\r\n logger.info('MCP channel server running on stdio');\r\n\r\n // Exit when stdin closes (Claude Code session ended)\r\n process.stdin.on('end', () => {\r\n logger.info('stdin closed, exiting');\r\n hubClient.disconnect();\r\n process.exit(0);\r\n });\r\n process.stdin.on('close', () => {\r\n logger.info('stdin closed, exiting');\r\n hubClient.disconnect();\r\n process.exit(0);\r\n });\r\n}\r\n\r\nmain().catch((err) => {\r\n logger.error('Fatal error:', err);\r\n process.exit(1);\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 fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { randomUUID } from 'node:crypto';\r\nimport { CONFIG_DIR, CONFIG_FILE, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT } from './constants.js';\r\nimport type { AppConfig } from './types.js';\r\n\r\nconst DEFAULT_CONFIG: AppConfig = {\r\n hub: {\r\n host: DEFAULT_HUB_HOST,\r\n port: DEFAULT_HUB_PORT,\r\n },\r\n notifications: {\r\n desktop: true,\r\n sound: true,\r\n },\r\n webhooks: [],\r\n};\r\n\r\nexport function ensureConfigDir(): void {\r\n if (!fs.existsSync(CONFIG_DIR)) {\r\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\r\n }\r\n}\r\n\r\nexport function loadConfig(): AppConfig {\r\n ensureConfigDir();\r\n let config: AppConfig;\r\n if (!fs.existsSync(CONFIG_FILE)) {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n } else {\r\n try {\r\n const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');\r\n const parsed = JSON.parse(raw);\r\n config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...(parsed.telegram ? { telegram: parsed.telegram } : {}) };\r\n } catch {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n }\r\n }\r\n\r\n // Auto-generate token if missing\r\n if (!config.hub.token) {\r\n config.hub.token = randomUUID();\r\n saveConfig(config);\r\n }\r\n\r\n return config;\r\n}\r\n\r\n/** Get the current token, generating one if needed */\r\nexport function getOrCreateToken(): string {\r\n const config = loadConfig();\r\n return config.hub.token!;\r\n}\r\n\r\nexport function saveConfig(config: AppConfig): void {\r\n ensureConfigDir();\r\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n}\r\n\r\n/**\r\n * Add claude-alarm as an MCP channel server to .mcp.json\r\n */\r\nexport function setupMcpConfig(targetDir?: string): string {\r\n const dir = targetDir ?? process.cwd();\r\n const mcpPath = path.join(dir, '.mcp.json');\r\n\r\n let mcpConfig: Record<string, any> = {};\r\n if (fs.existsSync(mcpPath)) {\r\n try {\r\n mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));\r\n } catch {\r\n mcpConfig = {};\r\n }\r\n }\r\n\r\n if (!mcpConfig.mcpServers) {\r\n mcpConfig.mcpServers = {};\r\n }\r\n\r\n mcpConfig.mcpServers['claude-alarm'] = {\r\n command: 'npx',\r\n args: ['-y', '@delt/claude-alarm', 'serve'],\r\n env: {\r\n CLAUDE_ALARM_SESSION_NAME: path.basename(dir),\r\n },\r\n };\r\n\r\n fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');\r\n return mcpPath;\r\n}\r\n","import WebSocket from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\nimport { DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, WS_PATH_CHANNEL } from '../shared/constants.js';\r\nimport type { ChannelMessage, SessionInfo } from '../shared/types.js';\r\n\r\nexport class HubClient {\r\n private ws: WebSocket | null = null;\r\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n private messageHandlers: Array<(msg: ChannelMessage) => void> = [];\r\n private queue: ChannelMessage[] = [];\r\n private connected = false;\r\n\r\n constructor(\r\n private sessionId: string,\r\n private sessionName: string,\r\n private hubHost = DEFAULT_HUB_HOST,\r\n private hubPort = DEFAULT_HUB_PORT,\r\n private token?: string,\r\n ) {}\r\n\r\n connect(): void {\r\n const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : '';\r\n const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;\r\n logger.debug(`Connecting to hub at ${url}`);\r\n\r\n try {\r\n this.ws = new WebSocket(url);\r\n\r\n this.ws.on('open', () => {\r\n logger.info('Connected to hub');\r\n this.connected = true;\r\n\r\n // Register this session\r\n const registration: ChannelMessage = {\r\n type: 'register',\r\n session: {\r\n id: this.sessionId,\r\n name: this.sessionName,\r\n status: 'idle',\r\n connectedAt: Date.now(),\r\n lastActivity: Date.now(),\r\n cwd: process.cwd(),\r\n channelEnabled: true,\r\n },\r\n };\r\n this.ws!.send(JSON.stringify(registration));\r\n\r\n // Flush queued messages\r\n for (const msg of this.queue) {\r\n this.ws!.send(JSON.stringify(msg));\r\n }\r\n this.queue = [];\r\n });\r\n\r\n this.ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n for (const handler of this.messageHandlers) {\r\n handler(msg);\r\n }\r\n } catch (err) {\r\n logger.warn('Failed to parse hub message:', err);\r\n }\r\n });\r\n\r\n this.ws.on('close', () => {\r\n logger.info('Disconnected from hub');\r\n this.connected = false;\r\n this.scheduleReconnect();\r\n });\r\n\r\n this.ws.on('error', (err) => {\r\n logger.debug(`Hub connection error: ${err.message}`);\r\n this.connected = false;\r\n });\r\n } catch {\r\n logger.debug('Failed to connect to hub, will retry');\r\n this.scheduleReconnect();\r\n }\r\n }\r\n\r\n send(msg: ChannelMessage): void {\r\n if (this.connected && this.ws?.readyState === WebSocket.OPEN) {\r\n this.ws.send(JSON.stringify(msg));\r\n } else {\r\n if (this.queue.length < 100) {\r\n this.queue.push(msg);\r\n }\r\n logger.debug('Hub not connected, message queued');\r\n }\r\n }\r\n\r\n onMessage(handler: (msg: ChannelMessage) => void): void {\r\n this.messageHandlers.push(handler);\r\n }\r\n\r\n disconnect(): void {\r\n if (this.reconnectTimer) {\r\n clearTimeout(this.reconnectTimer);\r\n this.reconnectTimer = null;\r\n }\r\n if (this.ws) {\r\n this.ws.close();\r\n this.ws = null;\r\n }\r\n this.connected = false;\r\n }\r\n\r\n private scheduleReconnect(): void {\r\n if (this.reconnectTimer) return;\r\n this.reconnectTimer = setTimeout(() => {\r\n this.reconnectTimer = null;\r\n this.connect();\r\n }, 5000);\r\n }\r\n}\r\n"],"mappings":"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","names":["randomUUID","path","path","config","config","sessionId","sessionName","hubHost","hubPort","randomUUID","path"]}
package/dist/cli.js CHANGED
@@ -134,6 +134,62 @@ var init_logger = __esm({
134
134
  }
135
135
  });
136
136
 
137
+ // src/shared/crash-guard.ts
138
+ import fs2 from "fs";
139
+ function appendToLog(text) {
140
+ try {
141
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
142
+ fs2.appendFileSync(LOG_FILE, text, "utf-8");
143
+ } catch {
144
+ }
145
+ }
146
+ function describe(err) {
147
+ if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
148
+ try {
149
+ return JSON.stringify(err);
150
+ } catch {
151
+ return String(err);
152
+ }
153
+ }
154
+ function installCrashGuard(context) {
155
+ let recent = [];
156
+ const handle = (kind, err) => {
157
+ const stack = describe(err);
158
+ appendToLog(`
159
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} (${context})
160
+ ${stack}
161
+ `);
162
+ logger.error(`${kind} caught by crash guard: ${stack}`);
163
+ const now = Date.now();
164
+ recent = recent.filter((t) => now - t < BURST_WINDOW_MS);
165
+ recent.push(now);
166
+ if (recent.length > BURST_LIMIT) {
167
+ const msg = `${recent.length} unhandled errors within ${BURST_WINDOW_MS / 1e3}s \u2014 exiting`;
168
+ appendToLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
169
+ `);
170
+ logger.error(msg);
171
+ process.exit(1);
172
+ }
173
+ };
174
+ process.on("uncaughtException", (err) => handle("uncaughtException", err));
175
+ process.on("unhandledRejection", (reason) => handle("unhandledRejection", reason));
176
+ }
177
+ function logStartup(context) {
178
+ appendToLog(`
179
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${context} started (pid ${process.pid})
180
+ `);
181
+ }
182
+ var BURST_WINDOW_MS, BURST_LIMIT;
183
+ var init_crash_guard = __esm({
184
+ "src/shared/crash-guard.ts"() {
185
+ "use strict";
186
+ init_constants();
187
+ init_logger();
188
+ BURST_WINDOW_MS = 6e4;
189
+ BURST_LIMIT = 10;
190
+ }
191
+ });
192
+
137
193
  // src/hub/session-manager.ts
138
194
  var SessionManager;
139
195
  var init_session_manager = __esm({
@@ -312,7 +368,7 @@ var init_notifier = __esm({
312
368
  });
313
369
 
314
370
  // src/hub/telegram.ts
315
- import fs2 from "fs";
371
+ import fs3 from "fs";
316
372
  import path3 from "path";
317
373
  import { randomUUID as randomUUID2 } from "crypto";
318
374
  var TELEGRAM_API, TelegramBot;
@@ -531,17 +587,17 @@ ${this.mdToHtml(message)}`;
531
587
  const buffer = Buffer.from(await imgRes.arrayBuffer());
532
588
  const ext = fileData.result.file_path.split(".").pop() || "jpg";
533
589
  const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
534
- fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
590
+ fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
535
591
  const filename = `${randomUUID2()}.${ext}`;
536
592
  const filePath = path3.join(UPLOADS_DIR, filename);
537
- fs2.writeFileSync(filePath, buffer);
593
+ fs3.writeFileSync(filePath, buffer);
538
594
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
539
595
  if (this.onImageToSession) {
540
596
  this.onImageToSession(sessionId2, filePath, mimeType, caption);
541
597
  }
542
598
  setTimeout(() => {
543
599
  try {
544
- fs2.unlinkSync(filePath);
600
+ fs3.unlinkSync(filePath);
545
601
  } catch {
546
602
  }
547
603
  }, 5 * 60 * 1e3);
@@ -728,7 +784,7 @@ __export(server_exports, {
728
784
  HubServer: () => HubServer
729
785
  });
730
786
  import http from "http";
731
- import fs3 from "fs";
787
+ import fs4 from "fs";
732
788
  import path4 from "path";
733
789
  import { fileURLToPath } from "url";
734
790
  import { WebSocketServer, WebSocket } from "ws";
@@ -743,6 +799,7 @@ var init_server = __esm({
743
799
  init_notifier();
744
800
  init_telegram();
745
801
  init_config();
802
+ init_crash_guard();
746
803
  __dirname = path4.dirname(fileURLToPath(import.meta.url));
747
804
  HubServer = class {
748
805
  httpServer;
@@ -785,9 +842,12 @@ var init_server = __esm({
785
842
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
786
843
  this.wssChannel = new WebSocketServer({ noServer: true });
787
844
  this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
845
+ this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
788
846
  this.wssDashboard = new WebSocketServer({ noServer: true });
789
847
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
848
+ this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
790
849
  this.httpServer.on("upgrade", (req, socket, head) => {
850
+ socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
791
851
  const url = new URL(req.url, `http://${req.headers.host}`);
792
852
  const pathname = url.pathname;
793
853
  if (this.token && !this.isLocalRequest(req)) {
@@ -814,8 +874,11 @@ var init_server = __esm({
814
874
  this.cleanupUploads();
815
875
  this.startHeartbeat();
816
876
  return new Promise((resolve, reject) => {
817
- this.httpServer.on("error", reject);
877
+ const onStartupError = (err) => reject(err);
878
+ this.httpServer.once("error", onStartupError);
818
879
  this.httpServer.listen(this.port, this.host, () => {
880
+ this.httpServer.removeListener("error", onStartupError);
881
+ this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
819
882
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
820
883
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
821
884
  resolve();
@@ -936,8 +999,8 @@ var init_server = __esm({
936
999
  ];
937
1000
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
938
1001
  for (const candidate of candidates) {
939
- if (fs3.existsSync(candidate)) {
940
- const html = fs3.readFileSync(candidate, "utf-8");
1002
+ if (fs4.existsSync(candidate)) {
1003
+ const html = fs4.readFileSync(candidate, "utf-8");
941
1004
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
942
1005
  res.end(html);
943
1006
  return;
@@ -984,6 +1047,10 @@ var init_server = __esm({
984
1047
  handleChannelConnection(ws, req) {
985
1048
  const isLocal = this.isLocalRequest(req);
986
1049
  logger.info(`Channel server connected (local: ${isLocal})`);
1050
+ ws.on("error", (err) => {
1051
+ logger.warn(`Channel WebSocket error: ${err.message}`);
1052
+ ws.terminate();
1053
+ });
987
1054
  ws.on("pong", () => {
988
1055
  for (const [sessionId2, sock] of this.channelSockets) {
989
1056
  if (sock === ws) {
@@ -1093,6 +1160,10 @@ var init_server = __esm({
1093
1160
  handleDashboardConnection(ws) {
1094
1161
  this.dashboardSockets.add(ws);
1095
1162
  logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);
1163
+ ws.on("error", (err) => {
1164
+ logger.warn(`Dashboard WebSocket error: ${err.message}`);
1165
+ ws.terminate();
1166
+ });
1096
1167
  const sessionsMsg = {
1097
1168
  type: "sessions_list",
1098
1169
  sessions: this.sessions.getAll()
@@ -1149,11 +1220,11 @@ var init_server = __esm({
1149
1220
  logger.warn("Image upload rejected: exceeds 10MB");
1150
1221
  return;
1151
1222
  }
1152
- fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
1223
+ fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
1153
1224
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1154
1225
  const filename = `${randomUUID3()}.${ext}`;
1155
1226
  const filePath = path4.join(UPLOADS_DIR, filename);
1156
- fs3.writeFileSync(filePath, buffer);
1227
+ fs4.writeFileSync(filePath, buffer);
1157
1228
  const forwardMsg = {
1158
1229
  type: "image_to_session",
1159
1230
  sessionId: sessionId2,
@@ -1166,7 +1237,7 @@ var init_server = __esm({
1166
1237
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1167
1238
  setTimeout(() => {
1168
1239
  try {
1169
- fs3.unlinkSync(filePath);
1240
+ fs4.unlinkSync(filePath);
1170
1241
  } catch {
1171
1242
  }
1172
1243
  }, 5 * 60 * 1e3);
@@ -1336,11 +1407,11 @@ var init_server = __esm({
1336
1407
  }
1337
1408
  cleanupUploads() {
1338
1409
  try {
1339
- if (!fs3.existsSync(UPLOADS_DIR)) return;
1340
- const files = fs3.readdirSync(UPLOADS_DIR);
1410
+ if (!fs4.existsSync(UPLOADS_DIR)) return;
1411
+ const files = fs4.readdirSync(UPLOADS_DIR);
1341
1412
  for (const file of files) {
1342
1413
  try {
1343
- fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
1414
+ fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
1344
1415
  } catch {
1345
1416
  }
1346
1417
  }
@@ -1384,6 +1455,8 @@ var init_server = __esm({
1384
1455
  }
1385
1456
  };
1386
1457
  if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
1458
+ installCrashGuard("hub daemon");
1459
+ logStartup("Hub daemon");
1387
1460
  const config2 = loadConfig();
1388
1461
  const hub = new HubServer(config2);
1389
1462
  hub.start().catch((err) => {
@@ -1589,7 +1662,20 @@ var init_server2 = __esm({
1589
1662
  },
1590
1663
  tools: {}
1591
1664
  },
1592
- instructions: 'Messages from the claude-alarm dashboard arrive as <channel source="claude-alarm" sender="...">. Read the message and act on it. Reply with the same detail and depth as you normally would \u2014 do not shorten your response. IMPORTANT: The dashboard user can ONLY see messages sent via the reply tool. Your terminal output is NOT visible on the dashboard. Therefore, when responding to a dashboard message, you MUST call the reply tool with your response so the dashboard user can see it. Use the notify tool to send desktop notifications for key events: task completion, errors, or when user input is needed. Do NOT notify for intermediate steps or simple acknowledgments. Use the status tool to update your session status.'
1665
+ instructions: `Messages from the claude-alarm dashboard arrive as <channel source="claude-alarm" sender="...">. Read the message and act on it.
1666
+
1667
+ REPLYING:
1668
+ - The dashboard user ONLY sees messages sent via the reply tool \u2014 your terminal output is invisible to them. You MUST call reply with your response.
1669
+ - The dashboard renders Markdown (headings, lists, tables, code blocks). Provide a thorough, complete answer; do not apply CLI-style brevity.
1670
+ - Reply in the user's language (match the language they wrote in).
1671
+
1672
+ IMAGES: If a channel message contains "[Image: ...] Read the file to view it: <path>", use the Read tool on that path before responding \u2014 otherwise the image is invisible to you.
1673
+
1674
+ STATUS: Call status("working") before starting a long task, status("waiting_input") when blocked on user input, status("idle") when finished responding.
1675
+
1676
+ NOTIFICATIONS:
1677
+ - Use notify only for key events: task completion, errors, or when user attention is needed. Not for intermediate steps or simple acknowledgments.
1678
+ - Pick level: success=completion, error=failure, warning=attention needed, info=neutral status.`
1593
1679
  }
1594
1680
  );
1595
1681
  config = loadConfig();
@@ -1736,8 +1822,9 @@ var init_server2 = __esm({
1736
1822
  init_config();
1737
1823
  init_constants();
1738
1824
  init_logger();
1825
+ init_crash_guard();
1739
1826
  import { spawn } from "child_process";
1740
- import fs4 from "fs";
1827
+ import fs5 from "fs";
1741
1828
  import path6 from "path";
1742
1829
  import readline from "readline";
1743
1830
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1763,7 +1850,7 @@ Quick start:
1763
1850
  async function checkForUpdates() {
1764
1851
  try {
1765
1852
  const pkgPath = path6.join(__dirname2, "..", "package.json");
1766
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
1853
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1767
1854
  const currentVersion = pkg.version;
1768
1855
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
1769
1856
  signal: AbortSignal.timeout(3e3)
@@ -1786,17 +1873,17 @@ async function hubStart(daemon) {
1786
1873
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1787
1874
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1788
1875
  checkForUpdates();
1789
- if (fs4.existsSync(PID_FILE)) {
1790
- const pid = parseInt(fs4.readFileSync(PID_FILE, "utf-8").trim(), 10);
1876
+ if (fs5.existsSync(PID_FILE)) {
1877
+ const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
1791
1878
  if (isProcessRunning(pid)) {
1792
1879
  console.log(`Hub is already running (PID: ${pid}) on http://${displayHost}:${port}`);
1793
1880
  return;
1794
1881
  }
1795
- fs4.unlinkSync(PID_FILE);
1882
+ fs5.unlinkSync(PID_FILE);
1796
1883
  }
1797
1884
  if (daemon) {
1798
1885
  ensureConfigDir();
1799
- const logFd = fs4.openSync(LOG_FILE, "a");
1886
+ const logFd = fs5.openSync(LOG_FILE, "a");
1800
1887
  const hubScript = path6.join(__dirname2, "hub", "server.js");
1801
1888
  const child = spawn(process.execPath, [hubScript], {
1802
1889
  detached: true,
@@ -1804,7 +1891,7 @@ async function hubStart(daemon) {
1804
1891
  env: { ...process.env }
1805
1892
  });
1806
1893
  if (child.pid) {
1807
- fs4.writeFileSync(PID_FILE, String(child.pid), "utf-8");
1894
+ fs5.writeFileSync(PID_FILE, String(child.pid), "utf-8");
1808
1895
  child.unref();
1809
1896
  console.log(`Hub started as daemon (PID: ${child.pid})`);
1810
1897
  console.log(`Dashboard: http://${displayHost}:${port}`);
@@ -1817,15 +1904,18 @@ async function hubStart(daemon) {
1817
1904
  } else {
1818
1905
  console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
1819
1906
  console.log(`Token: ${config2.hub.token}`);
1907
+ console.log(`Logs: ${LOG_FILE}`);
1908
+ installCrashGuard("hub foreground");
1909
+ logStartup("Hub foreground");
1820
1910
  const { HubServer: HubServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
1821
1911
  const hub = new HubServer2(config2);
1822
1912
  await hub.start();
1823
1913
  ensureConfigDir();
1824
- fs4.writeFileSync(PID_FILE, String(process.pid), "utf-8");
1914
+ fs5.writeFileSync(PID_FILE, String(process.pid), "utf-8");
1825
1915
  const shutdown = async () => {
1826
1916
  console.log("\nShutting down...");
1827
1917
  await hub.stop();
1828
- if (fs4.existsSync(PID_FILE)) fs4.unlinkSync(PID_FILE);
1918
+ if (fs5.existsSync(PID_FILE)) fs5.unlinkSync(PID_FILE);
1829
1919
  process.exit(0);
1830
1920
  };
1831
1921
  process.on("SIGINT", shutdown);
@@ -1833,18 +1923,18 @@ async function hubStart(daemon) {
1833
1923
  }
1834
1924
  }
1835
1925
  function hubStop() {
1836
- if (!fs4.existsSync(PID_FILE)) {
1926
+ if (!fs5.existsSync(PID_FILE)) {
1837
1927
  console.log("Hub is not running (no PID file found)");
1838
1928
  return;
1839
1929
  }
1840
- const pid = parseInt(fs4.readFileSync(PID_FILE, "utf-8").trim(), 10);
1930
+ const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
1841
1931
  try {
1842
1932
  process.kill(pid, "SIGTERM");
1843
1933
  console.log(`Hub stopped (PID: ${pid})`);
1844
1934
  } catch {
1845
1935
  console.log("Hub process not found (may have already stopped)");
1846
1936
  }
1847
- fs4.unlinkSync(PID_FILE);
1937
+ fs5.unlinkSync(PID_FILE);
1848
1938
  }
1849
1939
  async function hubStatus() {
1850
1940
  const config2 = loadConfig();
@@ -1852,8 +1942,8 @@ async function hubStatus() {
1852
1942
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1853
1943
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1854
1944
  let pidInfo = "not running";
1855
- if (fs4.existsSync(PID_FILE)) {
1856
- const pid = parseInt(fs4.readFileSync(PID_FILE, "utf-8").trim(), 10);
1945
+ if (fs5.existsSync(PID_FILE)) {
1946
+ const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
1857
1947
  if (isProcessRunning(pid)) {
1858
1948
  pidInfo = `running (PID: ${pid})`;
1859
1949
  } else {
@@ -1947,9 +2037,9 @@ claude-alarm init for "${projectName}"
1947
2037
  }
1948
2038
  const mcpPath = path6.join(dir, ".mcp.json");
1949
2039
  let mcpConfig = {};
1950
- if (fs4.existsSync(mcpPath)) {
2040
+ if (fs5.existsSync(mcpPath)) {
1951
2041
  try {
1952
- mcpConfig = JSON.parse(fs4.readFileSync(mcpPath, "utf-8"));
2042
+ mcpConfig = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
1953
2043
  } catch {
1954
2044
  mcpConfig = {};
1955
2045
  }
@@ -1960,7 +2050,7 @@ claude-alarm init for "${projectName}"
1960
2050
  args: ["-y", "@delt/claude-alarm", "serve"],
1961
2051
  env
1962
2052
  };
1963
- fs4.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2053
+ fs5.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
1964
2054
  console.log(`
1965
2055
  \u2713 Created ${mcpPath}`);
1966
2056
  if (remote.toLowerCase() !== "y") {