@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/README.md +43 -4
- package/dist/channel/server.js +69 -5
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +280 -69
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +239 -42
- package/dist/hub/server.js +160 -33
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +164 -35
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/dashboard/index.html +239 -42
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(
|
|
782
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
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,
|
|
984
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
898
985
|
const pathname = url.pathname;
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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,
|
|
957
|
-
|
|
958
|
-
|
|
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 (
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
1874
|
-
import
|
|
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 =
|
|
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 =
|
|
1899
|
-
const pkg = JSON.parse(
|
|
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 (
|
|
1923
|
-
const pid = parseInt(
|
|
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})
|
|
2135
|
+
console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
1926
2136
|
return;
|
|
1927
2137
|
}
|
|
1928
|
-
|
|
2138
|
+
fs6.unlinkSync(PID_FILE);
|
|
1929
2139
|
}
|
|
1930
2140
|
if (daemon) {
|
|
1931
2141
|
ensureConfigDir();
|
|
1932
|
-
const logFd =
|
|
1933
|
-
const hubScript =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
1992
|
-
const pid = parseInt(
|
|
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 =
|
|
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 =
|
|
2295
|
+
const mcpPath = path7.join(dir, ".mcp.json");
|
|
2085
2296
|
let mcpConfig = {};
|
|
2086
|
-
if (
|
|
2297
|
+
if (fs6.existsSync(mcpPath)) {
|
|
2087
2298
|
try {
|
|
2088
|
-
mcpConfig = JSON.parse(
|
|
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
|
-
|
|
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
|
}
|