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.
Files changed (86) hide show
  1. package/README.md +73 -16
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
  7. package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
  8. package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
  9. package/dist/chunk-GHRDW646.js +2506 -0
  10. package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
  11. package/dist/chunk-M73HCQD2.js +95 -0
  12. package/dist/chunk-MZO5QKUR.js +32 -0
  13. package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
  19. package/dist/doctor-GYHFTNZQ.js +387 -0
  20. package/dist/explainCmd-MF6PIMLW.js +222 -0
  21. package/dist/extensions-XJGHG7X3.js +134 -0
  22. package/dist/index.js +460 -88
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
  26. package/dist/profileProcCmd-N4H6MCBT.js +107 -0
  27. package/dist/prometheus-5BL5PVLC.js +6 -0
  28. package/dist/prometheusCmd-PL64BLN5.js +93 -0
  29. package/dist/replayCmd-Z3PYXY7V.js +164 -0
  30. package/dist/run-HOGQDDTA.js +8 -0
  31. package/dist/serveCmd-2H5VLF3P.js +153 -0
  32. package/dist/service-Q74BBILO.js +181 -0
  33. package/dist/smartCmd-ODWKSFNG.js +166 -0
  34. package/dist/syncCmd-A2W3QB3S.js +126 -0
  35. package/dist/topoCmd-DU2HHZNH.js +148 -0
  36. package/dist/watchdogCmd-UF53JPZB.js +161 -0
  37. package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
  38. package/install.ps1 +29 -0
  39. package/install.sh +10 -9
  40. package/package.json +7 -3
  41. package/src/ai/models.ts +138 -0
  42. package/src/alertChannels.ts +119 -0
  43. package/src/brewHealth.ts +157 -40
  44. package/src/buildCmd.ts +48 -0
  45. package/src/checkCmd.ts +5 -5
  46. package/src/completions.ts +35 -0
  47. package/src/doctor.ts +235 -7
  48. package/src/easy_features.test.ts +40 -0
  49. package/src/explainCmd.ts +261 -0
  50. package/src/extensions.ts +71 -7
  51. package/src/formatters/render.ts +34 -10
  52. package/src/formatters/types.ts +2 -0
  53. package/src/index.ts +251 -36
  54. package/src/live/input.ts +60 -0
  55. package/src/live/render.ts +67 -0
  56. package/src/live/state.ts +5 -0
  57. package/src/live/types.ts +1 -1
  58. package/src/monitors/batteryPredictor.ts +143 -0
  59. package/src/monitors/buildTracker.ts +121 -0
  60. package/src/monitors/collectors.ts +342 -178
  61. package/src/monitors/ioreport.py +72 -0
  62. package/src/monitors/platform/linux.ts +756 -0
  63. package/src/monitors/platform/windows.ts +519 -0
  64. package/src/monitors/run.ts +27 -3
  65. package/src/monitors/smc.ts +21 -1
  66. package/src/monitors/types.ts +15 -0
  67. package/src/netUsageCmd.ts +121 -0
  68. package/src/platform.test.ts +99 -0
  69. package/src/plugins/index.ts +143 -0
  70. package/src/profileProcCmd.ts +133 -0
  71. package/src/prometheusCmd.ts +107 -0
  72. package/src/replayCmd.ts +186 -0
  73. package/src/serveCmd.ts +180 -0
  74. package/src/service.ts +125 -7
  75. package/src/smartCmd.ts +205 -0
  76. package/src/state/config.ts +58 -17
  77. package/src/syncCmd.ts +166 -0
  78. package/src/topoCmd.ts +176 -0
  79. package/src/watchdogCmd.ts +198 -0
  80. package/src/xbar.ts +22 -11
  81. package/dist/brewHealth-F2BXF5M6.js +0 -74
  82. package/dist/chunk-FH5WDJBF.js +0 -15
  83. package/dist/chunk-N4WJVF5M.js +0 -1184
  84. package/dist/doctor-UV7TWIHX.js +0 -184
  85. package/dist/extensions-DVA5YLGH.js +0 -74
  86. 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\.-]+)/i);
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\.-]+)\s+\(([^)]+)\)\s+\[([^\]]+)\]/i);
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
- console.log(chalk.bold('\n pyre extensions — System Extensions Inspector\n'));
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
- for (const ext of cat.extensions) {
73
- const stateColor = ext.state.includes('activated') || ext.state.includes('enabled') ? chalk.green : chalk.yellow;
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 system extensions detected on this system.\n'));
145
+ console.log(chalk.dim(' No active extensions/modules detected on this system.\n'));
82
146
  }
83
147
  }
@@ -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.dischargeRatePerHour !== undefined && data.battery.state !== 'charged') {
318
- lines.push(statRow('Disch. Rate', `${data.battery.dischargeRatePerHour}%/h`));
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 && !data.battery) return ['No power data available'];
816
- const lines: string[] = [];
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': {
@@ -91,4 +91,6 @@ export interface TableOptions {
91
91
  inspectProcess?: any;
92
92
  /** Minimal mode: renders only CPU, GPU, Memory, and Temperature as large glanceable gauges. */
93
93
  minimal?: boolean;
94
+ /** Current selected AI diagnosis model ID. */
95
+ aiModel?: string;
94
96
  }