@delt/claude-alarm 0.9.1 → 1.0.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/hub/server.ts
2
2
  import http from "http";
3
- import fs4 from "fs";
4
- import path4 from "path";
3
+ import fs5 from "fs";
4
+ import path5 from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { WebSocketServer, WebSocket } from "ws";
7
7
 
@@ -36,6 +36,8 @@ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
36
36
  var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
37
37
  var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
38
38
  var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
39
+ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
40
+ var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
39
41
  var WS_PATH_CHANNEL = "/ws/channel";
40
42
  var WS_PATH_DASHBOARD = "/ws/dashboard";
41
43
  var CHANNEL_SERVER_NAME = "claude-alarm";
@@ -51,9 +53,9 @@ function isValidPeerName(name) {
51
53
  var SessionManager = class {
52
54
  sessions = /* @__PURE__ */ new Map();
53
55
  register(session) {
54
- const baseName = session.cwd?.replace(/^.*[/\\]/, "") || session.name;
56
+ const baseName = this.baseName(session);
55
57
  const existing = Array.from(this.sessions.values()).filter(
56
- (s) => s.id !== session.id && (s.cwd?.replace(/^.*[/\\]/, "") || s.name) === baseName
58
+ (s) => s.id !== session.id && this.baseName(s) === baseName
57
59
  );
58
60
  if (existing.length > 0) {
59
61
  const usedNums = existing.map((s) => {
@@ -107,6 +109,9 @@ var SessionManager = class {
107
109
  count() {
108
110
  return this.sessions.size;
109
111
  }
112
+ baseName(session) {
113
+ return session.title || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
114
+ }
110
115
  };
111
116
 
112
117
  // src/hub/notifier.ts
@@ -127,7 +132,7 @@ var Notifier = class {
127
132
  async notify(title, message, level = "info") {
128
133
  await this.notifyWithSession(void 0, void 0, title, message, level);
129
134
  }
130
- async notifyWithSession(sessionId, sessionLabel, title, message, level = "info") {
135
+ async notifyWithSession(sessionId, sessionLabel2, title, message, level = "info") {
131
136
  const promises = [];
132
137
  if (this.desktopEnabled) {
133
138
  promises.push(this.sendDesktop(title, message, level));
@@ -135,8 +140,8 @@ var Notifier = class {
135
140
  for (const webhook of this.webhooks) {
136
141
  promises.push(this.sendWebhook(webhook, title, message, level));
137
142
  }
138
- if (this.telegramBot && sessionId && sessionLabel) {
139
- promises.push(this.telegramBot.sendNotification(sessionId, sessionLabel, title, message));
143
+ if (this.telegramBot && sessionId && sessionLabel2) {
144
+ promises.push(this.telegramBot.sendNotification(sessionId, sessionLabel2, title, message));
140
145
  }
141
146
  await Promise.allSettled(promises);
142
147
  }
@@ -222,6 +227,14 @@ var Notifier = class {
222
227
  import fs from "fs";
223
228
  import path2 from "path";
224
229
  import { randomUUID } from "crypto";
230
+
231
+ // src/shared/session-label.ts
232
+ function sessionLabel(session) {
233
+ const base = session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
234
+ return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
235
+ }
236
+
237
+ // src/hub/telegram.ts
225
238
  var TELEGRAM_API = "https://api.telegram.org/bot";
226
239
  var TelegramBot = class _TelegramBot {
227
240
  config;
@@ -360,15 +373,14 @@ ${this.mdToHtml(message)}`;
360
373
  const pending = this.pendingMessages.get(msg.chat.id);
361
374
  if (pending) {
362
375
  this.pendingMessages.delete(msg.chat.id);
363
- const sessions2 = this.getSessions?.() ?? [];
364
- const idx = parseInt(selectMatch[1], 10) - 1;
365
- if (idx >= 0 && idx < sessions2.length) {
376
+ const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
377
+ if (session) {
366
378
  if (pending.photoFileId) {
367
- await this.deliverPhotoToSessionByFileId(sessions2[idx].id, pending.photoFileId, pending.caption);
379
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
368
380
  } else if (pending.text) {
369
- this.deliverToSession(sessions2[idx].id, pending.text);
381
+ this.deliverToSession(session.id, pending.text);
370
382
  }
371
- this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
383
+ this.sendMessage(`Sent to [${this.getLabel(session)}]`);
372
384
  } else {
373
385
  this.sendMessage("Invalid session number.");
374
386
  }
@@ -389,11 +401,12 @@ ${this.mdToHtml(message)}`;
389
401
  }
390
402
  return;
391
403
  }
404
+ const sessionIds = sessions.map((s) => s.id);
392
405
  if (hasPhoto) {
393
406
  const largest = msg.photo[msg.photo.length - 1];
394
- this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
407
+ this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
395
408
  } else {
396
- this.pendingMessages.set(msg.chat.id, { text });
409
+ this.pendingMessages.set(msg.chat.id, { text, sessionIds });
397
410
  }
398
411
  const buttons = sessions.map((s, i) => ({
399
412
  text: this.getLabel(s),
@@ -450,11 +463,16 @@ ${this.mdToHtml(message)}`;
450
463
  logger.warn(`Telegram photo download failed: ${err.message}`);
451
464
  }
452
465
  }
466
+ // Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
467
+ pendingSession(pending, idx) {
468
+ const id = pending.sessionIds[idx];
469
+ return id ? this.getSessions?.().find((s) => s.id === id) : void 0;
470
+ }
453
471
  getLabel(session) {
454
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
472
+ return sessionLabel(session);
455
473
  }
456
474
  /** Send a permission request with inline buttons */
457
- async sendPermissionRequest(sessionId, sessionLabel, requestId, toolName, description, inputPreview) {
475
+ async sendPermissionRequest(sessionId, sessionLabel2, requestId, toolName, description, inputPreview) {
458
476
  const isNotify = toolName.endsWith("__notify") || toolName === "notify";
459
477
  let preview = inputPreview;
460
478
  let kind = "code";
@@ -507,7 +525,7 @@ ${this.mdToHtml(message)}`;
507
525
  const mcpMatch = toolName.match(/__([^_]+)$/);
508
526
  if (mcpMatch) displayTool = mcpMatch[1].charAt(0).toUpperCase() + mcpMatch[1].slice(1);
509
527
  }
510
- const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel)}
528
+ const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
511
529
 
512
530
  \u{1F527} <b>${this.escHtml(displayTool)}</b>
513
531
  ${previewHtml}${truncNote}`;
@@ -547,22 +565,18 @@ ${label}`);
547
565
  const parts = query.data.split(":");
548
566
  if (parts.length < 3) return;
549
567
  const [, idxStr, chatIdStr] = parts;
550
- const idx = parseInt(idxStr, 10);
551
568
  const chatId = parseInt(chatIdStr, 10);
552
- const sessions = this.getSessions?.() ?? [];
553
- if (idx < 0 || idx >= sessions.length) {
569
+ const pending = this.pendingMessages.get(chatId);
570
+ const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
571
+ if (!pending || !session) {
554
572
  await this.answerCallbackQuery(query.id, "Session not found");
555
573
  return;
556
574
  }
557
- const session = sessions[idx];
558
- const pending = this.pendingMessages.get(chatId);
559
575
  this.pendingMessages.delete(chatId);
560
- if (pending) {
561
- if (pending.photoFileId) {
562
- await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
563
- } else if (pending.text) {
564
- this.deliverToSession(session.id, pending.text);
565
- }
576
+ if (pending.photoFileId) {
577
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
578
+ } else if (pending.text) {
579
+ this.deliverToSession(session.id, pending.text);
566
580
  }
567
581
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
568
582
  if (query.message) {
@@ -624,8 +638,8 @@ ${label}`);
624
638
  css: "css",
625
639
  scss: "scss"
626
640
  };
627
- langOf(path5) {
628
- const m = /\.([a-z0-9]+)$/i.exec(path5 || "");
641
+ langOf(path6) {
642
+ const m = /\.([a-z0-9]+)$/i.exec(path6 || "");
629
643
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
630
644
  }
631
645
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -668,9 +682,73 @@ ${bodyLines.join("\n")}`;
668
682
  }
669
683
  };
670
684
 
671
- // src/shared/config.ts
685
+ // src/hub/codex-supervisor.ts
686
+ import { spawn } from "child_process";
672
687
  import fs2 from "fs";
673
688
  import path3 from "path";
689
+ var HEALTHY_RUN_MS = 6e4;
690
+ function resolveAdapterScript(baseDir) {
691
+ return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
692
+ }
693
+ var CodexSupervisor = class {
694
+ constructor(script, spawnFn = spawn, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
695
+ this.script = script;
696
+ this.spawnFn = spawnFn;
697
+ this.minDelayMs = minDelayMs;
698
+ this.maxDelayMs = maxDelayMs;
699
+ this.stopGraceMs = stopGraceMs;
700
+ this.delay = minDelayMs;
701
+ }
702
+ child;
703
+ timer;
704
+ stopped = true;
705
+ delay;
706
+ start() {
707
+ this.stopped = false;
708
+ this.launch();
709
+ }
710
+ stop() {
711
+ this.stopped = true;
712
+ if (this.timer) clearTimeout(this.timer);
713
+ this.timer = void 0;
714
+ const child = this.child;
715
+ this.child = void 0;
716
+ if (!child) return;
717
+ child.stdin?.end();
718
+ const force = setTimeout(() => child.kill(), this.stopGraceMs);
719
+ force.unref();
720
+ child.once("exit", () => clearTimeout(force));
721
+ }
722
+ launch() {
723
+ const startedAt = Date.now();
724
+ const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
725
+ detached: true,
726
+ stdio: ["pipe", "pipe", "pipe"],
727
+ windowsHide: true
728
+ });
729
+ this.child = child;
730
+ child.stdout?.on("data", (d) => process.stdout.write(d));
731
+ child.stderr?.on("data", (d) => process.stderr.write(d));
732
+ child.stdin?.on("error", () => {
733
+ });
734
+ child.on("error", (err) => logger.warn(`Codex adapter failed to start: ${err.message}`));
735
+ child.on("exit", (code, signal) => {
736
+ if (this.child === child) this.child = void 0;
737
+ if (this.stopped || code === 0) return;
738
+ if (Date.now() - startedAt > HEALTHY_RUN_MS) this.delay = this.minDelayMs;
739
+ logger.warn(`Codex adapter exited (${signal ?? code}); restarting in ${this.delay}ms`);
740
+ this.timer = setTimeout(() => {
741
+ this.timer = void 0;
742
+ if (!this.stopped) this.launch();
743
+ }, this.delay);
744
+ this.delay = Math.min(this.delay * 2, this.maxDelayMs);
745
+ });
746
+ }
747
+ };
748
+
749
+ // src/shared/config.ts
750
+ import fs3 from "fs";
751
+ import path4 from "path";
674
752
  import { randomUUID as randomUUID2 } from "crypto";
675
753
  var DEFAULT_CONFIG = {
676
754
  hub: {
@@ -684,18 +762,18 @@ var DEFAULT_CONFIG = {
684
762
  webhooks: []
685
763
  };
686
764
  function ensureConfigDir() {
687
- if (!fs2.existsSync(CONFIG_DIR)) {
688
- fs2.mkdirSync(CONFIG_DIR, { recursive: true });
765
+ if (!fs3.existsSync(CONFIG_DIR)) {
766
+ fs3.mkdirSync(CONFIG_DIR, { recursive: true });
689
767
  }
690
768
  }
691
769
  function loadConfig() {
692
770
  ensureConfigDir();
693
771
  let config;
694
- if (!fs2.existsSync(CONFIG_FILE)) {
772
+ if (!fs3.existsSync(CONFIG_FILE)) {
695
773
  config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
696
774
  } else {
697
775
  try {
698
- const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
776
+ const raw = fs3.readFileSync(CONFIG_FILE, "utf-8");
699
777
  const parsed = JSON.parse(raw);
700
778
  config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
701
779
  } catch {
@@ -710,15 +788,15 @@ function loadConfig() {
710
788
  }
711
789
  function saveConfig(config) {
712
790
  ensureConfigDir();
713
- fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
791
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
714
792
  }
715
793
  function setupMcpConfig(targetDir) {
716
794
  const dir = targetDir ?? process.cwd();
717
- const mcpPath = path3.join(dir, ".mcp.json");
795
+ const mcpPath = path4.join(dir, ".mcp.json");
718
796
  let mcpConfig = {};
719
- if (fs2.existsSync(mcpPath)) {
797
+ if (fs3.existsSync(mcpPath)) {
720
798
  try {
721
- mcpConfig = JSON.parse(fs2.readFileSync(mcpPath, "utf-8"));
799
+ mcpConfig = JSON.parse(fs3.readFileSync(mcpPath, "utf-8"));
722
800
  } catch {
723
801
  mcpConfig = {};
724
802
  }
@@ -730,21 +808,21 @@ function setupMcpConfig(targetDir) {
730
808
  command: "npx",
731
809
  args: ["-y", "@delt/claude-alarm", "serve"],
732
810
  env: {
733
- CLAUDE_ALARM_SESSION_NAME: path3.basename(dir)
811
+ CLAUDE_ALARM_SESSION_NAME: path4.basename(dir)
734
812
  }
735
813
  };
736
- fs2.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
814
+ fs3.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
737
815
  return mcpPath;
738
816
  }
739
817
 
740
818
  // src/shared/crash-guard.ts
741
- import fs3 from "fs";
819
+ import fs4 from "fs";
742
820
  var BURST_WINDOW_MS = 6e4;
743
821
  var BURST_LIMIT = 10;
744
822
  function appendToLog(text) {
745
823
  try {
746
- fs3.mkdirSync(CONFIG_DIR, { recursive: true });
747
- fs3.appendFileSync(LOG_FILE, text, "utf-8");
824
+ fs4.mkdirSync(CONFIG_DIR, { recursive: true });
825
+ fs4.appendFileSync(LOG_FILE, text, "utf-8");
748
826
  } catch {
749
827
  }
750
828
  }
@@ -843,7 +921,7 @@ function isSecureRequest(req) {
843
921
  }
844
922
 
845
923
  // src/hub/server.ts
846
- var __dirname = path4.dirname(fileURLToPath(import.meta.url));
924
+ var __dirname = path5.dirname(fileURLToPath(import.meta.url));
847
925
  var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
848
926
  var HubServer = class {
849
927
  httpServer;
@@ -866,10 +944,13 @@ var HubServer = class {
866
944
  host;
867
945
  port;
868
946
  token;
947
+ codexEnabled;
948
+ codexSupervisor;
869
949
  constructor(config) {
870
950
  this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
871
951
  this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
872
952
  this.token = config?.hub?.token;
953
+ this.codexEnabled = config?.codex?.enabled === true;
873
954
  if (config?.notifications) {
874
955
  this.notifier.configure({
875
956
  desktop: config.notifications.desktop
@@ -927,12 +1008,15 @@ var HubServer = class {
927
1008
  this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
928
1009
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
929
1010
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
1011
+ this.startCodexAdapter();
930
1012
  resolve();
931
1013
  });
932
1014
  });
933
1015
  }
934
1016
  stop() {
935
1017
  return new Promise((resolve) => {
1018
+ this.codexSupervisor?.stop();
1019
+ this.codexSupervisor = void 0;
936
1020
  if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
937
1021
  if (this.telegramBot) this.telegramBot.stopPolling();
938
1022
  for (const ws of this.channelSockets.values()) ws.terminate();
@@ -1033,23 +1117,23 @@ var HubServer = class {
1033
1117
  return;
1034
1118
  }
1035
1119
  const candidates = [
1036
- path4.join(__dirname, "..", "dashboard", "index.html"),
1120
+ path5.join(__dirname, "..", "dashboard", "index.html"),
1037
1121
  // from dist/hub/
1038
- path4.join(__dirname, "dashboard", "index.html"),
1122
+ path5.join(__dirname, "dashboard", "index.html"),
1039
1123
  // from dist/ (bundled index.js)
1040
- path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1124
+ path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1041
1125
  // from dist/hub/ -> src/
1042
- path4.join(__dirname, "..", "src", "dashboard", "index.html"),
1126
+ path5.join(__dirname, "..", "src", "dashboard", "index.html"),
1043
1127
  // from dist/ -> src/
1044
- path4.join(process.cwd(), "dist", "dashboard", "index.html"),
1128
+ path5.join(process.cwd(), "dist", "dashboard", "index.html"),
1045
1129
  // from cwd
1046
- path4.join(process.cwd(), "src", "dashboard", "index.html")
1130
+ path5.join(process.cwd(), "src", "dashboard", "index.html")
1047
1131
  // from cwd/src
1048
1132
  ];
1049
1133
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
1050
1134
  for (const candidate of candidates) {
1051
- if (fs4.existsSync(candidate)) {
1052
- const html = fs4.readFileSync(candidate, "utf-8");
1135
+ if (fs5.existsSync(candidate)) {
1136
+ const html = fs5.readFileSync(candidate, "utf-8");
1053
1137
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1054
1138
  res.end(html);
1055
1139
  return;
@@ -1088,7 +1172,7 @@ var HubServer = class {
1088
1172
  this.jsonResponse(res, 404, { error: "Session not connected" });
1089
1173
  return;
1090
1174
  }
1091
- const msg = { type: "message_to_session", sessionId, content };
1175
+ const msg = { type: "message_to_session", sessionId, content, source: "api" };
1092
1176
  ws.send(JSON.stringify(msg));
1093
1177
  this.jsonResponse(res, 200, { ok: true });
1094
1178
  }
@@ -1262,7 +1346,7 @@ var HubServer = class {
1262
1346
  if (msg.type === "message_to_session") {
1263
1347
  const channelWs = this.channelSockets.get(msg.sessionId);
1264
1348
  if (channelWs?.readyState === WebSocket.OPEN) {
1265
- channelWs.send(JSON.stringify(msg));
1349
+ channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1266
1350
  }
1267
1351
  } else if (msg.type === "image_upload") {
1268
1352
  this.handleImageUpload(msg);
@@ -1307,11 +1391,11 @@ var HubServer = class {
1307
1391
  logger.warn("Image upload rejected: exceeds 10MB");
1308
1392
  return;
1309
1393
  }
1310
- fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
1394
+ fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
1311
1395
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1312
1396
  const filename = `${randomUUID3()}.${ext}`;
1313
- const filePath = path4.join(UPLOADS_DIR, filename);
1314
- fs4.writeFileSync(filePath, buffer);
1397
+ const filePath = path5.join(UPLOADS_DIR, filename);
1398
+ fs5.writeFileSync(filePath, buffer);
1315
1399
  const forwardMsg = {
1316
1400
  type: "image_to_session",
1317
1401
  sessionId,
@@ -1324,7 +1408,7 @@ var HubServer = class {
1324
1408
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1325
1409
  setTimeout(() => {
1326
1410
  try {
1327
- fs4.unlinkSync(filePath);
1411
+ fs5.unlinkSync(filePath);
1328
1412
  } catch {
1329
1413
  }
1330
1414
  }, 5 * 60 * 1e3);
@@ -1352,7 +1436,7 @@ var HubServer = class {
1352
1436
  this.telegramBot.onMessageToSession = (sessionId, content) => {
1353
1437
  const channelWs = this.channelSockets.get(sessionId);
1354
1438
  if (channelWs?.readyState === WebSocket.OPEN) {
1355
- const msg = { type: "message_to_session", sessionId, content };
1439
+ const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1356
1440
  channelWs.send(JSON.stringify(msg));
1357
1441
  logger.info(`Telegram message forwarded to session: ${sessionId}`);
1358
1442
  }
@@ -1479,6 +1563,17 @@ var HubServer = class {
1479
1563
  this.jsonResponse(res, 500, { error: err.message });
1480
1564
  }
1481
1565
  }
1566
+ startCodexAdapter() {
1567
+ if (!this.codexEnabled) return;
1568
+ const script = resolveAdapterScript(__dirname);
1569
+ if (!script) {
1570
+ logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
1571
+ return;
1572
+ }
1573
+ this.codexSupervisor = new CodexSupervisor(script);
1574
+ this.codexSupervisor.start();
1575
+ logger.info("Codex adapter started");
1576
+ }
1482
1577
  startHeartbeat() {
1483
1578
  this.heartbeatInterval = setInterval(() => {
1484
1579
  for (const [sessionId, ws] of this.channelSockets) {
@@ -1494,11 +1589,11 @@ var HubServer = class {
1494
1589
  }
1495
1590
  cleanupUploads() {
1496
1591
  try {
1497
- if (!fs4.existsSync(UPLOADS_DIR)) return;
1498
- const files = fs4.readdirSync(UPLOADS_DIR);
1592
+ if (!fs5.existsSync(UPLOADS_DIR)) return;
1593
+ const files = fs5.readdirSync(UPLOADS_DIR);
1499
1594
  for (const file of files) {
1500
1595
  try {
1501
- fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
1596
+ fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
1502
1597
  } catch {
1503
1598
  }
1504
1599
  }
@@ -1507,8 +1602,7 @@ var HubServer = class {
1507
1602
  }
1508
1603
  }
1509
1604
  getSessionLabel(session) {
1510
- if (!session) return "unknown";
1511
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
1605
+ return session ? sessionLabel(session) : "unknown";
1512
1606
  }
1513
1607
  jsonResponse(res, status, body) {
1514
1608
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -1572,48 +1666,41 @@ if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.arg
1572
1666
  // src/channel/hub-client.ts
1573
1667
  import WebSocket2 from "ws";
1574
1668
  var HubClient = class {
1575
- constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
1669
+ constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
1576
1670
  this.sessionId = sessionId;
1577
1671
  this.sessionName = sessionName;
1578
1672
  this.hubHost = hubHost;
1579
1673
  this.hubPort = hubPort;
1580
1674
  this.token = token;
1581
1675
  this.getPeerName = getPeerName;
1676
+ this.getRegistration = getRegistration;
1582
1677
  }
1583
1678
  ws = null;
1584
1679
  reconnectTimer = null;
1585
1680
  messageHandlers = [];
1586
1681
  queue = [];
1587
1682
  connected = false;
1683
+ closed = false;
1588
1684
  connect() {
1685
+ this.closed = false;
1589
1686
  const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
1590
1687
  const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
1591
1688
  logger.debug(`Connecting to hub at ${url}`);
1592
1689
  try {
1593
- this.ws = new WebSocket2(url);
1594
- this.ws.on("open", () => {
1690
+ const ws = new WebSocket2(url);
1691
+ this.ws = ws;
1692
+ ws.on("open", () => {
1693
+ if (this.ws !== ws) return;
1595
1694
  logger.info("Connected to hub");
1596
1695
  this.connected = true;
1597
- const registration = {
1598
- type: "register",
1599
- session: {
1600
- id: this.sessionId,
1601
- name: this.sessionName,
1602
- status: "idle",
1603
- connectedAt: Date.now(),
1604
- lastActivity: Date.now(),
1605
- cwd: process.cwd(),
1606
- channelEnabled: true,
1607
- peerName: this.getPeerName()
1608
- }
1609
- };
1610
- this.ws.send(JSON.stringify(registration));
1696
+ this.ws.send(JSON.stringify(this.registration()));
1611
1697
  for (const msg of this.queue) {
1612
1698
  this.ws.send(JSON.stringify(msg));
1613
1699
  }
1614
1700
  this.queue = [];
1615
1701
  });
1616
- this.ws.on("message", (data) => {
1702
+ ws.on("message", (data) => {
1703
+ if (this.ws !== ws) return;
1617
1704
  try {
1618
1705
  const msg = JSON.parse(data.toString());
1619
1706
  for (const handler of this.messageHandlers) {
@@ -1623,12 +1710,14 @@ var HubClient = class {
1623
1710
  logger.warn("Failed to parse hub message:", err);
1624
1711
  }
1625
1712
  });
1626
- this.ws.on("close", () => {
1713
+ ws.on("close", () => {
1714
+ if (this.ws !== ws) return;
1627
1715
  logger.info("Disconnected from hub");
1628
1716
  this.connected = false;
1629
- this.scheduleReconnect();
1717
+ if (!this.closed) this.scheduleReconnect();
1630
1718
  });
1631
- this.ws.on("error", (err) => {
1719
+ ws.on("error", (err) => {
1720
+ if (this.ws !== ws) return;
1632
1721
  logger.debug(`Hub connection error: ${err.message}`);
1633
1722
  this.connected = false;
1634
1723
  });
@@ -1651,6 +1740,7 @@ var HubClient = class {
1651
1740
  this.messageHandlers.push(handler);
1652
1741
  }
1653
1742
  disconnect() {
1743
+ this.closed = true;
1654
1744
  if (this.reconnectTimer) {
1655
1745
  clearTimeout(this.reconnectTimer);
1656
1746
  this.reconnectTimer = null;
@@ -1661,8 +1751,29 @@ var HubClient = class {
1661
1751
  }
1662
1752
  this.connected = false;
1663
1753
  }
1754
+ reregister() {
1755
+ if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
1756
+ this.ws.send(JSON.stringify(this.registration()));
1757
+ }
1758
+ }
1759
+ registration() {
1760
+ return {
1761
+ type: "register",
1762
+ session: {
1763
+ id: this.sessionId,
1764
+ name: this.sessionName,
1765
+ status: "idle",
1766
+ connectedAt: Date.now(),
1767
+ lastActivity: Date.now(),
1768
+ cwd: process.cwd(),
1769
+ channelEnabled: true,
1770
+ peerName: this.getPeerName(),
1771
+ ...this.getRegistration()
1772
+ }
1773
+ };
1774
+ }
1664
1775
  scheduleReconnect() {
1665
- if (this.reconnectTimer) return;
1776
+ if (this.reconnectTimer || this.closed) return;
1666
1777
  this.reconnectTimer = setTimeout(() => {
1667
1778
  this.reconnectTimer = null;
1668
1779
  this.connect();
@@ -1672,6 +1783,8 @@ var HubClient = class {
1672
1783
  export {
1673
1784
  CHANNEL_SERVER_NAME,
1674
1785
  CHANNEL_SERVER_VERSION,
1786
+ CODEX_LOG_FILE,
1787
+ CODEX_PID_FILE,
1675
1788
  CONFIG_DIR,
1676
1789
  CONFIG_FILE,
1677
1790
  DEFAULT_HUB_HOST,