pyre-cli 6.1.0 → 7.1.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/src/live/input.ts CHANGED
@@ -42,10 +42,10 @@ import { writeConfig } from '../state/config.js';
42
42
  }
43
43
 
44
44
  function isUpKey(key: readline.Key, str?: string): boolean {
45
- return key.name === 'up' || str === 'k' || key.sequence === '\x1b[A' || key.sequence === '\x1bOA';
45
+ return key.name === 'up' || key.sequence === '\x1b[A' || key.sequence === '\x1bOA';
46
46
  }
47
47
  function isDownKey(key: readline.Key, str?: string): boolean {
48
- return key.name === 'down' || str === 'j' || key.sequence === '\x1b[B' || key.sequence === '\x1bOB';
48
+ return key.name === 'down' || key.sequence === '\x1b[B' || key.sequence === '\x1bOB';
49
49
  }
50
50
  function isEnterKey(key: readline.Key, str?: string): boolean {
51
51
  return key.name === 'return' || key.name === 'enter' || key.sequence === '\r' || key.sequence === '\n' || str === '\r' || str === '\n';
@@ -6,7 +6,7 @@
6
6
  * alert checker. All state variables are imported from
7
7
  * `state.ts`.
8
8
  */
9
- import { execFile } from 'node:child_process';
9
+ import { execFile, exec } from 'node:child_process';
10
10
  import chalk from 'chalk';
11
11
  import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel, fitVisible } from '../formatters/index.js';
12
12
  import { detectAnomalies } from '../anomalies.js';
@@ -54,6 +54,7 @@ function tableCacheParams(): string {
54
54
  state.treeView ? '1' : '0',
55
55
  state.history.version,
56
56
  JSON.stringify(state.visiblePanels),
57
+ JSON.stringify(state.panelLayout),
57
58
  ].join('|');
58
59
  }
59
60
 
@@ -215,10 +216,12 @@ function render() {
215
216
  visible: state.visiblePanels,
216
217
  treeView: state.treeView,
217
218
  activePanel: state.activePanel,
218
- anomalies,
219
+ anomalies: anomalies,
220
+ anomalyHistory: state.anomalyHistory,
219
221
  tempUnit: state.tempUnit,
220
222
  history: state.history,
221
223
  graphMode: state.graphMode,
224
+ panelLayout: state.panelLayout,
222
225
  });
223
226
  _cachedTableData = state.lastData;
224
227
  _cachedTableParams = cacheParams;
@@ -353,6 +356,19 @@ function checkAlerts(data: StatsData) {
353
356
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
354
357
  const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
355
358
  const anomalies = detectAnomalies(data, state.history);
359
+
360
+ // Add timestamp to anomalies and push to history
361
+ for (const a of anomalies) {
362
+ const timestamped = { ...a, timestamp: new Date() };
363
+ // Only add if not already recently reported (same metric and similar zScore)
364
+ const recent = state.anomalyHistory.slice(-5);
365
+ const isDuplicate = recent.some(r => r.metric === a.metric && Math.abs(r.zScore - a.zScore) < 0.5);
366
+ if (!isDuplicate) {
367
+ state.anomalyHistory.push(timestamped as any);
368
+ if (state.anomalyHistory.length > 50) state.anomalyHistory.shift();
369
+ }
370
+ }
371
+
356
372
  const hasCriticalAnomaly = anomalies.some(a => a.severity === 'critical');
357
373
  const hasWarningAnomaly = anomalies.some(a => a.severity === 'warning');
358
374
  // Process watchdog check
@@ -381,6 +397,18 @@ function checkAlerts(data: StatsData) {
381
397
  }
382
398
  setStatus(chalk.red(`⚠ Alert: ${reasons.join('; ')}`), 5000);
383
399
  sendNotification('pyre Alert', reasons.join('; '));
400
+
401
+ const alertMsg = reasons.join('; ');
402
+ if (state.webhookUrl) {
403
+ fetch(state.webhookUrl, {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': 'application/json' },
406
+ body: JSON.stringify({ alert: alertMsg, timestamp: new Date().toISOString() })
407
+ }).catch(() => {});
408
+ }
409
+ if (state.alertCmd) {
410
+ exec(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {});
411
+ }
384
412
  } else if (!hot && !anomalyTriggered && !watchdogTriggered) {
385
413
  state.alerted = false;
386
414
  }
package/src/live/state.ts CHANGED
@@ -48,6 +48,7 @@ const state = {
48
48
  disk: config.visiblePanels.disk,
49
49
  process: config.visiblePanels.process,
50
50
  } as VisibleItems,
51
+ panelLayout: config.panelLayout || ['mem', 'disk', 'net'],
51
52
  logging: false,
52
53
  logStream: null as fs.WriteStream | null,
53
54
  statusMessage: '',
@@ -93,8 +94,10 @@ const state = {
93
94
  watchdogCpu: config.watchdogCpu || 80,
94
95
  watchdogMem: config.watchdogMem || 80,
95
96
  notificationsEnabled: config.notificationsEnabled,
97
+ webhookUrl: config.webhookUrl || '',
98
+ alertCmd: config.alertCmd || '',
96
99
  alerted: false,
97
- activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
100
+ activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies',
98
101
  mouseEnabled: config.mouseEnabled,
99
102
  PANEL_TABS: [
100
103
  { id: 'cpu', label: 'CPU', key: '1' },
@@ -109,13 +112,17 @@ const state = {
109
112
  { id: 'disk', label: 'Disk', key: '0' },
110
113
  { id: 'process', label: 'Process', key: 'P' },
111
114
  { id: 'p2p', label: 'P2P', key: 'R' },
112
- ] as const,
113
- p2pEvents: [] as { type: string; detail: string; ts: number }[],
114
- p2pBind: '',
115
- p2pServer: null as { stop: () => void; peerEventHistory: { type: string; detail: string; ts: number }[] } | null,
116
- p2pServerRunning: false,
117
- p2pPort: config.p2pPort,
118
- p2pPassword: config.p2pPassword,
115
+ { id: 'anomalies', label: 'Anomalies', key: 'A' },
116
+ ],
117
+ p2pServer: null as import('../p2p/server.js').P2PServer | null,
118
+ p2pClient: null as import('../p2p/client.js').P2PClient | null,
119
+ p2pServerRunning: false,
120
+ p2pClientConnected: false,
121
+ p2pEvents: [] as any[],
122
+ p2pBind: '',
123
+ p2pPassword: config.p2pPassword,
124
+ p2pPort: config.p2pPort,
125
+ anomalyHistory: [] as import('../anomalies.js').AnomalyAlert[],
119
126
  splashEnabled: config.splashEnabled,
120
127
  splashColorScheme: config.splashColorScheme as SplashColorScheme,
121
128
  splashAnimation: config.splashAnimation as SplashAnimation,
@@ -37,6 +37,8 @@ export interface PyreConfig {
37
37
  watchdogProcess?: string;
38
38
  watchdogCpu?: number;
39
39
  watchdogMem?: number;
40
+ webhookUrl?: string;
41
+ alertCmd?: string;
40
42
  visiblePanels?: {
41
43
  cpu?: boolean;
42
44
  mem?: boolean;
@@ -51,6 +53,7 @@ export interface PyreConfig {
51
53
  process?: boolean;
52
54
  tree?: boolean;
53
55
  };
56
+ panelLayout?: string[];
54
57
  }
55
58
 
56
59
  export const DEFAULT_CONFIG: Required<PyreConfig> = {
@@ -75,6 +78,8 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
75
78
  splashEnabled: true,
76
79
  splashColorScheme: 'fire',
77
80
  splashAnimation: 'classic',
81
+ webhookUrl: '',
82
+ alertCmd: '',
78
83
  visiblePanels: {
79
84
  cpu: true,
80
85
  mem: true,
@@ -89,6 +94,7 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
89
94
  process: true,
90
95
  tree: false,
91
96
  },
97
+ panelLayout: ['mem', 'disk', 'net'],
92
98
  };
93
99
 
94
100
  export function readConfig(): Required<PyreConfig> {
package/src/xbar.ts ADDED
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import chalk from 'chalk';
5
+
6
+ export function generateXbarPlugin() {
7
+ const pyrePath = process.argv[1];
8
+ const nodePath = process.execPath;
9
+
10
+ const script = `#!/bin/bash
11
+ # <xbar.title>Pyre Monitor</xbar.title>
12
+ # <xbar.version>v1.0</xbar.version>
13
+ # <xbar.author>somalip</xbar.author>
14
+ # <xbar.author.github>somalip</xbar.author.github>
15
+ # <xbar.desc>Mac system monitoring powered by pyre.</xbar.desc>
16
+
17
+ # Run pyre to get a JSON snapshot
18
+ # We use node directly to avoid env issues in xbar
19
+ DATA=$("${nodePath}" "${pyrePath}" --once --json)
20
+
21
+ if [ -z "$DATA" ]; then
22
+ echo "🔥 err"
23
+ echo "---"
24
+ echo "Could not fetch data"
25
+ exit 1
26
+ fi
27
+
28
+ CPU=$(echo "$DATA" | grep -o '"usage": *[0-9.]*' | head -1 | awk '{print $2}')
29
+ MEM=$(echo "$DATA" | grep -o '"usagePercent": *[0-9.]*' | head -1 | awk '{print $2}')
30
+ TEMP=$(echo "$DATA" | grep -o '"temperature": *[0-9.]*' | head -1 | awk '{print $2}')
31
+
32
+ # Format to 1 decimal place
33
+ CPU=$(printf "%.1f" "$CPU")
34
+ MEM=$(printf "%.1f" "$MEM")
35
+ TEMP=$(printf "%.1f" "$TEMP")
36
+
37
+ echo "🔥 C:\${CPU}% M:\${MEM}% T:\${TEMP}°C | font=Menlo"
38
+ echo "---"
39
+ echo "Open Dashboard | bash='${nodePath}' param1='${pyrePath}' param2='live' terminal=true"
40
+ `;
41
+
42
+ const pluginDir = path.join(os.homedir(), 'Library', 'Application Support', 'xbar', 'plugins');
43
+ const filename = 'pyre-monitor.2s.sh';
44
+ const outPath = path.join(process.cwd(), filename);
45
+
46
+ fs.writeFileSync(outPath, script);
47
+ fs.chmodSync(outPath, '755');
48
+
49
+ console.log(chalk.green(`Generated xbar plugin script at ${outPath}`));
50
+ console.log(`To install, move it to your xbar plugins directory:`);
51
+ console.log(chalk.cyan(` mv ${filename} "${pluginDir}/"`));
52
+ console.log(`Then refresh xbar.`);
53
+ }