@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.
- package/dist/channel/server.js +10 -0
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +79 -8
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +835 -705
- package/dist/hub/server.js +69 -5
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +72 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +835 -705
package/dist/channel/server.js
CHANGED
|
@@ -37,6 +37,7 @@ var CONFIG_DIR = path.join(os.homedir(), ".claude-alarm");
|
|
|
37
37
|
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
38
38
|
var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
|
|
39
39
|
var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
|
|
40
|
+
var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
40
41
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
41
42
|
var CHANNEL_SERVER_NAME = "claude-alarm";
|
|
42
43
|
var CHANNEL_SERVER_VERSION = "0.1.0";
|
|
@@ -322,6 +323,15 @@ async function main() {
|
|
|
322
323
|
meta: { sender: "dashboard", timestamp: String(Date.now()) }
|
|
323
324
|
}
|
|
324
325
|
});
|
|
326
|
+
} else if (msg.type === "image_to_session" && msg.sessionId === sessionId) {
|
|
327
|
+
logger.info(`Image from dashboard: ${msg.imagePath}`);
|
|
328
|
+
await server.notification({
|
|
329
|
+
method: "notifications/claude/channel",
|
|
330
|
+
params: {
|
|
331
|
+
content: `[Image: ${msg.originalName || "image"}] The user sent an image. Read the file to view it: ${msg.imagePath}`,
|
|
332
|
+
meta: { sender: "dashboard", timestamp: String(Date.now()), imagePath: msg.imagePath, mimeType: msg.mimeType }
|
|
333
|
+
}
|
|
334
|
+
});
|
|
325
335
|
}
|
|
326
336
|
});
|
|
327
337
|
const transport = new StdioServerTransport();
|
|
@@ -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 { 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: { 'claude/channel': {} },\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// --- 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 }\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\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\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 } };\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 { 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: { 'claude/channel': {} },\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// --- 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 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}`,\r\n meta: { sender: 'dashboard', timestamp: String(Date.now()), imagePath: msg.imagePath, mimeType: msg.mimeType },\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\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 } };\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
|
@@ -12,7 +12,7 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/shared/constants.ts
|
|
13
13
|
import path from "path";
|
|
14
14
|
import os from "os";
|
|
15
|
-
var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
|
|
15
|
+
var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
|
|
16
16
|
var init_constants = __esm({
|
|
17
17
|
"src/shared/constants.ts"() {
|
|
18
18
|
"use strict";
|
|
@@ -22,6 +22,7 @@ var init_constants = __esm({
|
|
|
22
22
|
CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
23
23
|
PID_FILE = path.join(CONFIG_DIR, "hub.pid");
|
|
24
24
|
LOG_FILE = path.join(CONFIG_DIR, "hub.log");
|
|
25
|
+
UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
25
26
|
WS_PATH_CHANNEL = "/ws/channel";
|
|
26
27
|
WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
27
28
|
CHANNEL_SERVER_NAME = "claude-alarm";
|
|
@@ -293,6 +294,7 @@ import fs2 from "fs";
|
|
|
293
294
|
import path3 from "path";
|
|
294
295
|
import { fileURLToPath } from "url";
|
|
295
296
|
import { WebSocketServer, WebSocket } from "ws";
|
|
297
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
296
298
|
var __dirname, HubServer;
|
|
297
299
|
var init_server = __esm({
|
|
298
300
|
"src/hub/server.ts"() {
|
|
@@ -311,6 +313,8 @@ var init_server = __esm({
|
|
|
311
313
|
startTime = Date.now();
|
|
312
314
|
// Map sessionId -> channel WebSocket
|
|
313
315
|
channelSockets = /* @__PURE__ */ new Map();
|
|
316
|
+
// Track which channel connections are local
|
|
317
|
+
localChannels = /* @__PURE__ */ new Set();
|
|
314
318
|
// All connected dashboard WebSockets
|
|
315
319
|
dashboardSockets = /* @__PURE__ */ new Set();
|
|
316
320
|
host;
|
|
@@ -332,7 +336,7 @@ var init_server = __esm({
|
|
|
332
336
|
this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
|
|
333
337
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
334
338
|
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
335
|
-
this.wssChannel.on("connection", (ws) => this.handleChannelConnection(ws));
|
|
339
|
+
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
336
340
|
this.wssDashboard = new WebSocketServer({ noServer: true });
|
|
337
341
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
338
342
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
@@ -359,6 +363,7 @@ var init_server = __esm({
|
|
|
359
363
|
});
|
|
360
364
|
}
|
|
361
365
|
async start() {
|
|
366
|
+
this.cleanupUploads();
|
|
362
367
|
return new Promise((resolve, reject) => {
|
|
363
368
|
this.httpServer.on("error", reject);
|
|
364
369
|
this.httpServer.listen(this.port, this.host, () => {
|
|
@@ -492,12 +497,13 @@ var init_server = __esm({
|
|
|
492
497
|
this.jsonResponse(res, 200, { ok: true });
|
|
493
498
|
}
|
|
494
499
|
// --- Channel WebSocket ---
|
|
495
|
-
handleChannelConnection(ws) {
|
|
496
|
-
|
|
500
|
+
handleChannelConnection(ws, req) {
|
|
501
|
+
const isLocal = this.isLocalRequest(req);
|
|
502
|
+
logger.info(`Channel server connected (local: ${isLocal})`);
|
|
497
503
|
ws.on("message", (data) => {
|
|
498
504
|
try {
|
|
499
505
|
const msg = JSON.parse(data.toString());
|
|
500
|
-
this.handleChannelMessage(ws, msg);
|
|
506
|
+
this.handleChannelMessage(ws, isLocal, msg);
|
|
501
507
|
} catch {
|
|
502
508
|
logger.warn("Invalid message from channel");
|
|
503
509
|
}
|
|
@@ -507,6 +513,7 @@ var init_server = __esm({
|
|
|
507
513
|
if (sock === ws) {
|
|
508
514
|
const session = this.sessions.unregister(sessionId2);
|
|
509
515
|
this.channelSockets.delete(sessionId2);
|
|
516
|
+
this.localChannels.delete(sessionId2);
|
|
510
517
|
logger.info(`Channel disconnected: ${sessionId2}`);
|
|
511
518
|
this.broadcastToDashboards({
|
|
512
519
|
type: "session_disconnected",
|
|
@@ -517,13 +524,15 @@ var init_server = __esm({
|
|
|
517
524
|
}
|
|
518
525
|
});
|
|
519
526
|
}
|
|
520
|
-
handleChannelMessage(ws, msg) {
|
|
527
|
+
handleChannelMessage(ws, isLocal, msg) {
|
|
521
528
|
switch (msg.type) {
|
|
522
529
|
case "register": {
|
|
523
530
|
const session = msg.session;
|
|
531
|
+
session.isLocal = isLocal;
|
|
524
532
|
const isReregister = !!this.sessions.get(session.id);
|
|
525
533
|
this.sessions.register(session);
|
|
526
534
|
this.channelSockets.set(session.id, ws);
|
|
535
|
+
if (isLocal) this.localChannels.add(session.id);
|
|
527
536
|
logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);
|
|
528
537
|
this.broadcastToDashboards({
|
|
529
538
|
type: isReregister ? "session_updated" : "session_connected",
|
|
@@ -585,6 +594,8 @@ var init_server = __esm({
|
|
|
585
594
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
586
595
|
channelWs.send(JSON.stringify(msg));
|
|
587
596
|
}
|
|
597
|
+
} else if (msg.type === "image_upload") {
|
|
598
|
+
this.handleImageUpload(msg);
|
|
588
599
|
}
|
|
589
600
|
} catch {
|
|
590
601
|
logger.warn("Invalid message from dashboard");
|
|
@@ -604,6 +615,57 @@ var init_server = __esm({
|
|
|
604
615
|
}
|
|
605
616
|
}
|
|
606
617
|
}
|
|
618
|
+
handleImageUpload(msg) {
|
|
619
|
+
const { sessionId: sessionId2, imageData, mimeType, originalName } = msg;
|
|
620
|
+
if (!this.localChannels.has(sessionId2)) {
|
|
621
|
+
logger.warn(`Image upload rejected: session ${sessionId2} is not local`);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const channelWs = this.channelSockets.get(sessionId2);
|
|
625
|
+
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
|
|
626
|
+
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
627
|
+
if (!allowedTypes.includes(mimeType)) return;
|
|
628
|
+
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
629
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
630
|
+
if (buffer.length > 10 * 1024 * 1024) {
|
|
631
|
+
logger.warn("Image upload rejected: exceeds 10MB");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
635
|
+
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
636
|
+
const filename = `${randomUUID2()}.${ext}`;
|
|
637
|
+
const filePath = path3.join(UPLOADS_DIR, filename);
|
|
638
|
+
fs2.writeFileSync(filePath, buffer);
|
|
639
|
+
const forwardMsg = {
|
|
640
|
+
type: "image_to_session",
|
|
641
|
+
sessionId: sessionId2,
|
|
642
|
+
imagePath: filePath,
|
|
643
|
+
mimeType,
|
|
644
|
+
originalName
|
|
645
|
+
};
|
|
646
|
+
channelWs.send(JSON.stringify(forwardMsg));
|
|
647
|
+
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
648
|
+
setTimeout(() => {
|
|
649
|
+
try {
|
|
650
|
+
fs2.unlinkSync(filePath);
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
}, 5 * 60 * 1e3);
|
|
654
|
+
}
|
|
655
|
+
cleanupUploads() {
|
|
656
|
+
try {
|
|
657
|
+
if (!fs2.existsSync(UPLOADS_DIR)) return;
|
|
658
|
+
const files = fs2.readdirSync(UPLOADS_DIR);
|
|
659
|
+
for (const file of files) {
|
|
660
|
+
try {
|
|
661
|
+
fs2.unlinkSync(path3.join(UPLOADS_DIR, file));
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (files.length > 0) logger.info(`Cleaned up ${files.length} leftover upload(s)`);
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
}
|
|
607
669
|
getSessionLabel(session) {
|
|
608
670
|
if (!session) return "unknown";
|
|
609
671
|
return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
@@ -769,7 +831,7 @@ import {
|
|
|
769
831
|
CallToolRequestSchema,
|
|
770
832
|
ListToolsRequestSchema
|
|
771
833
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
772
|
-
import { randomUUID as
|
|
834
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
773
835
|
import path4 from "path";
|
|
774
836
|
async function main() {
|
|
775
837
|
logger.info(`Starting MCP channel server (session: ${sessionId})`);
|
|
@@ -784,6 +846,15 @@ async function main() {
|
|
|
784
846
|
meta: { sender: "dashboard", timestamp: String(Date.now()) }
|
|
785
847
|
}
|
|
786
848
|
});
|
|
849
|
+
} else if (msg.type === "image_to_session" && msg.sessionId === sessionId) {
|
|
850
|
+
logger.info(`Image from dashboard: ${msg.imagePath}`);
|
|
851
|
+
await server.notification({
|
|
852
|
+
method: "notifications/claude/channel",
|
|
853
|
+
params: {
|
|
854
|
+
content: `[Image: ${msg.originalName || "image"}] The user sent an image. Read the file to view it: ${msg.imagePath}`,
|
|
855
|
+
meta: { sender: "dashboard", timestamp: String(Date.now()), imagePath: msg.imagePath, mimeType: msg.mimeType }
|
|
856
|
+
}
|
|
857
|
+
});
|
|
787
858
|
}
|
|
788
859
|
});
|
|
789
860
|
const transport = new StdioServerTransport();
|
|
@@ -798,7 +869,7 @@ var init_server2 = __esm({
|
|
|
798
869
|
init_constants();
|
|
799
870
|
init_config();
|
|
800
871
|
init_hub_client();
|
|
801
|
-
sessionId =
|
|
872
|
+
sessionId = randomUUID3();
|
|
802
873
|
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path4.basename(process.cwd());
|
|
803
874
|
server = new Server(
|
|
804
875
|
{
|