@remcp/runtime 0.2.229 → 0.2.230

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.229",
3
+ "version": "0.2.230",
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",
@@ -1,4 +1,5 @@
1
1
  import { msaaBrowserSnapshot, windowsUiRootScript } from './desktop-windows-msaa.mjs';
2
+ import { quoteWindowsArgument } from './windows-argv.mjs';
2
3
  import path from 'node:path';
3
4
  import process from 'node:process';
4
5
 
@@ -75,7 +76,8 @@ public static class RM {
75
76
  if(target!=0&&current!=target)attachTarget=AttachThreadInput(current,target,true);
76
77
  if(foreground!=0&&current!=foreground&&foreground!=target)attachForeground=AttachThreadInput(current,foreground,true);
77
78
  ShowWindow(h,9);AllowSetForegroundWindow(-1);BringWindowToTop(h);
78
- SetWindowPos(h,new IntPtr(-1),0,0,0,0,0x0001|0x0002|0x0010|0x0040);
79
+ // HWND_TOP preserves the window's existing topmost status.
80
+ SetWindowPos(h,IntPtr.Zero,0,0,0,0,0x0001|0x0002|0x0010|0x0040);
79
81
  return SetForegroundWindow(h);
80
82
  }finally{
81
83
  if(attachForeground)AttachThreadInput(current,foreground,false);
@@ -445,8 +447,8 @@ export async function notification(args = {}) {
445
447
 
446
448
  export async function launchApp(app,argv=[],options={}) {
447
449
  const query=escapePowerShellSingle(String(app));
448
- const argsText=argv.map(v=>`'${escapePowerShellSingle(v)}'`).join(',');
449
- const argumentClause=argv.length?`-ArgumentList @(${argsText})`:'';
450
+ const argsText=escapePowerShellSingle(argv.map(quoteWindowsArgument).join(' '));
451
+ const argumentClause=argv.length?`-ArgumentList '${argsText}'`:'';
450
452
  const hasArgs=argv.length?'$true':'$false';
451
453
  const cwd=options.cwd?escapePowerShellSingle(String(options.cwd)):'';
452
454
  const hasCwd=cwd?'$true':'$false';
@@ -98,21 +98,36 @@ export async function serviceTool(args) {
98
98
  return text(`${policy?.note ? `${policy.note}\n` : ''}${action} requested for service ${name}.`);
99
99
  }
100
100
 
101
+ // Windows Get-WinEvent raises NoMatchingEventsFound (and exits nonzero) when a
102
+ // bounded time range has no events. Empty logs are not failed diagnostics, but
103
+ // access denied/invalid log queries must still fail closed.
104
+ export function buildWindowsEventLogScript({ log, seconds, filter, limit }) {
105
+ const safeLog = escapePowerShellSingle(log);
106
+ const where = filter ? ` | Where-Object { $_.Message -like '*${escapePowerShellSingle(filter)}*' -or $_.ProviderName -like '*${escapePowerShellSingle(filter)}*' }` : '';
107
+ const pipeline = `Get-WinEvent -FilterHashtable @{LogName='${safeLog}';StartTime=(Get-Date).AddSeconds(-${seconds})} -ErrorAction Stop${where} | Select-Object -First ${limit} TimeCreated,Id,LevelDisplayName,ProviderName,Message | ConvertTo-Json -Compress -Depth 3`;
108
+ return `try { ${pipeline} } catch { if ([string]$_.FullyQualifiedErrorId -like 'NoMatchingEventsFound,*') { '[]' } else { throw } }`;
109
+ }
110
+
111
+ // Unified log records the invocation of `/usr/bin/log show` itself, whose
112
+ // arguments contain the sought text. Exclude that self-generated event.
113
+ export function macosEventLogPredicate(filter) {
114
+ return `eventMessage CONTAINS[c] ${JSON.stringify(filter)} AND process != "log"`;
115
+ }
116
+
101
117
  export async function eventLog(args) {
102
118
  const limit = clamp(args.limit, 200, 1, 5000);
103
119
  const since = durationSpec(args.since);
104
120
  const filter = optionalString(args.filter || args.query);
105
121
  if (process.platform === 'win32') {
106
- const log = escapePowerShellSingle(optionalString(args.log) || 'System');
122
+ const log = optionalString(args.log) || 'System';
107
123
  const n = Number.parseInt(since, 10), unit = since.at(-1);
108
124
  const seconds = n * ({ s:1, m:60, h:3600, d:86400 }[unit] || 60);
109
- const where = filter ? ` | Where-Object { $_.Message -like '*${escapePowerShellSingle(filter)}*' -or $_.ProviderName -like '*${escapePowerShellSingle(filter)}*' }` : '';
110
- const script = `Get-WinEvent -FilterHashtable @{LogName='${log}';StartTime=(Get-Date).AddSeconds(-${seconds})} -ErrorAction SilentlyContinue${where} | Select-Object -First ${limit} TimeCreated,Id,LevelDisplayName,ProviderName,Message | ConvertTo-Json -Compress -Depth 3`;
125
+ const script = buildWindowsEventLogScript({ log, seconds, filter, limit });
111
126
  return text((await runPowerShell(script, { label: 'event log', timeout: 30_000 })).stdout.trim() || '[]');
112
127
  }
113
128
  if (process.platform === 'darwin') {
114
129
  const argv = ['show','--last',since,'--style','ndjson'];
115
- if (filter) argv.push('--predicate', `eventMessage CONTAINS[c] ${JSON.stringify(filter)}`);
130
+ if (filter) argv.push('--predicate', macosEventLogPredicate(filter));
116
131
  const { stdout } = await runFileHeadLines('/usr/bin/log', argv, limit, { label: 'macOS unified log', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 });
117
132
  return text(stdout);
118
133
  }
@@ -0,0 +1,9 @@
1
+ // Start-Process joins ArgumentList into one command-line string. Encode each
2
+ // argument using the Windows CRT quoting rules before handing that string over.
3
+ export function quoteWindowsArgument(value) {
4
+ const argument = String(value);
5
+ if (argument.includes('\0')) throw new Error('Application arguments must not contain NUL');
6
+ const escaped = argument.replace(/(\\*)"/g, (_, slashes) => `${slashes}${slashes}\\"`)
7
+ .replace(/\\+$/g, slashes => slashes + slashes);
8
+ return `"${escaped}"`;
9
+ }
@@ -334,14 +334,28 @@ export async function moveToTrashTool(args) {
334
334
  destination = trash;
335
335
  } catch { destination = null; }
336
336
  }
337
- if (!destination) destination = path.join(path.dirname(source), '.remcp-trash');
338
- const existingDestination = await lstat(destination).catch(() => null);
339
- if (existingDestination?.isSymbolicLink()) fail('Refusing to use a symlink as the trash directory');
340
- if (existingDestination && !existingDestination.isDirectory()) fail('Trash destination is not a directory');
341
- await ensureDirectoryPath(destination);
342
- destination = await resolveSafePath(destination, 'trash');
343
- const destinationInfo = await lstat(destination);
344
- if (!destinationInfo.isDirectory() || destinationInfo.isSymbolicLink()) fail('Refusing to use a symlink as the trash directory');
337
+ const localTrash = path.join(path.dirname(source), '.remcp-trash');
338
+ if (!destination) destination = localTrash;
339
+ const prepareTrash = async candidate => {
340
+ const existing = await lstat(candidate).catch(() => null);
341
+ if (existing?.isSymbolicLink()) fail('Refusing to use a symlink as the trash directory');
342
+ if (existing && !existing.isDirectory()) fail('Trash destination is not a directory');
343
+ await ensureDirectoryPath(candidate);
344
+ const safe = await resolveSafePath(candidate, 'trash');
345
+ const info = await lstat(safe);
346
+ if (!info.isDirectory() || info.isSymbolicLink()) fail('Refusing to use a symlink as the trash directory');
347
+ return { path: safe, device: info.dev };
348
+ };
349
+ const sourceInfo = await lstat(source).catch(() => fail(`Path not found: ${displayPath(source)}`));
350
+ if (sourceInfo.isSymbolicLink()) fail('source cannot be a symbolic link');
351
+ let prepared = await prepareTrash(destination);
352
+ // A same-volume rename keeps an entire directory recoverable without copying it.
353
+ // If the home trash is on another filesystem, use a guarded sibling trash.
354
+ if (prepared.device !== sourceInfo.dev && sourceInfo.isDirectory()) {
355
+ prepared = await prepareTrash(localTrash);
356
+ if (prepared.device !== sourceInfo.dev) fail('No writable trash directory on the source filesystem');
357
+ }
358
+ destination = prepared.path;
345
359
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
346
360
  let target = path.join(destination, `${stamp}-${path.basename(source)}`);
347
361
  let counter = 1;