@remcp/runtime 0.2.134 → 0.2.135
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 +6 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/extended/catalog.mjs +1 -1
- package/src/extended/diagnostics.mjs +66 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.135 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
- Linux `audio` status is normalized across `wpctl`, `pactl`, and `amixer` into structured `volume`, `muted`, `action`, and `backend` fields; mutating audio actions re-read and return the verified state.
|
|
6
|
+
- Windows Core Audio and macOS audio responses expose the same `action`/`backend` fields.
|
|
7
|
+
- The additive `backend` output-schema field changes the tool-surface digest without changing tool names or the 83-tool count.
|
|
8
|
+
|
|
3
9
|
## 0.2.134 — 2026-10-02
|
|
4
10
|
|
|
5
11
|
- `power_action delay_seconds` is bounded to 0–90 seconds in schema and runtime validation so a delayed destructive call cannot intentionally outlive the hosted ~110-second device-call lifetime.
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ support and emits `notifications/tools/list_changed` when that capability set ch
|
|
|
49
49
|
| Runtime | `get_system_info`, `get_runtime_info`, `get_runtime_stats`, `set_config_value` |
|
|
50
50
|
| Computer use | `computer_snapshot`, `computer_action`, `list_windows`, `window_action`, `launch_app`, `ui_snapshot`, `ui_find`, `ui_action`, `type_text`, `keyboard`, `pointer`, `drag_drop`, `scroll`, `wait_for_ui`, `clipboard`, `display_inventory`, `screenshot_region`, `open_path`, `reveal_path`, `notification` |
|
|
51
51
|
| Browser | `browser_tabs`, `browser_navigate`, `browser_snapshot`, `browser_find`, `browser_action`, `browser_wait`, `browser_evaluate` |
|
|
52
|
-
| Diagnostics | `service`, `event_log`, `network`, `installed_apps`, `environment`, `audio`, `power_action`, `record_screen` |
|
|
52
|
+
| Diagnostics | `service`, `event_log`, `network`, `installed_apps`, `environment`, `audio`, `power_action`, `record_screen` — `audio` returns stable structured `volume`, `muted`, `action`, and `backend` fields across supported OS backends |
|
|
53
53
|
| Documents | `read_document`, `edit_spreadsheet`, `edit_document`, `pdf_action` |
|
|
54
54
|
|
|
55
55
|
The hosted ReMCP endpoint does not append those 83 granular operation names to its discovery list.
|
package/package.json
CHANGED
package/src/extended/catalog.mjs
CHANGED
|
@@ -214,7 +214,7 @@ const OUTPUT_FIELDS = Object.freeze({
|
|
|
214
214
|
network: { hostname:outputString(), interfaces:outputObject(), dns:outputArray(outputString()), servers:outputArray(outputString()), host:outputString(), port:outputNumber(), ok:outputBoolean(), latency_ms:outputNumber(), interface_count:outputNumber(), interface_count_returned:outputNumber(), address_count:outputNumber(), address_count_returned:outputNumber(), interfaces_truncated:outputBoolean(), addresses_truncated:outputBoolean(), data:outputArray(outputObject()) },
|
|
215
215
|
installed_apps: { data:outputArray(outputObject('Installed app/package with name/version/path where available.')) },
|
|
216
216
|
environment: { platform:outputString(), arch:outputString(), hostname:outputString(), release:outputString(), shell:outputString(), path:outputString(), node:outputString(), env:outputObject() },
|
|
217
|
-
audio: { volume:outputNumber(), muted:outputBoolean(), action:outputString(), text:outputString() },
|
|
217
|
+
audio: { volume:outputNumber(), muted:outputBoolean(), action:outputString(), backend:outputString(), text:outputString() },
|
|
218
218
|
power_action: { text:outputString(), action:outputString() },
|
|
219
219
|
record_screen: { path:outputString(), bytes:outputNumber(), duration_seconds:outputNumber(), format:outputString() },
|
|
220
220
|
read_document: { text:outputString(), path:outputString(), data:{}, sheets:outputArray(outputObject()), sheet:outputString(), rows:outputArray({}), cells:outputArray({}) },
|
|
@@ -367,45 +367,89 @@ export function windowsAudioPowerShell(action = 'status', volume = 50, { compile
|
|
|
367
367
|
].filter(Boolean).join('\n');
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
export function parseWpctlAudioStatus(output) {
|
|
371
|
+
const match = String(output || '').match(/\bVolume:\s*([0-9]+(?:\.[0-9]+)?)(?:\s+\[(MUTED)\])?/i);
|
|
372
|
+
if (!match) throw new Error('wpctl returned an unrecognized volume status');
|
|
373
|
+
const scalar = Number(match[1]);
|
|
374
|
+
if (!Number.isFinite(scalar)) throw new Error('wpctl returned a non-numeric volume');
|
|
375
|
+
return { volume:Math.round(Math.max(0, Math.min(1, scalar)) * 100), muted:Boolean(match[2]) };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function parsePactlAudioStatus(volumeOutput, muteOutput) {
|
|
379
|
+
const volumeMatch = String(volumeOutput || '').match(/\b(\d{1,3})%/);
|
|
380
|
+
const muteMatch = String(muteOutput || '').match(/\bMute:\s*(yes|no)\b/i);
|
|
381
|
+
if (!volumeMatch || !muteMatch) throw new Error('pactl returned an unrecognized volume/mute status');
|
|
382
|
+
return {
|
|
383
|
+
volume:Math.max(0, Math.min(100, Number(volumeMatch[1]))),
|
|
384
|
+
muted:muteMatch[1].toLowerCase() === 'yes',
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function parseAmixerAudioStatus(output) {
|
|
389
|
+
const rendered = String(output || '');
|
|
390
|
+
const volumeMatch = rendered.match(/\[(\d{1,3})%\]/);
|
|
391
|
+
const states = [...rendered.matchAll(/\[(on|off)\]/gi)].map(match => match[1].toLowerCase());
|
|
392
|
+
if (!volumeMatch || !states.length) throw new Error('amixer returned an unrecognized Master status');
|
|
393
|
+
return {
|
|
394
|
+
volume:Math.max(0, Math.min(100, Number(volumeMatch[1]))),
|
|
395
|
+
muted:states.every(state => state === 'off'),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function linuxAudioStatus(backend) {
|
|
400
|
+
if (backend === 'wpctl') {
|
|
401
|
+
const { stdout } = await runFile('wpctl', ['get-volume','@DEFAULT_AUDIO_SINK@'], { label:'audio status' });
|
|
402
|
+
return parseWpctlAudioStatus(stdout);
|
|
403
|
+
}
|
|
404
|
+
if (backend === 'pactl') {
|
|
405
|
+
const volumeResult = await runFile('pactl', ['get-sink-volume','@DEFAULT_SINK@'], { label:'audio status' });
|
|
406
|
+
const muteResult = await runFile('pactl', ['get-sink-mute','@DEFAULT_SINK@'], { label:'audio status' });
|
|
407
|
+
return parsePactlAudioStatus(volumeResult.stdout, muteResult.stdout);
|
|
408
|
+
}
|
|
409
|
+
const { stdout } = await runFile('amixer', ['get','Master'], { label:'audio status' });
|
|
410
|
+
return parseAmixerAudioStatus(stdout);
|
|
411
|
+
}
|
|
412
|
+
|
|
370
413
|
export async function audioTool(args) {
|
|
371
414
|
const action = requireEnum(args.action || 'status', 'action', ['status','set_volume','mute','unmute']);
|
|
372
415
|
const volume = clamp(args.volume, 50, 0, 100);
|
|
373
416
|
if (process.platform === 'darwin') {
|
|
374
|
-
|
|
417
|
+
const readState = async () => {
|
|
375
418
|
const { stdout } = await runOsa('set v to output volume of (get volume settings)\nset m to output muted of (get volume settings)\nreturn (v as text) & tab & (m as text)', { label:'audio status' });
|
|
376
|
-
const [v,m] = stdout.trim().split('\t');
|
|
419
|
+
const [v,m] = stdout.trim().split('\t');
|
|
420
|
+
return { volume:Number(v), muted:m === 'true' };
|
|
421
|
+
};
|
|
422
|
+
if (action !== 'status') {
|
|
423
|
+
await runOsa(action === 'set_volume' ? `set volume output volume ${volume}` : action === 'mute' ? 'set volume with output muted' : 'set volume without output muted', { label:'audio control' });
|
|
377
424
|
}
|
|
378
|
-
|
|
379
|
-
return audioTool({ action:'status' });
|
|
425
|
+
return jsonResult({ ...(await readState()), action, backend:'osascript' });
|
|
380
426
|
}
|
|
381
427
|
if (process.platform === 'win32') {
|
|
382
428
|
if (!commandExists('powershell.exe') && !process.env.SystemRoot) unavailable('Windows audio control');
|
|
383
429
|
const { stdout } = await runPowerShell(windowsAudioPowerShell(action, volume), { label:'Windows CoreAudio control', timeout:30_000 });
|
|
384
430
|
const rendered = stdout.trim();
|
|
385
431
|
try {
|
|
386
|
-
return jsonResult(JSON.parse(rendered));
|
|
432
|
+
return jsonResult({ ...JSON.parse(rendered), action, backend:'coreaudio' });
|
|
387
433
|
} catch {
|
|
388
434
|
throw new Error(`Windows CoreAudio returned an invalid state payload: ${rendered || '(empty)'}`);
|
|
389
435
|
}
|
|
390
436
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
await runFile('amixer', action === 'set_volume' ? ['set','Master',`${volume}%`] : ['set','Master',action === 'mute' ? 'mute' : 'unmute'], { label:'audio control' });
|
|
406
|
-
return text(`Audio action ${action} completed.`);
|
|
437
|
+
|
|
438
|
+
const backend = commandExists('wpctl') ? 'wpctl' : commandExists('pactl') ? 'pactl' : commandExists('amixer') ? 'amixer' : null;
|
|
439
|
+
if (!backend) unavailable('Audio control', 'wpctl, pactl or amixer is required on Linux');
|
|
440
|
+
|
|
441
|
+
if (action !== 'status') {
|
|
442
|
+
if (backend === 'wpctl') {
|
|
443
|
+
if (action === 'set_volume') await runFile('wpctl', ['set-volume','@DEFAULT_AUDIO_SINK@',`${volume}%`], { label:'audio control' });
|
|
444
|
+
else await runFile('wpctl', ['set-mute','@DEFAULT_AUDIO_SINK@',action === 'mute' ? '1' : '0'], { label:'audio control' });
|
|
445
|
+
} else if (backend === 'pactl') {
|
|
446
|
+
if (action === 'set_volume') await runFile('pactl', ['set-sink-volume','@DEFAULT_SINK@',`${volume}%`], { label:'audio control' });
|
|
447
|
+
else await runFile('pactl', ['set-sink-mute','@DEFAULT_SINK@',action === 'mute' ? '1' : '0'], { label:'audio control' });
|
|
448
|
+
} else {
|
|
449
|
+
await runFile('amixer', action === 'set_volume' ? ['set','Master',`${volume}%`] : ['set','Master',action === 'mute' ? 'mute' : 'unmute'], { label:'audio control' });
|
|
450
|
+
}
|
|
407
451
|
}
|
|
408
|
-
|
|
452
|
+
return jsonResult({ ...(await linuxAudioStatus(backend)), action, backend });
|
|
409
453
|
}
|
|
410
454
|
|
|
411
455
|
export const POWER_ACTION_MAX_DELAY_SECONDS = 90;
|