pyre-cli 10.3.0 → 10.4.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 +613 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +135 -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];
211
+ const loadAvg = updateEmaLoadAvg(usage, cores);
137
212
 
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
- }
152
-
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,240 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
448
560
  return null;
449
561
  }
450
562
 
451
- async function getNvidiaGpu(): Promise<GpuData | null> {
563
+ // ─── GPU Telemetry Across Intel, AMD, NVIDIA ─────────────────────────────────
564
+
565
+ async function findNvidiaSmiPath(): Promise<string> {
566
+ const commonPaths = [
567
+ 'nvidia-smi',
568
+ path.join(process.env.ProgramFiles || 'C:\\Program Files', 'NVIDIA Corporation', 'NVSMI', 'nvidia-smi.exe'),
569
+ path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'nvidia-smi.exe'),
570
+ ];
571
+ for (const p of commonPaths) {
572
+ try {
573
+ const test = await run(`"${p}" -L 2>nul`, '');
574
+ if (test.includes('GPU 0:')) return p;
575
+ } catch {}
576
+ }
577
+ return 'nvidia-smi';
578
+ }
579
+
580
+ async function runNvidiaSmiQuery(query: string): Promise<string> {
581
+ const smiPath = await findNvidiaSmiPath();
582
+ return (await run(`"${smiPath}" --query-gpu=${query} --format=csv,noheader,nounits 2>nul`, '')).trim();
583
+ }
584
+
585
+ export function parseNvidiaSmiOutput(csv: string): GpuData | null {
586
+ const lines = csv.split('\n').filter(l => l.trim().length > 0);
587
+ if (lines.length === 0) return null;
588
+ const parts = lines[0].split(',').map(s => s.trim());
589
+ if (parts.length < 3) return null;
590
+
591
+ const model = parts[0];
592
+ const memory = (safeParseFloat(parts[1])) * 1024 * 1024;
593
+ const utilization = Math.min(100, Math.max(0, Math.round(safeParseFloat(parts[2]))));
594
+ const temperature = parts[3] ? safeParseFloat(parts[3]) : undefined;
595
+ const powerDraw = parts[4] ? safeParseFloat(parts[4]) : undefined;
596
+
597
+ return {
598
+ model,
599
+ memory,
600
+ utilization,
601
+ temperature: Number.isFinite(temperature) && (temperature as number) > 0 ? temperature : undefined,
602
+ processes: 0,
603
+ powerDraw: Number.isFinite(powerDraw) && (powerDraw as number) > 0 ? powerDraw : undefined,
604
+ };
605
+ }
606
+
607
+ export async function getNvidiaGpu(): Promise<GpuData | null> {
452
608
  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
- };
471
- }
609
+ const raw = await runNvidiaSmiQuery('gpu_name,memory.total,utilization.gpu,temperature.gpu,power.draw');
610
+ const parsed = parseNvidiaSmiOutput(raw);
611
+ if (parsed) {
612
+ // Query active compute / graphics process count
613
+ try {
614
+ const smiPath = await findNvidiaSmiPath();
615
+ const procList = await run(`"${smiPath}" --query-compute-apps=pid --format=csv,noheader 2>nul`, '');
616
+ const pids = procList.split('\n').filter(l => l.trim().length > 0);
617
+ parsed.processes = pids.length;
618
+ } catch {}
619
+ return parsed;
472
620
  }
473
- } catch {
474
- // ignore
475
- }
621
+ } catch {}
476
622
  return null;
477
623
  }
478
624
 
479
- async function getWmiGpu(): Promise<GpuData | null> {
625
+ export interface WindowsVideoControllerInfo {
626
+ name: string;
627
+ adapterRam: number;
628
+ driverVersion: string;
629
+ videoProcessor: string;
630
+ pnpDeviceId: string;
631
+ }
632
+
633
+ export function parseWindowsVideoControllers(csvRaw: string): WindowsVideoControllerInfo[] {
634
+ const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
635
+ if (lines.length < 2) return [];
636
+
637
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
638
+ const controllers: WindowsVideoControllerInfo[] = [];
639
+
640
+ for (const line of lines.slice(1)) {
641
+ const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
642
+ const row: Record<string, string> = {};
643
+ headers.forEach((h, i) => { row[h] = values[i] || ''; });
644
+
645
+ const name = row.Name || row.VideoProcessor || 'Windows Display Adapter';
646
+ const ram = safeParseInt(row.AdapterRAM, 0);
647
+ controllers.push({
648
+ name,
649
+ adapterRam: ram,
650
+ driverVersion: row.DriverVersion || '',
651
+ videoProcessor: row.VideoProcessor || '',
652
+ pnpDeviceId: row.PNPDeviceID || '',
653
+ });
654
+ }
655
+
656
+ return controllers;
657
+ }
658
+
659
+ export function prioritizeGpuControllers(controllers: WindowsVideoControllerInfo[]): WindowsVideoControllerInfo[] {
660
+ // Sort discrete cards (NVIDIA, AMD Radeon RX, Intel Arc) before integrated adapters (Intel UHD, AMD Radeon(TM) Graphics)
661
+ return [...controllers].sort((a, b) => {
662
+ const aName = a.name.toLowerCase();
663
+ const bName = b.name.toLowerCase();
664
+ const aIsDiscrete = aName.includes('rtx') || aName.includes('gtx') || aName.includes('geforce') ||
665
+ aName.includes('radeon rx') || aName.includes('arc');
666
+ const bIsDiscrete = bName.includes('rtx') || bName.includes('gtx') || bName.includes('geforce') ||
667
+ bName.includes('radeon rx') || bName.includes('arc');
668
+ if (aIsDiscrete && !bIsDiscrete) return -1;
669
+ if (!aIsDiscrete && bIsDiscrete) return 1;
670
+ return b.adapterRam - a.adapterRam;
671
+ });
672
+ }
673
+
674
+ export async function getWindowsGpu64BitVram(): Promise<number> {
675
+ // Win32_VideoController clamps AdapterRAM to 4GB (UInt32).
676
+ // Read exact 64-bit VRAM from Registry Display Class hardware information:
480
677
  try {
481
678
  const raw = await runPowerShell(
482
- `Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor | ConvertTo-Csv -NoTypeInformation`,
679
+ `Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0*' -ErrorAction SilentlyContinue | Where-Object { $_.HardwareInformation.qwMemorySize -gt 0 } | Select-Object -First 1 -ExpandProperty HardwareInformation.qwMemorySize`,
483
680
  ''
484
681
  );
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
- }
682
+ const bytes = safeParseInt(raw, 0);
683
+ if (bytes > 0) return bytes;
684
+ } catch {}
685
+
686
+ // Fallback to Performance Counters Dedicated Usage + Free
687
+ try {
688
+ const rawDedicated = await runPowerShell(
689
+ `(Get-Counter '\\GPU Adapter Memory(*)\\Dedicated Usage' -ErrorAction SilentlyContinue).CounterSamples | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum`,
690
+ ''
691
+ );
692
+ const bytes = safeParseFloat(rawDedicated);
693
+ if (bytes > 0) return bytes;
694
+ } catch {}
695
+
696
+ return 0;
697
+ }
698
+
699
+ export async function getWindowsGpu3dUtilization(): Promise<{ utilization: number; processCount: number }> {
700
+ try {
701
+ const raw = await runPowerShell(
702
+ `$samples = (Get-Counter '\\GPU Engine(*engtype_3D*)\\Utilization Percentage' -ErrorAction SilentlyContinue).CounterSamples; if ($samples) { $activePids = $samples | Where-Object { $_.CookedValue -gt 0 } | ForEach-Object { if ($_.InstanceName -match 'pid_(\\d+)') { $matches[1] } } | Select-Object -Unique; $total = ($samples | Measure-Object -Property CookedValue -Sum).Sum; "$total|$($activePids.Count)" } else { '0|0' }`,
703
+ '0|0'
704
+ );
705
+ const [utilStr, procCountStr] = raw.split('|');
706
+ const util = Math.min(100, Math.max(0, Math.round(safeParseFloat(utilStr))));
707
+ const processCount = safeParseInt(procCountStr, 0);
708
+ return { utilization: util, processCount };
501
709
  } catch {
502
- // ignore
710
+ return { utilization: 0, processCount: 0 };
503
711
  }
504
- return null;
505
712
  }
506
713
 
507
- async function getPerfCounterGpu(): Promise<GpuData | null> {
714
+ export async function collectWindowsGpu(): Promise<GpuData | null> {
715
+ // 1. NVIDIA via nvidia-smi (first-class support when available)
716
+ const nv = await getNvidiaGpu();
717
+ if (nv) return nv;
718
+
719
+ // 2. AMD / Intel / General Windows GPUs
508
720
  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)" } }`,
721
+ const rawControllers = await runPowerShell(
722
+ `Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor, PNPDeviceID | ConvertTo-Csv -NoTypeInformation`,
511
723
  ''
512
- ).catch(() => '');
513
- if (!raw) return null;
724
+ );
725
+ const controllers = parseWindowsVideoControllers(rawControllers);
726
+ if (controllers.length === 0) return null;
514
727
 
515
- const lines = raw.split('\n').filter(l => l.trim());
516
- if (lines.length === 0) return null;
728
+ const prioritized = prioritizeGpuControllers(controllers);
729
+ const bestController = prioritized[0];
517
730
 
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);
731
+ // Read 64-bit VRAM (avoids 4GB uint32 clamp on RX 7900, Arc A770, etc.)
732
+ let vramBytes = await getWindowsGpu64BitVram();
733
+ if (vramBytes === 0 && bestController.adapterRam > 0) {
734
+ vramBytes = bestController.adapterRam;
526
735
  }
527
736
 
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
- }
737
+ // Read real 3D engine utilization and process count
738
+ const { utilization, processCount } = await getWindowsGpu3dUtilization();
536
739
 
537
- if (bestGpu) {
538
- return {
539
- model: bestGpu.name,
540
- memory: 0,
541
- utilization: bestGpu.util,
542
- processes: 0,
543
- };
544
- }
740
+ // Query temperature for AMD / Intel via LibreHardwareMonitor or Windows Thermal
741
+ let temperature: number | undefined;
742
+ try {
743
+ const rawTemp = await runPowerShell(
744
+ `Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
745
+ ''
746
+ );
747
+ const t = safeParseFloat(rawTemp);
748
+ if (t > 0 && t < 125) temperature = t;
749
+ } catch {}
750
+
751
+ return {
752
+ model: bestController.name,
753
+ memory: vramBytes,
754
+ utilization,
755
+ temperature,
756
+ processes: processCount,
757
+ };
545
758
  } catch {
546
- // ignore
759
+ return null;
547
760
  }
548
- return null;
549
761
  }
550
762
 
551
- export async function collectWindowsGpu(): Promise<GpuData | null> {
552
- const nv = await getNvidiaGpu();
553
- if (nv) return nv;
763
+ // ─── Network & Socket Monitoring (`pyre netusage`) ─────────────────────────────
554
764
 
555
- const perf = await getPerfCounterGpu();
556
- if (perf) return perf;
765
+ export interface WindowsTcpConnection {
766
+ localPort: number;
767
+ remoteAddress: string;
768
+ remotePort: number;
769
+ state: string;
770
+ owningPid: number;
771
+ }
557
772
 
558
- const wmi = await getWmiGpu();
559
- if (wmi) return wmi;
773
+ export function parseWindowsTcpConnections(csvRaw: string): WindowsTcpConnection[] {
774
+ const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
775
+ if (lines.length < 2) return [];
560
776
 
561
- return null;
777
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
778
+ const connections: WindowsTcpConnection[] = [];
779
+
780
+ for (const line of lines.slice(1)) {
781
+ const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
782
+ const row: Record<string, string> = {};
783
+ headers.forEach((h, i) => { row[h] = values[i] || ''; });
784
+
785
+ const localPort = safeParseInt(row.LocalPort, 0);
786
+ const remoteAddress = row.RemoteAddress || '';
787
+ const remotePort = safeParseInt(row.RemotePort, 0);
788
+ const state = row.State || '';
789
+ const owningPid = safeParseInt(row.OwningProcess, 0);
790
+
791
+ if (owningPid > 0) {
792
+ connections.push({ localPort, remoteAddress, remotePort, state, owningPid });
793
+ }
794
+ }
795
+
796
+ return connections;
562
797
  }
563
798
 
564
799
  export async function collectWindowsNetwork(): Promise<NetworkData> {
@@ -585,9 +820,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
585
820
  ''
586
821
  );
587
822
  if (raw) iface = raw;
588
- } catch {
589
- // ignore
590
- }
823
+ } catch {}
591
824
 
592
825
  try {
593
826
  const netstatE = await run('netstat -e 2>nul', '', 1500);
@@ -602,9 +835,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
602
835
  txPackets += parseInt(parts[2], 10) || 0;
603
836
  }
604
837
  }
605
- } catch {
606
- // ignore
607
- }
838
+ } catch {}
608
839
 
609
840
  const now = Date.now();
610
841
  let rxRate = 0;
@@ -618,6 +849,9 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
618
849
 
619
850
  const connectionStates = await getWindowsConnectionStates();
620
851
 
852
+ // Populate listening ports and remote hosts
853
+ const { listeningPorts, topRemoteHosts } = await getWindowsSocketDetails();
854
+
621
855
  return {
622
856
  interface: iface,
623
857
  ip,
@@ -633,42 +867,98 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
633
867
  udp: { connections: 0, rxBytes: 0, txBytes: 0 },
634
868
  },
635
869
  connectionStates,
636
- listeningPorts: [],
870
+ listeningPorts,
637
871
  establishedConnections: connectionStates.established,
638
- topRemoteHosts: [],
872
+ topRemoteHosts,
639
873
  };
640
874
  }
641
875
 
642
- async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
643
- const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
876
+ async function getWindowsSocketDetails(): Promise<{ listeningPorts: number[]; topRemoteHosts: RemoteHostInfo[] }> {
877
+ const listeningPorts: number[] = [];
878
+ const topRemoteHosts: RemoteHostInfo[] = [];
879
+
644
880
  try {
645
881
  const raw = await runPowerShell(
646
- `Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
647
- '0'
882
+ `$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 { '#' }`,
883
+ '#'
648
884
  );
649
- stats.established = safeParseInt(raw, 0);
885
+ const [portsPart, hostsPart] = raw.split('#');
886
+ if (portsPart) {
887
+ portsPart.split(',').forEach(p => {
888
+ const portNum = safeParseInt(p);
889
+ if (portNum > 0 && !listeningPorts.includes(portNum)) listeningPorts.push(portNum);
890
+ });
891
+ }
892
+ if (hostsPart) {
893
+ hostsPart.split('|').forEach(h => {
894
+ const [ip, countStr] = h.split(':');
895
+ if (ip) {
896
+ topRemoteHosts.push({
897
+ ip,
898
+ connections: safeParseInt(countStr, 1),
899
+ activePids: [],
900
+ });
901
+ }
902
+ });
903
+ }
904
+ } catch {}
650
905
 
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);
906
+ return { listeningPorts, topRemoteHosts };
907
+ }
656
908
 
657
- const twRaw = await runPowerShell(
658
- `Get-NetTCPConnection -State TimeWait -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
659
- '0'
909
+ async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
910
+ const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
911
+ try {
912
+ const raw = await runPowerShell(
913
+ `$c = Get-NetTCPConnection -ErrorAction SilentlyContinue; if ($c) { $groups = $c | Group-Object State; ($groups | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join '|' } else { '' }`,
914
+ ''
660
915
  );
661
- stats.timeWait = safeParseInt(twRaw, 0);
916
+ for (const pair of raw.split('|')) {
917
+ const [state, countStr] = pair.split('=');
918
+ const count = safeParseInt(countStr, 0);
919
+ if (state === 'Established') stats.established = count;
920
+ else if (state === 'Listen') stats.listening = count;
921
+ else if (state === 'TimeWait') stats.timeWait = count;
922
+ else if (state === 'CloseWait') stats.closeWait = count;
923
+ else if (count > 0) stats.other += count;
924
+ }
925
+ } catch {}
926
+ return stats;
927
+ }
662
928
 
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
929
+ export function buildWindowsNetworkProcesses(
930
+ conns: WindowsTcpConnection[],
931
+ procMap: Map<number, string>
932
+ ): NetworkProcess[] {
933
+ const agg = new Map<number, NetworkProcess>();
934
+
935
+ for (const c of conns) {
936
+ const pid = c.owningPid;
937
+ const name = procMap.get(pid) || `pid-${pid}`;
938
+ let item = agg.get(pid);
939
+ if (!item) {
940
+ item = {
941
+ pid,
942
+ command: name,
943
+ rxBytes: 0,
944
+ txBytes: 0,
945
+ rxPackets: 0,
946
+ txPackets: 0,
947
+ connections: [],
948
+ };
949
+ agg.set(pid, item);
950
+ }
951
+ if (c.remoteAddress && c.remoteAddress !== '0.0.0.0') {
952
+ item.connections.push({
953
+ proto: 'tcp',
954
+ localAddress: `127.0.0.1:${c.localPort}`,
955
+ remoteAddress: `${c.remoteAddress}:${c.remotePort}`,
956
+ state: c.state,
957
+ });
958
+ }
670
959
  }
671
- return stats;
960
+
961
+ return Array.from(agg.values()).sort((a, b) => b.connections.length - a.connections.length);
672
962
  }
673
963
 
674
964
  export async function collectWindowsPackets(): Promise<PacketData | null> {
@@ -690,11 +980,34 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
690
980
  txPackets += parseInt(parts[2], 10) || 0;
691
981
  }
692
982
  }
693
- } catch {
694
- // ignore
695
- }
983
+ } catch {}
696
984
 
697
985
  const connectionStates = await getWindowsConnectionStates();
986
+ const { listeningPorts } = await getWindowsSocketDetails();
987
+
988
+ // Populate per-process network socket metrics for `pyre netusage` and packet panel
989
+ let allProcesses: NetworkProcess[] = [];
990
+ try {
991
+ const rawConns = await runPowerShell(
992
+ `Get-NetTCPConnection -State Established,Listen -ErrorAction SilentlyContinue | Select-Object LocalPort, RemoteAddress, RemotePort, State, OwningProcess | ConvertTo-Csv -NoTypeInformation`,
993
+ ''
994
+ );
995
+ const conns = parseWindowsTcpConnections(rawConns);
996
+
997
+ // Build PID to process command name mapping
998
+ const rawProcs = await runPowerShell(
999
+ `Get-Process -ErrorAction SilentlyContinue | Select-Object Id, ProcessName | ForEach-Object { "$($_.Id)|$($_.ProcessName)" }`,
1000
+ ''
1001
+ );
1002
+ const procMap = new Map<number, string>();
1003
+ for (const line of rawProcs.split('\n')) {
1004
+ const [idStr, name] = line.trim().split('|');
1005
+ const pid = safeParseInt(idStr);
1006
+ if (pid > 0 && name) procMap.set(pid, name);
1007
+ }
1008
+
1009
+ allProcesses = buildWindowsNetworkProcesses(conns, procMap);
1010
+ } catch {}
698
1011
 
699
1012
  return {
700
1013
  totalPackets: rxPackets + txPackets,
@@ -703,73 +1016,91 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
703
1016
  rxRate: 0,
704
1017
  txRate: 0,
705
1018
  connections: connectionStates.established,
706
- topProcesses: [],
1019
+ topProcesses: allProcesses.slice(0, 10),
707
1020
  interfaces: [{ iface: 'Primary', rxPackets, txPackets, rxBytes, txBytes }],
708
- allProcesses: [],
1021
+ allProcesses,
709
1022
  connectionStates,
710
- listeningPorts: [],
1023
+ listeningPorts,
711
1024
  };
712
1025
  }
713
1026
 
1027
+ // ─── Process Collection with ParentProcessId (`ppid`) for Tree View ─────────────
1028
+
714
1029
  const winProcCache = new Map<number, { user: number; kernel: number; ts: number }>();
715
1030
 
1031
+ export function parseWindowsProcessCsv(
1032
+ csvRaw: string,
1033
+ totalMem: number,
1034
+ cache: Map<number, { user: number; kernel: number; ts: number }>,
1035
+ now: number
1036
+ ): ProcessData[] {
1037
+ const lines = csvRaw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
1038
+ const procs: ProcessData[] = [];
1039
+
1040
+ for (const line of lines) {
1041
+ const parts = line.split(',').map(s => s.trim().replace(/^"|"$/g, ''));
1042
+ if (parts.length < 6) continue;
1043
+
1044
+ const name = parts[0];
1045
+ const pid = safeParseInt(parts[1], 0);
1046
+ const ppid = safeParseInt(parts[2], 0);
1047
+ const threads = safeParseInt(parts[3], 0);
1048
+ const memBytes = safeParseInt(parts[4], 0);
1049
+ const userTime = safeParseInt(parts[5], 0);
1050
+ const kernelTime = safeParseInt(parts[6], 0);
1051
+ const commandLine = parts[7] || name;
1052
+
1053
+ if (!pid || pid <= 0) continue;
1054
+
1055
+ const totalTime = userTime + kernelTime;
1056
+ let cpu = 0;
1057
+
1058
+ const prev = cache.get(pid);
1059
+ if (prev) {
1060
+ const dt = now - prev.ts;
1061
+ if (dt > 0) {
1062
+ const timeDelta = totalTime - (prev.user + prev.kernel);
1063
+ if (timeDelta > 0) {
1064
+ const cpuSeconds = timeDelta / 10_000_000;
1065
+ cpu = Math.min(100, Math.round((cpuSeconds / dt) * 10000) / 100);
1066
+ }
1067
+ }
1068
+ }
1069
+ cache.set(pid, { user: userTime, kernel: kernelTime, ts: now });
1070
+
1071
+ const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 1000) / 10 : 0;
1072
+ const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python|nvidia|amd|radeon|directx|game/i.test(name) ||
1073
+ /python|node|ollama/i.test(commandLine);
1074
+
1075
+ procs.push({
1076
+ pid,
1077
+ ppid,
1078
+ user: 'SYSTEM',
1079
+ cpu,
1080
+ mem: memPct,
1081
+ state: 'R',
1082
+ threads,
1083
+ runtime: 0,
1084
+ command: name,
1085
+ isGpuOrMlAttributed: isGpuOrMl,
1086
+ });
1087
+ }
1088
+
1089
+ procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
1090
+ return procs;
1091
+ }
1092
+
716
1093
  export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
717
1094
  try {
1095
+ // Explicitly query ParentProcessId for process tree view (`pyre --tree`)
718
1096
  const raw = await runPowerShell(
719
- `Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime | ConvertTo-Csv -NoTypeInformation`,
1097
+ `Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ParentProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime, CommandLine | ConvertTo-Csv -NoTypeInformation`,
720
1098
  ''
721
1099
  );
722
- const lines = raw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
723
1100
  const now = Date.now();
724
- const procs: ProcessData[] = [];
725
1101
  const totalMem = os.totalmem();
1102
+ const procs = parseWindowsProcessCsv(raw, totalMem, winProcCache, now);
726
1103
 
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
1104
  if (limit && limit > 0) return procs.slice(0, limit);
774
1105
  return procs;
775
1106
  } catch {
@@ -790,6 +1121,8 @@ export async function collectWindowsTasks(limit = 12): Promise<TaskData[]> {
790
1121
  }));
791
1122
  }
792
1123
 
1124
+ // ─── Displays ──────────────────────────────────────────────────────────────────
1125
+
793
1126
  export async function getWindowsDisplayInfo(): Promise<DisplayInfo[]> {
794
1127
  try {
795
1128
  const raw = await runPowerShell(