@delt/claude-alarm 0.8.2 → 0.9.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/cli.js CHANGED
@@ -190,11 +190,24 @@ var init_crash_guard = __esm({
190
190
  }
191
191
  });
192
192
 
193
+ // src/shared/peer-name.ts
194
+ function isValidPeerName(name) {
195
+ return typeof name === "string" && name.trim() !== "" && SAFE_PEER_NAME.test(name);
196
+ }
197
+ var SAFE_PEER_NAME;
198
+ var init_peer_name = __esm({
199
+ "src/shared/peer-name.ts"() {
200
+ "use strict";
201
+ SAFE_PEER_NAME = /^[^\u0000-\u001f\u007f"\\[\]]{1,128}$/;
202
+ }
203
+ });
204
+
193
205
  // src/hub/session-manager.ts
194
206
  var SessionManager;
195
207
  var init_session_manager = __esm({
196
208
  "src/hub/session-manager.ts"() {
197
209
  "use strict";
210
+ init_peer_name();
198
211
  SessionManager = class {
199
212
  sessions = /* @__PURE__ */ new Map();
200
213
  register(session) {
@@ -217,7 +230,7 @@ var init_session_manager = __esm({
217
230
  } else {
218
231
  session.displayName = baseName;
219
232
  }
220
- this.sessions.set(session.id, { ...session });
233
+ this.sessions.set(session.id, { ...session, peerName: isValidPeerName(session.peerName) ? session.peerName : void 0 });
221
234
  }
222
235
  unregister(sessionId2) {
223
236
  const session = this.sessions.get(sessionId2);
@@ -232,6 +245,13 @@ var init_session_manager = __esm({
232
245
  }
233
246
  return session;
234
247
  }
248
+ setPeerName(sessionId2, peerName2) {
249
+ const session = this.sessions.get(sessionId2);
250
+ if (session) {
251
+ session.peerName = isValidPeerName(peerName2) ? peerName2 : void 0;
252
+ }
253
+ return session;
254
+ }
235
255
  updateActivity(sessionId2) {
236
256
  const session = this.sessions.get(sessionId2);
237
257
  if (session) {
@@ -676,6 +696,7 @@ ${previewHtml}${truncNote}`;
676
696
  }
677
697
  async handleCallbackQuery(query) {
678
698
  if (!query.data) return;
699
+ if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
679
700
  if (query.data.startsWith("sess:")) {
680
701
  await this.handleSessionSelectCallback(query);
681
702
  return;
@@ -778,8 +799,8 @@ ${label}`);
778
799
  css: "css",
779
800
  scss: "scss"
780
801
  };
781
- langOf(path7) {
782
- const m = /\.([a-z0-9]+)$/i.exec(path7 || "");
802
+ langOf(path8) {
803
+ const m = /\.([a-z0-9]+)$/i.exec(path8 || "");
783
804
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
784
805
  }
785
806
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -824,6 +845,69 @@ ${bodyLines.join("\n")}`;
824
845
  }
825
846
  });
826
847
 
848
+ // src/hub/auth.ts
849
+ import crypto from "crypto";
850
+ function safeEqual(a, b) {
851
+ if (typeof a !== "string") return false;
852
+ const x = Buffer.from(a);
853
+ const y = Buffer.from(b);
854
+ return x.length === y.length && crypto.timingSafeEqual(x, y);
855
+ }
856
+ function dashboardCookieValue(token) {
857
+ return crypto.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
858
+ }
859
+ function parseCookies(header) {
860
+ const out = {};
861
+ for (const part of (header ?? "").split(";")) {
862
+ const i = part.indexOf("=");
863
+ if (i <= 0) continue;
864
+ const raw = part.slice(i + 1).trim();
865
+ let value = raw;
866
+ try {
867
+ value = decodeURIComponent(raw);
868
+ } catch {
869
+ }
870
+ out[part.slice(0, i).trim()] = value;
871
+ }
872
+ return out;
873
+ }
874
+ function sessionCookieHeader(token, secure) {
875
+ return `${SESSION_COOKIE}=${dashboardCookieValue(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${COOKIE_MAX_AGE}${secure ? "; Secure" : ""}`;
876
+ }
877
+ function isAuthorized(req, token, allowQueryToken) {
878
+ const auth = req.headers.authorization;
879
+ if (auth?.startsWith("Bearer ") && safeEqual(auth.slice(7), token)) return true;
880
+ if (safeEqual(parseCookies(req.headers.cookie)[SESSION_COOKIE], dashboardCookieValue(token))) return true;
881
+ if (allowQueryToken) {
882
+ const q = new URL(req.url ?? "/", "http://hub").searchParams.get("token");
883
+ if (safeEqual(q, token)) return true;
884
+ }
885
+ return false;
886
+ }
887
+ function isCrossOrigin(req) {
888
+ const origin = req.headers.origin;
889
+ if (!origin) return false;
890
+ try {
891
+ return new URL(origin).host !== req.headers.host;
892
+ } catch {
893
+ return true;
894
+ }
895
+ }
896
+ function isJsonRequest(req) {
897
+ return String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json");
898
+ }
899
+ function isSecureRequest(req) {
900
+ return req.headers["x-forwarded-proto"] === "https";
901
+ }
902
+ var SESSION_COOKIE, COOKIE_MAX_AGE;
903
+ var init_auth = __esm({
904
+ "src/hub/auth.ts"() {
905
+ "use strict";
906
+ SESSION_COOKIE = "ca_session";
907
+ COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
908
+ }
909
+ });
910
+
827
911
  // src/hub/server.ts
828
912
  var server_exports = {};
829
913
  __export(server_exports, {
@@ -835,7 +919,7 @@ import path4 from "path";
835
919
  import { fileURLToPath } from "url";
836
920
  import { WebSocketServer, WebSocket } from "ws";
837
921
  import { randomUUID as randomUUID3 } from "crypto";
838
- var __dirname, HubServer;
922
+ var __dirname, MAX_WS_PAYLOAD, HubServer;
839
923
  var init_server = __esm({
840
924
  "src/hub/server.ts"() {
841
925
  "use strict";
@@ -846,7 +930,9 @@ var init_server = __esm({
846
930
  init_telegram();
847
931
  init_config();
848
932
  init_crash_guard();
933
+ init_auth();
849
934
  __dirname = path4.dirname(fileURLToPath(import.meta.url));
935
+ MAX_WS_PAYLOAD = 16 * 1024 * 1024;
850
936
  HubServer = class {
851
937
  httpServer;
852
938
  wssChannel;
@@ -864,6 +950,7 @@ var init_server = __esm({
864
950
  heartbeatInterval;
865
951
  channelAlive = /* @__PURE__ */ new Map();
866
952
  // sessionId -> alive flag
953
+ socketOwners = /* @__PURE__ */ new WeakMap();
867
954
  host;
868
955
  port;
869
956
  token;
@@ -886,22 +973,23 @@ var init_server = __esm({
886
973
  this.initTelegram(fullConfig.telegram);
887
974
  }
888
975
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
889
- this.wssChannel = new WebSocketServer({ noServer: true });
976
+ this.wssChannel = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
890
977
  this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
891
978
  this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
892
- this.wssDashboard = new WebSocketServer({ noServer: true });
979
+ this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
893
980
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
894
981
  this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
895
982
  this.httpServer.on("upgrade", (req, socket, head) => {
896
983
  socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
897
- const url = new URL(req.url, `http://${req.headers.host}`);
984
+ const url = new URL(req.url ?? "/", "http://hub");
898
985
  const pathname = url.pathname;
899
- if (this.token && !this.isLocalRequest(req)) {
900
- const wsToken = url.searchParams.get("token");
901
- if (wsToken !== this.token) {
902
- socket.destroy();
903
- return;
904
- }
986
+ if (isCrossOrigin(req)) {
987
+ this.rejectUpgrade(socket, 403, "Forbidden", pathname, req);
988
+ return;
989
+ }
990
+ if (!this.authorized(req, true)) {
991
+ this.rejectUpgrade(socket, 401, "Unauthorized", pathname, req);
992
+ return;
905
993
  }
906
994
  if (pathname === WS_PATH_CHANNEL) {
907
995
  this.wssChannel.handleUpgrade(req, socket, head, (ws) => {
@@ -953,31 +1041,28 @@ var init_server = __esm({
953
1041
  }
954
1042
  // --- HTTP Handler ---
955
1043
  handleHttp(req, res) {
956
- const url = new URL(req.url, `http://${req.headers.host}`);
957
- const origin = req.headers.origin;
958
- if (origin && (origin.includes("127.0.0.1") || origin.includes("localhost"))) {
959
- res.setHeader("Access-Control-Allow-Origin", origin);
960
- }
961
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
962
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
963
- if (req.method === "OPTIONS") {
964
- res.writeHead(204);
965
- res.end();
1044
+ const url = new URL(req.url ?? "/", "http://hub");
1045
+ if (isCrossOrigin(req)) {
1046
+ this.jsonResponse(res, 403, { error: "Cross-origin request rejected" });
966
1047
  return;
967
1048
  }
968
- if (url.pathname !== "/" && this.token) {
969
- if (!this.isLocalRequest(req)) {
970
- const authHeader = req.headers["authorization"];
971
- const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
972
- if (bearerToken !== this.token) {
973
- this.jsonResponse(res, 401, { error: "Unauthorized" });
974
- return;
975
- }
976
- }
1049
+ if (req.method === "POST" && !isJsonRequest(req)) {
1050
+ this.jsonResponse(res, 415, { error: "Content-Type must be application/json" });
1051
+ return;
977
1052
  }
978
1053
  if (url.pathname === "/" && req.method === "GET") {
979
- this.serveDashboard(res);
980
- } else if (url.pathname === "/api/sessions" && req.method === "GET") {
1054
+ this.serveDashboard(req, res, url);
1055
+ return;
1056
+ }
1057
+ if (url.pathname === "/api/login" && req.method === "POST") {
1058
+ this.handleLogin(req, res);
1059
+ return;
1060
+ }
1061
+ if (!this.authorized(req, false)) {
1062
+ this.jsonResponse(res, 401, { error: "Unauthorized" });
1063
+ return;
1064
+ }
1065
+ if (url.pathname === "/api/sessions" && req.method === "GET") {
981
1066
  this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });
982
1067
  } else if (url.pathname === "/api/status" && req.method === "GET") {
983
1068
  this.jsonResponse(res, 200, {
@@ -1028,7 +1113,13 @@ var init_server = __esm({
1028
1113
  this.jsonResponse(res, 404, { error: "Not found" });
1029
1114
  }
1030
1115
  }
1031
- serveDashboard(res) {
1116
+ serveDashboard(req, res, url) {
1117
+ const linkToken = url.searchParams.get("token");
1118
+ if (linkToken !== null && this.token && safeEqual(linkToken, this.token)) {
1119
+ res.writeHead(302, { Location: "/", "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
1120
+ res.end();
1121
+ return;
1122
+ }
1032
1123
  const candidates = [
1033
1124
  path4.join(__dirname, "..", "dashboard", "index.html"),
1034
1125
  // from dist/hub/
@@ -1055,6 +1146,20 @@ var init_server = __esm({
1055
1146
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1056
1147
  res.end("<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>");
1057
1148
  }
1149
+ async handleLogin(req, res) {
1150
+ const body = await this.readBody(req);
1151
+ if (!this.token) {
1152
+ res.writeHead(204);
1153
+ res.end();
1154
+ return;
1155
+ }
1156
+ if (typeof body?.token !== "string" || !safeEqual(body.token, this.token)) {
1157
+ this.jsonResponse(res, 401, { error: "Unauthorized" });
1158
+ return;
1159
+ }
1160
+ res.writeHead(204, { "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
1161
+ res.end();
1162
+ }
1058
1163
  async handleApiSend(req, res) {
1059
1164
  const body = await this.readBody(req);
1060
1165
  if (!body) {
@@ -1131,6 +1236,22 @@ var init_server = __esm({
1131
1236
  });
1132
1237
  }
1133
1238
  handleChannelMessage(ws, isLocal, msg) {
1239
+ if (msg.type === "register") {
1240
+ const id = msg.session.id;
1241
+ const owned = this.socketOwners.get(ws);
1242
+ if (owned && owned !== id) {
1243
+ logger.warn(`Rejected register for ${id}: connection already owns ${owned}`);
1244
+ return;
1245
+ }
1246
+ const holder = this.channelSockets.get(id);
1247
+ if (holder && holder !== ws) {
1248
+ this.socketOwners.delete(holder);
1249
+ holder.terminate();
1250
+ }
1251
+ this.socketOwners.set(ws, id);
1252
+ } else if ("sessionId" in msg && this.socketOwners.get(ws) !== msg.sessionId) {
1253
+ return;
1254
+ }
1134
1255
  switch (msg.type) {
1135
1256
  case "register": {
1136
1257
  const session = msg.session;
@@ -1139,7 +1260,7 @@ var init_server = __esm({
1139
1260
  this.sessions.register(session);
1140
1261
  this.channelSockets.set(session.id, ws);
1141
1262
  if (isLocal) this.localChannels.add(session.id);
1142
- logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);
1263
+ logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false}, peer: ${session.peerName ?? "-"})`);
1143
1264
  this.broadcastToDashboards({
1144
1265
  type: isReregister ? "session_updated" : "session_connected",
1145
1266
  session
@@ -1153,6 +1274,14 @@ var init_server = __esm({
1153
1274
  }
1154
1275
  break;
1155
1276
  }
1277
+ case "peer_name": {
1278
+ const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
1279
+ if (updated) {
1280
+ logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
1281
+ this.broadcastToDashboards({ type: "session_updated", session: updated });
1282
+ }
1283
+ break;
1284
+ }
1156
1285
  case "notify": {
1157
1286
  this.sessions.updateActivity(msg.sessionId);
1158
1287
  const notifySession = this.sessions.get(msg.sessionId);
@@ -1473,6 +1602,18 @@ var init_server = __esm({
1473
1602
  res.writeHead(status, { "Content-Type": "application/json" });
1474
1603
  res.end(JSON.stringify(body));
1475
1604
  }
1605
+ authorized(req, allowQueryToken) {
1606
+ return !this.token || isAuthorized(req, this.token, allowQueryToken);
1607
+ }
1608
+ rejectUpgrade(socket, status, text, pathname, req) {
1609
+ logger.warn(`Rejected ${pathname} upgrade (${status}) from ${req.socket.remoteAddress}`);
1610
+ socket.write(`HTTP/1.1 ${status} ${text}\r
1611
+ Connection: close\r
1612
+ Content-Length: 0\r
1613
+ \r
1614
+ `);
1615
+ socket.destroy();
1616
+ }
1476
1617
  isLocalRequest(req) {
1477
1618
  const addr = req.socket.remoteAddress;
1478
1619
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
@@ -1527,12 +1668,13 @@ var init_hub_client = __esm({
1527
1668
  init_logger();
1528
1669
  init_constants();
1529
1670
  HubClient = class {
1530
- constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token) {
1671
+ constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
1531
1672
  this.sessionId = sessionId2;
1532
1673
  this.sessionName = sessionName2;
1533
1674
  this.hubHost = hubHost2;
1534
1675
  this.hubPort = hubPort2;
1535
1676
  this.token = token;
1677
+ this.getPeerName = getPeerName;
1536
1678
  }
1537
1679
  ws = null;
1538
1680
  reconnectTimer = null;
@@ -1557,7 +1699,8 @@ var init_hub_client = __esm({
1557
1699
  connectedAt: Date.now(),
1558
1700
  lastActivity: Date.now(),
1559
1701
  cwd: process.cwd(),
1560
- channelEnabled: true
1702
+ channelEnabled: true,
1703
+ peerName: this.getPeerName()
1561
1704
  }
1562
1705
  };
1563
1706
  this.ws.send(JSON.stringify(registration));
@@ -1625,6 +1768,53 @@ var init_hub_client = __esm({
1625
1768
  }
1626
1769
  });
1627
1770
 
1771
+ // src/channel/peer-name.ts
1772
+ import fs5 from "fs";
1773
+ import os2 from "os";
1774
+ import path5 from "path";
1775
+ function findPeerName(records, env) {
1776
+ const valid = records.filter((r) => typeof r === "object" && r !== null);
1777
+ const nameOf = (match) => {
1778
+ const rec = valid.find(match);
1779
+ return isValidPeerName(rec?.name) ? rec.name : void 0;
1780
+ };
1781
+ if (env.messagingSocket) {
1782
+ const bySocket = nameOf((r) => r.messagingSocketPath === env.messagingSocket);
1783
+ if (bySocket) return bySocket;
1784
+ }
1785
+ if (env.sessionId) return nameOf((r) => r.sessionId === env.sessionId);
1786
+ return void 0;
1787
+ }
1788
+ function readPeerName(env = process.env) {
1789
+ const lookup = {
1790
+ messagingSocket: env.CLAUDE_CODE_MESSAGING_SOCKET,
1791
+ sessionId: env.CLAUDE_CODE_SESSION_ID
1792
+ };
1793
+ if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
1794
+ const configDir = env.CLAUDE_CONFIG_DIR || path5.join(os2.homedir(), ".claude");
1795
+ const dir = path5.join(configDir, "sessions");
1796
+ let files;
1797
+ try {
1798
+ files = fs5.readdirSync(dir).filter((f) => f.endsWith(".json"));
1799
+ } catch {
1800
+ return void 0;
1801
+ }
1802
+ const records = [];
1803
+ for (const f of files) {
1804
+ try {
1805
+ records.push(JSON.parse(fs5.readFileSync(path5.join(dir, f), "utf8")));
1806
+ } catch {
1807
+ }
1808
+ }
1809
+ return findPeerName(records, lookup);
1810
+ }
1811
+ var init_peer_name2 = __esm({
1812
+ "src/channel/peer-name.ts"() {
1813
+ "use strict";
1814
+ init_peer_name();
1815
+ }
1816
+ });
1817
+
1628
1818
  // src/channel/server.ts
1629
1819
  var server_exports2 = {};
1630
1820
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -1635,10 +1825,19 @@ import {
1635
1825
  } from "@modelcontextprotocol/sdk/types.js";
1636
1826
  import { z } from "zod";
1637
1827
  import { randomUUID as randomUUID4 } from "crypto";
1638
- import path5 from "path";
1828
+ import path6 from "path";
1639
1829
  async function main() {
1640
1830
  logger.info(`Starting MCP channel server (session: ${sessionId})`);
1641
1831
  hubClient.connect();
1832
+ const refreshPeerName = () => {
1833
+ const next = readPeerName();
1834
+ if (next === peerName) return;
1835
+ peerName = next;
1836
+ logger.info(`Peer name: ${next ?? "(none)"}`);
1837
+ hubClient.send({ type: "peer_name", sessionId, peerName: next });
1838
+ };
1839
+ setTimeout(refreshPeerName, 2e3).unref();
1840
+ setInterval(refreshPeerName, 3e4).unref();
1642
1841
  hubClient.onMessage(async (msg) => {
1643
1842
  if (msg.type === "message_to_session" && msg.sessionId === sessionId) {
1644
1843
  logger.info(`Message from dashboard: ${msg.content}`);
@@ -1685,7 +1884,7 @@ ${msg.content}` : "";
1685
1884
  process.exit(0);
1686
1885
  });
1687
1886
  }
1688
- var sessionId, sessionName, server, config, hubHost, hubPort, hubToken, hubClient, PermissionRequestSchema;
1887
+ var sessionId, sessionName, peerName, server, config, hubHost, hubPort, hubToken, hubClient, PermissionRequestSchema;
1689
1888
  var init_server2 = __esm({
1690
1889
  "src/channel/server.ts"() {
1691
1890
  "use strict";
@@ -1693,8 +1892,10 @@ var init_server2 = __esm({
1693
1892
  init_constants();
1694
1893
  init_config();
1695
1894
  init_hub_client();
1895
+ init_peer_name2();
1696
1896
  sessionId = randomUUID4();
1697
- sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path5.basename(process.cwd());
1897
+ sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path6.basename(process.cwd());
1898
+ peerName = readPeerName();
1698
1899
  server = new Server(
1699
1900
  {
1700
1901
  name: CHANNEL_SERVER_NAME,
@@ -1717,6 +1918,8 @@ REPLYING:
1717
1918
 
1718
1919
  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.
1719
1920
 
1921
+ ROUTING: If a dashboard message has lines like [claude-alarm] @X = SendMessage to "Y", do what the message asks and deliver the result with the SendMessage tool to exactly "Y". Never guess other recipients. Then reply to the dashboard with what you sent.
1922
+
1720
1923
  STATUS: Call status("working") before starting a long task, status("waiting_input") when blocked on user input, status("idle") when finished responding.
1721
1924
 
1722
1925
  NOTIFICATIONS:
@@ -1733,7 +1936,8 @@ NOTIFICATIONS:
1733
1936
  sessionName,
1734
1937
  hubHost,
1735
1938
  hubPort,
1736
- hubToken
1939
+ hubToken,
1940
+ () => peerName
1737
1941
  );
1738
1942
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1739
1943
  tools: [
@@ -1870,11 +2074,17 @@ init_constants();
1870
2074
  init_logger();
1871
2075
  init_crash_guard();
1872
2076
  import { spawn } from "child_process";
1873
- import fs5 from "fs";
1874
- import path6 from "path";
2077
+ import fs6 from "fs";
2078
+ import path7 from "path";
1875
2079
  import readline from "readline";
1876
2080
  import { fileURLToPath as fileURLToPath2 } from "url";
1877
- var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
2081
+ var __dirname2 = path7.dirname(fileURLToPath2(import.meta.url));
2082
+ function authHeaders(token) {
2083
+ return token ? { Authorization: `Bearer ${token}` } : {};
2084
+ }
2085
+ function loginLink(displayHost, port, token) {
2086
+ return `http://${displayHost}:${port}/${token ? `?token=${encodeURIComponent(token)}` : ""}`;
2087
+ }
1878
2088
  function printUsage() {
1879
2089
  console.log(`
1880
2090
  claude-alarm - Monitor Claude Code sessions with notifications
@@ -1895,8 +2105,8 @@ Quick start:
1895
2105
  }
1896
2106
  async function checkForUpdates() {
1897
2107
  try {
1898
- const pkgPath = path6.join(__dirname2, "..", "package.json");
1899
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
2108
+ const pkgPath = path7.join(__dirname2, "..", "package.json");
2109
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
1900
2110
  const currentVersion = pkg.version;
1901
2111
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
1902
2112
  signal: AbortSignal.timeout(3e3)
@@ -1919,28 +2129,28 @@ async function hubStart(daemon) {
1919
2129
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1920
2130
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1921
2131
  checkForUpdates();
1922
- if (fs5.existsSync(PID_FILE)) {
1923
- const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
2132
+ if (fs6.existsSync(PID_FILE)) {
2133
+ const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
1924
2134
  if (isProcessRunning(pid)) {
1925
- console.log(`Hub is already running (PID: ${pid}) on http://${displayHost}:${port}`);
2135
+ console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
1926
2136
  return;
1927
2137
  }
1928
- fs5.unlinkSync(PID_FILE);
2138
+ fs6.unlinkSync(PID_FILE);
1929
2139
  }
1930
2140
  if (daemon) {
1931
2141
  ensureConfigDir();
1932
- const logFd = fs5.openSync(LOG_FILE, "a");
1933
- const hubScript = path6.join(__dirname2, "hub", "server.js");
2142
+ const logFd = fs6.openSync(LOG_FILE, "a");
2143
+ const hubScript = path7.join(__dirname2, "hub", "server.js");
1934
2144
  const child = spawn(process.execPath, [hubScript], {
1935
2145
  detached: true,
1936
2146
  stdio: ["ignore", logFd, logFd],
1937
2147
  env: { ...process.env }
1938
2148
  });
1939
2149
  if (child.pid) {
1940
- fs5.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2150
+ fs6.writeFileSync(PID_FILE, String(child.pid), "utf-8");
1941
2151
  child.unref();
1942
2152
  console.log(`Hub started as daemon (PID: ${child.pid})`);
1943
- console.log(`Dashboard: http://${displayHost}:${port}`);
2153
+ console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
1944
2154
  console.log(`Token: ${config2.hub.token}`);
1945
2155
  console.log(`Logs: ${LOG_FILE}`);
1946
2156
  } else {
@@ -1949,6 +2159,7 @@ async function hubStart(daemon) {
1949
2159
  }
1950
2160
  } else {
1951
2161
  console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
2162
+ console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
1952
2163
  console.log(`Token: ${config2.hub.token}`);
1953
2164
  console.log(`Logs: ${LOG_FILE}`);
1954
2165
  installCrashGuard("hub foreground");
@@ -1957,11 +2168,11 @@ async function hubStart(daemon) {
1957
2168
  const hub = new HubServer2(config2);
1958
2169
  await hub.start();
1959
2170
  ensureConfigDir();
1960
- fs5.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2171
+ fs6.writeFileSync(PID_FILE, String(process.pid), "utf-8");
1961
2172
  const shutdown = async () => {
1962
2173
  console.log("\nShutting down...");
1963
2174
  await hub.stop();
1964
- if (fs5.existsSync(PID_FILE)) fs5.unlinkSync(PID_FILE);
2175
+ if (fs6.existsSync(PID_FILE)) fs6.unlinkSync(PID_FILE);
1965
2176
  process.exit(0);
1966
2177
  };
1967
2178
  process.on("SIGINT", shutdown);
@@ -1969,18 +2180,18 @@ async function hubStart(daemon) {
1969
2180
  }
1970
2181
  }
1971
2182
  function hubStop() {
1972
- if (!fs5.existsSync(PID_FILE)) {
2183
+ if (!fs6.existsSync(PID_FILE)) {
1973
2184
  console.log("Hub is not running (no PID file found)");
1974
2185
  return;
1975
2186
  }
1976
- const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
2187
+ const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
1977
2188
  try {
1978
2189
  process.kill(pid, "SIGTERM");
1979
2190
  console.log(`Hub stopped (PID: ${pid})`);
1980
2191
  } catch {
1981
2192
  console.log("Hub process not found (may have already stopped)");
1982
2193
  }
1983
- fs5.unlinkSync(PID_FILE);
2194
+ fs6.unlinkSync(PID_FILE);
1984
2195
  }
1985
2196
  async function hubStatus() {
1986
2197
  const config2 = loadConfig();
@@ -1988,8 +2199,8 @@ async function hubStatus() {
1988
2199
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1989
2200
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1990
2201
  let pidInfo = "not running";
1991
- if (fs5.existsSync(PID_FILE)) {
1992
- const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
2202
+ if (fs6.existsSync(PID_FILE)) {
2203
+ const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
1993
2204
  if (isProcessRunning(pid)) {
1994
2205
  pidInfo = `running (PID: ${pid})`;
1995
2206
  } else {
@@ -1997,7 +2208,7 @@ async function hubStatus() {
1997
2208
  }
1998
2209
  }
1999
2210
  try {
2000
- const res = await fetch(`http://${host}:${port}/api/status`);
2211
+ const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
2001
2212
  if (res.ok) {
2002
2213
  const data = await res.json();
2003
2214
  console.log(`Hub: running (PID: ${data.pid})`);
@@ -2061,7 +2272,7 @@ function ask(question) {
2061
2272
  }
2062
2273
  async function init() {
2063
2274
  const dir = process.cwd();
2064
- const projectName = path6.basename(dir);
2275
+ const projectName = path7.basename(dir);
2065
2276
  console.log(`
2066
2277
  claude-alarm init for "${projectName}"
2067
2278
  `);
@@ -2081,11 +2292,11 @@ claude-alarm init for "${projectName}"
2081
2292
  if (port) env.CLAUDE_ALARM_HUB_PORT = port;
2082
2293
  if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
2083
2294
  }
2084
- const mcpPath = path6.join(dir, ".mcp.json");
2295
+ const mcpPath = path7.join(dir, ".mcp.json");
2085
2296
  let mcpConfig = {};
2086
- if (fs5.existsSync(mcpPath)) {
2297
+ if (fs6.existsSync(mcpPath)) {
2087
2298
  try {
2088
- mcpConfig = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
2299
+ mcpConfig = JSON.parse(fs6.readFileSync(mcpPath, "utf-8"));
2089
2300
  } catch {
2090
2301
  mcpConfig = {};
2091
2302
  }
@@ -2096,7 +2307,7 @@ claude-alarm init for "${projectName}"
2096
2307
  args: ["-y", "@delt/claude-alarm", "serve"],
2097
2308
  env
2098
2309
  };
2099
- fs5.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2310
+ fs6.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2100
2311
  console.log(`
2101
2312
  \u2713 Created ${mcpPath}`);
2102
2313
  if (remote.toLowerCase() !== "y") {
@@ -2106,7 +2317,7 @@ claude-alarm init for "${projectName}"
2106
2317
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2107
2318
  let hubRunning = false;
2108
2319
  try {
2109
- const res = await fetch(`http://${host}:${port}/api/status`);
2320
+ const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
2110
2321
  hubRunning = res.ok;
2111
2322
  } catch {
2112
2323
  }