@remcp/runtime 0.2.47 → 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.
- package/CHANGELOG.md +15 -0
- package/README.md +42 -25
- package/package.json +6 -2
- package/src/catalog.mjs +1 -1
- package/src/extended/browser.mjs +592 -0
- package/src/extended/catalog.mjs +564 -0
- package/src/extended/common.mjs +209 -0
- package/src/extended/desktop-linux.mjs +1173 -0
- package/src/extended/desktop-macos.mjs +408 -0
- package/src/extended/desktop-windows.mjs +546 -0
- package/src/extended/desktop.mjs +1313 -0
- package/src/extended/diagnostics.mjs +274 -0
- package/src/extended/documents.mjs +381 -0
- package/src/extended/ocr.mjs +139 -0
- package/src/helpers/wayland-eis-helper.py +450 -0
- package/src/index.mjs +46 -44
- package/src/invoke.mjs +3 -3
- package/src/mcp-v2.mjs +104 -0
- package/src/permissions.mjs +1 -1
- package/src/util.mjs +24 -1
- package/src/wayland-remote-desktop.mjs +654 -0
|
@@ -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&¤t!=target)attachTarget=AttachThreadInput(current,target,true);
|
|
74
|
+
if(foreground!=0&¤t!=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]);}
|