@remcp/runtime 0.2.110 → 0.2.111

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.111 — 2026-09-25
4
+
5
+ - `read_document` reads `.xlsx` files again while `allowedRoots` confinement is enabled: the reader no longer re-enters its own temp staging path through the allowlist.
6
+ - Linux window inventory refreshes X11 rows after the AT-SPI walk, window selectors resolve X11/XWayland targets without a full semantic rebuild, and `window_action focus` verifies direct XWayland activation before falling back to portal cycling. Runtime tool names and schemas are unchanged.
7
+
3
8
  ## 0.2.110 — 2026-09-25
4
9
 
5
10
  - Bulk handlers (`read_multiple_files`, `read_files`, `replace_in_files`, `write_files`, `copy_paths`, `move_paths`, `delete_paths`, `create_archive`) and the directory walk now honour client cancellation between items, so an aborted call stops at the next item boundary instead of finishing on the device; single-file and external-tool (`tar`/`unzip`) operations remain atomic and are not interrupted mid-step.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.110",
3
+ "version": "0.2.111",
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",
@@ -55,7 +55,7 @@ async function x11PixelBounds(id) {
55
55
  }
56
56
 
57
57
  export async function listWindows() {
58
- const wmRows = commandExists('wmctrl')
58
+ let wmRows = commandExists('wmctrl')
59
59
  ? parseWindows((await runFile('wmctrl', ['-lpGx'], { label: 'window inventory', allowFailure:true, timeout:3000 })).stdout)
60
60
  : [];
61
61
  if (!isWaylandSession()) {
@@ -85,6 +85,15 @@ export async function listWindows() {
85
85
  });
86
86
  } catch {}
87
87
 
88
+ // AT-SPI enumeration can take several seconds on a busy desktop. Refresh the cheap
89
+ // X11 inventory afterwards so an XWayland window that appeared while AT-SPI was
90
+ // walking the tree is merged with its semantic window instead of being misclassified
91
+ // as native Wayland for the rest of this call.
92
+ if (commandExists('wmctrl')) {
93
+ const refreshedWmRows = parseWindows((await runFile('wmctrl', ['-lpGx'], { label:'window inventory refresh', allowFailure:true, timeout:3000 })).stdout);
94
+ if (refreshedWmRows.length) wmRows = refreshedWmRows;
95
+ }
96
+
88
97
  const pixelBounds = new Map();
89
98
  if (commandExists('xwininfo')) {
90
99
  // wmctrl geometry on Mutter/XWayland is decoration-shifted and cannot be used for pixel crops.
@@ -151,18 +160,50 @@ export async function listWindows() {
151
160
  return jsonResult(rows);
152
161
  }
153
162
 
163
+ function windowSelector(args = {}) {
164
+ return {
165
+ id: optionalString(args.id || args.window_id || args.windowId),
166
+ pid: Number.isInteger(Number(args.pid)) ? Number(args.pid) : null,
167
+ app: optionalString(args.app),
168
+ title: optionalString(args.title || args.window_title || args.windowTitle),
169
+ };
170
+ }
171
+
172
+ function windowMatchesSelector(item, selector) {
173
+ return (!selector.id || item.id === selector.id || item.wm_id === selector.id || item.ui_id === selector.id)
174
+ && (selector.pid == null || Number(item.pid) === selector.pid)
175
+ && (!selector.app || String(item.app || '').toLowerCase().includes(selector.app.toLowerCase()))
176
+ && (!selector.title || String(item.title || '').toLowerCase().includes(selector.title.toLowerCase()));
177
+ }
178
+
179
+ async function fastX11WindowMatch(args) {
180
+ if (!commandExists('wmctrl')) return null;
181
+ const selector = windowSelector(args);
182
+ const rows = parseWindows((await runFile('wmctrl', ['-lpGx'], {
183
+ label:'window match',
184
+ allowFailure:true,
185
+ timeout:2000,
186
+ })).stdout);
187
+ const row = rows.find(item => windowMatchesSelector({ ...item, wm_id:item.id, ui_id:null }, selector));
188
+ if (!row) return null;
189
+ return {
190
+ ...row,
191
+ wm_id:row.id,
192
+ ui_id:null,
193
+ backend:isWaylandSession() ? 'xwayland' : 'x11',
194
+ };
195
+ }
196
+
154
197
  async function windowMatch(args) {
198
+ // Most desktop actions target an X11/XWayland window. Resolve that cheap native id
199
+ // first instead of rebuilding the entire AT-SPI inventory before every action.
200
+ // Native Wayland windows have no wmctrl row and fall through to the semantic inventory.
201
+ const fast = await fastX11WindowMatch(args);
202
+ if (fast) return fast;
203
+
155
204
  const rows = JSON.parse((await listWindows()).content[0].text);
156
- const id = optionalString(args.id || args.window_id || args.windowId);
157
- const pid = Number.isInteger(Number(args.pid)) ? Number(args.pid) : null;
158
- const app = optionalString(args.app);
159
- const title = optionalString(args.title || args.window_title || args.windowTitle);
160
- const row = rows.find(item =>
161
- (!id || item.id === id || item.wm_id === id || item.ui_id === id)
162
- && (pid == null || item.pid === pid)
163
- && (!app || String(item.app || '').toLowerCase().includes(app.toLowerCase()))
164
- && (!title || String(item.title || '').toLowerCase().includes(title.toLowerCase()))
165
- );
205
+ const selector = windowSelector(args);
206
+ const row = rows.find(item => windowMatchesSelector(item, selector));
166
207
  if (!row) throw new Error('No matching window found');
167
208
  return row;
168
209
  }
@@ -502,7 +543,24 @@ export async function windowAction(args) {
502
543
  if (focusState.exact) return text('Window action focus completed via verified AT-SPI focus.');
503
544
  } catch {}
504
545
  }
505
- if (backend === 'x11') unavailable('Wayland window focus', 'AT-SPI focus did not take effect and X11 focus cannot target this Wayland window; use backend=portal');
546
+ // XWayland windows have a real X11 wm_id even inside a Wayland session. Prefer
547
+ // direct activation for those windows, verify the result through accessibility,
548
+ // and only fall back to compositor-level cycling if direct activation did not take.
549
+ // This avoids dozens of Alt+Esc/AT-SPI round trips on busy desktops.
550
+ if (row.wm_id && backend !== 'portal' && commandExists('wmctrl')) {
551
+ const activated = await runFile('wmctrl', ['-ia', row.wm_id], { label:'XWayland window focus', allowFailure:true, timeout:2000 });
552
+ if (activated.code === 0) {
553
+ if (commandExists('xdotool')) {
554
+ await runFile('xdotool', ['windowactivate','--sync',row.wm_id], { label:'XWayland window focus sync', allowFailure:true, timeout:3000 });
555
+ }
556
+ await new Promise(resolve => setTimeout(resolve, 80));
557
+ const focusState = await targetAccessibilityFocus(row);
558
+ if (focusState.exact) return text('Window action focus completed via verified XWayland activation.');
559
+ }
560
+ }
561
+ if (backend === 'x11') unavailable('Wayland window focus', row.wm_id
562
+ ? 'direct XWayland activation did not produce verifiable focus'
563
+ : 'the target is native Wayland and has no X11 window id; use backend=portal');
506
564
  if (!waylandPortalCandidate()) unavailable('Wayland window focus', 'no XDG RemoteDesktop portal is available');
507
565
  if (backend === 'auto' && !hasWaylandRemoteDesktopGrant()) portalPermissionHint('Wayland window focus');
508
566
  const portalOptions = { timeoutMs: backend === 'portal' ? 120_000 : 2500 };
@@ -334,10 +334,12 @@ function readWorksheetCells(xml, sharedStrings, maxCells) {
334
334
  }
335
335
 
336
336
  async function readXlsx(filePath, args) {
337
- const source = await snapshotDocumentFile(filePath);
338
337
  const dir = await tempDir('remcp-xlsx-read-');
339
338
  try {
340
- await extractZip(source.path, dir);
339
+ // extractZip already snapshots the user-approved source into a private temp file
340
+ // before invoking unzip. A second snapshot here used to re-open ReMCP's own temp
341
+ // path through allowedRoots, so read_document failed whenever confinement was on.
342
+ await extractZip(filePath, dir);
341
343
  const workbook = await readFile(path.join(dir,'xl','workbook.xml'),'utf8');
342
344
  const rels = await readFile(path.join(dir,'xl','_rels','workbook.xml.rels'),'utf8');
343
345
  const sheet = workbookSheetPath(workbook, rels, optionalString(args.sheet));
@@ -347,7 +349,6 @@ async function readXlsx(filePath, args) {
347
349
  const cells = readWorksheetCells(worksheet, shared, clamp(args.max_cells,5000,1,50_000));
348
350
  return { kind:'xlsx', sheet:sheet.name, count:cells.length, cells };
349
351
  } finally {
350
- await removeTemp(source.dir);
351
352
  await removeTemp(dir);
352
353
  }
353
354
  }