@delt/claude-alarm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # claude-alarm
2
+
3
+ Monitor and interact with multiple Claude Code sessions from a web dashboard. Get desktop notifications when tasks complete, send messages to Claude, and track session status — all through MCP Channels.
4
+
5
+ ```
6
+ [Dashboard] ──message──> [Hub Server] ──WebSocket──> [Channel Server] ──> Claude Code
7
+ <──
8
+ Claude Code ──reply/notify──> [Channel Server] ──> [Hub Server] ──> [Dashboard]
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Web Dashboard** — Monitor all Claude Code sessions in one place
14
+ - **Two-way Messaging** — Send messages to Claude and receive replies
15
+ - **Desktop Notifications** — Get Windows/macOS/Linux toast notifications
16
+ - **Session Status** — See which sessions are idle, working, or waiting for input
17
+ - **Token Auth** — Secure hub access with auto-generated tokens
18
+ - **Multi-session** — Connect multiple Claude Code instances simultaneously
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Install
23
+
24
+ ```bash
25
+ npm install -g claude-alarm
26
+ ```
27
+
28
+ ### 2. Start the Hub
29
+
30
+ ```bash
31
+ claude-alarm hub start -d
32
+ ```
33
+
34
+ This starts the hub server in the background and prints your auth token.
35
+
36
+ ### 3. Setup a Project
37
+
38
+ In your project directory:
39
+
40
+ ```bash
41
+ claude-alarm setup
42
+ ```
43
+
44
+ This creates `.mcp.json` with the claude-alarm channel server config.
45
+
46
+ ### 4. Run Claude Code
47
+
48
+ ```bash
49
+ claude --dangerously-load-development-channels server:claude-alarm
50
+ ```
51
+
52
+ ### 5. Open Dashboard
53
+
54
+ Open `http://127.0.0.1:7890` in your browser.
55
+
56
+ ## CLI Commands
57
+
58
+ ```
59
+ claude-alarm hub start [-d] Start the hub server (-d for daemon mode)
60
+ claude-alarm hub stop Stop the hub daemon
61
+ claude-alarm hub status Show hub status
62
+ claude-alarm setup [dir] Add claude-alarm to .mcp.json
63
+ claude-alarm test Send a test notification
64
+ claude-alarm token Show current auth token
65
+ ```
66
+
67
+ ## How It Works
68
+
69
+ claude-alarm uses [MCP Channels](https://modelcontextprotocol.io) to create a communication bridge between Claude Code and a web dashboard.
70
+
71
+ - **Hub Server** — Central server that manages sessions, serves the dashboard, and routes messages
72
+ - **Channel Server** — MCP server that runs inside Claude Code, providing tools and forwarding messages
73
+ - **Dashboard** — Web UI for monitoring sessions and sending messages
74
+
75
+ ### Tools Available to Claude
76
+
77
+ | Tool | Description |
78
+ |------|-------------|
79
+ | `notify` | Send a desktop notification (title, message, level) |
80
+ | `reply` | Send a message to the dashboard |
81
+ | `status` | Update session status (idle, working, waiting_input) |
82
+
83
+ ## Configuration
84
+
85
+ Config is stored at `~/.claude-alarm/config.json`:
86
+
87
+ ```json
88
+ {
89
+ "hub": {
90
+ "host": "127.0.0.1",
91
+ "port": 7890,
92
+ "token": "auto-generated-uuid"
93
+ },
94
+ "notifications": {
95
+ "desktop": true,
96
+ "sound": true
97
+ },
98
+ "webhooks": []
99
+ }
100
+ ```
101
+
102
+ ### Custom Session Names
103
+
104
+ The `.mcp.json` created by `claude-alarm setup` automatically uses the project directory name as the session name. You can customize it:
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "claude-alarm": {
110
+ "command": "npx",
111
+ "args": ["-y", "claude-alarm"],
112
+ "env": {
113
+ "CLAUDE_ALARM_SESSION_NAME": "my-project"
114
+ }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### Webhooks
121
+
122
+ Send notifications to external services:
123
+
124
+ ```json
125
+ {
126
+ "webhooks": [
127
+ {
128
+ "url": "https://hooks.slack.com/services/...",
129
+ "headers": { "Content-Type": "application/json" }
130
+ }
131
+ ]
132
+ }
133
+ ```
134
+
135
+ ## Remote Access
136
+
137
+ To access the hub from another machine:
138
+
139
+ 1. Set host to `0.0.0.0` in `~/.claude-alarm/config.json`
140
+ 2. Open port 7890 in your firewall
141
+ 3. On the remote machine, set the hub address in `.mcp.json`:
142
+
143
+ ```json
144
+ {
145
+ "mcpServers": {
146
+ "claude-alarm": {
147
+ "command": "npx",
148
+ "args": ["-y", "claude-alarm"],
149
+ "env": {
150
+ "CLAUDE_ALARM_HUB_HOST": "your-server-ip",
151
+ "CLAUDE_ALARM_HUB_PORT": "7890",
152
+ "CLAUDE_ALARM_HUB_TOKEN": "your-token"
153
+ }
154
+ }
155
+ }
156
+ }
157
+ ```
158
+
159
+ ## Requirements
160
+
161
+ - Node.js >= 18
162
+ - Claude Code with MCP Channels support
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/channel/server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { randomUUID as randomUUID2 } from "crypto";
11
+
12
+ // src/shared/logger.ts
13
+ var logger = {
14
+ info(msg, ...args) {
15
+ console.error(`[claude-alarm] ${msg}`, ...args);
16
+ },
17
+ warn(msg, ...args) {
18
+ console.error(`[claude-alarm WARN] ${msg}`, ...args);
19
+ },
20
+ error(msg, ...args) {
21
+ console.error(`[claude-alarm ERROR] ${msg}`, ...args);
22
+ },
23
+ debug(msg, ...args) {
24
+ if (process.env.CLAUDE_ALARM_DEBUG) {
25
+ console.error(`[claude-alarm DEBUG] ${msg}`, ...args);
26
+ }
27
+ }
28
+ };
29
+
30
+ // src/shared/constants.ts
31
+ import path from "path";
32
+ import os from "os";
33
+ var DEFAULT_HUB_HOST = "127.0.0.1";
34
+ var DEFAULT_HUB_PORT = 7890;
35
+ var CONFIG_DIR = path.join(os.homedir(), ".claude-alarm");
36
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
37
+ var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
38
+ var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
39
+ var WS_PATH_CHANNEL = "/ws/channel";
40
+ var CHANNEL_SERVER_NAME = "claude-alarm";
41
+ var CHANNEL_SERVER_VERSION = "0.1.0";
42
+
43
+ // src/shared/config.ts
44
+ import fs from "fs";
45
+ import path2 from "path";
46
+ import { randomUUID } from "crypto";
47
+ var DEFAULT_CONFIG = {
48
+ hub: {
49
+ host: DEFAULT_HUB_HOST,
50
+ port: DEFAULT_HUB_PORT
51
+ },
52
+ notifications: {
53
+ desktop: true,
54
+ sound: true
55
+ },
56
+ webhooks: []
57
+ };
58
+ function ensureConfigDir() {
59
+ if (!fs.existsSync(CONFIG_DIR)) {
60
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
61
+ }
62
+ }
63
+ function loadConfig() {
64
+ ensureConfigDir();
65
+ let config2;
66
+ if (!fs.existsSync(CONFIG_FILE)) {
67
+ config2 = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
68
+ } else {
69
+ try {
70
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
71
+ const parsed = JSON.parse(raw);
72
+ config2 = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };
73
+ } catch {
74
+ config2 = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
75
+ }
76
+ }
77
+ if (!config2.hub.token) {
78
+ config2.hub.token = randomUUID();
79
+ saveConfig(config2);
80
+ }
81
+ return config2;
82
+ }
83
+ function saveConfig(config2) {
84
+ ensureConfigDir();
85
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), { encoding: "utf-8", mode: 384 });
86
+ }
87
+
88
+ // src/channel/hub-client.ts
89
+ import WebSocket from "ws";
90
+ var HubClient = class {
91
+ constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token) {
92
+ this.sessionId = sessionId2;
93
+ this.sessionName = sessionName2;
94
+ this.hubHost = hubHost2;
95
+ this.hubPort = hubPort2;
96
+ this.token = token;
97
+ }
98
+ ws = null;
99
+ reconnectTimer = null;
100
+ messageHandlers = [];
101
+ queue = [];
102
+ connected = false;
103
+ connect() {
104
+ const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
105
+ const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
106
+ logger.debug(`Connecting to hub at ${url}`);
107
+ try {
108
+ this.ws = new WebSocket(url);
109
+ this.ws.on("open", () => {
110
+ logger.info("Connected to hub");
111
+ this.connected = true;
112
+ const registration = {
113
+ type: "register",
114
+ session: {
115
+ id: this.sessionId,
116
+ name: this.sessionName,
117
+ status: "idle",
118
+ connectedAt: Date.now(),
119
+ lastActivity: Date.now(),
120
+ cwd: process.cwd()
121
+ }
122
+ };
123
+ this.ws.send(JSON.stringify(registration));
124
+ for (const msg of this.queue) {
125
+ this.ws.send(JSON.stringify(msg));
126
+ }
127
+ this.queue = [];
128
+ });
129
+ this.ws.on("message", (data) => {
130
+ try {
131
+ const msg = JSON.parse(data.toString());
132
+ for (const handler of this.messageHandlers) {
133
+ handler(msg);
134
+ }
135
+ } catch (err) {
136
+ logger.warn("Failed to parse hub message:", err);
137
+ }
138
+ });
139
+ this.ws.on("close", () => {
140
+ logger.info("Disconnected from hub");
141
+ this.connected = false;
142
+ this.scheduleReconnect();
143
+ });
144
+ this.ws.on("error", (err) => {
145
+ logger.debug(`Hub connection error: ${err.message}`);
146
+ this.connected = false;
147
+ });
148
+ } catch {
149
+ logger.debug("Failed to connect to hub, will retry");
150
+ this.scheduleReconnect();
151
+ }
152
+ }
153
+ send(msg) {
154
+ if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
155
+ this.ws.send(JSON.stringify(msg));
156
+ } else {
157
+ if (this.queue.length < 100) {
158
+ this.queue.push(msg);
159
+ }
160
+ logger.debug("Hub not connected, message queued");
161
+ }
162
+ }
163
+ onMessage(handler) {
164
+ this.messageHandlers.push(handler);
165
+ }
166
+ disconnect() {
167
+ if (this.reconnectTimer) {
168
+ clearTimeout(this.reconnectTimer);
169
+ this.reconnectTimer = null;
170
+ }
171
+ if (this.ws) {
172
+ this.ws.close();
173
+ this.ws = null;
174
+ }
175
+ this.connected = false;
176
+ }
177
+ scheduleReconnect() {
178
+ if (this.reconnectTimer) return;
179
+ this.reconnectTimer = setTimeout(() => {
180
+ this.reconnectTimer = null;
181
+ this.connect();
182
+ }, 5e3);
183
+ }
184
+ };
185
+
186
+ // src/channel/server.ts
187
+ var sessionId = randomUUID2();
188
+ var sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? `session-${sessionId.slice(0, 8)}`;
189
+ var server = new Server(
190
+ {
191
+ name: CHANNEL_SERVER_NAME,
192
+ version: CHANNEL_SERVER_VERSION
193
+ },
194
+ {
195
+ capabilities: {
196
+ experimental: { "claude/channel": {} },
197
+ tools: {}
198
+ },
199
+ instructions: 'Messages from the claude-alarm dashboard arrive as <channel source="claude-alarm" sender="...">. Read the message and act on it. To reply, call the reply tool with the message content. Use the notify tool to send desktop notifications. Use the status tool to update your session status.'
200
+ }
201
+ );
202
+ var config = loadConfig();
203
+ var hubHost = process.env.CLAUDE_ALARM_HUB_HOST ?? config.hub.host;
204
+ var hubPort = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;
205
+ var hubToken = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;
206
+ var hubClient = new HubClient(
207
+ sessionId,
208
+ sessionName,
209
+ hubHost,
210
+ hubPort,
211
+ hubToken
212
+ );
213
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
214
+ tools: [
215
+ {
216
+ name: "notify",
217
+ description: "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.",
218
+ inputSchema: {
219
+ type: "object",
220
+ properties: {
221
+ title: { type: "string", description: "Notification title (short)" },
222
+ message: { type: "string", description: "Notification body text" },
223
+ level: {
224
+ type: "string",
225
+ enum: ["info", "warning", "error", "success"],
226
+ description: "Notification level (default: info)"
227
+ }
228
+ },
229
+ required: ["title", "message"]
230
+ }
231
+ },
232
+ {
233
+ name: "reply",
234
+ description: "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.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ content: { type: "string", description: "Message content to display on the dashboard" }
239
+ },
240
+ required: ["content"]
241
+ }
242
+ },
243
+ {
244
+ name: "status",
245
+ description: '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.',
246
+ inputSchema: {
247
+ type: "object",
248
+ properties: {
249
+ status: {
250
+ type: "string",
251
+ enum: ["idle", "working", "waiting_input"],
252
+ description: "Current session status"
253
+ }
254
+ },
255
+ required: ["status"]
256
+ }
257
+ }
258
+ ]
259
+ }));
260
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
261
+ const { name, arguments: args } = request.params;
262
+ switch (name) {
263
+ case "notify": {
264
+ const title = args?.title;
265
+ const message = args?.message;
266
+ const level = args?.level ?? "info";
267
+ logger.info(`Notify [${level}]: ${title} - ${message}`);
268
+ hubClient.send({
269
+ type: "notify",
270
+ sessionId,
271
+ title,
272
+ message,
273
+ level
274
+ });
275
+ return {
276
+ content: [{ type: "text", text: `Notification sent: "${title}"` }]
277
+ };
278
+ }
279
+ case "reply": {
280
+ const content = args?.content;
281
+ logger.info(`Reply: ${content.slice(0, 100)}...`);
282
+ hubClient.send({
283
+ type: "reply",
284
+ sessionId,
285
+ content
286
+ });
287
+ return {
288
+ content: [{ type: "text", text: "Message sent to dashboard." }]
289
+ };
290
+ }
291
+ case "status": {
292
+ const status = args?.status;
293
+ logger.info(`Status update: ${status}`);
294
+ hubClient.send({
295
+ type: "status",
296
+ sessionId,
297
+ status
298
+ });
299
+ return {
300
+ content: [{ type: "text", text: `Status updated to "${status}".` }]
301
+ };
302
+ }
303
+ default:
304
+ return {
305
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
306
+ isError: true
307
+ };
308
+ }
309
+ });
310
+ async function main() {
311
+ logger.info(`Starting MCP channel server (session: ${sessionId})`);
312
+ hubClient.connect();
313
+ hubClient.onMessage(async (msg) => {
314
+ if (msg.type === "message_to_session" && msg.sessionId === sessionId) {
315
+ logger.info(`Message from dashboard: ${msg.content}`);
316
+ await server.notification({
317
+ method: "notifications/claude/channel",
318
+ params: {
319
+ content: msg.content,
320
+ meta: { sender: "dashboard", timestamp: String(Date.now()) }
321
+ }
322
+ });
323
+ }
324
+ });
325
+ const transport = new StdioServerTransport();
326
+ await server.connect(transport);
327
+ logger.info("MCP channel server running on stdio");
328
+ }
329
+ main().catch((err) => {
330
+ logger.error("Fatal error:", err);
331
+ process.exit(1);
332
+ });
333
+ //# sourceMappingURL=server.js.map
@@ -0,0 +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';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { randomUUID } from 'node:crypto';\nimport { logger } from '../shared/logger.js';\nimport { CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION } from '../shared/constants.js';\nimport { loadConfig } from '../shared/config.js';\nimport { HubClient } from './hub-client.js';\nimport type { SessionStatus, NotifyLevel } from '../shared/types.js';\n\nconst sessionId = randomUUID();\nconst sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? `session-${sessionId.slice(0, 8)}`;\n\nconst server = new Server(\n {\n name: CHANNEL_SERVER_NAME,\n version: CHANNEL_SERVER_VERSION,\n },\n {\n capabilities: {\n experimental: { 'claude/channel': {} },\n tools: {},\n },\n instructions:\n 'Messages from the claude-alarm dashboard arrive as <channel source=\"claude-alarm\" sender=\"...\">. ' +\n 'Read the message and act on it. To reply, call the reply tool with the message content. ' +\n 'Use the notify tool to send desktop notifications. Use the status tool to update your session status.',\n },\n);\n\n// Load config for hub connection (env vars take priority)\nconst config = loadConfig();\nconst hubHost = process.env.CLAUDE_ALARM_HUB_HOST ?? config.hub.host;\nconst hubPort = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;\nconst hubToken = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;\n\n// Hub client for forwarding to central hub\nconst hubClient = new HubClient(\n sessionId,\n sessionName,\n hubHost,\n hubPort,\n hubToken,\n);\n\n// --- Tools ---\n\nserver.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [\n {\n name: 'notify',\n description:\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.',\n inputSchema: {\n type: 'object' as const,\n properties: {\n title: { type: 'string', description: 'Notification title (short)' },\n message: { type: 'string', description: 'Notification body text' },\n level: {\n type: 'string',\n enum: ['info', 'warning', 'error', 'success'],\n description: 'Notification level (default: info)',\n },\n },\n required: ['title', 'message'],\n },\n },\n {\n name: 'reply',\n description:\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.',\n inputSchema: {\n type: 'object' as const,\n properties: {\n content: { type: 'string', description: 'Message content to display on the dashboard' },\n },\n required: ['content'],\n },\n },\n {\n name: 'status',\n description:\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.',\n inputSchema: {\n type: 'object' as const,\n properties: {\n status: {\n type: 'string',\n enum: ['idle', 'working', 'waiting_input'],\n description: 'Current session status',\n },\n },\n required: ['status'],\n },\n },\n ],\n}));\n\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n switch (name) {\n case 'notify': {\n const title = args?.title as string;\n const message = args?.message as string;\n const level = (args?.level as NotifyLevel) ?? 'info';\n logger.info(`Notify [${level}]: ${title} - ${message}`);\n hubClient.send({\n type: 'notify',\n sessionId,\n title,\n message,\n level,\n });\n return {\n content: [{ type: 'text', text: `Notification sent: \"${title}\"` }],\n };\n }\n\n case 'reply': {\n const content = args?.content as string;\n logger.info(`Reply: ${content.slice(0, 100)}...`);\n hubClient.send({\n type: 'reply',\n sessionId,\n content,\n });\n return {\n content: [{ type: 'text', text: 'Message sent to dashboard.' }],\n };\n }\n\n case 'status': {\n const status = args?.status as SessionStatus;\n logger.info(`Status update: ${status}`);\n hubClient.send({\n type: 'status',\n sessionId,\n status,\n });\n return {\n content: [{ type: 'text', text: `Status updated to \"${status}\".` }],\n };\n }\n\n default:\n return {\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n});\n\n// --- Startup ---\n\nasync function main() {\n logger.info(`Starting MCP channel server (session: ${sessionId})`);\n\n // Connect to hub (non-blocking, will retry)\n hubClient.connect();\n\n // Listen for messages from hub and forward to Claude via channel notification\n hubClient.onMessage(async (msg) => {\n if (msg.type === 'message_to_session' && msg.sessionId === sessionId) {\n logger.info(`Message from dashboard: ${msg.content}`);\n await server.notification({\n method: 'notifications/claude/channel',\n params: {\n content: msg.content,\n meta: { sender: 'dashboard', timestamp: String(Date.now()) },\n },\n });\n }\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n logger.info('MCP channel server running on stdio');\n}\n\nmain().catch((err) => {\n logger.error('Fatal error:', err);\n process.exit(1);\n});\n","/**\n * Logger that writes to stderr only.\n * CRITICAL: In MCP channel servers, stdout is used for the stdio protocol.\n * Any console.log() would corrupt the protocol. Always use this logger.\n */\nexport const logger = {\n info(msg: string, ...args: unknown[]) {\n console.error(`[claude-alarm] ${msg}`, ...args);\n },\n warn(msg: string, ...args: unknown[]) {\n console.error(`[claude-alarm WARN] ${msg}`, ...args);\n },\n error(msg: string, ...args: unknown[]) {\n console.error(`[claude-alarm ERROR] ${msg}`, ...args);\n },\n debug(msg: string, ...args: unknown[]) {\n if (process.env.CLAUDE_ALARM_DEBUG) {\n console.error(`[claude-alarm DEBUG] ${msg}`, ...args);\n }\n },\n};\n","import path from 'node:path';\nimport os from 'node:os';\n\nexport const DEFAULT_HUB_HOST = '127.0.0.1';\nexport const DEFAULT_HUB_PORT = 7890;\n\nexport const CONFIG_DIR = path.join(os.homedir(), '.claude-alarm');\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\nexport const PID_FILE = path.join(CONFIG_DIR, 'hub.pid');\nexport const LOG_FILE = path.join(CONFIG_DIR, 'hub.log');\n\nexport const WS_PATH_CHANNEL = '/ws/channel';\nexport const WS_PATH_DASHBOARD = '/ws/dashboard';\n\nexport const CHANNEL_SERVER_NAME = 'claude-alarm';\nexport const CHANNEL_SERVER_VERSION = '0.1.0';\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport { CONFIG_DIR, CONFIG_FILE, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT } from './constants.js';\nimport type { AppConfig } from './types.js';\n\nconst DEFAULT_CONFIG: AppConfig = {\n hub: {\n host: DEFAULT_HUB_HOST,\n port: DEFAULT_HUB_PORT,\n },\n notifications: {\n desktop: true,\n sound: true,\n },\n webhooks: [],\n};\n\nexport function ensureConfigDir(): void {\n if (!fs.existsSync(CONFIG_DIR)) {\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\n }\n}\n\nexport function loadConfig(): AppConfig {\n ensureConfigDir();\n let config: AppConfig;\n if (!fs.existsSync(CONFIG_FILE)) {\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\n } else {\n try {\n const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');\n const parsed = JSON.parse(raw);\n config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };\n } catch {\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\n }\n }\n\n // Auto-generate token if missing\n if (!config.hub.token) {\n config.hub.token = randomUUID();\n saveConfig(config);\n }\n\n return config;\n}\n\n/** Get the current token, generating one if needed */\nexport function getOrCreateToken(): string {\n const config = loadConfig();\n return config.hub.token!;\n}\n\nexport function saveConfig(config: AppConfig): void {\n ensureConfigDir();\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });\n}\n\n/**\n * Add claude-alarm as an MCP channel server to .mcp.json\n */\nexport function setupMcpConfig(targetDir?: string): string {\n const dir = targetDir ?? process.cwd();\n const mcpPath = path.join(dir, '.mcp.json');\n\n let mcpConfig: Record<string, any> = {};\n if (fs.existsSync(mcpPath)) {\n try {\n mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));\n } catch {\n mcpConfig = {};\n }\n }\n\n if (!mcpConfig.mcpServers) {\n mcpConfig.mcpServers = {};\n }\n\n mcpConfig.mcpServers['claude-alarm'] = {\n command: 'npx',\n args: ['-y', '@delt/claude-alarm'],\n env: {\n CLAUDE_ALARM_SESSION_NAME: path.basename(dir),\n },\n };\n\n fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');\n return mcpPath;\n}\n","import WebSocket from 'ws';\nimport { logger } from '../shared/logger.js';\nimport { DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, WS_PATH_CHANNEL } from '../shared/constants.js';\nimport type { ChannelMessage, SessionInfo } from '../shared/types.js';\n\nexport class HubClient {\n private ws: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private messageHandlers: Array<(msg: ChannelMessage) => void> = [];\n private queue: ChannelMessage[] = [];\n private connected = false;\n\n constructor(\n private sessionId: string,\n private sessionName: string,\n private hubHost = DEFAULT_HUB_HOST,\n private hubPort = DEFAULT_HUB_PORT,\n private token?: string,\n ) {}\n\n connect(): void {\n const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : '';\n const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;\n logger.debug(`Connecting to hub at ${url}`);\n\n try {\n this.ws = new WebSocket(url);\n\n this.ws.on('open', () => {\n logger.info('Connected to hub');\n this.connected = true;\n\n // Register this session\n const registration: ChannelMessage = {\n type: 'register',\n session: {\n id: this.sessionId,\n name: this.sessionName,\n status: 'idle',\n connectedAt: Date.now(),\n lastActivity: Date.now(),\n cwd: process.cwd(),\n },\n };\n this.ws!.send(JSON.stringify(registration));\n\n // Flush queued messages\n for (const msg of this.queue) {\n this.ws!.send(JSON.stringify(msg));\n }\n this.queue = [];\n });\n\n this.ws.on('message', (data) => {\n try {\n const msg = JSON.parse(data.toString()) as ChannelMessage;\n for (const handler of this.messageHandlers) {\n handler(msg);\n }\n } catch (err) {\n logger.warn('Failed to parse hub message:', err);\n }\n });\n\n this.ws.on('close', () => {\n logger.info('Disconnected from hub');\n this.connected = false;\n this.scheduleReconnect();\n });\n\n this.ws.on('error', (err) => {\n logger.debug(`Hub connection error: ${err.message}`);\n this.connected = false;\n });\n } catch {\n logger.debug('Failed to connect to hub, will retry');\n this.scheduleReconnect();\n }\n }\n\n send(msg: ChannelMessage): void {\n if (this.connected && this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n } else {\n if (this.queue.length < 100) {\n this.queue.push(msg);\n }\n logger.debug('Hub not connected, message queued');\n }\n }\n\n onMessage(handler: (msg: ChannelMessage) => void): void {\n this.messageHandlers.push(handler);\n }\n\n disconnect(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n this.connected = false;\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer) return;\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.connect();\n }, 5000);\n }\n}\n"],"mappings":"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","names":["randomUUID","path","config","config","sessionId","sessionName","hubHost","hubPort","randomUUID"]}