@delt/claude-alarm 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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) {
@@ -378,7 +398,7 @@ var init_telegram = __esm({
378
398
  init_logger();
379
399
  init_constants();
380
400
  TELEGRAM_API = "https://api.telegram.org/bot";
381
- TelegramBot = class {
401
+ TelegramBot = class _TelegramBot {
382
402
  config;
383
403
  offset = 0;
384
404
  polling = false;
@@ -610,33 +630,45 @@ ${this.mdToHtml(message)}`;
610
630
  }
611
631
  /** Send a permission request with inline buttons */
612
632
  async sendPermissionRequest(sessionId2, sessionLabel, requestId, toolName, description, inputPreview) {
633
+ const isNotify = toolName.endsWith("__notify") || toolName === "notify";
613
634
  let preview = inputPreview;
635
+ let kind = "code";
636
+ let lang = "";
614
637
  let truncated = false;
615
638
  try {
616
639
  const p = JSON.parse(inputPreview);
617
- if (p.command) preview = `$ ${p.command}`;
618
- else if (p.title && p.message) preview = p.message;
619
- else if (p.file_path) {
640
+ if (p.command) {
641
+ preview = `$ ${p.command}`;
642
+ lang = "bash";
643
+ } else if (p.title && p.message) {
644
+ preview = p.message;
645
+ kind = "prose";
646
+ } else if (p.file_path) {
620
647
  preview = p.file_path;
648
+ lang = this.langOf(p.file_path);
621
649
  if (p.content) {
622
650
  preview += "\n" + p.content.slice(0, 3e3);
623
651
  if (p.content.length > 500) truncated = true;
624
652
  }
625
653
  } else if (p.content && typeof p.content === "string") {
626
654
  preview = p.content.slice(0, 3e3);
655
+ if (isNotify) kind = "prose";
627
656
  if (p.content.length > 500) truncated = true;
628
657
  }
629
658
  } catch {
630
659
  truncated = true;
631
660
  const cmdMatch = inputPreview.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
632
661
  const contentMatch = inputPreview.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"/);
633
- if (cmdMatch) preview = `$ ${cmdMatch[1]}`;
634
- else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
662
+ if (cmdMatch) {
663
+ preview = `$ ${cmdMatch[1]}`;
664
+ lang = "bash";
665
+ } else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
635
666
  }
636
667
  const truncNote = truncated ? "\n\n<i>...truncated</i>" : "";
637
668
  const previewSlice = preview.slice(0, 3e3);
638
669
  const isShort = !previewSlice.includes("\n") && previewSlice.length < 100;
639
- const previewHtml = isShort ? `<code>${this.escHtml(previewSlice)}</code>` : this.escHtml(previewSlice);
670
+ const langAttr = lang ? ` class="language-${lang}"` : "";
671
+ const previewHtml = kind === "prose" ? this.mdToHtml(previewSlice) : isShort ? `<code>${this.escHtml(previewSlice)}</code>` : `<pre><code${langAttr}>${this.escHtml(previewSlice)}</code></pre>`;
640
672
  let displayTool = toolName;
641
673
  if ((toolName.endsWith("__notify") || toolName === "notify") && inputPreview) {
642
674
  try {
@@ -736,6 +768,40 @@ ${label}`);
736
768
  escHtml(s) {
737
769
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
738
770
  }
771
+ static LANG_BY_EXT = {
772
+ js: "javascript",
773
+ mjs: "javascript",
774
+ cjs: "javascript",
775
+ jsx: "javascript",
776
+ ts: "typescript",
777
+ tsx: "typescript",
778
+ py: "python",
779
+ rb: "ruby",
780
+ go: "go",
781
+ java: "java",
782
+ kt: "kotlin",
783
+ rs: "rust",
784
+ php: "php",
785
+ cs: "csharp",
786
+ sh: "bash",
787
+ bash: "bash",
788
+ zsh: "bash",
789
+ ps1: "powershell",
790
+ json: "json",
791
+ yml: "yaml",
792
+ yaml: "yaml",
793
+ xml: "xml",
794
+ sql: "sql",
795
+ md: "markdown",
796
+ html: "html",
797
+ vue: "html",
798
+ css: "css",
799
+ scss: "scss"
800
+ };
801
+ langOf(path8) {
802
+ const m = /\.([a-z0-9]+)$/i.exec(path8 || "");
803
+ return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
804
+ }
739
805
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
740
806
  mdToHtml(s) {
741
807
  let text = s.replace(
@@ -1093,7 +1159,7 @@ var init_server = __esm({
1093
1159
  this.sessions.register(session);
1094
1160
  this.channelSockets.set(session.id, ws);
1095
1161
  if (isLocal) this.localChannels.add(session.id);
1096
- logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);
1162
+ logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false}, peer: ${session.peerName ?? "-"})`);
1097
1163
  this.broadcastToDashboards({
1098
1164
  type: isReregister ? "session_updated" : "session_connected",
1099
1165
  session
@@ -1107,6 +1173,15 @@ var init_server = __esm({
1107
1173
  }
1108
1174
  break;
1109
1175
  }
1176
+ case "peer_name": {
1177
+ if (this.channelSockets.get(msg.sessionId) !== ws) break;
1178
+ const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
1179
+ if (updated) {
1180
+ logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
1181
+ this.broadcastToDashboards({ type: "session_updated", session: updated });
1182
+ }
1183
+ break;
1184
+ }
1110
1185
  case "notify": {
1111
1186
  this.sessions.updateActivity(msg.sessionId);
1112
1187
  const notifySession = this.sessions.get(msg.sessionId);
@@ -1481,12 +1556,13 @@ var init_hub_client = __esm({
1481
1556
  init_logger();
1482
1557
  init_constants();
1483
1558
  HubClient = class {
1484
- constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token) {
1559
+ constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
1485
1560
  this.sessionId = sessionId2;
1486
1561
  this.sessionName = sessionName2;
1487
1562
  this.hubHost = hubHost2;
1488
1563
  this.hubPort = hubPort2;
1489
1564
  this.token = token;
1565
+ this.getPeerName = getPeerName;
1490
1566
  }
1491
1567
  ws = null;
1492
1568
  reconnectTimer = null;
@@ -1511,7 +1587,8 @@ var init_hub_client = __esm({
1511
1587
  connectedAt: Date.now(),
1512
1588
  lastActivity: Date.now(),
1513
1589
  cwd: process.cwd(),
1514
- channelEnabled: true
1590
+ channelEnabled: true,
1591
+ peerName: this.getPeerName()
1515
1592
  }
1516
1593
  };
1517
1594
  this.ws.send(JSON.stringify(registration));
@@ -1579,6 +1656,53 @@ var init_hub_client = __esm({
1579
1656
  }
1580
1657
  });
1581
1658
 
1659
+ // src/channel/peer-name.ts
1660
+ import fs5 from "fs";
1661
+ import os2 from "os";
1662
+ import path5 from "path";
1663
+ function findPeerName(records, env) {
1664
+ const valid = records.filter((r) => typeof r === "object" && r !== null);
1665
+ const nameOf = (match) => {
1666
+ const rec = valid.find(match);
1667
+ return isValidPeerName(rec?.name) ? rec.name : void 0;
1668
+ };
1669
+ if (env.messagingSocket) {
1670
+ const bySocket = nameOf((r) => r.messagingSocketPath === env.messagingSocket);
1671
+ if (bySocket) return bySocket;
1672
+ }
1673
+ if (env.sessionId) return nameOf((r) => r.sessionId === env.sessionId);
1674
+ return void 0;
1675
+ }
1676
+ function readPeerName(env = process.env) {
1677
+ const lookup = {
1678
+ messagingSocket: env.CLAUDE_CODE_MESSAGING_SOCKET,
1679
+ sessionId: env.CLAUDE_CODE_SESSION_ID
1680
+ };
1681
+ if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
1682
+ const configDir = env.CLAUDE_CONFIG_DIR || path5.join(os2.homedir(), ".claude");
1683
+ const dir = path5.join(configDir, "sessions");
1684
+ let files;
1685
+ try {
1686
+ files = fs5.readdirSync(dir).filter((f) => f.endsWith(".json"));
1687
+ } catch {
1688
+ return void 0;
1689
+ }
1690
+ const records = [];
1691
+ for (const f of files) {
1692
+ try {
1693
+ records.push(JSON.parse(fs5.readFileSync(path5.join(dir, f), "utf8")));
1694
+ } catch {
1695
+ }
1696
+ }
1697
+ return findPeerName(records, lookup);
1698
+ }
1699
+ var init_peer_name2 = __esm({
1700
+ "src/channel/peer-name.ts"() {
1701
+ "use strict";
1702
+ init_peer_name();
1703
+ }
1704
+ });
1705
+
1582
1706
  // src/channel/server.ts
1583
1707
  var server_exports2 = {};
1584
1708
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -1589,10 +1713,19 @@ import {
1589
1713
  } from "@modelcontextprotocol/sdk/types.js";
1590
1714
  import { z } from "zod";
1591
1715
  import { randomUUID as randomUUID4 } from "crypto";
1592
- import path5 from "path";
1716
+ import path6 from "path";
1593
1717
  async function main() {
1594
1718
  logger.info(`Starting MCP channel server (session: ${sessionId})`);
1595
1719
  hubClient.connect();
1720
+ const refreshPeerName = () => {
1721
+ const next = readPeerName();
1722
+ if (next === peerName) return;
1723
+ peerName = next;
1724
+ logger.info(`Peer name: ${next ?? "(none)"}`);
1725
+ hubClient.send({ type: "peer_name", sessionId, peerName: next });
1726
+ };
1727
+ setTimeout(refreshPeerName, 2e3).unref();
1728
+ setInterval(refreshPeerName, 3e4).unref();
1596
1729
  hubClient.onMessage(async (msg) => {
1597
1730
  if (msg.type === "message_to_session" && msg.sessionId === sessionId) {
1598
1731
  logger.info(`Message from dashboard: ${msg.content}`);
@@ -1639,7 +1772,7 @@ ${msg.content}` : "";
1639
1772
  process.exit(0);
1640
1773
  });
1641
1774
  }
1642
- var sessionId, sessionName, server, config, hubHost, hubPort, hubToken, hubClient, PermissionRequestSchema;
1775
+ var sessionId, sessionName, peerName, server, config, hubHost, hubPort, hubToken, hubClient, PermissionRequestSchema;
1643
1776
  var init_server2 = __esm({
1644
1777
  "src/channel/server.ts"() {
1645
1778
  "use strict";
@@ -1647,8 +1780,10 @@ var init_server2 = __esm({
1647
1780
  init_constants();
1648
1781
  init_config();
1649
1782
  init_hub_client();
1783
+ init_peer_name2();
1650
1784
  sessionId = randomUUID4();
1651
- sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path5.basename(process.cwd());
1785
+ sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path6.basename(process.cwd());
1786
+ peerName = readPeerName();
1652
1787
  server = new Server(
1653
1788
  {
1654
1789
  name: CHANNEL_SERVER_NAME,
@@ -1671,6 +1806,8 @@ REPLYING:
1671
1806
 
1672
1807
  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.
1673
1808
 
1809
+ 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.
1810
+
1674
1811
  STATUS: Call status("working") before starting a long task, status("waiting_input") when blocked on user input, status("idle") when finished responding.
1675
1812
 
1676
1813
  NOTIFICATIONS:
@@ -1687,7 +1824,8 @@ NOTIFICATIONS:
1687
1824
  sessionName,
1688
1825
  hubHost,
1689
1826
  hubPort,
1690
- hubToken
1827
+ hubToken,
1828
+ () => peerName
1691
1829
  );
1692
1830
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1693
1831
  tools: [
@@ -1824,11 +1962,11 @@ init_constants();
1824
1962
  init_logger();
1825
1963
  init_crash_guard();
1826
1964
  import { spawn } from "child_process";
1827
- import fs5 from "fs";
1828
- import path6 from "path";
1965
+ import fs6 from "fs";
1966
+ import path7 from "path";
1829
1967
  import readline from "readline";
1830
1968
  import { fileURLToPath as fileURLToPath2 } from "url";
1831
- var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
1969
+ var __dirname2 = path7.dirname(fileURLToPath2(import.meta.url));
1832
1970
  function printUsage() {
1833
1971
  console.log(`
1834
1972
  claude-alarm - Monitor Claude Code sessions with notifications
@@ -1849,8 +1987,8 @@ Quick start:
1849
1987
  }
1850
1988
  async function checkForUpdates() {
1851
1989
  try {
1852
- const pkgPath = path6.join(__dirname2, "..", "package.json");
1853
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1990
+ const pkgPath = path7.join(__dirname2, "..", "package.json");
1991
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
1854
1992
  const currentVersion = pkg.version;
1855
1993
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
1856
1994
  signal: AbortSignal.timeout(3e3)
@@ -1873,25 +2011,25 @@ async function hubStart(daemon) {
1873
2011
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1874
2012
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1875
2013
  checkForUpdates();
1876
- if (fs5.existsSync(PID_FILE)) {
1877
- const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
2014
+ if (fs6.existsSync(PID_FILE)) {
2015
+ const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
1878
2016
  if (isProcessRunning(pid)) {
1879
2017
  console.log(`Hub is already running (PID: ${pid}) on http://${displayHost}:${port}`);
1880
2018
  return;
1881
2019
  }
1882
- fs5.unlinkSync(PID_FILE);
2020
+ fs6.unlinkSync(PID_FILE);
1883
2021
  }
1884
2022
  if (daemon) {
1885
2023
  ensureConfigDir();
1886
- const logFd = fs5.openSync(LOG_FILE, "a");
1887
- const hubScript = path6.join(__dirname2, "hub", "server.js");
2024
+ const logFd = fs6.openSync(LOG_FILE, "a");
2025
+ const hubScript = path7.join(__dirname2, "hub", "server.js");
1888
2026
  const child = spawn(process.execPath, [hubScript], {
1889
2027
  detached: true,
1890
2028
  stdio: ["ignore", logFd, logFd],
1891
2029
  env: { ...process.env }
1892
2030
  });
1893
2031
  if (child.pid) {
1894
- fs5.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2032
+ fs6.writeFileSync(PID_FILE, String(child.pid), "utf-8");
1895
2033
  child.unref();
1896
2034
  console.log(`Hub started as daemon (PID: ${child.pid})`);
1897
2035
  console.log(`Dashboard: http://${displayHost}:${port}`);
@@ -1911,11 +2049,11 @@ async function hubStart(daemon) {
1911
2049
  const hub = new HubServer2(config2);
1912
2050
  await hub.start();
1913
2051
  ensureConfigDir();
1914
- fs5.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2052
+ fs6.writeFileSync(PID_FILE, String(process.pid), "utf-8");
1915
2053
  const shutdown = async () => {
1916
2054
  console.log("\nShutting down...");
1917
2055
  await hub.stop();
1918
- if (fs5.existsSync(PID_FILE)) fs5.unlinkSync(PID_FILE);
2056
+ if (fs6.existsSync(PID_FILE)) fs6.unlinkSync(PID_FILE);
1919
2057
  process.exit(0);
1920
2058
  };
1921
2059
  process.on("SIGINT", shutdown);
@@ -1923,18 +2061,18 @@ async function hubStart(daemon) {
1923
2061
  }
1924
2062
  }
1925
2063
  function hubStop() {
1926
- if (!fs5.existsSync(PID_FILE)) {
2064
+ if (!fs6.existsSync(PID_FILE)) {
1927
2065
  console.log("Hub is not running (no PID file found)");
1928
2066
  return;
1929
2067
  }
1930
- const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
2068
+ const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
1931
2069
  try {
1932
2070
  process.kill(pid, "SIGTERM");
1933
2071
  console.log(`Hub stopped (PID: ${pid})`);
1934
2072
  } catch {
1935
2073
  console.log("Hub process not found (may have already stopped)");
1936
2074
  }
1937
- fs5.unlinkSync(PID_FILE);
2075
+ fs6.unlinkSync(PID_FILE);
1938
2076
  }
1939
2077
  async function hubStatus() {
1940
2078
  const config2 = loadConfig();
@@ -1942,8 +2080,8 @@ async function hubStatus() {
1942
2080
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1943
2081
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1944
2082
  let pidInfo = "not running";
1945
- if (fs5.existsSync(PID_FILE)) {
1946
- const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
2083
+ if (fs6.existsSync(PID_FILE)) {
2084
+ const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
1947
2085
  if (isProcessRunning(pid)) {
1948
2086
  pidInfo = `running (PID: ${pid})`;
1949
2087
  } else {
@@ -2015,7 +2153,7 @@ function ask(question) {
2015
2153
  }
2016
2154
  async function init() {
2017
2155
  const dir = process.cwd();
2018
- const projectName = path6.basename(dir);
2156
+ const projectName = path7.basename(dir);
2019
2157
  console.log(`
2020
2158
  claude-alarm init for "${projectName}"
2021
2159
  `);
@@ -2035,11 +2173,11 @@ claude-alarm init for "${projectName}"
2035
2173
  if (port) env.CLAUDE_ALARM_HUB_PORT = port;
2036
2174
  if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
2037
2175
  }
2038
- const mcpPath = path6.join(dir, ".mcp.json");
2176
+ const mcpPath = path7.join(dir, ".mcp.json");
2039
2177
  let mcpConfig = {};
2040
- if (fs5.existsSync(mcpPath)) {
2178
+ if (fs6.existsSync(mcpPath)) {
2041
2179
  try {
2042
- mcpConfig = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
2180
+ mcpConfig = JSON.parse(fs6.readFileSync(mcpPath, "utf-8"));
2043
2181
  } catch {
2044
2182
  mcpConfig = {};
2045
2183
  }
@@ -2050,7 +2188,7 @@ claude-alarm init for "${projectName}"
2050
2188
  args: ["-y", "@delt/claude-alarm", "serve"],
2051
2189
  env
2052
2190
  };
2053
- fs5.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2191
+ fs6.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2054
2192
  console.log(`
2055
2193
  \u2713 Created ${mcpPath}`);
2056
2194
  if (remote.toLowerCase() !== "y") {