@remcp/runtime 0.2.96 → 0.2.98

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.98 — 2026-09-22
4
+
5
+ - `pdf_action info` fallback exposes numeric `annotation_count` instead of violating the declared `annotations` array output contract.
6
+ - Added outputSchema validation coverage for the no-`pdfinfo` fallback path.
7
+ - Live desktop smoke treats `record_screen` as capability-conditional and verifies every tool actually advertised by the runtime.
8
+ - Tool names remain unchanged.
9
+
10
+ ## 0.2.97 — 2026-09-22
11
+
12
+ - Native GNOME Wayland `move`, `resize`, and `move_resize` no longer trust a portal drag as proof of success.
13
+ - Window geometry is re-read after each gesture and must match the requested coordinates/size within a bounded tolerance; otherwise the action fails closed.
14
+ - `move_resize` confirms the move before computing the resize gesture. Tool names and schemas remain unchanged.
15
+
3
16
  ## 0.2.96 — 2026-09-22
4
17
 
5
18
  - Keeps readable search matches when an unreadable descendant is encountered while surfacing a warning.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.96",
3
+ "version": "0.2.98",
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",
@@ -219,7 +219,7 @@ const OUTPUT_FIELDS = Object.freeze({
219
219
  read_document: { text:outputString(), path:outputString(), data:{}, sheets:outputArray(outputObject()), sheet:outputString(), rows:outputArray({}), cells:outputArray({}) },
220
220
  edit_spreadsheet: { path:outputString(), sheet:outputString(), edited_cells:outputNumber(), bytes:outputNumber(), created:outputBoolean() },
221
221
  edit_document: { path:outputString(), operations:outputNumber(), changes:outputNumber(), bytes:outputNumber(), created:outputBoolean() },
222
- pdf_action: { action:outputString(), path:outputString(), source:outputString(), output:outputString(), output_dir:outputString(), files:outputArray(outputString()), pages:outputString(), bytes:outputNumber(), annotations:outputArray(outputObject()), inputs:outputNumber() },
222
+ pdf_action: { action:outputString(), path:outputString(), source:outputString(), output:outputString(), output_dir:outputString(), files:outputArray(outputString()), pages:outputString(), bytes:outputNumber(), annotations:outputArray(outputObject()), annotation_count:outputNumber('Number of PDF annotations found by the info action.'), inputs:outputNumber() },
223
223
  });
224
224
 
225
225
  function outputSchemaFor(name, title) {
@@ -283,6 +283,34 @@ async function focusExplicitWaylandInputTarget(args = {}) {
283
283
  return row;
284
284
  }
285
285
 
286
+ function nativeWaylandGeometryMatches(row, expected = {}, tolerance = 24) {
287
+ for (const key of ['x','y','width','height']) {
288
+ if (expected[key] == null) continue;
289
+ const actual = Number(row?.[key]);
290
+ const wanted = Number(expected[key]);
291
+ if (!Number.isFinite(actual) || !Number.isFinite(wanted) || Math.abs(actual - wanted) > tolerance) return false;
292
+ }
293
+ return true;
294
+ }
295
+
296
+ async function verifyNativeWaylandGeometry(row, expected, label) {
297
+ let latest = row;
298
+ for (let attempt = 0; attempt < 6; attempt += 1) {
299
+ if (attempt > 0) await new Promise(resolve => setTimeout(resolve, 120));
300
+ try {
301
+ latest = await windowMatch({ id:row.id });
302
+ if (nativeWaylandGeometryMatches(latest, expected)) return latest;
303
+ } catch {}
304
+ }
305
+ const actual = ['x','y','width','height']
306
+ .map(key => `${key}=${Number.isFinite(Number(latest?.[key])) ? Number(latest[key]) : 'unknown'}`)
307
+ .join(' ');
308
+ const wanted = Object.entries(expected)
309
+ .map(([key,value]) => `${key}=${value}`)
310
+ .join(' ');
311
+ throw new Error(`Could not verify native Wayland window ${label} for ${row.title || row.app || row.id}: wanted ${wanted}; observed ${actual}`);
312
+ }
313
+
286
314
  async function nativeWaylandWindowAction(row, action, args) {
287
315
  const backend = inputBackend(args);
288
316
  if (backend === 'x11') unavailable('Native Wayland window action', 'use backend=portal for compositor-managed windows');
@@ -336,8 +364,11 @@ async function nativeWaylandWindowAction(row, action, args) {
336
364
  hold_ms:80,
337
365
  duration_ms:120,
338
366
  });
339
- currentX = x;
340
- currentY = y;
367
+ const verified = await verifyNativeWaylandGeometry(row, { x, y }, 'move');
368
+ currentX = Number(verified.x);
369
+ currentY = Number(verified.y);
370
+ currentWidth = Number(verified.width);
371
+ currentHeight = Number(verified.height);
341
372
  }
342
373
 
343
374
  if (action === 'resize' || action === 'move_resize') {
@@ -352,7 +383,21 @@ async function nativeWaylandWindowAction(row, action, args) {
352
383
  hold_ms:80,
353
384
  duration_ms:120,
354
385
  });
386
+ const verified = await verifyNativeWaylandGeometry(row, { width, height }, 'resize');
387
+ currentX = Number(verified.x);
388
+ currentY = Number(verified.y);
389
+ currentWidth = Number(verified.width);
390
+ currentHeight = Number(verified.height);
355
391
  }
392
+ return jsonResult({
393
+ action,
394
+ backend:'xdg-desktop-portal',
395
+ id:row.id,
396
+ x:currentX,
397
+ y:currentY,
398
+ width:currentWidth,
399
+ height:currentHeight,
400
+ });
356
401
  }
357
402
  return jsonResult({ action, backend:'xdg-desktop-portal', id:row.id });
358
403
  }
@@ -442,7 +442,7 @@ export async function pdfAction(args) {
442
442
  if (action === 'info') {
443
443
  const source = await resolveSafePath(args.path,'path');
444
444
  if (commandExists('pdfinfo')) return text((await runFile('pdfinfo',[source],{label:'pdfinfo'})).stdout);
445
- const data = await readFile(source); return jsonResult({ path:source, bytes:data.length, annotations:pdfAnnotations(data).length });
445
+ const data = await readFile(source); return jsonResult({ path:source, bytes:data.length, annotation_count:pdfAnnotations(data).length });
446
446
  }
447
447
  if (action === 'merge') {
448
448
  const requested = Array.isArray(args.paths) ? args.paths : [];