@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.171",
3
+ "version": "0.2.173",
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",
@@ -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
+ }
@@ -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 diskUsage(),
73
+ disk: await diskPromise,
65
74
  home: os.homedir(),
66
75
  tempDir: os.tmpdir(),
67
76
  runtimeName: runtimeConfig.name,
@@ -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 waitForProcessExit(session, timeoutMs);
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.`;