@remcp/runtime 0.2.201 → 0.2.203

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.201",
3
+ "version": "0.2.203",
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",
@@ -3,7 +3,9 @@
3
3
  import { readFile, readdir } from 'node:fs/promises';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
+ import os from 'node:os';
6
7
  import process from 'node:process';
8
+ import { setTimeout as delay } from 'node:timers/promises';
7
9
  import { darwinTemperatureReadings } from './darwin-temperature.mjs';
8
10
  import { linuxTemperatureReadings } from './linux-temperature.mjs';
9
11
  import { windowsTemperatureReadings } from './windows-temperature.mjs';
@@ -27,6 +29,52 @@ export function parseNetworkCounters(input) {
27
29
  return interfaces;
28
30
  }
29
31
 
32
+ // netstat -ib has multiple address rows per interface on macOS. Only the
33
+ // <Link#N> row contains the canonical byte counters; never sum duplicates.
34
+ export function parseDarwinNetworkCounters(input, activeNames = null) {
35
+ const lines = String(input).split(/\r?\n/);
36
+ const header = lines[0]?.trim().split(/\s+/) ?? [];
37
+ const rxIndex = header.indexOf('Ibytes');
38
+ const txIndex = header.indexOf('Obytes');
39
+ if (rxIndex < 0 || txIndex < 0) return [];
40
+ const found = new Map();
41
+ for (const line of lines.slice(1, 160)) {
42
+ const parts = line.trim().split(/\s+/);
43
+ const name = parts[0];
44
+ if (!safeName(name) || !/^<Link#\d+>$/.test(parts[2]) ||
45
+ /^(?:lo\d*|awdl\d*|llw\d*|utun\d*|bridge\d*|gif\d*|stf\d*|ap\d*|anpi\d*)$/.test(name) ||
46
+ (activeNames && !activeNames.has(name))) continue;
47
+ const rx = Number(parts[rxIndex]);
48
+ const tx = Number(parts[txIndex]);
49
+ if (!numeric(rx) || !numeric(tx)) continue;
50
+ found.set(name, { name, rx, tx });
51
+ if (found.size >= 24) break;
52
+ }
53
+ return [...found.values()];
54
+ }
55
+
56
+ // Windows adapter statistics are real cumulative counters, not link-speed estimates.
57
+ export function parseWindowsNetworkCounters(input, activeNames = null) {
58
+ let parsed;
59
+ try { parsed = JSON.parse(String(input)); } catch { return []; }
60
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
61
+ const found = new Map();
62
+ for (const row of rows.slice(0, 128)) {
63
+ const name = row?.Name;
64
+ if (typeof name !== 'string' || !/^[\p{L}\p{N} _().:#-]{1,64}$/u.test(name) ||
65
+ (activeNames && !activeNames.has(name))) continue;
66
+ const parseCounter = value => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
67
+ ? value : typeof value === 'string' && /^\d{1,16}$/.test(value) && numeric(Number(value))
68
+ ? Number(value) : null;
69
+ const rx = parseCounter(row?.ReceivedBytes);
70
+ const tx = parseCounter(row?.SentBytes);
71
+ if (rx === null || tx === null) continue;
72
+ found.set(name, { name, rx, tx });
73
+ if (found.size >= 24) break;
74
+ }
75
+ return [...found.values()];
76
+ }
77
+
30
78
  export function calculateNetworkRates(current, previous, elapsedSeconds) {
31
79
  if (!Array.isArray(current) || !Array.isArray(previous) || !Number.isFinite(elapsedSeconds) ||
32
80
  elapsedSeconds < 1 || elapsedSeconds > 300) return null;
@@ -47,21 +95,66 @@ export function calculateNetworkRates(current, previous, elapsedSeconds) {
47
95
  }
48
96
 
49
97
  let lastNetwork = null;
50
- export async function networkRates() {
51
- if (process.platform !== 'linux') return null;
98
+ let pendingNetwork = null;
99
+
100
+ async function readNetworkCounters() {
101
+ if (process.platform === 'linux') {
102
+ const active = new Set(Object.entries(os.networkInterfaces())
103
+ .filter(([, addresses]) => addresses?.some(address => !address.internal))
104
+ .map(([name]) => name));
105
+ return parseNetworkCounters(await readFile('/proc/net/dev', 'utf8'))
106
+ .filter(row => active.has(row.name) && !/^(?:veth|docker|br-|virbr|tun|tap|wg|lo)/.test(row.name));
107
+ }
108
+ if (process.platform === 'darwin') {
109
+ const { stdout } = await run('/usr/sbin/netstat', ['-ibn'],
110
+ { timeout: 2500, maxBuffer: 128 * 1024 });
111
+ const active = new Set(Object.entries(os.networkInterfaces())
112
+ .filter(([, addresses]) => addresses?.some(address => !address.internal &&
113
+ !address.address.startsWith('fe80:'))).map(([name]) => name));
114
+ return parseDarwinNetworkCounters(stdout, active);
115
+ }
116
+ 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
+ const active = new Set(Object.entries(os.networkInterfaces())
121
+ .filter(([, addresses]) => addresses?.some(address => !address.internal))
122
+ .map(([name]) => name));
123
+ return parseWindowsNetworkCounters(stdout, active);
124
+ }
125
+ return [];
126
+ }
127
+
128
+ async function sampleNetworkRates() {
52
129
  try {
53
- const input = await readFile('/proc/net/dev', 'utf8');
54
- const current = parseNetworkCounters(input);
55
- const now = performance.now();
56
- const previous = lastNetwork;
130
+ let current = await readNetworkCounters();
131
+ let now = performance.now();
132
+ let previous = lastNetwork;
133
+ const elapsed = previous ? (now - previous.at) / 1000 : 0;
134
+ // The first diagnostics request must return a measured rate, not null.
135
+ // Bootstrap from two real counters; never manufacture a zero-rate sample.
136
+ if (elapsed < 1 || elapsed > 300) {
137
+ previous = { at: now, interfaces: current };
138
+ await delay(1100);
139
+ current = await readNetworkCounters();
140
+ now = performance.now();
141
+ }
57
142
  lastNetwork = { at: now, interfaces: current };
58
- return previous ? calculateNetworkRates(current, previous.interfaces, (now - previous.at) / 1000) : null;
143
+ return calculateNetworkRates(current, previous.interfaces, (now - previous.at) / 1000);
59
144
  } catch {
60
145
  lastNetwork = null;
61
146
  return null;
62
147
  }
63
148
  }
64
149
 
150
+ export async function networkRates() {
151
+ if (!['linux', 'darwin', 'win32'].includes(process.platform)) return null;
152
+ if (pendingNetwork) return pendingNetwork;
153
+ pendingNetwork = sampleNetworkRates();
154
+ try { return await pendingNetwork; }
155
+ finally { pendingNetwork = null; }
156
+ }
157
+
65
158
  export async function thermalReadings() {
66
159
  if (process.platform === 'darwin') return darwinTemperatureReadings();
67
160
  if (process.platform === 'linux') return linuxTemperatureReadings();