pyre-cli 9.5.2 → 10.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/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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre explain — AI-powered anomaly explanation.
|
|
3
|
+
*
|
|
4
|
+
* Reads anomaly spikes from pyre CSV logs and sends them to a
|
|
5
|
+
* local Ollama instance (default) or an OpenAI-compatible API
|
|
6
|
+
* to generate plain-English diagnosis and actionable advice.
|
|
7
|
+
*
|
|
8
|
+
* Backends:
|
|
9
|
+
* - ollama (default): POST http://localhost:11434/api/generate
|
|
10
|
+
* - openai: POST https://api.openai.com/v1/chat/completions
|
|
11
|
+
*
|
|
12
|
+
* No new dependencies — uses Node.js 18+ built-in fetch.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { explainAnomaliesLocally } from './ai/models.js';
|
|
19
|
+
|
|
20
|
+
export interface ExplainOptions {
|
|
21
|
+
since?: string;
|
|
22
|
+
backend?: 'builtin' | 'ollama' | 'openai';
|
|
23
|
+
model?: string;
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
dir?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AnomalyEvent {
|
|
29
|
+
timestamp: string;
|
|
30
|
+
metric: string;
|
|
31
|
+
value: number;
|
|
32
|
+
baseline: number;
|
|
33
|
+
zScore: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseAnomalies(dir: string, sinceStr: string): AnomalyEvent[] {
|
|
37
|
+
if (!fs.existsSync(dir)) return [];
|
|
38
|
+
const files = fs.readdirSync(dir).filter(f => f.startsWith('pyre-') && f.endsWith('.csv'));
|
|
39
|
+
if (files.length === 0) return [];
|
|
40
|
+
|
|
41
|
+
let cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
42
|
+
if (sinceStr === 'yesterday' || sinceStr === '1d') cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
43
|
+
else if (sinceStr.endsWith('d')) {
|
|
44
|
+
const d = parseInt(sinceStr.slice(0, -1), 10);
|
|
45
|
+
if (!isNaN(d)) cutoff = Date.now() - d * 24 * 60 * 60 * 1000;
|
|
46
|
+
} else if (!isNaN(Date.parse(sinceStr))) {
|
|
47
|
+
cutoff = Date.parse(sinceStr);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface LogRow { ts: Date; cpu: number; mem: number; temp?: number; }
|
|
51
|
+
const rows: LogRow[] = [];
|
|
52
|
+
|
|
53
|
+
for (const file of files.sort()) {
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
56
|
+
for (const line of content.split('\n').slice(1)) {
|
|
57
|
+
if (!line.trim()) continue;
|
|
58
|
+
const parts = line.split(',');
|
|
59
|
+
if (parts.length < 3) continue;
|
|
60
|
+
const ts = new Date(parts[0]);
|
|
61
|
+
if (!isNaN(ts.getTime()) && ts.getTime() >= cutoff) {
|
|
62
|
+
const cpu = parseFloat(parts[1]);
|
|
63
|
+
const mem = parseFloat(parts[2]);
|
|
64
|
+
const temp = parts[3] ? parseFloat(parts[3]) : NaN;
|
|
65
|
+
if (!isNaN(cpu) && !isNaN(mem)) {
|
|
66
|
+
rows.push({ ts, cpu, mem, temp: isNaN(temp) ? undefined : temp });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch { /* ignore */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (rows.length < 5) return [];
|
|
74
|
+
|
|
75
|
+
const mean = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
76
|
+
const std = (arr: number[], m: number) => Math.sqrt(arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length);
|
|
77
|
+
|
|
78
|
+
const cpuVals = rows.map(r => r.cpu);
|
|
79
|
+
const memVals = rows.map(r => r.mem);
|
|
80
|
+
const tempVals = rows.filter(r => r.temp !== undefined).map(r => r.temp!);
|
|
81
|
+
|
|
82
|
+
const cpuMean = mean(cpuVals); const cpuStd = std(cpuVals, cpuMean);
|
|
83
|
+
const memMean = mean(memVals); const memStd = std(memVals, memMean);
|
|
84
|
+
const tempMean = tempVals.length ? mean(tempVals) : 0;
|
|
85
|
+
const tempStd = tempVals.length ? std(tempVals, tempMean) : 0;
|
|
86
|
+
|
|
87
|
+
const anomalies: AnomalyEvent[] = [];
|
|
88
|
+
for (const r of rows) {
|
|
89
|
+
if (cpuStd > 0) {
|
|
90
|
+
const z = (r.cpu - cpuMean) / cpuStd;
|
|
91
|
+
if (z >= 2.5) anomalies.push({ timestamp: r.ts.toISOString(), metric: 'CPU', value: r.cpu, baseline: cpuMean, zScore: z });
|
|
92
|
+
}
|
|
93
|
+
if (memStd > 0) {
|
|
94
|
+
const z = (r.mem - memMean) / memStd;
|
|
95
|
+
if (z >= 2.5) anomalies.push({ timestamp: r.ts.toISOString(), metric: 'Memory', value: r.mem, baseline: memMean, zScore: z });
|
|
96
|
+
}
|
|
97
|
+
if (r.temp !== undefined && tempStd > 0) {
|
|
98
|
+
const z = (r.temp - tempMean) / tempStd;
|
|
99
|
+
if (z >= 2.5) anomalies.push({ timestamp: r.ts.toISOString(), metric: 'Temperature', value: r.temp, baseline: tempMean, zScore: z });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return anomalies;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildPrompt(anomalies: AnomalyEvent[], hostname: string, sinceStr: string): string {
|
|
106
|
+
const osLabel = process.platform === 'darwin' ? 'macOS' : (process.platform === 'win32' ? 'Windows' : 'Linux');
|
|
107
|
+
const lines = anomalies.slice(0, 10).map(a => {
|
|
108
|
+
const unit = a.metric === 'Temperature' ? '°C' : '%';
|
|
109
|
+
return `- ${a.metric} spike at ${a.timestamp}: ${a.value.toFixed(1)}${unit} (baseline ${a.baseline.toFixed(1)}${unit}, +${a.zScore.toFixed(1)}σ)`;
|
|
110
|
+
});
|
|
111
|
+
return [
|
|
112
|
+
`You are a ${osLabel} system performance expert analyzing a monitoring log from "${hostname}".`,
|
|
113
|
+
`The following statistical anomalies (z-score ≥ 2.5σ) were detected in the past ${sinceStr}:`,
|
|
114
|
+
'',
|
|
115
|
+
...lines,
|
|
116
|
+
'',
|
|
117
|
+
'For each anomaly, provide:',
|
|
118
|
+
`1. A plain-English explanation of what likely caused it (be specific to ${osLabel})`,
|
|
119
|
+
'2. Whether it is likely harmless or worth investigating',
|
|
120
|
+
'3. One actionable recommendation the user can take right now',
|
|
121
|
+
'',
|
|
122
|
+
'Be concise (2-3 sentences per anomaly). Use bullet points. Do not use technical jargon.',
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function queryOllama(prompt: string, model: string): Promise<string> {
|
|
127
|
+
const res = await fetch('http://localhost:11434/api/generate', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ model, prompt, stream: false }),
|
|
131
|
+
signal: AbortSignal.timeout(60000),
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) throw new Error(`Ollama error: ${res.status} ${res.statusText}`);
|
|
134
|
+
const data = await res.json() as { response?: string; error?: string };
|
|
135
|
+
if (data.error) throw new Error(data.error);
|
|
136
|
+
return data.response ?? '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function queryOpenAI(prompt: string, model: string, apiKey: string): Promise<string> {
|
|
140
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
143
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }], max_tokens: 800 }),
|
|
144
|
+
signal: AbortSignal.timeout(60000),
|
|
145
|
+
});
|
|
146
|
+
if (!res.ok) throw new Error(`OpenAI error: ${res.status} ${await res.text()}`);
|
|
147
|
+
const data = await res.json() as { choices?: Array<{ message: { content: string } }> };
|
|
148
|
+
return data.choices?.[0]?.message?.content ?? '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function runExplainCommand(opts: ExplainOptions = {}): Promise<void> {
|
|
152
|
+
const {
|
|
153
|
+
since = '7d',
|
|
154
|
+
backend = 'builtin',
|
|
155
|
+
model = backend === 'openai' ? 'gpt-4o-mini' : backend === 'ollama' ? 'llama3.2' : 'expert-rules-v1',
|
|
156
|
+
apiKey = process.env['OPENAI_API_KEY'] ?? '',
|
|
157
|
+
dir = './pyre-exports',
|
|
158
|
+
} = opts;
|
|
159
|
+
|
|
160
|
+
console.log(chalk.bold('\n 🔥 pyre explain — AI Anomaly Analysis\n'));
|
|
161
|
+
|
|
162
|
+
const anomalies = parseAnomalies(dir, since);
|
|
163
|
+
|
|
164
|
+
if (anomalies.length === 0) {
|
|
165
|
+
console.log(chalk.green(' ✔ No anomalies detected in the past ' + since + '.'));
|
|
166
|
+
console.log(chalk.dim(' Run pyre live --log to build a history first.\n'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (backend === 'builtin') {
|
|
171
|
+
console.log(chalk.dim(` Analyzing ${anomalies.length} anomaly event(s) using local built-in model (${model})...\n`));
|
|
172
|
+
const alerts = anomalies.map(a => ({
|
|
173
|
+
metric: a.metric,
|
|
174
|
+
value: a.value,
|
|
175
|
+
zScore: a.zScore,
|
|
176
|
+
severity: (a.zScore >= 3.5 ? 'critical' : 'warning') as 'critical' | 'warning',
|
|
177
|
+
timestamp: new Date(a.timestamp),
|
|
178
|
+
}));
|
|
179
|
+
const analysis = explainAnomaliesLocally(alerts, model);
|
|
180
|
+
console.log(chalk.bold(' Analysis:\n'));
|
|
181
|
+
for (const line of analysis.split('\n')) {
|
|
182
|
+
if (line.startsWith('Diagnosis Engine:')) {
|
|
183
|
+
console.log(chalk.bold.magenta(' ' + line));
|
|
184
|
+
} else if (line.startsWith('Tip:')) {
|
|
185
|
+
console.log(chalk.cyan.bold(' ' + line));
|
|
186
|
+
} else if (line.startsWith(' • Recommendation:')) {
|
|
187
|
+
console.log(' ' + chalk.green(line));
|
|
188
|
+
} else if (line.startsWith(' • Cause:')) {
|
|
189
|
+
console.log(' ' + chalk.yellow(line));
|
|
190
|
+
} else if (line.startsWith(' • Verdict:')) {
|
|
191
|
+
console.log(' ' + chalk.dim(line));
|
|
192
|
+
} else {
|
|
193
|
+
console.log(' ' + chalk.dim(line));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
console.log();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(chalk.dim(` Found ${anomalies.length} anomaly event(s). Sending to ${backend} (${model})...\n`));
|
|
201
|
+
|
|
202
|
+
const hostname = os.hostname();
|
|
203
|
+
const prompt = buildPrompt(anomalies, hostname, since);
|
|
204
|
+
let explanation = '';
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
if (backend === 'openai') {
|
|
208
|
+
if (!apiKey) {
|
|
209
|
+
console.log(chalk.yellow(' ⚠ No API key found.'));
|
|
210
|
+
console.log(chalk.dim(' Set OPENAI_API_KEY environment variable or use --api-key <key>\n'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
explanation = await queryOpenAI(prompt, model, apiKey);
|
|
214
|
+
} else {
|
|
215
|
+
explanation = await queryOllama(prompt, model);
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
219
|
+
if (backend === 'ollama' && (msg.includes('ECONNREFUSED') || msg.includes('fetch failed'))) {
|
|
220
|
+
console.log(chalk.yellow(' ⚠ Ollama server is not running at localhost:11434.\n'));
|
|
221
|
+
console.log(chalk.dim(' Switching to local built-in AI model (offline, zero-setup)...\n'));
|
|
222
|
+
const alerts = anomalies.map(a => ({
|
|
223
|
+
metric: a.metric,
|
|
224
|
+
value: a.value,
|
|
225
|
+
zScore: a.zScore,
|
|
226
|
+
severity: (a.zScore >= 3.5 ? 'critical' : 'warning') as 'critical' | 'warning',
|
|
227
|
+
timestamp: new Date(a.timestamp),
|
|
228
|
+
}));
|
|
229
|
+
const fallbackAnalysis = explainAnomaliesLocally(alerts, 'expert-rules-v1');
|
|
230
|
+
console.log(chalk.bold(' Analysis (Built-in Heuristic Model):\n'));
|
|
231
|
+
for (const line of fallbackAnalysis.split('\n')) {
|
|
232
|
+
console.log(' ' + line);
|
|
233
|
+
}
|
|
234
|
+
console.log();
|
|
235
|
+
return;
|
|
236
|
+
} else {
|
|
237
|
+
console.log(chalk.red(` ✖ LLM error: ${msg}\n`));
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!explanation.trim()) {
|
|
243
|
+
console.log(chalk.yellow(' ⚠ LLM returned an empty response. Try a different model.\n'));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log(chalk.bold(' Analysis:\n'));
|
|
248
|
+
for (const line of explanation.split('\n')) {
|
|
249
|
+
const t = line.trim();
|
|
250
|
+
if (t.startsWith('-') || t.startsWith('•')) {
|
|
251
|
+
console.log(chalk.dim(' ') + chalk.white(t));
|
|
252
|
+
} else if (t.match(/^\d+\./)) {
|
|
253
|
+
console.log(chalk.dim(' ') + chalk.cyan(t));
|
|
254
|
+
} else if (t) {
|
|
255
|
+
console.log(' ' + t);
|
|
256
|
+
} else {
|
|
257
|
+
console.log('');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
console.log();
|
|
261
|
+
}
|
package/src/extensions.ts
CHANGED
|
@@ -10,6 +10,10 @@ export interface SystemExtensionInfo {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export async function getSystemExtensions(): Promise<{ category: string; extensions: SystemExtensionInfo[] }[]> {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
if (platform === 'linux') return getLinuxExtensions();
|
|
15
|
+
if (platform === 'win32') return getWindowsExtensions();
|
|
16
|
+
|
|
13
17
|
try {
|
|
14
18
|
const raw = await run('systemextensionsctl list 2>&1', '');
|
|
15
19
|
return parseSystemExtensionsOutput(raw);
|
|
@@ -33,12 +37,12 @@ export function parseSystemExtensionsOutput(output: string): { category: string;
|
|
|
33
37
|
result.push({ category: currentCategory, extensions: currentList });
|
|
34
38
|
currentList = [];
|
|
35
39
|
}
|
|
36
|
-
const catMatch = trimmed.match(/category\s+([\w
|
|
40
|
+
const catMatch = trimmed.match(/category\s+([\w.-]+)/i);
|
|
37
41
|
currentCategory = catMatch ? catMatch[1] : trimmed.replace(/^-+\s*/, '').replace(/\s*-+$/, '');
|
|
38
42
|
continue;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
const extMatch = trimmed.match(/^(?:\*\s*)*([A-Z0-9]{10}|\?\?\?\?\?\?\?\?\?\?)\s+([\w
|
|
45
|
+
const extMatch = trimmed.match(/^(?:\*\s*)*([A-Z0-9]{10}|\?\?\?\?\?\?\?\?\?\?)\s+([\w.-]+)\s+\(([^)]+)\)\s+\[([^\]]+)\]/i);
|
|
42
46
|
if (extMatch) {
|
|
43
47
|
currentList.push({
|
|
44
48
|
teamId: extMatch[1],
|
|
@@ -57,8 +61,63 @@ export function parseSystemExtensionsOutput(output: string): { category: string;
|
|
|
57
61
|
return result.filter(group => group.extensions.length > 0 || result.length === 1);
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
async function getLinuxExtensions(): Promise<{ category: string; extensions: SystemExtensionInfo[] }[]> {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await run('lsmod 2>/dev/null', '');
|
|
67
|
+
const lines = raw.trim().split('\n').slice(1);
|
|
68
|
+
const extensions: SystemExtensionInfo[] = [];
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const parts = line.trim().split(/\s+/);
|
|
72
|
+
if (parts.length >= 3) {
|
|
73
|
+
const modName = parts[0];
|
|
74
|
+
const sizeBytes = parts[1];
|
|
75
|
+
const usedBy = parts.slice(3).join(' ') || 'none';
|
|
76
|
+
extensions.push({
|
|
77
|
+
teamId: 'KERNEL',
|
|
78
|
+
bundleId: modName,
|
|
79
|
+
version: `${sizeBytes} bytes`,
|
|
80
|
+
state: `used by: ${usedBy}`,
|
|
81
|
+
category: 'Kernel Modules',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return extensions.length > 0 ? [{ category: 'Linux Kernel Modules', extensions }] : [];
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getWindowsExtensions(): Promise<{ category: string; extensions: SystemExtensionInfo[] }[]> {
|
|
92
|
+
try {
|
|
93
|
+
const raw = await run('driverquery /fo csv /nh 2>nul', '', 3000);
|
|
94
|
+
const lines = raw.trim().split('\n');
|
|
95
|
+
const extensions: SystemExtensionInfo[] = [];
|
|
96
|
+
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const parts = line.split('","').map(s => s.replace(/(^"|"$)/g, '').trim());
|
|
99
|
+
if (parts.length >= 3) {
|
|
100
|
+
const modName = parts[0];
|
|
101
|
+
const dispName = parts[1];
|
|
102
|
+
const driverType = parts[2];
|
|
103
|
+
extensions.push({
|
|
104
|
+
teamId: driverType,
|
|
105
|
+
bundleId: modName,
|
|
106
|
+
version: dispName,
|
|
107
|
+
state: 'active',
|
|
108
|
+
category: 'Device Drivers',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return extensions.length > 0 ? [{ category: 'Windows Device Drivers', extensions }] : [];
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
60
118
|
export async function printExtensionsReport(): Promise<void> {
|
|
61
|
-
|
|
119
|
+
const osLabel = process.platform === 'darwin' ? 'macOS System Extensions' : (process.platform === 'win32' ? 'Windows Drivers' : 'Linux Kernel Modules');
|
|
120
|
+
console.log(chalk.bold(`\n pyre extensions — ${osLabel} Inspector\n`));
|
|
62
121
|
const categories = await getSystemExtensions();
|
|
63
122
|
|
|
64
123
|
let totalCount = 0;
|
|
@@ -69,15 +128,20 @@ export async function printExtensionsReport(): Promise<void> {
|
|
|
69
128
|
console.log(chalk.dim(' (No extensions registered in this category)'));
|
|
70
129
|
continue;
|
|
71
130
|
}
|
|
72
|
-
|
|
73
|
-
|
|
131
|
+
// Limit display so large module/driver tables do not overwhelm the terminal
|
|
132
|
+
const maxShow = 25;
|
|
133
|
+
for (const ext of cat.extensions.slice(0, maxShow)) {
|
|
134
|
+
const stateColor = ext.state.includes('activated') || ext.state.includes('enabled') || ext.state.includes('active') ? chalk.green : chalk.yellow;
|
|
74
135
|
console.log(` • ${chalk.bold(ext.bundleId)} (${ext.version})`);
|
|
75
|
-
console.log(chalk.dim(` Team: ${ext.teamId} | State: `) + stateColor(ext.state));
|
|
136
|
+
console.log(chalk.dim(` Type/Team: ${ext.teamId} | State: `) + stateColor(ext.state));
|
|
137
|
+
}
|
|
138
|
+
if (cat.extensions.length > maxShow) {
|
|
139
|
+
console.log(chalk.dim(` ... and ${cat.extensions.length - maxShow} more.`));
|
|
76
140
|
}
|
|
77
141
|
console.log();
|
|
78
142
|
}
|
|
79
143
|
|
|
80
144
|
if (totalCount === 0) {
|
|
81
|
-
console.log(chalk.dim(' No active
|
|
145
|
+
console.log(chalk.dim(' No active extensions/modules detected on this system.\n'));
|
|
82
146
|
}
|
|
83
147
|
}
|
package/src/formatters/render.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { History } from '../history.js';
|
|
|
15
15
|
import type { StatsData, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
|
|
16
16
|
import type { BlenderRenderData } from '../monitors/types.js';
|
|
17
17
|
import { THEMES, type ThemeName, type ThemeColors } from './themes.js';
|
|
18
|
+
import { explainAnomaliesLocally } from '../ai/models.js';
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
// --- low-level box drawing ---------------------------------------------------
|
|
@@ -314,8 +315,17 @@ function batteryCard(data: StatsData, contentWidth: number): string[] | null {
|
|
|
314
315
|
if (data.battery.estimatedTimeToEmpty && data.battery.state !== 'charged') {
|
|
315
316
|
lines.push(statRow('Est. Empty', data.battery.estimatedTimeToEmpty));
|
|
316
317
|
}
|
|
317
|
-
if (data.battery.
|
|
318
|
-
lines.push(statRow('
|
|
318
|
+
if (data.battery.powerWatts !== undefined && data.battery.powerWatts > 0 && data.battery.state !== 'charged') {
|
|
319
|
+
lines.push(statRow('Power Draw', `${data.battery.powerWatts.toFixed(2)} W`));
|
|
320
|
+
}
|
|
321
|
+
if (data.battery.smartPrediction) {
|
|
322
|
+
const sp = data.battery.smartPrediction;
|
|
323
|
+
lines.push('');
|
|
324
|
+
lines.push(chalk.bold('SMART RUNTIME FORECAST:'));
|
|
325
|
+
lines.push(statRow('Light Load', `${chalk.green(sp.scenarios.idle.timeRemaining)} (~${sp.scenarios.idle.ratePerHour}%/h)`));
|
|
326
|
+
lines.push(statRow('Current Load', `${chalk.yellow(sp.scenarios.current.timeRemaining)} (~${sp.scenarios.current.ratePerHour}%/h)`));
|
|
327
|
+
lines.push(statRow('Heavy Load', `${chalk.red(sp.scenarios.heavy.timeRemaining)} (~${sp.scenarios.heavy.ratePerHour}%/h)`));
|
|
328
|
+
lines.push(chalk.dim(`Advice: ${sp.recommendation}`));
|
|
319
329
|
}
|
|
320
330
|
return lines;
|
|
321
331
|
}
|
|
@@ -812,13 +822,8 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
|
|
|
812
822
|
}
|
|
813
823
|
case 'power': {
|
|
814
824
|
const power = powerCard(data);
|
|
815
|
-
if (!power
|
|
816
|
-
|
|
817
|
-
if (power) lines.push(...power);
|
|
818
|
-
if (data.battery?.powerWatts !== undefined && data.battery.powerWatts > 0) {
|
|
819
|
-
lines.push(statRow('Battery', `${data.battery.powerWatts.toFixed(2)} W`));
|
|
820
|
-
}
|
|
821
|
-
return lines.length ? lines : ['No power data available'];
|
|
825
|
+
if (!power) return ['No power data available'];
|
|
826
|
+
return power;
|
|
822
827
|
}
|
|
823
828
|
case 'battery':
|
|
824
829
|
return data.battery ? batteryCard(data, contentWidth) : ['No battery data available'];
|
|
@@ -954,13 +959,32 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
|
|
|
954
959
|
const lines: string[] = [];
|
|
955
960
|
// Sort by timestamp descending
|
|
956
961
|
const sorted = [...opts.anomalyHistory].sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0));
|
|
957
|
-
for (const a of sorted) {
|
|
962
|
+
for (const a of sorted.slice(0, 10)) {
|
|
958
963
|
const color = a.severity === 'critical' ? chalk.red.bold : chalk.yellow;
|
|
959
964
|
const direction = a.zScore > 0 ? '↑ spike' : '↓ drop';
|
|
960
965
|
const valStr = a.metric.toLowerCase().includes('net') ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
|
|
961
966
|
const timeStr = a.timestamp ? a.timestamp.toLocaleTimeString() : '';
|
|
962
967
|
lines.push(color(` ${timeStr.padEnd(10)} ⚠ ${a.metric.padEnd(8)}: ${valStr.padEnd(12)} (${direction}, σ=${a.zScore.toFixed(1)})`));
|
|
963
968
|
}
|
|
969
|
+
lines.push('');
|
|
970
|
+
// In-TUI AI Model Diagnosis section (offline, zero external server needed)
|
|
971
|
+
const modelId = opts.aiModel || 'expert-rules-v1';
|
|
972
|
+
const aiAnalysis = explainAnomaliesLocally(sorted, modelId);
|
|
973
|
+
for (const al of aiAnalysis.split('\n')) {
|
|
974
|
+
if (al.startsWith('Diagnosis Engine:')) {
|
|
975
|
+
lines.push(chalk.bold.magenta(al));
|
|
976
|
+
} else if (al.startsWith('Tip:')) {
|
|
977
|
+
lines.push(chalk.cyan.bold(al));
|
|
978
|
+
} else if (al.startsWith(' • Recommendation:')) {
|
|
979
|
+
lines.push(chalk.green(al));
|
|
980
|
+
} else if (al.startsWith(' • Cause:')) {
|
|
981
|
+
lines.push(chalk.yellow(al));
|
|
982
|
+
} else if (al.startsWith(' • Verdict:')) {
|
|
983
|
+
lines.push(chalk.dim(al));
|
|
984
|
+
} else {
|
|
985
|
+
lines.push(chalk.dim(al));
|
|
986
|
+
}
|
|
987
|
+
}
|
|
964
988
|
return lines;
|
|
965
989
|
}
|
|
966
990
|
case 'containers': {
|
package/src/formatters/types.ts
CHANGED