pyre-cli 6.0.0 → 7.0.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/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs5 from "fs";
5
- import os3 from "os";
6
- import path3 from "path";
4
+ import fs9 from "fs";
5
+ import os5 from "os";
6
+ import path6 from "path";
7
7
  import readline2 from "readline";
8
8
  import { Command } from "commander";
9
- import chalk9 from "chalk";
9
+ import chalk13 from "chalk";
10
10
 
11
11
  // src/monitors/collectors.ts
12
12
  import chalk from "chalk";
@@ -472,9 +472,41 @@ async function collectMemory() {
472
472
  usagePercent
473
473
  };
474
474
  }
475
+ var cachedDiskIo = null;
476
+ async function getDiskIoRates() {
477
+ const now = Date.now();
478
+ if (cachedDiskIo && now - cachedDiskIo.ts < 1e3) {
479
+ return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
480
+ }
481
+ try {
482
+ const raw = (await run("iostat -d -c 2 1 2", "")).trim();
483
+ const blocks = raw.split(/\n\s*\n/).filter(Boolean);
484
+ const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
485
+ if (targetBlock) {
486
+ const lines = targetBlock.trim().split("\n");
487
+ if (lines.length >= 2) {
488
+ const dataLine = lines[lines.length - 1].trim();
489
+ const parts = dataLine.split(/\s+/);
490
+ if (parts.length >= 3) {
491
+ const mbSec = parseFloat(parts[2]);
492
+ if (!isNaN(mbSec)) {
493
+ const bytesSec = Math.round(mbSec * 1024 * 1024);
494
+ cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
495
+ return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
496
+ }
497
+ }
498
+ }
499
+ }
500
+ } catch {
501
+ }
502
+ return { readBytesSec: 0, writeBytesSec: 0 };
503
+ }
475
504
  async function collectDisk() {
476
- const raw = (await run("df -h")).trim();
477
- const lines = raw.split("\n").slice(1);
505
+ const [rawDf, ioRates] = await Promise.all([
506
+ run("df -h"),
507
+ getDiskIoRates()
508
+ ]);
509
+ const lines = rawDf.trim().split("\n").slice(1);
478
510
  return lines.map((line) => {
479
511
  const parts = line.split(/\s+/);
480
512
  return {
@@ -483,7 +515,9 @@ async function collectDisk() {
483
515
  used: parts[2],
484
516
  available: parts[3],
485
517
  capacity: parts[4],
486
- mountpoint: parts[5] || parts[6] || "/"
518
+ mountpoint: parts[5] || parts[6] || "/",
519
+ readBytesSec: ioRates.readBytesSec,
520
+ writeBytesSec: ioRates.writeBytesSec
487
521
  };
488
522
  }).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
489
523
  }
@@ -788,10 +822,39 @@ function multiRowSparkline(values, height, opts2 = {}) {
788
822
  }
789
823
  return rows.map((r) => r.join(""));
790
824
  }
825
+ function brailleGraph(values, opts2 = {}) {
826
+ if (!values.length) return "";
827
+ let min = opts2.min ?? Infinity;
828
+ let max = opts2.max ?? -Infinity;
829
+ if (opts2.min === void 0 || opts2.max === void 0) {
830
+ for (const v of values) {
831
+ if (v < min) min = v;
832
+ if (v > max) max = v;
833
+ }
834
+ }
835
+ const range = max - min || 1;
836
+ const dotsCol0 = [64, 4, 2, 1];
837
+ const dotsCol1 = [128, 32, 16, 8];
838
+ let result = "";
839
+ for (let i = 0; i < values.length; i += 2) {
840
+ const v0 = values[i];
841
+ const v1 = i + 1 < values.length ? values[i + 1] : v0;
842
+ const fill0 = Math.min(4, Math.max(0, Math.round((v0 - min) / range * 4)));
843
+ const fill1 = Math.min(4, Math.max(0, Math.round((v1 - min) / range * 4)));
844
+ let code = 10240;
845
+ for (let d = 0; d < fill0; d++) code |= dotsCol0[d];
846
+ for (let d = 0; d < fill1; d++) code |= dotsCol1[d];
847
+ result += String.fromCharCode(code);
848
+ }
849
+ return result;
850
+ }
791
851
 
792
852
  // src/formatters/themes.ts
793
853
  import chalk2 from "chalk";
794
- var THEMES = {
854
+ import fs from "fs";
855
+ import os2 from "os";
856
+ import path from "path";
857
+ var BUILTIN_THEMES = {
795
858
  default: {
796
859
  border: chalk2.hex("#4b5563"),
797
860
  cpu: chalk2.hex("#22d3ee"),
@@ -871,6 +934,42 @@ var THEMES = {
871
934
  process: chalk2.hex("#ebdbb2")
872
935
  }
873
936
  };
937
+ function loadCustomThemes() {
938
+ const themes = { ...BUILTIN_THEMES };
939
+ const themesDir = path.join(os2.homedir(), ".config", "pyre", "themes");
940
+ try {
941
+ if (fs.existsSync(themesDir)) {
942
+ const files = fs.readdirSync(themesDir);
943
+ for (const file of files) {
944
+ if (file.endsWith(".json")) {
945
+ const name = path.basename(file, ".json");
946
+ try {
947
+ const raw = fs.readFileSync(path.join(themesDir, file), "utf-8");
948
+ const data = JSON.parse(raw);
949
+ const buildColor = (val) => val ? val.startsWith("#") ? chalk2.hex(val) : chalk2[val] || chalk2.white : chalk2.white;
950
+ themes[name] = {
951
+ border: buildColor(data.border),
952
+ cpu: buildColor(data.cpu),
953
+ mem: buildColor(data.mem),
954
+ gpu: buildColor(data.gpu),
955
+ power: buildColor(data.power),
956
+ battery: buildColor(data.battery),
957
+ thermal: buildColor(data.thermal),
958
+ network: buildColor(data.network),
959
+ disk: buildColor(data.disk),
960
+ graphs: buildColor(data.graphs),
961
+ process: buildColor(data.process)
962
+ };
963
+ } catch {
964
+ }
965
+ }
966
+ }
967
+ }
968
+ } catch {
969
+ }
970
+ return themes;
971
+ }
972
+ var THEMES = loadCustomThemes();
874
973
 
875
974
  // src/formatters/render.ts
876
975
  function visLen(s) {
@@ -926,8 +1025,8 @@ function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
926
1025
  } else if (visLen(topInner) < innerW) {
927
1026
  topInner = topInner + "\u2500".repeat(innerW - visLen(topInner));
928
1027
  }
929
- const top = borderAccent("\u250C" + topInner + "\u2510");
930
- const bottom = borderAccent("\u2514" + "\u2500".repeat(innerW) + "\u2518");
1028
+ const top = borderAccent("\u256D" + topInner + "\u256E");
1029
+ const bottom = borderAccent("\u2570" + "\u2500".repeat(innerW) + "\u256F");
931
1030
  const body = [];
932
1031
  for (let i = 0; i < h; i++) {
933
1032
  const line = lines[i] ?? "";
@@ -938,12 +1037,17 @@ function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
938
1037
  function gaugeBar(percent, width = 12) {
939
1038
  const w = Math.max(1, width);
940
1039
  const pct = Math.min(100, Math.max(0, percent));
941
- const filled = Math.round(pct / 100 * w);
942
- const empty = w - filled;
943
- const filledStr = "\u2588".repeat(filled);
944
- const emptyStr = "\u2591".repeat(empty);
1040
+ const totalEighths = Math.round(pct / 100 * w * 8);
1041
+ const fullBlocks = Math.floor(totalEighths / 8);
1042
+ const remainder = totalEighths % 8;
1043
+ const blocks = [" ", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589", "\u2588"];
1044
+ let str = "\u2588".repeat(fullBlocks);
1045
+ if (fullBlocks < w && remainder > 0) {
1046
+ str += blocks[remainder];
1047
+ }
1048
+ const empty = Math.max(0, w - visLen(str));
945
1049
  const color = pct > 85 ? chalk3.red : pct > 65 ? chalk3.yellow : chalk3.cyan;
946
- return color(filledStr) + chalk3.dim(emptyStr);
1050
+ return color(str) + chalk3.dim("\u2500".repeat(empty));
947
1051
  }
948
1052
  function hstack(blocks, gap = 2, targetWidth) {
949
1053
  if (!blocks.length) return [];
@@ -974,11 +1078,18 @@ function gaugeRow(label, percent, contentWidth) {
974
1078
  const labelW = 9;
975
1079
  const pctText = `${Math.round(percent)}%`;
976
1080
  const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
977
- const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * barW);
978
- const empty = barW - filled;
979
- const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
980
- const color = percent > 90 ? chalk3.red : percent > 70 ? chalk3.yellow : chalk3.green;
981
- return `${chalk3.dim(label.padEnd(labelW))}${color(barStr)} ${pctText}`;
1081
+ const pct = Math.min(100, Math.max(0, percent));
1082
+ const totalEighths = Math.round(pct / 100 * barW * 8);
1083
+ const fullBlocks = Math.floor(totalEighths / 8);
1084
+ const remainder = totalEighths % 8;
1085
+ const blocks = [" ", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589", "\u2588"];
1086
+ let str = "\u2588".repeat(fullBlocks);
1087
+ if (fullBlocks < barW && remainder > 0) {
1088
+ str += blocks[remainder];
1089
+ }
1090
+ const empty = Math.max(0, barW - visLen(str));
1091
+ const color = pct > 90 ? chalk3.red : pct > 70 ? chalk3.yellow : chalk3.green;
1092
+ return `${chalk3.dim(label.padEnd(labelW))}${color(str)}${chalk3.dim("\u2500".repeat(empty))} ${pctText}`;
982
1093
  }
983
1094
  function thermalColor(pressureLevel) {
984
1095
  if (pressureLevel >= 3) return chalk3.red;
@@ -1134,7 +1245,8 @@ var TAB_DEFS = [
1134
1245
  { id: "tasks", label: "Tasks", key: "9" },
1135
1246
  { id: "disk", label: "Disk", key: "0" },
1136
1247
  { id: "process", label: "Process", key: "P" },
1137
- { id: "p2p", label: "P2P", key: "R" }
1248
+ { id: "p2p", label: "P2P", key: "R" },
1249
+ { id: "anomalies", label: "Anomalies", key: "A" }
1138
1250
  ];
1139
1251
  var TAB_BAR_INDENT = 2;
1140
1252
  var TAB_SEPARATOR_WIDTH = 3;
@@ -1362,6 +1474,19 @@ function activeDetailLines(data, activePanel, width, opts2 = {}) {
1362
1474
  }
1363
1475
  return processTableLines(limited, width - 4, theme.border);
1364
1476
  }
1477
+ case "anomalies": {
1478
+ if (!opts2.anomalyHistory || opts2.anomalyHistory.length === 0) return ["No anomalies detected yet."];
1479
+ const lines = [];
1480
+ const sorted = [...opts2.anomalyHistory].sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0));
1481
+ for (const a of sorted) {
1482
+ const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
1483
+ const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
1484
+ const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
1485
+ const timeStr = a.timestamp ? a.timestamp.toLocaleTimeString() : "";
1486
+ lines.push(color(` ${timeStr.padEnd(10)} \u26A0 ${a.metric.padEnd(8)}: ${valStr.padEnd(12)} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
1487
+ }
1488
+ return lines;
1489
+ }
1365
1490
  default:
1366
1491
  return null;
1367
1492
  }
@@ -1405,7 +1530,7 @@ function btopCpuBox(data, width, theme, opts2) {
1405
1530
  if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
1406
1531
  const coreColW = Math.floor((rightW - 1) / 2);
1407
1532
  const half = Math.ceil(data.cpu.coreUsage.length / 2);
1408
- for (let i = 0; i < Math.min(2, half); i++) {
1533
+ for (let i = 0; i < half; i++) {
1409
1534
  const u1 = data.cpu.coreUsage[i];
1410
1535
  const u2 = data.cpu.coreUsage[i + half];
1411
1536
  const c1BarW = Math.max(3, coreColW - 8);
@@ -1424,7 +1549,9 @@ function btopCpuBox(data, width, theme, opts2) {
1424
1549
  lines.push(chalk3.bold(truncatePlain(data.cpu.brand, contentWidth)));
1425
1550
  lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
1426
1551
  if (opts2.history?.cpuUsage.length) {
1427
- lines.push(theme.cpu(sparkline(opts2.history.cpuUsage, { min: 0, max: 100 })));
1552
+ const gMode = opts2.graphMode || "spark";
1553
+ const gStr = gMode === "bar" ? sparkline(opts2.history.cpuUsage, { min: 0, max: 100 }) : brailleGraph(opts2.history.cpuUsage, { min: 0, max: 100 });
1554
+ lines.push(theme.cpu(gStr));
1428
1555
  }
1429
1556
  lines.push(`Load: ${data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ")}`);
1430
1557
  return panel("1 cpu", lines, width, theme.cpu, theme.border, lines.length, rightTitle);
@@ -1433,36 +1560,54 @@ function btopCpuBox(data, width, theme, opts2) {
1433
1560
  function btopLeftColumn(data, width, theme, opts2) {
1434
1561
  const contentWidth = width - 4;
1435
1562
  const out = [];
1436
- const memLines = [];
1437
- memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
1438
- memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
1439
- memLines.push(`Free: ${formatBytes(data.memory.free)} \xB7 Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
1440
- if (data.memory.swapTotal > 0) {
1441
- const swapPct = Math.round(data.memory.swapUsed / data.memory.swapTotal * 100);
1442
- memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
1443
- }
1444
- out.push(...panel("2 mem", memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
1445
- if (data.disk.length) {
1446
- const diskLines = [];
1447
- for (const d of data.disk.slice(0, 2)) {
1448
- const capNum = parseInt(d.capacity, 10) || 0;
1449
- const mountShort = truncatePlain(d.mountpoint, 8);
1450
- diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18))}] ${d.capacity}`);
1451
- }
1452
- out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length));
1453
- }
1454
- const netLines = [];
1455
- const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
1456
- const txRate = opts2.history?.netTxRate.length ? opts2.history.netTxRate[opts2.history.netTxRate.length - 1] : 0;
1457
- netLines.push(`down \u25BC ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
1458
- if (opts2.history?.netRxRate.length) {
1459
- netLines.push(chalk3.cyan(fitVisible(sparkline(opts2.history.netRxRate, {}), contentWidth)));
1460
- }
1461
- netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
1462
- if (opts2.history?.netTxRate.length) {
1463
- netLines.push(theme.network(fitVisible(sparkline(opts2.history.netTxRate, {}), contentWidth)));
1464
- }
1465
- out.push(...panel("7 net", netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
1563
+ const layout = opts2.panelLayout || ["mem", "disk", "net"];
1564
+ for (const panelName of layout) {
1565
+ if (panelName === "mem") {
1566
+ const memLines = [];
1567
+ memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
1568
+ memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
1569
+ memLines.push(`Free: ${formatBytes(data.memory.free)} \xB7 Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
1570
+ if (data.memory.swapTotal > 0) {
1571
+ const swapPct = Math.round(data.memory.swapUsed / data.memory.swapTotal * 100);
1572
+ memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
1573
+ }
1574
+ out.push(...panel("2 mem", memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
1575
+ } else if (panelName === "disk" && data.disk.length) {
1576
+ const diskLines = [];
1577
+ for (const d of data.disk.slice(0, 2)) {
1578
+ const capNum = parseInt(d.capacity, 10) || 0;
1579
+ const mountShort = truncatePlain(d.mountpoint, 8);
1580
+ let ioStr = "";
1581
+ if (d.readBytesSec !== void 0 || d.writeBytesSec !== void 0) {
1582
+ const rStr = formatBytes(d.readBytesSec || 0);
1583
+ const wStr = formatBytes(d.writeBytesSec || 0);
1584
+ ioStr = ` R:${rStr}/s W:${wStr}/s`;
1585
+ }
1586
+ diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18 - ioStr.length))}] ${d.capacity}${ioStr}`);
1587
+ }
1588
+ const totalRead = data.disk.reduce((acc, d) => acc + (d.readBytesSec || 0), 0);
1589
+ const totalWrite = data.disk.reduce((acc, d) => acc + (d.writeBytesSec || 0), 0);
1590
+ const diskHeaderRight = `R:${formatBytes(totalRead)}/s W:${formatBytes(totalWrite)}/s`;
1591
+ out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length, diskHeaderRight));
1592
+ } else if (panelName === "net") {
1593
+ const netLines = [];
1594
+ const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
1595
+ const txRate = opts2.history?.netTxRate.length ? opts2.history.netTxRate[opts2.history.netTxRate.length - 1] : 0;
1596
+ netLines.push(`down \u25BC ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
1597
+ if (opts2.history?.netRxRate.length) {
1598
+ const gMode = opts2.graphMode || "spark";
1599
+ const gStr = gMode === "bar" ? sparkline(opts2.history.netRxRate, {}) : brailleGraph(opts2.history.netRxRate, {});
1600
+ netLines.push(chalk3.cyan(fitVisible(gStr, contentWidth)));
1601
+ }
1602
+ netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
1603
+ if (opts2.history?.netTxRate.length) {
1604
+ const gMode = opts2.graphMode || "spark";
1605
+ const gStr = gMode === "bar" ? sparkline(opts2.history.netTxRate, {}) : brailleGraph(opts2.history.netTxRate, {});
1606
+ netLines.push(theme.network(fitVisible(gStr, contentWidth)));
1607
+ }
1608
+ out.push(...panel("7 net", netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
1609
+ }
1610
+ }
1466
1611
  return out;
1467
1612
  }
1468
1613
  function btopProcBox(data, width, targetHeight, theme, opts2) {
@@ -1689,21 +1834,118 @@ function formatHtml(data) {
1689
1834
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1690
1835
  <title>pyre \u2014 ${data.header.hostname}</title>
1691
1836
  <style>
1692
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 0; padding: 24px; }
1693
- h1 { color: #ff6a39; margin-bottom: 4px; }
1694
- .meta { color: #a6adc8; margin-bottom: 24px; }
1695
- h2 { color: #89b4fa; margin-top: 28px; border-bottom: 1px solid #313244; padding-bottom: 6px; }
1696
- table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
1697
- td { padding: 6px 10px; border-bottom: 1px solid #313244; vertical-align: top; }
1698
- td.label { color: #a6adc8; width: 22%; font-weight: 500; }
1699
- td.value { color: #cdd6f4; }
1837
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0908; color: #efe9dc; margin: 0; padding: 24px; }
1838
+ h1 { color: #ff6a39; margin-bottom: 4px; font-weight: 800; letter-spacing: -0.02em; }
1839
+ .meta { color: #9c9384; margin-bottom: 24px; font-size: 0.9rem; }
1840
+ h2 { color: #ffb130; margin-top: 28px; border-bottom: 1px solid #211d18; padding-bottom: 6px; font-size: 1.1rem; }
1841
+ table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-family: monospace; }
1842
+ td { padding: 8px 12px; border-bottom: 1px solid #1a1712; vertical-align: top; font-size: 0.9rem; }
1843
+ td.label { color: #9c9384; width: 22%; font-weight: 600; }
1844
+ td.value { color: #efe9dc; }
1700
1845
  tr:last-child td { border-bottom: none; }
1701
1846
  </style>
1702
1847
  </head>
1703
1848
  <body>
1704
- <h1>PYRE</h1>
1705
- <div class="meta">${escapeHtml(data.header.hostname)} \xB7 ${escapeHtml(data.header.os)} \xB7 up ${escapeHtml(data.header.uptime)} \xB7 ${escapeHtml(data.timestamp)}</div>
1849
+ <h1>PYRE <span style="font-size:0.5em; color:#82c774; font-weight:normal;">\u25CF LIVE PORTAL</span></h1>
1850
+ <div class="meta" id="header-meta">${escapeHtml(data.header.hostname)} \xB7 ${escapeHtml(data.header.os)} \xB7 up ${escapeHtml(data.header.uptime)} \xB7 <span id="timestamp-val">${escapeHtml(data.timestamp)}</span></div>
1851
+ <div id="portal-content">
1706
1852
  ${sections.join("\n ")}
1853
+ </div>
1854
+
1855
+ <script>
1856
+ function renderStats(data) {
1857
+ if (!data) return;
1858
+ const tsEl = document.getElementById('timestamp-val');
1859
+ if (tsEl && data.timestamp) tsEl.textContent = data.timestamp;
1860
+
1861
+ function formatBytes(bytes) {
1862
+ if (!bytes || bytes < 0) return '0 B';
1863
+ if (bytes < 1024) return bytes + ' B';
1864
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
1865
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
1866
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
1867
+ }
1868
+
1869
+ function row(label, val) {
1870
+ return '<tr><td class="label">' + label + '</td><td class="value">' + val + '</td></tr>';
1871
+ }
1872
+
1873
+ let html = '';
1874
+
1875
+ // CPU & Memory
1876
+ html += '<h2>CPU & Memory</h2><table>';
1877
+ if (data.cpu) {
1878
+ html += row('Brand', data.cpu.brand || 'Unknown');
1879
+ html += row('Cores', (data.cpu.physicalCores || '') + '/' + (data.cpu.cores || '') + ' (phys/log)');
1880
+ html += row('Usage', (data.cpu.usage || 0) + '%');
1881
+ if (data.cpu.loadAvg) html += row('Load Avg', data.cpu.loadAvg.map(l => l.toFixed(2)).join(' '));
1882
+ if (data.cpu.temperature) html += row('Temperature', data.cpu.temperature.toFixed(1) + '\xB0C');
1883
+ }
1884
+ if (data.memory) {
1885
+ html += row('Memory Usage', data.memory.usagePercent + '% (' + formatBytes(data.memory.used) + ' / ' + formatBytes(data.memory.total) + ')');
1886
+ html += row('Swap Used', formatBytes(data.memory.swapUsed) + ' / ' + formatBytes(data.memory.swapTotal));
1887
+ }
1888
+ html += '</table>';
1889
+
1890
+ // GPU
1891
+ if (data.gpu) {
1892
+ html += '<h2>GPU</h2><table>';
1893
+ html += row('Model', data.gpu.model || '');
1894
+ html += row('Utilization', (data.gpu.utilization || 0) + '%');
1895
+ html += row('Memory', formatBytes(data.gpu.memory));
1896
+ if (data.gpu.temperature) html += row('Temperature', data.gpu.temperature.toFixed(1) + '\xB0C');
1897
+ html += '</table>';
1898
+ }
1899
+
1900
+ // Network
1901
+ if (data.network) {
1902
+ html += '<h2>Network</h2><table>';
1903
+ html += row('Interface', (data.network.interface || '') + ' (' + (data.network.ip || '') + ')');
1904
+ html += row('RX', formatBytes(data.network.rxBytes) + ' / ' + (data.network.rxPackets || 0) + ' pkt');
1905
+ html += row('TX', formatBytes(data.network.txBytes) + ' / ' + (data.network.txPackets || 0) + ' pkt');
1906
+ html += '</table>';
1907
+ }
1908
+
1909
+ // Disk
1910
+ if (data.disk && data.disk.length) {
1911
+ html += '<h2>Disk</h2><table>';
1912
+ data.disk.forEach(d => {
1913
+ let io = '';
1914
+ if (d.readBytesSec !== undefined || d.writeBytesSec !== undefined) {
1915
+ io = ' | R: ' + formatBytes(d.readBytesSec || 0) + '/s W: ' + formatBytes(d.writeBytesSec || 0) + '/s';
1916
+ }
1917
+ html += row(d.mountpoint, d.used + ' / ' + d.size + ' (' + d.capacity + ')' + io);
1918
+ });
1919
+ html += '</table>';
1920
+ }
1921
+
1922
+ // Processes
1923
+ if (data.processes && data.processes.length) {
1924
+ html += '<h2>Processes (Top ' + Math.min(50, data.processes.length) + ')</h2><table>';
1925
+ data.processes.slice(0, 50).forEach(p => {
1926
+ html += row(String(p.pid), p.user + ' &nbsp; CPU: ' + p.cpu.toFixed(1) + '% &nbsp; MEM: ' + p.mem.toFixed(1) + '% &nbsp; ' + p.command);
1927
+ });
1928
+ html += '</table>';
1929
+ }
1930
+
1931
+ const contentEl = document.getElementById('portal-content');
1932
+ if (contentEl) contentEl.innerHTML = html;
1933
+ }
1934
+
1935
+ if (window.EventSource) {
1936
+ const evtSource = new EventSource('/api/stream');
1937
+ evtSource.onmessage = function(event) {
1938
+ try {
1939
+ const data = JSON.parse(event.data);
1940
+ renderStats(data);
1941
+ } catch(e) {}
1942
+ };
1943
+ } else {
1944
+ setInterval(function() {
1945
+ fetch('/api').then(r => r.json()).then(renderStats).catch(function(){});
1946
+ }, 2000);
1947
+ }
1948
+ </script>
1707
1949
  </body>
1708
1950
  </html>`;
1709
1951
  }
@@ -2152,11 +2394,11 @@ var History = class _History {
2152
2394
  };
2153
2395
 
2154
2396
  // src/state/config.ts
2155
- import fs from "fs";
2156
- import os2 from "os";
2157
- import path from "path";
2158
- var CONFIG_DIR = path.join(os2.homedir(), ".config", "pyre");
2159
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
2397
+ import fs2 from "fs";
2398
+ import os3 from "os";
2399
+ import path2 from "path";
2400
+ var CONFIG_DIR = path2.join(os3.homedir(), ".config", "pyre");
2401
+ var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
2160
2402
  var DEFAULT_CONFIG = {
2161
2403
  theme: "default",
2162
2404
  interval: 2,
@@ -2170,12 +2412,17 @@ var DEFAULT_CONFIG = {
2170
2412
  mouseEnabled: true,
2171
2413
  cpuAlertPct: 90,
2172
2414
  tempAlertC: 95,
2415
+ watchdogProcess: "",
2416
+ watchdogCpu: 80,
2417
+ watchdogMem: 80,
2173
2418
  notificationsEnabled: true,
2174
2419
  p2pPort: 9876,
2175
2420
  p2pPassword: "pyre",
2176
2421
  splashEnabled: true,
2177
2422
  splashColorScheme: "fire",
2178
2423
  splashAnimation: "classic",
2424
+ webhookUrl: "",
2425
+ alertCmd: "",
2179
2426
  visiblePanels: {
2180
2427
  cpu: true,
2181
2428
  mem: true,
@@ -2189,12 +2436,13 @@ var DEFAULT_CONFIG = {
2189
2436
  disk: true,
2190
2437
  process: true,
2191
2438
  tree: false
2192
- }
2439
+ },
2440
+ panelLayout: ["mem", "disk", "net"]
2193
2441
  };
2194
2442
  function readConfig() {
2195
2443
  try {
2196
- if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
2197
- const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
2444
+ if (!fs2.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
2445
+ const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
2198
2446
  const parsed = JSON.parse(raw);
2199
2447
  return deepMerge(DEFAULT_CONFIG, parsed);
2200
2448
  } catch {
@@ -2203,11 +2451,11 @@ function readConfig() {
2203
2451
  }
2204
2452
  function writeConfig(config3) {
2205
2453
  try {
2206
- if (!fs.existsSync(CONFIG_DIR)) {
2207
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
2454
+ if (!fs2.existsSync(CONFIG_DIR)) {
2455
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
2208
2456
  }
2209
2457
  const merged = deepMerge(readConfig(), config3);
2210
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
2458
+ fs2.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
2211
2459
  } catch {
2212
2460
  }
2213
2461
  }
@@ -2254,6 +2502,7 @@ var state = {
2254
2502
  disk: config.visiblePanels.disk,
2255
2503
  process: config.visiblePanels.process
2256
2504
  },
2505
+ panelLayout: config.panelLayout || ["mem", "disk", "net"],
2257
2506
  logging: false,
2258
2507
  logStream: null,
2259
2508
  statusMessage: "",
@@ -2295,7 +2544,12 @@ var state = {
2295
2544
  ],
2296
2545
  CPU_ALERT_PCT: config.cpuAlertPct,
2297
2546
  TEMP_ALERT_C: config.tempAlertC,
2547
+ watchdogProcess: config.watchdogProcess || "",
2548
+ watchdogCpu: config.watchdogCpu || 80,
2549
+ watchdogMem: config.watchdogMem || 80,
2298
2550
  notificationsEnabled: config.notificationsEnabled,
2551
+ webhookUrl: config.webhookUrl || "",
2552
+ alertCmd: config.alertCmd || "",
2299
2553
  alerted: false,
2300
2554
  activePanel: "grid",
2301
2555
  mouseEnabled: config.mouseEnabled,
@@ -2311,14 +2565,18 @@ var state = {
2311
2565
  { id: "tasks", label: "Tasks", key: "9" },
2312
2566
  { id: "disk", label: "Disk", key: "0" },
2313
2567
  { id: "process", label: "Process", key: "P" },
2314
- { id: "p2p", label: "P2P", key: "R" }
2568
+ { id: "p2p", label: "P2P", key: "R" },
2569
+ { id: "anomalies", label: "Anomalies", key: "A" }
2315
2570
  ],
2316
- p2pEvents: [],
2317
- p2pBind: "",
2318
2571
  p2pServer: null,
2572
+ p2pClient: null,
2319
2573
  p2pServerRunning: false,
2320
- p2pPort: config.p2pPort,
2574
+ p2pClientConnected: false,
2575
+ p2pEvents: [],
2576
+ p2pBind: "",
2321
2577
  p2pPassword: config.p2pPassword,
2578
+ p2pPort: config.p2pPort,
2579
+ anomalyHistory: [],
2322
2580
  splashEnabled: config.splashEnabled,
2323
2581
  splashColorScheme: config.splashColorScheme,
2324
2582
  splashAnimation: config.splashAnimation
@@ -2349,10 +2607,10 @@ function getToggleKey(opt) {
2349
2607
  }
2350
2608
 
2351
2609
  // src/live/export.ts
2352
- import fs2 from "fs";
2353
- import path2 from "path";
2610
+ import fs3 from "fs";
2611
+ import path3 from "path";
2354
2612
  function ensureDir(dir) {
2355
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
2613
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2356
2614
  }
2357
2615
  function timestampForFile() {
2358
2616
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -2365,7 +2623,7 @@ function exportSnapshot() {
2365
2623
  try {
2366
2624
  ensureDir(state.exportDir);
2367
2625
  const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
2368
- const file = path2.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
2626
+ const file = path3.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
2369
2627
  let content;
2370
2628
  switch (state.exportFormat) {
2371
2629
  case "json":
@@ -2384,7 +2642,7 @@ function exportSnapshot() {
2384
2642
  content = formatMarkdown(state.lastData);
2385
2643
  break;
2386
2644
  }
2387
- fs2.writeFileSync(file, content);
2645
+ fs3.writeFileSync(file, content);
2388
2646
  setStatus(`Exported snapshot -> ${file}`);
2389
2647
  } catch (err) {
2390
2648
  setStatus(`Export failed: ${err.message}`);
@@ -2394,8 +2652,8 @@ function startLogging() {
2394
2652
  try {
2395
2653
  ensureDir(state.exportDir);
2396
2654
  rotateLogs();
2397
- const file = path2.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
2398
- state.logStream = fs2.createWriteStream(file, { flags: "a" });
2655
+ const file = path3.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
2656
+ state.logStream = fs3.createWriteStream(file, { flags: "a" });
2399
2657
  state.logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n");
2400
2658
  state.logging = true;
2401
2659
  setStatus(`Logging every tick -> ${file}`);
@@ -2409,37 +2667,37 @@ var MAX_LOG_BYTES = 50 * 1024 * 1024;
2409
2667
  function rotateLogs() {
2410
2668
  try {
2411
2669
  const dir = state.exportDir;
2412
- if (!fs2.existsSync(dir)) return;
2413
- const files = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path2.join(dir, f), mtime: fs2.statSync(path2.join(dir, f)).mtimeMs, size: fs2.statSync(path2.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
2670
+ if (!fs3.existsSync(dir)) return;
2671
+ const files = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path3.join(dir, f), mtime: fs3.statSync(path3.join(dir, f)).mtimeMs, size: fs3.statSync(path3.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
2414
2672
  const now = Date.now();
2415
2673
  for (const f of files) {
2416
2674
  if (now - f.mtime > MAX_LOG_AGE_MS) {
2417
2675
  try {
2418
- fs2.unlinkSync(f.path);
2676
+ fs3.unlinkSync(f.path);
2419
2677
  } catch {
2420
2678
  }
2421
2679
  }
2422
2680
  }
2423
- const remaining = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
2681
+ const remaining = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
2424
2682
  if (remaining.length > MAX_LOG_COUNT) {
2425
2683
  const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
2426
2684
  for (const f of toDelete) {
2427
2685
  try {
2428
- fs2.unlinkSync(path2.join(dir, f));
2686
+ fs3.unlinkSync(path3.join(dir, f));
2429
2687
  } catch {
2430
2688
  }
2431
2689
  }
2432
2690
  }
2433
2691
  let totalSize = 0;
2434
2692
  for (const f of remaining) {
2435
- totalSize += fs2.statSync(path2.join(dir, f)).size;
2693
+ totalSize += fs3.statSync(path3.join(dir, f)).size;
2436
2694
  }
2437
2695
  if (totalSize > MAX_LOG_BYTES) {
2438
2696
  const sorted = remaining.sort();
2439
2697
  for (const f of sorted) {
2440
- const sz = fs2.statSync(path2.join(dir, f)).size;
2698
+ const sz = fs3.statSync(path3.join(dir, f)).size;
2441
2699
  try {
2442
- fs2.unlinkSync(path2.join(dir, f));
2700
+ fs3.unlinkSync(path3.join(dir, f));
2443
2701
  } catch {
2444
2702
  }
2445
2703
  totalSize -= sz;
@@ -2475,7 +2733,7 @@ function writeLogRow(data) {
2475
2733
  }
2476
2734
 
2477
2735
  // src/live/render.ts
2478
- import { execFile } from "child_process";
2736
+ import { execFile, exec as exec2 } from "child_process";
2479
2737
  import chalk5 from "chalk";
2480
2738
 
2481
2739
  // src/anomalies.ts
@@ -2590,7 +2848,8 @@ function tableCacheParams() {
2590
2848
  state.activePanel,
2591
2849
  state.treeView ? "1" : "0",
2592
2850
  state.history.version,
2593
- JSON.stringify(state.visiblePanels)
2851
+ JSON.stringify(state.visiblePanels),
2852
+ JSON.stringify(state.panelLayout)
2594
2853
  ].join("|");
2595
2854
  }
2596
2855
  function graphsCacheParams() {
@@ -2634,13 +2893,15 @@ function p2pPanelLines() {
2634
2893
  const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
2635
2894
  return { title, body };
2636
2895
  }
2637
- function renderCustomizerOverlay() {
2896
+ function renderCustomizerOverlay(scrollOffset, maxVisibleItems) {
2638
2897
  const themesList = Object.keys(THEMES);
2639
- const lines = [
2640
- chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
2641
- ];
2642
- state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
2643
- const isSelected = idx === state.customizerIndex;
2898
+ const headerLine = chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ");
2899
+ const totalItems = state.CUSTOMIZER_OPTIONS.length;
2900
+ const end = Math.min(scrollOffset + maxVisibleItems, totalItems);
2901
+ const lines = [headerLine];
2902
+ for (let i = scrollOffset; i < end; i++) {
2903
+ const opt = state.CUSTOMIZER_OPTIONS[i];
2904
+ const isSelected = i === state.customizerIndex;
2644
2905
  const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
2645
2906
  if (opt === "Theme") {
2646
2907
  lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
@@ -2664,7 +2925,7 @@ function renderCustomizerOverlay() {
2664
2925
  const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
2665
2926
  lines.push(`${prefix}${opt}: ${status}`);
2666
2927
  }
2667
- });
2928
+ }
2668
2929
  return lines.map((l) => ` ${l}`).join("\n");
2669
2930
  }
2670
2931
  var _prevFrameLines = [];
@@ -2719,9 +2980,11 @@ function render() {
2719
2980
  treeView: state.treeView,
2720
2981
  activePanel: state.activePanel,
2721
2982
  anomalies,
2983
+ anomalyHistory: state.anomalyHistory,
2722
2984
  tempUnit: state.tempUnit,
2723
2985
  history: state.history,
2724
- graphMode: state.graphMode
2986
+ graphMode: state.graphMode,
2987
+ panelLayout: state.panelLayout
2725
2988
  });
2726
2989
  _cachedTableData = state.lastData;
2727
2990
  _cachedTableParams = cacheParams;
@@ -2752,9 +3015,18 @@ function render() {
2752
3015
  const footerLine22 = footerLine()[1] || "";
2753
3016
  const footerLine3 = footerLine()[2] || " ";
2754
3017
  const footerLines = [footerLine1, footerLine22, footerLine3];
3018
+ const footerHeight = 3;
3019
+ const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
2755
3020
  if (state.inputMode === "customizer") {
2756
- bodyLines.push("");
2757
- bodyLines.push(...renderCustomizerOverlay().split("\n"));
3021
+ const preOverlayCount = bodyLines.length;
3022
+ const availableForOverlay = targetBodyHeight - preOverlayCount;
3023
+ const totalItems = state.CUSTOMIZER_OPTIONS.length;
3024
+ const headerLines = 1;
3025
+ const blankLines = availableForOverlay >= 2 ? 1 : 0;
3026
+ const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
3027
+ const scrollOffset = totalItems > maxVisibleItems ? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems)) : 0;
3028
+ if (blankLines > 0) bodyLines.push("");
3029
+ bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split("\n"));
2758
3030
  } else if (state.inputMode === "filter") {
2759
3031
  bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
2760
3032
  } else if (state.inputMode === "kill") {
@@ -2770,8 +3042,6 @@ function render() {
2770
3042
  } else if (state.statusMessage) {
2771
3043
  bodyLines.push(` ${state.statusMessage}`);
2772
3044
  }
2773
- const footerHeight = 3;
2774
- const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
2775
3045
  if (bodyLines.length > targetBodyHeight) {
2776
3046
  bodyLines.length = targetBodyHeight;
2777
3047
  }
@@ -2829,21 +3099,54 @@ function checkAlerts(data) {
2829
3099
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
2830
3100
  const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
2831
3101
  const anomalies = detectAnomalies(data, state.history);
3102
+ for (const a of anomalies) {
3103
+ const timestamped = { ...a, timestamp: /* @__PURE__ */ new Date() };
3104
+ const recent = state.anomalyHistory.slice(-5);
3105
+ const isDuplicate = recent.some((r) => r.metric === a.metric && Math.abs(r.zScore - a.zScore) < 0.5);
3106
+ if (!isDuplicate) {
3107
+ state.anomalyHistory.push(timestamped);
3108
+ if (state.anomalyHistory.length > 50) state.anomalyHistory.shift();
3109
+ }
3110
+ }
2832
3111
  const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
2833
3112
  const hasWarningAnomaly = anomalies.some((a) => a.severity === "warning");
3113
+ let watchdogTriggered = false;
3114
+ if (state.watchdogProcess) {
3115
+ const matches = data.processes.filter((p) => p.command.toLowerCase().includes(state.watchdogProcess.toLowerCase()));
3116
+ for (const p of matches) {
3117
+ if (p.cpu >= state.watchdogCpu || p.mem >= state.watchdogMem) {
3118
+ watchdogTriggered = true;
3119
+ break;
3120
+ }
3121
+ }
3122
+ }
2834
3123
  const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
2835
- if ((hot || anomalyTriggered) && !state.alerted) {
3124
+ if ((hot || anomalyTriggered || watchdogTriggered) && !state.alerted) {
2836
3125
  state.alerted = true;
2837
3126
  process.stdout.write("\x07");
2838
3127
  const reasons = [];
2839
3128
  if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
2840
3129
  if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
3130
+ if (watchdogTriggered) reasons.push(`Watchdog process "${state.watchdogProcess}" crossed threshold`);
2841
3131
  for (const a of anomalies) {
2842
3132
  reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
2843
3133
  }
2844
3134
  setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
2845
3135
  sendNotification("pyre Alert", reasons.join("; "));
2846
- } else if (!hot && !anomalyTriggered) {
3136
+ const alertMsg = reasons.join("; ");
3137
+ if (state.webhookUrl) {
3138
+ fetch(state.webhookUrl, {
3139
+ method: "POST",
3140
+ headers: { "Content-Type": "application/json" },
3141
+ body: JSON.stringify({ alert: alertMsg, timestamp: (/* @__PURE__ */ new Date()).toISOString() })
3142
+ }).catch(() => {
3143
+ });
3144
+ }
3145
+ if (state.alertCmd) {
3146
+ exec2(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {
3147
+ });
3148
+ }
3149
+ } else if (!hot && !anomalyTriggered && !watchdogTriggered) {
2847
3150
  state.alerted = false;
2848
3151
  }
2849
3152
  }
@@ -2851,7 +3154,7 @@ function checkAlerts(data) {
2851
3154
  // src/p2p/server.ts
2852
3155
  import net from "net";
2853
3156
  import tls from "tls";
2854
- import fs3 from "fs";
3157
+ import fs4 from "fs";
2855
3158
  import crypto2 from "crypto";
2856
3159
  import chalk6 from "chalk";
2857
3160
 
@@ -2996,9 +3299,9 @@ var P2PServer = class {
2996
3299
  async start() {
2997
3300
  if (this.options.auditLog) {
2998
3301
  try {
2999
- fs3.mkdirSync(this.options.auditLog, { recursive: true });
3302
+ fs4.mkdirSync(this.options.auditLog, { recursive: true });
3000
3303
  const logFile = `${this.options.auditLog}/p2p-audit.log`;
3001
- this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
3304
+ this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
3002
3305
  } catch {
3003
3306
  this.log(chalk6.yellow("Warning: could not open audit log"));
3004
3307
  }
@@ -3007,11 +3310,11 @@ var P2PServer = class {
3007
3310
  const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
3008
3311
  if (useTLS) {
3009
3312
  const tlsOptions = {
3010
- key: fs3.readFileSync(this.options.tlsKey),
3011
- cert: fs3.readFileSync(this.options.tlsCert)
3313
+ key: fs4.readFileSync(this.options.tlsKey),
3314
+ cert: fs4.readFileSync(this.options.tlsCert)
3012
3315
  };
3013
3316
  if (this.options.tlsCA) {
3014
- tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
3317
+ tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
3015
3318
  }
3016
3319
  this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
3017
3320
  } else {
@@ -3279,7 +3582,7 @@ async function startP2PServer(options) {
3279
3582
  // src/p2p/client.ts
3280
3583
  import net2 from "net";
3281
3584
  import tls2 from "tls";
3282
- import fs4 from "fs";
3585
+ import fs5 from "fs";
3283
3586
  import crypto3 from "crypto";
3284
3587
  import chalk7 from "chalk";
3285
3588
  var P2PClient = class {
@@ -3307,9 +3610,9 @@ var P2PClient = class {
3307
3610
  async connect() {
3308
3611
  if (this.options.auditLog) {
3309
3612
  try {
3310
- fs4.mkdirSync(this.options.auditLog, { recursive: true });
3613
+ fs5.mkdirSync(this.options.auditLog, { recursive: true });
3311
3614
  const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
3312
- this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
3615
+ this.auditStream = fs5.createWriteStream(logFile, { flags: "a" });
3313
3616
  } catch {
3314
3617
  console.log(chalk7.yellow("Warning: could not open audit log"));
3315
3618
  }
@@ -3324,7 +3627,7 @@ var P2PClient = class {
3324
3627
  rejectUnauthorized: !this.options.tlsInsecure
3325
3628
  };
3326
3629
  if (this.options.tlsCA) {
3327
- tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
3630
+ tlsOptions.ca = fs5.readFileSync(this.options.tlsCA);
3328
3631
  }
3329
3632
  this.socket = tls2.connect(tlsOptions, () => {
3330
3633
  this.running = true;
@@ -3678,11 +3981,19 @@ function handleInputModeKey(str, key) {
3678
3981
  }
3679
3982
  if (key.name === "backspace") {
3680
3983
  state.inputBuffer = state.inputBuffer.slice(0, -1);
3984
+ if (state.inputMode === "filter") {
3985
+ state.processFilter = state.inputBuffer;
3986
+ invalidateTableCache();
3987
+ }
3681
3988
  render();
3682
3989
  return;
3683
3990
  }
3684
3991
  if (str && str.length === 1 && !key.ctrl && !key.meta) {
3685
3992
  state.inputBuffer += str;
3993
+ if (state.inputMode === "filter") {
3994
+ state.processFilter = state.inputBuffer;
3995
+ invalidateTableCache();
3996
+ }
3686
3997
  render();
3687
3998
  }
3688
3999
  }
@@ -4439,11 +4750,183 @@ async function showSplash(config3) {
4439
4750
  });
4440
4751
  }
4441
4752
 
4753
+ // src/historyCmd.ts
4754
+ import fs6 from "fs";
4755
+ import path4 from "path";
4756
+ import chalk9 from "chalk";
4757
+ function runHistoryCommand(opts2 = {}) {
4758
+ const days = opts2.days || 7;
4759
+ const dir = opts2.dir || "./pyre-exports";
4760
+ console.log(chalk9.bold(`
4761
+ pyre history \u2014 Past ${days} Days Trend
4762
+ `));
4763
+ if (!fs6.existsSync(dir)) {
4764
+ console.log(chalk9.yellow(` No logs directory found at ${dir}`));
4765
+ return;
4766
+ }
4767
+ const files = fs6.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
4768
+ if (files.length === 0) {
4769
+ console.log(chalk9.yellow(` No CSV logs found in ${dir}`));
4770
+ return;
4771
+ }
4772
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
4773
+ const cpuPoints = [];
4774
+ const memPoints = [];
4775
+ const tempPoints = [];
4776
+ for (const file of files.sort()) {
4777
+ const fullPath = path4.join(dir, file);
4778
+ try {
4779
+ const content = fs6.readFileSync(fullPath, "utf-8");
4780
+ const lines = content.split("\n").slice(1);
4781
+ for (const line of lines) {
4782
+ if (!line.trim()) continue;
4783
+ const parts = line.split(",");
4784
+ if (parts.length >= 4) {
4785
+ const tsStr = parts[0];
4786
+ const ts = new Date(tsStr).getTime();
4787
+ if (!isNaN(ts) && ts >= cutoff) {
4788
+ const cpu = parseFloat(parts[1]);
4789
+ const mem = parseFloat(parts[2]);
4790
+ const temp = parseFloat(parts[3]);
4791
+ if (!isNaN(cpu)) cpuPoints.push(cpu);
4792
+ if (!isNaN(mem)) memPoints.push(mem);
4793
+ if (!isNaN(temp)) tempPoints.push(temp);
4794
+ }
4795
+ }
4796
+ }
4797
+ } catch {
4798
+ }
4799
+ }
4800
+ if (cpuPoints.length === 0) {
4801
+ console.log(chalk9.yellow(` No historical entries recorded within the last ${days} days.`));
4802
+ return;
4803
+ }
4804
+ console.log(` Data Points: ${cpuPoints.length} logged snapshots`);
4805
+ console.log(` CPU Usage: ${sparkline(cpuPoints, { min: 0, max: 100 })} (${(cpuPoints.reduce((a, b) => a + b, 0) / cpuPoints.length).toFixed(1)}% avg)`);
4806
+ console.log(` Memory: ${sparkline(memPoints, { min: 0, max: 100 })} (${(memPoints.reduce((a, b) => a + b, 0) / memPoints.length).toFixed(1)}% avg)`);
4807
+ if (tempPoints.length) {
4808
+ console.log(` Temperature: ${sparkline(tempPoints)} (${(tempPoints.reduce((a, b) => a + b, 0) / tempPoints.length).toFixed(1)}\xB0C avg)`);
4809
+ }
4810
+ console.log();
4811
+ }
4812
+
4813
+ // src/diffCmd.ts
4814
+ import fs7 from "fs";
4815
+ import chalk10 from "chalk";
4816
+ function runDiffCommand(file1, file2) {
4817
+ console.log(chalk10.bold(`
4818
+ pyre diff \u2014 Snapshot Comparison
4819
+ `));
4820
+ console.log(chalk10.dim(` Snapshot A: ${file1}`));
4821
+ console.log(chalk10.dim(` Snapshot B: ${file2}
4822
+ `));
4823
+ try {
4824
+ const dataA = JSON.parse(fs7.readFileSync(file1, "utf-8"));
4825
+ const dataB = JSON.parse(fs7.readFileSync(file2, "utf-8"));
4826
+ const diffNum = (label, valA, valB, unit = "") => {
4827
+ const delta = valB - valA;
4828
+ const sign = delta > 0 ? "+" : "";
4829
+ const color = delta > 0 ? chalk10.red : delta < 0 ? chalk10.green : chalk10.dim;
4830
+ console.log(` ${label.padEnd(20)} ${valA.toFixed(1)}${unit} \u2794 ${valB.toFixed(1)}${unit} (${color(sign + delta.toFixed(1) + unit)})`);
4831
+ };
4832
+ diffNum("CPU Usage", dataA.cpu.usage, dataB.cpu.usage, "%");
4833
+ diffNum("Memory Usage", dataA.memory.usagePercent, dataB.memory.usagePercent, "%");
4834
+ if (dataA.cpu.temperature && dataB.cpu.temperature) {
4835
+ diffNum("CPU Temp", dataA.cpu.temperature, dataB.cpu.temperature, "\xB0C");
4836
+ }
4837
+ if (dataA.gpu && dataB.gpu) {
4838
+ diffNum("GPU Util", dataA.gpu.utilization, dataB.gpu.utilization, "%");
4839
+ }
4840
+ diffNum("Network RX Packets", dataA.network.rxPackets, dataB.network.rxPackets);
4841
+ diffNum("Network TX Packets", dataA.network.txPackets, dataB.network.txPackets);
4842
+ console.log();
4843
+ } catch (err) {
4844
+ console.log(chalk10.red(`Failed to compare snapshots: ${err.message}`));
4845
+ }
4846
+ }
4847
+
4848
+ // src/xbar.ts
4849
+ import fs8 from "fs";
4850
+ import path5 from "path";
4851
+ import os4 from "os";
4852
+ import chalk11 from "chalk";
4853
+ function generateXbarPlugin() {
4854
+ const pyrePath = process.argv[1];
4855
+ const nodePath = process.execPath;
4856
+ const script = `#!/bin/bash
4857
+ # <xbar.title>Pyre Monitor</xbar.title>
4858
+ # <xbar.version>v1.0</xbar.version>
4859
+ # <xbar.author>somalip</xbar.author>
4860
+ # <xbar.author.github>somalip</xbar.author.github>
4861
+ # <xbar.desc>Mac system monitoring powered by pyre.</xbar.desc>
4862
+
4863
+ # Run pyre to get a JSON snapshot
4864
+ # We use node directly to avoid env issues in xbar
4865
+ DATA=$("${nodePath}" "${pyrePath}" --once --json)
4866
+
4867
+ if [ -z "$DATA" ]; then
4868
+ echo "\u{1F525} err"
4869
+ echo "---"
4870
+ echo "Could not fetch data"
4871
+ exit 1
4872
+ fi
4873
+
4874
+ CPU=$(echo "$DATA" | grep -o '"usage": *[0-9.]*' | head -1 | awk '{print $2}')
4875
+ MEM=$(echo "$DATA" | grep -o '"usagePercent": *[0-9.]*' | head -1 | awk '{print $2}')
4876
+ TEMP=$(echo "$DATA" | grep -o '"temperature": *[0-9.]*' | head -1 | awk '{print $2}')
4877
+
4878
+ # Format to 1 decimal place
4879
+ CPU=$(printf "%.1f" "$CPU")
4880
+ MEM=$(printf "%.1f" "$MEM")
4881
+ TEMP=$(printf "%.1f" "$TEMP")
4882
+
4883
+ echo "\u{1F525} C:\${CPU}% M:\${MEM}% T:\${TEMP}\xB0C | font=Menlo"
4884
+ echo "---"
4885
+ echo "Open Dashboard | bash='${nodePath}' param1='${pyrePath}' param2='live' terminal=true"
4886
+ `;
4887
+ const pluginDir = path5.join(os4.homedir(), "Library", "Application Support", "xbar", "plugins");
4888
+ const filename = "pyre-monitor.2s.sh";
4889
+ const outPath = path5.join(process.cwd(), filename);
4890
+ fs8.writeFileSync(outPath, script);
4891
+ fs8.chmodSync(outPath, "755");
4892
+ console.log(chalk11.green(`Generated xbar plugin script at ${outPath}`));
4893
+ console.log(`To install, move it to your xbar plugins directory:`);
4894
+ console.log(chalk11.cyan(` mv ${filename} "${pluginDir}/"`));
4895
+ console.log(`Then refresh xbar.`);
4896
+ }
4897
+
4898
+ // src/fleet.ts
4899
+ import { execSync } from "child_process";
4900
+ import chalk12 from "chalk";
4901
+ function runFleetCommand(hosts) {
4902
+ console.log(chalk12.cyan(`Fetching data from ${hosts.length} hosts...`));
4903
+ const results = [];
4904
+ for (const host of hosts) {
4905
+ try {
4906
+ const out = execSync(`ssh ${host} "pyre --once --json"`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
4907
+ const data = JSON.parse(out);
4908
+ results.push({ host, data });
4909
+ } catch (e) {
4910
+ results.push({ host, error: e.message });
4911
+ }
4912
+ }
4913
+ for (const result of results) {
4914
+ console.log(chalk12.bold.blue(`
4915
+ === ${result.host} ===
4916
+ `));
4917
+ if (result.error) {
4918
+ console.log(chalk12.red(`Error connecting to host: ${result.error}`));
4919
+ } else if (result.data) {
4920
+ console.log(formatTable(result.data, { width: process.stdout.columns || 80, activePanel: "grid" }));
4921
+ }
4922
+ }
4923
+ }
4924
+
4442
4925
  // src/index.ts
4443
4926
  var config2 = readConfig();
4444
- var pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4927
+ var pkg = JSON.parse(fs9.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4445
4928
  var program = new Command();
4446
- program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)").option("--port <port>", "Port number for web server mode", "3000");
4929
+ program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)").option("--webhook-url <url>", "URL to POST to on alert").option("--alert-cmd <cmd>", "Command to execute on alert").option("--port <port>", "Port number for web server mode", "3000");
4447
4930
  program.parse(process.argv);
4448
4931
  var opts = program.opts();
4449
4932
  function isExportMode() {
@@ -4456,7 +4939,18 @@ function sanitizeHost2(host) {
4456
4939
  return host.trim().replace(/%$/, "");
4457
4940
  }
4458
4941
  function detectLocalIP() {
4459
- const ifaces = os3.networkInterfaces();
4942
+ const ifaces = os5.networkInterfaces();
4943
+ const prioritizedNames = ["en0", "en1", "wlan0", "eth0"];
4944
+ for (const name of prioritizedNames) {
4945
+ const addrs = ifaces[name];
4946
+ if (addrs) {
4947
+ for (const addr of addrs) {
4948
+ if (addr.family === "IPv4" && !addr.internal) {
4949
+ return addr.address;
4950
+ }
4951
+ }
4952
+ }
4953
+ }
4460
4954
  for (const [, addrs] of Object.entries(ifaces)) {
4461
4955
  if (!addrs) continue;
4462
4956
  for (const addr of addrs) {
@@ -4470,34 +4964,46 @@ function detectLocalIP() {
4470
4964
  async function runServerCommand() {
4471
4965
  const ip = detectLocalIP();
4472
4966
  if (!ip) {
4473
- console.log(chalk9.red("Could not detect a non-loopback IP address. Connect to a network first."));
4967
+ console.log(chalk13.red("Could not detect a non-loopback IP address. Connect to a network first."));
4474
4968
  process.exit(1);
4475
4969
  }
4476
4970
  const port = 9876;
4477
4971
  const password = "mysecret";
4478
- console.log(chalk9.bold(`
4972
+ console.log(chalk13.bold(`
4479
4973
  pyre server`));
4480
- console.log(chalk9.dim(` Detected IP: ${ip}`));
4481
- console.log(chalk9.dim(` Port: ${port}`));
4482
- console.log(chalk9.dim(` Password: ${password}`));
4974
+ console.log(chalk13.dim(` Detected IP: ${ip}`));
4975
+ console.log(chalk13.dim(` Port: ${port}`));
4976
+ console.log(chalk13.dim(` Password: ${password}`));
4483
4977
  console.log();
4484
- console.log(chalk9.bold("Run this on THIS machine (server):"));
4485
- console.log(chalk9.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
4978
+ console.log(chalk13.bold("Run this on THIS machine (server):"));
4979
+ console.log(chalk13.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
4486
4980
  console.log();
4487
- console.log(chalk9.bold("Run this on the OTHER machine (client):"));
4488
- console.log(chalk9.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
4981
+ console.log(chalk13.bold("Run this on the OTHER machine (client):"));
4982
+ console.log(chalk13.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
4489
4983
  console.log();
4490
4984
  }
4491
4985
  async function main() {
4492
4986
  const cmd = program.args[0];
4493
- if (cmd === "info") {
4494
- await runInfoCommand();
4987
+ if (cmd === "history") {
4988
+ const daysIdx = program.args.indexOf("--days");
4989
+ const days = daysIdx !== -1 ? parseInt(program.args[daysIdx + 1], 10) || 7 : 7;
4990
+ runHistoryCommand({ days });
4991
+ return;
4992
+ }
4993
+ if (cmd === "diff") {
4994
+ const file1 = program.args[1];
4995
+ const file2 = program.args[2];
4996
+ if (!file1 || !file2) {
4997
+ console.log(chalk13.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
4998
+ process.exit(1);
4999
+ }
5000
+ runDiffCommand(file1, file2);
4495
5001
  return;
4496
5002
  }
4497
5003
  if (cmd === "ssh") {
4498
5004
  const host = program.args[1];
4499
5005
  if (!host) {
4500
- console.log(chalk9.red("Usage: pyre ssh <host>"));
5006
+ console.log(chalk13.red("Usage: pyre ssh <host>"));
4501
5007
  process.exit(1);
4502
5008
  }
4503
5009
  await runSshCommand(host);
@@ -4510,7 +5016,7 @@ async function main() {
4510
5016
  if (cmd === "bench") {
4511
5017
  const benchCmd = program.args.slice(1).join(" ");
4512
5018
  if (!benchCmd) {
4513
- console.log(chalk9.red("Usage: pyre bench <command>"));
5019
+ console.log(chalk13.red("Usage: pyre bench <command>"));
4514
5020
  process.exit(1);
4515
5021
  }
4516
5022
  await runBenchCommand(benchCmd);
@@ -4522,8 +5028,8 @@ async function main() {
4522
5028
  const cfg = readConfig();
4523
5029
  console.log(JSON.stringify(cfg, null, 2));
4524
5030
  } else if (sub === "reset") {
4525
- fs5.rmSync(CONFIG_FILE, { force: true });
4526
- console.log(chalk9.green("Config reset to defaults"));
5031
+ fs9.rmSync(CONFIG_FILE, { force: true });
5032
+ console.log(chalk13.green("Config reset to defaults"));
4527
5033
  } else {
4528
5034
  console.log("Usage: pyre config <show|reset>");
4529
5035
  }
@@ -4533,8 +5039,24 @@ async function main() {
4533
5039
  await runServerCommand();
4534
5040
  return;
4535
5041
  }
4536
- const alertCpu = opts.alertCpu ? parseInt(opts.alertCpu, 10) : void 0;
4537
- const alertTemp = opts.alertTemp ? parseInt(opts.alertTemp, 10) : void 0;
5042
+ if (cmd === "xbar") {
5043
+ generateXbarPlugin();
5044
+ return;
5045
+ }
5046
+ if (cmd === "fleet") {
5047
+ const hosts = program.args.slice(1);
5048
+ if (hosts.length === 0) {
5049
+ console.error(chalk13.red("Usage: pyre fleet <host1> [host2] ..."));
5050
+ process.exit(1);
5051
+ }
5052
+ runFleetCommand(hosts);
5053
+ return;
5054
+ }
5055
+ if (opts.alertCpu) config2.cpuAlertPct = Number(opts.alertCpu);
5056
+ if (opts.alertTemp) config2.tempAlertC = Number(opts.alertTemp);
5057
+ if (opts.webhookUrl) config2.webhookUrl = opts.webhookUrl;
5058
+ if (opts.alertCmd) config2.alertCmd = opts.alertCmd;
5059
+ if (opts.exportDir) config2.exportDir = opts.exportDir;
4538
5060
  const tempUnit = opts.tempUnit === "f" ? "f" : "c";
4539
5061
  if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
4540
5062
  if (!process.stdout.isTTY && cmd !== "live") {
@@ -4556,8 +5078,8 @@ async function main() {
4556
5078
  theme: opts.theme || config2.theme,
4557
5079
  exportDir: opts.exportDir || config2.exportDir,
4558
5080
  autoLog: opts.log || config2.autoLog,
4559
- alertCpu,
4560
- alertTemp,
5081
+ alertCpu: config2.cpuAlertPct,
5082
+ alertTemp: config2.tempAlertC,
4561
5083
  tempUnit
4562
5084
  }, splashPromise);
4563
5085
  return;
@@ -4571,7 +5093,7 @@ async function main() {
4571
5093
  await runP2PConnect();
4572
5094
  return;
4573
5095
  } else {
4574
- console.log(chalk9.red("Usage: pyre p2p <server|connect> [options]"));
5096
+ console.log(chalk13.red("Usage: pyre p2p <server|connect> [options]"));
4575
5097
  process.exit(1);
4576
5098
  }
4577
5099
  }
@@ -4587,13 +5109,13 @@ async function main() {
4587
5109
  else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config2.treeView, visible: { packets: opts.packets ? true : void 0 } });
4588
5110
  console.log(output);
4589
5111
  if (opts.out) {
4590
- fs5.writeFileSync(opts.out, stripAnsi(output));
4591
- console.log(chalk9.dim(`
5112
+ fs9.writeFileSync(opts.out, stripAnsi(output));
5113
+ console.log(chalk13.dim(`
4592
5114
  Saved to ${opts.out}`));
4593
5115
  }
4594
5116
  }
4595
5117
  main().catch((err) => {
4596
- console.error(chalk9.red(`Error: ${err.message}`));
5118
+ console.error(chalk13.red(`Error: ${err.message}`));
4597
5119
  process.exit(1);
4598
5120
  });
4599
5121
  async function runP2PServer() {
@@ -4609,15 +5131,15 @@ async function runP2PServer() {
4609
5131
  const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
4610
5132
  const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
4611
5133
  if (!password) {
4612
- console.log(chalk9.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
5134
+ console.log(chalk13.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
4613
5135
  process.exit(1);
4614
5136
  }
4615
5137
  if (tlsCert && !tlsKey) {
4616
- console.log(chalk9.red("Error: --p2p-key is required when --p2p-cert is specified"));
5138
+ console.log(chalk13.red("Error: --p2p-key is required when --p2p-cert is specified"));
4617
5139
  process.exit(1);
4618
5140
  }
4619
5141
  if (tlsKey && !tlsCert) {
4620
- console.log(chalk9.red("Error: --p2p-cert is required when --p2p-key is specified"));
5142
+ console.log(chalk13.red("Error: --p2p-cert is required when --p2p-key is specified"));
4621
5143
  process.exit(1);
4622
5144
  }
4623
5145
  const server = new P2PServer({
@@ -4637,18 +5159,18 @@ async function runP2PServer() {
4637
5159
  hmacKey: opts.p2pHmacKey
4638
5160
  });
4639
5161
  const tlsEnabled = !!(tlsCert && tlsKey);
4640
- console.log(chalk9.bold(`
5162
+ console.log(chalk13.bold(`
4641
5163
  pyre P2P Server`));
4642
- console.log(chalk9.dim(` Host: ${host}`));
4643
- console.log(chalk9.dim(` Port: ${port}`));
4644
- console.log(chalk9.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
4645
- console.log(chalk9.dim(` Password: ${password}`));
4646
- console.log(chalk9.dim(` Interval: ${interval}s`));
4647
- console.log(chalk9.dim(` Detailed: ${detailed ? "on" : "off"}`));
4648
- console.log(chalk9.dim(` Rate limit: ${rateLimit} attempts/min`));
4649
- if (allowList.length > 0) console.log(chalk9.dim(` Allowed IPs: ${allowList.join(", ")}`));
4650
- if (denyList.length > 0) console.log(chalk9.dim(` Denied IPs: ${denyList.join(", ")}`));
4651
- console.log(chalk9.dim(` Waiting for peers...
5164
+ console.log(chalk13.dim(` Host: ${host}`));
5165
+ console.log(chalk13.dim(` Port: ${port}`));
5166
+ console.log(chalk13.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
5167
+ console.log(chalk13.dim(` Password: ${password}`));
5168
+ console.log(chalk13.dim(` Interval: ${interval}s`));
5169
+ console.log(chalk13.dim(` Detailed: ${detailed ? "on" : "off"}`));
5170
+ console.log(chalk13.dim(` Rate limit: ${rateLimit} attempts/min`));
5171
+ if (allowList.length > 0) console.log(chalk13.dim(` Allowed IPs: ${allowList.join(", ")}`));
5172
+ if (denyList.length > 0) console.log(chalk13.dim(` Denied IPs: ${denyList.join(", ")}`));
5173
+ console.log(chalk13.dim(` Waiting for peers...
4652
5174
  `));
4653
5175
  await server.start();
4654
5176
  const rl = readline2.createInterface({ input: process.stdin });
@@ -4660,7 +5182,7 @@ async function runP2PServer() {
4660
5182
  process.exit(0);
4661
5183
  } else if (trimmed === "status") {
4662
5184
  const info = server.info;
4663
- console.log(chalk9.dim(` Peers connected: ${info.peersConnected}`));
5185
+ console.log(chalk13.dim(` Peers connected: ${info.peersConnected}`));
4664
5186
  }
4665
5187
  });
4666
5188
  process.once("SIGINT", () => {
@@ -4693,7 +5215,7 @@ async function runP2PConnect() {
4693
5215
  const tlsCA = opts.p2pCA;
4694
5216
  const tlsInsecure = !!opts.p2pInsecure;
4695
5217
  if (!password) {
4696
- console.log(chalk9.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
5218
+ console.log(chalk13.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
4697
5219
  process.exit(1);
4698
5220
  }
4699
5221
  const client = new P2PClient({
@@ -4707,12 +5229,12 @@ async function runP2PConnect() {
4707
5229
  hmacKey: opts.p2pHmacKey
4708
5230
  });
4709
5231
  client.setOnStatus((msg) => {
4710
- process.stdout.write(`\r${chalk9.dim(msg)}${" ".repeat(40)}\r`);
5232
+ process.stdout.write(`\r${chalk13.dim(msg)}${" ".repeat(40)}\r`);
4711
5233
  });
4712
5234
  client.setOnData((data) => {
4713
5235
  const lines = [];
4714
- lines.push(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
4715
- lines.push(chalk9.dim(` ${data.timestamp}`));
5236
+ lines.push(chalk13.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
5237
+ lines.push(chalk13.dim(` ${data.timestamp}`));
4716
5238
  lines.push("");
4717
5239
  lines.push(...formatTable(data, {
4718
5240
  width: process.stdout.columns || 80,
@@ -4729,11 +5251,11 @@ async function runP2PConnect() {
4729
5251
  writeFrame2(lines);
4730
5252
  });
4731
5253
  const protocolLabel = useTLS ? "TLS" : "TCP";
4732
- console.log(chalk9.bold(`
5254
+ console.log(chalk13.bold(`
4733
5255
  pyre P2P Client`));
4734
- console.log(chalk9.dim(` Protocol: ${protocolLabel}`));
4735
- console.log(chalk9.dim(` Connecting to ${host}:${port}...`));
4736
- console.log(chalk9.dim(` Press Ctrl+C to disconnect
5256
+ console.log(chalk13.dim(` Protocol: ${protocolLabel}`));
5257
+ console.log(chalk13.dim(` Connecting to ${host}:${port}...`));
5258
+ console.log(chalk13.dim(` Press Ctrl+C to disconnect
4737
5259
  `));
4738
5260
  process.stdout.write("\x1B[?25l");
4739
5261
  try {
@@ -4741,23 +5263,23 @@ async function runP2PConnect() {
4741
5263
  } catch (err) {
4742
5264
  const code = err.code;
4743
5265
  const msg = err.message || String(err);
4744
- console.log(chalk9.red(`Connection failed: ${msg}`));
5266
+ console.log(chalk13.red(`Connection failed: ${msg}`));
4745
5267
  if (code === "ECONNREFUSED") {
4746
- console.log(chalk9.yellow(" Hints:"));
4747
- console.log(chalk9.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
4748
- console.log(chalk9.dim(" 2. Check the server IP with: hostname -I"));
4749
- console.log(chalk9.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
5268
+ console.log(chalk13.yellow(" Hints:"));
5269
+ console.log(chalk13.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
5270
+ console.log(chalk13.dim(" 2. Check the server IP with: hostname -I"));
5271
+ console.log(chalk13.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
4750
5272
  } else if (code === "ETIMEDOUT") {
4751
- console.log(chalk9.yellow(" Hints:"));
4752
- console.log(chalk9.dim(" 1. Verify the server IP is correct"));
4753
- console.log(chalk9.dim(" 2. Check that both machines are on the same network/subnet"));
4754
- console.log(chalk9.dim(" 3. Open the port on the server firewall"));
5273
+ console.log(chalk13.yellow(" Hints:"));
5274
+ console.log(chalk13.dim(" 1. Verify the server IP is correct"));
5275
+ console.log(chalk13.dim(" 2. Check that both machines are on the same network/subnet"));
5276
+ console.log(chalk13.dim(" 3. Open the port on the server firewall"));
4755
5277
  } else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
4756
- console.log(chalk9.yellow(" Hints:"));
4757
- console.log(chalk9.dim(" 1. The server at " + host + " is not responding"));
4758
- console.log(chalk9.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
4759
- console.log(chalk9.dim(" 3. Check connectivity: ping " + host));
4760
- console.log(chalk9.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
5278
+ console.log(chalk13.yellow(" Hints:"));
5279
+ console.log(chalk13.dim(" 1. The server at " + host + " is not responding"));
5280
+ console.log(chalk13.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
5281
+ console.log(chalk13.dim(" 3. Check connectivity: ping " + host));
5282
+ console.log(chalk13.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
4761
5283
  }
4762
5284
  process.stdout.write("\x1B[?25h");
4763
5285
  process.exit(1);
@@ -4769,41 +5291,21 @@ async function runP2PConnect() {
4769
5291
  process.exit(0);
4770
5292
  });
4771
5293
  }
4772
- async function runInfoCommand() {
4773
- const data = await collectAll({ detailed: true });
4774
- const lines = [];
4775
- lines.push(chalk9.bold(" pyre info"));
4776
- lines.push("");
4777
- lines.push(` Hostname: ${data.header.hostname}`);
4778
- lines.push(` OS: ${data.header.os}`);
4779
- lines.push(` Uptime: ${data.header.uptime}`);
4780
- lines.push(` CPU: ${data.cpu.brand}`);
4781
- lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
4782
- lines.push(` Frequency: ${data.cpu.frequency} MHz`);
4783
- lines.push(` Memory: ${formatBytes(data.memory.total)}`);
4784
- if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
4785
- if (data.battery) {
4786
- lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
4787
- lines.push(` Health: ${data.battery.health}`);
4788
- }
4789
- lines.push(` Thermal: ${data.thermal.state}`);
4790
- console.log(lines.join("\n"));
4791
- }
4792
5294
  async function runSshCommand(host) {
4793
5295
  const password = opts.p2pPassword || "mysecret";
4794
5296
  const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
4795
- console.log(chalk9.bold(`
5297
+ console.log(chalk13.bold(`
4796
5298
  pyre ssh \u2014 ${host}`));
4797
- console.log(chalk9.dim(` Running: ${cmd}
5299
+ console.log(chalk13.dim(` Running: ${cmd}
4798
5300
  `));
4799
5301
  const { execFile: execFile2 } = await import("child_process");
4800
5302
  const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
4801
5303
  if (err) {
4802
- console.log(chalk9.red(` SSH failed: ${err.message}`));
4803
- console.log(chalk9.dim(" Ensure SSH keys are set up or the host is reachable."));
5304
+ console.log(chalk13.red(` SSH failed: ${err.message}`));
5305
+ console.log(chalk13.dim(" Ensure SSH keys are set up or the host is reachable."));
4804
5306
  process.exit(1);
4805
5307
  }
4806
- if (stderr) console.log(chalk9.dim(stderr));
5308
+ if (stderr) console.log(chalk13.dim(stderr));
4807
5309
  try {
4808
5310
  const remote = JSON.parse(stdout);
4809
5311
  console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
@@ -4823,17 +5325,48 @@ async function runWebCommand() {
4823
5325
  const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
4824
5326
  const server = http.createServer(async (req, res) => {
4825
5327
  const url = req.url || "/";
5328
+ if (url === "/health" || url === "/api/health") {
5329
+ res.writeHead(200, { "Content-Type": "application/json" });
5330
+ res.end(JSON.stringify({ status: "ok", uptime: process.uptime(), timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
5331
+ return;
5332
+ }
5333
+ if (url === "/api/stream") {
5334
+ res.writeHead(200, {
5335
+ "Content-Type": "text/event-stream",
5336
+ "Cache-Control": "no-cache",
5337
+ "Connection": "keep-alive",
5338
+ "Access-Control-Allow-Origin": "*"
5339
+ });
5340
+ const sendStats = async () => {
5341
+ try {
5342
+ const data = await collectAll({ detailed: true });
5343
+ res.write(`data: ${JSON.stringify(data)}
5344
+
5345
+ `);
5346
+ } catch {
5347
+ }
5348
+ };
5349
+ sendStats();
5350
+ const intervalId = setInterval(sendStats, 2e3);
5351
+ req.on("close", () => {
5352
+ clearInterval(intervalId);
5353
+ });
5354
+ return;
5355
+ }
4826
5356
  if (url === "/" || url === "/index.html") {
4827
5357
  res.writeHead(200, { "Content-Type": "text/html" });
4828
- const indexPath = path3.join(process.cwd(), "index.html");
4829
- let htmlContent = "";
4830
- if (fs5.existsSync(indexPath)) {
4831
- htmlContent = fs5.readFileSync(indexPath, "utf-8");
5358
+ const data = await collectAll({ detailed: true });
5359
+ const htmlContent = formatHtml(data);
5360
+ res.end(htmlContent);
5361
+ } else if (url === "/setup") {
5362
+ res.writeHead(200, { "Content-Type": "text/html" });
5363
+ const indexPath = path6.join(process.cwd(), "index.html");
5364
+ if (fs9.existsSync(indexPath)) {
5365
+ res.end(fs9.readFileSync(indexPath, "utf-8"));
4832
5366
  } else {
4833
5367
  const data = await collectAll({ detailed: true });
4834
- htmlContent = formatHtml(data);
5368
+ res.end(formatHtml(data));
4835
5369
  }
4836
- res.end(htmlContent);
4837
5370
  } else if (url === "/api" || url === "/api/stats" || url === "/data") {
4838
5371
  const data = await collectAll({ detailed: true });
4839
5372
  res.writeHead(200, {
@@ -4847,12 +5380,33 @@ async function runWebCommand() {
4847
5380
  res.end("Not found");
4848
5381
  }
4849
5382
  });
5383
+ server.on("error", (err) => {
5384
+ if (err.code === "EADDRINUSE") {
5385
+ console.log(chalk13.red(`
5386
+ Error: Port ${port} is already in use.`));
5387
+ console.log(chalk13.yellow(` Try specifying a different port using --port <port>, e.g.:`));
5388
+ console.log(chalk13.dim(` npx tsx src/index.ts web --port ${port + 1}
5389
+ `));
5390
+ process.exit(1);
5391
+ } else {
5392
+ console.log(chalk13.red(`Server error: ${err.message}`));
5393
+ process.exit(1);
5394
+ }
5395
+ });
5396
+ const ip = detectLocalIP() || "localhost";
4850
5397
  server.listen(port, "0.0.0.0", () => {
4851
- console.log(chalk9.bold(`
5398
+ console.log(chalk13.bold(`
4852
5399
  pyre web server running on port ${port}`));
4853
- console.log(chalk9.dim(` Dashboard: http://0.0.0.0:${port}/`));
4854
- console.log(chalk9.dim(` API: http://0.0.0.0:${port}/api`));
4855
- console.log(chalk9.dim(" Press Ctrl+C to stop\n"));
5400
+ console.log(chalk13.dim(` Local URL: http://localhost:${port}/`));
5401
+ console.log(chalk13.hex("#ff6a39").bold(` Network URL: http://${ip}:${port}/ (Accessible on same Wi-Fi / LAN)`));
5402
+ console.log(chalk13.dim(` API: http://${ip}:${port}/api`));
5403
+ console.log(chalk13.dim(` SSE Stream: http://${ip}:${port}/api/stream`));
5404
+ console.log(chalk13.yellow(`
5405
+ Troubleshooting connection from phone/other devices:`));
5406
+ console.log(chalk13.dim(` 1. macOS Firewall: System Settings -> Network -> Firewall -> Allow incoming node/pyre connections`));
5407
+ console.log(chalk13.dim(` 2. Wi-Fi Router: Ensure "AP Isolation" / "Guest Mode" is OFF on your Wi-Fi router`));
5408
+ console.log(chalk13.dim(` 3. Verify phone is connected to the exact same Wi-Fi network
5409
+ `));
4856
5410
  });
4857
5411
  process.once("SIGINT", () => {
4858
5412
  server.close();
@@ -4862,14 +5416,14 @@ async function runWebCommand() {
4862
5416
  async function runBenchCommand(benchCmd) {
4863
5417
  const { spawn } = await import("child_process");
4864
5418
  const logDir = opts.exportDir || "./pyre-exports";
4865
- const file = path3.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
4866
- if (!fs5.existsSync(logDir)) fs5.mkdirSync(logDir, { recursive: true });
4867
- const stream = fs5.createWriteStream(file, { flags: "a" });
5419
+ const file = path6.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
5420
+ if (!fs9.existsSync(logDir)) fs9.mkdirSync(logDir, { recursive: true });
5421
+ const stream = fs9.createWriteStream(file, { flags: "a" });
4868
5422
  stream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n");
4869
- console.log(chalk9.bold(`
5423
+ console.log(chalk13.bold(`
4870
5424
  pyre bench`));
4871
- console.log(chalk9.dim(` Command: ${benchCmd}`));
4872
- console.log(chalk9.dim(` Log: ${file}
5425
+ console.log(chalk13.dim(` Command: ${benchCmd}`));
5426
+ console.log(chalk13.dim(` Log: ${file}
4873
5427
  `));
4874
5428
  const child = spawn("sh", ["-c", benchCmd]);
4875
5429
  child.stdout.on("data", (d) => process.stdout.write(d));
@@ -4890,7 +5444,7 @@ async function runBenchCommand(benchCmd) {
4890
5444
  child.on("close", () => {
4891
5445
  clearInterval(handle);
4892
5446
  stream.end();
4893
- console.log(chalk9.green(`
5447
+ console.log(chalk13.green(`
4894
5448
  Bench complete. Log saved to ${file}`));
4895
5449
  process.exit(0);
4896
5450
  });