pyre-cli 10.3.0 → 10.5.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 (88) hide show
  1. package/README.md +41 -0
  2. package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
  3. package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
  4. package/dist/benchGitCmd-HIXUC5EK.js +87 -0
  5. package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
  6. package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
  7. package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
  8. package/dist/budgetCmd-TGO3RYLN.js +11 -0
  9. package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
  10. package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
  11. package/dist/chunk-3RG5ZIWI.js +10 -0
  12. package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
  13. package/dist/chunk-JH7P6KLN.js +143 -0
  14. package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
  15. package/dist/chunk-NMGQVRCA.js +111 -0
  16. package/dist/chunk-OCL7P7N7.js +936 -0
  17. package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
  18. package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
  19. package/dist/chunk-UF3XQ3R3.js +3800 -0
  20. package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
  21. package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
  22. package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
  23. package/dist/containersCmd-FSMYOBG2.js +84 -0
  24. package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
  25. package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
  26. package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
  27. package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
  28. package/dist/flightRecorder-GJIAQOXC.js +162 -0
  29. package/dist/index.js +736 -4125
  30. package/dist/inspectCmd-5FXJBJTK.js +137 -0
  31. package/dist/menu-AJFGDLBP.js +1233 -0
  32. package/dist/monitors/platform/windows-ui.ps1 +31 -29
  33. package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
  34. package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
  35. package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
  36. package/dist/playbookCmd-FRROIEXR.js +145 -0
  37. package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
  38. package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
  39. package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
  40. package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
  41. package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
  42. package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
  43. package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
  44. package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
  45. package/dist/statuslineCmd-EUKYRR2G.js +75 -0
  46. package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
  47. package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
  48. package/dist/topoCmd-RTXQJ6XA.js +189 -0
  49. package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
  50. package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
  51. package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
  52. package/package.json +2 -2
  53. package/src/benchGitCmd.ts +110 -0
  54. package/src/budgetCmd.ts +199 -0
  55. package/src/completions.ts +42 -3
  56. package/src/containersCmd.ts +106 -0
  57. package/src/doctor.ts +86 -6
  58. package/src/easy_features.test.ts +83 -0
  59. package/src/flightRecorder.ts +214 -0
  60. package/src/index.ts +181 -8
  61. package/src/inspectCmd.ts +165 -0
  62. package/src/live/input.ts +70 -3
  63. package/src/live/render.ts +1 -1
  64. package/src/live/state.ts +2 -1
  65. package/src/menu/commands.ts +754 -0
  66. package/src/menu/index.ts +358 -0
  67. package/src/menu/render.ts +272 -0
  68. package/src/menu/types.ts +63 -0
  69. package/src/menu.test.ts +110 -0
  70. package/src/monitors/blender.ts +49 -19
  71. package/src/monitors/buildTracker.ts +72 -54
  72. package/src/monitors/platform/windows-ui.ps1 +31 -29
  73. package/src/monitors/platform/windows.ts +636 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +146 -0
  76. package/src/playbookCmd.ts +156 -0
  77. package/src/power_features.test.ts +251 -0
  78. package/src/profileProcCmd.ts +68 -21
  79. package/src/replayCmd.ts +15 -6
  80. package/src/smartCmd.ts +149 -10
  81. package/src/state/config.ts +13 -0
  82. package/src/statuslineCmd.ts +87 -0
  83. package/src/syncCmd.ts +12 -0
  84. package/src/topoCmd.ts +83 -50
  85. package/src/watchdogCmd.ts +40 -3
  86. package/src/xbar.ts +195 -17
  87. package/dist/chunk-M73HCQD2.js +0 -95
  88. package/dist/topoCmd-DZ3YS5IL.js +0 -148
@@ -3,9 +3,13 @@
3
3
  *
4
4
  * Gathers metrics on Windows (win32) using Node.js standard APIs,
5
5
  * PowerShell CIM/WMI, Windows Performance Counters, tasklist, and netstat.
6
+ *
7
+ * Fully supports Intel, AMD, and NVIDIA processors and graphics cards.
6
8
  */
7
9
 
8
10
  import os from 'node:os';
11
+ import path from 'node:path';
12
+ import fs from 'node:fs';
9
13
  import { run, runPowerShell } from '../run.js';
10
14
  import type {
11
15
  CpuData,
@@ -21,9 +25,13 @@ import type {
21
25
  DisplayInfo,
22
26
  PowerData,
23
27
  ConnectionStateStats,
28
+ RemoteHostInfo,
29
+ NetworkProcess,
24
30
  } from '../types.js';
25
31
 
26
32
  let prevNetSample: { rxBytes: number; txBytes: number; ts: number } | null = null;
33
+ let emaLoadAvg: [number, number, number] = [0, 0, 0];
34
+ let lastLoadTime = 0;
27
35
 
28
36
  function safeParseFloat(s: string | undefined): number {
29
37
  if (!s) return 0;
@@ -37,15 +45,7 @@ function safeParseInt(s: string | undefined, def = 0): number {
37
45
  return Number.isFinite(n) ? n : def;
38
46
  }
39
47
 
40
- function parsePipeOutput(raw: string): string[][] {
41
- return raw
42
- .split('\n')
43
- .map(l => l.trim())
44
- .filter(l => l.length > 0)
45
- .map(l => l.split('|').map(s => s.trim()));
46
- }
47
-
48
- async function getCimProperty(className: string, property: string): Promise<string> {
48
+ export async function getCimProperty(className: string, property: string): Promise<string> {
49
49
  const raw = await runPowerShell(
50
50
  `(Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ${property}) -join '|'`,
51
51
  ''
@@ -53,7 +53,7 @@ async function getCimProperty(className: string, property: string): Promise<stri
53
53
  return raw.split('|')[0] || '';
54
54
  }
55
55
 
56
- async function getCimRow(className: string): Promise<Record<string, string>> {
56
+ export async function getCimRow(className: string): Promise<Record<string, string>> {
57
57
  const raw = await runPowerShell(
58
58
  `Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -Property * -ExcludeProperty CIM* | ConvertTo-Csv -NoTypeInformation`,
59
59
  ''
@@ -93,6 +93,82 @@ export async function collectWindowsSystem(): Promise<{ hostname: string; os: st
93
93
  return { hostname, os: osName, uptime };
94
94
  }
95
95
 
96
+ // ─── CPU & Architecture ────────────────────────────────────────────────────────
97
+
98
+ export function parseWindowsCpuBrand(rawName: string, fallback: string): string {
99
+ if (!rawName) return fallback;
100
+ return rawName
101
+ .replace(/\s+/g, ' ')
102
+ .replace(/,\s*$/, '')
103
+ .trim() || fallback;
104
+ }
105
+
106
+ export function detectProcessorVendor(brand: string): 'intel' | 'amd' | 'other' {
107
+ const b = brand.toLowerCase();
108
+ if (b.includes('amd') || b.includes('ryzen') || b.includes('threadripper') || b.includes('epyc') || b.includes('athlon')) {
109
+ return 'amd';
110
+ }
111
+ if (b.includes('intel') || b.includes('xeon') || /\bcore\s*(i[3579]|ultra|2)/i.test(brand)) {
112
+ return 'intel';
113
+ }
114
+ return 'other';
115
+ }
116
+
117
+ export function estimateCpuTdp(brand: string): number {
118
+ const b = brand.toLowerCase();
119
+ const isAmd = b.includes('amd') || b.includes('ryzen');
120
+ const isIntel = b.includes('intel') || b.includes('core');
121
+
122
+ if (isAmd) {
123
+ if (b.includes('threadripper') || b.includes('epyc')) return 280;
124
+ if (b.includes('ryzen 9') || b.includes('7950') || b.includes('7900') || b.includes('5950') || b.includes('5900') || b.includes('9950') || b.includes('9900')) return 170;
125
+ if (b.includes('ryzen 7') || b.includes('7700') || b.includes('7800x3d') || b.includes('5800') || b.includes('9700')) return 105;
126
+ if (b.includes('ryzen 5') || b.includes('7600') || b.includes('5600')) return 65;
127
+ if (b.includes('u') || b.includes('hs') || b.includes('mobile')) return 28;
128
+ return 65;
129
+ }
130
+
131
+ if (isIntel) {
132
+ if (b.includes('xeon')) return 205;
133
+ if (b.includes('i9') || b.includes('14900') || b.includes('13900') || b.includes('12900')) return 125;
134
+ if (b.includes('i7') || b.includes('14700') || b.includes('13700') || b.includes('12700')) return 125;
135
+ if (b.includes('i5')) return 65;
136
+ if (b.includes('i3')) return 60;
137
+ if (b.includes('ultra')) return 45;
138
+ if (b.includes('h') || b.includes('hx')) return 45;
139
+ if (b.includes('u') || b.includes('p')) return 28;
140
+ return 65;
141
+ }
142
+
143
+ return 65;
144
+ }
145
+
146
+ export function updateEmaLoadAvg(currentUsage: number, cores: number): [number, number, number] {
147
+ const activeLoad = Math.round((currentUsage / 100) * cores * 100) / 100;
148
+ const now = Date.now();
149
+ if (lastLoadTime === 0) {
150
+ lastLoadTime = now;
151
+ emaLoadAvg = [activeLoad, activeLoad, activeLoad];
152
+ return emaLoadAvg;
153
+ }
154
+
155
+ const dtSec = Math.max((now - lastLoadTime) / 1000, 0.1);
156
+ lastLoadTime = now;
157
+
158
+ // Exponential moving average constants for 1m, 5m, 15m
159
+ const exp1 = Math.exp(-dtSec / 60);
160
+ const exp5 = Math.exp(-dtSec / 300);
161
+ const exp15 = Math.exp(-dtSec / 900);
162
+
163
+ emaLoadAvg = [
164
+ Math.round((activeLoad * (1 - exp1) + emaLoadAvg[0] * exp1) * 100) / 100,
165
+ Math.round((activeLoad * (1 - exp5) + emaLoadAvg[1] * exp5) * 100) / 100,
166
+ Math.round((activeLoad * (1 - exp15) + emaLoadAvg[2] * exp15) * 100) / 100,
167
+ ];
168
+
169
+ return emaLoadAvg;
170
+ }
171
+
96
172
  export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: number; idle: number }[] | null }): Promise<CpuData> {
97
173
  const cpus = os.cpus();
98
174
  const cores = cpus.length || 1;
@@ -103,7 +179,7 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
103
179
 
104
180
  try {
105
181
  const cpuRow = await getCimRow('Win32_Processor');
106
- if (cpuRow.Name) brand = cpuRow.Name.trim();
182
+ if (cpuRow.Name) brand = parseWindowsCpuBrand(cpuRow.Name, brand);
107
183
  if (cpuRow.NumberOfCores) physicalCores = safeParseInt(cpuRow.NumberOfCores, cores);
108
184
  if (cpuRow.MaxClockSpeed) frequency = safeParseInt(cpuRow.MaxClockSpeed, frequency);
109
185
  } catch {
@@ -132,37 +208,10 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
132
208
  }
133
209
  prevCpuTimesRef.current = current;
134
210
 
135
- const activeLoad = Math.round((usage / 100) * cores * 100) / 100;
136
- const loadAvg = [activeLoad, activeLoad, activeLoad];
137
-
138
- let temperature: number | undefined;
139
-
140
- try {
141
- const raw = await runPowerShell(
142
- `(Get-Counter '\Thermal Zone Information(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
143
- ''
144
- ).catch(() => '');
145
- const tempK = parseFloat(raw);
146
- if (tempK > 200 && tempK < 400) {
147
- temperature = Math.round((tempK - 273.15) * 10) / 10;
148
- }
149
- } catch {
150
- // ignore
151
- }
211
+ const loadAvg = updateEmaLoadAvg(usage, cores);
152
212
 
153
- if (temperature === undefined) {
154
- try {
155
- const rawWmi = await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500);
156
- const match = rawWmi.match(/(\d{3,4})/);
157
- if (match) {
158
- const kelvinTenths = parseInt(match[1], 10);
159
- const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
160
- if (c >= 15 && c <= 115) temperature = c;
161
- }
162
- } catch {
163
- // ignore
164
- }
165
- }
213
+ // Temperature reading across modern CIM and Perf Counters
214
+ let temperature = await getWindowsCpuTemperature();
166
215
 
167
216
  return {
168
217
  brand,
@@ -176,6 +225,56 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
176
225
  };
177
226
  }
178
227
 
228
+ export async function getWindowsCpuTemperature(): Promise<number | undefined> {
229
+ // 1. Try LibreHardwareMonitor or OpenHardwareMonitor WMI (widely supported on AMD Ryzen and Intel desktops)
230
+ try {
231
+ const lhmRaw = await runPowerShell(
232
+ `Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%CPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
233
+ ''
234
+ );
235
+ const lhmVal = safeParseFloat(lhmRaw);
236
+ if (lhmVal >= 15 && lhmVal <= 115) return Math.round(lhmVal * 10) / 10;
237
+ } catch {}
238
+
239
+ try {
240
+ const ohmRaw = await runPowerShell(
241
+ `Get-CimInstance -Namespace root/OpenHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%CPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
242
+ ''
243
+ );
244
+ const ohmVal = safeParseFloat(ohmRaw);
245
+ if (ohmVal >= 15 && ohmVal <= 115) return Math.round(ohmVal * 10) / 10;
246
+ } catch {}
247
+
248
+ // 2. Try PowerShell CIM MSAcpi_ThermalZoneTemperature (modern Windows 10/11 replacement for wmic)
249
+ try {
250
+ const cimRaw = await runPowerShell(
251
+ `(Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty CurrentTemperature)`,
252
+ ''
253
+ );
254
+ const kelvinTenths = safeParseInt(cimRaw);
255
+ if (kelvinTenths > 2732 && kelvinTenths < 4200) {
256
+ const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
257
+ if (c >= 15 && c <= 115) return c;
258
+ }
259
+ } catch {}
260
+
261
+ // 3. Try Windows Performance Counter
262
+ try {
263
+ const raw = await runPowerShell(
264
+ `(Get-Counter '\\Thermal Zone Information(*)\\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
265
+ ''
266
+ );
267
+ const tempK = safeParseFloat(raw);
268
+ if (tempK > 200 && tempK < 400) {
269
+ return Math.round((tempK - 273.15) * 10) / 10;
270
+ }
271
+ } catch {}
272
+
273
+ return undefined;
274
+ }
275
+
276
+ // ─── Memory ───────────────────────────────────────────────────────────────────
277
+
179
278
  export async function collectWindowsMemory(): Promise<MemoryData> {
180
279
  const total = os.totalmem();
181
280
  const free = os.freemem();
@@ -221,6 +320,8 @@ export async function collectWindowsMemory(): Promise<MemoryData> {
221
320
  };
222
321
  }
223
322
 
323
+ // ─── Disks ─────────────────────────────────────────────────────────────────────
324
+
224
325
  export async function collectWindowsDisk(): Promise<DiskData[]> {
225
326
  try {
226
327
  const raw = await runPowerShell(
@@ -278,6 +379,8 @@ function formatBytesStr(bytes: number): string {
278
379
  return (bytes / 1024).toFixed(0) + 'K';
279
380
  }
280
381
 
382
+ // ─── Battery ───────────────────────────────────────────────────────────────────
383
+
281
384
  export async function collectWindowsBattery(): Promise<BatteryData | null> {
282
385
  try {
283
386
  const raw = await runPowerShell(
@@ -317,67 +420,53 @@ export async function collectWindowsBattery(): Promise<BatteryData | null> {
317
420
  }
318
421
  }
319
422
 
423
+ // ─── Thermal & Power ───────────────────────────────────────────────────────────
424
+
320
425
  export async function collectWindowsThermal(): Promise<ThermalData> {
321
426
  const temperatures: Record<string, number | null> = {};
322
427
 
323
- let tempC: number | undefined;
428
+ const cpuTemp = await getWindowsCpuTemperature();
429
+ if (cpuTemp !== undefined) {
430
+ temperatures['cpu'] = cpuTemp;
431
+ }
432
+
433
+ // GPU temperature across NVIDIA / AMD / Intel
434
+ let gpuTemp: number | undefined;
324
435
 
436
+ // Try nvidia-smi
325
437
  try {
326
- const raw = await runPowerShell(
327
- `(Get-Counter '\Thermal Zone Information(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
328
- ''
329
- ).catch(() => '');
330
- const tempK = parseFloat(raw);
331
- if (tempK > 200 && tempK < 400) {
332
- tempC = Math.round((tempK - 273.15) * 10) / 10;
333
- temperatures['cpu'] = tempC;
334
- }
335
- } catch {
336
- // ignore
337
- }
438
+ const nvRaw = await runNvidiaSmiQuery('temperature.gpu');
439
+ const t = safeParseFloat(nvRaw);
440
+ if (t > 0 && t < 125) gpuTemp = t;
441
+ } catch {}
338
442
 
339
- if (tempC === undefined) {
443
+ // Try LibreHardwareMonitor GPU temperature
444
+ if (gpuTemp === undefined) {
340
445
  try {
341
- const rawWmi = await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500);
342
- const match = rawWmi.match(/(\d{3,4})/);
343
- if (match) {
344
- const kelvinTenths = parseInt(match[1], 10);
345
- const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
346
- if (c >= 15 && c <= 115) {
347
- tempC = c;
348
- temperatures['cpu'] = c;
349
- }
350
- }
351
- } catch {
352
- // ignore
353
- }
446
+ const lhmGpu = await runPowerShell(
447
+ `Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
448
+ ''
449
+ );
450
+ const t = safeParseFloat(lhmGpu);
451
+ if (t > 0 && t < 125) gpuTemp = t;
452
+ } catch {}
354
453
  }
355
454
 
356
- let gpuTemp: number | undefined;
357
- try {
358
- const gpuRaw = await runPowerShell(
359
- `(Get-Counter '\GPU(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CounterName -match 'Temperature' -and $_.CookedValue -gt 0 } | Select-Object -First 1 -ExpandProperty CookedValue`,
360
- ''
361
- ).catch(() => '');
362
- gpuTemp = parseFloat(gpuRaw);
363
- if (gpuTemp > 0 && gpuTemp < 150) {
364
- temperatures['gpu'] = Math.round(gpuTemp * 10) / 10;
365
- }
366
- } catch {
367
- // ignore
455
+ if (gpuTemp !== undefined) {
456
+ temperatures['gpu'] = gpuTemp;
368
457
  }
369
458
 
370
- const cpuTemp = tempC ?? gpuTemp;
459
+ const primaryTemp = cpuTemp ?? gpuTemp;
371
460
  let state = 'Nominal';
372
461
  let pressureLevel = 0;
373
- if (cpuTemp !== undefined) {
374
- if (cpuTemp >= 100) {
462
+ if (primaryTemp !== undefined) {
463
+ if (primaryTemp >= 100) {
375
464
  state = 'Critical';
376
465
  pressureLevel = 3;
377
- } else if (cpuTemp >= 90) {
466
+ } else if (primaryTemp >= 90) {
378
467
  state = 'Serious';
379
468
  pressureLevel = 2;
380
- } else if (cpuTemp >= 80) {
469
+ } else if (primaryTemp >= 80) {
381
470
  state = 'Fair';
382
471
  pressureLevel = 1;
383
472
  }
@@ -385,7 +474,7 @@ export async function collectWindowsThermal(): Promise<ThermalData> {
385
474
 
386
475
  return {
387
476
  state,
388
- detail: cpuTemp !== undefined ? `${state} (${cpuTemp}°C)` : state,
477
+ detail: primaryTemp !== undefined ? `${state} (${primaryTemp}°C)` : state,
389
478
  pressureLevel,
390
479
  temperatures: Object.keys(temperatures).length ? temperatures : undefined,
391
480
  };
@@ -394,6 +483,7 @@ export async function collectWindowsThermal(): Promise<ThermalData> {
394
483
  export async function collectWindowsPower(): Promise<PowerData | null> {
395
484
  const power: PowerData = {};
396
485
 
486
+ // 1. Try battery discharge wattage
397
487
  try {
398
488
  const batRaw = await runPowerShell(
399
489
  `$bat = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue; if ($bat) { "$($bat.DischargeRate)|$($bat.BatteryStatus)" } else { '|0' }`,
@@ -406,36 +496,58 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
406
496
  if (dischargeRate > 0 && (statusCode === 1 || statusCode === 2)) {
407
497
  power.combinedWatts = Math.round((dischargeRate / 1000) * 100) / 100;
408
498
  }
409
- } catch {
410
- // ignore
411
- }
499
+ } catch {}
412
500
 
501
+ // 2. Try LibreHardwareMonitor exact power readings for CPU & GPU
413
502
  try {
414
- const cpu = os.cpus();
415
- const usage = cpu.length > 0 ? Math.round(cpu.reduce((a, c) => a + (c.times.user + c.times.nice + c.times.sys + c.times.irq - c.times.idle) / (c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq), 0) / cpu.length * 100) : 0;
416
- const tdpWatts = 65;
417
- if (usage > 0) {
418
- power.cpuWatts = Math.round((usage / 100) * tdpWatts * 100) / 100;
419
- }
420
- } catch {
421
- // ignore
503
+ const lhmCpu = await runPowerShell(
504
+ `Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Power' and Name like '%CPU Package%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
505
+ ''
506
+ );
507
+ const cpuP = safeParseFloat(lhmCpu);
508
+ if (cpuP > 0) power.cpuWatts = Math.round(cpuP * 100) / 100;
509
+
510
+ const lhmGpu = await runPowerShell(
511
+ `Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Power' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
512
+ ''
513
+ );
514
+ const gpuP = safeParseFloat(lhmGpu);
515
+ if (gpuP > 0) power.gpuWatts = Math.round(gpuP * 100) / 100;
516
+ } catch {}
517
+
518
+ // 3. NVIDIA SMI power draw fallback
519
+ if (!power.gpuWatts) {
520
+ try {
521
+ const nvPower = await runNvidiaSmiQuery('power.draw');
522
+ const p = safeParseFloat(nvPower);
523
+ if (p > 0) power.gpuWatts = Math.round(p * 100) / 100;
524
+ } catch {}
422
525
  }
423
526
 
424
- try {
425
- const nvRaw = await run('nvidia-smi --query-gpu=power.draw,utilization.gpu --format=csv,noheader,nounits 2>nul', '').catch(() => '');
426
- const nvLines = nvRaw.split('\n').filter(l => l.trim());
427
- for (const line of nvLines) {
428
- const parts = line.trim().split(',').map(s => s.trim());
429
- const pwr = parseFloat(parts[0]);
430
- if (pwr > 0) {
431
- power.gpuWatts = Math.round(pwr * 100) / 100;
432
- if (!power.combinedWatts) power.combinedWatts = power.gpuWatts;
433
- else power.combinedWatts = Math.round((power.combinedWatts + power.gpuWatts) * 100) / 100;
434
- break;
527
+ // 4. CPU usage TDP estimation fallback if direct sensor unavailable
528
+ if (!power.cpuWatts) {
529
+ try {
530
+ const cpu = os.cpus();
531
+ const brand = cpu[0]?.model || '';
532
+ const tdpWatts = estimateCpuTdp(brand);
533
+ const usage = cpu.length > 0
534
+ ? Math.round(
535
+ (cpu.reduce(
536
+ (a, c) =>
537
+ a +
538
+ (c.times.user + c.times.nice + c.times.sys + c.times.irq - c.times.idle) /
539
+ (c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq),
540
+ 0
541
+ ) /
542
+ cpu.length) *
543
+ 100
544
+ )
545
+ : 0;
546
+
547
+ if (usage > 0) {
548
+ power.cpuWatts = Math.round((usage / 100) * tdpWatts * 100) / 100;
435
549
  }
436
- }
437
- } catch {
438
- // ignore
550
+ } catch {}
439
551
  }
440
552
 
441
553
  if (power.combinedWatts || power.cpuWatts || power.gpuWatts) {
@@ -448,117 +560,263 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
448
560
  return null;
449
561
  }
450
562
 
451
- async function getNvidiaGpu(): Promise<GpuData | null> {
452
- try {
453
- const nvRaw = await run('nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu,temperature.gpu,power.draw --format=csv,noheader,nounits 2>nul', '');
454
- const nvLines = nvRaw.split('\n').filter(l => l.trim());
455
- if (nvLines.length > 0) {
456
- const parts = nvLines[0].trim().split(',').map(s => s.trim());
457
- if (parts.length >= 3) {
458
- const model = parts[0];
459
- const memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
460
- const utilization = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
461
- const temperature = parts[3] ? parseFloat(parts[3]) : undefined;
462
- const powerDraw = parts[4] ? parseFloat(parts[4]) : undefined;
463
- return {
464
- model,
465
- memory,
466
- utilization,
467
- temperature: Number.isFinite(temperature as number) ? temperature : undefined,
468
- processes: 0,
469
- powerDraw: Number.isFinite(powerDraw as number) ? powerDraw : undefined,
470
- };
563
+ // ─── GPU Telemetry Across Intel, AMD, NVIDIA ─────────────────────────────────
564
+
565
+ let nvidiaSmiPathCache: { path: string | null; checkedAt: number } | null = null;
566
+ const NVIDIA_SMI_NEGATIVE_TTL_MS = 5 * 60 * 1000;
567
+
568
+ async function findNvidiaSmiPath(): Promise<string> {
569
+ if (nvidiaSmiPathCache) {
570
+ if (nvidiaSmiPathCache.path) return nvidiaSmiPathCache.path;
571
+ // Not found previously: only re-probe occasionally (e.g. driver installed while running)
572
+ if (Date.now() - nvidiaSmiPathCache.checkedAt < NVIDIA_SMI_NEGATIVE_TTL_MS) return 'nvidia-smi';
573
+ }
574
+ const commonPaths = [
575
+ 'nvidia-smi',
576
+ path.join(process.env.ProgramFiles || 'C:\\Program Files', 'NVIDIA Corporation', 'NVSMI', 'nvidia-smi.exe'),
577
+ path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'nvidia-smi.exe'),
578
+ ];
579
+ for (const p of commonPaths) {
580
+ try {
581
+ const test = await run(`"${p}" -L 2>nul`, '');
582
+ if (test.includes('GPU 0:')) {
583
+ nvidiaSmiPathCache = { path: p, checkedAt: Date.now() };
584
+ return p;
471
585
  }
472
- }
473
- } catch {
474
- // ignore
586
+ } catch {}
475
587
  }
588
+ nvidiaSmiPathCache = { path: null, checkedAt: Date.now() };
589
+ return 'nvidia-smi';
590
+ }
591
+
592
+ async function runNvidiaSmiQuery(query: string): Promise<string> {
593
+ const smiPath = await findNvidiaSmiPath();
594
+ if (nvidiaSmiPathCache && nvidiaSmiPathCache.path === null) return '';
595
+ return (await run(`"${smiPath}" --query-gpu=${query} --format=csv,noheader,nounits 2>nul`, '')).trim();
596
+ }
597
+
598
+ export function parseNvidiaSmiOutput(csv: string): GpuData | null {
599
+ const lines = csv.split('\n').filter(l => l.trim().length > 0);
600
+ if (lines.length === 0) return null;
601
+ const parts = lines[0].split(',').map(s => s.trim());
602
+ if (parts.length < 3) return null;
603
+
604
+ const model = parts[0];
605
+ const memory = (safeParseFloat(parts[1])) * 1024 * 1024;
606
+ const utilization = Math.min(100, Math.max(0, Math.round(safeParseFloat(parts[2]))));
607
+ const temperature = parts[3] ? safeParseFloat(parts[3]) : undefined;
608
+ const powerDraw = parts[4] ? safeParseFloat(parts[4]) : undefined;
609
+
610
+ return {
611
+ model,
612
+ memory,
613
+ utilization,
614
+ temperature: Number.isFinite(temperature) && (temperature as number) > 0 ? temperature : undefined,
615
+ processes: 0,
616
+ powerDraw: Number.isFinite(powerDraw) && (powerDraw as number) > 0 ? powerDraw : undefined,
617
+ };
618
+ }
619
+
620
+ export async function getNvidiaGpu(): Promise<GpuData | null> {
621
+ try {
622
+ const raw = await runNvidiaSmiQuery('gpu_name,memory.total,utilization.gpu,temperature.gpu,power.draw');
623
+ const parsed = parseNvidiaSmiOutput(raw);
624
+ if (parsed) {
625
+ // Query active compute / graphics process count
626
+ try {
627
+ const smiPath = await findNvidiaSmiPath();
628
+ const procList = await run(`"${smiPath}" --query-compute-apps=pid --format=csv,noheader 2>nul`, '');
629
+ const pids = procList.split('\n').filter(l => l.trim().length > 0);
630
+ parsed.processes = pids.length;
631
+ } catch {}
632
+ return parsed;
633
+ }
634
+ } catch {}
476
635
  return null;
477
636
  }
478
637
 
479
- async function getWmiGpu(): Promise<GpuData | null> {
638
+ export interface WindowsVideoControllerInfo {
639
+ name: string;
640
+ adapterRam: number;
641
+ driverVersion: string;
642
+ videoProcessor: string;
643
+ pnpDeviceId: string;
644
+ }
645
+
646
+ export function parseWindowsVideoControllers(csvRaw: string): WindowsVideoControllerInfo[] {
647
+ const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
648
+ if (lines.length < 2) return [];
649
+
650
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
651
+ const controllers: WindowsVideoControllerInfo[] = [];
652
+
653
+ for (const line of lines.slice(1)) {
654
+ const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
655
+ const row: Record<string, string> = {};
656
+ headers.forEach((h, i) => { row[h] = values[i] || ''; });
657
+
658
+ const name = row.Name || row.VideoProcessor || 'Windows Display Adapter';
659
+ const ram = safeParseInt(row.AdapterRAM, 0);
660
+ controllers.push({
661
+ name,
662
+ adapterRam: ram,
663
+ driverVersion: row.DriverVersion || '',
664
+ videoProcessor: row.VideoProcessor || '',
665
+ pnpDeviceId: row.PNPDeviceID || '',
666
+ });
667
+ }
668
+
669
+ return controllers;
670
+ }
671
+
672
+ function isVirtualGpuName(name: string): boolean {
673
+ return /microsoft basic|remote display|hyper-v|virtual|vmware|virtualbox|parsec|citrix|indirect display|displaylink/i.test(name);
674
+ }
675
+
676
+ function isDiscreteGpuName(name: string): boolean {
677
+ const n = name.toLowerCase();
678
+ if (isVirtualGpuName(n)) return false;
679
+ return (
680
+ /\b(rtx|gtx|geforce|quadro|titan|tesla)\b/.test(n) ||
681
+ /\bnvidia\b/.test(n) ||
682
+ /radeon\s+(rx|pro|vii|r[5-9]|hd)\b/.test(n) ||
683
+ /\bfirepro\b/.test(n) ||
684
+ (/\barc\b/.test(n) && /\b(a\d{3}|b\d{3}|pro)\b/.test(n))
685
+ );
686
+ }
687
+
688
+ export function prioritizeGpuControllers(controllers: WindowsVideoControllerInfo[]): WindowsVideoControllerInfo[] {
689
+ // Order: discrete (NVIDIA, AMD Radeon RX/Pro, Intel Arc) > integrated (Intel UHD/Iris/Xe, AMD Radeon(TM) Graphics) > virtual/basic adapters
690
+ const rank = (c: WindowsVideoControllerInfo) =>
691
+ isVirtualGpuName(c.name) ? 2 : isDiscreteGpuName(c.name) ? 0 : 1;
692
+ return [...controllers].sort((a, b) => {
693
+ const ra = rank(a);
694
+ const rb = rank(b);
695
+ if (ra !== rb) return ra - rb;
696
+ return b.adapterRam - a.adapterRam;
697
+ });
698
+ }
699
+
700
+ export async function getWindowsGpu64BitVram(adapterName?: string): Promise<number> {
701
+ // Win32_VideoController clamps AdapterRAM to 4GB (UInt32).
702
+ // Read exact 64-bit VRAM from the Display Class registry keys. The value is literally named
703
+ // 'HardwareInformation.qwMemorySize' (dot is part of the name), so it must be quoted.
704
+ // Match by DriverDesc so hybrid systems report the VRAM of the selected adapter.
480
705
  try {
706
+ const safeName = (adapterName || '').replace(/'/g, "''");
481
707
  const raw = await runPowerShell(
482
- `Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor | ConvertTo-Csv -NoTypeInformation`,
708
+ `$name = '${safeName}'; $items = Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0*' -ErrorAction SilentlyContinue | Where-Object { $_.'HardwareInformation.qwMemorySize' -gt 0 }; $pick = $items | Where-Object { $name -and $_.DriverDesc -eq $name } | Select-Object -First 1; if (-not $pick) { $pick = $items | Sort-Object -Property 'HardwareInformation.qwMemorySize' -Descending | Select-Object -First 1 }; if ($pick) { [string][uint64]$pick.'HardwareInformation.qwMemorySize' }`,
483
709
  ''
484
710
  );
485
- const lines = raw.split('\n').filter(l => l.trim().length > 0);
486
- if (lines.length >= 2) {
487
- const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
488
- const values = lines[1].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
489
- const row: Record<string, string> = {};
490
- headers.forEach((h, i) => { row[h] = values[i] || ''; });
491
-
492
- const name = row.Name || row.VideoProcessor || 'Windows Display Adapter';
493
- const ram = safeParseInt(row.AdapterRAM);
494
- return {
495
- model: name,
496
- memory: ram,
497
- utilization: 0,
498
- processes: 0,
499
- };
500
- }
711
+ const bytes = safeParseFloat(raw.trim());
712
+ if (bytes > 0) return bytes;
713
+ } catch {}
714
+
715
+ return 0;
716
+ }
717
+
718
+ /**
719
+ * 3D engine utilization via CIM performance classes. Unlike Get-Counter paths these class/property
720
+ * names are not localized, so this works on non-English Windows. Works for NVIDIA, AMD and Intel (WDDM 2.0+).
721
+ */
722
+ export async function getWindowsGpu3dUtilization(): Promise<{ utilization: number; processCount: number }> {
723
+ try {
724
+ const raw = await runPowerShell(
725
+ `$samples = Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*engtype_3D*' }; if ($samples) { $activePids = $samples | Where-Object { $_.UtilizationPercentage -gt 0 } | ForEach-Object { if ($_.Name -match 'pid_(\\d+)') { $matches[1] } } | Select-Object -Unique; $total = ($samples | Measure-Object -Property UtilizationPercentage -Sum).Sum; "$total|$(@($activePids).Count)" } else { '0|0' }`,
726
+ '0|0'
727
+ );
728
+ const [utilStr, procCountStr] = raw.split('|');
729
+ const util = Math.min(100, Math.max(0, Math.round(safeParseFloat(utilStr))));
730
+ const processCount = safeParseInt(procCountStr, 0);
731
+ return { utilization: util, processCount };
501
732
  } catch {
502
- // ignore
733
+ return { utilization: 0, processCount: 0 };
503
734
  }
504
- return null;
505
735
  }
506
736
 
507
- async function getPerfCounterGpu(): Promise<GpuData | null> {
737
+ export async function collectWindowsGpu(): Promise<GpuData | null> {
738
+ // 1. NVIDIA via nvidia-smi (first-class support when available)
739
+ const nv = await getNvidiaGpu();
740
+ if (nv) return nv;
741
+
742
+ // 2. AMD / Intel / General Windows GPUs
508
743
  try {
509
- const raw = await runPowerShell(
510
- `$engines = Get-Counter '\GPU Engine(*)\*' -ErrorAction SilentlyContinue; if ($engines) { $engines.CounterSamples | Where-Object { $_.CounterName -match 'Utilization Percentage|GPU Time' -and $_.CookedValue -gt 0 } | ForEach-Object { "$($_.InstanceName)|$($_.CounterName)|$($_.CookedValue)" } }`,
744
+ const rawControllers = await runPowerShell(
745
+ `Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor, PNPDeviceID | ConvertTo-Csv -NoTypeInformation`,
511
746
  ''
512
- ).catch(() => '');
513
- if (!raw) return null;
747
+ );
748
+ const controllers = parseWindowsVideoControllers(rawControllers);
749
+ if (controllers.length === 0) return null;
514
750
 
515
- const lines = raw.split('\n').filter(l => l.trim());
516
- if (lines.length === 0) return null;
751
+ const prioritized = prioritizeGpuControllers(controllers);
752
+ const bestController = prioritized[0];
517
753
 
518
- const gpuGroups = new Map<string, { name: string; values: number[] }>();
519
- for (const line of lines) {
520
- const [instanceName, counterName, valueStr] = line.split('|');
521
- const baseName = instanceName.replace(/\s*\(\d+\)\s*$/, '').trim();
522
- if (!gpuGroups.has(baseName)) {
523
- gpuGroups.set(baseName, { name: baseName, values: [] });
524
- }
525
- gpuGroups.get(baseName)!.values.push(parseFloat(valueStr) || 0);
754
+ // Read 64-bit VRAM (avoids 4GB uint32 clamp on RX 7900, Arc A770, etc.)
755
+ let vramBytes = await getWindowsGpu64BitVram(bestController.name);
756
+ if (vramBytes === 0 && bestController.adapterRam > 0) {
757
+ vramBytes = bestController.adapterRam;
526
758
  }
527
759
 
528
- let bestGpu: { name: string; util: number } | null = null;
529
- for (const group of gpuGroups.values()) {
530
- const total = group.values.reduce((a, b) => a + b, 0);
531
- const util = Math.min(100, Math.round(total));
532
- if (!bestGpu || util > bestGpu.util) {
533
- bestGpu = { name: group.name, util };
534
- }
535
- }
760
+ // Read real 3D engine utilization and process count
761
+ const { utilization, processCount } = await getWindowsGpu3dUtilization();
536
762
 
537
- if (bestGpu) {
538
- return {
539
- model: bestGpu.name,
540
- memory: 0,
541
- utilization: bestGpu.util,
542
- processes: 0,
543
- };
544
- }
763
+ // Query temperature for AMD / Intel via LibreHardwareMonitor or Windows Thermal
764
+ let temperature: number | undefined;
765
+ try {
766
+ const rawTemp = await runPowerShell(
767
+ `Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
768
+ ''
769
+ );
770
+ const t = safeParseFloat(rawTemp);
771
+ if (t > 0 && t < 125) temperature = t;
772
+ } catch {}
773
+
774
+ return {
775
+ model: bestController.name,
776
+ memory: vramBytes,
777
+ utilization,
778
+ temperature,
779
+ processes: processCount,
780
+ };
545
781
  } catch {
546
- // ignore
782
+ return null;
547
783
  }
548
- return null;
549
784
  }
550
785
 
551
- export async function collectWindowsGpu(): Promise<GpuData | null> {
552
- const nv = await getNvidiaGpu();
553
- if (nv) return nv;
786
+ // ─── Network & Socket Monitoring (`pyre netusage`) ─────────────────────────────
554
787
 
555
- const perf = await getPerfCounterGpu();
556
- if (perf) return perf;
788
+ export interface WindowsTcpConnection {
789
+ localPort: number;
790
+ remoteAddress: string;
791
+ remotePort: number;
792
+ state: string;
793
+ owningPid: number;
794
+ }
557
795
 
558
- const wmi = await getWmiGpu();
559
- if (wmi) return wmi;
796
+ export function parseWindowsTcpConnections(csvRaw: string): WindowsTcpConnection[] {
797
+ const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
798
+ if (lines.length < 2) return [];
560
799
 
561
- return null;
800
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
801
+ const connections: WindowsTcpConnection[] = [];
802
+
803
+ for (const line of lines.slice(1)) {
804
+ const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
805
+ const row: Record<string, string> = {};
806
+ headers.forEach((h, i) => { row[h] = values[i] || ''; });
807
+
808
+ const localPort = safeParseInt(row.LocalPort, 0);
809
+ const remoteAddress = row.RemoteAddress || '';
810
+ const remotePort = safeParseInt(row.RemotePort, 0);
811
+ const state = row.State || '';
812
+ const owningPid = safeParseInt(row.OwningProcess, 0);
813
+
814
+ if (owningPid > 0) {
815
+ connections.push({ localPort, remoteAddress, remotePort, state, owningPid });
816
+ }
817
+ }
818
+
819
+ return connections;
562
820
  }
563
821
 
564
822
  export async function collectWindowsNetwork(): Promise<NetworkData> {
@@ -585,9 +843,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
585
843
  ''
586
844
  );
587
845
  if (raw) iface = raw;
588
- } catch {
589
- // ignore
590
- }
846
+ } catch {}
591
847
 
592
848
  try {
593
849
  const netstatE = await run('netstat -e 2>nul', '', 1500);
@@ -602,9 +858,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
602
858
  txPackets += parseInt(parts[2], 10) || 0;
603
859
  }
604
860
  }
605
- } catch {
606
- // ignore
607
- }
861
+ } catch {}
608
862
 
609
863
  const now = Date.now();
610
864
  let rxRate = 0;
@@ -618,6 +872,9 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
618
872
 
619
873
  const connectionStates = await getWindowsConnectionStates();
620
874
 
875
+ // Populate listening ports and remote hosts
876
+ const { listeningPorts, topRemoteHosts } = await getWindowsSocketDetails();
877
+
621
878
  return {
622
879
  interface: iface,
623
880
  ip,
@@ -633,42 +890,98 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
633
890
  udp: { connections: 0, rxBytes: 0, txBytes: 0 },
634
891
  },
635
892
  connectionStates,
636
- listeningPorts: [],
893
+ listeningPorts,
637
894
  establishedConnections: connectionStates.established,
638
- topRemoteHosts: [],
895
+ topRemoteHosts,
639
896
  };
640
897
  }
641
898
 
642
- async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
643
- const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
899
+ async function getWindowsSocketDetails(): Promise<{ listeningPorts: number[]; topRemoteHosts: RemoteHostInfo[] }> {
900
+ const listeningPorts: number[] = [];
901
+ const topRemoteHosts: RemoteHostInfo[] = [];
902
+
644
903
  try {
645
904
  const raw = await runPowerShell(
646
- `Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
647
- '0'
905
+ `$conns = Get-NetTCPConnection -ErrorAction SilentlyContinue; if ($conns) { $ports = ($conns | Where-Object { $_.State -eq 'Listen' } | Select-Object -ExpandProperty LocalPort -Unique) -join ','; $hosts = ($conns | Where-Object { $_.State -eq 'Established' -and $_.RemoteAddress -ne '127.0.0.1' -and $_.RemoteAddress -ne '0.0.0.0' } | Group-Object RemoteAddress | Sort-Object Count -Descending | Select-Object -First 10 | ForEach-Object { "$($_.Name):$($_.Count)" }) -join '|'; "$ports#$hosts" } else { '#' }`,
906
+ '#'
648
907
  );
649
- stats.established = safeParseInt(raw, 0);
908
+ const [portsPart, hostsPart] = raw.split('#');
909
+ if (portsPart) {
910
+ portsPart.split(',').forEach(p => {
911
+ const portNum = safeParseInt(p);
912
+ if (portNum > 0 && !listeningPorts.includes(portNum)) listeningPorts.push(portNum);
913
+ });
914
+ }
915
+ if (hostsPart) {
916
+ hostsPart.split('|').forEach(h => {
917
+ const [ip, countStr] = h.split(':');
918
+ if (ip) {
919
+ topRemoteHosts.push({
920
+ ip,
921
+ connections: safeParseInt(countStr, 1),
922
+ activePids: [],
923
+ });
924
+ }
925
+ });
926
+ }
927
+ } catch {}
650
928
 
651
- const listenRaw = await runPowerShell(
652
- `Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
653
- '0'
654
- );
655
- stats.listening = safeParseInt(listenRaw, 0);
929
+ return { listeningPorts, topRemoteHosts };
930
+ }
656
931
 
657
- const twRaw = await runPowerShell(
658
- `Get-NetTCPConnection -State TimeWait -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
659
- '0'
932
+ async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
933
+ const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
934
+ try {
935
+ const raw = await runPowerShell(
936
+ `$c = Get-NetTCPConnection -ErrorAction SilentlyContinue; if ($c) { $groups = $c | Group-Object State; ($groups | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join '|' } else { '' }`,
937
+ ''
660
938
  );
661
- stats.timeWait = safeParseInt(twRaw, 0);
939
+ for (const pair of raw.split('|')) {
940
+ const [state, countStr] = pair.split('=');
941
+ const count = safeParseInt(countStr, 0);
942
+ if (state === 'Established') stats.established = count;
943
+ else if (state === 'Listen') stats.listening = count;
944
+ else if (state === 'TimeWait') stats.timeWait = count;
945
+ else if (state === 'CloseWait') stats.closeWait = count;
946
+ else if (count > 0) stats.other += count;
947
+ }
948
+ } catch {}
949
+ return stats;
950
+ }
662
951
 
663
- const cwRaw = await runPowerShell(
664
- `Get-NetTCPConnection -State CloseWait -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
665
- '0'
666
- );
667
- stats.closeWait = safeParseInt(cwRaw, 0);
668
- } catch {
669
- // ignore
952
+ export function buildWindowsNetworkProcesses(
953
+ conns: WindowsTcpConnection[],
954
+ procMap: Map<number, string>
955
+ ): NetworkProcess[] {
956
+ const agg = new Map<number, NetworkProcess>();
957
+
958
+ for (const c of conns) {
959
+ const pid = c.owningPid;
960
+ const name = procMap.get(pid) || `pid-${pid}`;
961
+ let item = agg.get(pid);
962
+ if (!item) {
963
+ item = {
964
+ pid,
965
+ command: name,
966
+ rxBytes: 0,
967
+ txBytes: 0,
968
+ rxPackets: 0,
969
+ txPackets: 0,
970
+ connections: [],
971
+ };
972
+ agg.set(pid, item);
973
+ }
974
+ if (c.remoteAddress && c.remoteAddress !== '0.0.0.0') {
975
+ item.connections.push({
976
+ proto: 'tcp',
977
+ localAddress: `127.0.0.1:${c.localPort}`,
978
+ remoteAddress: `${c.remoteAddress}:${c.remotePort}`,
979
+ state: c.state,
980
+ });
981
+ }
670
982
  }
671
- return stats;
983
+
984
+ return Array.from(agg.values()).sort((a, b) => b.connections.length - a.connections.length);
672
985
  }
673
986
 
674
987
  export async function collectWindowsPackets(): Promise<PacketData | null> {
@@ -690,11 +1003,34 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
690
1003
  txPackets += parseInt(parts[2], 10) || 0;
691
1004
  }
692
1005
  }
693
- } catch {
694
- // ignore
695
- }
1006
+ } catch {}
696
1007
 
697
1008
  const connectionStates = await getWindowsConnectionStates();
1009
+ const { listeningPorts } = await getWindowsSocketDetails();
1010
+
1011
+ // Populate per-process network socket metrics for `pyre netusage` and packet panel
1012
+ let allProcesses: NetworkProcess[] = [];
1013
+ try {
1014
+ const rawConns = await runPowerShell(
1015
+ `Get-NetTCPConnection -State Established,Listen -ErrorAction SilentlyContinue | Select-Object LocalPort, RemoteAddress, RemotePort, State, OwningProcess | ConvertTo-Csv -NoTypeInformation`,
1016
+ ''
1017
+ );
1018
+ const conns = parseWindowsTcpConnections(rawConns);
1019
+
1020
+ // Build PID to process command name mapping
1021
+ const rawProcs = await runPowerShell(
1022
+ `Get-Process -ErrorAction SilentlyContinue | Select-Object Id, ProcessName | ForEach-Object { "$($_.Id)|$($_.ProcessName)" }`,
1023
+ ''
1024
+ );
1025
+ const procMap = new Map<number, string>();
1026
+ for (const line of rawProcs.split('\n')) {
1027
+ const [idStr, name] = line.trim().split('|');
1028
+ const pid = safeParseInt(idStr);
1029
+ if (pid > 0 && name) procMap.set(pid, name);
1030
+ }
1031
+
1032
+ allProcesses = buildWindowsNetworkProcesses(conns, procMap);
1033
+ } catch {}
698
1034
 
699
1035
  return {
700
1036
  totalPackets: rxPackets + txPackets,
@@ -703,73 +1039,91 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
703
1039
  rxRate: 0,
704
1040
  txRate: 0,
705
1041
  connections: connectionStates.established,
706
- topProcesses: [],
1042
+ topProcesses: allProcesses.slice(0, 10),
707
1043
  interfaces: [{ iface: 'Primary', rxPackets, txPackets, rxBytes, txBytes }],
708
- allProcesses: [],
1044
+ allProcesses,
709
1045
  connectionStates,
710
- listeningPorts: [],
1046
+ listeningPorts,
711
1047
  };
712
1048
  }
713
1049
 
1050
+ // ─── Process Collection with ParentProcessId (`ppid`) for Tree View ─────────────
1051
+
714
1052
  const winProcCache = new Map<number, { user: number; kernel: number; ts: number }>();
715
1053
 
1054
+ export function parseWindowsProcessCsv(
1055
+ csvRaw: string,
1056
+ totalMem: number,
1057
+ cache: Map<number, { user: number; kernel: number; ts: number }>,
1058
+ now: number
1059
+ ): ProcessData[] {
1060
+ const lines = csvRaw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
1061
+ const procs: ProcessData[] = [];
1062
+
1063
+ for (const line of lines) {
1064
+ const parts = line.split(',').map(s => s.trim().replace(/^"|"$/g, ''));
1065
+ if (parts.length < 6) continue;
1066
+
1067
+ const name = parts[0];
1068
+ const pid = safeParseInt(parts[1], 0);
1069
+ const ppid = safeParseInt(parts[2], 0);
1070
+ const threads = safeParseInt(parts[3], 0);
1071
+ const memBytes = safeParseInt(parts[4], 0);
1072
+ const userTime = safeParseInt(parts[5], 0);
1073
+ const kernelTime = safeParseInt(parts[6], 0);
1074
+ const commandLine = parts[7] || name;
1075
+
1076
+ if (!pid || pid <= 0) continue;
1077
+
1078
+ const totalTime = userTime + kernelTime;
1079
+ let cpu = 0;
1080
+
1081
+ const prev = cache.get(pid);
1082
+ if (prev) {
1083
+ const dt = now - prev.ts;
1084
+ if (dt > 0) {
1085
+ const timeDelta = totalTime - (prev.user + prev.kernel);
1086
+ if (timeDelta > 0) {
1087
+ const cpuSeconds = timeDelta / 10_000_000;
1088
+ cpu = Math.min(100, Math.round((cpuSeconds / dt) * 10000) / 100);
1089
+ }
1090
+ }
1091
+ }
1092
+ cache.set(pid, { user: userTime, kernel: kernelTime, ts: now });
1093
+
1094
+ const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 1000) / 10 : 0;
1095
+ const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python|nvidia|amd|radeon|directx|game/i.test(name) ||
1096
+ /python|node|ollama/i.test(commandLine);
1097
+
1098
+ procs.push({
1099
+ pid,
1100
+ ppid,
1101
+ user: 'SYSTEM',
1102
+ cpu,
1103
+ mem: memPct,
1104
+ state: 'R',
1105
+ threads,
1106
+ runtime: 0,
1107
+ command: name,
1108
+ isGpuOrMlAttributed: isGpuOrMl,
1109
+ });
1110
+ }
1111
+
1112
+ procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
1113
+ return procs;
1114
+ }
1115
+
716
1116
  export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
717
1117
  try {
1118
+ // Explicitly query ParentProcessId for process tree view (`pyre --tree`)
718
1119
  const raw = await runPowerShell(
719
- `Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime | ConvertTo-Csv -NoTypeInformation`,
1120
+ `Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ParentProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime, CommandLine | ConvertTo-Csv -NoTypeInformation`,
720
1121
  ''
721
1122
  );
722
- const lines = raw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
723
1123
  const now = Date.now();
724
- const procs: ProcessData[] = [];
725
1124
  const totalMem = os.totalmem();
1125
+ const procs = parseWindowsProcessCsv(raw, totalMem, winProcCache, now);
726
1126
 
727
- for (const line of lines) {
728
- const parts = line.split(',').map(s => s.trim().replace(/^"|"$/g, ''));
729
- if (parts.length < 6) continue;
730
- const name = parts[0];
731
- const pid = safeParseInt(parts[1], 0);
732
- const threads = safeParseInt(parts[2], 0);
733
- const memBytes = safeParseInt(parts[3], 0);
734
- const userTime = safeParseInt(parts[4], 0);
735
- const kernelTime = safeParseInt(parts[5], 0);
736
-
737
- if (!pid || pid <= 0) continue;
738
-
739
- const totalTime = userTime + kernelTime;
740
- let cpu = 0;
741
-
742
- const prev = winProcCache.get(pid);
743
- if (prev) {
744
- const dt = now - prev.ts;
745
- if (dt > 0) {
746
- const timeDelta = totalTime - (prev.user + prev.kernel);
747
- if (timeDelta > 0) {
748
- const cpuSeconds = timeDelta / 10_000_000;
749
- cpu = Math.min(100, Math.round((cpuSeconds / dt) * 10000) / 100);
750
- }
751
- }
752
- }
753
- winProcCache.set(pid, { user: userTime, kernel: kernelTime, ts: now });
754
-
755
- const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 1000) / 10 : 0;
756
- const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python|nvidia|amd|radeon|intel.*graphics/i.test(name);
757
-
758
- procs.push({
759
- pid,
760
- ppid: 0,
761
- user: 'SYSTEM',
762
- cpu,
763
- mem: memPct,
764
- state: 'R',
765
- threads,
766
- runtime: 0,
767
- command: name,
768
- isGpuOrMlAttributed: isGpuOrMl,
769
- });
770
- }
771
-
772
- procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
773
1127
  if (limit && limit > 0) return procs.slice(0, limit);
774
1128
  return procs;
775
1129
  } catch {
@@ -790,6 +1144,8 @@ export async function collectWindowsTasks(limit = 12): Promise<TaskData[]> {
790
1144
  }));
791
1145
  }
792
1146
 
1147
+ // ─── Displays ──────────────────────────────────────────────────────────────────
1148
+
793
1149
  export async function getWindowsDisplayInfo(): Promise<DisplayInfo[]> {
794
1150
  try {
795
1151
  const raw = await runPowerShell(