@remcp/runtime 0.2.171 → 0.2.173
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/tools/cpu-telemetry.mjs +34 -0
- package/src/tools/system.mjs +10 -1
- package/src/tools/terminal.mjs +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.173 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
- Return initial terminal output promptly after a bounded 175 ms grace when a long-running process writes without exiting.
|
|
6
|
+
- Preserve exit-status reporting for immediately completing commands, timeout upper bounds and process-session read/termination semantics.
|
|
7
|
+
- Add terminal regression tests; retain 83 runtime operation routes and unchanged authorization and device safety controls.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## 0.2.172 — 2026-10-22
|
|
11
|
+
|
|
12
|
+
- Add bounded host-wide CPU utilization sampling based on deltas from `os.cpus().times` so the dashboard can display real CPU percentages instead of treating load average as CPU%.
|
|
13
|
+
- Add `cpu.frequencyMHz` from reported per-core clock rates, with `null` for unsupported or invalid measurements.
|
|
14
|
+
- Preserve the existing `get_system_info` schema, 83-tool catalog, authentication restrictions, and backwards compatibility.
|
|
15
|
+
|
|
16
|
+
|
|
3
17
|
## 0.2.171 — 2026-10-21
|
|
4
18
|
|
|
5
19
|
- Require MCP Server SDK 2.3.0 or later; update the published dependency lock to the patched 2.3.x line.
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// OS-wide CPU utilization over a short bounded interval, not process CPU time or load average.
|
|
2
|
+
const timeKeys = ['user', 'nice', 'sys', 'idle', 'irq'];
|
|
3
|
+
|
|
4
|
+
function totals(cpu) {
|
|
5
|
+
const times = cpu?.times;
|
|
6
|
+
if (!times || !timeKeys.every(key => Number.isFinite(times[key]) && times[key] >= 0)) return null;
|
|
7
|
+
return {
|
|
8
|
+
total: timeKeys.reduce((sum, key) => sum + times[key], 0),
|
|
9
|
+
idle: times.idle,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function cpuUtilizationRatio(before, after) {
|
|
14
|
+
if (!Array.isArray(before) || !Array.isArray(after) || !before.length || before.length !== after.length) return null;
|
|
15
|
+
let deltaTotal = 0;
|
|
16
|
+
let deltaIdle = 0;
|
|
17
|
+
for (let i = 0; i < before.length; i++) {
|
|
18
|
+
const previous = totals(before[i]);
|
|
19
|
+
const current = totals(after[i]);
|
|
20
|
+
if (!previous || !current) return null;
|
|
21
|
+
const total = current.total - previous.total;
|
|
22
|
+
const idle = current.idle - previous.idle;
|
|
23
|
+
if (total < 0 || idle < 0 || idle > total) return null;
|
|
24
|
+
deltaTotal += total;
|
|
25
|
+
deltaIdle += idle;
|
|
26
|
+
}
|
|
27
|
+
return deltaTotal > 0 ? Number(Math.max(0, Math.min(1, 1 - deltaIdle / deltaTotal)).toFixed(3)) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function cpuFrequencyMHz(cores) {
|
|
31
|
+
if (!Array.isArray(cores) || !cores.length) return null;
|
|
32
|
+
const speeds = cores.map(core => core?.speed).filter(speed => Number.isFinite(speed) && speed > 0 && speed <= 100000);
|
|
33
|
+
return speeds.length ? Math.round(speeds.reduce((sum, speed) => sum + speed, 0) / speeds.length) : null;
|
|
34
|
+
}
|
package/src/tools/system.mjs
CHANGED
|
@@ -4,6 +4,7 @@ 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
8
|
|
|
8
9
|
const run = promisify(execFile);
|
|
9
10
|
|
|
@@ -42,6 +43,12 @@ export async function getSystemInfoTool() {
|
|
|
42
43
|
const totalMem = os.totalmem();
|
|
43
44
|
const freeMem = os.freemem();
|
|
44
45
|
const load = os.loadavg();
|
|
46
|
+
// Sample for 140 ms while retrieving disk facts, so CPU % is actual host activity.
|
|
47
|
+
// Load average is a queue-length metric and must never be displayed as a percentage.
|
|
48
|
+
const diskPromise = diskUsage();
|
|
49
|
+
const sampledCpus = await new Promise(resolve => setTimeout(() => resolve(os.cpus()), 140));
|
|
50
|
+
const utilizationRatio = cpuUtilizationRatio(cpus, sampledCpus);
|
|
51
|
+
const frequencyMHz = cpuFrequencyMHz(sampledCpus);
|
|
45
52
|
return text(JSON.stringify({
|
|
46
53
|
hostname: os.hostname(),
|
|
47
54
|
platform: process.platform,
|
|
@@ -53,6 +60,8 @@ export async function getSystemInfoTool() {
|
|
|
53
60
|
cpu: {
|
|
54
61
|
model: cpus[0]?.model?.trim() || 'unknown',
|
|
55
62
|
count: cpus.length,
|
|
63
|
+
utilizationRatio,
|
|
64
|
+
frequencyMHz,
|
|
56
65
|
loadAverage: process.platform === 'win32' ? null : load.map(value => Number(value.toFixed(2))),
|
|
57
66
|
loadPerCore: process.platform === 'win32' || !cpus.length ? null : Number((load[0] / cpus.length).toFixed(2)),
|
|
58
67
|
},
|
|
@@ -61,7 +70,7 @@ export async function getSystemInfoTool() {
|
|
|
61
70
|
freeBytes: freeMem,
|
|
62
71
|
usedRatio: totalMem ? Number(((totalMem - freeMem) / totalMem).toFixed(3)) : 0,
|
|
63
72
|
},
|
|
64
|
-
disk: await
|
|
73
|
+
disk: await diskPromise,
|
|
65
74
|
home: os.homedir(),
|
|
66
75
|
tempDir: os.tmpdir(),
|
|
67
76
|
runtimeName: runtimeConfig.name,
|
package/src/tools/terminal.mjs
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
readNewOutput,
|
|
15
15
|
readOutputRange,
|
|
16
16
|
totalLines,
|
|
17
|
-
waitForProcessExit,
|
|
18
17
|
waitForProcessActivity,
|
|
19
18
|
} from '../sessions.mjs';
|
|
20
19
|
import { clampInteger, fail, requireInteger, requireString, structured, text, truncate } from '../util.mjs';
|
|
@@ -66,6 +65,21 @@ function terminalStructured(value, fallbackText) {
|
|
|
66
65
|
return structured(bounded, fallbackText);
|
|
67
66
|
}
|
|
68
67
|
|
|
68
|
+
// The start call promises initial output, not process completion. Return once
|
|
69
|
+
// useful output arrives, keeping a short grace window so quick commands can
|
|
70
|
+
// still report their real exit status without requiring a follow-up tool call.
|
|
71
|
+
// Explicit timeout_ms remains an upper bound, including for silent commands.
|
|
72
|
+
async function waitForInitialOutput(session, timeoutMs) {
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
const graceUntil = Date.now() + Math.min(175, timeoutMs);
|
|
75
|
+
while (!session.exited) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
if (now >= deadline || (now >= graceUntil && hasNewOutput(session))) break;
|
|
78
|
+
const wakeAt = now < graceUntil ? Math.min(deadline, graceUntil) : deadline;
|
|
79
|
+
await waitForProcessActivity(session, Math.min(100, Math.max(1, wakeAt - now)));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
export async function startProcessTool(args, extra = {}) {
|
|
70
84
|
const command = requireString(args.command, 'command');
|
|
71
85
|
const verdict = assertAllowedCommand(command);
|
|
@@ -105,7 +119,7 @@ export async function startProcessTool(args, extra = {}) {
|
|
|
105
119
|
child.on('exit', (code, signal) => markProcessExited(session, code, signal));
|
|
106
120
|
child.on('close', (code, signal) => markProcessExited(session, code, signal));
|
|
107
121
|
|
|
108
|
-
await
|
|
122
|
+
await waitForInitialOutput(session, timeoutMs);
|
|
109
123
|
const headline = session.exited
|
|
110
124
|
? `Process ${session.pid} finished${session.exitCode === null ? '' : ` with code ${session.exitCode}`}${session.signal ? ` (${session.signal})` : ''}.`
|
|
111
125
|
: `Process ${session.pid} is running.`;
|