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
package/src/live/render.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel,
|
|
|
15
15
|
import { detectAnomalies } from '../anomalies.js';
|
|
16
16
|
import { state, setStatus, SIGNAL_OPTIONS, getToggleKey, MENU_OPTIONS } from './state.js';
|
|
17
17
|
import type { StatsData } from '../monitors/index.js';
|
|
18
|
+
import { sendAlert, type AlertChannelConfig } from '../alertChannels.js';
|
|
19
|
+
import { readConfig } from '../state/config.js';
|
|
20
|
+
import { AVAILABLE_AI_MODELS } from '../ai/models.js';
|
|
18
21
|
|
|
19
22
|
function formatTempForUnit(c: number): string {
|
|
20
23
|
if (state.tempUnit === 'f') {
|
|
@@ -61,6 +64,7 @@ function tableCacheParams(): string {
|
|
|
61
64
|
state.history.version,
|
|
62
65
|
JSON.stringify(state.visiblePanels),
|
|
63
66
|
JSON.stringify(state.panelLayout),
|
|
67
|
+
state.aiModel,
|
|
64
68
|
].join('|');
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -142,6 +146,11 @@ function renderCustomizerOverlay(scrollOffset: number, maxVisibleItems: number):
|
|
|
142
146
|
lines.push(`${prefix}${opt}: ${status}`);
|
|
143
147
|
} else if (opt === 'Temperature Unit') {
|
|
144
148
|
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
|
|
149
|
+
} else if (opt === 'AI Model') {
|
|
150
|
+
const curModel = AVAILABLE_AI_MODELS.find(m => m.id === state.aiModel) || AVAILABLE_AI_MODELS[0];
|
|
151
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.cyan(curModel.name)} ${chalk.dim(`[${curModel.badge}]`)}`);
|
|
152
|
+
} else if (opt === 'Grid Panel Order') {
|
|
153
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.panelLayout.join(' → '))}`);
|
|
145
154
|
} else {
|
|
146
155
|
const toggleKey = getToggleKey(opt);
|
|
147
156
|
const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
|
|
@@ -216,6 +225,14 @@ function render() {
|
|
|
216
225
|
writeFrame(renderCredits(cols, rows));
|
|
217
226
|
return;
|
|
218
227
|
}
|
|
228
|
+
if (state.inputMode === 'docker-alert') {
|
|
229
|
+
writeFrame(renderDockerAlert(cols, rows));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (state.inputMode === 'ai-model') {
|
|
233
|
+
writeFrame(renderAiModelSelector(cols, rows));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
219
236
|
const lines: string[] = [];
|
|
220
237
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
221
238
|
|
|
@@ -258,6 +275,7 @@ function render() {
|
|
|
258
275
|
selectedProcessIndex: state.processSelectionIndex,
|
|
259
276
|
trackedPid: state.trackedPid,
|
|
260
277
|
inspectProcess: state.inspectingProcess,
|
|
278
|
+
aiModel: state.aiModel,
|
|
261
279
|
});
|
|
262
280
|
_cachedTableData = state.lastData;
|
|
263
281
|
_cachedTableParams = cacheParams;
|
|
@@ -373,6 +391,7 @@ function footerLine(): string[] {
|
|
|
373
391
|
['f', state.exportFormat],
|
|
374
392
|
['+/-', `${state.interval}s`],
|
|
375
393
|
['r', state.p2pServerRunning ? 'stop p2p' : 'start p2p'],
|
|
394
|
+
['m', `ai:${state.aiModel.split('-')[0]}`],
|
|
376
395
|
];
|
|
377
396
|
const fmt = (list: [string, string][]) => list.map(([k, label]) => `${chalk.hex('#50fa7b').bold(k)} ${chalk.dim(label)}`).join(chalk.dim(' | '));
|
|
378
397
|
const str1 = fmt(row1);
|
|
@@ -383,6 +402,7 @@ function footerLine(): string[] {
|
|
|
383
402
|
if (state.logging) badges.push(chalk.red.bold('● REC'));
|
|
384
403
|
if (state.activePanel !== 'grid') badges.push(chalk.cyan.bold(`◉ ${state.activePanel.toUpperCase()}`));
|
|
385
404
|
if (state.p2pServerRunning) badges.push(chalk.green.bold(`P2P ${state.p2pBind || '0.0.0.0'}:${state.p2pPort}`));
|
|
405
|
+
badges.push(chalk.magenta.bold(`AI:${state.aiModel}`));
|
|
386
406
|
const badgeStr = badges.join(' ') || ' ';
|
|
387
407
|
|
|
388
408
|
return [str1, str2, badgeStr];
|
|
@@ -454,6 +474,29 @@ function checkAlerts(data: StatsData) {
|
|
|
454
474
|
if (state.alertCmd) {
|
|
455
475
|
exec(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {});
|
|
456
476
|
}
|
|
477
|
+
|
|
478
|
+
// Multi-channel alert fan-out (Slack, Discord, Pushover, ntfy)
|
|
479
|
+
const cfg = readConfig();
|
|
480
|
+
const channelConfig: AlertChannelConfig = {
|
|
481
|
+
slackUrl: cfg.slackAlertUrl || undefined,
|
|
482
|
+
discordUrl: cfg.discordAlertUrl || undefined,
|
|
483
|
+
pushoverToken: cfg.pushoverToken || undefined,
|
|
484
|
+
pushoverUser: cfg.pushoverUser || undefined,
|
|
485
|
+
ntfyUrl: cfg.ntfyUrl || undefined,
|
|
486
|
+
};
|
|
487
|
+
const hasChannels = Object.values(channelConfig).some(v => !!v);
|
|
488
|
+
if (hasChannels) {
|
|
489
|
+
const severity = hasCriticalAnomaly ? 'critical' : 'warning';
|
|
490
|
+
sendAlert({
|
|
491
|
+
metric: reasons[0]?.split(' ')[0] ?? 'System',
|
|
492
|
+
value: data.cpu.usage,
|
|
493
|
+
threshold: state.CPU_ALERT_PCT,
|
|
494
|
+
unit: '%',
|
|
495
|
+
host: data.header.hostname,
|
|
496
|
+
timestamp: new Date().toISOString(),
|
|
497
|
+
severity,
|
|
498
|
+
}, channelConfig).catch(() => {});
|
|
499
|
+
}
|
|
457
500
|
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
458
501
|
state.alerted = false;
|
|
459
502
|
}
|
|
@@ -564,4 +607,59 @@ function renderCredits(cols: number, rows: number): string[] {
|
|
|
564
607
|
return out;
|
|
565
608
|
}
|
|
566
609
|
|
|
610
|
+
function renderDockerAlert(cols: number, rows: number): string[] {
|
|
611
|
+
const out: string[] = [];
|
|
612
|
+
const content = [
|
|
613
|
+
chalk.bold.red('Docker Container Detected'),
|
|
614
|
+
'',
|
|
615
|
+
'Pyre is running inside a container. Some hardware sensors',
|
|
616
|
+
'(battery, thermal, power) may not be available or relevant.',
|
|
617
|
+
'',
|
|
618
|
+
chalk.cyan('Do you wish to continue in Docker mode?'),
|
|
619
|
+
''
|
|
620
|
+
];
|
|
621
|
+
|
|
622
|
+
const options = ['Yes, use Docker mode (hide hardware sensors)', 'No, launch regular TUI with all features'];
|
|
623
|
+
for (let i = 0; i < options.length; i++) {
|
|
624
|
+
const isSelected = i === state.dockerSelectionIndex;
|
|
625
|
+
const prefix = isSelected ? chalk.cyan.bold(' ▶ ') : ' ';
|
|
626
|
+
const text = isSelected ? chalk.bgCyan.black(` ${options[i]} `) : chalk.dim(` ${options[i]} `);
|
|
627
|
+
content.push(prefix + text);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
content.push('');
|
|
631
|
+
content.push(chalk.dim('Press ↑/↓ to select, Enter to confirm, Esc to skip.'));
|
|
632
|
+
|
|
633
|
+
out.push(...panel('DOCKER DETECTED', content, cols, chalk.red, chalk.dim, rows - 2));
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function renderAiModelSelector(cols: number, rows: number): string[] {
|
|
638
|
+
const out: string[] = [];
|
|
639
|
+
const content: string[] = [
|
|
640
|
+
chalk.bold.hex('#ff6004')('Choose AI Anomaly Explanation Model'),
|
|
641
|
+
chalk.dim('Select your preferred diagnosis engine. Offline models run entirely in-process.'),
|
|
642
|
+
'',
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
for (let i = 0; i < AVAILABLE_AI_MODELS.length; i++) {
|
|
646
|
+
const m = AVAILABLE_AI_MODELS[i];
|
|
647
|
+
const isSelected = i === state.modelSelectionIndex;
|
|
648
|
+
const isCurrent = m.id === state.aiModel;
|
|
649
|
+
const prefix = isSelected ? chalk.cyan.bold(' ▶ ') : ' ';
|
|
650
|
+
const curBadge = isCurrent ? chalk.bgGreen.black.bold(' ACTIVE ') : '';
|
|
651
|
+
const badgeStr = m.offline ? chalk.green(`[${m.badge}]`) : chalk.yellow(`[${m.badge}]`);
|
|
652
|
+
|
|
653
|
+
const nameStr = isSelected ? chalk.bgCyan.black.bold(` ${m.name} `) : chalk.bold.white(m.name);
|
|
654
|
+
content.push(`${prefix}${nameStr} ${badgeStr} ${curBadge}`);
|
|
655
|
+
content.push(chalk.dim(` ${m.description}`));
|
|
656
|
+
content.push('');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
content.push(chalk.dim('Use ↑/↓ or J/K to navigate · Enter/Space to select · Esc to cancel'));
|
|
660
|
+
|
|
661
|
+
out.push(...panel('AI MODEL SELECTOR', content, cols, chalk.cyan, chalk.dim, Math.max(1, rows - 2)));
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
|
|
567
665
|
export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget, invalidateTableCache, invalidateFrame };
|
package/src/live/state.ts
CHANGED
|
@@ -23,6 +23,8 @@ export const MENU_OPTIONS = ['Resume Dashboard', 'Settings (Customizer)', 'Readm
|
|
|
23
23
|
|
|
24
24
|
const config = readConfig();
|
|
25
25
|
|
|
26
|
+
const isDockerEnv = fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
|
|
27
|
+
|
|
26
28
|
const state = {
|
|
27
29
|
intervalHandle: null as NodeJS.Timeout | null,
|
|
28
30
|
uiIntervalHandle: null as NodeJS.Timeout | null,
|
|
@@ -37,19 +39,21 @@ const state = {
|
|
|
37
39
|
|
|
38
40
|
exportDir: config.exportDir,
|
|
39
41
|
currentTheme: config.theme as ThemeName,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
visiblePanels: {
|
|
43
|
+
cpu: config.visiblePanels.cpu,
|
|
44
|
+
mem: config.visiblePanels.mem,
|
|
45
|
+
gpu: config.visiblePanels.gpu,
|
|
46
|
+
power: config.visiblePanels.power,
|
|
47
|
+
battery: config.visiblePanels.battery,
|
|
48
|
+
thermal: config.visiblePanels.thermal,
|
|
49
|
+
network: config.visiblePanels.network,
|
|
50
|
+
packets: config.visiblePanels.packets,
|
|
51
|
+
tasks: config.visiblePanels.tasks,
|
|
52
|
+
disk: config.visiblePanels.disk,
|
|
53
|
+
process: config.visiblePanels.process,
|
|
54
|
+
containers: config.visiblePanels.containers,
|
|
55
|
+
blender: config.visiblePanels.blender,
|
|
56
|
+
} as VisibleItems,
|
|
53
57
|
panelLayout: config.panelLayout || ['mem', 'disk', 'net'],
|
|
54
58
|
logging: false,
|
|
55
59
|
logStream: null as fs.WriteStream | null,
|
|
@@ -66,34 +70,38 @@ const state = {
|
|
|
66
70
|
processScrollOffset: 0,
|
|
67
71
|
trackedPid: null as number | null,
|
|
68
72
|
inspectingProcess: null as any | null,
|
|
69
|
-
inputMode: null as InputMode,
|
|
73
|
+
inputMode: (isDockerEnv && !config.dockerModeConfirmed) ? 'docker-alert' as InputMode : null as InputMode,
|
|
70
74
|
inputBuffer: '',
|
|
71
75
|
targetPid: '',
|
|
72
76
|
selectedSignal: 'SIGTERM' as typeof SIGNAL_OPTIONS[number],
|
|
73
77
|
treeView: config.treeView,
|
|
74
78
|
SIGNAL_OPTIONS,
|
|
75
79
|
customizerIndex: 0,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
CUSTOMIZER_OPTIONS: [
|
|
81
|
+
'Theme',
|
|
82
|
+
'Graph Mode',
|
|
83
|
+
'Splash Screen',
|
|
84
|
+
'Splash Color',
|
|
85
|
+
'Splash Animation',
|
|
86
|
+
'Notifications',
|
|
87
|
+
'Temperature Unit',
|
|
88
|
+
'AI Model',
|
|
89
|
+
'Grid Panel Order',
|
|
90
|
+
'Toggle CPU',
|
|
91
|
+
'Toggle Memory',
|
|
92
|
+
'Toggle GPU',
|
|
93
|
+
'Toggle Power',
|
|
94
|
+
'Toggle Battery',
|
|
95
|
+
'Toggle Thermal',
|
|
96
|
+
'Toggle Network',
|
|
97
|
+
'Toggle Conns',
|
|
98
|
+
'Toggle Tasks',
|
|
99
|
+
'Toggle Disk',
|
|
100
|
+
'Toggle Processes',
|
|
101
|
+
'Toggle Containers',
|
|
102
|
+
'Toggle Blender',
|
|
103
|
+
'Toggle Tree View',
|
|
104
|
+
],
|
|
97
105
|
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
98
106
|
TEMP_ALERT_C: config.tempAlertC,
|
|
99
107
|
watchdogProcess: config.watchdogProcess || '',
|
|
@@ -103,23 +111,25 @@ const state = {
|
|
|
103
111
|
webhookUrl: config.webhookUrl || '',
|
|
104
112
|
alertCmd: config.alertCmd || '',
|
|
105
113
|
alerted: false,
|
|
106
|
-
|
|
114
|
+
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies' | 'containers' | 'blender',
|
|
107
115
|
mouseEnabled: config.mouseEnabled,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
PANEL_TABS: [
|
|
117
|
+
{ id: 'cpu', label: 'CPU', key: '1' },
|
|
118
|
+
{ id: 'mem', label: 'Memory', key: '2' },
|
|
119
|
+
{ id: 'gpu', label: 'GPU', key: '3' },
|
|
120
|
+
{ id: 'power', label: 'Power', key: '4' },
|
|
121
|
+
{ id: 'battery', label: 'Battery', key: '5' },
|
|
122
|
+
{ id: 'thermal', label: 'Thermal', key: '6' },
|
|
123
|
+
{ id: 'network', label: 'Network', key: '7' },
|
|
124
|
+
{ id: 'packets', label: 'Conns', key: '8' },
|
|
125
|
+
{ id: 'tasks', label: 'Tasks', key: '9' },
|
|
126
|
+
{ id: 'disk', label: 'Disk', key: '0' },
|
|
127
|
+
{ id: 'process', label: 'Process', key: 'P' },
|
|
128
|
+
{ id: 'containers', label: 'Containers', key: 'C' },
|
|
129
|
+
{ id: 'p2p', label: 'P2P', key: 'R' },
|
|
130
|
+
{ id: 'anomalies', label: 'Anomalies', key: 'A' },
|
|
131
|
+
{ id: 'blender', label: 'Blender', key: 'B' },
|
|
132
|
+
],
|
|
123
133
|
p2pServer: null as import('../p2p/server.js').P2PServer | null,
|
|
124
134
|
p2pClient: null as import('../p2p/client.js').P2PClient | null,
|
|
125
135
|
p2pServerRunning: false,
|
|
@@ -134,6 +144,12 @@ const state = {
|
|
|
134
144
|
splashAnimation: config.splashAnimation as SplashAnimation,
|
|
135
145
|
menuSelectionIndex: 0,
|
|
136
146
|
readmeScrollOffset: 0,
|
|
147
|
+
isDocker: isDockerEnv,
|
|
148
|
+
dockerModeConfirmed: config.dockerModeConfirmed || false,
|
|
149
|
+
dockerSelectionIndex: 0,
|
|
150
|
+
aiModel: config.aiModel || 'expert-rules-v1',
|
|
151
|
+
aiBackend: config.aiBackend || 'builtin',
|
|
152
|
+
modelSelectionIndex: 0,
|
|
137
153
|
};
|
|
138
154
|
|
|
139
155
|
function setStatus(msg: string, ms = 3000) {
|
|
@@ -153,10 +169,12 @@ function getToggleKey(opt: string): keyof VisibleItems | null {
|
|
|
153
169
|
'Toggle Battery': 'battery',
|
|
154
170
|
'Toggle Thermal': 'thermal',
|
|
155
171
|
'Toggle Network': 'network',
|
|
156
|
-
'Toggle
|
|
172
|
+
'Toggle Conns': 'packets',
|
|
157
173
|
'Toggle Tasks': 'tasks',
|
|
158
174
|
'Toggle Disk': 'disk',
|
|
159
175
|
'Toggle Processes': 'process',
|
|
176
|
+
'Toggle Containers': 'containers',
|
|
177
|
+
'Toggle Blender': 'blender',
|
|
160
178
|
'Toggle Tree View': 'tree',
|
|
161
179
|
};
|
|
162
180
|
return map[opt] ?? null;
|
package/src/live/types.ts
CHANGED
|
@@ -20,13 +20,13 @@ export interface LiveOptions {
|
|
|
20
20
|
|
|
21
21
|
export type ExportFormat = 'json' | 'csv' | 'tsv' | 'html' | 'md';
|
|
22
22
|
|
|
23
|
-
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p' | 'menu' | 'readme' | 'credits';
|
|
23
|
+
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p' | 'menu' | 'readme' | 'credits' | 'docker-alert' | 'ai-model';
|
|
24
24
|
|
|
25
25
|
type SortMode = 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
|
|
26
26
|
|
|
27
27
|
type GraphMode = 'spark' | 'bar';
|
|
28
28
|
|
|
29
|
-
type ActivePanel = 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p';
|
|
29
|
+
type ActivePanel = 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'containers' | 'anomalies' | 'blender';
|
|
30
30
|
|
|
31
31
|
export type { InputMode, SortMode, GraphMode, ActivePanel, ThemeName };
|
|
32
32
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Battery Predictor.
|
|
3
|
+
*
|
|
4
|
+
* Predicts multi-scenario battery longevity by synthesizing:
|
|
5
|
+
* 1. Current instantaneous discharge rate and state of charge.
|
|
6
|
+
* 2. Historical discharge profiles parsed from local CSV log exports.
|
|
7
|
+
* 3. Machine load heuristics (idle vs. current compute vs. sustained high-power).
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import type { BatteryData, PowerData } from './types.js';
|
|
12
|
+
|
|
13
|
+
export interface BatteryPredictionScenario {
|
|
14
|
+
label: string;
|
|
15
|
+
ratePerHour: number;
|
|
16
|
+
timeRemaining: string;
|
|
17
|
+
hours: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SmartBatteryPrediction {
|
|
21
|
+
currentLevel: number;
|
|
22
|
+
healthPercentage?: number;
|
|
23
|
+
cycleCount?: number;
|
|
24
|
+
powerWatts?: number;
|
|
25
|
+
scenarios: {
|
|
26
|
+
idle: BatteryPredictionScenario;
|
|
27
|
+
current: BatteryPredictionScenario;
|
|
28
|
+
heavy: BatteryPredictionScenario;
|
|
29
|
+
};
|
|
30
|
+
recommendation: string;
|
|
31
|
+
confidence: 'low' | 'medium' | 'high';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatHours(h: number): string {
|
|
35
|
+
if (!isFinite(h) || h <= 0) return 'calculating';
|
|
36
|
+
const hours = Math.floor(h);
|
|
37
|
+
const mins = Math.round((h - hours) * 60);
|
|
38
|
+
if (hours === 0 && mins === 0) return '< 1m';
|
|
39
|
+
if (hours === 0) return `${mins}m`;
|
|
40
|
+
return `${hours}h ${mins}m`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function computeSmartBatteryPrediction(
|
|
44
|
+
battery: BatteryData | null,
|
|
45
|
+
power: PowerData | null,
|
|
46
|
+
logDir: string = './pyre-exports'
|
|
47
|
+
): SmartBatteryPrediction | null {
|
|
48
|
+
if (!battery) return null;
|
|
49
|
+
|
|
50
|
+
const level = battery.level;
|
|
51
|
+
const isCharging = battery.state.toLowerCase().includes('charg');
|
|
52
|
+
|
|
53
|
+
// Baseline assumption for typical MacBook battery Wh capacity
|
|
54
|
+
const assumedCapacityWh = 58;
|
|
55
|
+
|
|
56
|
+
// 1. Current discharge rate in %/hour
|
|
57
|
+
let currentRate = battery.dischargeRatePerHour || 12; // default fallback 12%/h
|
|
58
|
+
if (battery.dischargeRatePerHour === undefined && power && power.combinedWatts) {
|
|
59
|
+
currentRate = (power.combinedWatts / assumedCapacityWh) * 100;
|
|
60
|
+
}
|
|
61
|
+
currentRate = Math.max(2, Math.min(60, currentRate));
|
|
62
|
+
|
|
63
|
+
// 2. Scan historical logs for typical average discharge rate if available
|
|
64
|
+
let historicalSamples: number[] = [];
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(logDir)) {
|
|
67
|
+
const files = fs.readdirSync(logDir).filter(f => f.startsWith('pyre-') && f.endsWith('.csv')).slice(-3);
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const content = fs.readFileSync(path.join(logDir, file), 'utf-8');
|
|
70
|
+
const lines = content.split('\n').slice(1);
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
const parts = line.split(',');
|
|
73
|
+
if (parts.length >= 4) {
|
|
74
|
+
const cpu = parseFloat(parts[1]);
|
|
75
|
+
if (!isNaN(cpu)) {
|
|
76
|
+
// Estimate power based on CPU history
|
|
77
|
+
const estRate = 5 + (cpu / 100) * 20;
|
|
78
|
+
historicalSamples.push(estRate);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// fallback
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const confidence: 'low' | 'medium' | 'high' = isCharging
|
|
89
|
+
? 'low'
|
|
90
|
+
: historicalSamples.length > 50
|
|
91
|
+
? 'high'
|
|
92
|
+
: battery.dischargeRatePerHour ? 'medium' : 'low';
|
|
93
|
+
|
|
94
|
+
// Compute scenarios
|
|
95
|
+
// Idle load scenario (light browser/reading, ~5-7% per hour)
|
|
96
|
+
const idleRate = 6.0;
|
|
97
|
+
const idleHours = level / idleRate;
|
|
98
|
+
|
|
99
|
+
// Current load scenario
|
|
100
|
+
const currentHours = level / currentRate;
|
|
101
|
+
|
|
102
|
+
// Heavy load scenario (compiling, 3D render, gaming: ~25-35% per hour)
|
|
103
|
+
const heavyRate = Math.max(28.0, currentRate * 1.5);
|
|
104
|
+
const heavyHours = level / heavyRate;
|
|
105
|
+
|
|
106
|
+
let recommendation = 'Power consumption is normal.';
|
|
107
|
+
if (isCharging) {
|
|
108
|
+
recommendation = `Device is currently ${battery.state}. Full capacity health: ${battery.health || 'Good'}.`;
|
|
109
|
+
} else if (currentRate > 25) {
|
|
110
|
+
recommendation = 'High power drain detected. Reducing display brightness and closing heavy background processes can extend battery significantly.';
|
|
111
|
+
} else if (currentRate < 10) {
|
|
112
|
+
recommendation = 'Excellent power efficiency. Machine is running under low-drain conditions.';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
currentLevel: level,
|
|
117
|
+
healthPercentage: battery.maxCapacityPercent,
|
|
118
|
+
cycleCount: battery.cycles,
|
|
119
|
+
powerWatts: battery.powerWatts || power?.combinedWatts,
|
|
120
|
+
scenarios: {
|
|
121
|
+
idle: {
|
|
122
|
+
label: 'Light / Reading Load',
|
|
123
|
+
ratePerHour: idleRate,
|
|
124
|
+
timeRemaining: formatHours(idleHours),
|
|
125
|
+
hours: idleHours,
|
|
126
|
+
},
|
|
127
|
+
current: {
|
|
128
|
+
label: 'Current Activity',
|
|
129
|
+
ratePerHour: Math.round(currentRate * 10) / 10,
|
|
130
|
+
timeRemaining: isCharging ? 'Charging' : formatHours(currentHours),
|
|
131
|
+
hours: currentHours,
|
|
132
|
+
},
|
|
133
|
+
heavy: {
|
|
134
|
+
label: 'Sustained Heavy Compute',
|
|
135
|
+
ratePerHour: Math.round(heavyRate * 10) / 10,
|
|
136
|
+
timeRemaining: formatHours(heavyHours),
|
|
137
|
+
hours: heavyHours,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
recommendation,
|
|
141
|
+
confidence,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { run } from './run.js';
|
|
2
|
+
import type { BlenderRenderData } from './types.js';
|
|
3
|
+
|
|
4
|
+
const RENDER_LOG_DIR = '/tmp/blender_render_logs';
|
|
5
|
+
|
|
6
|
+
function blendFileFromArgs(args: string[]): string {
|
|
7
|
+
const nonFlag = args.filter(a => !a.startsWith('-'));
|
|
8
|
+
for (const a of nonFlag) {
|
|
9
|
+
if (a.endsWith('.blend') || a.includes('.blend')) {
|
|
10
|
+
return a.split('/').pop() || a;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return 'unknown.blend';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function engineFromArgs(args: string[]): string {
|
|
17
|
+
const idx = args.indexOf('-E');
|
|
18
|
+
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
19
|
+
const flag = args.find(a => a.startsWith('-E='));
|
|
20
|
+
if (flag) return flag.slice(3);
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function outputPathFromArgs(args: string[]): string {
|
|
25
|
+
const oIdx = args.indexOf('-o');
|
|
26
|
+
if (oIdx !== -1 && args[oIdx + 1]) {
|
|
27
|
+
let p = args[oIdx + 1];
|
|
28
|
+
if (p.endsWith('/')) p = p.slice(0, -1);
|
|
29
|
+
return p.split('/').slice(0, -1).join('/') || '/tmp';
|
|
30
|
+
}
|
|
31
|
+
const fFlag = args.find(a => a.startsWith('-o='));
|
|
32
|
+
if (fFlag) {
|
|
33
|
+
const p = fFlag.slice(3);
|
|
34
|
+
return p.endsWith('/') ? p.slice(0, -1).split('/').slice(0, -1).join('/') || '/tmp' : p.split('/').slice(0, -1).join('/') || '/tmp';
|
|
35
|
+
}
|
|
36
|
+
return '/tmp';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function frameRangeFromArgs(args: string[]): { start: number; end: number } {
|
|
40
|
+
const fIdx = args.indexOf('-f');
|
|
41
|
+
if (fIdx !== -1 && args[fIdx + 1]) {
|
|
42
|
+
const raw = args[fIdx + 1];
|
|
43
|
+
if (raw.includes('..')) {
|
|
44
|
+
const [s, e] = raw.split('..').map(Number);
|
|
45
|
+
if (!isNaN(s) && !isNaN(e)) return { start: s, end: e };
|
|
46
|
+
}
|
|
47
|
+
const n = parseInt(raw, 10);
|
|
48
|
+
if (!isNaN(n)) return { start: n, end: n };
|
|
49
|
+
}
|
|
50
|
+
const sFlag = args.find(a => a.startsWith('-s='));
|
|
51
|
+
const eFlag = args.find(a => a.startsWith('-e='));
|
|
52
|
+
const start = sFlag ? parseInt(sFlag.slice(3), 10) : 1;
|
|
53
|
+
const end = eFlag ? parseInt(eFlag.slice(3), 10) : 250;
|
|
54
|
+
return { start: isNaN(start) ? 1 : start, end: isNaN(end) ? 250 : end };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function parseRenderLog(blendName: string): Promise<{ framesRendered: number; totalFrames: number; startTime: number; samples?: number } | null> {
|
|
58
|
+
const baseName = blendName.replace(/\.blend$/i, '');
|
|
59
|
+
try {
|
|
60
|
+
const entries = (await run(`ls -t "${RENDER_LOG_DIR}" 2>/dev/null | grep "^${baseName}_" | head -5`, '')).trim().split('\n').filter(Boolean);
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const logPath = `${RENDER_LOG_DIR}/${entry}`;
|
|
63
|
+
try {
|
|
64
|
+
const content = await run(`cat "${logPath}" 2>/dev/null`, '', 500);
|
|
65
|
+
if (!content) continue;
|
|
66
|
+
const lines = content.split('\n');
|
|
67
|
+
const rendered = lines.filter(l => /Fra:(\d+)/i.test(l)).length;
|
|
68
|
+
const totalMatch = content.match(/Fra:\s*\d+\s*\/\s*(\d+)/);
|
|
69
|
+
const totalFrames = totalMatch ? parseInt(totalMatch[1], 10) : 0;
|
|
70
|
+
const startMatch = content.match(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/m);
|
|
71
|
+
const startTime = startMatch ? new Date(startMatch[1]).getTime() : Date.now() - 60000;
|
|
72
|
+
const sampleMatch = content.match(/Sample\s*(\d+)/i);
|
|
73
|
+
if (rendered > 0) {
|
|
74
|
+
return { framesRendered: rendered, totalFrames: totalFrames || rendered, startTime, samples: sampleMatch ? parseInt(sampleMatch[1], 10) : undefined };
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// try next log file
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function collectBlenderRenders(): Promise<BlenderRenderData[]> {
|
|
87
|
+
const renders: BlenderRenderData[] = [];
|
|
88
|
+
try {
|
|
89
|
+
const psRaw = await run("ps -eo pid,pcpu,pmem,state,command 2>/dev/null | grep -iE '[b]lender'", '', 2000);
|
|
90
|
+
if (!psRaw) return renders;
|
|
91
|
+
|
|
92
|
+
const lines = psRaw.split('\n');
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const parts = line.trim().match(/\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(.+)/);
|
|
95
|
+
if (!parts) continue;
|
|
96
|
+
|
|
97
|
+
const pid = parseInt(parts[1], 10);
|
|
98
|
+
const cpuPct = parseFloat(parts[2]);
|
|
99
|
+
const command = parts[5];
|
|
100
|
+
const args = command.split(/\s+/);
|
|
101
|
+
|
|
102
|
+
const isRender = /-b|--background|-f\s/.test(command) || /render/i.test(command);
|
|
103
|
+
if (!isRender && !command.includes('blender')) continue;
|
|
104
|
+
|
|
105
|
+
const blendFile = blendFileFromArgs(args);
|
|
106
|
+
const engine = engineFromArgs(args);
|
|
107
|
+
const outputPath = outputPathFromArgs(args);
|
|
108
|
+
const { start: frameStart, end: frameEnd } = frameRangeFromArgs(args);
|
|
109
|
+
const totalFrames = Math.max(1, frameEnd - frameStart + 1);
|
|
110
|
+
|
|
111
|
+
const logData = await parseRenderLog(blendFile);
|
|
112
|
+
const startTime = logData?.startTime ?? Date.now() - 60000;
|
|
113
|
+
const elapsedSec = Math.max(0, Math.round((Date.now() - startTime) / 1000));
|
|
114
|
+
let currentFrame = logData?.framesRendered ?? 0;
|
|
115
|
+
if (currentFrame === 0 && cpuPct > 0) currentFrame = 1;
|
|
116
|
+
|
|
117
|
+
let status: BlenderRenderData['status'] = 'unknown';
|
|
118
|
+
if (cpuPct > 0 && currentFrame > 0 && currentFrame < totalFrames) status = 'rendering';
|
|
119
|
+
else if (cpuPct > 0 && currentFrame >= totalFrames) status = 'finalizing';
|
|
120
|
+
else if (currentFrame === 0 && cpuPct > 0) status = 'loading';
|
|
121
|
+
else if (cpuPct === 0) status = 'idle';
|
|
122
|
+
|
|
123
|
+
const completionPercent = totalFrames > 0 ? Math.min(100, Math.round((currentFrame / totalFrames) * 100)) : 0;
|
|
124
|
+
|
|
125
|
+
renders.push({
|
|
126
|
+
pid,
|
|
127
|
+
blendFile,
|
|
128
|
+
renderEngine: engine,
|
|
129
|
+
frameStart,
|
|
130
|
+
frameEnd,
|
|
131
|
+
currentFrame,
|
|
132
|
+
totalFrames,
|
|
133
|
+
elapsedSec,
|
|
134
|
+
completionPercent,
|
|
135
|
+
status,
|
|
136
|
+
outputPath,
|
|
137
|
+
sampleCount: logData?.samples,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore
|
|
142
|
+
}
|
|
143
|
+
return renders;
|
|
144
|
+
}
|