@lardexx/arenacode 2.7.0 → 2.8.0
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/README.md +11 -5
- package/package.json +1 -1
- package/src/automation.js +359 -92
- package/src/local.js +6 -17
package/README.md
CHANGED
|
@@ -148,11 +148,17 @@ from the same bridge request the desktop app answers:
|
|
|
148
148
|
| `ui apps` | the Start menu (Windows), `/Applications` (macOS), `.desktop` entries (Linux) |
|
|
149
149
|
| `ui multi-select`, `ui multi-edit` | built from the primitives above |
|
|
150
150
|
|
|
151
|
-
No extra install on **Windows
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
No extra install on **Windows**: PowerShell + `user32.dll` + the .NET UI Automation assemblies. The desktop client
|
|
152
|
+
ships a `windows-use.dll` (exports `wu_call_json`, `wu_free`, `wu_version`) for the same job; the terminal client
|
|
153
|
+
reproduces its behaviour instead of depending on it — the same JSON keys (`bounding_box`, `center`, `control_type`,
|
|
154
|
+
`interactive_elements_text`, `scrollable_elements`, `screenshot_png_base64`), the same ideas: the UI Automation tree of
|
|
155
|
+
the focused window for `ui state`, **unicode typing through `SendInput`** so the clipboard is untouched, extended-key
|
|
156
|
+
flags for arrows/navigation, fuzzy window matching (`ui app --name chrome` finds *"Google Chrome — New Tab"*), and
|
|
157
|
+
numbered labels drawn on the elements for `ui shot --annotated`.
|
|
158
|
+
|
|
159
|
+
**macOS**: grant Accessibility to your terminal, and `brew install cliclick` for the mouse. **Linux** (X11):
|
|
160
|
+
`sudo apt install xdotool wmctrl imagemagick`; on Wayland `xdotool` cannot inject input, so Computer Agent needs
|
|
161
|
+
XWayland. On macOS/Linux the accessibility tree is not read: `ui state` returns windows and geometry as elements.
|
|
156
162
|
|
|
157
163
|
**Ask and Proceed / Proceed.** The desktop's Settings switch, `ctrl+x a` here (also `/ask`, `/proceed`,
|
|
158
164
|
`--proceed`, and `arenacode config set approval proceed`). The default, **Ask and Proceed**, asks before commands,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lardexx/arenacode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Terminal client for ArenaCode: talks to an arena-bridge, streams the Agent Mode timeline, and runs local commands and file operations on your PC. Node port of the ArenaCode desktop client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
package/src/automation.js
CHANGED
|
@@ -55,6 +55,8 @@ $ErrorActionPreference = 'Stop'
|
|
|
55
55
|
$req = ([Console]::In.ReadToEnd() | ConvertFrom-Json)
|
|
56
56
|
Add-Type -AssemblyName System.Drawing
|
|
57
57
|
Add-Type -AssemblyName System.Windows.Forms
|
|
58
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
59
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
58
60
|
Add-Type -TypeDefinition @'
|
|
59
61
|
using System;
|
|
60
62
|
using System.Text;
|
|
@@ -67,48 +69,66 @@ public class AC {
|
|
|
67
69
|
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
|
68
70
|
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
|
69
71
|
[DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr h, int x, int y, int w, int ht, bool repaint);
|
|
70
|
-
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
|
|
71
|
-
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
72
|
-
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
|
73
|
-
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
74
72
|
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
73
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
75
74
|
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
|
|
76
|
-
public delegate bool EnumProc(IntPtr h, IntPtr p);
|
|
77
75
|
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
|
78
76
|
}
|
|
79
77
|
'@
|
|
80
78
|
|
|
79
|
+
$AC_TYPES = @(
|
|
80
|
+
[System.Windows.Automation.ControlType]::Button,
|
|
81
|
+
[System.Windows.Automation.ControlType]::CheckBox,
|
|
82
|
+
[System.Windows.Automation.ControlType]::ComboBox,
|
|
83
|
+
[System.Windows.Automation.ControlType]::Custom,
|
|
84
|
+
[System.Windows.Automation.ControlType]::DataItem,
|
|
85
|
+
[System.Windows.Automation.ControlType]::Edit,
|
|
86
|
+
[System.Windows.Automation.ControlType]::Hyperlink,
|
|
87
|
+
[System.Windows.Automation.ControlType]::ListItem,
|
|
88
|
+
[System.Windows.Automation.ControlType]::MenuItem,
|
|
89
|
+
[System.Windows.Automation.ControlType]::RadioButton,
|
|
90
|
+
[System.Windows.Automation.ControlType]::ScrollBar,
|
|
91
|
+
[System.Windows.Automation.ControlType]::Slider,
|
|
92
|
+
[System.Windows.Automation.ControlType]::Spinner,
|
|
93
|
+
[System.Windows.Automation.ControlType]::SplitButton,
|
|
94
|
+
[System.Windows.Automation.ControlType]::Tab,
|
|
95
|
+
[System.Windows.Automation.ControlType]::TabItem,
|
|
96
|
+
[System.Windows.Automation.ControlType]::Text,
|
|
97
|
+
[System.Windows.Automation.ControlType]::ToolBar,
|
|
98
|
+
[System.Windows.Automation.ControlType]::TreeItem
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
function Get-ACWindow($h) {
|
|
102
|
+
$hh = [IntPtr][int64]$h
|
|
103
|
+
$r = New-Object AC+RECT
|
|
104
|
+
[void][AC]::GetWindowRect($hh, [ref]$r)
|
|
105
|
+
$sb = New-Object System.Text.StringBuilder 1024
|
|
106
|
+
[void][AC]::GetWindowText($hh, $sb, 1024)
|
|
107
|
+
return [pscustomobject]@{
|
|
108
|
+
name = $sb.ToString()
|
|
109
|
+
handle = $hh.ToInt64()
|
|
110
|
+
hwnd = $hh.ToInt64()
|
|
111
|
+
x = $r.Left; y = $r.Top
|
|
112
|
+
width = [Math]::Max(0, $r.Right - $r.Left); height = [Math]::Max(0, $r.Bottom - $r.Top)
|
|
113
|
+
status = $(if ([AC]::IsIconic($hh)) { 'minimized' } else { 'normal' })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
function Get-ACWindows {
|
|
82
118
|
$list = New-Object System.Collections.ArrayList
|
|
83
119
|
foreach ($p in (Get-Process | Where-Object { $_.MainWindowHandle -ne 0 })) {
|
|
84
120
|
try {
|
|
85
|
-
$h = [IntPtr]$p.MainWindowHandle
|
|
86
|
-
$r = New-Object AC+RECT
|
|
87
|
-
[void][AC]::GetWindowRect($h, [ref]$r)
|
|
88
121
|
$title = [string]$p.MainWindowTitle
|
|
89
122
|
if ($title.Trim().Length -eq 0) { continue }
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
x = $r.Left; y = $r.Top
|
|
95
|
-
width = [Math]::Max(0, $r.Right - $r.Left); height = [Math]::Max(0, $r.Bottom - $r.Top)
|
|
96
|
-
process_id = [int]$p.Id
|
|
97
|
-
status = $(if ($p.MainWindowHandle -eq 0) { 'hidden' } else { 'normal' })
|
|
98
|
-
})
|
|
123
|
+
$w = Get-ACWindow $p.MainWindowHandle
|
|
124
|
+
$w | Add-Member -NotePropertyName process_id -NotePropertyValue ([int]$p.Id)
|
|
125
|
+
$w | Add-Member -NotePropertyName app -NotePropertyValue ([string]$p.ProcessName)
|
|
126
|
+
[void]$list.Add($w)
|
|
99
127
|
} catch { }
|
|
100
128
|
}
|
|
101
129
|
return $list
|
|
102
130
|
}
|
|
103
131
|
|
|
104
|
-
function Find-ACWindow($name) {
|
|
105
|
-
if (-not $name) { return $null }
|
|
106
|
-
$all = Get-ACWindows
|
|
107
|
-
$exact = $all | Where-Object { $_.name -eq $name } | Select-Object -First 1
|
|
108
|
-
if ($exact) { return $exact }
|
|
109
|
-
return ($all | Where-Object { $_.name -like "*$name*" } | Select-Object -First 1)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
132
|
function Get-ACScreen {
|
|
113
133
|
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
114
134
|
return @{ width = $b.Width; height = $b.Height }
|
|
@@ -116,37 +136,119 @@ function Get-ACScreen {
|
|
|
116
136
|
|
|
117
137
|
function Send-ACKeys($pairs) {
|
|
118
138
|
foreach ($k in $pairs) {
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
$flags = 0
|
|
140
|
+
if ($k.flags -eq 2) { $flags = $flags -bor 2 }
|
|
141
|
+
if ($k.ext) { $flags = $flags -bor 1 }
|
|
142
|
+
[AC]::keybd_event([byte]$k.vk, 0, $flags, [UIntPtr]::Zero)
|
|
121
143
|
Start-Sleep -Milliseconds 12
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
146
|
|
|
147
|
+
# windows-use types through SendInput unicode events: the clipboard is never touched
|
|
148
|
+
function Send-ACUnicode($text) {
|
|
149
|
+
$typed = 0
|
|
150
|
+
foreach ($ch in $text.ToCharArray()) {
|
|
151
|
+
if ($ch -eq [char]10) {
|
|
152
|
+
[AC]::keybd_event(0x0D, 0, 0, [UIntPtr]::Zero)
|
|
153
|
+
[AC]::keybd_event(0x0D, 0, 2, [UIntPtr]::Zero)
|
|
154
|
+
$typed++
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if ($ch -eq [char]13) { continue }
|
|
158
|
+
[AC]::keybd_event(0, [byte][char]$ch, 4, [UIntPtr]::Zero)
|
|
159
|
+
[AC]::keybd_event(0, [byte][char]$ch, 6, [UIntPtr]::Zero)
|
|
160
|
+
$typed++
|
|
161
|
+
Start-Sleep -Milliseconds 3
|
|
162
|
+
}
|
|
163
|
+
return $typed
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function Get-ACElements($hwnd, $interactiveOnly) {
|
|
167
|
+
$handle = [IntPtr][int64]$hwnd
|
|
168
|
+
$root = $null
|
|
169
|
+
if ($handle -ne [IntPtr]::Zero) {
|
|
170
|
+
$root = [System.Windows.Automation.AutomationElement]::FromHandle($handle)
|
|
171
|
+
} else {
|
|
172
|
+
$root = [System.Windows.Automation.AutomationElement]::FocusedElement
|
|
173
|
+
while ($root -ne $null -and $root.Current.NativeWindowHandle -eq 0) {
|
|
174
|
+
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
|
175
|
+
$root = $walker.GetParent($root)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if ($root -eq $null) { return @() }
|
|
179
|
+
|
|
180
|
+
$conditions = @()
|
|
181
|
+
foreach ($ct in $AC_TYPES) {
|
|
182
|
+
$conditions += New-Object System.Windows.Automation.PropertyCondition(
|
|
183
|
+
[System.Windows.Automation.AutomationElement]::ControlTypeProperty, $ct)
|
|
184
|
+
}
|
|
185
|
+
$cond = New-Object System.Windows.Automation.OrCondition $conditions
|
|
186
|
+
$found = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $cond)
|
|
187
|
+
|
|
188
|
+
$out = New-Object System.Collections.ArrayList
|
|
189
|
+
$n = 0
|
|
190
|
+
foreach ($el in $found) {
|
|
191
|
+
if ($n -ge 120) { break }
|
|
192
|
+
try {
|
|
193
|
+
$cur = $el.Current
|
|
194
|
+
if ($cur.IsOffscreen) { continue }
|
|
195
|
+
$rect = $cur.BoundingRectangle
|
|
196
|
+
if ($rect.Width -le 0 -or $rect.Height -le 0) { continue }
|
|
197
|
+
$name = [string]$cur.Name
|
|
198
|
+
$ctype = [string]$cur.ControlType.ProgrammaticName -replace '^ControlType\.', ''
|
|
199
|
+
if ($interactiveOnly -and $ctype -eq 'Text' -and $name.Trim().Length -eq 0) { continue }
|
|
200
|
+
$val = ''
|
|
201
|
+
try {
|
|
202
|
+
$vp = $null
|
|
203
|
+
if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) { $val = [string]$vp.Current.Value }
|
|
204
|
+
} catch { $val = '' }
|
|
205
|
+
$scrollable = $false
|
|
206
|
+
try {
|
|
207
|
+
$sp = $null
|
|
208
|
+
if ($el.TryGetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern, [ref]$sp)) {
|
|
209
|
+
$scrollable = [bool]$sp.Current.VerticallyScrollable -or [bool]$sp.Current.HorizontallyScrollable
|
|
210
|
+
}
|
|
211
|
+
} catch { }
|
|
212
|
+
[void]$out.Add([pscustomobject]@{
|
|
213
|
+
name = $name
|
|
214
|
+
control_type = $ctype
|
|
215
|
+
kind = $ctype
|
|
216
|
+
bounding_box = @{ left = [int]$rect.X; top = [int]$rect.Y; right = [int]($rect.X + $rect.Width); bottom = [int]($rect.Y + $rect.Height) }
|
|
217
|
+
center = @{ x = [int]($rect.X + $rect.Width / 2); y = [int]($rect.Y + $rect.Height / 2) }
|
|
218
|
+
value = $val
|
|
219
|
+
scrollable = $scrollable
|
|
220
|
+
enabled = [bool]$cur.IsEnabled
|
|
221
|
+
automation_id = [string]$cur.AutomationId
|
|
222
|
+
})
|
|
223
|
+
$n++
|
|
224
|
+
} catch { }
|
|
225
|
+
}
|
|
226
|
+
return $out
|
|
227
|
+
}
|
|
228
|
+
|
|
125
229
|
$out = @{ ok = $true }
|
|
126
230
|
switch ($req.action) {
|
|
127
231
|
'screen' { $s = Get-ACScreen; $out.width = $s.width; $out.height = $s.height }
|
|
128
|
-
'windows' { $out.windows = @(Get-ACWindows)
|
|
232
|
+
'windows' { $out.windows = @(Get-ACWindows) }
|
|
129
233
|
'active' {
|
|
130
|
-
$
|
|
131
|
-
$
|
|
132
|
-
|
|
133
|
-
$out.
|
|
234
|
+
$hh = [AC]::GetForegroundWindow()
|
|
235
|
+
$w = Get-ACWindow $hh.ToInt64()
|
|
236
|
+
$out.name = $w.name
|
|
237
|
+
$out.handle = $w.handle
|
|
134
238
|
}
|
|
135
239
|
'focus' {
|
|
136
|
-
$
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
} else { $out = @{ ok = $false; error = "window not found: $($req.name)" } }
|
|
240
|
+
$hh = [IntPtr][int64]$req.handle
|
|
241
|
+
[void][AC]::ShowWindow($hh, 9)
|
|
242
|
+
[void][AC]::SetForegroundWindow($hh)
|
|
243
|
+
Start-Sleep -Milliseconds 200
|
|
244
|
+
$w = Get-ACWindow $req.handle
|
|
245
|
+
$out.name = $w.name
|
|
143
246
|
}
|
|
144
247
|
'resize' {
|
|
145
|
-
$
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} else { $out = @{ ok = $false; error = "window not found: $($req.name)" } }
|
|
248
|
+
$hh = [IntPtr][int64]$req.handle
|
|
249
|
+
[void][AC]::MoveWindow($hh, [int]$req.x, [int]$req.y, [int]$req.width, [int]$req.height, $true)
|
|
250
|
+
$w = Get-ACWindow $req.handle
|
|
251
|
+
$out.name = $w.name
|
|
150
252
|
}
|
|
151
253
|
'launch' {
|
|
152
254
|
$name = [string]$req.name
|
|
@@ -161,7 +263,7 @@ switch ($req.action) {
|
|
|
161
263
|
if ($exe) { Start-Process $exe.Source; $done = $true }
|
|
162
264
|
}
|
|
163
265
|
}
|
|
164
|
-
if ($done) { Start-Sleep -Milliseconds
|
|
266
|
+
if ($done) { Start-Sleep -Milliseconds 900; $out.name = $name } else { $out = @{ ok = $false; error = "app not found: $name" } }
|
|
165
267
|
}
|
|
166
268
|
'move' { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y) }
|
|
167
269
|
'click' {
|
|
@@ -185,7 +287,7 @@ switch ($req.action) {
|
|
|
185
287
|
}
|
|
186
288
|
'up' {
|
|
187
289
|
$flag = 4; if ($req.button -eq 'right') { $flag = 16 } elseif ($req.button -eq 'middle') { $flag = 64 }
|
|
188
|
-
if ($
|
|
290
|
+
if ($null -ne $req.x) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y) }
|
|
189
291
|
[AC]::mouse_event($flag, 0, 0, 0, [UIntPtr]::Zero)
|
|
190
292
|
}
|
|
191
293
|
'drag' {
|
|
@@ -202,7 +304,7 @@ switch ($req.action) {
|
|
|
202
304
|
[AC]::mouse_event(4, 0, 0, 0, [UIntPtr]::Zero)
|
|
203
305
|
}
|
|
204
306
|
'scroll' {
|
|
205
|
-
if ($req.x -ne $
|
|
307
|
+
if ($null -ne $req.x -and $null -ne $req.y) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y); Start-Sleep -Milliseconds 40 }
|
|
206
308
|
$amount = [int]$req.amount
|
|
207
309
|
$dir = 120; if ($amount -lt 0) { $dir = -120 }
|
|
208
310
|
for ($i = 0; $i -lt [Math]::Abs($amount); $i++) {
|
|
@@ -212,24 +314,38 @@ switch ($req.action) {
|
|
|
212
314
|
}
|
|
213
315
|
'keys' { Send-ACKeys $req.sequence }
|
|
214
316
|
'type' {
|
|
215
|
-
if ($req.x -ne $
|
|
317
|
+
if ($null -ne $req.x -and $null -ne $req.y) {
|
|
216
318
|
[void][AC]::SetCursorPos([int]$req.x, [int]$req.y)
|
|
217
319
|
Start-Sleep -Milliseconds 60
|
|
218
320
|
[AC]::mouse_event(2, 0, 0, 0, [UIntPtr]::Zero)
|
|
219
321
|
[AC]::mouse_event(4, 0, 0, 0, [UIntPtr]::Zero)
|
|
220
|
-
Start-Sleep -Milliseconds
|
|
322
|
+
Start-Sleep -Milliseconds 150
|
|
221
323
|
}
|
|
222
|
-
|
|
223
|
-
Start-Sleep -Milliseconds 80
|
|
224
|
-
[System.Windows.Forms.SendKeys]::SendWait('^v')
|
|
225
|
-
Start-Sleep -Milliseconds 150
|
|
226
|
-
$out.typed = ([string]$req.text).Length
|
|
324
|
+
$out.typed = (Send-ACUnicode ([string]$req.text))
|
|
227
325
|
}
|
|
228
326
|
'shot' {
|
|
229
327
|
$s = Get-ACScreen
|
|
230
328
|
$bmp = New-Object System.Drawing.Bitmap $s.width, $s.height
|
|
231
329
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
232
330
|
$g.CopyFromScreen(0, 0, 0, 0, $bmp.Size)
|
|
331
|
+
if ($req.annotate) {
|
|
332
|
+
$fg = $null
|
|
333
|
+
try { $fg = [AC]::GetForegroundWindow().ToInt64() } catch { $fg = 0 }
|
|
334
|
+
$els = @(Get-ACElements $fg $true)
|
|
335
|
+
$font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
|
|
336
|
+
$brush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(230, 250, 178, 131))
|
|
337
|
+
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(200, 250, 178, 131)), 1
|
|
338
|
+
$i = 1
|
|
339
|
+
foreach ($e in $els) {
|
|
340
|
+
$bb = $e.bounding_box
|
|
341
|
+
$g.DrawRectangle($pen, $bb.left, $bb.top, [Math]::Max(1, $bb.right - $bb.left), [Math]::Max(1, $bb.bottom - $bb.top))
|
|
342
|
+
$g.FillRectangle($brush, $bb.left, [Math]::Max(0, $bb.top - 14), 18, 14)
|
|
343
|
+
$g.DrawString([string]$i, $font, (New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::Black)), $bb.left + 2, [Math]::Max(0, $bb.top - 14))
|
|
344
|
+
$i++
|
|
345
|
+
}
|
|
346
|
+
$pen.Dispose(); $brush.Dispose(); $font.Dispose()
|
|
347
|
+
$out.elements = $els.Count
|
|
348
|
+
}
|
|
233
349
|
$bmp.Save($req.path, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
234
350
|
$g.Dispose(); $bmp.Dispose()
|
|
235
351
|
$out.width = $s.width; $out.height = $s.height; $out.file = $req.path
|
|
@@ -238,6 +354,35 @@ switch ($req.action) {
|
|
|
238
354
|
try { $out.apps = @(Get-StartApps | ForEach-Object { @{ name = $_.Name; app_id = $_.AppID } }) }
|
|
239
355
|
catch { $out.apps = @() }
|
|
240
356
|
}
|
|
357
|
+
'state' {
|
|
358
|
+
$hh = $null
|
|
359
|
+
if ($null -ne $req.handle -and [int64]$req.handle -ne 0) { $hh = [int64]$req.handle } else { $hh = [AC]::GetForegroundWindow().ToInt64() }
|
|
360
|
+
$els = @(Get-ACElements $hh $true)
|
|
361
|
+
$interactive = @()
|
|
362
|
+
$scrollable = @()
|
|
363
|
+
$lines = New-Object System.Collections.ArrayList
|
|
364
|
+
$i = 1
|
|
365
|
+
foreach ($e in $els) {
|
|
366
|
+
if ($e.scrollable) { $scrollable += $e } else { $interactive += $e }
|
|
367
|
+
[void]$lines.Add("$i. $($e.control_type) '$($e.name)' at ($($e.center.x), $($e.center.y))")
|
|
368
|
+
$i++
|
|
369
|
+
}
|
|
370
|
+
$out.windows = @(Get-ACWindows)
|
|
371
|
+
$out.active_window = (Get-ACWindow $hh)
|
|
372
|
+
$out.interactive_elements = $interactive
|
|
373
|
+
$out.scrollable_elements = $scrollable
|
|
374
|
+
$out.interactive_elements_text = ($lines -join [Environment]::NewLine)
|
|
375
|
+
$out.scrollable_elements_text = (($scrollable | ForEach-Object { "$($_.control_type) '$($_.name)' at ($($_.center.x), $($_.center.y))" }) -join [Environment]::NewLine)
|
|
376
|
+
$out.system = @{
|
|
377
|
+
windows_version = [string](Get-CimInstance Win32_OperatingSystem).Caption
|
|
378
|
+
os_version = [Environment]::OSVersion.Version.ToString()
|
|
379
|
+
screen_size = (Get-ACScreen)
|
|
380
|
+
user_account_type = 'Local Account'
|
|
381
|
+
default_language = (Get-Culture).Name
|
|
382
|
+
dpi_scaling = 1
|
|
383
|
+
}
|
|
384
|
+
$out.metadata = @{ kind = 'ui-automation'; depth = 'descendants'; all_desktops = $false }
|
|
385
|
+
}
|
|
241
386
|
'sysinfo' {
|
|
242
387
|
$out.system = @{
|
|
243
388
|
windows_version = [string](Get-CimInstance Win32_OperatingSystem).Caption
|
|
@@ -250,9 +395,8 @@ switch ($req.action) {
|
|
|
250
395
|
}
|
|
251
396
|
default { $out = @{ ok = $false; error = "unknown action: $($req.action)" } }
|
|
252
397
|
}
|
|
253
|
-
$out | ConvertTo-Json -Depth
|
|
398
|
+
$out | ConvertTo-Json -Depth 8 -Compress
|
|
254
399
|
`;
|
|
255
|
-
|
|
256
400
|
let psEnginePath = null;
|
|
257
401
|
function psEngine() {
|
|
258
402
|
if (psEnginePath && fs.existsSync(psEnginePath)) return psEnginePath;
|
|
@@ -384,54 +528,103 @@ const IMAGE_TOOLS = [
|
|
|
384
528
|
['spectacle', (p) => ['-b', '-n', '-o', p]],
|
|
385
529
|
];
|
|
386
530
|
|
|
531
|
+
const VK = {
|
|
532
|
+
ctrl: 0x11, control: 0x11, alt: 0x12, option: 0x12, shift: 0x10, win: 0x5b, super: 0x5b,
|
|
533
|
+
meta: 0x5b, cmd: 0x5b, command: 0x5b,
|
|
534
|
+
enter: 0x0d, return: 0x0d, tab: 0x09, esc: 0x1b, escape: 0x1b, space: 0x20, spacebar: 0x20,
|
|
535
|
+
backspace: 0x08, bs: 0x08, delete: 0x2e, del: 0x2e, insert: 0x2d, ins: 0x2d,
|
|
536
|
+
home: 0x24, end: 0x23, pageup: 0x21, pagedown: 0x22, pgup: 0x21, pgdn: 0x22,
|
|
537
|
+
up: 0x26, down: 0x28, left: 0x25, right: 0x27, arrowup: 0x26, arrowdown: 0x28,
|
|
538
|
+
arrowleft: 0x25, arrowright: 0x27,
|
|
539
|
+
capslock: 0x14, numlock: 0x90, scrolllock: 0x91, printscreen: 0x2c, prtsc: 0x2c,
|
|
540
|
+
pause: 0x13, menu: 0x5d, contextmenu: 0x5d, apps: 0x5d,
|
|
541
|
+
plus: 0xbb, minus: 0xbd, comma: 0xbc, period: 0xbe, slash: 0xbf, backslash: 0xdc,
|
|
542
|
+
semicolon: 0xba, quote: 0xde, bracketleft: 0xdb, bracketright: 0xdd, backtick: 0xc0,
|
|
543
|
+
numpad0: 0x60, numpad1: 0x61, numpad2: 0x62, numpad3: 0x63, numpad4: 0x64,
|
|
544
|
+
numpad5: 0x65, numpad6: 0x66, numpad7: 0x67, numpad8: 0x68, numpad9: 0x69,
|
|
545
|
+
};
|
|
546
|
+
for (let i = 1; i <= 24; i += 1) VK[`f${i}`] = 0x6f + i;
|
|
547
|
+
|
|
548
|
+
// keys that need KEYEVENTF_EXTENDEDKEY, like windows-use's kScExtendedVks
|
|
549
|
+
const EXTENDED = new Set([
|
|
550
|
+
0x2e, 0x2d, 0x24, 0x23, 0x21, 0x22, 0x26, 0x28, 0x25, 0x27, 0x5b, 0x5d, 0x2c, 0x90, 0x5d,
|
|
551
|
+
0x6f + 11, 0x6f + 12,
|
|
552
|
+
]);
|
|
553
|
+
const MODIFIERS = new Set(['ctrl', 'control', 'alt', 'option', 'shift', 'win', 'super', 'meta', 'cmd', 'command']);
|
|
554
|
+
|
|
387
555
|
function keyboardSequence(combo) {
|
|
388
|
-
// "ctrl+shift+s" -> [{vk, flags}]
|
|
389
|
-
// PowerShell engine sends through keybd_event. Unknown names are ignored.
|
|
390
|
-
const VK = {
|
|
391
|
-
ctrl: 0x11, control: 0x11, alt: 0x12, shift: 0x10, win: 0x5b, meta: 0x5b, cmd: 0x5b,
|
|
392
|
-
enter: 0x0d, return: 0x0d, tab: 0x09, esc: 0x1b, escape: 0x1b, space: 0x20,
|
|
393
|
-
backspace: 0x08, delete: 0x2e, del: 0x2e, insert: 0x2d, home: 0x24, end: 0x23,
|
|
394
|
-
pageup: 0x21, pagedown: 0x22, pgup: 0x21, pgdn: 0x22, up: 0x26, down: 0x28,
|
|
395
|
-
left: 0x25, right: 0x27, printscreen: 0x2c, capslock: 0x14, menu: 0x5d,
|
|
396
|
-
};
|
|
397
|
-
for (let i = 1; i <= 24; i += 1) VK[`f${i}`] = 0x6f + i;
|
|
556
|
+
// "ctrl+shift+s" -> [{ vk, flags, ext }] for keybd_event, the call windows-use makes too
|
|
398
557
|
const parts = String(combo || '')
|
|
399
558
|
.toLowerCase()
|
|
400
559
|
.split('+')
|
|
401
560
|
.map((p) => p.trim())
|
|
402
561
|
.filter(Boolean);
|
|
403
562
|
if (!parts.length) return [];
|
|
404
|
-
|
|
405
|
-
const repeat = parts.filter((p) => p === 'repeat').length;
|
|
406
|
-
const keys = parts.filter((p) => p !== 'repeat');
|
|
563
|
+
let repeat = 1;
|
|
407
564
|
const modVks = [];
|
|
408
565
|
const mainVks = [];
|
|
409
|
-
for (const key of
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
} else if (key.length === 1) {
|
|
414
|
-
mainVks.push(key.toUpperCase().charCodeAt(0));
|
|
566
|
+
for (const key of parts) {
|
|
567
|
+
if (key === 'repeat' || /^x\d+$/.test(key)) {
|
|
568
|
+
repeat = key === 'repeat' ? 2 : Number(key.slice(1));
|
|
569
|
+
continue;
|
|
415
570
|
}
|
|
571
|
+
let vk = VK[key];
|
|
572
|
+
if (vk === undefined && key.length === 1) vk = key.toUpperCase().charCodeAt(0);
|
|
573
|
+
if (vk === undefined) continue;
|
|
574
|
+
if (MODIFIERS.has(key)) modVks.push(vk);
|
|
575
|
+
else mainVks.push(vk);
|
|
416
576
|
}
|
|
417
|
-
const
|
|
577
|
+
const seq = [];
|
|
578
|
+
const times = Math.max(1, Math.min(50, repeat));
|
|
418
579
|
for (let i = 0; i < times; i += 1) {
|
|
419
580
|
if (!mainVks.length && modVks.length) {
|
|
420
581
|
// "win" on its own: press and release the modifier itself
|
|
421
582
|
const only = modVks[modVks.length - 1];
|
|
422
|
-
seq.push({ vk: only, flags: 0
|
|
583
|
+
seq.push({ vk: only, flags: 0, ext: EXTENDED.has(only) });
|
|
584
|
+
seq.push({ vk: only, flags: 2, ext: EXTENDED.has(only) });
|
|
423
585
|
continue;
|
|
424
586
|
}
|
|
425
|
-
for (const vk of modVks) seq.push({ vk, flags: 0 });
|
|
587
|
+
for (const vk of modVks) seq.push({ vk, flags: 0, ext: EXTENDED.has(vk) });
|
|
426
588
|
for (const vk of mainVks) {
|
|
427
|
-
seq.push({ vk, flags: 0 });
|
|
428
|
-
seq.push({ vk, flags: 2 });
|
|
589
|
+
seq.push({ vk, flags: 0, ext: EXTENDED.has(vk) });
|
|
590
|
+
seq.push({ vk, flags: 2, ext: EXTENDED.has(vk) });
|
|
429
591
|
}
|
|
430
|
-
for (const vk of [...modVks].reverse()) seq.push({ vk, flags: 2 });
|
|
592
|
+
for (const vk of [...modVks].reverse()) seq.push({ vk, flags: 2, ext: EXTENDED.has(vk) });
|
|
431
593
|
}
|
|
432
594
|
return seq;
|
|
433
595
|
}
|
|
434
596
|
|
|
597
|
+
// windows-use matches window names with fuzzy scores (wu::fuzzy::WRatio); the same
|
|
598
|
+
// idea keeps `ui app --name chrome` working when the title is "Google Chrome — …".
|
|
599
|
+
const fuzzyScore = (needle, haystack) => {
|
|
600
|
+
const a = String(needle || '').toLowerCase().trim();
|
|
601
|
+
const b = String(haystack || '').toLowerCase().trim();
|
|
602
|
+
if (!a || !b) return 0;
|
|
603
|
+
if (a === b) return 1;
|
|
604
|
+
if (b === a || b.startsWith(a)) return 0.95;
|
|
605
|
+
if (b.includes(a)) return 0.85;
|
|
606
|
+
const tokens = a.split(/\s+/).filter(Boolean);
|
|
607
|
+
const words = b.split(/\s+/).filter(Boolean);
|
|
608
|
+
const hits = tokens.filter((t) => words.some((w) => w.startsWith(t) || t.startsWith(w))).length;
|
|
609
|
+
if (hits) return 0.6 + (0.2 * hits) / tokens.length;
|
|
610
|
+
// character-level overlap, same spirit as partial_ratio
|
|
611
|
+
const chars = new Set(b.replace(/\s+/g, ''));
|
|
612
|
+
const overlap = [...a.replace(/\s+/g, '')].filter((c) => chars.has(c)).length;
|
|
613
|
+
return 0.4 * (overlap / Math.max(1, a.replace(/\s+/g, '').length));
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const AVOIDED = [/arenacode/i, /arena code/i];
|
|
617
|
+
|
|
618
|
+
function pickWindow(list, name) {
|
|
619
|
+
const candidates = (list || []).filter((w) => w && w.name && !AVOIDED.some((re) => re.test(w.name)));
|
|
620
|
+
let best = null;
|
|
621
|
+
for (const w of candidates) {
|
|
622
|
+
const score = fuzzyScore(name, w.name);
|
|
623
|
+
if (score >= 0.6 && (!best || score > best.score)) best = { ...w, score };
|
|
624
|
+
}
|
|
625
|
+
return best;
|
|
626
|
+
}
|
|
627
|
+
|
|
435
628
|
const automation = {
|
|
436
629
|
platform: process.platform,
|
|
437
630
|
|
|
@@ -471,16 +664,31 @@ const automation = {
|
|
|
471
664
|
|
|
472
665
|
windows() {
|
|
473
666
|
if (isWin) {
|
|
474
|
-
const r = winRun({ action: 'windows' });
|
|
475
|
-
|
|
667
|
+
const r = winRun({ action: 'windows' }, 45000);
|
|
668
|
+
if (!r.ok) return fail(r.error);
|
|
669
|
+
return { ok: true, windows: r.windows || [] };
|
|
476
670
|
}
|
|
477
671
|
const list = isMac ? macWindows() : linuxWindows();
|
|
478
|
-
return { ok: true, windows: list
|
|
672
|
+
return { ok: true, windows: list };
|
|
479
673
|
},
|
|
480
674
|
|
|
481
675
|
activeWindow() {
|
|
676
|
+
if (isWin) {
|
|
677
|
+
const r = winRun({ action: 'active' });
|
|
678
|
+
return r.ok ? { ok: true, name: r.name, handle: r.handle } : fail(r.error);
|
|
679
|
+
}
|
|
482
680
|
const w = this.windows();
|
|
483
|
-
|
|
681
|
+
const first = w.ok && w.windows.length ? w.windows[0] : null;
|
|
682
|
+
return { ok: true, name: first ? first.name : '', handle: first ? first.handle : null };
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
/** Finds the window the user means, the way windows-use does: best fuzzy match. */
|
|
686
|
+
findWindow(name) {
|
|
687
|
+
const list = this.windows();
|
|
688
|
+
if (!list.ok) return list;
|
|
689
|
+
const hit = pickWindow(list.windows, name);
|
|
690
|
+
if (!hit) return { ok: false, error: `window not found: ${name}` };
|
|
691
|
+
return { ok: true, window: hit };
|
|
484
692
|
},
|
|
485
693
|
|
|
486
694
|
launchApp(name) {
|
|
@@ -501,8 +709,10 @@ const automation = {
|
|
|
501
709
|
|
|
502
710
|
focusWindow(name) {
|
|
503
711
|
if (isWin) {
|
|
504
|
-
const
|
|
505
|
-
|
|
712
|
+
const found = this.findWindow(name);
|
|
713
|
+
if (!found.ok) return found;
|
|
714
|
+
const r = winRun({ action: 'focus', handle: found.window.handle });
|
|
715
|
+
return r.ok ? { ok: true, name: r.name, handle: found.window.handle, matched: found.window.name } : fail(r.error);
|
|
506
716
|
}
|
|
507
717
|
if (isMac) {
|
|
508
718
|
const res = osa(`tell application "System Events" to set frontmost of process ${JSON.stringify(name)} to true`);
|
|
@@ -514,7 +724,16 @@ const automation = {
|
|
|
514
724
|
|
|
515
725
|
resizeWindow(name, x, y, width, height) {
|
|
516
726
|
if (isWin) {
|
|
517
|
-
const
|
|
727
|
+
const found = this.findWindow(name);
|
|
728
|
+
if (!found.ok) return found;
|
|
729
|
+
const r = winRun({
|
|
730
|
+
action: 'resize',
|
|
731
|
+
handle: found.window.handle,
|
|
732
|
+
x,
|
|
733
|
+
y,
|
|
734
|
+
width,
|
|
735
|
+
height,
|
|
736
|
+
});
|
|
518
737
|
return r.ok ? { ok: true, name: r.name, x, y, width, height } : fail(r.error);
|
|
519
738
|
}
|
|
520
739
|
if (isMac) {
|
|
@@ -527,6 +746,52 @@ const automation = {
|
|
|
527
746
|
return res.ok ? { ok: true, name, x, y, width, height } : fail(res.stderr.trim() || 'resize failed');
|
|
528
747
|
},
|
|
529
748
|
|
|
749
|
+
/** The UI Automation tree of the focused window: what the desktop's windows-use returns. */
|
|
750
|
+
state(handle = null) {
|
|
751
|
+
if (isWin) {
|
|
752
|
+
const r = winRun({ action: 'state', handle }, 60000);
|
|
753
|
+
if (!r.ok) return fail(r.error);
|
|
754
|
+
return {
|
|
755
|
+
ok: true,
|
|
756
|
+
windows: r.windows || [],
|
|
757
|
+
active_window: r.active_window || null,
|
|
758
|
+
interactive_elements: r.interactive_elements || [],
|
|
759
|
+
scrollable_elements: r.scrollable_elements || [],
|
|
760
|
+
interactive_elements_text: r.interactive_elements_text || '',
|
|
761
|
+
scrollable_elements_text: r.scrollable_elements_text || '',
|
|
762
|
+
system: r.system || {},
|
|
763
|
+
metadata: r.metadata || {},
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
// no accessibility tree on the other platforms: windows, geometry and pixels
|
|
767
|
+
const wins = this.windows();
|
|
768
|
+
const screen = this.screenSize();
|
|
769
|
+
const active = wins.ok && wins.windows.length ? wins.windows[0] : null;
|
|
770
|
+
const elements = (wins.ok ? wins.windows : []).map((w) => ({
|
|
771
|
+
name: w.name,
|
|
772
|
+
control_type: 'Window',
|
|
773
|
+
kind: 'Window',
|
|
774
|
+
bounding_box: { left: w.x, top: w.y, right: w.x + w.width, bottom: w.y + w.height },
|
|
775
|
+
center: { x: w.x + Math.floor(w.width / 2), y: w.y + Math.floor(w.height / 2) },
|
|
776
|
+
value: '',
|
|
777
|
+
enabled: true,
|
|
778
|
+
automation_id: '',
|
|
779
|
+
}));
|
|
780
|
+
return {
|
|
781
|
+
ok: true,
|
|
782
|
+
windows: wins.ok ? wins.windows : [],
|
|
783
|
+
active_window: active,
|
|
784
|
+
interactive_elements: elements,
|
|
785
|
+
scrollable_elements: [],
|
|
786
|
+
interactive_elements_text: elements
|
|
787
|
+
.map((e, i) => `${i + 1}. ${e.control_type} '${e.name}' at (${e.center.x}, ${e.center.y})`)
|
|
788
|
+
.join('\n'),
|
|
789
|
+
scrollable_elements_text: '',
|
|
790
|
+
system: this.sysinfo().system,
|
|
791
|
+
metadata: { kind: 'windows-only-list', depth: 'top-level', all_desktops: false },
|
|
792
|
+
};
|
|
793
|
+
},
|
|
794
|
+
|
|
530
795
|
click(x, y, { button = 'left', count = 1 } = {}) {
|
|
531
796
|
if (isWin) {
|
|
532
797
|
const r = winRun({ action: 'click', x, y, button, count });
|
|
@@ -644,11 +909,13 @@ const automation = {
|
|
|
644
909
|
return res.ok ? { ok: true, keys: combo } : fail(res.stderr.trim() || 'xdotool key failed');
|
|
645
910
|
},
|
|
646
911
|
|
|
647
|
-
screenshot(dest) {
|
|
912
|
+
screenshot(dest, { annotate = false } = {}) {
|
|
648
913
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
649
914
|
if (isWin) {
|
|
650
|
-
const r = winRun({ action: 'shot', path: dest },
|
|
651
|
-
return r.ok
|
|
915
|
+
const r = winRun({ action: 'shot', path: dest, annotate }, 90000);
|
|
916
|
+
return r.ok
|
|
917
|
+
? { ok: true, file: dest, width: r.width, height: r.height, mime: 'image/png', annotated: Boolean(annotate), elements: r.elements || 0 }
|
|
918
|
+
: fail(r.error);
|
|
652
919
|
}
|
|
653
920
|
if (isMac) {
|
|
654
921
|
const res = run('screencapture', ['-x', '-t', 'png', dest], { timeout: 30000 });
|
|
@@ -723,4 +990,4 @@ const automation = {
|
|
|
723
990
|
},
|
|
724
991
|
};
|
|
725
992
|
|
|
726
|
-
module.exports = { ...automation, keyboardSequence };
|
|
993
|
+
module.exports = { ...automation, keyboardSequence, fuzzyScore, pickWindow };
|
package/src/local.js
CHANGED
|
@@ -531,22 +531,10 @@ async function runUiAction(bridge, taskId, request, opts) {
|
|
|
531
531
|
};
|
|
532
532
|
|
|
533
533
|
if (op === 'state') {
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return {
|
|
539
|
-
ok: true,
|
|
540
|
-
windows: wins.windows,
|
|
541
|
-
active_window: wins.active_window,
|
|
542
|
-
interactive_elements: [],
|
|
543
|
-
scrollable_elements: [],
|
|
544
|
-
interactive_elements_text: '',
|
|
545
|
-
system: info.system,
|
|
546
|
-
screen: { width: screen.width, height: screen.height },
|
|
547
|
-
// the terminal client reads windows and pixels, not the accessibility tree
|
|
548
|
-
note: 'interactive_elements are empty: this client does not read the UI Automation tree',
|
|
549
|
-
};
|
|
534
|
+
// the desktop's windows-use returns the UI Automation tree; so do we on Windows
|
|
535
|
+
const state = automation.state();
|
|
536
|
+
if (!state.ok) return state;
|
|
537
|
+
return state;
|
|
550
538
|
}
|
|
551
539
|
|
|
552
540
|
if (op === 'sysinfo') {
|
|
@@ -626,7 +614,7 @@ async function runUiAction(bridge, taskId, request, opts) {
|
|
|
626
614
|
const dir = path.join(os.tmpdir(), 'arenacode-shots');
|
|
627
615
|
fs.mkdirSync(dir, { recursive: true });
|
|
628
616
|
const file = path.join(dir, `shot-${stamp()}.png`);
|
|
629
|
-
const shot = automation.screenshot(file);
|
|
617
|
+
const shot = automation.screenshot(file, { annotate: Boolean(request.annotated) });
|
|
630
618
|
if (!shot.ok) return shot;
|
|
631
619
|
const buf = fs.readFileSync(shot.file);
|
|
632
620
|
// the agent runs in the sandbox: the image travels through the bridge workspace
|
|
@@ -639,6 +627,7 @@ async function runUiAction(bridge, taskId, request, opts) {
|
|
|
639
627
|
height: shot.height,
|
|
640
628
|
file: remote || shot.file,
|
|
641
629
|
local_file: shot.file,
|
|
630
|
+
annotated: Boolean(request.annotated),
|
|
642
631
|
};
|
|
643
632
|
if (buf.length <= 2 * 1024 * 1024) out.image = buf.toString('base64');
|
|
644
633
|
return out;
|