@remcp/runtime 0.2.124 → 0.2.125

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.125 — 2026-10-01
4
+
5
+ - Windows `audio` now controls the default render endpoint through the native Core Audio `IAudioEndpointVolume` interface hosted by inbox PowerShell `Add-Type`.
6
+ - `status` returns the actual 0–100 master level and mute state; `set_volume` is absolute; `mute`/`unmute` use idempotent `SetMute(true/false)` instead of a shared toggle key.
7
+ - Windows release CI compiles the interop definition with PowerShell 5.1 on `windows-latest`. No third-party runtime dependency is added and the 83 tool schemas are unchanged.
8
+
3
9
  ## 0.2.124 — 2026-10-01
4
10
 
5
11
  - P3 document capability discovery is cross-platform: Windows uses the native .NET OOXML archive backend, while macOS/Linux require both `zip` and `unzip` before advertising the XLSX/DOCX editor tools.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.124",
3
+ "version": "0.2.125",
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",
@@ -183,6 +183,117 @@ export async function environmentTool(args) {
183
183
  });
184
184
  }
185
185
 
186
+ const WINDOWS_AUDIO_CORE_CSHARP = String.raw`
187
+ using System;
188
+ using System.Runtime.InteropServices;
189
+
190
+ namespace ReMCP {
191
+ enum EDataFlow { eRender = 0, eCapture = 1, eAll = 2 }
192
+ enum ERole { eConsole = 0, eMultimedia = 1, eCommunications = 2 }
193
+
194
+ [ComImport]
195
+ [Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
196
+ class MMDeviceEnumeratorComObject { }
197
+
198
+ [ComImport]
199
+ [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
200
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
201
+ interface IMMDeviceEnumerator {
202
+ [PreserveSig] int EnumAudioEndpoints(EDataFlow dataFlow, uint stateMask, out IntPtr devices);
203
+ [PreserveSig] int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
204
+ }
205
+
206
+ [ComImport]
207
+ [Guid("D666063F-1587-4E43-81F1-B948E807363F")]
208
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
209
+ interface IMMDevice {
210
+ [PreserveSig] int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object instance);
211
+ }
212
+
213
+ [ComImport]
214
+ [Guid("5CDF2C82-841E-4546-9722-0CF74078229A")]
215
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
216
+ interface IAudioEndpointVolume {
217
+ [PreserveSig] int RegisterControlChangeNotify(IntPtr notify);
218
+ [PreserveSig] int UnregisterControlChangeNotify(IntPtr notify);
219
+ [PreserveSig] int GetChannelCount(out uint count);
220
+ [PreserveSig] int SetMasterVolumeLevel(float levelDb, ref Guid eventContext);
221
+ [PreserveSig] int SetMasterVolumeLevelScalar(float level, ref Guid eventContext);
222
+ [PreserveSig] int GetMasterVolumeLevel(out float levelDb);
223
+ [PreserveSig] int GetMasterVolumeLevelScalar(out float level);
224
+ [PreserveSig] int SetChannelVolumeLevel(uint channel, float levelDb, ref Guid eventContext);
225
+ [PreserveSig] int SetChannelVolumeLevelScalar(uint channel, float level, ref Guid eventContext);
226
+ [PreserveSig] int GetChannelVolumeLevel(uint channel, out float levelDb);
227
+ [PreserveSig] int GetChannelVolumeLevelScalar(uint channel, out float level);
228
+ [PreserveSig] int SetMute([MarshalAs(UnmanagedType.Bool)] bool muted, ref Guid eventContext);
229
+ [PreserveSig] int GetMute([MarshalAs(UnmanagedType.Bool)] out bool muted);
230
+ [PreserveSig] int GetVolumeStepInfo(out uint step, out uint stepCount);
231
+ [PreserveSig] int VolumeStepUp(ref Guid eventContext);
232
+ [PreserveSig] int VolumeStepDown(ref Guid eventContext);
233
+ [PreserveSig] int QueryHardwareSupport(out uint mask);
234
+ [PreserveSig] int GetVolumeRange(out float minDb, out float maxDb, out float incrementDb);
235
+ }
236
+
237
+ public static class WindowsAudio {
238
+ const uint CLSCTX_ALL = 23;
239
+
240
+ static IAudioEndpointVolume Endpoint() {
241
+ IMMDeviceEnumerator enumerator = (IMMDeviceEnumerator)(new MMDeviceEnumeratorComObject());
242
+ IMMDevice device;
243
+ Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out device));
244
+ Guid iid = typeof(IAudioEndpointVolume).GUID;
245
+ object endpoint;
246
+ Marshal.ThrowExceptionForHR(device.Activate(ref iid, CLSCTX_ALL, IntPtr.Zero, out endpoint));
247
+ return (IAudioEndpointVolume)endpoint;
248
+ }
249
+
250
+ public static string GetStateJson() {
251
+ IAudioEndpointVolume endpoint = Endpoint();
252
+ float scalar;
253
+ bool muted;
254
+ Marshal.ThrowExceptionForHR(endpoint.GetMasterVolumeLevelScalar(out scalar));
255
+ Marshal.ThrowExceptionForHR(endpoint.GetMute(out muted));
256
+ int volume = (int)Math.Round(Math.Max(0.0f, Math.Min(1.0f, scalar)) * 100.0f, MidpointRounding.AwayFromZero);
257
+ return "{\"volume\":" + volume.ToString() + ",\"muted\":" + (muted ? "true" : "false") + "}";
258
+ }
259
+
260
+ public static void SetVolume(int volume) {
261
+ if (volume < 0 || volume > 100) throw new ArgumentOutOfRangeException("volume");
262
+ IAudioEndpointVolume endpoint = Endpoint();
263
+ Guid context = Guid.Empty;
264
+ Marshal.ThrowExceptionForHR(endpoint.SetMasterVolumeLevelScalar(volume / 100.0f, ref context));
265
+ }
266
+
267
+ public static void SetMuted(bool muted) {
268
+ IAudioEndpointVolume endpoint = Endpoint();
269
+ Guid context = Guid.Empty;
270
+ Marshal.ThrowExceptionForHR(endpoint.SetMute(muted, ref context));
271
+ }
272
+ }
273
+ }
274
+ `;
275
+
276
+ export function windowsAudioPowerShell(action = 'status', volume = 50, { compileOnly = false } = {}) {
277
+ const selected = requireEnum(action, 'action', ['status','set_volume','mute','unmute']);
278
+ const normalizedVolume = clamp(volume, 50, 0, 100);
279
+ const mutation = selected === 'set_volume'
280
+ ? `[ReMCP.WindowsAudio]::SetVolume(${normalizedVolume})`
281
+ : selected === 'mute'
282
+ ? '[ReMCP.WindowsAudio]::SetMuted($true)'
283
+ : selected === 'unmute'
284
+ ? '[ReMCP.WindowsAudio]::SetMuted($false)'
285
+ : '';
286
+ return [
287
+ "$ErrorActionPreference='Stop'",
288
+ "$source=@'",
289
+ WINDOWS_AUDIO_CORE_CSHARP,
290
+ "'@",
291
+ 'Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop',
292
+ compileOnly ? "Write-Output 'compiled'" : mutation,
293
+ compileOnly ? '' : '[Console]::Out.WriteLine([ReMCP.WindowsAudio]::GetStateJson())',
294
+ ].filter(Boolean).join('\n');
295
+ }
296
+
186
297
  export async function audioTool(args) {
187
298
  const action = requireEnum(args.action || 'status', 'action', ['status','set_volume','mute','unmute']);
188
299
  const volume = clamp(args.volume, 50, 0, 100);
@@ -196,12 +307,13 @@ export async function audioTool(args) {
196
307
  }
197
308
  if (process.platform === 'win32') {
198
309
  if (!commandExists('powershell.exe') && !process.env.SystemRoot) unavailable('Windows audio control');
199
- if (action === 'status') return text('Windows master-volume status requires the CoreAudio adapter; use set_volume/mute/unmute or install a system audio helper.');
200
- // Windows has no stable built-in CLI for CoreAudio; use media keys for mute/volume changes.
201
- const key = action === 'mute' || action === 'unmute' ? '{VOLUME_MUTE}' : null;
202
- if (key) await runPowerShell(`$w=New-Object -ComObject WScript.Shell;$w.SendKeys('${key}')`, { label:'audio control' });
203
- else throw new Error('set_volume on Windows requires an installed CoreAudio helper; exact absolute volume is not exposed by a stable built-in command');
204
- return text(`Audio action ${action} requested.`);
310
+ const { stdout } = await runPowerShell(windowsAudioPowerShell(action, volume), { label:'Windows CoreAudio control', timeout:30_000 });
311
+ const rendered = stdout.trim();
312
+ try {
313
+ return jsonResult(JSON.parse(rendered));
314
+ } catch {
315
+ throw new Error(`Windows CoreAudio returned an invalid state payload: ${rendered || '(empty)'}`);
316
+ }
205
317
  }
206
318
  if (commandExists('wpctl')) {
207
319
  if (action === 'status') return text((await runFile('wpctl', ['get-volume','@DEFAULT_AUDIO_SINK@'], { label:'audio status' })).stdout.trim());