@remcp/runtime 0.2.220 → 0.2.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.220",
3
+ "version": "0.2.222",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,17 @@ export function cpuPerCoreUtilizationRatios(before, after) {
34
34
  return ratios.every(value => value !== null) ? ratios : null;
35
35
  }
36
36
 
37
+ // Windows diagnostics spawn PowerShell probes. Measure host CPU first, or the
38
+ // diagnostics themselves can appear as a false 100% CPU spike on small VMs.
39
+ export async function scheduleCpuSampleAndProbes(platform, sampleCpu, startProbes) {
40
+ if (platform === 'win32') {
41
+ const cpus = await sampleCpu();
42
+ return { cpus, probes: startProbes() };
43
+ }
44
+ const probes = startProbes();
45
+ return { cpus: await sampleCpu(), probes };
46
+ }
47
+
37
48
  export function cpuFrequencyMHz(cores) {
38
49
  if (!Array.isArray(cores) || !cores.length) return null;
39
50
  const speeds = cores.map(core => core?.speed).filter(speed => Number.isFinite(speed) && speed > 0 && speed <= 100000);
@@ -75,6 +75,25 @@ export function parseWindowsNetworkCounters(input, activeNames = null) {
75
75
  return [...found.values()];
76
76
  }
77
77
 
78
+ // netsh exposes cumulative interface bytes even on Windows hosts where the
79
+ // NetAdapterStatistics PowerShell provider returns no entries. Match columns
80
+ // rather than localized header words; reject unsafe aliases and fake rates.
81
+ export function parseWindowsNetshCounters(input, activeNames = null) {
82
+ const found = new Map();
83
+ for (const line of String(input).split(/\r?\n/).slice(0, 130)) {
84
+ const match = line.match(/^\s*\d{1,10}\s+1\s+(\d{1,16})\s+(\d{1,16})\s+(.+?)\s*$/u);
85
+ if (!match) continue;
86
+ const name = match[3];
87
+ if (!/^[\p{L}\p{N} _().:#-]{1,64}$/u.test(name) ||
88
+ (activeNames && !activeNames.has(name))) continue;
89
+ const rx = Number(match[1]), tx = Number(match[2]);
90
+ if (!numeric(rx) || !numeric(tx)) continue;
91
+ found.set(name, { name, rx, tx });
92
+ if (found.size >= 24) break;
93
+ }
94
+ return [...found.values()];
95
+ }
96
+
78
97
  export function calculateNetworkRates(current, previous, elapsedSeconds) {
79
98
  if (!Array.isArray(current) || !Array.isArray(previous) || !Number.isFinite(elapsedSeconds) ||
80
99
  elapsedSeconds < 1 || elapsedSeconds > 300) return null;
@@ -114,13 +133,21 @@ async function readNetworkCounters() {
114
133
  return parseDarwinNetworkCounters(stdout, active);
115
134
  }
116
135
  if (process.platform === 'win32') {
117
- const script = 'Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress';
118
- const { stdout } = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script],
119
- { windowsHide: true, timeout: 5000, maxBuffer: 128 * 1024 });
120
136
  const active = new Set(Object.entries(os.networkInterfaces())
121
137
  .filter(([, addresses]) => addresses?.some(address => !address.internal))
122
138
  .map(([name]) => name));
123
- return parseWindowsNetworkCounters(stdout, active);
139
+ try {
140
+ const { stdout } = await run('netsh', ['interface', 'ipv4', 'show', 'subinterfaces'],
141
+ { windowsHide: true, timeout: 2500, maxBuffer: 128 * 1024 });
142
+ const counters = parseWindowsNetshCounters(stdout, active);
143
+ if (counters.length) return counters;
144
+ } catch { /* Fall through to the existing adapter provider. */ }
145
+ try {
146
+ const script = 'Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress';
147
+ const { stdout } = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script],
148
+ { windowsHide: true, timeout: 5000, maxBuffer: 128 * 1024 });
149
+ return parseWindowsNetworkCounters(stdout, active);
150
+ } catch { return []; }
124
151
  }
125
152
  return [];
126
153
  }
@@ -6,7 +6,7 @@ import { statfs } from 'node:fs/promises';
6
6
  import { promisify } from 'node:util';
7
7
  import { runtimeConfig } from '../config.mjs';
8
8
  import { clampInteger, fail, requireInteger, text } from '../util.mjs';
9
- import { cpuUtilizationRatio, cpuFrequencyMHz, cpuPerCoreUtilizationRatios } from './cpu-telemetry.mjs';
9
+ import { cpuUtilizationRatio, cpuFrequencyMHz, cpuPerCoreUtilizationRatios, scheduleCpuSampleAndProbes } from './cpu-telemetry.mjs';
10
10
  import { diskVolumes, networkRates, thermalReadings } from './system-telemetry.mjs';
11
11
  import { readDarwinMemoryPressure } from './darwin-memory-pressure.mjs';
12
12
 
@@ -63,14 +63,21 @@ export async function getSystemInfoTool() {
63
63
  const totalMem = os.totalmem();
64
64
  const freeMem = os.freemem();
65
65
  const load = os.loadavg();
66
- // Sample for 140 ms while retrieving disk facts, so CPU % is actual host activity.
67
- // Load average is a queue-length metric and must never be displayed as a percentage.
68
- const diskPromise = diskUsage();
69
- const volumesPromise = diskVolumes();
70
- const thermalPromise = thermalReadings();
71
- const networkPromise = networkRates();
72
- const memoryPressurePromise = process.platform === 'darwin' ? readDarwinMemoryPressure() : Promise.resolve(null);
73
- const sampledCpus = await new Promise(resolve => setTimeout(() => resolve(os.cpus()), 140));
66
+ // On Windows, the PowerShell sensor probes can saturate short CPU samples.
67
+ // Measure CPU before launching them; keep other platforms' parallel probes.
68
+ // Load average is queue length, never a percentage.
69
+ const { cpus: sampledCpus, probes } = await scheduleCpuSampleAndProbes(
70
+ process.platform,
71
+ () => new Promise(resolve => setTimeout(() => resolve(os.cpus()), 140)),
72
+ () => ({
73
+ diskPromise: diskUsage(),
74
+ volumesPromise: diskVolumes(),
75
+ thermalPromise: thermalReadings(),
76
+ networkPromise: networkRates(),
77
+ memoryPressurePromise: process.platform === 'darwin'
78
+ ? readDarwinMemoryPressure() : Promise.resolve(null),
79
+ }),
80
+ );
74
81
  const utilizationRatio = cpuUtilizationRatio(cpus, sampledCpus);
75
82
  const frequencyMHz = cpuFrequencyMHz(sampledCpus);
76
83
  return text(JSON.stringify({
@@ -95,12 +102,12 @@ export async function getSystemInfoTool() {
95
102
  freeBytes: freeMem,
96
103
  usedRatio: totalMem ? Number(((totalMem - freeMem) / totalMem).toFixed(3)) : 0,
97
104
  usedMethod: 'os.freemem-physical-occupancy',
98
- ...(await memoryPressurePromise),
105
+ ...(await probes.memoryPressurePromise),
99
106
  },
100
- disk: await diskPromise,
101
- volumes: await volumesPromise,
102
- temperatures: await thermalPromise,
103
- network: await networkPromise,
107
+ disk: await probes.diskPromise,
108
+ volumes: await probes.volumesPromise,
109
+ temperatures: await probes.thermalPromise,
110
+ network: await probes.networkPromise,
104
111
  home: os.homedir(),
105
112
  tempDir: os.tmpdir(),
106
113
  runtimeName: runtimeConfig.name,