@remcp/runtime 0.2.152 → 0.2.154
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 +12 -0
- package/package.json +1 -1
- package/src/extended/diagnostics-audio.mjs +237 -0
- package/src/extended/diagnostics-platform.mjs +308 -0
- package/src/extended/diagnostics.mjs +4 -489
- package/src/extended/documents-filesystem.mjs +270 -0
- package/src/extended/documents-reading.mjs +163 -0
- package/src/extended/documents.mjs +3 -386
- package/src/helpers/wayland-eis-helper.py +2 -339
- package/src/helpers/wayland_eis_sender.py +344 -0
- package/src/wayland-remote-desktop-input.mjs +168 -0
- package/src/wayland-remote-desktop-utils.mjs +27 -0
- package/src/wayland-remote-desktop.mjs +11 -180
|
@@ -26,495 +26,10 @@ import {
|
|
|
26
26
|
unavailable,
|
|
27
27
|
} from './common.mjs';
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function macosLaunchctlDomain(scope = 'user', uid = process.getuid?.() ?? 0) {
|
|
36
|
-
const normalized = requireEnum(scope, 'scope', ['user','system']);
|
|
37
|
-
return normalized === 'system' ? 'system' : `gui/${uid}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function serviceTool(args) {
|
|
41
|
-
const action = requireEnum(args.action || 'list', 'action', ['list','status','start','stop','restart']);
|
|
42
|
-
const name = optionalString(args.name);
|
|
43
|
-
if (!['list'].includes(action) && !name) throw new Error('name is required unless action=list');
|
|
44
|
-
const scope = requireEnum(args.scope || 'user', 'scope', ['user','system']);
|
|
45
|
-
const policy = ['list','status'].includes(action) ? null : assertAllowedCommand(`${process.platform === 'win32' ? `${action}-service` : process.platform === 'darwin' ? 'launchctl' : 'systemctl'} ${action} ${name}`);
|
|
46
|
-
|
|
47
|
-
if (process.platform === 'win32') {
|
|
48
|
-
if (action === 'list') {
|
|
49
|
-
const { stdout } = await runPowerShell('Get-Service | Select-Object Name,DisplayName,Status,StartType | ConvertTo-Json -Compress', { label: 'list services' });
|
|
50
|
-
return text(stdout.trim() || '[]');
|
|
51
|
-
}
|
|
52
|
-
const safe = escapePowerShellSingle(name);
|
|
53
|
-
if (action === 'status') {
|
|
54
|
-
const { stdout } = await runPowerShell(`Get-Service -Name '${safe}' | Select-Object Name,DisplayName,Status,StartType | ConvertTo-Json -Compress`, { label: 'service status' });
|
|
55
|
-
return text(stdout.trim());
|
|
56
|
-
}
|
|
57
|
-
const verb = action === 'start' ? 'Start-Service' : action === 'stop' ? 'Stop-Service' : 'Restart-Service';
|
|
58
|
-
await runPowerShell(`${verb} -Name '${safe}' -ErrorAction Stop`, { label: `${action} service`, timeout: 30_000 });
|
|
59
|
-
return text(`${policy?.note ? `${policy.note}\n` : ''}${action} requested for service ${name}.`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (process.platform === 'darwin') {
|
|
63
|
-
const domain = macosLaunchctlDomain(scope);
|
|
64
|
-
if (action === 'list') return text((await runFile('/bin/launchctl', ['print', domain], { label: `launchctl print ${domain}` })).stdout);
|
|
65
|
-
const target = `${domain}/${name}`;
|
|
66
|
-
if (action === 'status') {
|
|
67
|
-
const result = await runFile('/bin/launchctl', ['print', target], { label: 'launchctl print', allowFailure: true });
|
|
68
|
-
if (result.code !== 0) throw new Error(result.stderr.trim() || `Service ${target} was not found`);
|
|
69
|
-
return text(result.stdout);
|
|
70
|
-
}
|
|
71
|
-
if (action === 'restart') await runFile('/bin/launchctl', ['kickstart', '-k', target], { label: 'launchctl kickstart' });
|
|
72
|
-
else if (action === 'start') await runFile('/bin/launchctl', ['kickstart', target], { label: 'launchctl kickstart' });
|
|
73
|
-
else await runFile('/bin/launchctl', ['kill', 'SIGTERM', target], { label: 'launchctl stop' });
|
|
74
|
-
return text(`${policy?.note ? `${policy.note}\n` : ''}${action} requested for ${target}.`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!commandExists('systemctl')) unavailable('Service control', 'systemctl is required on Linux');
|
|
78
|
-
const base = scope === 'user' ? ['--user'] : [];
|
|
79
|
-
if (action === 'list') return text((await runFile('systemctl', [...base, 'list-units', '--type=service', '--all', '--no-pager', '--plain'], { label: 'systemctl list' })).stdout);
|
|
80
|
-
if (action === 'status') {
|
|
81
|
-
const result = await runFile('systemctl', [...base, 'status', name, '--no-pager', '--plain'], { label: 'systemctl status', allowFailure: true });
|
|
82
|
-
const rendered = `${result.stdout}${result.stderr}`.trim();
|
|
83
|
-
return text(rendered, result.code !== 0 && !result.stdout.trim());
|
|
84
|
-
}
|
|
85
|
-
await runFile('systemctl', [...base, action, name], { label: `systemctl ${action}`, timeout: 30_000 });
|
|
86
|
-
return text(`${policy?.note ? `${policy.note}\n` : ''}${action} requested for service ${name}.`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function eventLog(args) {
|
|
90
|
-
const limit = clamp(args.limit, 200, 1, 5000);
|
|
91
|
-
const since = durationSpec(args.since);
|
|
92
|
-
const filter = optionalString(args.filter || args.query);
|
|
93
|
-
if (process.platform === 'win32') {
|
|
94
|
-
const log = escapePowerShellSingle(optionalString(args.log) || 'System');
|
|
95
|
-
const n = Number.parseInt(since, 10), unit = since.at(-1);
|
|
96
|
-
const seconds = n * ({ s:1, m:60, h:3600, d:86400 }[unit] || 60);
|
|
97
|
-
const where = filter ? ` | Where-Object { $_.Message -like '*${escapePowerShellSingle(filter)}*' -or $_.ProviderName -like '*${escapePowerShellSingle(filter)}*' }` : '';
|
|
98
|
-
const script = `Get-WinEvent -FilterHashtable @{LogName='${log}';StartTime=(Get-Date).AddSeconds(-${seconds})} -ErrorAction SilentlyContinue${where} | Select-Object -First ${limit} TimeCreated,Id,LevelDisplayName,ProviderName,Message | ConvertTo-Json -Compress -Depth 3`;
|
|
99
|
-
return text((await runPowerShell(script, { label: 'event log', timeout: 30_000 })).stdout.trim() || '[]');
|
|
100
|
-
}
|
|
101
|
-
if (process.platform === 'darwin') {
|
|
102
|
-
const argv = ['show','--last',since,'--style','ndjson'];
|
|
103
|
-
if (filter) argv.push('--predicate', `eventMessage CONTAINS[c] ${JSON.stringify(filter)}`);
|
|
104
|
-
const { stdout } = await runFileHeadLines('/usr/bin/log', argv, limit, { label: 'macOS unified log', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 });
|
|
105
|
-
return text(stdout);
|
|
106
|
-
}
|
|
107
|
-
if (!commandExists('journalctl')) unavailable('Event logs', 'journalctl is required on Linux');
|
|
108
|
-
const n = Number.parseInt(since, 10), unit = since.at(-1);
|
|
109
|
-
const argv = ['--no-pager','-n',String(limit),'--since',`${n} ${{s:'seconds',m:'minutes',h:'hours',d:'days'}[unit]} ago`,'-o','short-iso'];
|
|
110
|
-
if (filter) argv.push('--grep', filter);
|
|
111
|
-
const result = await runFile('journalctl', argv, { label: 'journalctl', timeout: 30_000, allowFailure: true });
|
|
112
|
-
const rendered = `${result.stdout}${result.stderr}`.trim();
|
|
113
|
-
return text(rendered, result.code !== 0 && !result.stdout.trim());
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function filterMacosTcpListeners(output) {
|
|
117
|
-
const lines = String(output || '').split(/\r?\n/);
|
|
118
|
-
const header = lines.filter(line => /^Proto\s+/i.test(line) || /^Active Internet connections/i.test(line));
|
|
119
|
-
const listeners = lines.filter(line => /\bLISTEN\b/i.test(line));
|
|
120
|
-
return [...header, ...listeners].join('\n').trim();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function connectivityTest(host, port, timeoutMs) {
|
|
124
|
-
return new Promise(resolve => {
|
|
125
|
-
const started = performance.now();
|
|
126
|
-
const socket = net.createConnection({ host, port });
|
|
127
|
-
let settled = false;
|
|
128
|
-
const finish = value => { if (settled) return; settled = true; clearTimeout(timer); socket.destroy(); resolve(value); };
|
|
129
|
-
const timer = setTimeout(() => finish({ ok:false, host, port, error:'timeout', latency_ms:Math.round(performance.now()-started) }), timeoutMs);
|
|
130
|
-
socket.once('connect', () => finish({ ok:true, host, port, latency_ms:Math.round(performance.now()-started) }));
|
|
131
|
-
socket.once('error', error => finish({ ok:false, host, port, error:error.code || error.message, latency_ms:Math.round(performance.now()-started) }));
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function linuxListenerBackend({ commandExistsFn = commandExists } = {}) {
|
|
136
|
-
if (commandExistsFn('ss')) return { command:'ss', args:['-lntup'] };
|
|
137
|
-
if (commandExistsFn('netstat')) return { command:'netstat', args:['-lntu'] };
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const CONTAINER_INTERFACE_PATTERN = /^(?:veth|docker\d*$|br-[0-9a-f]{6,}$)/i;
|
|
142
|
-
|
|
143
|
-
export function compactNetworkSummary({
|
|
144
|
-
hostname = os.hostname(),
|
|
145
|
-
interfaces = os.networkInterfaces(),
|
|
146
|
-
dnsServers = dns.getServers(),
|
|
147
|
-
maxInterfaces = 8,
|
|
148
|
-
maxAddressesPerInterface = 4,
|
|
149
|
-
} = {}) {
|
|
150
|
-
const entries = Object.entries(interfaces || {}).map(([name, rows], index) => {
|
|
151
|
-
const normalizedRows = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
152
|
-
const hasExternal = normalizedRows.some(row => row?.internal !== true);
|
|
153
|
-
const rank = hasExternal
|
|
154
|
-
? (CONTAINER_INTERFACE_PATTERN.test(name) ? 1 : 0)
|
|
155
|
-
: 2;
|
|
156
|
-
return { name, rows:normalizedRows, index, rank };
|
|
157
|
-
});
|
|
158
|
-
entries.sort((left, right) => left.rank - right.rank || left.index - right.index);
|
|
159
|
-
|
|
160
|
-
const interfaceLimit = clamp(maxInterfaces, 8, 1, 64);
|
|
161
|
-
const addressLimit = clamp(maxAddressesPerInterface, 4, 1, 16);
|
|
162
|
-
const selected = entries.slice(0, interfaceLimit);
|
|
163
|
-
const compact = {};
|
|
164
|
-
let addressCount = 0;
|
|
165
|
-
let addressCountReturned = 0;
|
|
166
|
-
let addressesTruncated = false;
|
|
167
|
-
|
|
168
|
-
for (const entry of entries) addressCount += entry.rows.length;
|
|
169
|
-
for (const entry of selected) {
|
|
170
|
-
const rows = entry.rows.slice(0, addressLimit);
|
|
171
|
-
compact[entry.name] = rows;
|
|
172
|
-
addressCountReturned += rows.length;
|
|
173
|
-
if (entry.rows.length > rows.length) addressesTruncated = true;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
hostname,
|
|
178
|
-
dns:Array.isArray(dnsServers) ? dnsServers : [],
|
|
179
|
-
interfaces:compact,
|
|
180
|
-
interface_count:entries.length,
|
|
181
|
-
interface_count_returned:selected.length,
|
|
182
|
-
address_count:addressCount,
|
|
183
|
-
address_count_returned:addressCountReturned,
|
|
184
|
-
interfaces_truncated:entries.length > selected.length,
|
|
185
|
-
addresses_truncated:addressesTruncated,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export async function networkTool(args) {
|
|
190
|
-
const action = requireEnum(args.action || 'summary', 'action', ['summary','interfaces','dns','routes','listeners','test']);
|
|
191
|
-
if (action === 'interfaces') return jsonResult(os.networkInterfaces());
|
|
192
|
-
if (action === 'dns') return jsonResult({ servers:dns.getServers(), hostname:os.hostname() });
|
|
193
|
-
if (action === 'summary') return jsonResult(compactNetworkSummary());
|
|
194
|
-
if (action === 'test') {
|
|
195
|
-
const host = optionalString(args.host); if (!host) throw new Error('host is required for action=test');
|
|
196
|
-
return jsonResult(await connectivityTest(host, clamp(args.port, 443, 1, 65535), clamp(args.timeout_ms, 3000, 100, 30_000)));
|
|
197
|
-
}
|
|
198
|
-
if (process.platform === 'win32') {
|
|
199
|
-
const script = action === 'routes'
|
|
200
|
-
? 'Get-NetRoute | Select-Object DestinationPrefix,NextHop,RouteMetric,InterfaceAlias,AddressFamily | ConvertTo-Json -Compress'
|
|
201
|
-
: 'Get-NetTCPConnection -State Listen | Select-Object LocalAddress,LocalPort,OwningProcess,State | Sort-Object LocalPort | ConvertTo-Json -Compress';
|
|
202
|
-
return text((await runPowerShell(script, { label:`network ${action}` })).stdout.trim() || '[]');
|
|
203
|
-
}
|
|
204
|
-
if (process.platform === 'darwin' && action === 'routes') {
|
|
205
|
-
return text((await runFile('/usr/sbin/netstat', ['-rn'], { label:'routes' })).stdout);
|
|
206
|
-
}
|
|
207
|
-
if (process.platform === 'darwin' && action === 'listeners') {
|
|
208
|
-
const { stdout } = await runFile('/usr/sbin/netstat', ['-an', '-p', 'tcp'], { label:'listeners' });
|
|
209
|
-
return text(filterMacosTcpListeners(stdout));
|
|
210
|
-
}
|
|
211
|
-
if (action === 'routes') {
|
|
212
|
-
if (commandExists('ip')) return text((await runFile('ip', ['route','show'], { label:'routes' })).stdout);
|
|
213
|
-
if (commandExists('route')) return text((await runFile('route', ['-n'], { label:'routes' })).stdout);
|
|
214
|
-
unavailable('Route inventory', 'ip or route is required');
|
|
215
|
-
}
|
|
216
|
-
const listenerBackend = linuxListenerBackend();
|
|
217
|
-
if (listenerBackend) return text((await runFile(listenerBackend.command, listenerBackend.args, { label:'listeners', allowFailure:true })).stdout);
|
|
218
|
-
unavailable('Listener inventory', 'ss or netstat is required');
|
|
219
|
-
}
|
|
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
|
-
|
|
281
|
-
export async function installedApps(args) {
|
|
282
|
-
const limit = clamp(args.limit, 1000, 1, 10_000);
|
|
283
|
-
const filter = optionalString(args.filter);
|
|
284
|
-
if (process.platform === 'win32') {
|
|
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 }));
|
|
293
|
-
}
|
|
294
|
-
if (process.platform === 'darwin') {
|
|
295
|
-
const parsed = JSON.parse((await runFile('/usr/sbin/system_profiler', ['SPApplicationsDataType','-json'], { label:'installed apps', timeout:60_000, maxBuffer:64*1024*1024 })).stdout);
|
|
296
|
-
return jsonResult(installedAppsPayload(parsed.SPApplicationsDataType || [], { backend:'system_profiler', filter, limit }));
|
|
297
|
-
}
|
|
298
|
-
if (commandExists('dpkg-query')) {
|
|
299
|
-
const { stdout } = await runFile('dpkg-query', ['-W','-f=${binary:Package}\t${Version}\t${Maintainer}\n'], { label:'installed apps', maxBuffer:32*1024*1024 });
|
|
300
|
-
return jsonResult(installedAppsPayload(parseInstalledAppsTsv(stdout), { backend:'dpkg-query', filter, limit }));
|
|
301
|
-
}
|
|
302
|
-
if (commandExists('rpm')) {
|
|
303
|
-
const { stdout } = await runFile('rpm', ['-qa','--qf','%{NAME}\t%{VERSION}-%{RELEASE}\t%{VENDOR}\n'], { label:'installed apps', maxBuffer:32*1024*1024 });
|
|
304
|
-
return jsonResult(installedAppsPayload(parseInstalledAppsTsv(stdout), { backend:'rpm', filter, limit }));
|
|
305
|
-
}
|
|
306
|
-
unavailable('Installed application inventory', 'dpkg-query or rpm is required');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export async function environmentTool(args) {
|
|
310
|
-
return jsonResult({
|
|
311
|
-
platform:process.platform,
|
|
312
|
-
arch:process.arch,
|
|
313
|
-
node:process.versions.node,
|
|
314
|
-
cwd:process.cwd(),
|
|
315
|
-
home:os.homedir(),
|
|
316
|
-
temp:os.tmpdir(),
|
|
317
|
-
shell:process.platform === 'win32' ? process.env.ComSpec || null : process.env.SHELL || null,
|
|
318
|
-
path_entries:String(process.env.PATH || '').split(path.delimiter).filter(Boolean),
|
|
319
|
-
...(args.include_env === true ? { environment:safeEnvironment() } : {}),
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const WINDOWS_AUDIO_CORE_CSHARP = String.raw`
|
|
324
|
-
using System;
|
|
325
|
-
using System.Runtime.InteropServices;
|
|
326
|
-
|
|
327
|
-
namespace ReMCP {
|
|
328
|
-
enum EDataFlow { eRender = 0, eCapture = 1, eAll = 2 }
|
|
329
|
-
enum ERole { eConsole = 0, eMultimedia = 1, eCommunications = 2 }
|
|
330
|
-
|
|
331
|
-
[ComImport]
|
|
332
|
-
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
|
333
|
-
class MMDeviceEnumeratorComObject { }
|
|
334
|
-
|
|
335
|
-
[ComImport]
|
|
336
|
-
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
|
|
337
|
-
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
338
|
-
interface IMMDeviceEnumerator {
|
|
339
|
-
[PreserveSig] int EnumAudioEndpoints(EDataFlow dataFlow, uint stateMask, out IntPtr devices);
|
|
340
|
-
[PreserveSig] int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
[ComImport]
|
|
344
|
-
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
|
|
345
|
-
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
346
|
-
interface IMMDevice {
|
|
347
|
-
[PreserveSig] int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object instance);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
[ComImport]
|
|
351
|
-
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A")]
|
|
352
|
-
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
353
|
-
interface IAudioEndpointVolume {
|
|
354
|
-
[PreserveSig] int RegisterControlChangeNotify(IntPtr notify);
|
|
355
|
-
[PreserveSig] int UnregisterControlChangeNotify(IntPtr notify);
|
|
356
|
-
[PreserveSig] int GetChannelCount(out uint count);
|
|
357
|
-
[PreserveSig] int SetMasterVolumeLevel(float levelDb, ref Guid eventContext);
|
|
358
|
-
[PreserveSig] int SetMasterVolumeLevelScalar(float level, ref Guid eventContext);
|
|
359
|
-
[PreserveSig] int GetMasterVolumeLevel(out float levelDb);
|
|
360
|
-
[PreserveSig] int GetMasterVolumeLevelScalar(out float level);
|
|
361
|
-
[PreserveSig] int SetChannelVolumeLevel(uint channel, float levelDb, ref Guid eventContext);
|
|
362
|
-
[PreserveSig] int SetChannelVolumeLevelScalar(uint channel, float level, ref Guid eventContext);
|
|
363
|
-
[PreserveSig] int GetChannelVolumeLevel(uint channel, out float levelDb);
|
|
364
|
-
[PreserveSig] int GetChannelVolumeLevelScalar(uint channel, out float level);
|
|
365
|
-
[PreserveSig] int SetMute([MarshalAs(UnmanagedType.Bool)] bool muted, ref Guid eventContext);
|
|
366
|
-
[PreserveSig] int GetMute([MarshalAs(UnmanagedType.Bool)] out bool muted);
|
|
367
|
-
[PreserveSig] int GetVolumeStepInfo(out uint step, out uint stepCount);
|
|
368
|
-
[PreserveSig] int VolumeStepUp(ref Guid eventContext);
|
|
369
|
-
[PreserveSig] int VolumeStepDown(ref Guid eventContext);
|
|
370
|
-
[PreserveSig] int QueryHardwareSupport(out uint mask);
|
|
371
|
-
[PreserveSig] int GetVolumeRange(out float minDb, out float maxDb, out float incrementDb);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
public static class WindowsAudio {
|
|
375
|
-
const uint CLSCTX_ALL = 23;
|
|
376
|
-
|
|
377
|
-
static IAudioEndpointVolume Endpoint() {
|
|
378
|
-
IMMDeviceEnumerator enumerator = (IMMDeviceEnumerator)(new MMDeviceEnumeratorComObject());
|
|
379
|
-
IMMDevice device;
|
|
380
|
-
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out device));
|
|
381
|
-
Guid iid = typeof(IAudioEndpointVolume).GUID;
|
|
382
|
-
object endpoint;
|
|
383
|
-
Marshal.ThrowExceptionForHR(device.Activate(ref iid, CLSCTX_ALL, IntPtr.Zero, out endpoint));
|
|
384
|
-
return (IAudioEndpointVolume)endpoint;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
public static string GetStateJson() {
|
|
388
|
-
IAudioEndpointVolume endpoint = Endpoint();
|
|
389
|
-
float scalar;
|
|
390
|
-
bool muted;
|
|
391
|
-
Marshal.ThrowExceptionForHR(endpoint.GetMasterVolumeLevelScalar(out scalar));
|
|
392
|
-
Marshal.ThrowExceptionForHR(endpoint.GetMute(out muted));
|
|
393
|
-
int volume = (int)Math.Round(Math.Max(0.0f, Math.Min(1.0f, scalar)) * 100.0f, MidpointRounding.AwayFromZero);
|
|
394
|
-
return "{\"volume\":" + volume.ToString() + ",\"muted\":" + (muted ? "true" : "false") + "}";
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
public static void SetVolume(int volume) {
|
|
398
|
-
if (volume < 0 || volume > 100) throw new ArgumentOutOfRangeException("volume");
|
|
399
|
-
IAudioEndpointVolume endpoint = Endpoint();
|
|
400
|
-
Guid context = Guid.Empty;
|
|
401
|
-
Marshal.ThrowExceptionForHR(endpoint.SetMasterVolumeLevelScalar(volume / 100.0f, ref context));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
public static void SetMuted(bool muted) {
|
|
405
|
-
IAudioEndpointVolume endpoint = Endpoint();
|
|
406
|
-
Guid context = Guid.Empty;
|
|
407
|
-
Marshal.ThrowExceptionForHR(endpoint.SetMute(muted, ref context));
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
`;
|
|
412
|
-
|
|
413
|
-
export function windowsAudioPowerShell(action = 'status', volume = 50, { compileOnly = false } = {}) {
|
|
414
|
-
const selected = requireEnum(action, 'action', ['status','set_volume','mute','unmute']);
|
|
415
|
-
const normalizedVolume = clamp(volume, 50, 0, 100);
|
|
416
|
-
const mutation = selected === 'set_volume'
|
|
417
|
-
? `[ReMCP.WindowsAudio]::SetVolume(${normalizedVolume})`
|
|
418
|
-
: selected === 'mute'
|
|
419
|
-
? '[ReMCP.WindowsAudio]::SetMuted($true)'
|
|
420
|
-
: selected === 'unmute'
|
|
421
|
-
? '[ReMCP.WindowsAudio]::SetMuted($false)'
|
|
422
|
-
: '';
|
|
423
|
-
return [
|
|
424
|
-
"$ErrorActionPreference='Stop'",
|
|
425
|
-
"$source=@'",
|
|
426
|
-
WINDOWS_AUDIO_CORE_CSHARP,
|
|
427
|
-
"'@",
|
|
428
|
-
'Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop',
|
|
429
|
-
compileOnly ? "Write-Output 'compiled'" : mutation,
|
|
430
|
-
compileOnly ? '' : '[Console]::Out.WriteLine([ReMCP.WindowsAudio]::GetStateJson())',
|
|
431
|
-
].filter(Boolean).join('\n');
|
|
432
|
-
}
|
|
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
|
-
|
|
477
|
-
export async function audioTool(args) {
|
|
478
|
-
const action = requireEnum(args.action || 'status', 'action', ['status','set_volume','mute','unmute']);
|
|
479
|
-
const volume = clamp(args.volume, 50, 0, 100);
|
|
480
|
-
if (process.platform === 'darwin') {
|
|
481
|
-
const readState = async () => {
|
|
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' });
|
|
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' });
|
|
488
|
-
}
|
|
489
|
-
return jsonResult({ ...(await readState()), action, backend:'osascript' });
|
|
490
|
-
}
|
|
491
|
-
if (process.platform === 'win32') {
|
|
492
|
-
if (!commandExists('powershell.exe') && !process.env.SystemRoot) unavailable('Windows audio control');
|
|
493
|
-
const { stdout } = await runPowerShell(windowsAudioPowerShell(action, volume), { label:'Windows CoreAudio control', timeout:30_000 });
|
|
494
|
-
const rendered = stdout.trim();
|
|
495
|
-
try {
|
|
496
|
-
return jsonResult({ ...JSON.parse(rendered), action, backend:'coreaudio' });
|
|
497
|
-
} catch {
|
|
498
|
-
throw new Error(`Windows CoreAudio returned an invalid state payload: ${rendered || '(empty)'}`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
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
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return jsonResult({ ...(await linuxAudioStatus(backend)), action, backend });
|
|
517
|
-
}
|
|
29
|
+
import { serviceTool, eventLog, networkTool, installedApps } from './diagnostics-platform.mjs';
|
|
30
|
+
import { environmentTool, audioTool } from './diagnostics-audio.mjs';
|
|
31
|
+
export * from './diagnostics-platform.mjs';
|
|
32
|
+
export * from './diagnostics-audio.mjs';
|
|
518
33
|
|
|
519
34
|
export const POWER_ACTION_MAX_DELAY_SECONDS = 90;
|
|
520
35
|
|