@delt/claude-alarm 0.8.1 → 0.8.2

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.
@@ -617,12 +617,35 @@
617
617
  border-radius: 5px;
618
618
  padding: 6px 10px;
619
619
  margin-top: 5px;
620
- max-height: 60px;
621
- overflow-y: auto;
620
+ max-height: 220px;
621
+ overflow: auto;
622
622
  white-space: pre-wrap;
623
623
  word-break: break-all;
624
624
  line-height: 1.4;
625
625
  }
626
+ .perm-preview pre {
627
+ margin: 0;
628
+ white-space: pre;
629
+ font-family: inherit;
630
+ font-size: inherit;
631
+ }
632
+ .perm-preview code { font-family: inherit; }
633
+ .perm-preview.prose {
634
+ font-family: inherit;
635
+ white-space: normal;
636
+ word-break: normal;
637
+ }
638
+ .perm-preview.prose pre,
639
+ .perm-preview.prose code { font-family: monospace; }
640
+ .perm-preview.prose ul { margin: 4px 0; padding-left: 20px; }
641
+ .perm-preview.prose li { margin: 2px 0; }
642
+ .perm-preview.prose strong { font-weight: 600; }
643
+ .perm-preview.prose h2,
644
+ .perm-preview.prose h3,
645
+ .perm-preview.prose h4 { font-size: 12px; font-weight: 600; margin: 4px 0; }
646
+ .perm-preview.prose table { border-collapse: collapse; margin: 4px 0; font-size: 11px; }
647
+ .perm-preview.prose th,
648
+ .perm-preview.prose td { border: 1px solid var(--border); padding: 3px 6px; text-align: left; }
626
649
  .perm-actions {
627
650
  display: flex;
628
651
  gap: 10px;
@@ -1670,9 +1693,26 @@
1670
1693
  return toolName;
1671
1694
  }
1672
1695
 
1673
- // Returns { text } — dashboard shows full content, no truncation
1696
+ const LANG_BY_EXT = {
1697
+ js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
1698
+ ts: 'typescript', tsx: 'typescript', py: 'python', rb: 'ruby', go: 'go',
1699
+ java: 'java', kt: 'kotlin', rs: 'rust', php: 'php', cs: 'csharp',
1700
+ sh: 'bash', bash: 'bash', zsh: 'bash', ps1: 'powershell',
1701
+ json: 'json', yml: 'yaml', yaml: 'yaml', xml: 'xml', sql: 'sql',
1702
+ md: 'markdown', html: 'html', vue: 'html', css: 'css', scss: 'scss',
1703
+ };
1704
+ function langOf(path) {
1705
+ const m = /\.([a-z0-9]+)$/i.exec(path || '');
1706
+ return m ? (LANG_BY_EXT[m[1].toLowerCase()] || '') : '';
1707
+ }
1708
+
1709
+ // Returns { text, kind, lang } — dashboard shows full content, no truncation.
1710
+ // kind 'code' must stay byte-exact: markdown rendering would swallow the ** and `
1711
+ // characters that the approver needs to see to judge what actually gets executed.
1712
+ // Only notify-style messages (prose Claude wrote) are safe to render as markdown.
1674
1713
  function formatPermPreview(toolName, raw) {
1675
- if (!raw) return { text: '' };
1714
+ if (!raw) return { text: '', kind: 'code', lang: '' };
1715
+ const isNotify = toolName === 'notify' || toolName.endsWith('__notify');
1676
1716
  let p = null;
1677
1717
  try { p = JSON.parse(raw); } catch {
1678
1718
  // Truncated JSON - extract known fields with regex (closing quote optional for truncated strings)
@@ -1682,43 +1722,52 @@
1682
1722
  const contentMatch = raw.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"?/);
1683
1723
  const titleMatch = raw.match(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"?/);
1684
1724
  const msgMatch = raw.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"?/);
1685
- if (titleMatch && msgMatch) return { text: msgMatch[1] };
1686
- if (cmdMatch) return { text: '$ ' + cmdMatch[1] };
1687
- if (fileMatch) return { text: fileMatch[1] };
1688
- if (patternMatch) return { text: patternMatch[1] };
1689
- if (contentMatch) return { text: contentMatch[1] };
1690
- return { text: raw };
1725
+ if (titleMatch && msgMatch) return { text: msgMatch[1], kind: 'prose', lang: '' };
1726
+ if (cmdMatch) return { text: '$ ' + cmdMatch[1], kind: 'code', lang: 'bash' };
1727
+ if (fileMatch) return { text: fileMatch[1], kind: 'code', lang: langOf(fileMatch[1]) };
1728
+ if (patternMatch) return { text: patternMatch[1], kind: 'code', lang: '' };
1729
+ if (contentMatch) return { text: contentMatch[1], kind: 'code', lang: '' };
1730
+ return { text: raw, kind: 'code', lang: '' };
1691
1731
  }
1692
1732
  let text;
1733
+ let kind = 'code';
1734
+ let lang = '';
1693
1735
  switch (toolName) {
1694
1736
  case 'Bash':
1695
- text = p.command ? '$ ' + p.command : raw; break;
1737
+ text = p.command ? '$ ' + p.command : raw; lang = 'bash'; break;
1696
1738
  case 'Write':
1697
1739
  case 'Edit':
1698
1740
  if (p.file_path) {
1699
1741
  text = p.file_path;
1742
+ lang = langOf(p.file_path);
1700
1743
  if (p.new_string) text += '\n+ ' + p.new_string;
1701
1744
  else if (p.content) text += '\n' + p.content;
1702
1745
  } else { text = raw; }
1703
1746
  break;
1704
1747
  case 'Read':
1705
- text = p.file_path || raw; break;
1748
+ text = p.file_path || raw; lang = langOf(p.file_path); break;
1706
1749
  case 'Glob':
1707
1750
  text = p.pattern || raw; break;
1708
1751
  case 'Grep':
1709
1752
  text = (p.pattern || '') + (p.path ? ' in ' + p.path : ''); break;
1710
1753
  default:
1711
1754
  if (p.title && p.message) {
1712
- text = p.message;
1755
+ text = p.message; kind = 'prose';
1713
1756
  } else if (p.content && typeof p.content === 'string') {
1714
- text = p.content;
1757
+ text = p.content; kind = isNotify ? 'prose' : 'code';
1715
1758
  } else {
1716
1759
  const entries = Object.entries(p);
1717
1760
  if (entries.length <= 3) text = entries.map(([k, v]) => k + ': ' + String(v)).join('\n');
1718
1761
  else text = JSON.stringify(p, null, 1);
1719
1762
  }
1720
1763
  }
1721
- return { text: text || '' };
1764
+ return { text: text || '', kind, lang };
1765
+ }
1766
+
1767
+ function renderPermPreview(text, kind, lang) {
1768
+ if (kind === 'prose') return `<div class="perm-preview prose">${renderMarkdown(text)}</div>`;
1769
+ const cls = lang ? ` class="lang-${esc(lang)}"` : '';
1770
+ return `<div class="perm-preview"><pre><code${cls}>${esc(text)}</code></pre></div>`;
1722
1771
  }
1723
1772
 
1724
1773
  function sendPermissionVerdict(sessionId, requestId, behavior) {
@@ -1742,7 +1791,7 @@
1742
1791
  if (!reqs.length) { bar.classList.remove('active'); bar.innerHTML = ''; return; }
1743
1792
  bar.classList.add('active');
1744
1793
  bar.innerHTML = reqs.map(r => {
1745
- const { text: previewText } = formatPermPreview(r.toolName, r.inputPreview);
1794
+ const { text: previewText, kind: previewKind, lang: previewLang } = formatPermPreview(r.toolName, r.inputPreview);
1746
1795
  const displayName = formatToolName(r.toolName, r.inputPreview);
1747
1796
  return `<div class="perm-item">
1748
1797
  <div class="perm-icon">&#9888;</div>
@@ -1750,7 +1799,7 @@
1750
1799
  <div class="perm-header">
1751
1800
  <span class="perm-tool">${esc(displayName)}</span>
1752
1801
  </div>
1753
- ${previewText ? `<div class="perm-preview">${esc(previewText)}</div>` : ''}
1802
+ ${previewText ? renderPermPreview(previewText, previewKind, previewLang) : ''}
1754
1803
  </div>
1755
1804
  <div class="perm-actions">
1756
1805
  <button class="perm-allow" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Allow <span class="perm-kbd">(Enter)</span></button>
@@ -210,7 +210,7 @@ import fs from "fs";
210
210
  import path2 from "path";
211
211
  import { randomUUID } from "crypto";
212
212
  var TELEGRAM_API = "https://api.telegram.org/bot";
213
- var TelegramBot = class {
213
+ var TelegramBot = class _TelegramBot {
214
214
  config;
215
215
  offset = 0;
216
216
  polling = false;
@@ -442,33 +442,45 @@ ${this.mdToHtml(message)}`;
442
442
  }
443
443
  /** Send a permission request with inline buttons */
444
444
  async sendPermissionRequest(sessionId, sessionLabel, requestId, toolName, description, inputPreview) {
445
+ const isNotify = toolName.endsWith("__notify") || toolName === "notify";
445
446
  let preview = inputPreview;
447
+ let kind = "code";
448
+ let lang = "";
446
449
  let truncated = false;
447
450
  try {
448
451
  const p = JSON.parse(inputPreview);
449
- if (p.command) preview = `$ ${p.command}`;
450
- else if (p.title && p.message) preview = p.message;
451
- else if (p.file_path) {
452
+ if (p.command) {
453
+ preview = `$ ${p.command}`;
454
+ lang = "bash";
455
+ } else if (p.title && p.message) {
456
+ preview = p.message;
457
+ kind = "prose";
458
+ } else if (p.file_path) {
452
459
  preview = p.file_path;
460
+ lang = this.langOf(p.file_path);
453
461
  if (p.content) {
454
462
  preview += "\n" + p.content.slice(0, 3e3);
455
463
  if (p.content.length > 500) truncated = true;
456
464
  }
457
465
  } else if (p.content && typeof p.content === "string") {
458
466
  preview = p.content.slice(0, 3e3);
467
+ if (isNotify) kind = "prose";
459
468
  if (p.content.length > 500) truncated = true;
460
469
  }
461
470
  } catch {
462
471
  truncated = true;
463
472
  const cmdMatch = inputPreview.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
464
473
  const contentMatch = inputPreview.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"/);
465
- if (cmdMatch) preview = `$ ${cmdMatch[1]}`;
466
- else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
474
+ if (cmdMatch) {
475
+ preview = `$ ${cmdMatch[1]}`;
476
+ lang = "bash";
477
+ } else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
467
478
  }
468
479
  const truncNote = truncated ? "\n\n<i>...truncated</i>" : "";
469
480
  const previewSlice = preview.slice(0, 3e3);
470
481
  const isShort = !previewSlice.includes("\n") && previewSlice.length < 100;
471
- const previewHtml = isShort ? `<code>${this.escHtml(previewSlice)}</code>` : this.escHtml(previewSlice);
482
+ const langAttr = lang ? ` class="language-${lang}"` : "";
483
+ const previewHtml = kind === "prose" ? this.mdToHtml(previewSlice) : isShort ? `<code>${this.escHtml(previewSlice)}</code>` : `<pre><code${langAttr}>${this.escHtml(previewSlice)}</code></pre>`;
472
484
  let displayTool = toolName;
473
485
  if ((toolName.endsWith("__notify") || toolName === "notify") && inputPreview) {
474
486
  try {
@@ -568,6 +580,40 @@ ${label}`);
568
580
  escHtml(s) {
569
581
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
570
582
  }
583
+ static LANG_BY_EXT = {
584
+ js: "javascript",
585
+ mjs: "javascript",
586
+ cjs: "javascript",
587
+ jsx: "javascript",
588
+ ts: "typescript",
589
+ tsx: "typescript",
590
+ py: "python",
591
+ rb: "ruby",
592
+ go: "go",
593
+ java: "java",
594
+ kt: "kotlin",
595
+ rs: "rust",
596
+ php: "php",
597
+ cs: "csharp",
598
+ sh: "bash",
599
+ bash: "bash",
600
+ zsh: "bash",
601
+ ps1: "powershell",
602
+ json: "json",
603
+ yml: "yaml",
604
+ yaml: "yaml",
605
+ xml: "xml",
606
+ sql: "sql",
607
+ md: "markdown",
608
+ html: "html",
609
+ vue: "html",
610
+ css: "css",
611
+ scss: "scss"
612
+ };
613
+ langOf(path5) {
614
+ const m = /\.([a-z0-9]+)$/i.exec(path5 || "");
615
+ return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
616
+ }
571
617
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
572
618
  mdToHtml(s) {
573
619
  let text = s.replace(