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.
Files changed (94) hide show
  1. package/README.md +84 -14
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/blender-FGH3ASEQ.js +7 -0
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
  7. package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
  8. package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
  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-VIACN2RU.js +133 -0
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-4ZLF4HTS.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 +841 -206
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -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-GVIAHIVV.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/anomalies.ts +114 -60
  44. package/src/brewHealth.ts +157 -40
  45. package/src/buildCmd.ts +48 -0
  46. package/src/checkCmd.ts +5 -5
  47. package/src/completions.ts +35 -0
  48. package/src/doctor.ts +235 -7
  49. package/src/easy_features.test.ts +40 -0
  50. package/src/explainCmd.ts +261 -0
  51. package/src/extensions.ts +71 -7
  52. package/src/formatters/index.ts +1 -1
  53. package/src/formatters/output.ts +100 -13
  54. package/src/formatters/render.ts +274 -38
  55. package/src/formatters/themes.ts +8 -0
  56. package/src/formatters/types.ts +6 -1
  57. package/src/index.ts +298 -56
  58. package/src/live/input.ts +122 -4
  59. package/src/live/render.ts +98 -0
  60. package/src/live/state.ts +70 -52
  61. package/src/live/types.ts +2 -2
  62. package/src/monitors/batteryPredictor.ts +143 -0
  63. package/src/monitors/blender.ts +144 -0
  64. package/src/monitors/buildTracker.ts +121 -0
  65. package/src/monitors/collectors.ts +558 -147
  66. package/src/monitors/dns_cache.ts +48 -0
  67. package/src/monitors/index.ts +3 -2
  68. package/src/monitors/ioreport.py +72 -0
  69. package/src/monitors/platform/linux.ts +756 -0
  70. package/src/monitors/platform/windows.ts +519 -0
  71. package/src/monitors/run.ts +27 -3
  72. package/src/monitors/smc.ts +21 -1
  73. package/src/monitors/types.ts +112 -14
  74. package/src/netUsageCmd.ts +121 -0
  75. package/src/platform.test.ts +99 -0
  76. package/src/plugins/index.ts +143 -0
  77. package/src/profileProcCmd.ts +133 -0
  78. package/src/prometheusCmd.ts +107 -0
  79. package/src/replayCmd.ts +186 -0
  80. package/src/serveCmd.ts +180 -0
  81. package/src/service.ts +125 -7
  82. package/src/smartCmd.ts +205 -0
  83. package/src/state/config.ts +64 -17
  84. package/src/syncCmd.ts +166 -0
  85. package/src/topoCmd.ts +176 -0
  86. package/src/watchdogCmd.ts +198 -0
  87. package/src/xbar.ts +22 -11
  88. package/dist/brewHealth-F2BXF5M6.js +0 -74
  89. package/dist/chunk-FH5WDJBF.js +0 -15
  90. package/dist/chunk-HGRBBPYX.js +0 -937
  91. package/dist/doctor-L6FZYA7F.js +0 -184
  92. package/dist/extensions-DVA5YLGH.js +0 -74
  93. package/dist/service-WTS3NBQU.js +0 -73
  94. /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
@@ -15,4 +15,4 @@ export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getT
15
15
  export { formatJson, formatCsv, formatTsv, formatGraphs, formatHtml, formatMarkdown } from './output.js';
16
16
  export { getDashboardHtml } from './html_template.js';
17
17
  export type { ThemeName, ThemeColors, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
18
- export { THEMES } from './themes.js';
18
+ export { THEMES } from './themes.js';
@@ -111,21 +111,38 @@ export function formatHtml(data: StatsData): string {
111
111
  <table>
112
112
  ${rows([
113
113
  { label: 'Interface', value: `${data.network.interface} (${data.network.ip})` },
114
- { label: 'RX', value: `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt` },
115
- { label: 'TX', value: `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt` },
114
+ { label: 'RX', value: `${formatBytes(data.network.rxBytes)} · ${formatBytes(data.network.rxRate)}/s` },
115
+ { label: 'TX', value: `${formatBytes(data.network.txBytes)} · ${formatBytes(data.network.txRate)}/s` },
116
+ ...(data.network.protocols ? [
117
+ { label: 'TCP', value: `${data.network.protocols.tcp.connections} conns` },
118
+ { label: 'UDP', value: `${data.network.protocols.udp.connections} conns` },
119
+ ] : []),
120
+ ...(data.network.connectionStates ? [
121
+ { label: 'Active', value: `${data.network.connectionStates.established} ESTABLISHED` },
122
+ { label: 'Listening', value: `${data.network.connectionStates.listening}` },
123
+ ] : []),
124
+ ...(data.network.listeningPorts && data.network.listeningPorts.length ? [
125
+ { label: 'Ports', value: data.network.listeningPorts.slice(0, 8).join(', ') },
126
+ ] : []),
116
127
  ]).trim()}
117
128
  </table>`);
118
129
 
119
130
  if (data.packets) {
120
- sections.push(`<h2>Packets</h2>
121
- <table>
122
- ${rows([
123
- { label: 'Total', value: `${data.packets.totalPackets} pkt` },
124
- { label: 'RX', value: `${data.packets.rxPackets} pkt` },
125
- { label: 'TX', value: `${data.packets.txPackets} pkt` },
126
- { label: 'Connections', value: `${data.packets.connections} TCP` },
127
- ]).trim()}
128
- </table>`);
131
+ const packetRows: { label: string; value: string }[] = [
132
+ { label: 'Total', value: `${data.packets.totalPackets} pkt` },
133
+ { label: 'RX', value: `${data.packets.rxPackets} pkt` },
134
+ { label: 'TX', value: `${data.packets.txPackets} pkt` },
135
+ { label: 'Connections', value: `${data.packets.connections} TCP` },
136
+ ];
137
+ if (data.packets.protocolStats) {
138
+ packetRows.push({ label: 'TCP', value: `${data.packets.protocolStats.tcp.connections} conns` });
139
+ packetRows.push({ label: 'UDP', value: `${data.packets.protocolStats.udp.connections} conns` });
140
+ }
141
+ if (data.packets.connectionStates) {
142
+ packetRows.push({ label: 'ESTABLISHED', value: `${data.packets.connectionStates.established}` });
143
+ packetRows.push({ label: 'LISTENING', value: `${data.packets.connectionStates.listening}` });
144
+ }
145
+ sections.push(`<h2>Packets</h2>\n<table>\n ${rows(packetRows).trim()}\n</table>`);
129
146
  }
130
147
 
131
148
  if (data.tasks.length) {
@@ -152,6 +169,14 @@ export function formatHtml(data: StatsData): string {
152
169
  sections.push(`<h2>Processes</h2>\n<table>\n ${rows(procRows).trim()}\n</table>`);
153
170
  }
154
171
 
172
+ if (data.blenderRenders && data.blenderRenders.length) {
173
+ const blenderRows = data.blenderRenders.map(r => ({
174
+ label: `PID ${r.pid}`,
175
+ value: `${r.blendFile} · ${r.renderEngine} · ${r.currentFrame}/${r.totalFrames} frames · ${r.completionPercent}% · ${r.status} · ${r.elapsedSec}s`,
176
+ }));
177
+ sections.push(`<h2>Blender Renders</h2>\n<table>\n ${rows(blenderRows).trim()}\n</table>`);
178
+ }
179
+
155
180
  return `<!DOCTYPE html>
156
181
  <html lang="en">
157
182
  <head>
@@ -380,8 +405,19 @@ export function formatMarkdown(data: StatsData): string {
380
405
  lines.push('| Property | Value |');
381
406
  lines.push('|---|---|');
382
407
  lines.push(`| Interface | ${mdCell(`${data.network.interface} (${data.network.ip})`)} |`);
383
- lines.push(`| RX | ${mdCell(`${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`)} |`);
384
- lines.push(`| TX | ${mdCell(`${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)} |`);
408
+ lines.push(`| RX | ${mdCell(`${formatBytes(data.network.rxBytes)} · ${formatBytes(data.network.rxRate)}/s`)} |`);
409
+ lines.push(`| TX | ${mdCell(`${formatBytes(data.network.txBytes)} · ${formatBytes(data.network.txRate)}/s`)} |`);
410
+ if (data.network.protocols) {
411
+ lines.push(`| TCP | ${mdCell(`${data.network.protocols.tcp.connections} conns`)} |`);
412
+ lines.push(`| UDP | ${mdCell(`${data.network.protocols.udp.connections} conns`)} |`);
413
+ }
414
+ if (data.network.connectionStates) {
415
+ lines.push(`| Active | ${mdCell(`${data.network.connectionStates.established} ESTABLISHED`)} |`);
416
+ lines.push(`| Listening | ${mdCell(`${data.network.connectionStates.listening}`)} |`);
417
+ }
418
+ if (data.network.listeningPorts && data.network.listeningPorts.length) {
419
+ lines.push(`| Ports | ${mdCell(data.network.listeningPorts.slice(0, 8).join(', '))} |`);
420
+ }
385
421
  lines.push('');
386
422
 
387
423
  if (data.packets) {
@@ -393,6 +429,14 @@ export function formatMarkdown(data: StatsData): string {
393
429
  lines.push(`| RX | ${mdCell(`${data.packets.rxPackets} pkt`)} |`);
394
430
  lines.push(`| TX | ${mdCell(`${data.packets.txPackets} pkt`)} |`);
395
431
  lines.push(`| Connections | ${mdCell(`${data.packets.connections} TCP`)} |`);
432
+ if (data.packets.protocolStats) {
433
+ lines.push(`| TCP | ${mdCell(`${data.packets.protocolStats.tcp.connections} conns`)} |`);
434
+ lines.push(`| UDP | ${mdCell(`${data.packets.protocolStats.udp.connections} conns`)} |`);
435
+ }
436
+ if (data.packets.connectionStates) {
437
+ lines.push(`| ESTABLISHED | ${mdCell(`${data.packets.connectionStates.established}`)} |`);
438
+ lines.push(`| LISTENING | ${mdCell(`${data.packets.connectionStates.listening}`)} |`);
439
+ }
396
440
  lines.push('');
397
441
  }
398
442
 
@@ -429,6 +473,17 @@ export function formatMarkdown(data: StatsData): string {
429
473
  lines.push('');
430
474
  }
431
475
 
476
+ if (data.blenderRenders && data.blenderRenders.length) {
477
+ lines.push('## Blender Renders');
478
+ lines.push('');
479
+ lines.push('| PID | Blend File | Engine | Frames | Completion | Status | Elapsed |');
480
+ lines.push('|---|---|---|---|---|---|---|');
481
+ for (const r of data.blenderRenders) {
482
+ lines.push(`| ${r.pid} | ${mdCell(r.blendFile)} | ${mdCell(r.renderEngine)} | ${r.currentFrame}/${r.totalFrames} | ${r.completionPercent}% | ${r.status} | ${r.elapsedSec}s |`);
483
+ }
484
+ lines.push('');
485
+ }
486
+
432
487
  return lines.join('\n');
433
488
  }
434
489
 
@@ -465,9 +520,16 @@ export function formatCsv(data: StatsData): string {
465
520
  ['thermal', 'cpuTempF', data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ''],
466
521
  ['network', 'rxBytes', String(data.network.rxBytes)],
467
522
  ['network', 'txBytes', String(data.network.txBytes)],
523
+ ['network', 'rxRate', String(data.network.rxRate)],
524
+ ['network', 'txRate', String(data.network.txRate)],
468
525
  ['network', 'rxPackets', String(data.network.rxPackets)],
469
526
  ['network', 'txPackets', String(data.network.txPackets)],
470
527
  ['network', 'connections', data.network.connections ? String(data.network.connections) : ''],
528
+ ['network', 'tcpConns', data.network.protocols ? String(data.network.protocols.tcp.connections) : ''],
529
+ ['network', 'udpConns', data.network.protocols ? String(data.network.protocols.udp.connections) : ''],
530
+ ['network', 'established', data.network.connectionStates ? String(data.network.connectionStates.established) : ''],
531
+ ['network', 'listening', data.network.connectionStates ? String(data.network.connectionStates.listening) : ''],
532
+ ['network', 'listeningPorts', data.network.listeningPorts ? data.network.listeningPorts.slice(0, 10).join(';') : ''],
471
533
  ['battery', 'level', data.battery ? String(data.battery.level) : ''],
472
534
  ['battery', 'powerSource', data.battery ? data.battery.powerSource : ''],
473
535
  ['battery', 'condition', data.battery?.condition ?? ''],
@@ -492,6 +554,16 @@ export function formatCsv(data: StatsData): string {
492
554
  rows.push(['packets', 'rxPackets', String(data.packets.rxPackets)]);
493
555
  rows.push(['packets', 'txPackets', String(data.packets.txPackets)]);
494
556
  rows.push(['packets', 'connections', String(data.packets.connections)]);
557
+ if (data.packets.protocolStats) {
558
+ rows.push(['packets', 'tcpConns', String(data.packets.protocolStats.tcp.connections)]);
559
+ rows.push(['packets', 'udpConns', String(data.packets.protocolStats.udp.connections)]);
560
+ }
561
+ if (data.packets.connectionStates) {
562
+ rows.push(['packets', 'established', String(data.packets.connectionStates.established)]);
563
+ rows.push(['packets', 'listening', String(data.packets.connectionStates.listening)]);
564
+ rows.push(['packets', 'timeWait', String(data.packets.connectionStates.timeWait)]);
565
+ rows.push(['packets', 'closeWait', String(data.packets.connectionStates.closeWait)]);
566
+ }
495
567
  for (const iface of data.packets.interfaces || []) {
496
568
  rows.push(['packets_iface', iface.iface, `${iface.rxPackets},${iface.txPackets},${iface.rxBytes},${iface.txBytes}`]);
497
569
  }
@@ -519,6 +591,21 @@ export function formatCsv(data: StatsData): string {
519
591
  rows.push(['process', `runtime=${p.pid}`, String(p.runtime)]);
520
592
  rows.push(['process', `command=${p.pid}`, p.command]);
521
593
  }
594
+
595
+ for (const r of data.blenderRenders || []) {
596
+ rows.push(['blender', `pid=${r.pid}`, `${r.pid}`]);
597
+ rows.push(['blender', `blend_file=${r.pid}`, r.blendFile]);
598
+ rows.push(['blender', `engine=${r.pid}`, r.renderEngine]);
599
+ rows.push(['blender', `frame_start=${r.pid}`, String(r.frameStart)]);
600
+ rows.push(['blender', `frame_end=${r.pid}`, String(r.frameEnd)]);
601
+ rows.push(['blender', `current_frame=${r.pid}`, String(r.currentFrame)]);
602
+ rows.push(['blender', `total_frames=${r.pid}`, String(r.totalFrames)]);
603
+ rows.push(['blender', `completion_pct=${r.pid}`, String(r.completionPercent)]);
604
+ rows.push(['blender', `status=${r.pid}`, r.status]);
605
+ rows.push(['blender', `elapsed_sec=${r.pid}`, String(r.elapsedSec)]);
606
+ rows.push(['blender', `output_path=${r.pid}`, r.outputPath]);
607
+ if (r.sampleCount !== undefined) rows.push(['blender', `samples=${r.pid}`, String(r.sampleCount)]);
608
+ }
522
609
  return rows.map(r => r.map(item => `"${String(item).replace(/"/g, '""')}"`).join(',')).join('\n');
523
610
  }
524
611
 
@@ -13,7 +13,9 @@ import chalk from 'chalk';
13
13
  import { sparkline, multiRowSparkline, brailleGraph, multiRowBrailleGraph } from '../sparkline.js';
14
14
  import type { History } from '../history.js';
15
15
  import type { StatsData, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
16
+ import type { BlenderRenderData } from '../monitors/types.js';
16
17
  import { THEMES, type ThemeName, type ThemeColors } from './themes.js';
18
+ import { explainAnomaliesLocally } from '../ai/models.js';
17
19
 
18
20
 
19
21
  // --- low-level box drawing ---------------------------------------------------
@@ -313,8 +315,17 @@ function batteryCard(data: StatsData, contentWidth: number): string[] | null {
313
315
  if (data.battery.estimatedTimeToEmpty && data.battery.state !== 'charged') {
314
316
  lines.push(statRow('Est. Empty', data.battery.estimatedTimeToEmpty));
315
317
  }
316
- if (data.battery.dischargeRatePerHour !== undefined && data.battery.state !== 'charged') {
317
- 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}`));
318
329
  }
319
330
  return lines;
320
331
  }
@@ -333,11 +344,102 @@ function thermalCard(data: StatsData, contentWidth: number): string[] {
333
344
 
334
345
  function networkCard(data: StatsData, contentWidth: number): string[] {
335
346
  const valW = Math.max(6, contentWidth - 8);
336
- return [
337
- statRow('Iface', truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
338
- statRow('RX', `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`),
339
- statRow('TX', `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`),
340
- ];
347
+ const lines: string[] = [];
348
+ lines.push(statRow('Iface', truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)));
349
+ const rxTotal = formatBytes(data.network.rxBytes);
350
+ const txTotal = formatBytes(data.network.txBytes);
351
+ const rxRateStr = formatBytes(data.network.rxRate);
352
+ const txRateStr = formatBytes(data.network.txRate);
353
+ lines.push(statRow('RX', `${rxTotal} · ${rxRateStr}/s`));
354
+ lines.push(statRow('TX', `${txTotal} · ${txRateStr}/s`));
355
+
356
+ const totalBytes = data.network.rxBytes + data.network.txBytes;
357
+ if (totalBytes > 0) {
358
+ const rxPct = Math.round((data.network.rxBytes / totalBytes) * 100);
359
+ const barW = Math.max(4, contentWidth - 20);
360
+ const rxBar = gaugeBar(rxPct, barW);
361
+ lines.push(statRow('RX/TX', `${rxBar} ${rxPct}%`));
362
+ }
363
+
364
+ if (data.network.protocols) {
365
+ const p = data.network.protocols;
366
+ const totalConns = (p.tcp.connections || 0) + (p.udp.connections || 0);
367
+ if (totalConns > 0) {
368
+ const tcpPct = Math.round((p.tcp.connections / totalConns) * 100);
369
+ lines.push(statRow('TCP', `${p.tcp.connections} conns · ${gaugeBar(tcpPct, Math.max(4, contentWidth - 20))} ${tcpPct}%`));
370
+ lines.push(statRow('UDP', `${p.udp.connections} conns`));
371
+ }
372
+ }
373
+ if (data.network.connectionStates) {
374
+ const cs = data.network.connectionStates;
375
+ const total = cs.established + cs.listening + cs.timeWait + cs.closeWait + cs.other;
376
+ if (total > 0) {
377
+ const estPct = Math.round((cs.established / total) * 100);
378
+ lines.push(statRow('Active', `${cs.established} ESTABLISHED · ${estPct}%`));
379
+ lines.push(statRow('Listen', `${cs.listening} LISTEN`));
380
+ if (cs.timeWait > 0) lines.push(statRow('TimeWait', `${cs.timeWait}`));
381
+ if (cs.closeWait > 0) lines.push(statRow('CloseWait', `${cs.closeWait}`));
382
+ }
383
+ }
384
+ if (data.network.listeningPorts && data.network.listeningPorts.length > 0) {
385
+ const ports = data.network.listeningPorts.slice(0, 8).join(', ');
386
+ lines.push(statRow('Ports', truncatePlain(ports, valW)));
387
+ }
388
+ if (data.network.topRemoteHosts && data.network.topRemoteHosts.length > 0) {
389
+ for (const host of data.network.topRemoteHosts.slice(0, 3)) {
390
+ lines.push(statRow('Host', truncatePlain(`${host.host} (${host.connectionCount})`, valW)));
391
+ }
392
+ }
393
+ return lines;
394
+ }
395
+
396
+ function networkDetailCard(data: StatsData, width: number, theme: ThemeColors): string[] {
397
+ const contentWidth = width - 4;
398
+ const lines = networkCard(data, contentWidth);
399
+
400
+ if (data.packets && data.packets.allProcesses && data.packets.allProcesses.length > 0) {
401
+ lines.push('');
402
+ lines.push(chalk.bold('PROCESSES & BANDWIDTH USAGE:'));
403
+ lines.push(chalk.dim('─'.repeat(contentWidth)));
404
+
405
+ const pWidth = 8;
406
+ const rxWidth = 12;
407
+ const txWidth = 12;
408
+ const totalWidth = 12;
409
+ const cmdWidth = Math.max(15, contentWidth - (pWidth + rxWidth + txWidth + totalWidth + 4 * 2));
410
+
411
+ lines.push(
412
+ chalk.bold('PID'.padEnd(pWidth)) + ' ' +
413
+ chalk.bold('COMMAND'.padEnd(cmdWidth)) + ' ' +
414
+ chalk.bold('RX'.padStart(rxWidth)) + ' ' +
415
+ chalk.bold('TX'.padStart(txWidth)) + ' ' +
416
+ chalk.bold('TOTAL'.padStart(totalWidth))
417
+ );
418
+
419
+ const sorted = [...data.packets.allProcesses]
420
+ .sort((a, b) => (b.rxBytes + b.txBytes) - (a.rxBytes + a.txBytes));
421
+
422
+ for (const proc of sorted) {
423
+ if (proc.rxBytes + proc.txBytes === 0) continue;
424
+ const rxStr = formatBytes(proc.rxBytes).padStart(rxWidth);
425
+ const txStr = formatBytes(proc.txBytes).padStart(txWidth);
426
+ const totalStr = formatBytes(proc.rxBytes + proc.txBytes).padStart(totalWidth);
427
+ const cmdStr = truncatePlain(proc.command, cmdWidth).padEnd(cmdWidth);
428
+
429
+ lines.push(
430
+ chalk.cyan(proc.pid.toString().padEnd(pWidth)) + ' ' +
431
+ cmdStr + ' ' +
432
+ chalk.yellow(rxStr) + ' ' +
433
+ chalk.yellow(txStr) + ' ' +
434
+ chalk.bold(totalStr)
435
+ );
436
+ }
437
+ } else {
438
+ lines.push('');
439
+ lines.push(chalk.dim('No active network process data available.'));
440
+ }
441
+
442
+ return lines;
341
443
  }
342
444
 
343
445
  function packetCard(data: StatsData, contentWidth: number): string[] | null {
@@ -347,8 +449,19 @@ function packetCard(data: StatsData, contentWidth: number): string[] | null {
347
449
  lines.push(statRow('RX', `${data.packets.rxPackets} pkt`));
348
450
  lines.push(statRow('TX', `${data.packets.txPackets} pkt`));
349
451
  lines.push(statRow('Conns', `${data.packets.connections} TCP`));
452
+ if (data.packets.protocolStats) {
453
+ const ps = data.packets.protocolStats;
454
+ lines.push(statRow('TCP', `${ps.tcp.connections} conns`));
455
+ lines.push(statRow('UDP', `${ps.udp.connections} conns`));
456
+ }
457
+ if (data.packets.connectionStates) {
458
+ const cs = data.packets.connectionStates;
459
+ lines.push(statRow('ESTAB', `${cs.established}`));
460
+ lines.push(statRow('LISTEN', `${cs.listening}`));
461
+ }
350
462
  if (data.packets.topProcesses && data.packets.topProcesses.length) {
351
- lines.push(statRow('Top Proc', truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
463
+ const topProc = data.packets.topProcesses[0];
464
+ lines.push(statRow('Top Proc', `${truncatePlain(topProc.command, contentWidth - 25)} (${formatBytes(topProc.rxBytes + topProc.txBytes)})`));
352
465
  }
353
466
  if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
354
467
  lines.push(statRow('Net Procs', `${data.packets.allProcesses.length} processes`));
@@ -392,13 +505,14 @@ export const TAB_DEFS: { id: string; label: string; key: string }[] = [
392
505
  { id: 'power', label: 'Power', key: '4' },
393
506
  { id: 'battery', label: 'Battery', key: '5' },
394
507
  { id: 'thermal', label: 'Thermal', key: '6' },
395
- { id: 'network', label: 'Network', key: '7' },
396
- { id: 'packets', label: 'Packets', key: '8' },
397
- { id: 'tasks', label: 'Tasks', key: '9' },
398
- { id: 'disk', label: 'Disk', key: '0' },
399
- { id: 'process', label: 'Process', key: 'P' },
508
+ { id: 'packets', label: 'Conns', key: '7' },
509
+ { id: 'tasks', label: 'Tasks', key: '8' },
510
+ { id: 'disk', label: 'Disk', key: '9' },
511
+ { id: 'process', label: 'Process', key: '0' },
400
512
  { id: 'p2p', label: 'P2P', key: 'R' },
401
513
  { id: 'anomalies', label: 'Anomalies', key: 'A' },
514
+ { id: 'containers', label: 'Containers', key: 'C' },
515
+ { id: 'blender', label: 'Blender', key: 'B' },
402
516
  ];
403
517
 
404
518
  /** The number of leading columns of indent before the tab bar's first character (see `tabBar`). */
@@ -620,6 +734,58 @@ function diskTableLines(
620
734
  return [head, borderAccent('─'.repeat(contentWidth)), ...rows];
621
735
  }
622
736
 
737
+ function containersTableLines(
738
+ containers: { id: string; name: string; cpuPercent: number; memUsage: string; memPercent: number; netIO: string; blockIO: string; pids: number }[],
739
+ contentWidth: number,
740
+ borderAccent = THEMES.default.border
741
+ ): string[] {
742
+ const idW = 14, cpuW = 8, memW = 8, memUsgW = 20, netW = 20, blockW = 20, pidsW = 6;
743
+ const nameW = Math.max(10, contentWidth - idW - cpuW - memW - memUsgW - netW - blockW - pidsW - 7);
744
+
745
+ const head =
746
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CONTAINER ID ') +
747
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CPU% ') +
748
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' MEM% ') +
749
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' MEM USAGE ') +
750
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' NET I/O ') +
751
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' BLOCK I/O ') +
752
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' PIDS ') +
753
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' NAME'.padEnd(nameW + 1));
754
+
755
+ const rows = containers.map(c => {
756
+ const cpuStr = pctColor(c.cpuPercent)(`${c.cpuPercent.toFixed(1)}`.padEnd(cpuW));
757
+ const memStr = pctColor(c.memPercent)(`${c.memPercent.toFixed(1)}`.padEnd(memW));
758
+
759
+ return (
760
+ truncatePlain(c.id, idW - 1).padEnd(idW) +
761
+ cpuStr +
762
+ memStr +
763
+ c.memUsage.padEnd(memUsgW) +
764
+ c.netIO.padEnd(netW) +
765
+ c.blockIO.padEnd(blockW) +
766
+ String(c.pids).padEnd(pidsW) +
767
+ truncatePlain(c.name, nameW)
768
+ );
769
+ });
770
+
771
+ return [head, borderAccent('─'.repeat(contentWidth)), ...rows];
772
+ }
773
+
774
+ function blenderCard(renders: BlenderRenderData[], contentWidth: number, theme: ThemeColors): string[] {
775
+ if (!renders.length) return ['No active Blender renders detected'];
776
+ const lines: string[] = [];
777
+ for (const r of renders.slice(0, 8)) {
778
+ const statusColor = r.status === 'rendering' ? chalk.green : r.status === 'finalizing' ? chalk.yellow : r.status === 'loading' ? chalk.cyan : chalk.dim;
779
+ const pctStr = `${r.completionPercent}%`;
780
+ const barW = Math.max(4, contentWidth - 28);
781
+ const bar = gaugeBar(r.completionPercent, barW);
782
+ lines.push(`${theme.blender(`PID ${r.pid}`)} ${truncatePlain(r.blendFile, 18)} ${r.renderEngine}`);
783
+ lines.push(` Fra ${String(r.currentFrame).padStart(4)}/${r.totalFrames} ${bar} ${pctStr}`);
784
+ lines.push(` ${r.elapsedSec}s elapsed · ${statusColor(r.status)}${r.sampleCount !== undefined ? ` · ${r.sampleCount} spp` : ''}`);
785
+ }
786
+ return lines;
787
+ }
788
+
623
789
  // --- main dashboard ------------------------------------------------------------
624
790
 
625
791
  function detailPanelTitle(panel: string, trackedPid?: number | null): string {
@@ -635,6 +801,8 @@ function detailPanelTitle(panel: string, trackedPid?: number | null): string {
635
801
  tasks: 'Tasks',
636
802
  disk: 'Disk',
637
803
  process: trackedPid !== undefined && trackedPid !== null ? `Processes (Following PID ${trackedPid})` : 'Processes',
804
+ containers: 'Containers',
805
+ blender: 'Blender Renders',
638
806
  };
639
807
  return map[panel] || panel;
640
808
  }
@@ -654,39 +822,81 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
654
822
  }
655
823
  case 'power': {
656
824
  const power = powerCard(data);
657
- if (!power && !data.battery) return ['No power data available'];
658
- const lines: string[] = [];
659
- if (power) lines.push(...power);
660
- if (data.battery?.powerWatts !== undefined && data.battery.powerWatts > 0) {
661
- lines.push(statRow('Battery', `${data.battery.powerWatts.toFixed(2)} W`));
662
- }
663
- return lines.length ? lines : ['No power data available'];
825
+ if (!power) return ['No power data available'];
826
+ return power;
664
827
  }
665
828
  case 'battery':
666
829
  return data.battery ? batteryCard(data, contentWidth) : ['No battery data available'];
667
830
  case 'thermal':
668
831
  return thermalCard(data, contentWidth);
669
832
  case 'network':
670
- return networkCard(data, contentWidth);
833
+ return networkDetailCard(data, width, theme);
671
834
  case 'packets': {
672
- if (!data.packets) return ['No packet data available'];
673
- const lines: string[] = [];
674
- lines.push(statRow('Total', `${data.packets.totalPackets} pkt`));
675
- lines.push(statRow('RX', `${data.packets.rxPackets} pkt`));
676
- lines.push(statRow('TX', `${data.packets.txPackets} pkt`));
677
- lines.push(statRow('Conns', `${data.packets.connections} TCP`));
678
- if (data.packets.interfaces && data.packets.interfaces.length) {
679
- lines.push(statRow('Ifaces', `${data.packets.interfaces.length} active`));
680
- for (const iface of data.packets.interfaces.slice(0, 8)) {
681
- lines.push(statRow(` ${iface.iface}`, `RX ${formatBytes(iface.rxBytes)} / TX ${formatBytes(iface.txBytes)}`));
682
- }
835
+ if (!data.packets || !data.packets.allProcesses || !data.packets.allProcesses.length) {
836
+ return ['No advanced network tracking data available.'];
683
837
  }
684
- if (data.packets.allProcesses && data.packets.allProcesses.length) {
685
- lines.push(statRow('Net Procs', `${data.packets.allProcesses.length} total`));
686
- for (const proc of data.packets.allProcesses.slice(0, 10)) {
687
- lines.push(statRow(` ${proc.pid}`, `${truncatePlain(proc.command, 20)} TX ${formatBytes(proc.txBytes)}`));
838
+
839
+ const lines: string[] = [];
840
+ const pWidth = 8;
841
+ const cmdWidth = 15;
842
+ const protoWidth = 5;
843
+ const stateWidth = 11;
844
+ const rxWidth = 10;
845
+ const txWidth = 10;
846
+
847
+ const remaining = Math.max(20, width - (pWidth + cmdWidth + protoWidth + stateWidth + rxWidth + txWidth + 7 * 2));
848
+ const localWidth = Math.floor(remaining * 0.4);
849
+ const remoteWidth = remaining - localWidth;
850
+
851
+ const header =
852
+ chalk.bold('PID'.padEnd(pWidth)) + ' ' +
853
+ chalk.bold('COMMAND'.padEnd(cmdWidth)) + ' ' +
854
+ chalk.bold('PROTO'.padEnd(protoWidth)) + ' ' +
855
+ chalk.bold('LOCAL'.padEnd(localWidth)) + ' ' +
856
+ chalk.bold('REMOTE'.padEnd(remoteWidth)) + ' ' +
857
+ chalk.bold('STATE'.padEnd(stateWidth)) + ' ' +
858
+ chalk.bold('RX'.padStart(rxWidth)) + ' ' +
859
+ chalk.bold('TX'.padStart(txWidth));
860
+
861
+ lines.push(header);
862
+ lines.push(chalk.dim('─'.repeat(width)));
863
+
864
+ for (const proc of data.packets.allProcesses) {
865
+ if (!proc.connections || proc.connections.length === 0) continue;
866
+
867
+ const summaryCmd = truncatePlain(proc.command, cmdWidth + protoWidth + localWidth + remoteWidth + stateWidth + 8);
868
+ const summaryRx = formatBytes(proc.rxBytes).padStart(rxWidth);
869
+ const summaryTx = formatBytes(proc.txBytes).padStart(txWidth);
870
+
871
+ lines.push(
872
+ chalk.cyan(proc.pid.toString().padEnd(pWidth)) + ' ' +
873
+ chalk.cyan(summaryCmd.padEnd(width - pWidth - rxWidth - txWidth - 6)) + ' ' +
874
+ chalk.yellow(summaryRx) + ' ' +
875
+ chalk.yellow(summaryTx)
876
+ );
877
+
878
+ for (const conn of proc.connections) {
879
+ const cProto = chalk.dim(truncatePlain(conn.protocol, protoWidth).padEnd(protoWidth));
880
+ const cLocal = chalk.dim(truncatePlain(conn.local, localWidth).padEnd(localWidth));
881
+ const cRemote = truncatePlain(conn.remote, remoteWidth).padEnd(remoteWidth);
882
+
883
+ const cStateStr = conn.state === 'Established' ? chalk.green('ESTABLISHED'.padEnd(stateWidth)) : chalk.dim(truncatePlain(conn.state, stateWidth).padEnd(stateWidth));
884
+ const cRx = chalk.dim(formatBytes(conn.rxBytes).padStart(rxWidth));
885
+ const cTx = chalk.dim(formatBytes(conn.txBytes).padStart(txWidth));
886
+
887
+ lines.push(
888
+ ' '.repeat(pWidth) + ' ' +
889
+ ' '.repeat(cmdWidth) + ' ' +
890
+ cProto + ' ' +
891
+ cLocal + ' ' +
892
+ cRemote + ' ' +
893
+ cStateStr + ' ' +
894
+ cRx + ' ' +
895
+ cTx
896
+ );
688
897
  }
689
898
  }
899
+
690
900
  return lines;
691
901
  }
692
902
  case 'tasks':
@@ -749,15 +959,41 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
749
959
  const lines: string[] = [];
750
960
  // Sort by timestamp descending
751
961
  const sorted = [...opts.anomalyHistory].sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0));
752
- for (const a of sorted) {
962
+ for (const a of sorted.slice(0, 10)) {
753
963
  const color = a.severity === 'critical' ? chalk.red.bold : chalk.yellow;
754
964
  const direction = a.zScore > 0 ? '↑ spike' : '↓ drop';
755
965
  const valStr = a.metric.toLowerCase().includes('net') ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
756
966
  const timeStr = a.timestamp ? a.timestamp.toLocaleTimeString() : '';
757
967
  lines.push(color(` ${timeStr.padEnd(10)} ⚠ ${a.metric.padEnd(8)}: ${valStr.padEnd(12)} (${direction}, σ=${a.zScore.toFixed(1)})`));
758
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
+ }
759
988
  return lines;
760
989
  }
990
+ case 'containers': {
991
+ return data.containers && data.containers.length ? containersTableLines(data.containers, width - 4, theme.border) : ['No container data available'];
992
+ }
993
+ case 'blender': {
994
+ const renders = data.blenderRenders && data.blenderRenders.length ? data.blenderRenders : [];
995
+ return blenderCard(renders, contentWidth, theme);
996
+ }
761
997
  default:
762
998
  return null;
763
999
  }
@@ -1034,7 +1270,7 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
1034
1270
  lines.push(gaugeRow('GPU', data.gpu.utilization, contentWidth));
1035
1271
  }
1036
1272
  const tempC = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
1037
- if (tempC !== undefined) {
1273
+ if (tempC != null) {
1038
1274
  const tempStr = formatTempUnit(tempC, opts.tempUnit);
1039
1275
  lines.push(statRow('Temp', thermalColor(data.thermal.pressureLevel)(tempStr)));
1040
1276
  }
@@ -26,6 +26,7 @@ export interface ThemeColors {
26
26
  disk: (s: string) => string;
27
27
  graphs: (s: string) => string;
28
28
  process: (s: string) => string;
29
+ blender: (s: string) => string;
29
30
  }
30
31
 
31
32
  export const BUILTIN_THEMES: Record<string, ThemeColors> = {
@@ -41,6 +42,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
41
42
  disk: chalk.hex('#f472b6'),
42
43
  graphs: chalk.hex('#22d3ee'),
43
44
  process: chalk.hex('#e5e7eb'),
45
+ blender: chalk.hex('#f97316'),
44
46
  },
45
47
  dracula: {
46
48
  border: chalk.hex('#6272a4'),
@@ -54,6 +56,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
54
56
  disk: chalk.hex('#ff5555'),
55
57
  graphs: chalk.hex('#8be9fd'),
56
58
  process: chalk.hex('#f8f8f2'),
59
+ blender: chalk.hex('#ffb86c'),
57
60
  },
58
61
  cyberpunk: {
59
62
  border: chalk.hex('#ff0055'),
@@ -67,6 +70,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
67
70
  disk: chalk.hex('#ff007f'),
68
71
  graphs: chalk.hex('#00ffcc'),
69
72
  process: chalk.hex('#ffffff'),
73
+ blender: chalk.hex('#ff6600'),
70
74
  },
71
75
  monochrome: {
72
76
  border: chalk.gray,
@@ -80,6 +84,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
80
84
  disk: chalk.white.bold,
81
85
  graphs: chalk.white.bold,
82
86
  process: chalk.white.bold,
87
+ blender: chalk.white.bold,
83
88
  },
84
89
  nord: {
85
90
  border: chalk.hex('#4c566a'),
@@ -93,6 +98,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
93
98
  disk: chalk.hex('#b48ead'),
94
99
  graphs: chalk.hex('#88c0d0'),
95
100
  process: chalk.hex('#eceff4'),
101
+ blender: chalk.hex('#d08770'),
96
102
  },
97
103
  gruvbox: {
98
104
  border: chalk.hex('#928374'),
@@ -106,6 +112,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
106
112
  disk: chalk.hex('#d3869b'),
107
113
  graphs: chalk.hex('#fb4934'),
108
114
  process: chalk.hex('#ebdbb2'),
115
+ blender: chalk.hex('#fe8019'),
109
116
  },
110
117
  };
111
118
 
@@ -134,6 +141,7 @@ export function loadCustomThemes(): Record<string, ThemeColors> {
134
141
  disk: buildColor(data.disk),
135
142
  graphs: buildColor(data.graphs),
136
143
  process: buildColor(data.process),
144
+ blender: buildColor(data.blender),
137
145
  };
138
146
  } catch {
139
147
  // ignore bad theme file