@remcp/runtime 0.2.227 → 0.2.229
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/package.json
CHANGED
package/src/extended/catalog.mjs
CHANGED
|
@@ -177,7 +177,7 @@ export const extendedToolDefinitions = [
|
|
|
177
177
|
define('environment', 'Runtime environment', 'Report platform, architecture, Node version, working/home/temp paths, shell, PATH, and optionally environment variables. Environment variables are opt-in and secret-looking or credential-bearing values are masked.', o({ include_env:b('Include sanitized environment variables; default false.') }), readOnly, diagnosticHandlers.environment),
|
|
178
178
|
define('audio', 'Audio control', 'Read or change default output volume or mute state using native operating-system audio controls when available.', o({ action:e(['status','set_volume','mute','unmute']), volume:n('0..100 for set_volume.') }, ['action']), mutatingNonDestructive, diagnosticHandlers.audio, ['audio']),
|
|
179
179
|
define('power_action', 'Power action', 'Lock, sleep, restart, or shut down the paired computer using native operating-system power facilities. The action passes through ReMCP command policy. Delayed actions are cancelled if the client stops waiting.', o({ action:e(['lock','sleep','restart','shutdown']), delay_seconds:{ type:'number', minimum:0, maximum:90, description:'Optional delay in seconds, maximum 90. Longer delays are rejected because hosted calls expire before they can complete safely.' } }, ['action']), mutating, diagnosticHandlers.power_action, ['power']),
|
|
180
|
-
define('record_screen', 'Record screen', 'Record a short bounded desktop video to a permitted local file for debugging and return its path and size. Uses native/available recording helpers.', o({ duration_seconds:n('1..120 seconds.'), fps:n('1..60.'), destination:s('Permitted output path.
|
|
180
|
+
define('record_screen', 'Record screen', 'Record a short bounded desktop video to a permitted local file for debugging and return its path and size. Uses native/available recording helpers.', o({ duration_seconds:n('1..120 seconds.'), fps:n('1..60.'), destination:s('Permitted output path. Omit the extension to accept the native format. GNOME may produce .webm or .mp4; other backends produce .mp4. An explicit extension must match the recorded format.') }), mutating, diagnosticHandlers.record_screen, ['record']),
|
|
181
181
|
|
|
182
182
|
define('read_document', 'Read document', 'Read PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON, or XML. PDF/DOCX extraction is built in and XLSX is parsed from its OOXML worksheet cells.', o({ path:s('Document path.'), sheet:s('Worksheet name for XLSX.'), max_cells:n() }, ['path']), readOnly, documentHandlers.read_document, ['documents']),
|
|
183
183
|
define('edit_spreadsheet', 'Edit spreadsheet', 'Create a new XLSX workbook or edit up to 500 expanded XLSX cells directly in OOXML without launching Excel. For creation set create=true and use path as a new destination; existing files are never overwritten by create mode. Each edit may target one cell or a rectangular range, with a scalar fill, a values matrix, or a formula.', o({ path:s('Existing .xlsx path, or the new workbook destination when create=true.'), create:b('Create a new XLSX at path. Fails if path already exists; do not combine with output.'), output:s('Optional output .xlsx when editing an existing workbook; defaults to replacing input.'), sheet:s('Worksheet name. For create=true, defaults to Sheet1 and becomes the new workbook sheet name.'), edits:{ type:'array', maxItems:500, items:o({ cell:s('Single A1 cell reference.'), range:s('Rectangular range such as A1:C3.'), value:{ description:'Scalar value for one cell or to fill a range.' }, values:{ type:'array', items:{ type:'array', items:{} }, description:'2D matrix matching the range dimensions.' }, formula:s('Optional formula; for a range it is written to each expanded cell.') }) } }, ['path','edits']), mutating, documentHandlers.edit_spreadsheet, ['documents','ooxml']),
|
|
@@ -113,7 +113,9 @@ export async function windowAction(args) {
|
|
|
113
113
|
return text(`Window action ${action} completed.`);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export function pyAtSpiPrelude() {
|
|
116
|
+
export function pyAtSpiPrelude({ pid = 0, app = '' } = {}) {
|
|
117
|
+
const scopedPid = Number.isInteger(Number(pid)) ? Number(pid) : 0;
|
|
118
|
+
const scopedApp = JSON.stringify(optionalString(app) || '');
|
|
117
119
|
return `import json,sys,math
|
|
118
120
|
try:
|
|
119
121
|
import pyatspi
|
|
@@ -188,10 +190,13 @@ def _proc(e):
|
|
|
188
190
|
except: pass
|
|
189
191
|
return 0
|
|
190
192
|
q=[]
|
|
193
|
+
scope_pid=${scopedPid};scope_app=${scopedApp}
|
|
191
194
|
for app in desktop:
|
|
192
195
|
try:
|
|
193
|
-
app_name=app.name or ""
|
|
194
196
|
pid=_proc(app)
|
|
197
|
+
if scope_pid and pid!=scope_pid: continue
|
|
198
|
+
app_name=app.name or ""
|
|
199
|
+
if scope_app and scope_app.lower() not in app_name.lower(): continue
|
|
195
200
|
active=False
|
|
196
201
|
try:
|
|
197
202
|
active=bool(app.getState().contains(pyatspi.STATE_ACTIVE))
|
|
@@ -216,7 +221,7 @@ export async function uiSnapshot(args = {}) {
|
|
|
216
221
|
const wantedPid = Number.isInteger(Number(args.pid)) ? Number(args.pid) : 0;
|
|
217
222
|
const wantedWindow = JSON.stringify(optionalString(args.window_title || args.windowTitle) || '');
|
|
218
223
|
const activeOnly = args.active_only === true;
|
|
219
|
-
const script = pyAtSpiPrelude() + `
|
|
224
|
+
const script = pyAtSpiPrelude({ pid:wantedPid, app:args.app }) + `
|
|
220
225
|
want_app=${wantedApp};want_pid=${wantedPid};want_window=${wantedWindow};want_active=${activeOnly ? 'True' : 'False'}
|
|
221
226
|
roots=[]
|
|
222
227
|
for root,app_name,pid,active in q:
|
|
@@ -288,7 +293,7 @@ export async function uiAction(args = {}) {
|
|
|
288
293
|
const wantedName = JSON.stringify(optionalString(args.name) || '');
|
|
289
294
|
const wantedRole = JSON.stringify(optionalString(args.role) || '');
|
|
290
295
|
const value = JSON.stringify(String(args.value ?? ''));
|
|
291
|
-
const script = pyAtSpiPrelude() + `
|
|
296
|
+
const script = pyAtSpiPrelude({ pid:wantedPid, app:args.app }) + `
|
|
292
297
|
want_index=${legacyIndex};want_pid=${wantedPid};want_path=${JSON.stringify(stablePath)}
|
|
293
298
|
want_app=${wantedApp};want_window=${wantedWindow};want_name=${wantedName};want_role=${wantedRole};action=${JSON.stringify(action)};value=${value}
|
|
294
299
|
queue=[];seen=0;target=None;target_meta={}
|
|
@@ -67,7 +67,7 @@ export async function nativeWaylandAccessibilityAction(row, actionNames) {
|
|
|
67
67
|
if (!Number.isInteger(pid) || pid <= 0 || !Array.isArray(actionNames) || !actionNames.length) {
|
|
68
68
|
return { applied:false, available:[] };
|
|
69
69
|
}
|
|
70
|
-
const script = pyAtSpiPrelude() + `
|
|
70
|
+
const script = pyAtSpiPrelude({ pid }) + `
|
|
71
71
|
want_pid=${pid};want_window=${JSON.stringify(String(row.title || ''))};wanted_actions=${JSON.stringify(actionNames)}
|
|
72
72
|
target=None
|
|
73
73
|
for root,app_name,pid,active in q:
|
|
@@ -253,11 +253,26 @@ print(JSON.stringify({ destination:started[1], stopped:stopped[0] }));
|
|
|
253
253
|
async function recordGnomeShellScreen({ staged, stagingDirectory, seconds, fps }) {
|
|
254
254
|
const helper = path.join(stagingDirectory, 'gnome-screencast.js');
|
|
255
255
|
await writeFile(helper, GNOME_SCREENCAST_HELPER, { mode:0o600 });
|
|
256
|
-
await runFile('gjs', [helper, staged, String(seconds), String(fps)], {
|
|
256
|
+
const result = await runFile('gjs', [helper, staged, String(seconds), String(fps)], {
|
|
257
257
|
label:'GNOME Shell screen recording',
|
|
258
258
|
timeout:(seconds + 20) * 1000,
|
|
259
259
|
maxBuffer:2 * 1024 * 1024,
|
|
260
260
|
});
|
|
261
|
+
let report;
|
|
262
|
+
try { report = JSON.parse(result.stdout.trim()); } catch {}
|
|
263
|
+
const output = report?.destination;
|
|
264
|
+
// GNOME selects the container and may replace the template extension. Accept
|
|
265
|
+
// only the exact recording stem in our private staging directory, never an
|
|
266
|
+
// arbitrary path returned by the desktop service.
|
|
267
|
+
if (report?.stopped !== true || typeof output !== 'string' || !path.isAbsolute(output)
|
|
268
|
+
|| path.dirname(output) !== stagingDirectory
|
|
269
|
+
|| path.parse(output).name !== path.parse(staged).name
|
|
270
|
+
|| !['.mp4','.webm'].includes(path.extname(output))) {
|
|
271
|
+
throw new Error('GNOME returned an invalid screen recording staging path or format');
|
|
272
|
+
}
|
|
273
|
+
const info = await lstat(output);
|
|
274
|
+
if (!info.isFile() || !info.size) throw new Error('GNOME screen recording is not a non-empty regular file');
|
|
275
|
+
return { path:output, format:path.extname(output).slice(1) };
|
|
261
276
|
}
|
|
262
277
|
|
|
263
278
|
export function validateRecordingDestinationFormat(destination, format) {
|
|
@@ -328,21 +343,27 @@ export async function recordScreen(args) {
|
|
|
328
343
|
unavailable('Screen recording', 'ffmpeg is required on X11');
|
|
329
344
|
}
|
|
330
345
|
|
|
331
|
-
|
|
346
|
+
let format = backend === 'gnome-shell' ? 'webm' : 'mp4';
|
|
332
347
|
const requestedDestination = args.destination ? await resolveSafePath(args.destination, 'destination') : '';
|
|
333
348
|
if (requestedDestination) {
|
|
334
|
-
validateRecordingDestinationFormat(requestedDestination, format);
|
|
349
|
+
if (backend !== 'gnome-shell') validateRecordingDestinationFormat(requestedDestination, format);
|
|
350
|
+
else if (path.extname(requestedDestination) && !['.mp4','.webm'].includes(path.extname(requestedDestination).toLowerCase())) {
|
|
351
|
+
throw new Error('GNOME screen recording destination must use .mp4, .webm or omit the extension');
|
|
352
|
+
}
|
|
335
353
|
const existing = await lstat(requestedDestination).catch(() => null);
|
|
336
354
|
if (existing?.isSymbolicLink()) throw new Error('Screen recording destination cannot be a symbolic link');
|
|
337
355
|
if (existing && !existing.isFile()) throw new Error('Screen recording destination is not a regular file');
|
|
338
356
|
}
|
|
339
357
|
|
|
340
358
|
const stagingDirectory = await mkdtemp(path.join(os.tmpdir(), 'remcp-screen-'));
|
|
341
|
-
|
|
359
|
+
let staged = path.join(stagingDirectory, `capture-${randomUUID()}.${format}`);
|
|
342
360
|
let keepStaging = false;
|
|
343
361
|
try {
|
|
344
362
|
if (backend === 'gnome-shell') {
|
|
345
|
-
await recordGnomeShellScreen({ staged, stagingDirectory, seconds, fps });
|
|
363
|
+
const recorded = await recordGnomeShellScreen({ staged, stagingDirectory, seconds, fps });
|
|
364
|
+
staged = recorded.path;
|
|
365
|
+
format = recorded.format;
|
|
366
|
+
validateRecordingDestinationFormat(requestedDestination, format);
|
|
346
367
|
} else if (backend === 'wf-recorder') {
|
|
347
368
|
const result = await runFile('timeout', ['--signal=INT', `${seconds}s`, 'wf-recorder', '-f',staged,'-r',String(fps),'-c','libx264'], { label:'screen recording', timeout:(seconds+10)*1000, allowFailure:true });
|
|
348
369
|
if (![0, 124, 130].includes(Number(result.code))) throw new Error(result.stderr.trim() || `wf-recorder exited ${result.code}`);
|