@remcp/runtime 0.2.126 → 0.2.128

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.128 — 2026-10-02
4
+
5
+ - Linux `power_action` is advertised only when both `loginctl` and `systemctl` exist, matching the full lock/sleep/restart/shutdown schema instead of exposing a partially unusable tool.
6
+ - Linux `network listeners` keeps `ss -lntup` as the preferred backend and changes the `netstat` fallback from `-an` to `-lntu`, excluding established/non-listening connections.
7
+ - Tool names and schemas remain unchanged; dynamic capability updates continue to use `tools/list_changed`.
8
+
9
+ ## 0.2.127 — 2026-10-01
10
+
11
+ - macOS `network listeners` now returns only TCP `LISTEN` rows from the native network table instead of all active connections.
12
+ - Established/closing and Unix-domain rows are filtered out, aligning macOS with the documented listener-only contract.
13
+ - The macOS release CI runs the routing + listener diagnostics test on `macos-latest`. Tool names and schemas remain unchanged.
14
+
3
15
  ## 0.2.126 — 2026-10-01
4
16
 
5
17
  - macOS `network` route inventory now uses `/usr/sbin/netstat -rn`; the previous generic Unix fallback invoked `route -n`, which on BSD/macOS expects a command and can return usage instead of the table.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.126",
3
+ "version": "0.2.128",
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",
@@ -598,6 +598,15 @@ async function linuxHasPyAtSpi() {
598
598
  return pyAtSpiCache.value;
599
599
  }
600
600
 
601
+ export function powerCapabilitySnapshot({
602
+ platform = process.platform,
603
+ commandExistsFn = commandExists,
604
+ } = {}) {
605
+ if (platform === 'win32' || platform === 'darwin') return true;
606
+ if (platform !== 'linux') return false;
607
+ return commandExistsFn('loginctl') && commandExistsFn('systemctl');
608
+ }
609
+
601
610
  export function documentCapabilitySnapshot({
602
611
  platform = process.platform,
603
612
  commandExistsFn = commandExists,
@@ -641,7 +650,7 @@ export async function capabilitySnapshot() {
641
650
  logs: win || mac || commandExists('journalctl'),
642
651
  apps: win || mac || commandExists('dpkg-query') || commandExists('rpm'),
643
652
  audio: win || mac || commandExists('wpctl') || commandExists('pactl') || commandExists('amixer'),
644
- power: win || mac || commandExists('systemctl') || commandExists('loginctl'),
653
+ power: powerCapabilitySnapshot(),
645
654
  record: recordScreenAvailable({ platform:process.platform, wayland, gnomeSupported:gnomeRecord }),
646
655
  ...documentCapabilities,
647
656
  };
@@ -108,6 +108,13 @@ export async function eventLog(args) {
108
108
  return text(rendered, result.code !== 0 && !result.stdout.trim());
109
109
  }
110
110
 
111
+ export function filterMacosTcpListeners(output) {
112
+ const lines = String(output || '').split(/\r?\n/);
113
+ const header = lines.filter(line => /^Proto\s+/i.test(line) || /^Active Internet connections/i.test(line));
114
+ const listeners = lines.filter(line => /\bLISTEN\b/i.test(line));
115
+ return [...header, ...listeners].join('\n').trim();
116
+ }
117
+
111
118
  async function connectivityTest(host, port, timeoutMs) {
112
119
  return new Promise(resolve => {
113
120
  const started = performance.now();
@@ -120,6 +127,12 @@ async function connectivityTest(host, port, timeoutMs) {
120
127
  });
121
128
  }
122
129
 
130
+ export function linuxListenerBackend({ commandExistsFn = commandExists } = {}) {
131
+ if (commandExistsFn('ss')) return { command:'ss', args:['-lntup'] };
132
+ if (commandExistsFn('netstat')) return { command:'netstat', args:['-lntu'] };
133
+ return null;
134
+ }
135
+
123
136
  export async function networkTool(args) {
124
137
  const action = requireEnum(args.action || 'summary', 'action', ['summary','interfaces','dns','routes','listeners','test']);
125
138
  if (action === 'interfaces') return jsonResult(os.networkInterfaces());
@@ -138,13 +151,17 @@ export async function networkTool(args) {
138
151
  if (process.platform === 'darwin' && action === 'routes') {
139
152
  return text((await runFile('/usr/sbin/netstat', ['-rn'], { label:'routes' })).stdout);
140
153
  }
154
+ if (process.platform === 'darwin' && action === 'listeners') {
155
+ const { stdout } = await runFile('/usr/sbin/netstat', ['-an', '-p', 'tcp'], { label:'listeners' });
156
+ return text(filterMacosTcpListeners(stdout));
157
+ }
141
158
  if (action === 'routes') {
142
159
  if (commandExists('ip')) return text((await runFile('ip', ['route','show'], { label:'routes' })).stdout);
143
160
  if (commandExists('route')) return text((await runFile('route', ['-n'], { label:'routes' })).stdout);
144
161
  unavailable('Route inventory', 'ip or route is required');
145
162
  }
146
- if (commandExists('ss')) return text((await runFile('ss', ['-lntup'], { label:'listeners', allowFailure:true })).stdout);
147
- if (commandExists('netstat')) return text((await runFile('netstat', ['-an'], { label:'listeners' })).stdout);
163
+ const listenerBackend = linuxListenerBackend();
164
+ if (listenerBackend) return text((await runFile(listenerBackend.command, listenerBackend.args, { label:'listeners', allowFailure:true })).stdout);
148
165
  unavailable('Listener inventory', 'ss or netstat is required');
149
166
  }
150
167