@phuetz/code-buddy 0.1.3 → 0.1.5
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 +49 -33
- package/dist/agent/tool-handler.js +21 -1
- package/dist/agent/tool-handler.js.map +1 -1
- package/dist/browser-automation/browser-manager.js +0 -2
- package/dist/browser-automation/browser-manager.js.map +1 -1
- package/dist/codebuddy/client.js +24 -0
- package/dist/codebuddy/client.js.map +1 -1
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +2 -0
- package/dist/codebuddy/tool-definitions/computer-control-tools.js.map +1 -1
- package/dist/desktop-automation/automation-manager.d.ts +8 -0
- package/dist/desktop-automation/automation-manager.js +52 -0
- package/dist/desktop-automation/automation-manager.js.map +1 -1
- package/dist/desktop-automation/base-native-provider.d.ts +72 -0
- package/dist/desktop-automation/base-native-provider.js +75 -0
- package/dist/desktop-automation/base-native-provider.js.map +1 -0
- package/dist/desktop-automation/index.d.ts +4 -0
- package/dist/desktop-automation/index.js +5 -0
- package/dist/desktop-automation/index.js.map +1 -1
- package/dist/desktop-automation/linux-native-provider.d.ts +48 -0
- package/dist/desktop-automation/linux-native-provider.js +515 -0
- package/dist/desktop-automation/linux-native-provider.js.map +1 -0
- package/dist/desktop-automation/macos-native-provider.d.ts +61 -0
- package/dist/desktop-automation/macos-native-provider.js +768 -0
- package/dist/desktop-automation/macos-native-provider.js.map +1 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +21 -2
- package/dist/desktop-automation/smart-snapshot.js +237 -12
- package/dist/desktop-automation/smart-snapshot.js.map +1 -1
- package/dist/desktop-automation/types.d.ts +1 -1
- package/dist/desktop-automation/types.js +2 -2
- package/dist/desktop-automation/types.js.map +1 -1
- package/dist/desktop-automation/windows-native-provider.d.ts +71 -0
- package/dist/desktop-automation/windows-native-provider.js +653 -0
- package/dist/desktop-automation/windows-native-provider.js.map +1 -0
- package/dist/index.js +0 -0
- package/dist/tools/computer-control-tool.d.ts +6 -1
- package/dist/tools/computer-control-tool.js +97 -1
- package/dist/tools/computer-control-tool.js.map +1 -1
- package/dist/tools/screenshot-tool.d.ts +23 -0
- package/dist/tools/screenshot-tool.js +224 -1
- package/dist/tools/screenshot-tool.js.map +1 -1
- package/dist/tools/web-search.d.ts +2 -0
- package/dist/tools/web-search.js +12 -2
- package/dist/tools/web-search.js.map +1 -1
- package/package.json +14 -5
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows Native Desktop Automation Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses PowerShell P/Invoke (user32.dll) for native Windows desktop automation.
|
|
5
|
+
* Supports WSL2 interop via powershell.exe.
|
|
6
|
+
*/
|
|
7
|
+
import { BaseNativeProvider } from './base-native-provider.js';
|
|
8
|
+
// Virtual key code mapping
|
|
9
|
+
const VK_CODES = {
|
|
10
|
+
'return': 0x0D,
|
|
11
|
+
'enter': 0x0D,
|
|
12
|
+
'escape': 0x1B,
|
|
13
|
+
'esc': 0x1B,
|
|
14
|
+
'tab': 0x09,
|
|
15
|
+
'backspace': 0x08,
|
|
16
|
+
'delete': 0x2E,
|
|
17
|
+
'space': 0x20,
|
|
18
|
+
'up': 0x26,
|
|
19
|
+
'down': 0x28,
|
|
20
|
+
'left': 0x25,
|
|
21
|
+
'right': 0x27,
|
|
22
|
+
'control': 0x11,
|
|
23
|
+
'ctrl': 0x11,
|
|
24
|
+
'alt': 0x12,
|
|
25
|
+
'menu': 0x12,
|
|
26
|
+
'shift': 0x10,
|
|
27
|
+
'win': 0x5B,
|
|
28
|
+
'meta': 0x5B,
|
|
29
|
+
'command': 0x5B,
|
|
30
|
+
'home': 0x24,
|
|
31
|
+
'end': 0x23,
|
|
32
|
+
'pageup': 0x21,
|
|
33
|
+
'pagedown': 0x22,
|
|
34
|
+
'insert': 0x2D,
|
|
35
|
+
'printscreen': 0x2C,
|
|
36
|
+
'capslock': 0x14,
|
|
37
|
+
'numlock': 0x90,
|
|
38
|
+
'scrolllock': 0x91,
|
|
39
|
+
'pause': 0x13,
|
|
40
|
+
'f1': 0x70,
|
|
41
|
+
'f2': 0x71,
|
|
42
|
+
'f3': 0x72,
|
|
43
|
+
'f4': 0x73,
|
|
44
|
+
'f5': 0x74,
|
|
45
|
+
'f6': 0x75,
|
|
46
|
+
'f7': 0x76,
|
|
47
|
+
'f8': 0x77,
|
|
48
|
+
'f9': 0x78,
|
|
49
|
+
'f10': 0x79,
|
|
50
|
+
'f11': 0x7A,
|
|
51
|
+
'f12': 0x7B,
|
|
52
|
+
// a-z (populated below)
|
|
53
|
+
// 0-9 (populated below)
|
|
54
|
+
};
|
|
55
|
+
// Populate a-z
|
|
56
|
+
for (let i = 0; i < 26; i++) {
|
|
57
|
+
const letter = String.fromCharCode(97 + i); // 'a' to 'z'
|
|
58
|
+
VK_CODES[letter] = 0x41 + i;
|
|
59
|
+
}
|
|
60
|
+
// Populate 0-9
|
|
61
|
+
for (let i = 0; i <= 9; i++) {
|
|
62
|
+
VK_CODES[String(i)] = 0x30 + i;
|
|
63
|
+
}
|
|
64
|
+
// Modifier keys to VK codes
|
|
65
|
+
const MODIFIER_VK = {
|
|
66
|
+
'ctrl': 0x11,
|
|
67
|
+
'control': 0x11,
|
|
68
|
+
'alt': 0x12,
|
|
69
|
+
'shift': 0x10,
|
|
70
|
+
'meta': 0x5B,
|
|
71
|
+
'command': 0x5B,
|
|
72
|
+
'win': 0x5B,
|
|
73
|
+
};
|
|
74
|
+
// Mouse event flags
|
|
75
|
+
const MOUSEEVENTF_LEFTDOWN = 0x2;
|
|
76
|
+
const MOUSEEVENTF_LEFTUP = 0x4;
|
|
77
|
+
const MOUSEEVENTF_RIGHTDOWN = 0x8;
|
|
78
|
+
const MOUSEEVENTF_RIGHTUP = 0x10;
|
|
79
|
+
const MOUSEEVENTF_MIDDLEDOWN = 0x20;
|
|
80
|
+
const MOUSEEVENTF_MIDDLEUP = 0x40;
|
|
81
|
+
const MOUSEEVENTF_WHEEL = 0x800;
|
|
82
|
+
const MOUSEEVENTF_HWHEEL = 0x1000;
|
|
83
|
+
// Keyboard event flags
|
|
84
|
+
const KEYEVENTF_KEYUP = 0x2;
|
|
85
|
+
export class WindowsNativeProvider extends BaseNativeProvider {
|
|
86
|
+
platformName = 'Windows';
|
|
87
|
+
capabilities = {
|
|
88
|
+
mouse: true,
|
|
89
|
+
keyboard: true,
|
|
90
|
+
windows: true,
|
|
91
|
+
apps: true,
|
|
92
|
+
screenshots: true,
|
|
93
|
+
colorPicker: true,
|
|
94
|
+
clipboard: true,
|
|
95
|
+
ocr: false,
|
|
96
|
+
};
|
|
97
|
+
psCmd;
|
|
98
|
+
wsl;
|
|
99
|
+
P_INVOKE_BLOCK = `Add-Type @"
|
|
100
|
+
using System;
|
|
101
|
+
using System.Runtime.InteropServices;
|
|
102
|
+
public class NativeInput {
|
|
103
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
104
|
+
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT lpPoint);
|
|
105
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, int dwData, IntPtr dwExtraInfo);
|
|
106
|
+
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, IntPtr dwExtraInfo);
|
|
107
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
108
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
109
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
110
|
+
[DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
|
111
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
112
|
+
[DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
|
|
113
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
|
|
114
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
|
115
|
+
[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
|
|
116
|
+
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
|
|
117
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
|
118
|
+
}
|
|
119
|
+
"@`;
|
|
120
|
+
constructor(options) {
|
|
121
|
+
super();
|
|
122
|
+
this.wsl = options?.wsl ?? false;
|
|
123
|
+
this.psCmd = this.wsl ? 'powershell.exe' : 'powershell';
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// PowerShell execution helper
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Execute a PowerShell script via the configured PS command.
|
|
130
|
+
* Escapes double quotes in the script for shell invocation.
|
|
131
|
+
*/
|
|
132
|
+
async ps(script) {
|
|
133
|
+
const escapedScript = script.replace(/"/g, '\\"');
|
|
134
|
+
return this.exec(`${this.psCmd} -NoProfile -NonInteractive -Command "${escapedScript}"`, 15000);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Validate that a value is a finite number to prevent injection.
|
|
138
|
+
*/
|
|
139
|
+
validateNumber(value, name) {
|
|
140
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
141
|
+
throw new Error(`Invalid ${name}: must be a finite number`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Escape a string for use inside PowerShell single quotes (double the single quotes).
|
|
146
|
+
*/
|
|
147
|
+
escapePsSingleQuote(s) {
|
|
148
|
+
return s.replace(/'/g, "''");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get mouse button event flags.
|
|
152
|
+
*/
|
|
153
|
+
getMouseFlags(button = 'left') {
|
|
154
|
+
switch (button) {
|
|
155
|
+
case 'right':
|
|
156
|
+
return { down: MOUSEEVENTF_RIGHTDOWN, up: MOUSEEVENTF_RIGHTUP };
|
|
157
|
+
case 'middle':
|
|
158
|
+
return { down: MOUSEEVENTF_MIDDLEDOWN, up: MOUSEEVENTF_MIDDLEUP };
|
|
159
|
+
case 'left':
|
|
160
|
+
default:
|
|
161
|
+
return { down: MOUSEEVENTF_LEFTDOWN, up: MOUSEEVENTF_LEFTUP };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Resolve a key name to its virtual key code.
|
|
166
|
+
*/
|
|
167
|
+
resolveVK(key) {
|
|
168
|
+
const normalized = key.toLowerCase();
|
|
169
|
+
const vk = VK_CODES[normalized];
|
|
170
|
+
if (vk === undefined) {
|
|
171
|
+
// If single character, use its char code
|
|
172
|
+
if (key.length === 1) {
|
|
173
|
+
return key.toUpperCase().charCodeAt(0);
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`Unknown key: ${key}`);
|
|
176
|
+
}
|
|
177
|
+
return vk;
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Lifecycle
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
async initialize() {
|
|
183
|
+
try {
|
|
184
|
+
await this.ps('Write-Output ok');
|
|
185
|
+
this.initialized = true;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
throw new Error(`Failed to initialize Windows native provider: ${err instanceof Error ? err.message : String(err)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async isAvailable() {
|
|
192
|
+
try {
|
|
193
|
+
await this.exec(`${this.psCmd} -NoProfile -NonInteractive -Command "Write-Output ok"`, 5000);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async shutdown() {
|
|
201
|
+
this.initialized = false;
|
|
202
|
+
}
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Mouse
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
async getMousePosition() {
|
|
207
|
+
this.ensureInitialized();
|
|
208
|
+
const result = await this.ps(`${this.P_INVOKE_BLOCK}; $p = New-Object NativeInput+POINT; [NativeInput]::GetCursorPos([ref]$p) | Out-Null; Write-Output "$($p.X),$($p.Y)"`);
|
|
209
|
+
const parts = result.trim().split(',');
|
|
210
|
+
return {
|
|
211
|
+
x: parseInt(parts[0], 10),
|
|
212
|
+
y: parseInt(parts[1], 10),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async moveMouse(x, y, _options) {
|
|
216
|
+
this.ensureInitialized();
|
|
217
|
+
this.validateNumber(x, 'x');
|
|
218
|
+
this.validateNumber(y, 'y');
|
|
219
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::SetCursorPos(${Math.round(x)}, ${Math.round(y)})`);
|
|
220
|
+
}
|
|
221
|
+
async click(options) {
|
|
222
|
+
this.ensureInitialized();
|
|
223
|
+
const button = options?.button ?? 'left';
|
|
224
|
+
const clicks = options?.clicks ?? 1;
|
|
225
|
+
const clickDelay = options?.delay ?? 50;
|
|
226
|
+
const flags = this.getMouseFlags(button);
|
|
227
|
+
for (let i = 0; i < clicks; i++) {
|
|
228
|
+
if (i > 0) {
|
|
229
|
+
await this.delay(clickDelay);
|
|
230
|
+
}
|
|
231
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${flags.down}, 0, 0, 0, [IntPtr]::Zero); [NativeInput]::mouse_event(${flags.up}, 0, 0, 0, [IntPtr]::Zero)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async doubleClick(button) {
|
|
235
|
+
this.ensureInitialized();
|
|
236
|
+
const flags = this.getMouseFlags(button ?? 'left');
|
|
237
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${flags.down}, 0, 0, 0, [IntPtr]::Zero); [NativeInput]::mouse_event(${flags.up}, 0, 0, 0, [IntPtr]::Zero)`);
|
|
238
|
+
await this.delay(50);
|
|
239
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${flags.down}, 0, 0, 0, [IntPtr]::Zero); [NativeInput]::mouse_event(${flags.up}, 0, 0, 0, [IntPtr]::Zero)`);
|
|
240
|
+
}
|
|
241
|
+
async rightClick() {
|
|
242
|
+
this.ensureInitialized();
|
|
243
|
+
const flags = this.getMouseFlags('right');
|
|
244
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${flags.down}, 0, 0, 0, [IntPtr]::Zero); [NativeInput]::mouse_event(${flags.up}, 0, 0, 0, [IntPtr]::Zero)`);
|
|
245
|
+
}
|
|
246
|
+
async drag(fromX, fromY, toX, toY, options) {
|
|
247
|
+
this.ensureInitialized();
|
|
248
|
+
this.validateNumber(fromX, 'fromX');
|
|
249
|
+
this.validateNumber(fromY, 'fromY');
|
|
250
|
+
this.validateNumber(toX, 'toX');
|
|
251
|
+
this.validateNumber(toY, 'toY');
|
|
252
|
+
const button = options?.button ?? 'left';
|
|
253
|
+
const flags = this.getMouseFlags(button);
|
|
254
|
+
const duration = options?.duration ?? 300;
|
|
255
|
+
// Move to start position
|
|
256
|
+
await this.moveMouse(fromX, fromY);
|
|
257
|
+
await this.delay(50);
|
|
258
|
+
// Mouse down
|
|
259
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${flags.down}, 0, 0, 0, [IntPtr]::Zero)`);
|
|
260
|
+
await this.delay(duration);
|
|
261
|
+
// Move to end position
|
|
262
|
+
await this.moveMouse(toX, toY);
|
|
263
|
+
await this.delay(50);
|
|
264
|
+
// Mouse up
|
|
265
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${flags.up}, 0, 0, 0, [IntPtr]::Zero)`);
|
|
266
|
+
}
|
|
267
|
+
async scroll(options) {
|
|
268
|
+
this.ensureInitialized();
|
|
269
|
+
const deltaY = options.deltaY ?? 0;
|
|
270
|
+
const deltaX = options.deltaX ?? 0;
|
|
271
|
+
if (deltaY !== 0) {
|
|
272
|
+
this.validateNumber(deltaY, 'deltaY');
|
|
273
|
+
const amount = Math.round(deltaY * 120);
|
|
274
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${MOUSEEVENTF_WHEEL}, 0, 0, ${amount}, [IntPtr]::Zero)`);
|
|
275
|
+
}
|
|
276
|
+
if (deltaX !== 0) {
|
|
277
|
+
this.validateNumber(deltaX, 'deltaX');
|
|
278
|
+
const amount = Math.round(deltaX * 120);
|
|
279
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::mouse_event(${MOUSEEVENTF_HWHEEL}, 0, 0, ${amount}, [IntPtr]::Zero)`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Keyboard
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
async keyPress(key, options) {
|
|
286
|
+
this.ensureInitialized();
|
|
287
|
+
const vk = this.resolveVK(key);
|
|
288
|
+
const modifiers = options?.modifiers ?? [];
|
|
289
|
+
const modVKs = modifiers.map(m => {
|
|
290
|
+
const mvk = MODIFIER_VK[m];
|
|
291
|
+
if (mvk === undefined)
|
|
292
|
+
throw new Error(`Unknown modifier: ${m}`);
|
|
293
|
+
return mvk;
|
|
294
|
+
});
|
|
295
|
+
// Build PS script: modifiers down, key down/up, modifiers up
|
|
296
|
+
const lines = [this.P_INVOKE_BLOCK];
|
|
297
|
+
for (const mvk of modVKs) {
|
|
298
|
+
lines.push(`[NativeInput]::keybd_event(${mvk}, 0, 0, [IntPtr]::Zero)`);
|
|
299
|
+
}
|
|
300
|
+
lines.push(`[NativeInput]::keybd_event(${vk}, 0, 0, [IntPtr]::Zero)`);
|
|
301
|
+
if (options?.delay) {
|
|
302
|
+
lines.push(`Start-Sleep -Milliseconds ${Math.round(options.delay)}`);
|
|
303
|
+
}
|
|
304
|
+
lines.push(`[NativeInput]::keybd_event(${vk}, 0, ${KEYEVENTF_KEYUP}, [IntPtr]::Zero)`);
|
|
305
|
+
for (const mvk of modVKs.reverse()) {
|
|
306
|
+
lines.push(`[NativeInput]::keybd_event(${mvk}, 0, ${KEYEVENTF_KEYUP}, [IntPtr]::Zero)`);
|
|
307
|
+
}
|
|
308
|
+
await this.ps(lines.join('; '));
|
|
309
|
+
}
|
|
310
|
+
async keyDown(key) {
|
|
311
|
+
this.ensureInitialized();
|
|
312
|
+
const vk = this.resolveVK(key);
|
|
313
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::keybd_event(${vk}, 0, 0, [IntPtr]::Zero)`);
|
|
314
|
+
}
|
|
315
|
+
async keyUp(key) {
|
|
316
|
+
this.ensureInitialized();
|
|
317
|
+
const vk = this.resolveVK(key);
|
|
318
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::keybd_event(${vk}, 0, ${KEYEVENTF_KEYUP}, [IntPtr]::Zero)`);
|
|
319
|
+
}
|
|
320
|
+
async type(text, _options) {
|
|
321
|
+
this.ensureInitialized();
|
|
322
|
+
// Escape special SendKeys characters: +^%~(){}[]
|
|
323
|
+
const escaped = text.replace(/([+^%~(){}[\]])/g, '{$1}');
|
|
324
|
+
const psEscaped = this.escapePsSingleQuote(escaped);
|
|
325
|
+
await this.ps(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${psEscaped}')`);
|
|
326
|
+
}
|
|
327
|
+
async hotkey(sequence) {
|
|
328
|
+
this.ensureInitialized();
|
|
329
|
+
const modifiers = sequence.modifiers ?? [];
|
|
330
|
+
const modVKs = modifiers.map(m => {
|
|
331
|
+
const mvk = MODIFIER_VK[m];
|
|
332
|
+
if (mvk === undefined)
|
|
333
|
+
throw new Error(`Unknown modifier: ${m}`);
|
|
334
|
+
return mvk;
|
|
335
|
+
});
|
|
336
|
+
const lines = [this.P_INVOKE_BLOCK];
|
|
337
|
+
// Modifiers down
|
|
338
|
+
for (const mvk of modVKs) {
|
|
339
|
+
lines.push(`[NativeInput]::keybd_event(${mvk}, 0, 0, [IntPtr]::Zero)`);
|
|
340
|
+
}
|
|
341
|
+
// Key presses
|
|
342
|
+
for (const key of sequence.keys) {
|
|
343
|
+
const vk = this.resolveVK(key);
|
|
344
|
+
lines.push(`[NativeInput]::keybd_event(${vk}, 0, 0, [IntPtr]::Zero)`);
|
|
345
|
+
lines.push(`[NativeInput]::keybd_event(${vk}, 0, ${KEYEVENTF_KEYUP}, [IntPtr]::Zero)`);
|
|
346
|
+
}
|
|
347
|
+
// Modifiers up (reverse order)
|
|
348
|
+
for (const mvk of [...modVKs].reverse()) {
|
|
349
|
+
lines.push(`[NativeInput]::keybd_event(${mvk}, 0, ${KEYEVENTF_KEYUP}, [IntPtr]::Zero)`);
|
|
350
|
+
}
|
|
351
|
+
await this.ps(lines.join('; '));
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Windows
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
async getActiveWindow() {
|
|
357
|
+
this.ensureInitialized();
|
|
358
|
+
try {
|
|
359
|
+
const handleStr = await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::GetForegroundWindow().ToInt64()`);
|
|
360
|
+
const handle = handleStr.trim();
|
|
361
|
+
if (!handle || handle === '0')
|
|
362
|
+
return null;
|
|
363
|
+
return this.getWindow(handle);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async getWindows(_options) {
|
|
370
|
+
this.ensureInitialized();
|
|
371
|
+
const result = await this.ps('Get-Process | Where-Object {$_.MainWindowHandle -ne 0} | ForEach-Object { Write-Output "$($_.MainWindowHandle)|$($_.Id)|$($_.ProcessName)|$($_.MainWindowTitle)" }');
|
|
372
|
+
const windows = [];
|
|
373
|
+
const lines = result.split('\n').filter(l => l.trim());
|
|
374
|
+
for (const line of lines) {
|
|
375
|
+
const parts = line.split('|');
|
|
376
|
+
if (parts.length < 4)
|
|
377
|
+
continue;
|
|
378
|
+
const handle = parts[0].trim();
|
|
379
|
+
const pid = parseInt(parts[1].trim(), 10);
|
|
380
|
+
const processName = parts[2].trim();
|
|
381
|
+
const title = parts.slice(3).join('|').trim();
|
|
382
|
+
windows.push({
|
|
383
|
+
handle,
|
|
384
|
+
title,
|
|
385
|
+
pid,
|
|
386
|
+
processName,
|
|
387
|
+
bounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
388
|
+
focused: false,
|
|
389
|
+
visible: true,
|
|
390
|
+
minimized: false,
|
|
391
|
+
maximized: false,
|
|
392
|
+
fullscreen: false,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
// Apply search filters
|
|
396
|
+
if (_options) {
|
|
397
|
+
return windows.filter(w => {
|
|
398
|
+
if (_options.title) {
|
|
399
|
+
const titleMatch = _options.title instanceof RegExp
|
|
400
|
+
? _options.title.test(w.title)
|
|
401
|
+
: w.title.includes(_options.title);
|
|
402
|
+
if (!titleMatch)
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
if (_options.processName && w.processName !== _options.processName)
|
|
406
|
+
return false;
|
|
407
|
+
if (_options.pid && w.pid !== _options.pid)
|
|
408
|
+
return false;
|
|
409
|
+
return true;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return windows;
|
|
413
|
+
}
|
|
414
|
+
async getWindow(handle) {
|
|
415
|
+
this.ensureInitialized();
|
|
416
|
+
const handleNum = parseInt(handle, 10);
|
|
417
|
+
this.validateNumber(handleNum, 'handle');
|
|
418
|
+
try {
|
|
419
|
+
const script = `${this.P_INVOKE_BLOCK};
|
|
420
|
+
$h = [IntPtr]${handleNum};
|
|
421
|
+
$r = New-Object NativeInput+RECT;
|
|
422
|
+
[NativeInput]::GetWindowRect($h, [ref]$r) | Out-Null;
|
|
423
|
+
$len = [NativeInput]::GetWindowTextLength($h);
|
|
424
|
+
$sb = New-Object System.Text.StringBuilder($len + 1);
|
|
425
|
+
[NativeInput]::GetWindowText($h, $sb, $sb.Capacity) | Out-Null;
|
|
426
|
+
$pid = [uint32]0;
|
|
427
|
+
[NativeInput]::GetWindowThreadProcessId($h, [ref]$pid) | Out-Null;
|
|
428
|
+
$fg = [NativeInput]::GetForegroundWindow();
|
|
429
|
+
$pname = '';
|
|
430
|
+
try { $pname = (Get-Process -Id $pid -ErrorAction SilentlyContinue).ProcessName } catch {}
|
|
431
|
+
Write-Output "$($r.Left)|$($r.Top)|$($r.Right)|$($r.Bottom)|$($sb.ToString())|$pid|$pname|$($fg -eq $h)"`;
|
|
432
|
+
const result = await this.ps(script);
|
|
433
|
+
const parts = result.trim().split('|');
|
|
434
|
+
if (parts.length < 8)
|
|
435
|
+
return null;
|
|
436
|
+
const left = parseInt(parts[0], 10);
|
|
437
|
+
const top = parseInt(parts[1], 10);
|
|
438
|
+
const right = parseInt(parts[2], 10);
|
|
439
|
+
const bottom = parseInt(parts[3], 10);
|
|
440
|
+
const title = parts[4];
|
|
441
|
+
const pid = parseInt(parts[5], 10);
|
|
442
|
+
const processName = parts[6];
|
|
443
|
+
const focused = parts[7].trim().toLowerCase() === 'true';
|
|
444
|
+
return {
|
|
445
|
+
handle,
|
|
446
|
+
title,
|
|
447
|
+
pid,
|
|
448
|
+
processName,
|
|
449
|
+
bounds: {
|
|
450
|
+
x: left,
|
|
451
|
+
y: top,
|
|
452
|
+
width: right - left,
|
|
453
|
+
height: bottom - top,
|
|
454
|
+
},
|
|
455
|
+
focused,
|
|
456
|
+
visible: true,
|
|
457
|
+
minimized: false,
|
|
458
|
+
maximized: false,
|
|
459
|
+
fullscreen: false,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async focusWindow(handle) {
|
|
467
|
+
this.ensureInitialized();
|
|
468
|
+
const handleNum = parseInt(handle, 10);
|
|
469
|
+
this.validateNumber(handleNum, 'handle');
|
|
470
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::SetForegroundWindow([IntPtr]${handleNum})`);
|
|
471
|
+
}
|
|
472
|
+
async minimizeWindow(handle) {
|
|
473
|
+
this.ensureInitialized();
|
|
474
|
+
const handleNum = parseInt(handle, 10);
|
|
475
|
+
this.validateNumber(handleNum, 'handle');
|
|
476
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::ShowWindow([IntPtr]${handleNum}, 6)`);
|
|
477
|
+
}
|
|
478
|
+
async maximizeWindow(handle) {
|
|
479
|
+
this.ensureInitialized();
|
|
480
|
+
const handleNum = parseInt(handle, 10);
|
|
481
|
+
this.validateNumber(handleNum, 'handle');
|
|
482
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::ShowWindow([IntPtr]${handleNum}, 3)`);
|
|
483
|
+
}
|
|
484
|
+
async restoreWindow(handle) {
|
|
485
|
+
this.ensureInitialized();
|
|
486
|
+
const handleNum = parseInt(handle, 10);
|
|
487
|
+
this.validateNumber(handleNum, 'handle');
|
|
488
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::ShowWindow([IntPtr]${handleNum}, 9)`);
|
|
489
|
+
}
|
|
490
|
+
async closeWindow(handle) {
|
|
491
|
+
this.ensureInitialized();
|
|
492
|
+
const handleNum = parseInt(handle, 10);
|
|
493
|
+
this.validateNumber(handleNum, 'handle');
|
|
494
|
+
await this.ps(`${this.P_INVOKE_BLOCK}; [NativeInput]::SendMessage([IntPtr]${handleNum}, 0x10, [IntPtr]::Zero, [IntPtr]::Zero)`);
|
|
495
|
+
}
|
|
496
|
+
async setWindow(handle, options) {
|
|
497
|
+
this.ensureInitialized();
|
|
498
|
+
const handleNum = parseInt(handle, 10);
|
|
499
|
+
this.validateNumber(handleNum, 'handle');
|
|
500
|
+
const lines = [this.P_INVOKE_BLOCK];
|
|
501
|
+
if (options.position || options.size) {
|
|
502
|
+
// Need current rect to fill in missing values
|
|
503
|
+
const current = await this.getWindow(handle);
|
|
504
|
+
const x = options.position?.x ?? current?.bounds.x ?? 0;
|
|
505
|
+
const y = options.position?.y ?? current?.bounds.y ?? 0;
|
|
506
|
+
const w = options.size?.width ?? current?.bounds.width ?? 800;
|
|
507
|
+
const h = options.size?.height ?? current?.bounds.height ?? 600;
|
|
508
|
+
this.validateNumber(x, 'x');
|
|
509
|
+
this.validateNumber(y, 'y');
|
|
510
|
+
this.validateNumber(w, 'width');
|
|
511
|
+
this.validateNumber(h, 'height');
|
|
512
|
+
lines.push(`[NativeInput]::MoveWindow([IntPtr]${handleNum}, ${Math.round(x)}, ${Math.round(y)}, ${Math.round(w)}, ${Math.round(h)}, $true)`);
|
|
513
|
+
}
|
|
514
|
+
if (options.focus) {
|
|
515
|
+
lines.push(`[NativeInput]::SetForegroundWindow([IntPtr]${handleNum})`);
|
|
516
|
+
}
|
|
517
|
+
if (lines.length > 1) {
|
|
518
|
+
await this.ps(lines.join('; '));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Applications
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
async getRunningApps() {
|
|
525
|
+
this.ensureInitialized();
|
|
526
|
+
const result = await this.ps('Get-Process | Where-Object {$_.MainWindowHandle -ne 0} | Select-Object Id,ProcessName,Path | ConvertTo-Json -Compress');
|
|
527
|
+
try {
|
|
528
|
+
const parsed = JSON.parse(result);
|
|
529
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
530
|
+
return items.map((item) => ({
|
|
531
|
+
name: item.ProcessName ?? '',
|
|
532
|
+
path: item.Path ?? '',
|
|
533
|
+
pid: item.Id,
|
|
534
|
+
running: true,
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async launchApp(appPath, options) {
|
|
542
|
+
this.ensureInitialized();
|
|
543
|
+
const escapedPath = this.escapePsSingleQuote(appPath);
|
|
544
|
+
let cmd = `Start-Process '${escapedPath}' -PassThru`;
|
|
545
|
+
if (options?.args && options.args.length > 0) {
|
|
546
|
+
const args = options.args.map(a => this.escapePsSingleQuote(a)).join(' ');
|
|
547
|
+
cmd += ` -ArgumentList '${args}'`;
|
|
548
|
+
}
|
|
549
|
+
if (options?.cwd) {
|
|
550
|
+
cmd += ` -WorkingDirectory '${this.escapePsSingleQuote(options.cwd)}'`;
|
|
551
|
+
}
|
|
552
|
+
if (options?.hidden) {
|
|
553
|
+
cmd += ' -WindowStyle Hidden';
|
|
554
|
+
}
|
|
555
|
+
cmd += ' | Select-Object Id,ProcessName | ConvertTo-Json -Compress';
|
|
556
|
+
const result = await this.ps(cmd);
|
|
557
|
+
try {
|
|
558
|
+
const parsed = JSON.parse(result);
|
|
559
|
+
return {
|
|
560
|
+
name: parsed.ProcessName ?? '',
|
|
561
|
+
path: appPath,
|
|
562
|
+
pid: parsed.Id,
|
|
563
|
+
running: true,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
return {
|
|
568
|
+
name: appPath,
|
|
569
|
+
path: appPath,
|
|
570
|
+
running: true,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async closeApp(pid) {
|
|
575
|
+
this.ensureInitialized();
|
|
576
|
+
this.validateNumber(pid, 'pid');
|
|
577
|
+
await this.ps(`Stop-Process -Id ${Math.round(pid)}`);
|
|
578
|
+
}
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// Screens
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
async getScreens() {
|
|
583
|
+
this.ensureInitialized();
|
|
584
|
+
const result = await this.ps('Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::AllScreens | ForEach-Object { Write-Output "$($_.DeviceName)|$($_.Bounds.X)|$($_.Bounds.Y)|$($_.Bounds.Width)|$($_.Bounds.Height)|$($_.WorkingArea.X)|$($_.WorkingArea.Y)|$($_.WorkingArea.Width)|$($_.WorkingArea.Height)|$($_.Primary)" }');
|
|
585
|
+
const screens = [];
|
|
586
|
+
const lines = result.split('\n').filter(l => l.trim());
|
|
587
|
+
for (let i = 0; i < lines.length; i++) {
|
|
588
|
+
const parts = lines[i].split('|');
|
|
589
|
+
if (parts.length < 10)
|
|
590
|
+
continue;
|
|
591
|
+
screens.push({
|
|
592
|
+
id: i,
|
|
593
|
+
name: parts[0].trim(),
|
|
594
|
+
bounds: {
|
|
595
|
+
x: parseInt(parts[1], 10),
|
|
596
|
+
y: parseInt(parts[2], 10),
|
|
597
|
+
width: parseInt(parts[3], 10),
|
|
598
|
+
height: parseInt(parts[4], 10),
|
|
599
|
+
},
|
|
600
|
+
workArea: {
|
|
601
|
+
x: parseInt(parts[5], 10),
|
|
602
|
+
y: parseInt(parts[6], 10),
|
|
603
|
+
width: parseInt(parts[7], 10),
|
|
604
|
+
height: parseInt(parts[8], 10),
|
|
605
|
+
},
|
|
606
|
+
scaleFactor: 1,
|
|
607
|
+
primary: parts[9].trim().toLowerCase() === 'true',
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return screens;
|
|
611
|
+
}
|
|
612
|
+
async getPixelColor(x, y) {
|
|
613
|
+
this.ensureInitialized();
|
|
614
|
+
this.validateNumber(x, 'x');
|
|
615
|
+
this.validateNumber(y, 'y');
|
|
616
|
+
const result = await this.ps(`Add-Type -AssemblyName System.Drawing; $bmp = New-Object System.Drawing.Bitmap(1, 1); $g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen(${Math.round(x)}, ${Math.round(y)}, 0, 0, (New-Object System.Drawing.Size(1, 1))); $c = $bmp.GetPixel(0, 0); Write-Output "$($c.R)|$($c.G)|$($c.B)|$($c.A)"; $g.Dispose(); $bmp.Dispose()`);
|
|
617
|
+
const parts = result.trim().split('|');
|
|
618
|
+
const r = parseInt(parts[0], 10);
|
|
619
|
+
const g = parseInt(parts[1], 10);
|
|
620
|
+
const b = parseInt(parts[2], 10);
|
|
621
|
+
const a = parts.length > 3 ? parseInt(parts[3], 10) : 255;
|
|
622
|
+
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
623
|
+
return { r, g, b, a, hex };
|
|
624
|
+
}
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Clipboard
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
async getClipboard() {
|
|
629
|
+
this.ensureInitialized();
|
|
630
|
+
try {
|
|
631
|
+
const text = await this.ps('Get-Clipboard');
|
|
632
|
+
return {
|
|
633
|
+
text: text || undefined,
|
|
634
|
+
formats: text ? ['text'] : [],
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
return { formats: [] };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async setClipboard(content) {
|
|
642
|
+
this.ensureInitialized();
|
|
643
|
+
if (content.text !== undefined) {
|
|
644
|
+
const escaped = this.escapePsSingleQuote(content.text);
|
|
645
|
+
await this.ps(`Set-Clipboard -Value '${escaped}'`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async clearClipboard() {
|
|
649
|
+
this.ensureInitialized();
|
|
650
|
+
await this.ps('Set-Clipboard -Value $null');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
//# sourceMappingURL=windows-native-provider.js.map
|