@remcp/runtime 0.2.46 → 0.2.48

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.
@@ -0,0 +1,546 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { readFile, stat } from 'node:fs/promises';
4
+ import { image, multi, text } from '../util.mjs';
5
+ import {
6
+ clamp,
7
+ escapePowerShellSingle,
8
+ jsonResult,
9
+ optionalString,
10
+ removeTemp,
11
+ requireEnum,
12
+ runPowerShell,
13
+ spawnDetached,
14
+ tempDir,
15
+ } from './common.mjs';
16
+
17
+ export async function listWindows() {
18
+ const script=String.raw`
19
+ Add-Type @'
20
+ using System;using System.Runtime.InteropServices;
21
+ public static class RM {
22
+ [StructLayout(LayoutKind.Sequential)] public struct RECT{public int Left,Top,Right,Bottom;}
23
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h,out RECT r);
24
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
25
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
26
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
27
+ [DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr h);
28
+ }
29
+ '@
30
+ $o=@();Get-Process|Where-Object{$_.MainWindowHandle -ne 0}|ForEach-Object{
31
+ $h=$_.MainWindowHandle;if([RM]::IsWindowVisible($h)){$r=New-Object RM+RECT;[void][RM]::GetWindowRect($h,[ref]$r);
32
+ $o += [pscustomobject]@{id=('0x{0:X}' -f $h.ToInt64());pid=$_.Id;app=$_.ProcessName;title=$_.MainWindowTitle;active=($h -eq [RM]::GetForegroundWindow());minimized=[RM]::IsIconic($h);maximized=[RM]::IsZoomed($h);x=$r.Left;y=$r.Top;width=$r.Right-$r.Left;height=$r.Bottom-$r.Top}}
33
+ };$o|ConvertTo-Json -Compress`;
34
+ const {stdout}=await runPowerShell(script,{label:'list windows'});
35
+ const parsed=stdout.trim()?JSON.parse(stdout):[];
36
+ return jsonResult(Array.isArray(parsed)?parsed:[parsed]);
37
+ }
38
+
39
+ function selectorScript(args) {
40
+ const id=optionalString(args.id);
41
+ const idExpr=id?.startsWith('0x')?`[IntPtr]${Number.parseInt(id,16)}`:'[IntPtr]::Zero';
42
+ const pid=Number.isInteger(Number(args.pid))?Number(args.pid):0;
43
+ const app=escapePowerShellSingle(optionalString(args.app)||'');
44
+ const title=escapePowerShellSingle(optionalString(args.title)||'');
45
+ return `$h=${idExpr};if($h -eq [IntPtr]::Zero){$p=Get-Process|Where-Object{$_.MainWindowHandle -ne 0 -and (${pid} -eq 0 -or $_.Id -eq ${pid}) -and ('${app}' -eq '' -or $_.ProcessName -like '*${app}*') -and ('${title}' -eq '' -or $_.MainWindowTitle -like '*${title}*')}|Select-Object -First 1;if(-not $p){throw 'No matching window found'};$h=$p.MainWindowHandle}`;
46
+ }
47
+
48
+ export async function windowAction(args = {}) {
49
+ const action=requireEnum(args.action,'action',['focus','minimize','maximize','restore','move','resize','move_resize','close']);
50
+ const x=Number(args.x),y=Number(args.y),width=Number(args.width),height=Number(args.height);
51
+ const script=String.raw`
52
+ Add-Type @'
53
+ using System;using System.Runtime.InteropServices;
54
+ public static class RM {
55
+ [StructLayout(LayoutKind.Sequential)] public struct RECT{public int Left,Top,Right,Bottom;}
56
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h,int c);
57
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
58
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
59
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h,IntPtr p);
60
+ [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint from,uint to,bool attach);
61
+ [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
62
+ [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
63
+ [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h,IntPtr after,int x,int y,int cx,int cy,uint flags);
64
+ [DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int pid);
65
+ [DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr h,int x,int y,int w,int h2,bool r);
66
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h,out RECT r);
67
+ [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h,uint m,IntPtr w,IntPtr l);
68
+ public static bool FocusWindow(IntPtr h){
69
+ uint current=GetCurrentThreadId(),target=GetWindowThreadProcessId(h,IntPtr.Zero);
70
+ IntPtr fg=GetForegroundWindow();uint foreground=fg==IntPtr.Zero?0:GetWindowThreadProcessId(fg,IntPtr.Zero);
71
+ bool attachTarget=false,attachForeground=false;
72
+ try{
73
+ if(target!=0&&current!=target)attachTarget=AttachThreadInput(current,target,true);
74
+ if(foreground!=0&&current!=foreground&&foreground!=target)attachForeground=AttachThreadInput(current,foreground,true);
75
+ ShowWindow(h,9);AllowSetForegroundWindow(-1);BringWindowToTop(h);
76
+ SetWindowPos(h,new IntPtr(-1),0,0,0,0,0x0001|0x0002|0x0010|0x0040);
77
+ return SetForegroundWindow(h);
78
+ }finally{
79
+ if(attachForeground)AttachThreadInput(current,foreground,false);
80
+ if(attachTarget)AttachThreadInput(current,target,false);
81
+ }
82
+ }
83
+ }
84
+ '@
85
+ ${selectorScript(args)}
86
+ $r=New-Object RM+RECT;[void][RM]::GetWindowRect($h,[ref]$r)
87
+ switch('${action}'){
88
+ 'focus'{if(-not [RM]::FocusWindow($h)){throw 'Windows refused foreground focus'}}
89
+ 'minimize'{[void][RM]::ShowWindow($h,6)}
90
+ 'maximize'{[void][RM]::ShowWindow($h,3)}
91
+ 'restore'{[void][RM]::ShowWindow($h,9)}
92
+ 'close'{[void][RM]::PostMessage($h,0x0010,[IntPtr]::Zero,[IntPtr]::Zero)}
93
+ 'move'{[void][RM]::MoveWindow($h,${Number.isFinite(x)?Math.trunc(x):0},${Number.isFinite(y)?Math.trunc(y):0},$r.Right-$r.Left,$r.Bottom-$r.Top,$true)}
94
+ 'resize'{[void][RM]::MoveWindow($h,$r.Left,$r.Top,${Number.isFinite(width)?Math.trunc(width):800},${Number.isFinite(height)?Math.trunc(height):600},$true)}
95
+ 'move_resize'{[void][RM]::MoveWindow($h,${Number.isFinite(x)?Math.trunc(x):0},${Number.isFinite(y)?Math.trunc(y):0},${Number.isFinite(width)?Math.trunc(width):800},${Number.isFinite(height)?Math.trunc(height):600},$true)}
96
+ }
97
+ `;
98
+ await runPowerShell(script,{label:'window action'});
99
+ return text(`Window action ${action} completed.`);
100
+ }
101
+
102
+ async function msaaBrowserSnapshot(args = {}) {
103
+ const maxNodes = clamp(args.max_nodes, 1200, 1, 5000);
104
+ const maxDepth = clamp(args.max_depth, 64, 1, 128);
105
+ const script = String.raw`
106
+ Add-Type -AssemblyName UIAutomationClient;Add-Type -AssemblyName UIAutomationTypes;Add-Type -AssemblyName Accessibility
107
+ ${windowsUiRootScript(args)}
108
+ Add-Type @'
109
+ using System;
110
+ using System.Runtime.InteropServices;
111
+ public static class RMAcc {
112
+ [DllImport("oleacc.dll")]
113
+ public static extern int AccessibleObjectFromWindow(IntPtr hwnd,uint dwId,ref Guid riid,[MarshalAs(UnmanagedType.Interface)] out object ppvObject);
114
+ public static object FromWindow(IntPtr hwnd) {
115
+ Guid iid=new Guid("618736E0-3C3D-11CF-810C-00AA00389B71");
116
+ object value=null;
117
+ int hr=AccessibleObjectFromWindow(hwnd,0xFFFFFFFCu,ref iid,out value);
118
+ if(hr<0) Marshal.ThrowExceptionForHR(hr);
119
+ return value;
120
+ }
121
+ }
122
+ '@
123
+ $rootAcc=[Accessibility.IAccessible][RMAcc]::FromWindow($h)
124
+ $nodes=New-Object System.Collections.ArrayList
125
+ $seen=0
126
+ $truncated=$false
127
+ $interactive=@(30,33,34,36,37,43,44,45,46,47,51,56,60,62)
128
+ $informative=@(15,20,22,24,31,32,40,41,42)
129
+ function Get-RoleName([int]$role){
130
+ switch($role){
131
+ 15{'Document'};20{'Group'};22{'Toolbar'};24{'Table'};30{'Link'};33{'List'};34{'ListItem'};35{'Tree'};36{'TreeItem'};37{'TabItem'};40{'Graphic'};41{'Text'};42{'Edit'};43{'Button'};44{'CheckBox'};45{'RadioButton'};46{'ComboBox'};47{'DropList'};51{'Slider'};56{'Button'};60{'TabList'};62{'Button'};default{'Accessible'}
132
+ }
133
+ }
134
+ function Walk-Acc($acc,[int]$depth,[string]$path,[bool]$inDocument,[string]$parentId){
135
+ if($null -eq $acc -or $depth -gt ${maxDepth} -or $script:seen -ge ${maxNodes}){if($script:seen -ge ${maxNodes}){$script:truncated=$true};return}
136
+ $script:seen++
137
+ try{$roleRaw=$acc.get_accRole(0);$role=[int]$roleRaw}catch{$role=-1}
138
+ try{$state=[int]$acc.get_accState(0)}catch{$state=0}
139
+ $invisible=(($state -band 0x8000) -ne 0) -or (($state -band 0x10000) -ne 0)
140
+ if($depth -gt 0 -and $invisible){return}
141
+ $isDocument=$role -eq 15
142
+ $inside=$inDocument -or $isDocument
143
+ $l=0;$t=0;$w=0;$hh=0;$hasRect=$true
144
+ try{$acc.accLocation([ref]$l,[ref]$t,[ref]$w,[ref]$hh,0)}catch{$hasRect=$false}
145
+ try{$name=[string]$acc.get_accName(0)}catch{$name=''}
146
+ try{$value=[string]$acc.get_accValue(0)}catch{$value=''}
147
+ $visible=$hasRect -and $w -gt 0 -and $hh -gt 0 -and -not $invisible
148
+ if($inside -and $visible -and (($interactive -contains $role) -or ($informative -contains $role) -or $name -or $value)){
149
+ $id='msaa:'+$rootPid+':'+$path
150
+ $patterns=New-Object System.Collections.ArrayList
151
+ if($role -in @(30,43,56,62)){[void]$patterns.Add('invoke')}
152
+ if($role -in @(42,46,47)){[void]$patterns.Add('value')}
153
+ if($role -in @(34,36,37,45)){[void]$patterns.Add('selection_item')}
154
+ if($role -eq 44){[void]$patterns.Add('toggle')}
155
+ if($role -eq 51){[void]$patterns.Add('range_value')}
156
+ [void]$nodes.Add([pscustomobject]@{
157
+ id=$id;parent_id=$parentId;depth=$depth;active=($depth -eq 0);pid=$rootPid;app=$rootApp;window=$windowName;
158
+ name=$name;role=(Get-RoleName $role);localized_role=(Get-RoleName $role);automationId='';className='';frameworkId='MSAA';
159
+ enabled=$true;offscreen=$false;focused=(($state -band 0x4) -ne 0);focusable=(($state -band 0x100000) -ne 0);password=$false;
160
+ value=$value;patterns=@($patterns);x=[int]$l;y=[int]$t;width=[int]$w;height=[int]$hh
161
+ })
162
+ $parentId=$id
163
+ }
164
+ try{$count=[int]$acc.accChildCount}catch{$count=0}
165
+ for($i=1;$i -le $count;$i++){
166
+ if($script:seen -ge ${maxNodes}){$script:truncated=$true;break}
167
+ try{$child=$acc.get_accChild($i)}catch{continue}
168
+ if($child -is [Accessibility.IAccessible]){
169
+ Walk-Acc $child ($depth+1) ($path+'.'+$i) $inside $parentId
170
+ }
171
+ }
172
+ }
173
+ Walk-Acc $rootAcc 0 'r' $false $null
174
+ [pscustomobject]@{platform='win32';count=$nodes.Count;nodes=@($nodes);truncated=$truncated;max_nodes=${maxNodes};browser_dom=[pscustomobject]@{requested=$true;available=($nodes.Count -gt 0);provider='msaa-iaccessible'}}|ConvertTo-Json -Compress -Depth 7`;
175
+ const { stdout } = await runPowerShell(script, { label:'MSAA browser accessibility snapshot', timeout:30_000 });
176
+ return stdout.trim() ? JSON.parse(stdout) : { platform:'win32', count:0, nodes:[], browser_dom:{ requested:true, available:false, provider:'msaa-iaccessible' } };
177
+ }
178
+
179
+ function windowsUiRootScript(args = {}) {
180
+ const pid = Number.isInteger(Number(args.pid)) ? Number(args.pid) : 0;
181
+ const app = escapePowerShellSingle(optionalString(args.app) || '');
182
+ const title = escapePowerShellSingle(optionalString(args.window_title || args.windowTitle) || '');
183
+ const scoped = pid > 0 || app || title;
184
+ const selector = scoped
185
+ ? `$p=Get-Process|Where-Object{$_.MainWindowHandle -ne 0 -and (${pid} -eq 0 -or $_.Id -eq ${pid}) -and ('${app}' -eq '' -or $_.ProcessName -like '*${app}*') -and ('${title}' -eq '' -or $_.MainWindowTitle -like '*${title}*')}|Select-Object -First 1;if(-not $p){throw 'No matching UI window found'};$h=$p.MainWindowHandle`
186
+ : '$h=[RMFG]::GetForegroundWindow()';
187
+ return String.raw`
188
+ Add-Type @'
189
+ using System;using System.Runtime.InteropServices;
190
+ public static class RMFG{
191
+ [DllImport("user32.dll")]public static extern IntPtr GetForegroundWindow();
192
+ }
193
+ '@
194
+ $h=[IntPtr]::Zero
195
+ ${selector}
196
+ if($h -eq [IntPtr]::Zero){throw 'No foreground UI window is available'}
197
+ $root=[System.Windows.Automation.AutomationElement]::FromHandle($h)
198
+ if(-not $root){throw 'Could not create UI Automation root'}
199
+ $windowName=$root.Current.Name
200
+ $rootPid=$root.Current.ProcessId
201
+ try{$rootApp=(Get-Process -Id $rootPid -ErrorAction Stop).ProcessName}catch{$rootApp=''}
202
+ `;
203
+ }
204
+
205
+ export async function uiSnapshot(args = {}) {
206
+ const maxNodes = clamp(args.max_nodes, 500, 1, 5000);
207
+ const maxDepth = clamp(args.max_depth, 8, 1, 32);
208
+ const browserDom = args.browser_dom === true;
209
+ const script = String.raw`
210
+ Add-Type -AssemblyName UIAutomationClient;Add-Type -AssemblyName UIAutomationTypes
211
+ ${windowsUiRootScript(args)}
212
+ $browserProvider=$null
213
+ if(${browserDom ? '$true' : '$false'}){
214
+ try{
215
+ $cond=New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::AutomationIdProperty,'RootWebArea')
216
+ $dom=$root.FindFirst([System.Windows.Automation.TreeScope]::Subtree,$cond)
217
+ if($dom){$root=$dom;$browserProvider='uia-rootwebarea'}
218
+ }catch{}
219
+ }
220
+ $cache=New-Object System.Windows.Automation.CacheRequest
221
+ $cache.TreeScope=[System.Windows.Automation.TreeScope]::Element
222
+ @(
223
+ [System.Windows.Automation.AutomationElement]::NameProperty,
224
+ [System.Windows.Automation.AutomationElement]::AutomationIdProperty,
225
+ [System.Windows.Automation.AutomationElement]::LocalizedControlTypeProperty,
226
+ [System.Windows.Automation.AutomationElement]::AcceleratorKeyProperty,
227
+ [System.Windows.Automation.AutomationElement]::ClassNameProperty,
228
+ [System.Windows.Automation.AutomationElement]::ControlTypeProperty,
229
+ [System.Windows.Automation.AutomationElement]::FrameworkIdProperty,
230
+ [System.Windows.Automation.AutomationElement]::IsEnabledProperty,
231
+ [System.Windows.Automation.AutomationElement]::IsOffscreenProperty,
232
+ [System.Windows.Automation.AutomationElement]::HasKeyboardFocusProperty,
233
+ [System.Windows.Automation.AutomationElement]::IsKeyboardFocusableProperty,
234
+ [System.Windows.Automation.AutomationElement]::IsPasswordProperty,
235
+ [System.Windows.Automation.AutomationElement]::BoundingRectangleProperty,
236
+ [System.Windows.Automation.AutomationElement]::HelpTextProperty,
237
+ [System.Windows.Automation.AutomationElement]::ProcessIdProperty
238
+ )|ForEach-Object{$cache.Add($_)}
239
+ @(
240
+ [System.Windows.Automation.InvokePattern]::Pattern,
241
+ [System.Windows.Automation.ValuePattern]::Pattern,
242
+ [System.Windows.Automation.TogglePattern]::Pattern,
243
+ [System.Windows.Automation.SelectionItemPattern]::Pattern,
244
+ [System.Windows.Automation.ExpandCollapsePattern]::Pattern,
245
+ [System.Windows.Automation.RangeValuePattern]::Pattern,
246
+ [System.Windows.Automation.ScrollPattern]::Pattern,
247
+ [System.Windows.Automation.ScrollItemPattern]::Pattern,
248
+ [System.Windows.Automation.LegacyIAccessiblePattern]::Pattern
249
+ )|ForEach-Object{try{$cache.Add($_)}catch{}}
250
+ try{$root=$root.GetUpdatedCache($cache)}catch{}
251
+ $q=New-Object System.Collections.Queue
252
+ $q.Enqueue([pscustomobject]@{e=$root;d=0;parent=$null})
253
+ $o=New-Object System.Collections.ArrayList
254
+ $seen=0
255
+ while($q.Count -gt 0 -and $seen -lt ${maxNodes}){
256
+ $item=$q.Dequeue();$e=$item.e;$d=[int]$item.d;$parent=$item.parent;$seen++
257
+ try{
258
+ $c=$e.Cached
259
+ $r=$c.BoundingRectangle
260
+ try{$rid=($e.GetRuntimeId()-join '.')}catch{$rid=($rootPid.ToString()+'.'+$seen.ToString())}
261
+ $id='win:'+$rid
262
+ $patterns=New-Object System.Collections.ArrayList
263
+ $value=$null;$toggleState=$null;$expandState=$null;$rangeValue=$null;$rangeMin=$null;$rangeMax=$null
264
+ $selected=$null;$scrollH=$false;$scrollV=$false;$scrollHPct=$null;$scrollVPct=$null;$defaultAction=$null
265
+ try{$p=$e.GetCachedPattern([System.Windows.Automation.InvokePattern]::Pattern);if($p){[void]$patterns.Add('invoke')}}catch{}
266
+ try{$p=[System.Windows.Automation.ValuePattern]$e.GetCachedPattern([System.Windows.Automation.ValuePattern]::Pattern);if($p){[void]$patterns.Add('value');$value=$p.Cached.Value}}catch{}
267
+ try{$p=[System.Windows.Automation.TogglePattern]$e.GetCachedPattern([System.Windows.Automation.TogglePattern]::Pattern);if($p){[void]$patterns.Add('toggle');$toggleState=$p.Cached.ToggleState.ToString().ToLowerInvariant()}}catch{}
268
+ try{$p=[System.Windows.Automation.SelectionItemPattern]$e.GetCachedPattern([System.Windows.Automation.SelectionItemPattern]::Pattern);if($p){[void]$patterns.Add('selection_item');$selected=$p.Cached.IsSelected}}catch{}
269
+ try{$p=[System.Windows.Automation.ExpandCollapsePattern]$e.GetCachedPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern);if($p){[void]$patterns.Add('expand_collapse');$expandState=$p.Cached.ExpandCollapseState.ToString().ToLowerInvariant()}}catch{}
270
+ try{$p=[System.Windows.Automation.RangeValuePattern]$e.GetCachedPattern([System.Windows.Automation.RangeValuePattern]::Pattern);if($p){[void]$patterns.Add('range_value');$rangeValue=$p.Cached.Value;$rangeMin=$p.Cached.Minimum;$rangeMax=$p.Cached.Maximum}}catch{}
271
+ try{$p=[System.Windows.Automation.ScrollPattern]$e.GetCachedPattern([System.Windows.Automation.ScrollPattern]::Pattern);if($p){[void]$patterns.Add('scroll');$scrollH=$p.Cached.HorizontallyScrollable;$scrollV=$p.Cached.VerticallyScrollable;$scrollHPct=$p.Cached.HorizontalScrollPercent;$scrollVPct=$p.Cached.VerticalScrollPercent}}catch{}
272
+ try{$p=$e.GetCachedPattern([System.Windows.Automation.ScrollItemPattern]::Pattern);if($p){[void]$patterns.Add('scroll_item')}}catch{}
273
+ try{$p=[System.Windows.Automation.LegacyIAccessiblePattern]$e.GetCachedPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern);if($p){[void]$patterns.Add('legacy_iaccessible');$defaultAction=$p.Cached.DefaultAction;if($null -eq $value -or $value -eq ''){$value=$p.Cached.Value}}}catch{}
274
+ [void]$o.Add([pscustomobject]@{
275
+ id=$id;parent_id=$parent;depth=$d;active=($d -eq 0);pid=$c.ProcessId;app=$rootApp;window=$windowName;
276
+ name=$c.Name;role=$c.ControlType.ProgrammaticName.Replace('ControlType.','');localized_role=$c.LocalizedControlType;
277
+ automationId=$c.AutomationId;className=$c.ClassName;frameworkId=$c.FrameworkId;enabled=$c.IsEnabled;offscreen=$c.IsOffscreen;
278
+ focused=$c.HasKeyboardFocus;focusable=$c.IsKeyboardFocusable;password=$c.IsPassword;help_text=$c.HelpText;shortcut=$c.AcceleratorKey;
279
+ value=$value;patterns=@($patterns);toggle_state=$toggleState;expand_collapse_state=$expandState;selected=$selected;
280
+ range_value=$rangeValue;range_min=$rangeMin;range_max=$rangeMax;scrollable=($scrollH -or $scrollV);
281
+ horizontal_scrollable=$scrollH;vertical_scrollable=$scrollV;horizontal_scroll_percent=$scrollHPct;vertical_scroll_percent=$scrollVPct;
282
+ default_action=$defaultAction;x=[int]$r.X;y=[int]$r.Y;width=[int]$r.Width;height=[int]$r.Height
283
+ })
284
+ if($d -lt ${maxDepth}){
285
+ try{$activation=$cache.Activate();try{$children=$e.FindAll([System.Windows.Automation.TreeScope]::Children,[System.Windows.Automation.Condition]::TrueCondition)}finally{if($activation){$activation.Dispose()}}}
286
+ catch{$children=$null}
287
+ if($children){for($i=0;$i -lt $children.Count;$i++){$q.Enqueue([pscustomobject]@{e=$children.Item($i);d=$d+1;parent=$id})}}
288
+ }
289
+ }catch{}
290
+ }
291
+ [pscustomobject]@{platform='win32';count=$o.Count;nodes=@($o);truncated=($q.Count -gt 0);max_nodes=${maxNodes};browser_dom=[pscustomobject]@{requested=${browserDom ? '$true' : '$false'};available=($null -ne $browserProvider);provider=$browserProvider}}|ConvertTo-Json -Compress -Depth 8`;
292
+ const { stdout } = await runPowerShell(script, { label:'UI snapshot', timeout:30_000 });
293
+ const parsed = stdout.trim() ? JSON.parse(stdout) : { platform:'win32', count:0, nodes:[] };
294
+ if (browserDom && !parsed?.browser_dom?.available) {
295
+ const fallback = await msaaBrowserSnapshot(args).catch(() => null);
296
+ if (fallback?.nodes?.length) return jsonResult(fallback);
297
+ }
298
+ return jsonResult(parsed);
299
+ }
300
+
301
+ export async function uiAction(args = {}) {
302
+ const action = requireEnum(args.action, 'action', [
303
+ 'click','invoke','focus','set_value','select','toggle','expand','collapse',
304
+ 'scroll_into_view','set_range_value','add_to_selection','remove_from_selection',
305
+ ]);
306
+ const id = escapePowerShellSingle(optionalString(args.id) || '');
307
+ const name = escapePowerShellSingle(optionalString(args.name) || '');
308
+ const role = escapePowerShellSingle(optionalString(args.role) || '');
309
+ const automationId = escapePowerShellSingle(optionalString(args.automation_id || args.automationId) || '');
310
+ const value = escapePowerShellSingle(String(args.value ?? ''));
311
+ const numericValue = Number(args.value);
312
+ if (action === 'set_range_value' && !Number.isFinite(numericValue)) throw new Error('set_range_value requires a finite numeric value');
313
+ const script = String.raw`
314
+ Add-Type -AssemblyName UIAutomationClient;Add-Type -AssemblyName UIAutomationTypes
315
+ ${windowsUiRootScript(args)}
316
+ Add-Type @'
317
+ using System;using System.Runtime.InteropServices;
318
+ public static class RMMouse{
319
+ [DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);
320
+ [DllImport("user32.dll")]public static extern void mouse_event(uint f,uint x,uint y,uint d,UIntPtr i);
321
+ }
322
+ '@
323
+ $q=New-Object System.Collections.Queue;$q.Enqueue($root);$found=$null;$visited=0
324
+ while($q.Count -gt 0 -and -not $found -and $visited -lt 10000){
325
+ $e=$q.Dequeue();$visited++
326
+ try{
327
+ $rid='win:'+($e.GetRuntimeId()-join '.')
328
+ $r=$e.Current.ControlType.ProgrammaticName.Replace('ControlType.','')
329
+ if(('${id}' -eq '' -or $rid -eq '${id}') -and ('${name}' -eq '' -or $e.Current.Name -like '*${name}*') -and ('${role}' -eq '' -or $r -like '*${role}*') -and ('${automationId}' -eq '' -or $e.Current.AutomationId -eq '${automationId}')){$found=$e;break}
330
+ $children=$e.FindAll([System.Windows.Automation.TreeScope]::Children,[System.Windows.Automation.Condition]::TrueCondition)
331
+ for($i=0;$i -lt $children.Count;$i++){$q.Enqueue($children.Item($i))}
332
+ }catch{}
333
+ }
334
+ if(-not $found){throw 'UI element not found'}
335
+ $used=$null
336
+ switch('${action}'){
337
+ 'focus'{[void]$found.SetFocus();$used='SetFocus'}
338
+ 'invoke'{
339
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern,[ref]$p)){([System.Windows.Automation.InvokePattern]$p).Invoke();$used='InvokePattern'}else{throw 'InvokePattern unavailable'}
340
+ }
341
+ 'click'{
342
+ $p=$null
343
+ if($found.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern,[ref]$p)){([System.Windows.Automation.InvokePattern]$p).Invoke();$used='InvokePattern'}
344
+ else{
345
+ $pt=New-Object System.Windows.Point
346
+ $hasPoint=$false
347
+ try{$hasPoint=$found.TryGetClickablePoint([ref]$pt)}catch{}
348
+ if(-not $hasPoint){$b=$found.Current.BoundingRectangle;$pt=New-Object System.Windows.Point(($b.X+$b.Width/2),($b.Y+$b.Height/2))}
349
+ [void][RMMouse]::SetCursorPos([int]$pt.X,[int]$pt.Y)
350
+ [RMMouse]::mouse_event(2,0,0,0,[UIntPtr]::Zero);[RMMouse]::mouse_event(4,0,0,0,[UIntPtr]::Zero);$used='pointer'
351
+ }
352
+ }
353
+ 'set_value'{
354
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern,[ref]$p)){([System.Windows.Automation.ValuePattern]$p).SetValue('${value}');$used='ValuePattern'}else{throw 'ValuePattern unavailable'}
355
+ }
356
+ 'select'{
357
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern,[ref]$p)){([System.Windows.Automation.SelectionItemPattern]$p).Select();$used='SelectionItemPattern.Select'}else{throw 'SelectionItemPattern unavailable'}
358
+ }
359
+ 'add_to_selection'{
360
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern,[ref]$p)){([System.Windows.Automation.SelectionItemPattern]$p).AddToSelection();$used='SelectionItemPattern.AddToSelection'}else{throw 'SelectionItemPattern unavailable'}
361
+ }
362
+ 'remove_from_selection'{
363
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern,[ref]$p)){([System.Windows.Automation.SelectionItemPattern]$p).RemoveFromSelection();$used='SelectionItemPattern.RemoveFromSelection'}else{throw 'SelectionItemPattern unavailable'}
364
+ }
365
+ 'toggle'{
366
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern,[ref]$p)){([System.Windows.Automation.TogglePattern]$p).Toggle();$used='TogglePattern'}else{throw 'TogglePattern unavailable'}
367
+ }
368
+ 'expand'{
369
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern,[ref]$p)){([System.Windows.Automation.ExpandCollapsePattern]$p).Expand();$used='ExpandCollapsePattern.Expand'}else{throw 'ExpandCollapsePattern unavailable'}
370
+ }
371
+ 'collapse'{
372
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern,[ref]$p)){([System.Windows.Automation.ExpandCollapsePattern]$p).Collapse();$used='ExpandCollapsePattern.Collapse'}else{throw 'ExpandCollapsePattern unavailable'}
373
+ }
374
+ 'scroll_into_view'{
375
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.ScrollItemPattern]::Pattern,[ref]$p)){([System.Windows.Automation.ScrollItemPattern]$p).ScrollIntoView();$used='ScrollItemPattern'}else{throw 'ScrollItemPattern unavailable'}
376
+ }
377
+ 'set_range_value'{
378
+ $p=$null;if($found.TryGetCurrentPattern([System.Windows.Automation.RangeValuePattern]::Pattern,[ref]$p)){([System.Windows.Automation.RangeValuePattern]$p).SetValue([double]${numericValue});$used='RangeValuePattern'}else{throw 'RangeValuePattern unavailable'}
379
+ }
380
+ }
381
+ [pscustomobject]@{id=('win:'+($found.GetRuntimeId()-join '.'));name=$found.Current.Name;role=$found.Current.ControlType.ProgrammaticName.Replace('ControlType.','');action='${action}';backend=$used}|ConvertTo-Json -Compress`;
382
+ const raw = (await runPowerShell(script, { label:'UI action', timeout:30_000 })).stdout.trim();
383
+ return jsonResult(raw ? JSON.parse(raw) : { action });
384
+ }
385
+
386
+ export async function clipboard(args = {}) {
387
+ const action=requireEnum(args.action,'action',['read','write','clear']);
388
+ if(action==='read')return text((await runPowerShell("$v=Get-Clipboard -Raw;if($null -eq $v){$v=''};$v",{label:'clipboard read'})).stdout.replace(/\r?\n$/,''));
389
+ const value=action==='clear'?'':String(args.text??args.value??'');
390
+ const encoded=Buffer.from(value,'utf16le').toString('base64');
391
+ await runPowerShell(`$b=[Convert]::FromBase64String('${encoded}');Set-Clipboard -Value ([Text.Encoding]::Unicode.GetString($b))`,{label:'clipboard write'});
392
+ return jsonResult({action,length:value.length});
393
+ }
394
+
395
+ export async function keyboard(args = {}) {
396
+ const shortcut=optionalString(args.shortcut)||(Array.isArray(args.keys)?args.keys.join('+'):optionalString(args.key));
397
+ if(!shortcut)throw new Error('shortcut or key is required');
398
+ const tokens=shortcut.split('+').map(value=>value.trim()).filter(Boolean);
399
+ const mods={ctrl:'^',control:'^',alt:'%',shift:'+'};
400
+ const special={enter:'{ENTER}',return:'{ENTER}',esc:'{ESC}',escape:'{ESC}',tab:'{TAB}',backspace:'{BACKSPACE}',delete:'{DELETE}',up:'{UP}',down:'{DOWN}',left:'{LEFT}',right:'{RIGHT}',home:'{HOME}',end:'{END}',pageup:'{PGUP}',pagedown:'{PGDN}',space:' '};
401
+ const prefix=tokens.slice(0,-1).map(v=>mods[v.toLowerCase()]||'').join('');
402
+ const key=tokens.at(-1);
403
+ const send=prefix+(special[key.toLowerCase()]||(key.length===1?key:`{${key.toUpperCase()}}`));
404
+ await runPowerShell(`$w=New-Object -ComObject WScript.Shell;$w.SendKeys('${escapePowerShellSingle(send)}')`,{label:'keyboard'});
405
+ return text(`Sent ${shortcut}.`);
406
+ }
407
+
408
+ export async function typeTextKeys(value) {
409
+ const escaped=escapePowerShellSingle(String(value).replace(/[+^%~(){}\[\]]/g,'{$&}'));
410
+ await runPowerShell(`$w=New-Object -ComObject WScript.Shell;$w.SendKeys('${escaped}')`,{label:'type text'});
411
+ }
412
+
413
+ function mousePrelude() {
414
+ return String.raw`Add-Type @'
415
+ 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,uint d,UIntPtr i);}
416
+ '@`;
417
+ }
418
+
419
+ function flags(button='left') {
420
+ const b=String(button).toLowerCase();
421
+ return b==='right'?{down:'0x0008',up:'0x0010'}:b==='middle'?{down:'0x0020',up:'0x0040'}:{down:'0x0002',up:'0x0004'};
422
+ }
423
+
424
+ export async function pointer(args = {}) {
425
+ const action=requireEnum(args.action,'action',['move','click','double_click','right_click','down','up']);
426
+ const x=Number(args.x),y=Number(args.y),f=flags(action==='right_click'?'right':args.button);
427
+ const clicks=action==='double_click'?2:1;
428
+ const op=action==='move'?'':action==='down'?`[RMMouse]::mouse_event(${f.down},0,0,0,[UIntPtr]::Zero)`:action==='up'?`[RMMouse]::mouse_event(${f.up},0,0,0,[UIntPtr]::Zero)`:`for($i=0;$i -lt ${clicks};$i++){[RMMouse]::mouse_event(${f.down},0,0,0,[UIntPtr]::Zero);[RMMouse]::mouse_event(${f.up},0,0,0,[UIntPtr]::Zero);Start-Sleep -Milliseconds 70}`;
429
+ const move=Number.isFinite(x)&&Number.isFinite(y)?`[void][RMMouse]::SetCursorPos(${Math.trunc(x)},${Math.trunc(y)})`:'';
430
+ await runPowerShell(`${mousePrelude()}\n${move}\n${op}`,{label:'pointer'});
431
+ return text(`Pointer ${action} completed.`);
432
+ }
433
+
434
+ export async function dragDrop(args = {}) {
435
+ const f=flags(args.button);
436
+ const script=`${mousePrelude()}\n[void][RMMouse]::SetCursorPos(${Math.trunc(args.from_x)},${Math.trunc(args.from_y)});[RMMouse]::mouse_event(${f.down},0,0,0,[UIntPtr]::Zero);Start-Sleep -Milliseconds ${clamp(args.hold_ms,120,0,5000)};[void][RMMouse]::SetCursorPos(${Math.trunc(args.to_x)},${Math.trunc(args.to_y)});Start-Sleep -Milliseconds ${clamp(args.duration_ms,120,0,5000)};[RMMouse]::mouse_event(${f.up},0,0,0,[UIntPtr]::Zero)`;
437
+ await runPowerShell(script,{label:'drag and drop'});
438
+ return jsonResult({from:[args.from_x,args.from_y],to:[args.to_x,args.to_y]});
439
+ }
440
+
441
+ export async function scroll(args = {}) {
442
+ const dx=Number(args.delta_x||0),dy=Number(args.delta_y??args.delta??0);
443
+ const script=`${mousePrelude()}\n[RMMouse]::mouse_event(0x0800,0,0,[uint32]([int]${Math.trunc(-dy)}),[UIntPtr]::Zero);[RMMouse]::mouse_event(0x01000,0,0,[uint32]([int]${Math.trunc(dx)}),[UIntPtr]::Zero)`;
444
+ await runPowerShell(script,{label:'scroll'});
445
+ return jsonResult({delta_x:dx,delta_y:dy});
446
+ }
447
+
448
+ export async function cursorPosition() {
449
+ const script=String.raw`Add-Type @'
450
+ using System;using System.Runtime.InteropServices;public static class RMCursor{[StructLayout(LayoutKind.Sequential)]public struct POINT{public int X;public int Y;}[DllImport("user32.dll")]public static extern bool GetCursorPos(out POINT p);}
451
+ '@;$p=New-Object RMCursor+POINT;if(-not [RMCursor]::GetCursorPos([ref]$p)){throw 'GetCursorPos failed'};[pscustomobject]@{x=$p.X;y=$p.Y}|ConvertTo-Json -Compress`;
452
+ const raw=(await runPowerShell(script,{label:'cursor position'})).stdout.trim();
453
+ return jsonResult(raw?JSON.parse(raw):{x:null,y:null});
454
+ }
455
+
456
+ export async function displayInventory() {
457
+ const script=String.raw`Add-Type -AssemblyName System.Windows.Forms
458
+ Add-Type @'
459
+ using System;using System.Runtime.InteropServices;
460
+ public static class RMDisplay{
461
+ [StructLayout(LayoutKind.Sequential)]public struct POINT{public int X;public int Y;public POINT(int x,int y){X=x;Y=y;}}
462
+ [DllImport("user32.dll")]public static extern uint GetDpiForSystem();
463
+ [DllImport("user32.dll")]public static extern IntPtr MonitorFromPoint(POINT pt,uint flags);
464
+ [DllImport("Shcore.dll")]public static extern int GetDpiForMonitor(IntPtr monitor,int type,out uint x,out uint y);
465
+ public static uint DpiAt(int x,int y){
466
+ uint fallback=GetDpiForSystem();
467
+ try{IntPtr h=MonitorFromPoint(new POINT(x,y),2);uint dx,dy;if(h!=IntPtr.Zero&&GetDpiForMonitor(h,0,out dx,out dy)==0&&dx>0)return dx;}catch{}
468
+ return fallback;
469
+ }
470
+ }
471
+ '@
472
+ [System.Windows.Forms.Screen]::AllScreens|ForEach-Object{
473
+ $cx=$_.Bounds.X+[int]($_.Bounds.Width/2);$cy=$_.Bounds.Y+[int]($_.Bounds.Height/2);$dpi=[RMDisplay]::DpiAt($cx,$cy);$scale=[Math]::Round($dpi/96.0,3)
474
+ [pscustomobject]@{name=$_.DeviceName;display_name=$_.DeviceName;primary=$_.Primary;x=$_.Bounds.X;y=$_.Bounds.Y;width=$_.Bounds.Width;height=$_.Bounds.Height;pixel_width=$_.Bounds.Width;pixel_height=$_.Bounds.Height;workingX=$_.WorkingArea.X;workingY=$_.WorkingArea.Y;workingWidth=$_.WorkingArea.Width;workingHeight=$_.WorkingArea.Height;scale=$scale;dpi=$dpi}
475
+ }|ConvertTo-Json -Compress`;
476
+ const raw=(await runPowerShell(script,{label:'display inventory'})).stdout.trim();
477
+ const parsed=raw?JSON.parse(raw):[];
478
+ return jsonResult(Array.isArray(parsed)?parsed:[parsed]);
479
+ }
480
+
481
+ export async function screenshotRegion(args = {}) {
482
+ const x=Math.trunc(Number(args.x)),y=Math.trunc(Number(args.y)),width=Math.trunc(Number(args.width)),height=Math.trunc(Number(args.height));
483
+ if(![x,y,width,height].every(Number.isFinite)||width<=0||height<=0)throw new Error('x, y, width and height are required; width/height must be positive');
484
+ if(width>8192||height>8192||width*height>16_777_216)throw new Error('screenshot region is too large; width/height must be <= 8192 and area <= 16 megapixels');
485
+ const dir=await tempDir('remcp-region-'),target=path.join(dir,'region.png');
486
+ try{
487
+ const safe=escapePowerShellSingle(target);
488
+ await runPowerShell(`Add-Type -AssemblyName System.Drawing;$b=New-Object System.Drawing.Bitmap(${width},${height});$g=[System.Drawing.Graphics]::FromImage($b);$g.CopyFromScreen(${x},${y},0,0,$b.Size);$b.Save('${safe}',[System.Drawing.Imaging.ImageFormat]::Png);$g.Dispose();$b.Dispose()`,{label:'screenshot region'});
489
+ const info=await stat(target);if(info.size>4*1024*1024)throw new Error(`captured region is ${info.size} bytes; request a smaller region so it fits the inline image limit`);
490
+ const data=await readFile(target);
491
+ return multi([{type:'text',text:`Captured ${width}x${height} at ${x},${y}.`},image(data.toString('base64'),'image/png')]);
492
+ }finally{await removeTemp(dir);}
493
+ }
494
+
495
+ export async function notification(args = {}) {
496
+ const title=escapePowerShellSingle(optionalString(args.title)||'ReMCP');
497
+ const body=escapePowerShellSingle(String(args.message??args.body??''));
498
+ await runPowerShell(`$w=New-Object -ComObject WScript.Shell;[void]$w.Popup('${body}',${clamp(args.timeout_seconds,5,1,60)},'${title}',0x40)`,{label:'notification'});
499
+ return text('Notification sent.');
500
+ }
501
+
502
+ export async function launchApp(app,argv=[],options={}) {
503
+ const query=escapePowerShellSingle(String(app));
504
+ const argsText=argv.map(v=>`'${escapePowerShellSingle(v)}'`).join(',');
505
+ const argumentClause=argv.length?`-ArgumentList @(${argsText})`:'';
506
+ const hasArgs=argv.length?'$true':'$false';
507
+ const cwd=options.cwd?escapePowerShellSingle(String(options.cwd)):'';
508
+ const hasCwd=cwd?'$true':'$false';
509
+ const cwdClause=cwd?`-WorkingDirectory '${cwd}'`:'';
510
+ const script=String.raw`
511
+ $ErrorActionPreference='Stop'
512
+ $name='${query}'
513
+ $mode='direct';$pidValue=$null;$resolved=$name
514
+ try{
515
+ $p=Start-Process -FilePath $name ${argumentClause} ${cwdClause} -PassThru -ErrorAction Stop
516
+ $pidValue=$p.Id
517
+ }catch{
518
+ if(${hasArgs} -or ${hasCwd}){throw}
519
+ $apps=@()
520
+ if(Get-Command Get-StartApps -ErrorAction SilentlyContinue){$apps=@(Get-StartApps)}
521
+ $row=$apps|Where-Object{$_.Name -ieq $name -or $_.AppID -ieq $name}|Select-Object -First 1
522
+ if(-not $row){$row=$apps|Where-Object{$_.Name -like ('*'+$name+'*') -or $_.AppID -like ('*'+$name+'*')}|Sort-Object @{Expression={if($_.Name -like ($name+'*')){0}else{1}}},@{Expression={$_.Name.Length}}|Select-Object -First 1}
523
+ if($row){
524
+ $mode='start_menu';$resolved=$row.Name
525
+ Start-Process -FilePath 'explorer.exe' -ArgumentList ('shell:AppsFolder\'+$row.AppID) -ErrorAction Stop
526
+ }else{
527
+ $roots=@(
528
+ (Join-Path $env:ProgramData 'Microsoft\Windows\Start Menu\Programs'),
529
+ (Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs')
530
+ )|Where-Object{$_ -and (Test-Path $_)}
531
+ $links=@($roots|ForEach-Object{Get-ChildItem -LiteralPath $_ -Filter '*.lnk' -Recurse -ErrorAction SilentlyContinue})
532
+ $link=$links|Where-Object{$_.BaseName -ieq $name}|Select-Object -First 1
533
+ if(-not $link){$link=$links|Where-Object{$_.BaseName -like ('*'+$name+'*')}|Sort-Object @{Expression={if($_.BaseName -like ($name+'*')){0}else{1}}},@{Expression={$_.BaseName.Length}}|Select-Object -First 1}
534
+ if(-not $link){throw ('Application not found by executable/path or Start Menu name: '+$name)}
535
+ $mode='start_menu_link';$resolved=$link.BaseName
536
+ Start-Process -FilePath $link.FullName -ErrorAction Stop
537
+ }
538
+ }
539
+ [pscustomobject]@{requested=$name;resolved=$resolved;mode=$mode;pid=$pidValue}|ConvertTo-Json -Compress`;
540
+ const raw=(await runPowerShell(script,{label:'launch app',timeout:15_000})).stdout.trim();
541
+ const payload=raw?JSON.parse(raw):{requested:String(app),resolved:String(app),mode:'direct',pid:null};
542
+ return `Launched ${payload.resolved} via ${payload.mode}${payload.pid?` (pid ${payload.pid})`:''}.`;
543
+ }
544
+
545
+ export async function openPath(target){await runPowerShell(`Start-Process -FilePath '${escapePowerShellSingle(target)}'`,{label:'open path'});}
546
+ export async function revealPath(target){spawnDetached('explorer.exe',['/select,',target]);}