pyre-cli 9.5.2 → 10.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/README.md +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +15 -0
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +58 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectNetwork
|
|
3
|
+
} from "./chunk-GHRDW646.js";
|
|
4
|
+
import "./chunk-M73HCQD2.js";
|
|
5
|
+
import "./chunk-VIACN2RU.js";
|
|
6
|
+
import {
|
|
7
|
+
run
|
|
8
|
+
} from "./chunk-MZO5QKUR.js";
|
|
9
|
+
|
|
10
|
+
// src/topoCmd.ts
|
|
11
|
+
import net from "net";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
async function probePort(ip, port, timeoutMs = 400) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const socket = new net.Socket();
|
|
17
|
+
socket.setTimeout(timeoutMs);
|
|
18
|
+
socket.once("connect", () => {
|
|
19
|
+
socket.destroy();
|
|
20
|
+
resolve(true);
|
|
21
|
+
});
|
|
22
|
+
socket.once("timeout", () => {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
resolve(false);
|
|
25
|
+
});
|
|
26
|
+
socket.once("error", () => {
|
|
27
|
+
socket.destroy();
|
|
28
|
+
resolve(false);
|
|
29
|
+
});
|
|
30
|
+
socket.connect(port, ip);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function getDefaultGateway() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = await run("route -n get default 2>/dev/null | grep gateway | awk '{print $2}'", "");
|
|
36
|
+
if (raw.trim()) return raw.trim();
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const raw = await run("netstat -nr | grep default | awk '{print $2}' | head -n1", "");
|
|
41
|
+
if (raw.trim()) return raw.trim();
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return "192.168.1.1";
|
|
45
|
+
}
|
|
46
|
+
async function getArpNeighbors(gatewayIp) {
|
|
47
|
+
const peers = [];
|
|
48
|
+
try {
|
|
49
|
+
const raw = await run("arp -a -n 2>/dev/null", "");
|
|
50
|
+
const lines = raw.split("\n");
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const match = line.match(/\(([\d.]+)\)\s+at\s+([0-9a-fA-F:]+)\s+on\s+(\S+)/);
|
|
53
|
+
if (match) {
|
|
54
|
+
const ip = match[1];
|
|
55
|
+
const mac = match[2];
|
|
56
|
+
const iface = match[3];
|
|
57
|
+
if (ip === "255.255.255.255" || ip.startsWith("224.") || mac.includes("incomplete")) continue;
|
|
58
|
+
peers.push({
|
|
59
|
+
ip,
|
|
60
|
+
mac,
|
|
61
|
+
interface: iface,
|
|
62
|
+
isGateway: ip === gatewayIp
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
return peers;
|
|
69
|
+
}
|
|
70
|
+
async function runTopoCommand(opts = {}) {
|
|
71
|
+
const localNet = await collectNetwork();
|
|
72
|
+
const gatewayIp = await getDefaultGateway();
|
|
73
|
+
const lanPeers = await getArpNeighbors(gatewayIp);
|
|
74
|
+
const hostname = os.hostname();
|
|
75
|
+
if (opts.scan || lanPeers.length <= 15) {
|
|
76
|
+
await Promise.all(
|
|
77
|
+
lanPeers.map(async (peer) => {
|
|
78
|
+
const [hasP2P, hasRest] = await Promise.all([
|
|
79
|
+
probePort(peer.ip, 9876, 250),
|
|
80
|
+
probePort(peer.ip, 8080, 250)
|
|
81
|
+
]);
|
|
82
|
+
peer.pyreP2P = hasP2P;
|
|
83
|
+
peer.pyreRest = hasRest;
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const establishedConns = (localNet.topRemoteHosts || []).slice(0, 6);
|
|
88
|
+
if (opts.json) {
|
|
89
|
+
console.log(
|
|
90
|
+
JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
localNode: {
|
|
93
|
+
hostname,
|
|
94
|
+
ip: localNet.ip,
|
|
95
|
+
interface: localNet.interface
|
|
96
|
+
},
|
|
97
|
+
gateway: gatewayIp,
|
|
98
|
+
lanPeers,
|
|
99
|
+
activeRemoteConnections: establishedConns
|
|
100
|
+
},
|
|
101
|
+
null,
|
|
102
|
+
2
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log(chalk.bold("\n \u{1F525} pyre topo \u2014 Network Topology Map\n"));
|
|
108
|
+
console.log(` ${chalk.bgCyan.black.bold(" LOCAL HOST ")} ${chalk.bold(hostname)}`);
|
|
109
|
+
console.log(` \u251C\u2500\u2500 IP Address: ${chalk.green.bold(localNet.ip || "127.0.0.1")} (${localNet.interface})`);
|
|
110
|
+
console.log(` \u251C\u2500\u2500 Active RX / TX: \u2193 ${chalk.cyan(localNet.rxBytes ? `${(localNet.rxBytes / 1e6).toFixed(1)} MB` : "0 MB")} \u2191 ${chalk.blue(localNet.txBytes ? `${(localNet.txBytes / 1e6).toFixed(1)} MB` : "0 MB")}`);
|
|
111
|
+
console.log(" \u2502");
|
|
112
|
+
console.log(` \u251C\u2500\u2500 ${chalk.yellow.bold("DEFAULT GATEWAY / ROUTER")}`);
|
|
113
|
+
console.log(` \u2502 \u2514\u2500\u2500 IP: ${chalk.yellow(gatewayIp)} [Route \u2192 WAN / Internet]`);
|
|
114
|
+
console.log(" \u2502");
|
|
115
|
+
console.log(` \u251C\u2500\u2500 ${chalk.magenta.bold("LAN NEIGHBORS & PEERS")} (${lanPeers.length} discovered via ARP)`);
|
|
116
|
+
if (lanPeers.length === 0) {
|
|
117
|
+
console.log(` \u2502 \u2514\u2500\u2500 ${chalk.dim("No neighboring ARP entries currently cached.")}`);
|
|
118
|
+
} else {
|
|
119
|
+
for (let i = 0; i < lanPeers.length; i++) {
|
|
120
|
+
const peer = lanPeers[i];
|
|
121
|
+
const isLast = i === lanPeers.length - 1;
|
|
122
|
+
const prefix = isLast ? " \u2502 \u2514\u2500\u2500" : " \u2502 \u251C\u2500\u2500";
|
|
123
|
+
const gwBadge = peer.isGateway ? chalk.yellow(" [Gateway]") : "";
|
|
124
|
+
let pyreBadge = "";
|
|
125
|
+
if (peer.pyreP2P) pyreBadge += chalk.hex("#ff5500").bold(" [\u{1F525} pyre P2P :9876]");
|
|
126
|
+
if (peer.pyreRest) pyreBadge += chalk.hex("#00ffcc").bold(" [\u{1F525} pyre REST :8080]");
|
|
127
|
+
console.log(`${prefix} ${peer.ip.padEnd(16)} ${chalk.dim(peer.mac)}${gwBadge}${pyreBadge}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log(" \u2502");
|
|
131
|
+
console.log(` \u2514\u2500\u2500 ${chalk.cyan.bold("TOP ACTIVE REMOTE CONNECTIONS")}`);
|
|
132
|
+
if (establishedConns.length === 0) {
|
|
133
|
+
console.log(` \u2514\u2500\u2500 ${chalk.dim("No active established sockets detected.")}`);
|
|
134
|
+
} else {
|
|
135
|
+
for (let i = 0; i < establishedConns.length; i++) {
|
|
136
|
+
const conn = establishedConns[i];
|
|
137
|
+
const isLast = i === establishedConns.length - 1;
|
|
138
|
+
const prefix = isLast ? " \u2514\u2500\u2500" : " \u251C\u2500\u2500";
|
|
139
|
+
const connsBadge = chalk.dim(`(${conn.connectionCount} socket${conn.connectionCount > 1 ? "s" : ""})`);
|
|
140
|
+
console.log(`${prefix} ${chalk.white(conn.host.padEnd(24))} ${chalk.green("ESTABLISHED")} ${connsBadge}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.log();
|
|
144
|
+
console.log(chalk.dim(' Tip: Run "pyre p2p server" on any node to allow automatic live cluster streaming.\n'));
|
|
145
|
+
}
|
|
146
|
+
export {
|
|
147
|
+
runTopoCommand
|
|
148
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sendAlert
|
|
3
|
+
} from "./chunk-Z2YV5HKV.js";
|
|
4
|
+
import {
|
|
5
|
+
getConfigDir,
|
|
6
|
+
readConfig
|
|
7
|
+
} from "./chunk-53O2RGSL.js";
|
|
8
|
+
import {
|
|
9
|
+
collectAll
|
|
10
|
+
} from "./chunk-GHRDW646.js";
|
|
11
|
+
import "./chunk-M73HCQD2.js";
|
|
12
|
+
import "./chunk-VIACN2RU.js";
|
|
13
|
+
import "./chunk-MZO5QKUR.js";
|
|
14
|
+
|
|
15
|
+
// src/watchdogCmd.ts
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
var DEFAULT_WATCHDOG_RULES = [
|
|
20
|
+
{
|
|
21
|
+
name: "High CPU System Alert",
|
|
22
|
+
metric: "cpu",
|
|
23
|
+
threshold: 92,
|
|
24
|
+
durationSec: 5,
|
|
25
|
+
action: "alert",
|
|
26
|
+
cooldownSec: 120
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "Critical Temperature Protection",
|
|
30
|
+
metric: "temp",
|
|
31
|
+
threshold: 98,
|
|
32
|
+
durationSec: 5,
|
|
33
|
+
action: "alert",
|
|
34
|
+
cooldownSec: 180
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
async function runWatchdogCommand(opts = {}) {
|
|
38
|
+
const configDir = getConfigDir();
|
|
39
|
+
const defaultRulePath = path.join(configDir, "watchdog.json");
|
|
40
|
+
const rulePath = opts.configFile || defaultRulePath;
|
|
41
|
+
const intervalSec = opts.interval || 2;
|
|
42
|
+
const isDryRun = !!opts.dryRun;
|
|
43
|
+
let rules = DEFAULT_WATCHDOG_RULES;
|
|
44
|
+
if (fs.existsSync(rulePath)) {
|
|
45
|
+
try {
|
|
46
|
+
rules = JSON.parse(fs.readFileSync(rulePath, "utf-8"));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(chalk.yellow(` \u26A0 Could not parse watchdog config at ${rulePath}: ${err.message}. Using default rules.`));
|
|
49
|
+
}
|
|
50
|
+
} else if (!opts.configFile) {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
53
|
+
fs.writeFileSync(defaultRulePath, JSON.stringify(DEFAULT_WATCHDOG_RULES, null, 2));
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.bold("\n \u{1F525} pyre watchdog \u2014 Autonomous Monitoring Daemon\n"));
|
|
58
|
+
console.log(` ${chalk.green("\u2714")} Loaded ${chalk.bold(String(rules.length))} rule(s) from ${chalk.cyan(rulePath)}`);
|
|
59
|
+
console.log(` ${chalk.green("\u2714")} Refresh interval: ${intervalSec}s | Dry run mode: ${isDryRun ? chalk.yellow("ON") : chalk.dim("OFF")}`);
|
|
60
|
+
console.log(`
|
|
61
|
+
${chalk.dim("Active Rules:")}`);
|
|
62
|
+
for (const r of rules) {
|
|
63
|
+
const target = r.process ? `proc "${r.process}"` : r.metric || "system";
|
|
64
|
+
console.log(` \u2022 ${chalk.bold(r.name)} [if ${target} > ${r.threshold} \u2192 ${chalk.cyan(r.action)}]`);
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.dim("\n Watching... Press Ctrl+C to stop.\n"));
|
|
67
|
+
const lastTriggered = /* @__PURE__ */ new Map();
|
|
68
|
+
const config = readConfig();
|
|
69
|
+
const alertChannels = {
|
|
70
|
+
slackUrl: config.slackAlertUrl,
|
|
71
|
+
discordUrl: config.discordAlertUrl,
|
|
72
|
+
pushoverToken: config.pushoverToken,
|
|
73
|
+
pushoverUser: config.pushoverUser,
|
|
74
|
+
ntfyUrl: config.ntfyUrl
|
|
75
|
+
};
|
|
76
|
+
const evaluate = async () => {
|
|
77
|
+
try {
|
|
78
|
+
const data = await collectAll();
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
for (const rule of rules) {
|
|
81
|
+
const cooldown = (rule.cooldownSec || 60) * 1e3;
|
|
82
|
+
const last = lastTriggered.get(rule.name) || 0;
|
|
83
|
+
if (now - last < cooldown) continue;
|
|
84
|
+
let triggered = false;
|
|
85
|
+
let detail = "";
|
|
86
|
+
let targetPid = null;
|
|
87
|
+
if (rule.process) {
|
|
88
|
+
const match = data.processes.find((p) => p.command.toLowerCase().includes(rule.process.toLowerCase()));
|
|
89
|
+
if (match) {
|
|
90
|
+
const val = rule.metric === "process_mem" ? match.mem : match.cpu;
|
|
91
|
+
if (val >= rule.threshold) {
|
|
92
|
+
triggered = true;
|
|
93
|
+
targetPid = match.pid;
|
|
94
|
+
detail = `Process "${match.command}" (PID ${match.pid}) at ${val.toFixed(1)}% (threshold ${rule.threshold}%)`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else if (rule.metric === "cpu") {
|
|
98
|
+
if (data.cpu.usage >= rule.threshold) {
|
|
99
|
+
triggered = true;
|
|
100
|
+
detail = `System CPU usage at ${data.cpu.usage.toFixed(1)}% (threshold ${rule.threshold}%)`;
|
|
101
|
+
}
|
|
102
|
+
} else if (rule.metric === "mem") {
|
|
103
|
+
if (data.memory.usagePercent >= rule.threshold) {
|
|
104
|
+
triggered = true;
|
|
105
|
+
detail = `System Memory at ${data.memory.usagePercent.toFixed(1)}% (threshold ${rule.threshold}%)`;
|
|
106
|
+
}
|
|
107
|
+
} else if (rule.metric === "temp") {
|
|
108
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
|
|
109
|
+
if (temp !== void 0 && temp >= rule.threshold) {
|
|
110
|
+
triggered = true;
|
|
111
|
+
detail = `System Temperature at ${temp.toFixed(1)}\xB0C (threshold ${rule.threshold}\xB0C)`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (triggered) {
|
|
115
|
+
lastTriggered.set(rule.name, now);
|
|
116
|
+
const timeStr = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
117
|
+
console.log(` [${chalk.dim(timeStr)}] ${chalk.red.bold("\u26A0 TRIGGERED:")} ${chalk.bold(rule.name)}: ${detail}`);
|
|
118
|
+
if (isDryRun) {
|
|
119
|
+
console.log(chalk.yellow(` [DRY RUN] Would execute action: ${rule.action}`));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (rule.action === "kill" && targetPid) {
|
|
123
|
+
try {
|
|
124
|
+
process.kill(targetPid, "SIGTERM");
|
|
125
|
+
console.log(chalk.green(` \u2714 Killed PID ${targetPid} with SIGTERM`));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.log(chalk.red(` \u2716 Failed to kill PID ${targetPid}: ${err.message}`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (rule.action === "alert") {
|
|
131
|
+
await sendAlert({
|
|
132
|
+
metric: rule.name,
|
|
133
|
+
value: rule.threshold,
|
|
134
|
+
threshold: rule.threshold,
|
|
135
|
+
unit: "%",
|
|
136
|
+
host: data.header.hostname,
|
|
137
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
138
|
+
severity: "critical"
|
|
139
|
+
}, alertChannels).catch(() => {
|
|
140
|
+
});
|
|
141
|
+
console.log(chalk.green(" \u2714 Multi-channel alert dispatched."));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
await evaluate();
|
|
149
|
+
const handle = setInterval(evaluate, intervalSec * 1e3);
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
process.on("SIGINT", () => {
|
|
152
|
+
clearInterval(handle);
|
|
153
|
+
console.log(chalk.dim("\n Watchdog stopped."));
|
|
154
|
+
resolve();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
runWatchdogCommand
|
|
161
|
+
};
|
package/install.ps1
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# pyre Windows PowerShell installer
|
|
2
|
+
# Usage: iwr -useb https://raw.githubusercontent.com/somalip/pyre/main/install.ps1 | iex
|
|
3
|
+
|
|
4
|
+
$ErrorActionPreference = 'Stop'
|
|
5
|
+
|
|
6
|
+
Write-Host "🔥 Installing pyre system monitor for Windows..." -ForegroundColor Cyan
|
|
7
|
+
|
|
8
|
+
# Check for Node.js / npm
|
|
9
|
+
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
|
10
|
+
Write-Host "✖ Node.js and npm are required to run pyre." -ForegroundColor Red
|
|
11
|
+
Write-Host " Please install Node.js (>= 18) from https://nodejs.org" -ForegroundColor Yellow
|
|
12
|
+
exit 1
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Write-Host "📦 Installing pyre-cli globally via npm..." -ForegroundColor Green
|
|
16
|
+
try {
|
|
17
|
+
npm install -g pyre-cli
|
|
18
|
+
} catch {
|
|
19
|
+
Write-Host "✖ Failed to install pyre-cli globally." -ForegroundColor Red
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (Get-Command pyre -ErrorAction SilentlyContinue) {
|
|
24
|
+
Write-Host "✔ pyre installed successfully!" -ForegroundColor Green
|
|
25
|
+
Write-Host " Run 'pyre live' to start the interactive dashboard." -ForegroundColor Cyan
|
|
26
|
+
Write-Host " Run 'pyre --help' to see all available commands." -ForegroundColor Cyan
|
|
27
|
+
} else {
|
|
28
|
+
Write-Host "ℹ Installation complete. If 'pyre' command is not recognized, restart your PowerShell session." -ForegroundColor Yellow
|
|
29
|
+
}
|
package/install.sh
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# pyre install script - curl installation for macOS
|
|
2
|
+
# pyre install script - curl installation for macOS and Linux
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
5
|
REPO="somalip/pyre"
|
|
6
6
|
TOOL="pyre"
|
|
7
|
-
BIN_DIR="${BIN_DIR:-/usr/local/bin}"
|
|
8
7
|
|
|
9
8
|
RED='\033[0;31m'
|
|
10
9
|
GREEN='\033[0;32m'
|
|
@@ -13,23 +12,25 @@ NC='\033[0m'
|
|
|
13
12
|
info() { echo -e "${GREEN}[pyre]${NC} $1"; }
|
|
14
13
|
err() { echo -e "${RED}[pyre]${NC} $1" >&2; }
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
OS="$(uname -s)"
|
|
16
|
+
if [[ "$OS" != "Darwin" && "$OS" != "Linux" ]]; then
|
|
17
|
+
err "This installer supports macOS and Linux (detected $OS). On Windows, run install.ps1 in PowerShell."
|
|
18
18
|
exit 1
|
|
19
19
|
fi
|
|
20
20
|
|
|
21
21
|
if ! command -v npm &> /dev/null; then
|
|
22
|
-
err "Node.js
|
|
22
|
+
err "Node.js (>= 18) and npm are required. Install from https://nodejs.org"
|
|
23
23
|
exit 1
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
-
info "
|
|
27
|
-
|
|
26
|
+
info "Detected platform: $OS"
|
|
27
|
+
info "Installing ${TOOL} globally via npm..."
|
|
28
|
+
npm install -g pyre-cli || npm install -g "@$REPO"
|
|
28
29
|
|
|
29
30
|
info "Verifying installation..."
|
|
30
31
|
if command -v "$TOOL" &> /dev/null; then
|
|
31
32
|
info "${TOOL} installed successfully!"
|
|
32
|
-
info "Run ${TOOL} --help to get started."
|
|
33
|
+
info "Run '${TOOL} live' or '${TOOL} --help' to get started."
|
|
33
34
|
else
|
|
34
|
-
info "Installation complete. If
|
|
35
|
+
info "Installation complete. If '${TOOL}' is not found in your PATH, restart your shell or check npm global bin path."
|
|
35
36
|
fi
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyre-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
|
+
"description": "Cross-platform system monitoring CLI for macOS, Linux, and Windows: temps, system stats, live dashboard, packet monitor, battery predictor, export",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"src",
|
|
13
13
|
"tsconfig.json",
|
|
14
14
|
"install.sh",
|
|
15
|
+
"install.ps1",
|
|
15
16
|
"Formula/pyre.rb",
|
|
16
17
|
"README.md",
|
|
17
18
|
"parity.md"
|
|
@@ -20,12 +21,15 @@
|
|
|
20
21
|
"build": "tsup src/index.ts --format esm --clean",
|
|
21
22
|
"start": "node dist/index.js",
|
|
22
23
|
"dev": "tsx src/index.ts",
|
|
23
|
-
"test": "node dist/index.js --help
|
|
24
|
+
"test": "node dist/index.js --help && tsx --test src/easy_features.test.ts src/platform.test.ts"
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=18.0.0"
|
|
27
28
|
},
|
|
28
29
|
"keywords": [
|
|
30
|
+
"cross-platform",
|
|
31
|
+
"linux",
|
|
32
|
+
"windows",
|
|
29
33
|
"mac",
|
|
30
34
|
"monitoring",
|
|
31
35
|
"temperature",
|
package/src/ai/models.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in AI Model definitions and diagnostic inference engine.
|
|
3
|
+
*
|
|
4
|
+
* Allows users to choose their AI explanation model directly inside the TUI
|
|
5
|
+
* without depending on an external Ollama server host.
|
|
6
|
+
*
|
|
7
|
+
* Includes fast, private, zero-dependency built-in offline heuristic & rule models,
|
|
8
|
+
* alongside optional local Ollama and remote cloud models if selected.
|
|
9
|
+
*/
|
|
10
|
+
import type { AnomalyAlert } from '../anomalies.js';
|
|
11
|
+
|
|
12
|
+
export interface AIModelOption {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
backend: 'builtin' | 'ollama' | 'openai';
|
|
16
|
+
description: string;
|
|
17
|
+
badge: string;
|
|
18
|
+
offline: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const AVAILABLE_AI_MODELS: AIModelOption[] = [
|
|
22
|
+
{
|
|
23
|
+
id: 'expert-rules-v1',
|
|
24
|
+
name: 'Pyre Expert Heuristics (Local Built-in)',
|
|
25
|
+
backend: 'builtin',
|
|
26
|
+
description: 'Instant zero-latency diagnostic model. No server or setup required.',
|
|
27
|
+
badge: 'Offline · Built-in',
|
|
28
|
+
offline: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'macos-kernel-analyst',
|
|
32
|
+
name: 'macOS Thermal & I/O Engine (Local Built-in)',
|
|
33
|
+
backend: 'builtin',
|
|
34
|
+
description: 'Specialized for Apple Silicon throttling, memory pressure, and swap spikes.',
|
|
35
|
+
badge: 'Offline · Built-in',
|
|
36
|
+
offline: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'llama3.2',
|
|
40
|
+
name: 'Ollama: Llama 3.2 (Local Host)',
|
|
41
|
+
backend: 'ollama',
|
|
42
|
+
description: 'Connects to local Ollama server on port 11434.',
|
|
43
|
+
badge: 'Ollama Server',
|
|
44
|
+
offline: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'deepseek-r1:1.5b',
|
|
48
|
+
name: 'Ollama: DeepSeek R1 1.5B (Local Host)',
|
|
49
|
+
backend: 'ollama',
|
|
50
|
+
description: 'Fast local reasoning model via Ollama server.',
|
|
51
|
+
badge: 'Ollama Server',
|
|
52
|
+
offline: true,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'mistral',
|
|
56
|
+
name: 'Ollama: Mistral (Local Host)',
|
|
57
|
+
backend: 'ollama',
|
|
58
|
+
description: 'General system analysis via Ollama server.',
|
|
59
|
+
badge: 'Ollama Server',
|
|
60
|
+
offline: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'gpt-4o-mini',
|
|
64
|
+
name: 'OpenAI: GPT-4o Mini (Cloud API)',
|
|
65
|
+
backend: 'openai',
|
|
66
|
+
description: 'Requires OPENAI_API_KEY environment variable.',
|
|
67
|
+
badge: 'Cloud API',
|
|
68
|
+
offline: false,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Diagnostic rule engine that provides instantaneous, private,
|
|
74
|
+
* macOS-tailored explanations for anomaly spikes without calling any external host.
|
|
75
|
+
*/
|
|
76
|
+
export function explainAnomaliesLocally(
|
|
77
|
+
anomalies: AnomalyAlert[],
|
|
78
|
+
modelId: string = 'expert-rules-v1'
|
|
79
|
+
): string {
|
|
80
|
+
if (anomalies.length === 0) {
|
|
81
|
+
return '✔ No statistical anomalies detected in this session.';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lines: string[] = [];
|
|
85
|
+
lines.push(`Diagnosis Engine: ${modelId} (Self-Contained TUI Analysis)`);
|
|
86
|
+
lines.push('──────────────────────────────────────────────────────────────────────────────');
|
|
87
|
+
lines.push('');
|
|
88
|
+
|
|
89
|
+
for (const a of anomalies.slice(0, 8)) {
|
|
90
|
+
const unit = a.metric === 'Temp' || a.metric === 'Temperature' ? '°C' : a.metric.toLowerCase().includes('net') ? 'B/s' : '%';
|
|
91
|
+
const severityTag = a.severity === 'critical' ? '[CRITICAL SPIKE]' : '[MODERATE SPIKE]';
|
|
92
|
+
const zStr = a.zScore > 0 ? `+${a.zScore.toFixed(1)}σ` : `${a.zScore.toFixed(1)}σ`;
|
|
93
|
+
|
|
94
|
+
lines.push(`${severityTag} ${a.metric} anomaly at ${a.value.toFixed(1)}${unit} (${zStr} deviation)`);
|
|
95
|
+
|
|
96
|
+
// Model reasoning logic
|
|
97
|
+
if (a.metric === 'CPU') {
|
|
98
|
+
if (modelId === 'macos-kernel-analyst') {
|
|
99
|
+
lines.push(' • Cause: High burst of thread execution, possibly kernel_task scheduling or JIT compiler compilation.');
|
|
100
|
+
lines.push(' • Verdict: Harmless if transient (<30s). Check for runaway build tools or browser renderer loops.');
|
|
101
|
+
lines.push(' • Recommendation: Press "P" to open the Process table and inspect the top CPU consumers.');
|
|
102
|
+
} else {
|
|
103
|
+
lines.push(' • Cause: Sudden compute surge exceeding typical baseline workload.');
|
|
104
|
+
lines.push(' • Verdict: Likely a compiler, media encoder, or intensive background sync process.');
|
|
105
|
+
lines.push(' • Recommendation: Press "k" to terminate high-consuming PIDs if unresponsive.');
|
|
106
|
+
}
|
|
107
|
+
} else if (a.metric === 'Memory') {
|
|
108
|
+
lines.push(' • Cause: Rapid memory allocation or buffer spike.');
|
|
109
|
+
lines.push(' • Verdict: Check for memory leaks or large dataset in-memory parsing.');
|
|
110
|
+
lines.push(' • Recommendation: Review swap activity; close unused Electron apps to relieve memory pressure.');
|
|
111
|
+
} else if (a.metric === 'Temp' || a.metric === 'Temperature') {
|
|
112
|
+
lines.push(' • Cause: Die thermal elevation caused by sustained CPU/GPU power draw.');
|
|
113
|
+
lines.push(' • Verdict: Normal under heavy compute; Apple Silicon will throttle if >100°C.');
|
|
114
|
+
lines.push(' • Recommendation: Ensure MacBook airflow vents are clear; watch fan RPM in Thermal panel.');
|
|
115
|
+
} else if (a.metric === 'GPU') {
|
|
116
|
+
lines.push(' • Cause: Metal pipeline or display compositor acceleration spike.');
|
|
117
|
+
lines.push(' • Verdict: Often triggered by 3D rendering, video decoding, or WebGL.');
|
|
118
|
+
lines.push(' • Recommendation: Check window manager or running 3D applications.');
|
|
119
|
+
} else if (a.metric.toLowerCase().includes('net')) {
|
|
120
|
+
lines.push(' • Cause: Outgoing or incoming bandwidth burst.');
|
|
121
|
+
lines.push(' • Verdict: Cloud sync, large file transfer, or package manager download.');
|
|
122
|
+
lines.push(' • Recommendation: Switch to Connections panel (key "8") to check remote hosts.');
|
|
123
|
+
} else if (a.metric === 'Power') {
|
|
124
|
+
lines.push(' • Cause: Combined wattage draw spike above typical power envelope.');
|
|
125
|
+
lines.push(' • Verdict: SoC hitting turbo or sustained multi-core boost.');
|
|
126
|
+
lines.push(' • Recommendation: If on battery power, consider reducing background workloads.');
|
|
127
|
+
} else {
|
|
128
|
+
lines.push(` • Cause: Statistical variance in metric ${a.metric}.`);
|
|
129
|
+
lines.push(' • Verdict: Deviates significantly from session mean.');
|
|
130
|
+
lines.push(' • Recommendation: Monitor in live graphs to observe whether it returns to baseline.');
|
|
131
|
+
}
|
|
132
|
+
lines.push('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push('──────────────────────────────────────────────────────────────────────────────');
|
|
136
|
+
lines.push('Tip: Press "m" anytime in the TUI to change the active AI explanation model.');
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre multi-channel alert sender.
|
|
3
|
+
*
|
|
4
|
+
* Sends alert notifications to one or more configured channels:
|
|
5
|
+
* - Slack (incoming webhook URL)
|
|
6
|
+
* - Discord (webhook URL)
|
|
7
|
+
* - Pushover (API token + user key)
|
|
8
|
+
* - ntfy.sh (topic URL)
|
|
9
|
+
*
|
|
10
|
+
* All channels use Node.js 18+ built-in fetch.
|
|
11
|
+
* Failures are always silent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface AlertPayload {
|
|
15
|
+
metric: string;
|
|
16
|
+
value: number;
|
|
17
|
+
threshold: number;
|
|
18
|
+
unit: string;
|
|
19
|
+
host: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
severity?: 'warning' | 'critical';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AlertChannelConfig {
|
|
25
|
+
slackUrl?: string;
|
|
26
|
+
discordUrl?: string;
|
|
27
|
+
pushoverToken?: string;
|
|
28
|
+
pushoverUser?: string;
|
|
29
|
+
ntfyUrl?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildMessage(payload: AlertPayload): string {
|
|
33
|
+
const icon = payload.severity === 'critical' ? '🔴' : '⚠️';
|
|
34
|
+
return `${icon} pyre alert on ${payload.host}: ${payload.metric} reached ${payload.value.toFixed(1)}${payload.unit} (threshold: ${payload.threshold}${payload.unit}) at ${payload.timestamp}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function postJson(url: string, body: unknown): Promise<void> {
|
|
38
|
+
await fetch(url, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sendSlack(url: string, payload: AlertPayload): Promise<void> {
|
|
46
|
+
const color = payload.severity === 'critical' ? '#ff0000' : '#ffa500';
|
|
47
|
+
await postJson(url, {
|
|
48
|
+
attachments: [{
|
|
49
|
+
color,
|
|
50
|
+
text: buildMessage(payload),
|
|
51
|
+
footer: 'pyre system monitor',
|
|
52
|
+
ts: Math.floor(Date.now() / 1000),
|
|
53
|
+
}],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function sendDiscord(url: string, payload: AlertPayload): Promise<void> {
|
|
58
|
+
const color = payload.severity === 'critical' ? 0xff0000 : 0xffa500;
|
|
59
|
+
await postJson(url, {
|
|
60
|
+
embeds: [{
|
|
61
|
+
title: `pyre — ${payload.metric} Alert`,
|
|
62
|
+
description: buildMessage(payload),
|
|
63
|
+
color,
|
|
64
|
+
footer: { text: 'pyre system monitor' },
|
|
65
|
+
timestamp: payload.timestamp,
|
|
66
|
+
}],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function sendPushover(token: string, user: string, payload: AlertPayload): Promise<void> {
|
|
71
|
+
await postJson('https://api.pushover.net/1/messages.json', {
|
|
72
|
+
token,
|
|
73
|
+
user,
|
|
74
|
+
title: `pyre — ${payload.metric} Alert`,
|
|
75
|
+
message: buildMessage(payload),
|
|
76
|
+
priority: payload.severity === 'critical' ? 1 : 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function sendNtfy(url: string, payload: AlertPayload): Promise<void> {
|
|
81
|
+
await fetch(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'text/plain',
|
|
85
|
+
'Title': `pyre — ${payload.metric} Alert`,
|
|
86
|
+
'Priority': payload.severity === 'critical' ? 'high' : 'default',
|
|
87
|
+
'Tags': payload.severity === 'critical' ? 'rotating_light' : 'warning',
|
|
88
|
+
},
|
|
89
|
+
body: buildMessage(payload),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Send an alert to all configured channels. All failures are silently ignored. */
|
|
94
|
+
export async function sendAlert(payload: AlertPayload, config: AlertChannelConfig): Promise<void> {
|
|
95
|
+
const tasks: Promise<void>[] = [];
|
|
96
|
+
|
|
97
|
+
if (config.slackUrl) tasks.push(sendSlack(config.slackUrl, payload).catch(() => {}));
|
|
98
|
+
if (config.discordUrl) tasks.push(sendDiscord(config.discordUrl, payload).catch(() => {}));
|
|
99
|
+
if (config.pushoverToken && config.pushoverUser) {
|
|
100
|
+
tasks.push(sendPushover(config.pushoverToken, config.pushoverUser, payload).catch(() => {}));
|
|
101
|
+
}
|
|
102
|
+
if (config.ntfyUrl) tasks.push(sendNtfy(config.ntfyUrl, payload).catch(() => {}));
|
|
103
|
+
|
|
104
|
+
if (tasks.length > 0) await Promise.allSettled(tasks);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Fire a test alert to all configured channels (used by pyre alert test). */
|
|
108
|
+
export async function sendTestAlert(config: AlertChannelConfig): Promise<void> {
|
|
109
|
+
const hostname = (await import('node:os')).hostname();
|
|
110
|
+
await sendAlert({
|
|
111
|
+
metric: 'CPU',
|
|
112
|
+
value: 95.0,
|
|
113
|
+
threshold: 90,
|
|
114
|
+
unit: '%',
|
|
115
|
+
host: hostname,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
severity: 'warning',
|
|
118
|
+
}, config);
|
|
119
|
+
}
|