@remcp/runtime 0.2.187 → 0.2.188
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
|
@@ -27,6 +27,13 @@ export function cpuUtilizationRatio(before, after) {
|
|
|
27
27
|
return deltaTotal > 0 ? Number(Math.max(0, Math.min(1, 1 - deltaIdle / deltaTotal)).toFixed(3)) : null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function cpuPerCoreUtilizationRatios(before, after) {
|
|
31
|
+
if (!Array.isArray(before) || !Array.isArray(after) || !before.length ||
|
|
32
|
+
before.length !== after.length || before.length > 256) return null;
|
|
33
|
+
const ratios = before.map((core, index) => cpuUtilizationRatio([core], [after[index]]));
|
|
34
|
+
return ratios.every(value => value !== null) ? ratios : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export function cpuFrequencyMHz(cores) {
|
|
31
38
|
if (!Array.isArray(cores) || !cores.length) return null;
|
|
32
39
|
const speeds = cores.map(core => core?.speed).filter(speed => Number.isFinite(speed) && speed > 0 && speed <= 100000);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Best-effort host measurements. Keep interface names, mount paths and sensor labels bounded;
|
|
2
|
+
// never expose raw process arguments or arbitrary kernel sensor metadata.
|
|
3
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
const numeric = value => Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
|
|
10
|
+
const safeName = value => typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,40}$/.test(value);
|
|
11
|
+
|
|
12
|
+
export function parseNetworkCounters(input) {
|
|
13
|
+
const interfaces = [];
|
|
14
|
+
for (const line of String(input).split('\n').slice(2, 130)) {
|
|
15
|
+
const match = line.match(/^\s*([^:\s]+):\s*(.+)$/);
|
|
16
|
+
if (!match || !safeName(match[1]) || match[1] === 'lo') continue;
|
|
17
|
+
const columns = match[2].trim().split(/\s+/);
|
|
18
|
+
const rx = Number(columns[0]);
|
|
19
|
+
const tx = Number(columns[8]);
|
|
20
|
+
if (!numeric(rx) || !numeric(tx) || columns.length < 16) continue;
|
|
21
|
+
interfaces.push({ name: match[1], rx, tx });
|
|
22
|
+
if (interfaces.length >= 24) break;
|
|
23
|
+
}
|
|
24
|
+
return interfaces;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function calculateNetworkRates(current, previous, elapsedSeconds) {
|
|
28
|
+
if (!Array.isArray(current) || !Array.isArray(previous) || !Number.isFinite(elapsedSeconds) ||
|
|
29
|
+
elapsedSeconds < 1 || elapsedSeconds > 300) return null;
|
|
30
|
+
const old = new Map(previous.map(row => [row.name, row]));
|
|
31
|
+
const interfaces = current.flatMap(row => {
|
|
32
|
+
const prior = old.get(row.name);
|
|
33
|
+
if (!prior || !numeric(row.rx) || !numeric(row.tx) || row.rx < prior.rx || row.tx < prior.tx) return [];
|
|
34
|
+
const rxBytesPerSecond = Math.round((row.rx - prior.rx) / elapsedSeconds);
|
|
35
|
+
const txBytesPerSecond = Math.round((row.tx - prior.tx) / elapsedSeconds);
|
|
36
|
+
return [{ name: row.name, rxBytesPerSecond, txBytesPerSecond }];
|
|
37
|
+
});
|
|
38
|
+
if (!interfaces.length) return null;
|
|
39
|
+
return {
|
|
40
|
+
rxBytesPerSecond: interfaces.reduce((sum, row) => sum + row.rxBytesPerSecond, 0),
|
|
41
|
+
txBytesPerSecond: interfaces.reduce((sum, row) => sum + row.txBytesPerSecond, 0),
|
|
42
|
+
interfaces,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let lastNetwork = null;
|
|
47
|
+
export async function networkRates() {
|
|
48
|
+
if (process.platform !== 'linux') return null;
|
|
49
|
+
try {
|
|
50
|
+
const input = await readFile('/proc/net/dev', 'utf8');
|
|
51
|
+
const current = parseNetworkCounters(input);
|
|
52
|
+
const now = performance.now();
|
|
53
|
+
const previous = lastNetwork;
|
|
54
|
+
lastNetwork = { at: now, interfaces: current };
|
|
55
|
+
return previous ? calculateNetworkRates(current, previous.interfaces, (now - previous.at) / 1000) : null;
|
|
56
|
+
} catch {
|
|
57
|
+
lastNetwork = null;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function thermalReadings() {
|
|
63
|
+
if (process.platform !== 'linux') return [];
|
|
64
|
+
const sensors = [];
|
|
65
|
+
// Linux thermal zones expose millidegrees Celsius; only fixed kernel paths are read.
|
|
66
|
+
try {
|
|
67
|
+
const names = (await readdir('/sys/class/thermal')).filter(name => /^thermal_zone\d{1,3}$/.test(name)).slice(0, 16);
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
try {
|
|
70
|
+
const value = Number((await readFile('/sys/class/thermal/' + name + '/temp', 'utf8')).trim()) / 1000;
|
|
71
|
+
if (Number.isFinite(value) && value >= -40 && value <= 125)
|
|
72
|
+
sensors.push({ name: 'Thermal ' + name.slice(12), celsius: Number(value.toFixed(1)) });
|
|
73
|
+
} catch { /* Sensor is optional or permissions deny reading it. */ }
|
|
74
|
+
}
|
|
75
|
+
} catch { /* No thermal zone inventory. */ }
|
|
76
|
+
// hwmon is common on desktop hardware without usable thermal zones.
|
|
77
|
+
if (!sensors.length) {
|
|
78
|
+
try {
|
|
79
|
+
const chips = (await readdir('/sys/class/hwmon')).filter(name => /^hwmon\d{1,3}$/.test(name)).slice(0, 12);
|
|
80
|
+
for (const chip of chips) {
|
|
81
|
+
for (let index = 1; index <= 4 && sensors.length < 12; index++) {
|
|
82
|
+
try {
|
|
83
|
+
const value = Number((await readFile('/sys/class/hwmon/' + chip + '/temp' + index + '_input', 'utf8')).trim()) / 1000;
|
|
84
|
+
if (Number.isFinite(value) && value >= -40 && value <= 125)
|
|
85
|
+
sensors.push({ name: 'Sensor ' + (sensors.length + 1), celsius: Number(value.toFixed(1)) });
|
|
86
|
+
} catch { /* Not every chip exposes every temperature index. */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch { /* No readable hwmon inventory. */ }
|
|
90
|
+
}
|
|
91
|
+
return sensors.slice(0, 12);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function parseDiskVolumes(input) {
|
|
95
|
+
const volumes = [];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const line of String(input).split('\n').slice(1, 256)) {
|
|
98
|
+
const match = line.trim().match(/^(\/dev\/\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(\S+)$/);
|
|
99
|
+
if (!match || seen.has(match[6])) continue;
|
|
100
|
+
// Snap/loop and memory-backed devices are virtual mounts, not storage volumes.
|
|
101
|
+
if (/^\/dev\/(?:loop\d+|zram\d+|ram\d+)$/.test(match[1])) continue;
|
|
102
|
+
const [size, used, available] = match.slice(2, 5).map(Number);
|
|
103
|
+
const percent = Number(match[5]);
|
|
104
|
+
if (!numeric(size) || !numeric(used) || !numeric(available) || size <= 0 ||
|
|
105
|
+
used > size || available > size || percent > 100 ||
|
|
106
|
+
![size, used, available].every(value => Number.isSafeInteger(value * 1024))) continue;
|
|
107
|
+
const mount = match[6];
|
|
108
|
+
if (mount.length > 100 || !/^\/[\w./ -]*$/.test(mount)) continue;
|
|
109
|
+
seen.add(mount);
|
|
110
|
+
volumes.push({ mount, totalBytes: size * 1024, usedBytes: used * 1024,
|
|
111
|
+
availableBytes: available * 1024, usedRatio: Number((used / size).toFixed(3)) });
|
|
112
|
+
if (volumes.length >= 12) break;
|
|
113
|
+
}
|
|
114
|
+
return volumes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function diskVolumes() {
|
|
118
|
+
if (process.platform === 'win32') return [];
|
|
119
|
+
try {
|
|
120
|
+
const { stdout } = await run('df', ['-kP'], { timeout: 2000, maxBuffer: 256 * 1024 });
|
|
121
|
+
return parseDiskVolumes(stdout);
|
|
122
|
+
} catch { return []; }
|
|
123
|
+
}
|
package/src/tools/system.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { execFile } from 'node:child_process';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { runtimeConfig } from '../config.mjs';
|
|
6
6
|
import { clampInteger, fail, requireInteger, text } from '../util.mjs';
|
|
7
|
-
import { cpuUtilizationRatio, cpuFrequencyMHz } from './cpu-telemetry.mjs';
|
|
7
|
+
import { cpuUtilizationRatio, cpuFrequencyMHz, cpuPerCoreUtilizationRatios } from './cpu-telemetry.mjs';
|
|
8
|
+
import { diskVolumes, networkRates, thermalReadings } from './system-telemetry.mjs';
|
|
8
9
|
|
|
9
10
|
const run = promisify(execFile);
|
|
10
11
|
|
|
@@ -46,6 +47,9 @@ export async function getSystemInfoTool() {
|
|
|
46
47
|
// Sample for 140 ms while retrieving disk facts, so CPU % is actual host activity.
|
|
47
48
|
// Load average is a queue-length metric and must never be displayed as a percentage.
|
|
48
49
|
const diskPromise = diskUsage();
|
|
50
|
+
const volumesPromise = diskVolumes();
|
|
51
|
+
const thermalPromise = thermalReadings();
|
|
52
|
+
const networkPromise = networkRates();
|
|
49
53
|
const sampledCpus = await new Promise(resolve => setTimeout(() => resolve(os.cpus()), 140));
|
|
50
54
|
const utilizationRatio = cpuUtilizationRatio(cpus, sampledCpus);
|
|
51
55
|
const frequencyMHz = cpuFrequencyMHz(sampledCpus);
|
|
@@ -61,6 +65,7 @@ export async function getSystemInfoTool() {
|
|
|
61
65
|
model: cpus[0]?.model?.trim() || 'unknown',
|
|
62
66
|
count: cpus.length,
|
|
63
67
|
utilizationRatio,
|
|
68
|
+
perCoreUtilizationRatios: cpuPerCoreUtilizationRatios(cpus, sampledCpus),
|
|
64
69
|
frequencyMHz,
|
|
65
70
|
loadAverage: process.platform === 'win32' ? null : load.map(value => Number(value.toFixed(2))),
|
|
66
71
|
loadPerCore: process.platform === 'win32' || !cpus.length ? null : Number((load[0] / cpus.length).toFixed(2)),
|
|
@@ -71,6 +76,9 @@ export async function getSystemInfoTool() {
|
|
|
71
76
|
usedRatio: totalMem ? Number(((totalMem - freeMem) / totalMem).toFixed(3)) : 0,
|
|
72
77
|
},
|
|
73
78
|
disk: await diskPromise,
|
|
79
|
+
volumes: await volumesPromise,
|
|
80
|
+
temperatures: await thermalPromise,
|
|
81
|
+
network: await networkPromise,
|
|
74
82
|
home: os.homedir(),
|
|
75
83
|
tempDir: os.tmpdir(),
|
|
76
84
|
runtimeName: runtimeConfig.name,
|