@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 +5 -0
- package/package.json +1 -1
- package/src/extended/desktop-linux.mjs +70 -12
- package/src/extended/documents.mjs +4 -3
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
|
@@ -55,7 +55,7 @@ async function x11PixelBounds(id) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export async function listWindows() {
|
|
58
|
-
|
|
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
|
|
157
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|