@delt/claude-alarm 0.9.0 → 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 +29 -4
- package/dist/cli.js +156 -37
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +26 -33
- package/dist/hub/server.js +137 -32
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +137 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +26 -33
package/README.md
CHANGED
|
@@ -179,15 +179,40 @@ When Claude wants to run a tool (Bash, Write, Edit, etc.), a permission request
|
|
|
179
179
|
- Local terminal prompt stays open; whichever answer arrives first (local or dashboard) is applied
|
|
180
180
|
- Parsed previews: Bash commands show `$ command`, file operations show file paths
|
|
181
181
|
|
|
182
|
+
## Authentication
|
|
183
|
+
|
|
184
|
+
Every connection to the hub needs the hub token — including ones from the same machine.
|
|
185
|
+
|
|
186
|
+
- **Channel servers** read it from `~/.claude-alarm/config.json` automatically (or `CLAUDE_ALARM_HUB_TOKEN`).
|
|
187
|
+
- **Dashboard**: open the login link printed by `claude-alarm hub start` (`http://127.0.0.1:7900/?token=…`), or paste the token from `claude-alarm token` into the login form. The browser keeps an HttpOnly cookie for 30 days, and the token is removed from the URL.
|
|
188
|
+
- **CLI / scripts**: send `Authorization: Bearer <token>`.
|
|
189
|
+
|
|
190
|
+
> Upgrading from 0.9.x: a local session that sets `CLAUDE_ALARM_HUB_TOKEN` to a different value than the hub's token no longer connects. Scripts that called the local API without a token now get `401`.
|
|
191
|
+
|
|
182
192
|
## Remote Access
|
|
183
193
|
|
|
184
194
|
<p align="center">
|
|
185
195
|
<img src="https://raw.githubusercontent.com/delt96/delt-claude-alarm/main/docs/remote-access.svg" alt="Remote Access" width="600">
|
|
186
196
|
</p>
|
|
187
197
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
Pick one:
|
|
199
|
+
|
|
200
|
+
1. **Tailscale (recommended)** — set `host` to the hub machine's Tailscale IP and use that address from other machines. Traffic is encrypted and never exposed to the internet.
|
|
201
|
+
2. **Cloudflare Tunnel / nginx with HTTPS** — keep `host: "127.0.0.1"` and point the tunnel or proxy at `http://127.0.0.1:7900`. The proxy must keep the browser's `Host` header (the hub rejects requests whose `Origin` doesn't match it), pass WebSocket upgrades, and forward `X-Forwarded-Proto` so the dashboard cookie is marked `Secure`. For nginx:
|
|
202
|
+
|
|
203
|
+
```nginx
|
|
204
|
+
location / {
|
|
205
|
+
proxy_pass http://127.0.0.1:7900;
|
|
206
|
+
proxy_http_version 1.1;
|
|
207
|
+
proxy_set_header Host $host;
|
|
208
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
209
|
+
proxy_set_header Connection "upgrade";
|
|
210
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
3. **Direct `0.0.0.0`** — set `host` to `0.0.0.0` and open port 7900. Traffic, including the token, is plain HTTP: use only on networks you trust.
|
|
214
|
+
|
|
215
|
+
On the remote machine run `claude-alarm init` → select remote hub (Y), or configure:
|
|
191
216
|
|
|
192
217
|
```json
|
|
193
218
|
{
|
|
@@ -196,7 +221,7 @@ When Claude wants to run a tool (Bash, Write, Edit, etc.), a permission request
|
|
|
196
221
|
"command": "npx",
|
|
197
222
|
"args": ["-y", "@delt/claude-alarm", "serve"],
|
|
198
223
|
"env": {
|
|
199
|
-
"CLAUDE_ALARM_HUB_HOST": "your-
|
|
224
|
+
"CLAUDE_ALARM_HUB_HOST": "your-hub-address",
|
|
200
225
|
"CLAUDE_ALARM_HUB_PORT": "7900",
|
|
201
226
|
"CLAUDE_ALARM_HUB_TOKEN": "your-token"
|
|
202
227
|
}
|
package/dist/cli.js
CHANGED
|
@@ -696,6 +696,7 @@ ${previewHtml}${truncNote}`;
|
|
|
696
696
|
}
|
|
697
697
|
async handleCallbackQuery(query) {
|
|
698
698
|
if (!query.data) return;
|
|
699
|
+
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
699
700
|
if (query.data.startsWith("sess:")) {
|
|
700
701
|
await this.handleSessionSelectCallback(query);
|
|
701
702
|
return;
|
|
@@ -844,6 +845,69 @@ ${bodyLines.join("\n")}`;
|
|
|
844
845
|
}
|
|
845
846
|
});
|
|
846
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
|
+
|
|
847
911
|
// src/hub/server.ts
|
|
848
912
|
var server_exports = {};
|
|
849
913
|
__export(server_exports, {
|
|
@@ -855,7 +919,7 @@ import path4 from "path";
|
|
|
855
919
|
import { fileURLToPath } from "url";
|
|
856
920
|
import { WebSocketServer, WebSocket } from "ws";
|
|
857
921
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
858
|
-
var __dirname, HubServer;
|
|
922
|
+
var __dirname, MAX_WS_PAYLOAD, HubServer;
|
|
859
923
|
var init_server = __esm({
|
|
860
924
|
"src/hub/server.ts"() {
|
|
861
925
|
"use strict";
|
|
@@ -866,7 +930,9 @@ var init_server = __esm({
|
|
|
866
930
|
init_telegram();
|
|
867
931
|
init_config();
|
|
868
932
|
init_crash_guard();
|
|
933
|
+
init_auth();
|
|
869
934
|
__dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
935
|
+
MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
870
936
|
HubServer = class {
|
|
871
937
|
httpServer;
|
|
872
938
|
wssChannel;
|
|
@@ -884,6 +950,7 @@ var init_server = __esm({
|
|
|
884
950
|
heartbeatInterval;
|
|
885
951
|
channelAlive = /* @__PURE__ */ new Map();
|
|
886
952
|
// sessionId -> alive flag
|
|
953
|
+
socketOwners = /* @__PURE__ */ new WeakMap();
|
|
887
954
|
host;
|
|
888
955
|
port;
|
|
889
956
|
token;
|
|
@@ -906,22 +973,23 @@ var init_server = __esm({
|
|
|
906
973
|
this.initTelegram(fullConfig.telegram);
|
|
907
974
|
}
|
|
908
975
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
909
|
-
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
976
|
+
this.wssChannel = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
910
977
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
911
978
|
this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
|
|
912
|
-
this.wssDashboard = new WebSocketServer({ noServer: true });
|
|
979
|
+
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
913
980
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
914
981
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
915
982
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
916
983
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
917
|
-
const url = new URL(req.url,
|
|
984
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
918
985
|
const pathname = url.pathname;
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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;
|
|
925
993
|
}
|
|
926
994
|
if (pathname === WS_PATH_CHANNEL) {
|
|
927
995
|
this.wssChannel.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -973,31 +1041,28 @@ var init_server = __esm({
|
|
|
973
1041
|
}
|
|
974
1042
|
// --- HTTP Handler ---
|
|
975
1043
|
handleHttp(req, res) {
|
|
976
|
-
const url = new URL(req.url,
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
980
|
-
}
|
|
981
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
982
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
983
|
-
if (req.method === "OPTIONS") {
|
|
984
|
-
res.writeHead(204);
|
|
985
|
-
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" });
|
|
986
1047
|
return;
|
|
987
1048
|
}
|
|
988
|
-
if (
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
992
|
-
if (bearerToken !== this.token) {
|
|
993
|
-
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
1049
|
+
if (req.method === "POST" && !isJsonRequest(req)) {
|
|
1050
|
+
this.jsonResponse(res, 415, { error: "Content-Type must be application/json" });
|
|
1051
|
+
return;
|
|
997
1052
|
}
|
|
998
1053
|
if (url.pathname === "/" && req.method === "GET") {
|
|
999
|
-
this.serveDashboard(res);
|
|
1000
|
-
|
|
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") {
|
|
1001
1066
|
this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });
|
|
1002
1067
|
} else if (url.pathname === "/api/status" && req.method === "GET") {
|
|
1003
1068
|
this.jsonResponse(res, 200, {
|
|
@@ -1048,7 +1113,13 @@ var init_server = __esm({
|
|
|
1048
1113
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
1049
1114
|
}
|
|
1050
1115
|
}
|
|
1051
|
-
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
|
+
}
|
|
1052
1123
|
const candidates = [
|
|
1053
1124
|
path4.join(__dirname, "..", "dashboard", "index.html"),
|
|
1054
1125
|
// from dist/hub/
|
|
@@ -1075,6 +1146,20 @@ var init_server = __esm({
|
|
|
1075
1146
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1076
1147
|
res.end("<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>");
|
|
1077
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
|
+
}
|
|
1078
1163
|
async handleApiSend(req, res) {
|
|
1079
1164
|
const body = await this.readBody(req);
|
|
1080
1165
|
if (!body) {
|
|
@@ -1151,6 +1236,22 @@ var init_server = __esm({
|
|
|
1151
1236
|
});
|
|
1152
1237
|
}
|
|
1153
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
|
+
}
|
|
1154
1255
|
switch (msg.type) {
|
|
1155
1256
|
case "register": {
|
|
1156
1257
|
const session = msg.session;
|
|
@@ -1174,7 +1275,6 @@ var init_server = __esm({
|
|
|
1174
1275
|
break;
|
|
1175
1276
|
}
|
|
1176
1277
|
case "peer_name": {
|
|
1177
|
-
if (this.channelSockets.get(msg.sessionId) !== ws) break;
|
|
1178
1278
|
const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
|
|
1179
1279
|
if (updated) {
|
|
1180
1280
|
logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
|
|
@@ -1502,6 +1602,18 @@ var init_server = __esm({
|
|
|
1502
1602
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1503
1603
|
res.end(JSON.stringify(body));
|
|
1504
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
|
+
}
|
|
1505
1617
|
isLocalRequest(req) {
|
|
1506
1618
|
const addr = req.socket.remoteAddress;
|
|
1507
1619
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
@@ -1967,6 +2079,12 @@ import path7 from "path";
|
|
|
1967
2079
|
import readline from "readline";
|
|
1968
2080
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1969
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
|
+
}
|
|
1970
2088
|
function printUsage() {
|
|
1971
2089
|
console.log(`
|
|
1972
2090
|
claude-alarm - Monitor Claude Code sessions with notifications
|
|
@@ -2014,7 +2132,7 @@ async function hubStart(daemon) {
|
|
|
2014
2132
|
if (fs6.existsSync(PID_FILE)) {
|
|
2015
2133
|
const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2016
2134
|
if (isProcessRunning(pid)) {
|
|
2017
|
-
console.log(`Hub is already running (PID: ${pid})
|
|
2135
|
+
console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2018
2136
|
return;
|
|
2019
2137
|
}
|
|
2020
2138
|
fs6.unlinkSync(PID_FILE);
|
|
@@ -2032,7 +2150,7 @@ async function hubStart(daemon) {
|
|
|
2032
2150
|
fs6.writeFileSync(PID_FILE, String(child.pid), "utf-8");
|
|
2033
2151
|
child.unref();
|
|
2034
2152
|
console.log(`Hub started as daemon (PID: ${child.pid})`);
|
|
2035
|
-
console.log(`Dashboard:
|
|
2153
|
+
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2036
2154
|
console.log(`Token: ${config2.hub.token}`);
|
|
2037
2155
|
console.log(`Logs: ${LOG_FILE}`);
|
|
2038
2156
|
} else {
|
|
@@ -2041,6 +2159,7 @@ async function hubStart(daemon) {
|
|
|
2041
2159
|
}
|
|
2042
2160
|
} else {
|
|
2043
2161
|
console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
|
|
2162
|
+
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2044
2163
|
console.log(`Token: ${config2.hub.token}`);
|
|
2045
2164
|
console.log(`Logs: ${LOG_FILE}`);
|
|
2046
2165
|
installCrashGuard("hub foreground");
|
|
@@ -2089,7 +2208,7 @@ async function hubStatus() {
|
|
|
2089
2208
|
}
|
|
2090
2209
|
}
|
|
2091
2210
|
try {
|
|
2092
|
-
const res = await fetch(`http://${host}:${port}/api/status
|
|
2211
|
+
const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
|
|
2093
2212
|
if (res.ok) {
|
|
2094
2213
|
const data = await res.json();
|
|
2095
2214
|
console.log(`Hub: running (PID: ${data.pid})`);
|
|
@@ -2198,7 +2317,7 @@ claude-alarm init for "${projectName}"
|
|
|
2198
2317
|
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2199
2318
|
let hubRunning = false;
|
|
2200
2319
|
try {
|
|
2201
|
-
const res = await fetch(`http://${host}:${port}/api/status
|
|
2320
|
+
const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
|
|
2202
2321
|
hubRunning = res.ok;
|
|
2203
2322
|
} catch {
|
|
2204
2323
|
}
|