pyre-cli 9.5.1 → 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 +84 -14
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/blender-FGH3ASEQ.js +7 -0
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
- package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
- package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
- 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-VIACN2RU.js +133 -0
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-4ZLF4HTS.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 +841 -206
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -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-GVIAHIVV.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/anomalies.ts +114 -60
- 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/index.ts +1 -1
- package/src/formatters/output.ts +100 -13
- package/src/formatters/render.ts +274 -38
- package/src/formatters/themes.ts +8 -0
- package/src/formatters/types.ts +6 -1
- package/src/index.ts +298 -56
- package/src/live/input.ts +122 -4
- package/src/live/render.ts +98 -0
- package/src/live/state.ts +70 -52
- package/src/live/types.ts +2 -2
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/blender.ts +144 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +558 -147
- package/src/monitors/dns_cache.ts +48 -0
- package/src/monitors/index.ts +3 -2
- 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 +112 -14
- 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 +64 -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-HGRBBPYX.js +0 -937
- package/dist/doctor-L6FZYA7F.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
- /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_FILE,
|
|
3
|
+
readConfig
|
|
4
|
+
} from "./chunk-53O2RGSL.js";
|
|
5
|
+
import {
|
|
6
|
+
run
|
|
7
|
+
} from "./chunk-MZO5QKUR.js";
|
|
8
|
+
|
|
9
|
+
// src/doctor.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
async function runDoctor() {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
if (platform === "linux") return runLinuxDoctor();
|
|
15
|
+
if (platform === "win32") return runWindowsDoctor();
|
|
16
|
+
return runMacDoctor();
|
|
17
|
+
}
|
|
18
|
+
async function runMacDoctor() {
|
|
19
|
+
const checks = [];
|
|
20
|
+
try {
|
|
21
|
+
const pmResult = await run("sudo -n powermetrics --samplers cpu_power -n 1 2>&1", "");
|
|
22
|
+
if (pmResult.includes("Password:") || pmResult.includes("Permission denied") || pmResult.includes("sudo: a password is required")) {
|
|
23
|
+
checks.push({
|
|
24
|
+
name: "Sudo / Powermetrics",
|
|
25
|
+
status: "warn",
|
|
26
|
+
message: "Passwordless sudo for powermetrics is not configured.",
|
|
27
|
+
details: "Powermetrics allows high-precision GPU & CPU power sampling. Run with sudo or configure passwordless sudo in /etc/sudoers."
|
|
28
|
+
});
|
|
29
|
+
} else {
|
|
30
|
+
checks.push({
|
|
31
|
+
name: "Sudo / Powermetrics",
|
|
32
|
+
status: "ok",
|
|
33
|
+
message: "Powermetrics access available."
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
checks.push({
|
|
38
|
+
name: "Sudo / Powermetrics",
|
|
39
|
+
status: "warn",
|
|
40
|
+
message: "Powermetrics check failed."
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const psCheck = await run("ps -ax -o pid,user,command", "");
|
|
45
|
+
if (psCheck.length > 0) {
|
|
46
|
+
checks.push({
|
|
47
|
+
name: "TCC / Process Visibility",
|
|
48
|
+
status: "ok",
|
|
49
|
+
message: "Process listing access available."
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
checks.push({
|
|
53
|
+
name: "TCC / Process Visibility",
|
|
54
|
+
status: "warn",
|
|
55
|
+
message: "Restricted process visibility.",
|
|
56
|
+
details: "Grant Full Disk Access or Terminal permissions in System Settings -> Privacy & Security."
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
checks.push({
|
|
61
|
+
name: "TCC / Process Visibility",
|
|
62
|
+
status: "error",
|
|
63
|
+
message: "Unable to list system processes."
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const netCheck = await run("netstat -rn", "");
|
|
68
|
+
if (netCheck.includes("default") || netCheck.includes("gateway")) {
|
|
69
|
+
checks.push({
|
|
70
|
+
name: "Network Reachability",
|
|
71
|
+
status: "ok",
|
|
72
|
+
message: "Network interface active with default gateway."
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
checks.push({
|
|
76
|
+
name: "Network Reachability",
|
|
77
|
+
status: "warn",
|
|
78
|
+
message: "No active network route detected for P2P mode."
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
checks.push({
|
|
83
|
+
name: "Network Reachability",
|
|
84
|
+
status: "warn",
|
|
85
|
+
message: "Could not verify network routes."
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const spctlOut = await run("spctl --status 2>&1", "");
|
|
90
|
+
if (spctlOut.includes("assessments enabled")) {
|
|
91
|
+
checks.push({
|
|
92
|
+
name: "Gatekeeper",
|
|
93
|
+
status: "ok",
|
|
94
|
+
message: "Gatekeeper enforcement is enabled."
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
checks.push({
|
|
98
|
+
name: "Gatekeeper",
|
|
99
|
+
status: "warn",
|
|
100
|
+
message: "Gatekeeper is disabled or status check unconfirmed.",
|
|
101
|
+
details: 'Re-enable Gatekeeper using "sudo spctl --master-enable" to protect against untrusted software.'
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
checks.push({
|
|
106
|
+
name: "Gatekeeper",
|
|
107
|
+
status: "warn",
|
|
108
|
+
message: "Could not query Gatekeeper status."
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const sipOut = await run("csrutil status 2>&1", "");
|
|
113
|
+
if (sipOut.includes("enabled")) {
|
|
114
|
+
checks.push({
|
|
115
|
+
name: "System Integrity Protection",
|
|
116
|
+
status: "ok",
|
|
117
|
+
message: "SIP is enabled."
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
checks.push({
|
|
121
|
+
name: "System Integrity Protection",
|
|
122
|
+
status: "warn",
|
|
123
|
+
message: "SIP is disabled or restricted.",
|
|
124
|
+
details: "System Integrity Protection protects core system files. Enable it from macOS Recovery."
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
checks.push({
|
|
129
|
+
name: "System Integrity Protection",
|
|
130
|
+
status: "warn",
|
|
131
|
+
message: "Could not query SIP status."
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const xprotectOut = await run("system_profiler SPInstallHistoryDataType 2>&1", "");
|
|
136
|
+
const xpMatches = Array.from(xprotectOut.matchAll(/XProtect(?:Remediator)?ConfigData.*?\n.*?Install Date:\s*(.+)/gi));
|
|
137
|
+
if (xpMatches.length > 0) {
|
|
138
|
+
const latestDateStr = xpMatches[xpMatches.length - 1][1];
|
|
139
|
+
checks.push({
|
|
140
|
+
name: "XProtect Definitions",
|
|
141
|
+
status: "ok",
|
|
142
|
+
message: `XProtect active (last updated ${latestDateStr.trim()}).`
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
checks.push({
|
|
146
|
+
name: "XProtect Definitions",
|
|
147
|
+
status: "ok",
|
|
148
|
+
message: "XProtect definitions installed."
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
checks.push({
|
|
153
|
+
name: "XProtect Definitions",
|
|
154
|
+
status: "warn",
|
|
155
|
+
message: "Could not determine XProtect update history."
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
checks.push(checkConfigIntegrity());
|
|
159
|
+
return checks;
|
|
160
|
+
}
|
|
161
|
+
async function runLinuxDoctor() {
|
|
162
|
+
const checks = [];
|
|
163
|
+
try {
|
|
164
|
+
const psCheck = await run("ps -eo pid,user,comm", "");
|
|
165
|
+
if (psCheck.length > 0) {
|
|
166
|
+
checks.push({
|
|
167
|
+
name: "Process Visibility",
|
|
168
|
+
status: "ok",
|
|
169
|
+
message: "Full process table access available."
|
|
170
|
+
});
|
|
171
|
+
} else {
|
|
172
|
+
checks.push({
|
|
173
|
+
name: "Process Visibility",
|
|
174
|
+
status: "warn",
|
|
175
|
+
message: "Restricted process table access."
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
checks.push({
|
|
180
|
+
name: "Process Visibility",
|
|
181
|
+
status: "error",
|
|
182
|
+
message: "Unable to query process list."
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const statExists = fs.existsSync("/proc/stat");
|
|
187
|
+
const memExists = fs.existsSync("/proc/meminfo");
|
|
188
|
+
if (statExists && memExists) {
|
|
189
|
+
checks.push({
|
|
190
|
+
name: "Kernel Telemetry (/proc)",
|
|
191
|
+
status: "ok",
|
|
192
|
+
message: "Kernel telemetry interfaces readable."
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
checks.push({
|
|
196
|
+
name: "Kernel Telemetry (/proc)",
|
|
197
|
+
status: "error",
|
|
198
|
+
message: "Core pseudo-filesystems (/proc) unavailable or restricted."
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
checks.push({
|
|
203
|
+
name: "Kernel Telemetry (/proc)",
|
|
204
|
+
status: "warn",
|
|
205
|
+
message: "Could not check /proc readability."
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const thermZones = fs.existsSync("/sys/class/thermal");
|
|
210
|
+
const hwmon = fs.existsSync("/sys/class/hwmon");
|
|
211
|
+
if (thermZones || hwmon) {
|
|
212
|
+
checks.push({
|
|
213
|
+
name: "Hardware Sensors (/sys)",
|
|
214
|
+
status: "ok",
|
|
215
|
+
message: "Thermal and hardware monitoring sensors detected."
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
checks.push({
|
|
219
|
+
name: "Hardware Sensors (/sys)",
|
|
220
|
+
status: "warn",
|
|
221
|
+
message: "No hardware sensor paths detected in /sys/class."
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
checks.push({
|
|
226
|
+
name: "Hardware Sensors (/sys)",
|
|
227
|
+
status: "warn",
|
|
228
|
+
message: "Unable to query sensor interfaces."
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const netCheck = await run("ip route 2>/dev/null || route -n 2>/dev/null", "");
|
|
233
|
+
if (netCheck.includes("default") || netCheck.includes("0.0.0.0")) {
|
|
234
|
+
checks.push({
|
|
235
|
+
name: "Network Reachability",
|
|
236
|
+
status: "ok",
|
|
237
|
+
message: "Active default route detected."
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
checks.push({
|
|
241
|
+
name: "Network Reachability",
|
|
242
|
+
status: "warn",
|
|
243
|
+
message: "No default network gateway detected."
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
checks.push({
|
|
248
|
+
name: "Network Reachability",
|
|
249
|
+
status: "warn",
|
|
250
|
+
message: "Could not verify network routes."
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const sysctlRes = await run("systemctl --user status 2>&1", "");
|
|
255
|
+
if (!sysctlRes.includes("Failed to connect to bus") && !sysctlRes.includes("not found")) {
|
|
256
|
+
checks.push({
|
|
257
|
+
name: "Systemd User Daemon",
|
|
258
|
+
status: "ok",
|
|
259
|
+
message: "Systemd user session active (ready for pyre --install)."
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
checks.push({
|
|
263
|
+
name: "Systemd User Daemon",
|
|
264
|
+
status: "warn",
|
|
265
|
+
message: "Systemd user session not available.",
|
|
266
|
+
details: "Background services via pyre --install require an active user systemd session."
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
checks.push({
|
|
271
|
+
name: "Systemd User Daemon",
|
|
272
|
+
status: "warn",
|
|
273
|
+
message: "Systemd check skipped."
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
checks.push(checkConfigIntegrity());
|
|
277
|
+
return checks;
|
|
278
|
+
}
|
|
279
|
+
async function runWindowsDoctor() {
|
|
280
|
+
const checks = [];
|
|
281
|
+
try {
|
|
282
|
+
const psCheck = await run("tasklist /nh 2>nul", "");
|
|
283
|
+
if (psCheck.length > 0) {
|
|
284
|
+
checks.push({
|
|
285
|
+
name: "Process Visibility",
|
|
286
|
+
status: "ok",
|
|
287
|
+
message: "Windows process table access available."
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
checks.push({
|
|
291
|
+
name: "Process Visibility",
|
|
292
|
+
status: "warn",
|
|
293
|
+
message: "Tasklist query returned no output."
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
checks.push({
|
|
298
|
+
name: "Process Visibility",
|
|
299
|
+
status: "error",
|
|
300
|
+
message: "Unable to query Windows tasklist."
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const wmiCheck = await run('wmic os get Caption 2>nul || powershell -Command "(Get-CimInstance Win32_OperatingSystem).Caption"', "");
|
|
305
|
+
if (wmiCheck.toLowerCase().includes("windows")) {
|
|
306
|
+
checks.push({
|
|
307
|
+
name: "WMI / CIM Subsystem",
|
|
308
|
+
status: "ok",
|
|
309
|
+
message: "WMI/CIM management queries responsive."
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
checks.push({
|
|
313
|
+
name: "WMI / CIM Subsystem",
|
|
314
|
+
status: "warn",
|
|
315
|
+
message: "WMI/CIM query did not return expected response.",
|
|
316
|
+
details: "Some hardware metrics (disks, thermal) rely on WMI or PowerShell CIM."
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
checks.push({
|
|
321
|
+
name: "WMI / CIM Subsystem",
|
|
322
|
+
status: "warn",
|
|
323
|
+
message: "Could not execute WMI query."
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const netCheck = await run("netstat -rn 2>nul", "");
|
|
328
|
+
if (netCheck.includes("0.0.0.0")) {
|
|
329
|
+
checks.push({
|
|
330
|
+
name: "Network Reachability",
|
|
331
|
+
status: "ok",
|
|
332
|
+
message: "Active default IPv4 gateway detected."
|
|
333
|
+
});
|
|
334
|
+
} else {
|
|
335
|
+
checks.push({
|
|
336
|
+
name: "Network Reachability",
|
|
337
|
+
status: "warn",
|
|
338
|
+
message: "No default route detected in routing table."
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
checks.push({
|
|
343
|
+
name: "Network Reachability",
|
|
344
|
+
status: "warn",
|
|
345
|
+
message: "Could not verify Windows network routes."
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
checks.push(checkConfigIntegrity());
|
|
349
|
+
return checks;
|
|
350
|
+
}
|
|
351
|
+
function checkConfigIntegrity() {
|
|
352
|
+
try {
|
|
353
|
+
readConfig();
|
|
354
|
+
return {
|
|
355
|
+
name: "Configuration File",
|
|
356
|
+
status: "ok",
|
|
357
|
+
message: `Config file valid (${CONFIG_FILE}).`
|
|
358
|
+
};
|
|
359
|
+
} catch (err) {
|
|
360
|
+
return {
|
|
361
|
+
name: "Configuration File",
|
|
362
|
+
status: "error",
|
|
363
|
+
message: "Failed to parse config file.",
|
|
364
|
+
details: err.message
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function printDoctorReport(checks) {
|
|
369
|
+
const osLabel = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
|
|
370
|
+
console.log(chalk.bold(`
|
|
371
|
+
pyre doctor \u2014 System Diagnostics (${osLabel})
|
|
372
|
+
`));
|
|
373
|
+
for (const check of checks) {
|
|
374
|
+
let icon = chalk.green("\u2714");
|
|
375
|
+
if (check.status === "warn") icon = chalk.yellow("\u26A0");
|
|
376
|
+
if (check.status === "error") icon = chalk.red("\u2716");
|
|
377
|
+
console.log(` ${icon} ${chalk.bold(check.name.padEnd(26))} ${check.message}`);
|
|
378
|
+
if (check.details) {
|
|
379
|
+
console.log(chalk.dim(` \u21B3 ${check.details}`));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
console.log();
|
|
383
|
+
}
|
|
384
|
+
export {
|
|
385
|
+
printDoctorReport,
|
|
386
|
+
runDoctor
|
|
387
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
explainAnomaliesLocally
|
|
3
|
+
} from "./chunk-ZLVIVFSO.js";
|
|
4
|
+
|
|
5
|
+
// src/explainCmd.ts
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
function parseAnomalies(dir, sinceStr) {
|
|
11
|
+
if (!fs.existsSync(dir)) return [];
|
|
12
|
+
const files = fs.readdirSync(dir).filter((f) => f.startsWith("pyre-") && f.endsWith(".csv"));
|
|
13
|
+
if (files.length === 0) return [];
|
|
14
|
+
let cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
15
|
+
if (sinceStr === "yesterday" || sinceStr === "1d") cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
16
|
+
else if (sinceStr.endsWith("d")) {
|
|
17
|
+
const d = parseInt(sinceStr.slice(0, -1), 10);
|
|
18
|
+
if (!isNaN(d)) cutoff = Date.now() - d * 24 * 60 * 60 * 1e3;
|
|
19
|
+
} else if (!isNaN(Date.parse(sinceStr))) {
|
|
20
|
+
cutoff = Date.parse(sinceStr);
|
|
21
|
+
}
|
|
22
|
+
const rows = [];
|
|
23
|
+
for (const file of files.sort()) {
|
|
24
|
+
try {
|
|
25
|
+
const content = fs.readFileSync(path.join(dir, file), "utf-8");
|
|
26
|
+
for (const line of content.split("\n").slice(1)) {
|
|
27
|
+
if (!line.trim()) continue;
|
|
28
|
+
const parts = line.split(",");
|
|
29
|
+
if (parts.length < 3) continue;
|
|
30
|
+
const ts = new Date(parts[0]);
|
|
31
|
+
if (!isNaN(ts.getTime()) && ts.getTime() >= cutoff) {
|
|
32
|
+
const cpu = parseFloat(parts[1]);
|
|
33
|
+
const mem = parseFloat(parts[2]);
|
|
34
|
+
const temp = parts[3] ? parseFloat(parts[3]) : NaN;
|
|
35
|
+
if (!isNaN(cpu) && !isNaN(mem)) {
|
|
36
|
+
rows.push({ ts, cpu, mem, temp: isNaN(temp) ? void 0 : temp });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (rows.length < 5) return [];
|
|
44
|
+
const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
45
|
+
const std = (arr, m) => Math.sqrt(arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length);
|
|
46
|
+
const cpuVals = rows.map((r) => r.cpu);
|
|
47
|
+
const memVals = rows.map((r) => r.mem);
|
|
48
|
+
const tempVals = rows.filter((r) => r.temp !== void 0).map((r) => r.temp);
|
|
49
|
+
const cpuMean = mean(cpuVals);
|
|
50
|
+
const cpuStd = std(cpuVals, cpuMean);
|
|
51
|
+
const memMean = mean(memVals);
|
|
52
|
+
const memStd = std(memVals, memMean);
|
|
53
|
+
const tempMean = tempVals.length ? mean(tempVals) : 0;
|
|
54
|
+
const tempStd = tempVals.length ? std(tempVals, tempMean) : 0;
|
|
55
|
+
const anomalies = [];
|
|
56
|
+
for (const r of rows) {
|
|
57
|
+
if (cpuStd > 0) {
|
|
58
|
+
const z = (r.cpu - cpuMean) / cpuStd;
|
|
59
|
+
if (z >= 2.5) anomalies.push({ timestamp: r.ts.toISOString(), metric: "CPU", value: r.cpu, baseline: cpuMean, zScore: z });
|
|
60
|
+
}
|
|
61
|
+
if (memStd > 0) {
|
|
62
|
+
const z = (r.mem - memMean) / memStd;
|
|
63
|
+
if (z >= 2.5) anomalies.push({ timestamp: r.ts.toISOString(), metric: "Memory", value: r.mem, baseline: memMean, zScore: z });
|
|
64
|
+
}
|
|
65
|
+
if (r.temp !== void 0 && tempStd > 0) {
|
|
66
|
+
const z = (r.temp - tempMean) / tempStd;
|
|
67
|
+
if (z >= 2.5) anomalies.push({ timestamp: r.ts.toISOString(), metric: "Temperature", value: r.temp, baseline: tempMean, zScore: z });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return anomalies;
|
|
71
|
+
}
|
|
72
|
+
function buildPrompt(anomalies, hostname, sinceStr) {
|
|
73
|
+
const osLabel = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
|
|
74
|
+
const lines = anomalies.slice(0, 10).map((a) => {
|
|
75
|
+
const unit = a.metric === "Temperature" ? "\xB0C" : "%";
|
|
76
|
+
return `- ${a.metric} spike at ${a.timestamp}: ${a.value.toFixed(1)}${unit} (baseline ${a.baseline.toFixed(1)}${unit}, +${a.zScore.toFixed(1)}\u03C3)`;
|
|
77
|
+
});
|
|
78
|
+
return [
|
|
79
|
+
`You are a ${osLabel} system performance expert analyzing a monitoring log from "${hostname}".`,
|
|
80
|
+
`The following statistical anomalies (z-score \u2265 2.5\u03C3) were detected in the past ${sinceStr}:`,
|
|
81
|
+
"",
|
|
82
|
+
...lines,
|
|
83
|
+
"",
|
|
84
|
+
"For each anomaly, provide:",
|
|
85
|
+
`1. A plain-English explanation of what likely caused it (be specific to ${osLabel})`,
|
|
86
|
+
"2. Whether it is likely harmless or worth investigating",
|
|
87
|
+
"3. One actionable recommendation the user can take right now",
|
|
88
|
+
"",
|
|
89
|
+
"Be concise (2-3 sentences per anomaly). Use bullet points. Do not use technical jargon."
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
async function queryOllama(prompt, model) {
|
|
93
|
+
const res = await fetch("http://localhost:11434/api/generate", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ model, prompt, stream: false }),
|
|
97
|
+
signal: AbortSignal.timeout(6e4)
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) throw new Error(`Ollama error: ${res.status} ${res.statusText}`);
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
if (data.error) throw new Error(data.error);
|
|
102
|
+
return data.response ?? "";
|
|
103
|
+
}
|
|
104
|
+
async function queryOpenAI(prompt, model, apiKey) {
|
|
105
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
108
|
+
body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }], max_tokens: 800 }),
|
|
109
|
+
signal: AbortSignal.timeout(6e4)
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) throw new Error(`OpenAI error: ${res.status} ${await res.text()}`);
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
114
|
+
}
|
|
115
|
+
async function runExplainCommand(opts = {}) {
|
|
116
|
+
const {
|
|
117
|
+
since = "7d",
|
|
118
|
+
backend = "builtin",
|
|
119
|
+
model = backend === "openai" ? "gpt-4o-mini" : backend === "ollama" ? "llama3.2" : "expert-rules-v1",
|
|
120
|
+
apiKey = process.env["OPENAI_API_KEY"] ?? "",
|
|
121
|
+
dir = "./pyre-exports"
|
|
122
|
+
} = opts;
|
|
123
|
+
console.log(chalk.bold("\n \u{1F525} pyre explain \u2014 AI Anomaly Analysis\n"));
|
|
124
|
+
const anomalies = parseAnomalies(dir, since);
|
|
125
|
+
if (anomalies.length === 0) {
|
|
126
|
+
console.log(chalk.green(" \u2714 No anomalies detected in the past " + since + "."));
|
|
127
|
+
console.log(chalk.dim(" Run pyre live --log to build a history first.\n"));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (backend === "builtin") {
|
|
131
|
+
console.log(chalk.dim(` Analyzing ${anomalies.length} anomaly event(s) using local built-in model (${model})...
|
|
132
|
+
`));
|
|
133
|
+
const alerts = anomalies.map((a) => ({
|
|
134
|
+
metric: a.metric,
|
|
135
|
+
value: a.value,
|
|
136
|
+
zScore: a.zScore,
|
|
137
|
+
severity: a.zScore >= 3.5 ? "critical" : "warning",
|
|
138
|
+
timestamp: new Date(a.timestamp)
|
|
139
|
+
}));
|
|
140
|
+
const analysis = explainAnomaliesLocally(alerts, model);
|
|
141
|
+
console.log(chalk.bold(" Analysis:\n"));
|
|
142
|
+
for (const line of analysis.split("\n")) {
|
|
143
|
+
if (line.startsWith("Diagnosis Engine:")) {
|
|
144
|
+
console.log(chalk.bold.magenta(" " + line));
|
|
145
|
+
} else if (line.startsWith("Tip:")) {
|
|
146
|
+
console.log(chalk.cyan.bold(" " + line));
|
|
147
|
+
} else if (line.startsWith(" \u2022 Recommendation:")) {
|
|
148
|
+
console.log(" " + chalk.green(line));
|
|
149
|
+
} else if (line.startsWith(" \u2022 Cause:")) {
|
|
150
|
+
console.log(" " + chalk.yellow(line));
|
|
151
|
+
} else if (line.startsWith(" \u2022 Verdict:")) {
|
|
152
|
+
console.log(" " + chalk.dim(line));
|
|
153
|
+
} else {
|
|
154
|
+
console.log(" " + chalk.dim(line));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
console.log();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(chalk.dim(` Found ${anomalies.length} anomaly event(s). Sending to ${backend} (${model})...
|
|
161
|
+
`));
|
|
162
|
+
const hostname = os.hostname();
|
|
163
|
+
const prompt = buildPrompt(anomalies, hostname, since);
|
|
164
|
+
let explanation = "";
|
|
165
|
+
try {
|
|
166
|
+
if (backend === "openai") {
|
|
167
|
+
if (!apiKey) {
|
|
168
|
+
console.log(chalk.yellow(" \u26A0 No API key found."));
|
|
169
|
+
console.log(chalk.dim(" Set OPENAI_API_KEY environment variable or use --api-key <key>\n"));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
explanation = await queryOpenAI(prompt, model, apiKey);
|
|
173
|
+
} else {
|
|
174
|
+
explanation = await queryOllama(prompt, model);
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
178
|
+
if (backend === "ollama" && (msg.includes("ECONNREFUSED") || msg.includes("fetch failed"))) {
|
|
179
|
+
console.log(chalk.yellow(" \u26A0 Ollama server is not running at localhost:11434.\n"));
|
|
180
|
+
console.log(chalk.dim(" Switching to local built-in AI model (offline, zero-setup)...\n"));
|
|
181
|
+
const alerts = anomalies.map((a) => ({
|
|
182
|
+
metric: a.metric,
|
|
183
|
+
value: a.value,
|
|
184
|
+
zScore: a.zScore,
|
|
185
|
+
severity: a.zScore >= 3.5 ? "critical" : "warning",
|
|
186
|
+
timestamp: new Date(a.timestamp)
|
|
187
|
+
}));
|
|
188
|
+
const fallbackAnalysis = explainAnomaliesLocally(alerts, "expert-rules-v1");
|
|
189
|
+
console.log(chalk.bold(" Analysis (Built-in Heuristic Model):\n"));
|
|
190
|
+
for (const line of fallbackAnalysis.split("\n")) {
|
|
191
|
+
console.log(" " + line);
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
return;
|
|
195
|
+
} else {
|
|
196
|
+
console.log(chalk.red(` \u2716 LLM error: ${msg}
|
|
197
|
+
`));
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!explanation.trim()) {
|
|
202
|
+
console.log(chalk.yellow(" \u26A0 LLM returned an empty response. Try a different model.\n"));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
console.log(chalk.bold(" Analysis:\n"));
|
|
206
|
+
for (const line of explanation.split("\n")) {
|
|
207
|
+
const t = line.trim();
|
|
208
|
+
if (t.startsWith("-") || t.startsWith("\u2022")) {
|
|
209
|
+
console.log(chalk.dim(" ") + chalk.white(t));
|
|
210
|
+
} else if (t.match(/^\d+\./)) {
|
|
211
|
+
console.log(chalk.dim(" ") + chalk.cyan(t));
|
|
212
|
+
} else if (t) {
|
|
213
|
+
console.log(" " + t);
|
|
214
|
+
} else {
|
|
215
|
+
console.log("");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
console.log();
|
|
219
|
+
}
|
|
220
|
+
export {
|
|
221
|
+
runExplainCommand
|
|
222
|
+
};
|