@remcp/runtime 0.2.47 → 0.2.48
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 +15 -0
- package/README.md +42 -25
- package/package.json +6 -2
- package/src/catalog.mjs +1 -1
- package/src/extended/browser.mjs +592 -0
- package/src/extended/catalog.mjs +564 -0
- package/src/extended/common.mjs +209 -0
- package/src/extended/desktop-linux.mjs +1173 -0
- package/src/extended/desktop-macos.mjs +408 -0
- package/src/extended/desktop-windows.mjs +546 -0
- package/src/extended/desktop.mjs +1313 -0
- package/src/extended/diagnostics.mjs +274 -0
- package/src/extended/documents.mjs +381 -0
- package/src/extended/ocr.mjs +139 -0
- package/src/helpers/wayland-eis-helper.py +450 -0
- package/src/index.mjs +46 -44
- package/src/invoke.mjs +3 -3
- package/src/mcp-v2.mjs +104 -0
- package/src/permissions.mjs +1 -1
- package/src/util.mjs +24 -1
- package/src/wayland-remote-desktop.mjs +654 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import dns from 'node:dns';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { stat } from 'node:fs/promises';
|
|
7
|
+
import { assertAllowedCommand } from '../policy.mjs';
|
|
8
|
+
import { resolveSafePath, text } from '../util.mjs';
|
|
9
|
+
import {
|
|
10
|
+
clamp,
|
|
11
|
+
commandExists,
|
|
12
|
+
escapePowerShellSingle,
|
|
13
|
+
jsonResult,
|
|
14
|
+
optionalString,
|
|
15
|
+
requireEnum,
|
|
16
|
+
runFile,
|
|
17
|
+
runOsa,
|
|
18
|
+
runPowerShell,
|
|
19
|
+
safeEnvironment,
|
|
20
|
+
spawnDetached,
|
|
21
|
+
unavailable,
|
|
22
|
+
} from './common.mjs';
|
|
23
|
+
|
|
24
|
+
function durationSpec(value) {
|
|
25
|
+
const raw = optionalString(value) || '10m';
|
|
26
|
+
if (!/^\d+(?:s|m|h|d)$/.test(raw)) throw new Error('since must look like 30s, 10m, 2h, or 1d');
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function serviceTool(args) {
|
|
31
|
+
const action = requireEnum(args.action || 'list', 'action', ['list','status','start','stop','restart']);
|
|
32
|
+
const name = optionalString(args.name);
|
|
33
|
+
if (!['list'].includes(action) && !name) throw new Error('name is required unless action=list');
|
|
34
|
+
const scope = requireEnum(args.scope || 'user', 'scope', ['user','system']);
|
|
35
|
+
const policy = ['list','status'].includes(action) ? null : assertAllowedCommand(`${process.platform === 'win32' ? `${action}-service` : process.platform === 'darwin' ? 'launchctl' : 'systemctl'} ${action} ${name}`);
|
|
36
|
+
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
if (action === 'list') {
|
|
39
|
+
const { stdout } = await runPowerShell('Get-Service | Select-Object Name,DisplayName,Status,StartType | ConvertTo-Json -Compress', { label: 'list services' });
|
|
40
|
+
return text(stdout.trim() || '[]');
|
|
41
|
+
}
|
|
42
|
+
const safe = escapePowerShellSingle(name);
|
|
43
|
+
if (action === 'status') {
|
|
44
|
+
const { stdout } = await runPowerShell(`Get-Service -Name '${safe}' | Select-Object Name,DisplayName,Status,StartType | ConvertTo-Json -Compress`, { label: 'service status' });
|
|
45
|
+
return text(stdout.trim());
|
|
46
|
+
}
|
|
47
|
+
const verb = action === 'start' ? 'Start-Service' : action === 'stop' ? 'Stop-Service' : 'Restart-Service';
|
|
48
|
+
await runPowerShell(`${verb} -Name '${safe}' -ErrorAction Stop`, { label: `${action} service`, timeout: 30_000 });
|
|
49
|
+
return text(`${policy?.note ? `${policy.note}\n` : ''}${action} requested for service ${name}.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (process.platform === 'darwin') {
|
|
53
|
+
if (action === 'list') return text((await runFile('/bin/launchctl', ['list'], { label: 'launchctl list' })).stdout);
|
|
54
|
+
const domain = scope === 'system' ? 'system' : `gui/${process.getuid?.() ?? 0}`;
|
|
55
|
+
const target = `${domain}/${name}`;
|
|
56
|
+
if (action === 'status') {
|
|
57
|
+
const result = await runFile('/bin/launchctl', ['print', target], { label: 'launchctl print', allowFailure: true });
|
|
58
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || `Service ${target} was not found`);
|
|
59
|
+
return text(result.stdout);
|
|
60
|
+
}
|
|
61
|
+
if (action === 'restart') await runFile('/bin/launchctl', ['kickstart', '-k', target], { label: 'launchctl kickstart' });
|
|
62
|
+
else if (action === 'start') await runFile('/bin/launchctl', ['kickstart', target], { label: 'launchctl kickstart' });
|
|
63
|
+
else await runFile('/bin/launchctl', ['kill', 'SIGTERM', target], { label: 'launchctl stop' });
|
|
64
|
+
return text(`${policy?.note ? `${policy.note}\n` : ''}${action} requested for ${target}.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!commandExists('systemctl')) unavailable('Service control', 'systemctl is required on Linux');
|
|
68
|
+
const base = scope === 'user' ? ['--user'] : [];
|
|
69
|
+
if (action === 'list') return text((await runFile('systemctl', [...base, 'list-units', '--type=service', '--all', '--no-pager', '--plain'], { label: 'systemctl list' })).stdout);
|
|
70
|
+
if (action === 'status') {
|
|
71
|
+
const result = await runFile('systemctl', [...base, 'status', name, '--no-pager', '--plain'], { label: 'systemctl status', allowFailure: true });
|
|
72
|
+
const rendered = `${result.stdout}${result.stderr}`.trim();
|
|
73
|
+
return text(rendered, result.code !== 0 && !result.stdout.trim());
|
|
74
|
+
}
|
|
75
|
+
await runFile('systemctl', [...base, action, name], { label: `systemctl ${action}`, timeout: 30_000 });
|
|
76
|
+
return text(`${policy?.note ? `${policy.note}\n` : ''}${action} requested for service ${name}.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function eventLog(args) {
|
|
80
|
+
const limit = clamp(args.limit, 200, 1, 5000);
|
|
81
|
+
const since = durationSpec(args.since);
|
|
82
|
+
const filter = optionalString(args.filter || args.query);
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
const log = escapePowerShellSingle(optionalString(args.log) || 'System');
|
|
85
|
+
const n = Number.parseInt(since, 10), unit = since.at(-1);
|
|
86
|
+
const seconds = n * ({ s:1, m:60, h:3600, d:86400 }[unit] || 60);
|
|
87
|
+
const where = filter ? ` | Where-Object { $_.Message -like '*${escapePowerShellSingle(filter)}*' -or $_.ProviderName -like '*${escapePowerShellSingle(filter)}*' }` : '';
|
|
88
|
+
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`;
|
|
89
|
+
return text((await runPowerShell(script, { label: 'event log', timeout: 30_000 })).stdout.trim() || '[]');
|
|
90
|
+
}
|
|
91
|
+
if (process.platform === 'darwin') {
|
|
92
|
+
const argv = ['show','--last',since,'--style','ndjson'];
|
|
93
|
+
if (filter) argv.push('--predicate', `eventMessage CONTAINS[c] ${JSON.stringify(filter)}`);
|
|
94
|
+
const { stdout } = await runFile('/usr/bin/log', argv, { label: 'macOS unified log', timeout: 30_000, maxBuffer: 32 * 1024 * 1024 });
|
|
95
|
+
return text(stdout.split('\n').slice(0, limit).join('\n'));
|
|
96
|
+
}
|
|
97
|
+
if (!commandExists('journalctl')) unavailable('Event logs', 'journalctl is required on Linux');
|
|
98
|
+
const n = Number.parseInt(since, 10), unit = since.at(-1);
|
|
99
|
+
const argv = ['--no-pager','-n',String(limit),'--since',`${n} ${{s:'seconds',m:'minutes',h:'hours',d:'days'}[unit]} ago`,'-o','short-iso'];
|
|
100
|
+
if (filter) argv.push('--grep', filter);
|
|
101
|
+
const result = await runFile('journalctl', argv, { label: 'journalctl', timeout: 30_000, allowFailure: true });
|
|
102
|
+
const rendered = `${result.stdout}${result.stderr}`.trim();
|
|
103
|
+
return text(rendered, result.code !== 0 && !result.stdout.trim());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function connectivityTest(host, port, timeoutMs) {
|
|
107
|
+
return new Promise(resolve => {
|
|
108
|
+
const started = performance.now();
|
|
109
|
+
const socket = net.createConnection({ host, port });
|
|
110
|
+
let settled = false;
|
|
111
|
+
const finish = value => { if (settled) return; settled = true; clearTimeout(timer); socket.destroy(); resolve(value); };
|
|
112
|
+
const timer = setTimeout(() => finish({ ok:false, host, port, error:'timeout', latency_ms:Math.round(performance.now()-started) }), timeoutMs);
|
|
113
|
+
socket.once('connect', () => finish({ ok:true, host, port, latency_ms:Math.round(performance.now()-started) }));
|
|
114
|
+
socket.once('error', error => finish({ ok:false, host, port, error:error.code || error.message, latency_ms:Math.round(performance.now()-started) }));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function networkTool(args) {
|
|
119
|
+
const action = requireEnum(args.action || 'summary', 'action', ['summary','interfaces','dns','routes','listeners','test']);
|
|
120
|
+
if (action === 'interfaces') return jsonResult(os.networkInterfaces());
|
|
121
|
+
if (action === 'dns') return jsonResult({ servers:dns.getServers(), hostname:os.hostname() });
|
|
122
|
+
if (action === 'summary') return jsonResult({ hostname:os.hostname(), interfaces:os.networkInterfaces(), dns:dns.getServers() });
|
|
123
|
+
if (action === 'test') {
|
|
124
|
+
const host = optionalString(args.host); if (!host) throw new Error('host is required for action=test');
|
|
125
|
+
return jsonResult(await connectivityTest(host, clamp(args.port, 443, 1, 65535), clamp(args.timeout_ms, 3000, 100, 30_000)));
|
|
126
|
+
}
|
|
127
|
+
if (process.platform === 'win32') {
|
|
128
|
+
const script = action === 'routes'
|
|
129
|
+
? 'Get-NetRoute | Select-Object DestinationPrefix,NextHop,RouteMetric,InterfaceAlias,AddressFamily | ConvertTo-Json -Compress'
|
|
130
|
+
: 'Get-NetTCPConnection -State Listen | Select-Object LocalAddress,LocalPort,OwningProcess,State | Sort-Object LocalPort | ConvertTo-Json -Compress';
|
|
131
|
+
return text((await runPowerShell(script, { label:`network ${action}` })).stdout.trim() || '[]');
|
|
132
|
+
}
|
|
133
|
+
if (action === 'routes') {
|
|
134
|
+
if (commandExists('ip')) return text((await runFile('ip', ['route','show'], { label:'routes' })).stdout);
|
|
135
|
+
if (commandExists('route')) return text((await runFile('route', ['-n'], { label:'routes' })).stdout);
|
|
136
|
+
unavailable('Route inventory', 'ip or route is required');
|
|
137
|
+
}
|
|
138
|
+
if (commandExists('ss')) return text((await runFile('ss', ['-lntup'], { label:'listeners', allowFailure:true })).stdout);
|
|
139
|
+
if (commandExists('netstat')) return text((await runFile('netstat', ['-an'], { label:'listeners' })).stdout);
|
|
140
|
+
unavailable('Listener inventory', 'ss or netstat is required');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function installedApps(args) {
|
|
144
|
+
const limit = clamp(args.limit, 1000, 1, 10_000);
|
|
145
|
+
const filter = optionalString(args.filter);
|
|
146
|
+
if (process.platform === 'win32') {
|
|
147
|
+
const where = filter ? ` | Where-Object { $_.DisplayName -like '*${escapePowerShellSingle(filter)}*' }` : '';
|
|
148
|
+
const script = `$paths=@('HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*');Get-ItemProperty $paths -ErrorAction SilentlyContinue${where}|Where-Object{$_.DisplayName}|Select-Object -First ${limit} DisplayName,DisplayVersion,Publisher,InstallLocation|Sort-Object DisplayName -Unique|ConvertTo-Json -Compress`;
|
|
149
|
+
return text((await runPowerShell(script, { label:'installed apps', timeout:30_000 })).stdout.trim() || '[]');
|
|
150
|
+
}
|
|
151
|
+
if (process.platform === 'darwin') {
|
|
152
|
+
const parsed = JSON.parse((await runFile('/usr/sbin/system_profiler', ['SPApplicationsDataType','-json'], { label:'installed apps', timeout:60_000, maxBuffer:64*1024*1024 })).stdout);
|
|
153
|
+
const apps = (parsed.SPApplicationsDataType || []).filter(app => !filter || String(app._name || '').toLowerCase().includes(filter.toLowerCase())).slice(0, limit);
|
|
154
|
+
return jsonResult(apps.map(app => ({ name:app._name, version:app.version || null, path:app.path || null, signed_by:app.signed_by || null })));
|
|
155
|
+
}
|
|
156
|
+
if (commandExists('dpkg-query')) {
|
|
157
|
+
const { stdout } = await runFile('dpkg-query', ['-W','-f=${binary:Package}\t${Version}\t${Maintainer}\n'], { label:'installed apps', maxBuffer:32*1024*1024 });
|
|
158
|
+
return text(stdout.split('\n').filter(Boolean).filter(line => !filter || line.toLowerCase().includes(filter.toLowerCase())).slice(0, limit).join('\n'));
|
|
159
|
+
}
|
|
160
|
+
if (commandExists('rpm')) {
|
|
161
|
+
const { stdout } = await runFile('rpm', ['-qa','--qf','%{NAME}\t%{VERSION}-%{RELEASE}\t%{VENDOR}\n'], { label:'installed apps', maxBuffer:32*1024*1024 });
|
|
162
|
+
return text(stdout.split('\n').filter(Boolean).filter(line => !filter || line.toLowerCase().includes(filter.toLowerCase())).slice(0, limit).join('\n'));
|
|
163
|
+
}
|
|
164
|
+
unavailable('Installed application inventory', 'dpkg-query or rpm is required');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function environmentTool(args) {
|
|
168
|
+
return jsonResult({
|
|
169
|
+
platform:process.platform,
|
|
170
|
+
arch:process.arch,
|
|
171
|
+
node:process.versions.node,
|
|
172
|
+
cwd:process.cwd(),
|
|
173
|
+
home:os.homedir(),
|
|
174
|
+
temp:os.tmpdir(),
|
|
175
|
+
shell:process.platform === 'win32' ? process.env.ComSpec || null : process.env.SHELL || null,
|
|
176
|
+
path_entries:String(process.env.PATH || '').split(path.delimiter).filter(Boolean),
|
|
177
|
+
...(args.include_env === true ? { environment:safeEnvironment() } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function audioTool(args) {
|
|
182
|
+
const action = requireEnum(args.action || 'status', 'action', ['status','set_volume','mute','unmute']);
|
|
183
|
+
const volume = clamp(args.volume, 50, 0, 100);
|
|
184
|
+
if (process.platform === 'darwin') {
|
|
185
|
+
if (action === 'status') {
|
|
186
|
+
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' });
|
|
187
|
+
const [v,m] = stdout.trim().split('\t'); return jsonResult({ volume:Number(v), muted:m === 'true' });
|
|
188
|
+
}
|
|
189
|
+
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' });
|
|
190
|
+
return audioTool({ action:'status' });
|
|
191
|
+
}
|
|
192
|
+
if (process.platform === 'win32') {
|
|
193
|
+
if (!commandExists('powershell.exe') && !process.env.SystemRoot) unavailable('Windows audio control');
|
|
194
|
+
if (action === 'status') return text('Windows master-volume status requires the CoreAudio adapter; use set_volume/mute/unmute or install a system audio helper.');
|
|
195
|
+
// Windows has no stable built-in CLI for CoreAudio; use media keys for mute/volume changes.
|
|
196
|
+
const key = action === 'mute' || action === 'unmute' ? '{VOLUME_MUTE}' : null;
|
|
197
|
+
if (key) await runPowerShell(`$w=New-Object -ComObject WScript.Shell;$w.SendKeys('${key}')`, { label:'audio control' });
|
|
198
|
+
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');
|
|
199
|
+
return text(`Audio action ${action} requested.`);
|
|
200
|
+
}
|
|
201
|
+
if (commandExists('wpctl')) {
|
|
202
|
+
if (action === 'status') return text((await runFile('wpctl', ['get-volume','@DEFAULT_AUDIO_SINK@'], { label:'audio status' })).stdout.trim());
|
|
203
|
+
if (action === 'set_volume') await runFile('wpctl', ['set-volume','@DEFAULT_AUDIO_SINK@',`${volume}%`], { label:'audio control' });
|
|
204
|
+
else await runFile('wpctl', ['set-mute','@DEFAULT_AUDIO_SINK@',action === 'mute' ? '1' : '0'], { label:'audio control' });
|
|
205
|
+
return text(`Audio action ${action} completed.`);
|
|
206
|
+
}
|
|
207
|
+
if (commandExists('pactl')) {
|
|
208
|
+
if (action === 'status') return text(`${(await runFile('pactl',['get-sink-volume','@DEFAULT_SINK@'],{label:'audio status'})).stdout}${(await runFile('pactl',['get-sink-mute','@DEFAULT_SINK@'],{label:'audio status'})).stdout}`.trim());
|
|
209
|
+
if (action === 'set_volume') await runFile('pactl', ['set-sink-volume','@DEFAULT_SINK@',`${volume}%`], { label:'audio control' });
|
|
210
|
+
else await runFile('pactl', ['set-sink-mute','@DEFAULT_SINK@',action === 'mute' ? '1' : '0'], { label:'audio control' });
|
|
211
|
+
return text(`Audio action ${action} completed.`);
|
|
212
|
+
}
|
|
213
|
+
if (commandExists('amixer')) {
|
|
214
|
+
if (action === 'status') return text((await runFile('amixer',['get','Master'],{label:'audio status'})).stdout);
|
|
215
|
+
await runFile('amixer', action === 'set_volume' ? ['set','Master',`${volume}%`] : ['set','Master',action === 'mute' ? 'mute' : 'unmute'], { label:'audio control' });
|
|
216
|
+
return text(`Audio action ${action} completed.`);
|
|
217
|
+
}
|
|
218
|
+
unavailable('Audio control', 'wpctl, pactl or amixer is required on Linux');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function powerAction(args) {
|
|
222
|
+
const action = requireEnum(args.action, 'action', ['lock','sleep','restart','shutdown']);
|
|
223
|
+
const policy = assertAllowedCommand(action === 'restart' ? 'reboot' : action === 'shutdown' ? 'shutdown' : action);
|
|
224
|
+
const delay = clamp(args.delay_seconds, 0, 0, 3600);
|
|
225
|
+
if (delay) await new Promise(resolve => setTimeout(resolve, delay * 1000));
|
|
226
|
+
if (process.platform === 'win32') {
|
|
227
|
+
if (action === 'lock') await runPowerShell('rundll32.exe user32.dll,LockWorkStation', { label:'lock workstation' });
|
|
228
|
+
else if (action === 'sleep') await runPowerShell('rundll32.exe powrprof.dll,SetSuspendState 0,1,0', { label:'sleep workstation' });
|
|
229
|
+
else spawnDetached('shutdown.exe', [action === 'restart' ? '/r' : '/s','/t','0']);
|
|
230
|
+
} else if (process.platform === 'darwin') {
|
|
231
|
+
if (action === 'lock') spawnDetached('/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession', ['-suspend']);
|
|
232
|
+
else if (action === 'sleep') await runFile('/usr/bin/pmset', ['sleepnow'], { label:'sleep' });
|
|
233
|
+
else await runOsa(`tell application "System Events" to ${action === 'restart' ? 'restart' : 'shut down'}`, { label:action });
|
|
234
|
+
} else {
|
|
235
|
+
if (action === 'lock') {
|
|
236
|
+
if (!commandExists('loginctl')) unavailable('Session lock', 'loginctl is required');
|
|
237
|
+
await runFile('loginctl', ['lock-session'], { label:'lock session' });
|
|
238
|
+
} else {
|
|
239
|
+
if (!commandExists('systemctl')) unavailable('Power action', 'systemctl is required');
|
|
240
|
+
await runFile('systemctl', [action === 'sleep' ? 'suspend' : action === 'restart' ? 'reboot' : 'poweroff'], { label:action });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return text(`${policy.note ? `${policy.note}\n` : ''}Power action ${action} requested.`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function recordScreen(args) {
|
|
247
|
+
const seconds = clamp(args.duration_seconds, 5, 1, 120);
|
|
248
|
+
const fps = clamp(args.fps, 15, 1, 60);
|
|
249
|
+
const destination = args.destination ? await resolveSafePath(args.destination, 'destination') : path.join(os.tmpdir(), `remcp-screen-${Date.now()}.mp4`);
|
|
250
|
+
if (process.platform === 'linux' && commandExists('wf-recorder') && commandExists('timeout')) {
|
|
251
|
+
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 });
|
|
252
|
+
if (![0, 124, 130].includes(Number(result.code))) throw new Error(result.stderr.trim() || `wf-recorder exited ${result.code}`);
|
|
253
|
+
} else {
|
|
254
|
+
if (!commandExists('ffmpeg')) unavailable('Screen recording', 'install wf-recorder on Wayland or ffmpeg');
|
|
255
|
+
let argv;
|
|
256
|
+
if (process.platform === 'win32') argv=['-y','-f','gdigrab','-framerate',String(fps),'-i','desktop','-t',String(seconds),'-pix_fmt','yuv420p',destination];
|
|
257
|
+
else if (process.platform === 'darwin') argv=['-y','-f','avfoundation','-framerate',String(fps),'-i','1:none','-t',String(seconds),'-pix_fmt','yuv420p',destination];
|
|
258
|
+
else argv=['-y','-f','x11grab','-framerate',String(fps),'-i',process.env.DISPLAY || ':0.0','-t',String(seconds),'-pix_fmt','yuv420p',destination];
|
|
259
|
+
await runFile('ffmpeg', argv, { label:'screen recording', timeout:(seconds+20)*1000, maxBuffer:8*1024*1024 });
|
|
260
|
+
}
|
|
261
|
+
const info = await stat(destination);
|
|
262
|
+
return jsonResult({ path:destination, bytes:info.size, duration_seconds:seconds, format:path.extname(destination).slice(1) || 'mp4' });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const diagnosticHandlers = {
|
|
266
|
+
service:serviceTool,
|
|
267
|
+
event_log:eventLog,
|
|
268
|
+
network:networkTool,
|
|
269
|
+
installed_apps:installedApps,
|
|
270
|
+
environment:environmentTool,
|
|
271
|
+
audio:audioTool,
|
|
272
|
+
power_action:powerAction,
|
|
273
|
+
record_screen:recordScreen,
|
|
274
|
+
};
|