@remcp/runtime 0.2.211 → 0.2.213
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 +1 -1
- package/src/extended/browser-actions.mjs +4 -2
- package/src/extended/browser-endpoint.mjs +1 -0
- package/src/extended/common.mjs +5 -1
- package/src/extended/desktop-computer-snapshot.mjs +24 -8
- package/src/extended/desktop-windows.mjs +41 -9
- package/src/screenshot-portal.mjs +13 -3
- package/src/tools/files-bulk-operations.mjs +4 -1
- package/src/tools/search.mjs +3 -1
- package/src/tools/terminal.mjs +12 -3
package/package.json
CHANGED
|
@@ -51,8 +51,10 @@ export const PAGE_SCROLL_ACTION = `function(selector, needle, deltaX, deltaY) {
|
|
|
51
51
|
|
|
52
52
|
const beforeX = Number(scroller.scrollLeft || 0);
|
|
53
53
|
const beforeY = Number(scroller.scrollTop || 0);
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// CSS scroll-behavior:smooth can defer these changes indefinitely in a
|
|
55
|
+
// background target. Apply the requested move before reading its result.
|
|
56
|
+
scroller.scrollTo({left:beforeX + Number(deltaX || 0),
|
|
57
|
+
top:beforeY + Number(deltaY || 0), behavior:'instant'});
|
|
56
58
|
return {
|
|
57
59
|
target: target ? target.tagName.toLowerCase() : 'page',
|
|
58
60
|
scroller: scroller === document.scrollingElement || scroller === document.documentElement ? 'page' : scroller.tagName.toLowerCase(),
|
|
@@ -35,6 +35,7 @@ export function browserExecutable() {
|
|
|
35
35
|
path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
36
36
|
path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
37
37
|
path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
38
|
+
path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
38
39
|
]
|
|
39
40
|
: ['/opt/google/chrome/chrome','/usr/bin/google-chrome','/usr/bin/google-chrome-stable','chromium','chromium-browser','microsoft-edge','microsoft-edge-stable'];
|
|
40
41
|
return candidates.find(candidate => path.isAbsolute(candidate) ? existsSync(candidate) : commandExists(candidate)) || null;
|
package/src/extended/common.mjs
CHANGED
|
@@ -197,11 +197,15 @@ export async function runFileHeadLines(file, args = [], lineLimit = 200, options
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// Windows PowerShell writes redirected stdout in the OEM code page by default,
|
|
201
|
+
// which corrupts Unicode names and clipboard text. Emit UTF-8 without a BOM.
|
|
202
|
+
const WINDOWS_UTF8_OUTPUT = '[Console]::OutputEncoding=New-Object System.Text.UTF8Encoding $false;';
|
|
203
|
+
|
|
200
204
|
export async function runPowerShell(script, options = {}) {
|
|
201
205
|
const executable = process.env.SystemRoot
|
|
202
206
|
? path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
203
207
|
: 'powershell.exe';
|
|
204
|
-
return runFile(executable, ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
|
|
208
|
+
return runFile(executable, ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', WINDOWS_UTF8_OUTPUT + script], {
|
|
205
209
|
...options,
|
|
206
210
|
label: options.label || 'PowerShell',
|
|
207
211
|
});
|
|
@@ -16,15 +16,31 @@ import { findOcrText, ocrImage } from './ocr.mjs';
|
|
|
16
16
|
import { boundedComputerSnapshot, clipboard, cursorPosition, displayInventory, listWindows, objectRecord, resultValue, screenshotRegion, snapshotError, suggestedUiAction, toolResultMessage, uiSnapshot } from './desktop-core.mjs';
|
|
17
17
|
import { activeWindowCaptureBounds, browserTitleHint, computerScreenshotTask, hasExplicitComputerScreenshotTarget, ocrScreenshotResult } from './desktop-vision.mjs';
|
|
18
18
|
|
|
19
|
+
// Several simultaneous Windows PowerShell starts can stall until their shared
|
|
20
|
+
// timeout on low-core hosts, which made the whole snapshot report every probe
|
|
21
|
+
// as unavailable. Queue the probes on Windows; other platforms stay parallel.
|
|
22
|
+
export async function settleComputerSnapshotProbes(tasks, { serial = process.platform === 'win32' } = {}) {
|
|
23
|
+
if (!serial) return Promise.allSettled(tasks.map((task) => task()));
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const task of tasks) {
|
|
26
|
+
try {
|
|
27
|
+
results.push({ status: 'fulfilled', value: await task() });
|
|
28
|
+
} catch (reason) {
|
|
29
|
+
results.push({ status: 'rejected', reason });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export async function computerSnapshot(args = {}) {
|
|
20
36
|
const desktopAvailable = desktopSessionAvailable();
|
|
21
37
|
const screenshotRequested = args.include_screenshot !== false || args.include_ocr === true;
|
|
22
38
|
const captureScreenshot = desktopAvailable && screenshotRequested;
|
|
23
39
|
const includeUi = args.include_ui !== false;
|
|
24
|
-
const [windowsResult, displaysResult, uiResult, clipboardResult, cursorResult, shotResult, readinessResult] = await
|
|
25
|
-
desktopAvailable ? listWindows() : Promise.resolve(null),
|
|
26
|
-
desktopAvailable ? displayInventory() : Promise.resolve(null),
|
|
27
|
-
includeUi && desktopAvailable
|
|
40
|
+
const [windowsResult, displaysResult, uiResult, clipboardResult, cursorResult, shotResult, readinessResult] = await settleComputerSnapshotProbes([
|
|
41
|
+
() => desktopAvailable ? listWindows() : Promise.resolve(null),
|
|
42
|
+
() => desktopAvailable ? displayInventory() : Promise.resolve(null),
|
|
43
|
+
() => includeUi && desktopAvailable
|
|
28
44
|
? uiSnapshot({
|
|
29
45
|
max_nodes:args.max_ui_nodes || 800,
|
|
30
46
|
max_depth:args.max_ui_depth || 12,
|
|
@@ -33,10 +49,10 @@ export async function computerSnapshot(args = {}) {
|
|
|
33
49
|
...(args.ui_browser_dom === true ? { browser_dom:true } : {}),
|
|
34
50
|
})
|
|
35
51
|
: Promise.resolve(null),
|
|
36
|
-
desktopAvailable ? clipboard({ action:'read' }) : Promise.resolve(null),
|
|
37
|
-
desktopAvailable ? cursorPosition() : Promise.resolve(null),
|
|
38
|
-
captureScreenshot ? computerScreenshotTask(args) : Promise.resolve(null),
|
|
39
|
-
args.browser_readiness === true ? probeBrowserReadiness() : Promise.resolve(null),
|
|
52
|
+
() => desktopAvailable ? clipboard({ action:'read' }) : Promise.resolve(null),
|
|
53
|
+
() => desktopAvailable ? cursorPosition() : Promise.resolve(null),
|
|
54
|
+
() => captureScreenshot ? computerScreenshotTask(args) : Promise.resolve(null),
|
|
55
|
+
() => args.browser_readiness === true ? probeBrowserReadiness() : Promise.resolve(null),
|
|
40
56
|
]);
|
|
41
57
|
const parse = (result, fallback = null) => {
|
|
42
58
|
if (result.status !== 'fulfilled' || !result.value || result.value.isError === true) return fallback;
|
|
@@ -197,7 +197,7 @@ while($q.Count -gt 0 -and $seen -lt ${maxNodes}){
|
|
|
197
197
|
return jsonResult(parsed);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
export async function uiAction(args = {}) {
|
|
200
|
+
export async function uiAction(args = {}, { runPowerShellFn = runPowerShell } = {}) {
|
|
201
201
|
const action = requireEnum(args.action, 'action', [
|
|
202
202
|
'click','invoke','focus','set_value','select','toggle','expand','collapse',
|
|
203
203
|
'scroll_into_view','set_range_value','add_to_selection','remove_from_selection',
|
|
@@ -207,7 +207,8 @@ export async function uiAction(args = {}) {
|
|
|
207
207
|
const role = escapePowerShellSingle(optionalString(args.role) || '');
|
|
208
208
|
const automationId = escapePowerShellSingle(optionalString(args.automation_id || args.automationId) || '');
|
|
209
209
|
const value = escapePowerShellSingle(String(args.value ?? ''));
|
|
210
|
-
|
|
210
|
+
// The entire switch must parse even when the selected action has no numeric value.
|
|
211
|
+
const numericValue = action === 'set_range_value' ? Number(args.value) : 0;
|
|
211
212
|
if (action === 'set_range_value' && !Number.isFinite(numericValue)) throw new Error('set_range_value requires a finite numeric value');
|
|
212
213
|
const script = String.raw`
|
|
213
214
|
Add-Type -AssemblyName UIAutomationClient;Add-Type -AssemblyName UIAutomationTypes
|
|
@@ -278,16 +279,40 @@ switch('${action}'){
|
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
[pscustomobject]@{id=('win:'+($found.GetRuntimeId()-join '.'));name=$found.Current.Name;role=$found.Current.ControlType.ProgrammaticName.Replace('ControlType.','');action='${action}';backend=$used}|ConvertTo-Json -Compress`;
|
|
281
|
-
const raw = (await
|
|
282
|
+
const raw = (await runPowerShellFn(script, { label:'UI action', timeout:30_000 })).stdout.trim();
|
|
282
283
|
return jsonResult(raw ? JSON.parse(raw) : { action });
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
export function buildWindowsClipboardPowerShell(action, value = '') {
|
|
287
|
+
// Set-Clipboard rejects an empty value in Windows PowerShell 5.1.
|
|
288
|
+
// Clear the clipboard using the OS clipboard API instead of treating it as a write.
|
|
289
|
+
if (action === 'clear' || value.length === 0) {
|
|
290
|
+
return 'Add-Type -AssemblyName System.Windows.Forms;[System.Windows.Forms.Clipboard]::Clear()';
|
|
291
|
+
}
|
|
292
|
+
const encoded = Buffer.from(value, 'utf16le').toString('base64');
|
|
293
|
+
return `$b=[Convert]::FromBase64String('${encoded}');Set-Clipboard -Value ([Text.Encoding]::Unicode.GetString($b))`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function buildWindowsClipboardReadPowerShell() {
|
|
297
|
+
// Windows PowerShell writes redirected stdout in the OEM code page, which
|
|
298
|
+
// corrupts non-ASCII clipboard text. Return UTF-8 bytes as base64 instead.
|
|
299
|
+
return "$v=Get-Clipboard -Raw;if($null -eq $v){$v=''};[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($v))";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function decodeWindowsClipboardRead(stdout = '') {
|
|
303
|
+
const encoded = String(stdout).trim();
|
|
304
|
+
if (!encoded) return '';
|
|
305
|
+
return Buffer.from(encoded, 'base64').toString('utf8').replace(/\r?\n$/, '');
|
|
306
|
+
}
|
|
307
|
+
|
|
285
308
|
export async function clipboard(args = {}) {
|
|
286
309
|
const action=requireEnum(args.action,'action',['read','write','clear']);
|
|
287
|
-
if(action==='read')
|
|
310
|
+
if(action==='read'){
|
|
311
|
+
const {stdout}=await runPowerShell(buildWindowsClipboardReadPowerShell(),{label:'clipboard read'});
|
|
312
|
+
return text(decodeWindowsClipboardRead(stdout));
|
|
313
|
+
}
|
|
288
314
|
const value=action==='clear'?'':String(args.text??args.value??'');
|
|
289
|
-
|
|
290
|
-
await runPowerShell(`$b=[Convert]::FromBase64String('${encoded}');Set-Clipboard -Value ([Text.Encoding]::Unicode.GetString($b))`,{label:'clipboard write'});
|
|
315
|
+
await runPowerShell(buildWindowsClipboardPowerShell(action, value),{label: action === 'clear' ? 'clipboard clear' : 'clipboard write'});
|
|
291
316
|
return jsonResult({action,length:value.length});
|
|
292
317
|
}
|
|
293
318
|
|
|
@@ -310,8 +335,10 @@ export async function typeTextKeys(value) {
|
|
|
310
335
|
}
|
|
311
336
|
|
|
312
337
|
function mousePrelude() {
|
|
338
|
+
// dwData is a signed wheel delta. Declaring it as uint made the generated
|
|
339
|
+
// negative literal cast throw before the wheel event was sent.
|
|
313
340
|
return String.raw`Add-Type @'
|
|
314
|
-
using System;using System.Runtime.InteropServices;public static class RMMouse{[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint x,uint y,
|
|
341
|
+
using System;using System.Runtime.InteropServices;public static class RMMouse{[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint x,uint y,int d,UIntPtr i);}
|
|
315
342
|
'@`;
|
|
316
343
|
}
|
|
317
344
|
|
|
@@ -337,10 +364,15 @@ export async function dragDrop(args = {}) {
|
|
|
337
364
|
return jsonResult({from:[args.from_x,args.from_y],to:[args.to_x,args.to_y]});
|
|
338
365
|
}
|
|
339
366
|
|
|
367
|
+
export function buildWindowsScrollPowerShell(dx, dy) {
|
|
368
|
+
const wheel = Math.trunc(Number(dy) || 0);
|
|
369
|
+
const horizontal = Math.trunc(Number(dx) || 0);
|
|
370
|
+
return `${mousePrelude()}\n[RMMouse]::mouse_event(0x0800,0,0,${-wheel},[UIntPtr]::Zero);[RMMouse]::mouse_event(0x01000,0,0,${horizontal},[UIntPtr]::Zero)`;
|
|
371
|
+
}
|
|
372
|
+
|
|
340
373
|
export async function scroll(args = {}) {
|
|
341
374
|
const dx=Number(args.delta_x||0),dy=Number(args.delta_y??args.delta??0);
|
|
342
|
-
|
|
343
|
-
await runPowerShell(script,{label:'scroll'});
|
|
375
|
+
await runPowerShell(buildWindowsScrollPowerShell(dx,dy),{label:'scroll'});
|
|
344
376
|
return jsonResult({delta_x:dx,delta_y:dy});
|
|
345
377
|
}
|
|
346
378
|
|
|
@@ -210,9 +210,19 @@ async function copyPortalFile(source, destination) {
|
|
|
210
210
|
try {
|
|
211
211
|
sourceParent = await openDirectoryNoFollow(path.dirname(source));
|
|
212
212
|
destinationParent = await openDirectoryNoFollow(path.dirname(destination));
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
const sourcePath = path.join(sourceParent.anchor, path.basename(source));
|
|
214
|
+
// On Windows O_NOFOLLOW is not available: inspect the leaf explicitly.
|
|
215
|
+
// BigInt identifiers avoid rounding NTFS file IDs above 2^53.
|
|
216
|
+
const before = await lstat(sourcePath, { bigint: true });
|
|
217
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
218
|
+
throw new Error('desktop portal screenshot source cannot be a symbolic link');
|
|
219
|
+
input = await open(sourcePath, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
220
|
+
const info = await input.stat({ bigint: true });
|
|
221
|
+
const current = await lstat(sourcePath, { bigint: true });
|
|
222
|
+
if (!info.isFile() || current.isSymbolicLink() || !current.isFile() ||
|
|
223
|
+
info.dev !== before.dev || info.ino !== before.ino ||
|
|
224
|
+
current.dev !== before.dev || current.ino !== before.ino)
|
|
225
|
+
throw new Error('desktop portal screenshot source changed or is not regular');
|
|
216
226
|
const destinationPath = path.join(destinationParent.anchor, path.basename(destination));
|
|
217
227
|
const existing = await lstat(destinationPath).catch(() => null);
|
|
218
228
|
if (existing?.isSymbolicLink()) throw new Error('desktop portal screenshot destination cannot be a symbolic link');
|
|
@@ -339,7 +339,10 @@ export async function setPermissionsTool(args) {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
|
-
const
|
|
342
|
+
const windowsNote = process.platform === 'win32'
|
|
343
|
+
? '\nWindows applies only the read-only attribute; POSIX group/other bits and ownership are not changed.'
|
|
344
|
+
: '';
|
|
345
|
+
const summary = `Set mode ${raw}${uid !== null || gid !== null ? ` (uid ${uid ?? '-'} gid ${gid ?? '-'})` : ''} on ${changed} path(s) starting at ${displayPath(absolute)}.${windowsNote}`;
|
|
343
346
|
if (!failures.length) return text(summary);
|
|
344
347
|
return text(`${summary}\n${failures.length} path(s) could not be changed:\n${failures.slice(0, 20).join('\n')}`, true);
|
|
345
348
|
}
|
package/src/tools/search.mjs
CHANGED
|
@@ -184,7 +184,9 @@ function runRipgrep(session, { target, pattern, searchType, filePattern, ignoreC
|
|
|
184
184
|
const searchTarget = target.anchor;
|
|
185
185
|
if (ignoreCase) args.push('--ignore-case');
|
|
186
186
|
if (includeHidden) args.push('--hidden');
|
|
187
|
-
|
|
187
|
+
// includeIgnored must override both our skip globs and ripgrep's own ignore files.
|
|
188
|
+
if (includeIgnored) args.push('--no-ignore');
|
|
189
|
+
else for (const glob of SKIP_GLOBS) args.push('-g', glob);
|
|
188
190
|
for (const glob of splitGlobs(filePattern)) args.push('-g', glob);
|
|
189
191
|
if (searchType === 'files') {
|
|
190
192
|
args.push('--files');
|
package/src/tools/terminal.mjs
CHANGED
|
@@ -24,8 +24,16 @@ function shellCommand() {
|
|
|
24
24
|
return process.env.SHELL || '/bin/bash';
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
|
|
27
|
+
function usesWindowsCmd(shell) {
|
|
28
|
+
return process.platform === 'win32' && /(?:^|[\\/])cmd(?:\.exe)?$/i.test(shell);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shellArgs(command, shell) {
|
|
32
|
+
// CMD parses its own quoting: wrap the full command and disable CRT escaping below.
|
|
33
|
+
if (usesWindowsCmd(shell)) return ['/d', '/s', '/c', '"' + command + '"'];
|
|
34
|
+
if (process.platform === 'win32' && /(?:^|[\\/])(?:powershell|pwsh)(?:\.exe)?$/i.test(shell)) {
|
|
35
|
+
return ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command];
|
|
36
|
+
}
|
|
29
37
|
return ['-c', command];
|
|
30
38
|
}
|
|
31
39
|
|
|
@@ -90,11 +98,12 @@ export async function startProcessTool(args, extra = {}) {
|
|
|
90
98
|
const shell = shellCommand();
|
|
91
99
|
// `detached` gives the child its own process group so a session can be stopped as a
|
|
92
100
|
// tree; a pipeline such as `sleep 20 | cat` otherwise survives force_terminate.
|
|
93
|
-
const child = spawn(shell, shellArgs(command), {
|
|
101
|
+
const child = spawn(shell, shellArgs(command, shell), {
|
|
94
102
|
cwd: process.cwd(),
|
|
95
103
|
env: process.env,
|
|
96
104
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
105
|
windowsHide: true,
|
|
106
|
+
windowsVerbatimArguments: usesWindowsCmd(shell),
|
|
98
107
|
detached: process.platform !== 'win32',
|
|
99
108
|
});
|
|
100
109
|
// The error listener has to exist before anything can throw: a bogus REMCP_RUNTIME_SHELL
|