@remcp/runtime 0.2.134 → 0.2.136
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 +13 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/extended/catalog.mjs +14 -2
- package/src/extended/diagnostics.mjs +137 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.136 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
- `installed_apps` now normalizes Windows registry, macOS `system_profiler`, `dpkg-query`, and `rpm` inventories into one structured `data[]` contract with stable app name/version/publisher/path fields.
|
|
6
|
+
- Stable top-level `backend`, `count`, `returned`, and `truncated` metadata make result limits explicit and remove platform-specific text parsing from callers.
|
|
7
|
+
- Name filtering and de-duplication are shared across platforms. Windows no longer interpolates the filter into PowerShell.
|
|
8
|
+
- Public release CI exercises the live native inventory on Linux, macOS, and Windows. Tool names remain unchanged at 83; the output-schema digest changes intentionally.
|
|
9
|
+
|
|
10
|
+
## 0.2.135 — 2026-10-02
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- Windows Core Audio and macOS audio responses expose the same `action`/`backend` fields.
|
|
14
|
+
- The additive `backend` output-schema field changes the tool-surface digest without changing tool names or the 83-tool count.
|
|
15
|
+
|
|
3
16
|
## 0.2.134 — 2026-10-02
|
|
4
17
|
|
|
5
18
|
- `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
|
@@ -212,9 +212,21 @@ const OUTPUT_FIELDS = Object.freeze({
|
|
|
212
212
|
service: { data:outputArray(outputObject('Service record.')), action:outputString(), name:outputString(), text:outputString() },
|
|
213
213
|
event_log: { text:outputString('Bounded log output.'), data:outputArray(outputObject()) },
|
|
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
|
-
installed_apps: {
|
|
215
|
+
installed_apps: {
|
|
216
|
+
data:outputArray(outputRecord({
|
|
217
|
+
name:outputString('Installed application/package name.'),
|
|
218
|
+
version:{ type:['string','null'], description:'Installed version when available.' },
|
|
219
|
+
publisher:{ type:['string','null'], description:'Publisher/maintainer/vendor when available.' },
|
|
220
|
+
path:{ type:['string','null'], description:'Installation path when available.' },
|
|
221
|
+
signed_by:outputString('Optional macOS signing identity summary.'),
|
|
222
|
+
}, 'Normalized installed app/package record.')),
|
|
223
|
+
backend:outputString('Native inventory backend used on this platform.'),
|
|
224
|
+
count:outputNumber('Total matching records before the requested limit.'),
|
|
225
|
+
returned:outputNumber('Number of records returned in data.'),
|
|
226
|
+
truncated:outputBoolean('True when count is larger than returned because of the requested limit.'),
|
|
227
|
+
},
|
|
216
228
|
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() },
|
|
229
|
+
audio: { volume:outputNumber(), muted:outputBoolean(), action:outputString(), backend:outputString(), text:outputString() },
|
|
218
230
|
power_action: { text:outputString(), action:outputString() },
|
|
219
231
|
record_screen: { path:outputString(), bytes:outputNumber(), duration_seconds:outputNumber(), format:outputString() },
|
|
220
232
|
read_document: { text:outputString(), path:outputString(), data:{}, sheets:outputArray(outputObject()), sheet:outputString(), rows:outputArray({}), cells:outputArray({}) },
|
|
@@ -218,26 +218,90 @@ export async function networkTool(args) {
|
|
|
218
218
|
unavailable('Listener inventory', 'ss or netstat is required');
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
function installedAppString(value) {
|
|
222
|
+
const normalized = value == null ? '' : String(value).trim();
|
|
223
|
+
return normalized || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function normalizeInstalledApp(row = {}) {
|
|
227
|
+
const name = installedAppString(row.name ?? row.DisplayName ?? row._name);
|
|
228
|
+
if (!name) return null;
|
|
229
|
+
const signedBy = installedAppString(row.signed_by);
|
|
230
|
+
return {
|
|
231
|
+
name,
|
|
232
|
+
version:installedAppString(row.version ?? row.DisplayVersion),
|
|
233
|
+
publisher:installedAppString(row.publisher ?? row.Publisher),
|
|
234
|
+
path:installedAppString(row.path ?? row.InstallLocation),
|
|
235
|
+
...(signedBy ? { signed_by:signedBy } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function parseInstalledAppsTsv(output) {
|
|
240
|
+
return String(output || '').split(/\r?\n/).filter(Boolean).map(line => {
|
|
241
|
+
const [name = '', version = '', publisher = '', pathValue = ''] = line.split('\t');
|
|
242
|
+
return normalizeInstalledApp({ name, version, publisher, path:pathValue });
|
|
243
|
+
}).filter(Boolean);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function installedAppsPayload(rows, { backend, filter = null, limit = 1000 } = {}) {
|
|
247
|
+
const wanted = optionalString(filter)?.toLowerCase() || null;
|
|
248
|
+
const boundedLimit = clamp(limit, 1000, 1, 10_000);
|
|
249
|
+
const seen = new Set();
|
|
250
|
+
const normalized = [];
|
|
251
|
+
for (const raw of Array.isArray(rows) ? rows : (rows ? [rows] : [])) {
|
|
252
|
+
const app = normalizeInstalledApp(raw);
|
|
253
|
+
if (!app) continue;
|
|
254
|
+
if (wanted && !app.name.toLowerCase().includes(wanted)) continue;
|
|
255
|
+
const key = [app.name.toLowerCase(), app.version || '', app.path || ''].join('\u0000');
|
|
256
|
+
if (seen.has(key)) continue;
|
|
257
|
+
seen.add(key);
|
|
258
|
+
normalized.push(app);
|
|
259
|
+
}
|
|
260
|
+
normalized.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity:'base' })
|
|
261
|
+
|| String(left.version || '').localeCompare(String(right.version || ''), undefined, { sensitivity:'base' }));
|
|
262
|
+
const data = normalized.slice(0, boundedLimit);
|
|
263
|
+
return {
|
|
264
|
+
data,
|
|
265
|
+
backend:String(backend || 'unknown'),
|
|
266
|
+
count:normalized.length,
|
|
267
|
+
returned:data.length,
|
|
268
|
+
truncated:normalized.length > data.length,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function windowsInstalledAppsPowerShell() {
|
|
273
|
+
return [
|
|
274
|
+
"$ErrorActionPreference='Stop'",
|
|
275
|
+
"$paths=@('HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*')",
|
|
276
|
+
"$items=foreach($path in $paths){Get-ItemProperty -Path $path -ErrorAction SilentlyContinue}",
|
|
277
|
+
"$items|Where-Object{$_.DisplayName}|ForEach-Object{[PSCustomObject]@{name=[string]$_.DisplayName;version=[string]$_.DisplayVersion;publisher=[string]$_.Publisher;path=[string]$_.InstallLocation}}|ConvertTo-Json -Compress",
|
|
278
|
+
].join(';');
|
|
279
|
+
}
|
|
280
|
+
|
|
221
281
|
export async function installedApps(args) {
|
|
222
282
|
const limit = clamp(args.limit, 1000, 1, 10_000);
|
|
223
283
|
const filter = optionalString(args.filter);
|
|
224
284
|
if (process.platform === 'win32') {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
285
|
+
const { stdout } = await runPowerShell(windowsInstalledAppsPowerShell(), { label:'installed apps', timeout:30_000, maxBuffer:32*1024*1024 });
|
|
286
|
+
let parsed;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(stdout.trim() || '[]');
|
|
289
|
+
} catch {
|
|
290
|
+
throw new Error('Windows installed-app inventory returned invalid JSON');
|
|
291
|
+
}
|
|
292
|
+
return jsonResult(installedAppsPayload(parsed, { backend:'windows-registry', filter, limit }));
|
|
228
293
|
}
|
|
229
294
|
if (process.platform === 'darwin') {
|
|
230
295
|
const parsed = JSON.parse((await runFile('/usr/sbin/system_profiler', ['SPApplicationsDataType','-json'], { label:'installed apps', timeout:60_000, maxBuffer:64*1024*1024 })).stdout);
|
|
231
|
-
|
|
232
|
-
return jsonResult(apps.map(app => ({ name:app._name, version:app.version || null, path:app.path || null, signed_by:app.signed_by || null })));
|
|
296
|
+
return jsonResult(installedAppsPayload(parsed.SPApplicationsDataType || [], { backend:'system_profiler', filter, limit }));
|
|
233
297
|
}
|
|
234
298
|
if (commandExists('dpkg-query')) {
|
|
235
299
|
const { stdout } = await runFile('dpkg-query', ['-W','-f=${binary:Package}\t${Version}\t${Maintainer}\n'], { label:'installed apps', maxBuffer:32*1024*1024 });
|
|
236
|
-
return
|
|
300
|
+
return jsonResult(installedAppsPayload(parseInstalledAppsTsv(stdout), { backend:'dpkg-query', filter, limit }));
|
|
237
301
|
}
|
|
238
302
|
if (commandExists('rpm')) {
|
|
239
303
|
const { stdout } = await runFile('rpm', ['-qa','--qf','%{NAME}\t%{VERSION}-%{RELEASE}\t%{VENDOR}\n'], { label:'installed apps', maxBuffer:32*1024*1024 });
|
|
240
|
-
return
|
|
304
|
+
return jsonResult(installedAppsPayload(parseInstalledAppsTsv(stdout), { backend:'rpm', filter, limit }));
|
|
241
305
|
}
|
|
242
306
|
unavailable('Installed application inventory', 'dpkg-query or rpm is required');
|
|
243
307
|
}
|
|
@@ -367,45 +431,89 @@ export function windowsAudioPowerShell(action = 'status', volume = 50, { compile
|
|
|
367
431
|
].filter(Boolean).join('\n');
|
|
368
432
|
}
|
|
369
433
|
|
|
434
|
+
export function parseWpctlAudioStatus(output) {
|
|
435
|
+
const match = String(output || '').match(/\bVolume:\s*([0-9]+(?:\.[0-9]+)?)(?:\s+\[(MUTED)\])?/i);
|
|
436
|
+
if (!match) throw new Error('wpctl returned an unrecognized volume status');
|
|
437
|
+
const scalar = Number(match[1]);
|
|
438
|
+
if (!Number.isFinite(scalar)) throw new Error('wpctl returned a non-numeric volume');
|
|
439
|
+
return { volume:Math.round(Math.max(0, Math.min(1, scalar)) * 100), muted:Boolean(match[2]) };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function parsePactlAudioStatus(volumeOutput, muteOutput) {
|
|
443
|
+
const volumeMatch = String(volumeOutput || '').match(/\b(\d{1,3})%/);
|
|
444
|
+
const muteMatch = String(muteOutput || '').match(/\bMute:\s*(yes|no)\b/i);
|
|
445
|
+
if (!volumeMatch || !muteMatch) throw new Error('pactl returned an unrecognized volume/mute status');
|
|
446
|
+
return {
|
|
447
|
+
volume:Math.max(0, Math.min(100, Number(volumeMatch[1]))),
|
|
448
|
+
muted:muteMatch[1].toLowerCase() === 'yes',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function parseAmixerAudioStatus(output) {
|
|
453
|
+
const rendered = String(output || '');
|
|
454
|
+
const volumeMatch = rendered.match(/\[(\d{1,3})%\]/);
|
|
455
|
+
const states = [...rendered.matchAll(/\[(on|off)\]/gi)].map(match => match[1].toLowerCase());
|
|
456
|
+
if (!volumeMatch || !states.length) throw new Error('amixer returned an unrecognized Master status');
|
|
457
|
+
return {
|
|
458
|
+
volume:Math.max(0, Math.min(100, Number(volumeMatch[1]))),
|
|
459
|
+
muted:states.every(state => state === 'off'),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function linuxAudioStatus(backend) {
|
|
464
|
+
if (backend === 'wpctl') {
|
|
465
|
+
const { stdout } = await runFile('wpctl', ['get-volume','@DEFAULT_AUDIO_SINK@'], { label:'audio status' });
|
|
466
|
+
return parseWpctlAudioStatus(stdout);
|
|
467
|
+
}
|
|
468
|
+
if (backend === 'pactl') {
|
|
469
|
+
const volumeResult = await runFile('pactl', ['get-sink-volume','@DEFAULT_SINK@'], { label:'audio status' });
|
|
470
|
+
const muteResult = await runFile('pactl', ['get-sink-mute','@DEFAULT_SINK@'], { label:'audio status' });
|
|
471
|
+
return parsePactlAudioStatus(volumeResult.stdout, muteResult.stdout);
|
|
472
|
+
}
|
|
473
|
+
const { stdout } = await runFile('amixer', ['get','Master'], { label:'audio status' });
|
|
474
|
+
return parseAmixerAudioStatus(stdout);
|
|
475
|
+
}
|
|
476
|
+
|
|
370
477
|
export async function audioTool(args) {
|
|
371
478
|
const action = requireEnum(args.action || 'status', 'action', ['status','set_volume','mute','unmute']);
|
|
372
479
|
const volume = clamp(args.volume, 50, 0, 100);
|
|
373
480
|
if (process.platform === 'darwin') {
|
|
374
|
-
|
|
481
|
+
const readState = async () => {
|
|
375
482
|
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');
|
|
483
|
+
const [v,m] = stdout.trim().split('\t');
|
|
484
|
+
return { volume:Number(v), muted:m === 'true' };
|
|
485
|
+
};
|
|
486
|
+
if (action !== 'status') {
|
|
487
|
+
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
488
|
}
|
|
378
|
-
|
|
379
|
-
return audioTool({ action:'status' });
|
|
489
|
+
return jsonResult({ ...(await readState()), action, backend:'osascript' });
|
|
380
490
|
}
|
|
381
491
|
if (process.platform === 'win32') {
|
|
382
492
|
if (!commandExists('powershell.exe') && !process.env.SystemRoot) unavailable('Windows audio control');
|
|
383
493
|
const { stdout } = await runPowerShell(windowsAudioPowerShell(action, volume), { label:'Windows CoreAudio control', timeout:30_000 });
|
|
384
494
|
const rendered = stdout.trim();
|
|
385
495
|
try {
|
|
386
|
-
return jsonResult(JSON.parse(rendered));
|
|
496
|
+
return jsonResult({ ...JSON.parse(rendered), action, backend:'coreaudio' });
|
|
387
497
|
} catch {
|
|
388
498
|
throw new Error(`Windows CoreAudio returned an invalid state payload: ${rendered || '(empty)'}`);
|
|
389
499
|
}
|
|
390
500
|
}
|
|
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.`);
|
|
501
|
+
|
|
502
|
+
const backend = commandExists('wpctl') ? 'wpctl' : commandExists('pactl') ? 'pactl' : commandExists('amixer') ? 'amixer' : null;
|
|
503
|
+
if (!backend) unavailable('Audio control', 'wpctl, pactl or amixer is required on Linux');
|
|
504
|
+
|
|
505
|
+
if (action !== 'status') {
|
|
506
|
+
if (backend === 'wpctl') {
|
|
507
|
+
if (action === 'set_volume') await runFile('wpctl', ['set-volume','@DEFAULT_AUDIO_SINK@',`${volume}%`], { label:'audio control' });
|
|
508
|
+
else await runFile('wpctl', ['set-mute','@DEFAULT_AUDIO_SINK@',action === 'mute' ? '1' : '0'], { label:'audio control' });
|
|
509
|
+
} else if (backend === 'pactl') {
|
|
510
|
+
if (action === 'set_volume') await runFile('pactl', ['set-sink-volume','@DEFAULT_SINK@',`${volume}%`], { label:'audio control' });
|
|
511
|
+
else await runFile('pactl', ['set-sink-mute','@DEFAULT_SINK@',action === 'mute' ? '1' : '0'], { label:'audio control' });
|
|
512
|
+
} else {
|
|
513
|
+
await runFile('amixer', action === 'set_volume' ? ['set','Master',`${volume}%`] : ['set','Master',action === 'mute' ? 'mute' : 'unmute'], { label:'audio control' });
|
|
514
|
+
}
|
|
407
515
|
}
|
|
408
|
-
|
|
516
|
+
return jsonResult({ ...(await linuxAudioStatus(backend)), action, backend });
|
|
409
517
|
}
|
|
410
518
|
|
|
411
519
|
export const POWER_ACTION_MAX_DELAY_SECONDS = 90;
|