@remcp/runtime 0.2.100 → 0.2.102
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/extended/common.mjs +8 -1
- package/src/extended/diagnostics.mjs +46 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.102 — 2026-09-22
|
|
4
|
+
|
|
5
|
+
- `runWithInput` installs a stdin error handler before writing input, so an early child pipe close is returned as a `ToolError` instead of an uncaught `EPIPE` that can terminate the runtime.
|
|
6
|
+
- A failed input pipe now terminates the child before rejecting, preventing a rejected call from leaving an orphan process behind.
|
|
7
|
+
- Empty input closes stdin without a zero-byte write, reducing races with short-lived subprocesses.
|
|
8
|
+
- Added a deterministic closed-stdin regression and stress coverage; runtime tool names and schemas are unchanged.
|
|
9
|
+
|
|
10
|
+
## 0.2.101 — 2026-09-22
|
|
11
|
+
|
|
12
|
+
- macOS `record_screen` resolves FFmpeg from standard Homebrew locations when launchd's minimal `PATH` cannot see it.
|
|
13
|
+
- AVFoundation screen capture now enumerates devices and selects the actual `Capture screen` index instead of assuming input 1.
|
|
14
|
+
- Capability discovery stays conservative: no recorder backend means no `record_screen`; Linux Wayland still requires `wf-recorder`.
|
|
15
|
+
- Added regression coverage for Homebrew discovery and real-world AVFoundation ordering where input 1 is OBS Virtual Camera and the screen is input 3.
|
|
16
|
+
|
|
3
17
|
## 0.2.100 — 2026-09-22
|
|
4
18
|
|
|
5
19
|
- Wayland portal-backed region screenshots clip compositor window bounds to the visible virtual desktop before FFmpeg cropping.
|
package/package.json
CHANGED
package/src/extended/common.mjs
CHANGED
|
@@ -98,7 +98,14 @@ export async function runWithInput(file, args = [], input = '', options = {}) {
|
|
|
98
98
|
if (code === 0 || options.allowFailure) finish(resolve, { stdout, stderr, code });
|
|
99
99
|
else finish(reject, new ToolError(`${options.label || file} failed: ${stderr.trim() || `exit ${code}`}`));
|
|
100
100
|
});
|
|
101
|
-
child.stdin.
|
|
101
|
+
child.stdin.on('error', error => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
child.kill('SIGKILL');
|
|
104
|
+
finish(reject, new ToolError(`${options.label || file} input failed: ${error.message}`));
|
|
105
|
+
});
|
|
106
|
+
const inputText = String(input);
|
|
107
|
+
if (inputText.length) child.stdin.end(inputText);
|
|
108
|
+
else child.stdin.end();
|
|
102
109
|
});
|
|
103
110
|
}
|
|
104
111
|
|
|
@@ -3,6 +3,7 @@ import net from 'node:net';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import process from 'node:process';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
6
7
|
import { stat } from 'node:fs/promises';
|
|
7
8
|
import { assertAllowedCommand } from '../policy.mjs';
|
|
8
9
|
import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
@@ -245,17 +246,54 @@ export async function powerAction(args) {
|
|
|
245
246
|
return text(`${policy.note ? `${policy.note}\n` : ''}Power action ${action} requested.`);
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
export function resolveRecordScreenFfmpeg({
|
|
250
|
+
platform = process.platform,
|
|
251
|
+
commandExistsFn = commandExists,
|
|
252
|
+
existsSyncFn = existsSync,
|
|
253
|
+
} = {}) {
|
|
254
|
+
if (commandExistsFn('ffmpeg')) return 'ffmpeg';
|
|
255
|
+
if (platform === 'darwin') {
|
|
256
|
+
for (const candidate of ['/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg']) {
|
|
257
|
+
if (existsSyncFn(candidate)) return candidate;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function parseAvfoundationScreenInput(output) {
|
|
264
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
265
|
+
const match = line.match(/\[(\d+)\]\s+Capture screen(?:\s+\d+)?\s*$/i);
|
|
266
|
+
if (match) return match[1];
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function avfoundationScreenInput(ffmpeg) {
|
|
272
|
+
const result = await runFile(ffmpeg, [
|
|
273
|
+
'-hide_banner',
|
|
274
|
+
'-f','avfoundation',
|
|
275
|
+
'-list_devices','true',
|
|
276
|
+
'-i','',
|
|
277
|
+
], { label:'AVFoundation device inventory', timeout:10_000, allowFailure:true });
|
|
278
|
+
const index = parseAvfoundationScreenInput(`${result.stderr || ''}\n${result.stdout || ''}`);
|
|
279
|
+
if (!index) unavailable('Screen recording', 'ffmpeg could not find an AVFoundation Capture screen input');
|
|
280
|
+
return index;
|
|
281
|
+
}
|
|
282
|
+
|
|
248
283
|
export function recordScreenAvailable({
|
|
249
284
|
platform = process.platform,
|
|
250
285
|
wayland = platform === 'linux' ? isWaylandSession() : false,
|
|
251
286
|
display = process.env.DISPLAY || '',
|
|
252
287
|
commandExistsFn = commandExists,
|
|
288
|
+
existsSyncFn = existsSync,
|
|
253
289
|
} = {}) {
|
|
254
290
|
if (platform === 'linux') {
|
|
255
291
|
if (wayland) return commandExistsFn('wf-recorder') && commandExistsFn('timeout');
|
|
256
292
|
return Boolean(String(display).trim()) && commandExistsFn('ffmpeg');
|
|
257
293
|
}
|
|
258
|
-
if (platform === 'darwin' || platform === 'win32')
|
|
294
|
+
if (platform === 'darwin' || platform === 'win32') {
|
|
295
|
+
return Boolean(resolveRecordScreenFfmpeg({ platform, commandExistsFn, existsSyncFn }));
|
|
296
|
+
}
|
|
259
297
|
return false;
|
|
260
298
|
}
|
|
261
299
|
|
|
@@ -268,22 +306,26 @@ export async function recordScreen(args) {
|
|
|
268
306
|
const result = await runFile('timeout', ['--signal=INT', `${seconds}s`, 'wf-recorder', '-f',destination,'-r',String(fps),'-c','libx264'], { label:'screen recording', timeout:(seconds+10)*1000, allowFailure:true });
|
|
269
307
|
if (![0, 124, 130].includes(Number(result.code))) throw new Error(result.stderr.trim() || `wf-recorder exited ${result.code}`);
|
|
270
308
|
} else {
|
|
309
|
+
const ffmpeg = resolveRecordScreenFfmpeg();
|
|
271
310
|
if (wayland) {
|
|
272
311
|
unavailable('Screen recording', 'wf-recorder and timeout are required on Wayland');
|
|
273
312
|
}
|
|
274
313
|
if (process.platform === 'linux' && !String(process.env.DISPLAY || '').trim()) {
|
|
275
314
|
unavailable('Screen recording', 'an active X11 DISPLAY is required on Linux X11');
|
|
276
315
|
}
|
|
277
|
-
if (!
|
|
316
|
+
if (!ffmpeg) {
|
|
278
317
|
if (process.platform === 'darwin') unavailable('Screen recording', 'ffmpeg is required on macOS');
|
|
279
318
|
if (process.platform === 'win32') unavailable('Screen recording', 'ffmpeg is required on Windows');
|
|
280
319
|
unavailable('Screen recording', 'ffmpeg is required on X11');
|
|
281
320
|
}
|
|
282
321
|
let argv;
|
|
283
322
|
if (process.platform === 'win32') argv=['-y','-f','gdigrab','-framerate',String(fps),'-i','desktop','-t',String(seconds),'-pix_fmt','yuv420p',destination];
|
|
284
|
-
else if (process.platform === 'darwin')
|
|
323
|
+
else if (process.platform === 'darwin') {
|
|
324
|
+
const input = await avfoundationScreenInput(ffmpeg);
|
|
325
|
+
argv=['-y','-f','avfoundation','-framerate',String(fps),'-i',`${input}:none`,'-t',String(seconds),'-pix_fmt','yuv420p',destination];
|
|
326
|
+
}
|
|
285
327
|
else argv=['-y','-f','x11grab','-framerate',String(fps),'-i',process.env.DISPLAY,'-t',String(seconds),'-pix_fmt','yuv420p',destination];
|
|
286
|
-
await runFile(
|
|
328
|
+
await runFile(ffmpeg, argv, { label:'screen recording', timeout:(seconds+20)*1000, maxBuffer:8*1024*1024 });
|
|
287
329
|
}
|
|
288
330
|
const info = await stat(destination);
|
|
289
331
|
return jsonResult({ path:destination, bytes:info.size, duration_seconds:seconds, format:path.extname(destination).slice(1) || 'mp4' });
|