dsh-win-cua 0.1.0 → 0.2.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/LICENSE +1 -1
- package/README.md +59 -11
- package/cordis.patch.yml +3 -0
- package/index.js +432 -35
- package/package.json +14 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-
|
|
1
|
+
# dsh-win-cua
|
|
2
2
|
|
|
3
3
|
Windows computer-use for DeepSeek Harness: **real** mouse and keyboard input plus screen
|
|
4
4
|
capture, with no external driver.
|
|
@@ -19,18 +19,30 @@ the virtual screen; the primary display's top-left is `(0, 0)`.
|
|
|
19
19
|
|
|
20
20
|
## Tools
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
- `
|
|
22
|
+
**Look**
|
|
23
|
+
- `cua_screenshot` — capture the whole virtual screen to a PNG
|
|
24
|
+
- `cua_screenshot_window` — capture one window by handle (`PrintWindow`, screen-blit fallback)
|
|
25
|
+
- `cua_zoom` — capture a region and upscale it x1–x4 for reading small UI text
|
|
26
|
+
- `cua_windows` — visible top-level windows with bounds and flags
|
|
27
|
+
- `cua_app_list` — same list enriched with the process executable name
|
|
28
|
+
- `cua_cursor` — pointer position and virtual-screen geometry
|
|
29
|
+
|
|
30
|
+
**Act**
|
|
31
|
+
- `cua_mouse_move` — move the pointer along a human path
|
|
32
|
+
- `cua_mouse_click` — click / double / right / middle, optionally at x,y
|
|
33
|
+
- `cua_drag` — press, travel and release with the button held
|
|
34
|
+
- `cua_scroll` — wheel, vertical or horizontal
|
|
26
35
|
- `cua_key` — one key with optional modifiers
|
|
27
|
-
- `cua_type` — type text
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- `
|
|
36
|
+
- `cua_type` — type text (unicode, incl. surrogate pairs)
|
|
37
|
+
|
|
38
|
+
**Window & app**
|
|
39
|
+
- `cua_focus_window` — restore and foreground a window
|
|
40
|
+
- `cua_wait` — sleep and/or poll until a window title appears
|
|
41
|
+
- `cua_app_launch` — launch an executable without a shell
|
|
31
42
|
|
|
32
43
|
Every tool declares `output { schema, render }` — the DSH tool registry rejects raw
|
|
33
|
-
definitions without it, so keep that shape when adding tools.
|
|
44
|
+
definitions without it, so keep that shape when adding tools. Read-only tools are marked
|
|
45
|
+
`readOnly` and register as `isConcurrencySafe`.
|
|
34
46
|
|
|
35
47
|
## Config
|
|
36
48
|
|
|
@@ -39,16 +51,36 @@ Set in this bundle's `cordis.patch.yml` under `config`:
|
|
|
39
51
|
| Key | Default | Meaning |
|
|
40
52
|
|---|---|---|
|
|
41
53
|
| `humanize` | `true` | Pointer travels a smoothstep path, typing and clicking keep a human cadence |
|
|
42
|
-
| `keepScreenshots` | `30` | Newest
|
|
54
|
+
| `keepScreenshots` | `30` | Newest captures kept per directory |
|
|
43
55
|
| `moveSteps` | `16` | Max pointer steps for a humanized move |
|
|
44
56
|
| `keyDelayMs` | `[12, 34]` | Per-character delay range for humanized typing |
|
|
45
57
|
| `clickSettleMs` | `[60, 140]` | Pause between the pointer arriving and the button press |
|
|
58
|
+
| `zoomFactor` | `2` | Default `cua_zoom` upscale factor (hard cap 4) |
|
|
59
|
+
| `zoomMaxPixels` | `24000000` | Largest upscaled image allowed |
|
|
60
|
+
| `maxWaitMs` | `10000` | Upper bound for `cua_wait` |
|
|
61
|
+
| `allowLaunch` | `true` | Set `false` to disable `cua_app_launch` |
|
|
46
62
|
|
|
47
63
|
Pointer targets are clamped into the virtual screen, and astral code points (emoji,
|
|
48
64
|
CJK extension B) are sent as surrogate pairs so `KEYEVENTF_UNICODE` types them correctly.
|
|
49
65
|
|
|
66
|
+
## No external driver
|
|
67
|
+
|
|
68
|
+
Unlike `dsh-computer-use` (which shells out to the `cua-driver` binary) this plugin calls
|
|
69
|
+
Win32 through koffi **inside the host process**: nothing to install, no daemon, no
|
|
70
|
+
`CUA_DRIVER_BIN`. The trade-off is honest: without the driver there is no UIA/accessibility
|
|
71
|
+
tree, so targeting is pixel-based — use `cua_zoom` / `cua_screenshot_window` to read a
|
|
72
|
+
window precisely before clicking.
|
|
73
|
+
|
|
50
74
|
## Install
|
|
51
75
|
|
|
76
|
+
From npm:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
dsh plugin --profile <profile> add dsh-win-cua
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or from a checkout / local directory:
|
|
83
|
+
|
|
52
84
|
```powershell
|
|
53
85
|
dsh plugin --profile <profile> add <this directory>
|
|
54
86
|
```
|
|
@@ -57,3 +89,19 @@ Or in the Desktop app: Plugin Manager → install bundle → pick this directory
|
|
|
57
89
|
|
|
58
90
|
Windows only. `SendInput` injects into the session whose input queue is active — a window
|
|
59
91
|
on a locked or disconnected desktop receives nothing.
|
|
92
|
+
|
|
93
|
+
## Changelog
|
|
94
|
+
|
|
95
|
+
### 0.2.0
|
|
96
|
+
|
|
97
|
+
- New look tools: `cua_screenshot_window` (PrintWindow with screen-blit fallback),
|
|
98
|
+
`cua_zoom` (region capture upscaled x1–x4), `cua_app_list` (windows with process names).
|
|
99
|
+
- New act tools: `cua_drag`, `cua_wait`, `cua_app_launch` (disabled with `allowLaunch: false`).
|
|
100
|
+
- `humanize` is now a real config knob: smoothstep pointer path, per-character typing
|
|
101
|
+
delay and click settle range, with `moveSteps` / `keyDelayMs` / `clickSettleMs` tuning.
|
|
102
|
+
- PNG encoder, screenshot retention (`keepScreenshots`) and coordinate clamping tightened.
|
|
103
|
+
|
|
104
|
+
### 0.1.0
|
|
105
|
+
|
|
106
|
+
First public release: screenshot, cursor, window list/focus, mouse move/click/scroll,
|
|
107
|
+
key press and unicode typing.
|
package/cordis.patch.yml
CHANGED
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* dsh-win-cua — Windows computer-use for DeepSeek Harness.
|
|
3
3
|
*
|
|
4
4
|
* Real mouse and keyboard input through user32 `SendInput`, screen capture through
|
|
5
5
|
* GDI `BitBlt` (CAPTUREBLT so layered windows are included), and a top-level window
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { deflateSync } from 'node:zlib';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
-
import { mkdirSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
|
|
14
|
+
import { mkdirSync, writeFileSync, readdirSync, unlinkSync, existsSync } from 'node:fs';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
15
16
|
import { join } from 'node:path';
|
|
16
17
|
import { tmpdir } from 'node:os';
|
|
17
18
|
|
|
@@ -50,6 +51,7 @@ function loadNative() {
|
|
|
50
51
|
|
|
51
52
|
const user32 = koffi.load('user32.dll');
|
|
52
53
|
const gdi32 = koffi.load('gdi32.dll');
|
|
54
|
+
const kernel32 = koffi.load('kernel32.dll');
|
|
53
55
|
|
|
54
56
|
koffi.alias('HANDLE', 'void *');
|
|
55
57
|
koffi.alias('HWND', 'HANDLE');
|
|
@@ -105,6 +107,11 @@ function loadNative() {
|
|
|
105
107
|
DeleteDC: gdi32.func('bool __stdcall DeleteDC(HANDLE dc)'),
|
|
106
108
|
BitBlt: gdi32.func('bool __stdcall BitBlt(HANDLE dst, int x, int y, int w, int h, HANDLE src, int sx, int sy, uint32_t rop)'),
|
|
107
109
|
GetDIBits: gdi32.func('int __stdcall GetDIBits(HANDLE dc, HANDLE bmp, uint32_t start, uint32_t lines, void *bits, _Inout_ BITMAPINFOHEADER *bmi, uint32_t usage)'),
|
|
110
|
+
PrintWindow: user32.func('bool __stdcall PrintWindow(HWND hwnd, HANDLE dc, uint32_t flags)'),
|
|
111
|
+
GetWindowDC: user32.func('HANDLE __stdcall GetWindowDC(HWND hwnd)'),
|
|
112
|
+
OpenProcess: kernel32.func('HANDLE __stdcall OpenProcess(uint32_t access, bool inherit, uint32_t pid)'),
|
|
113
|
+
QueryFullProcessImageNameW: kernel32.func('bool __stdcall QueryFullProcessImageNameW(HANDLE proc, uint32_t flags, _Out_ uint16_t *buf, _Inout_ uint32_t *size)'),
|
|
114
|
+
CloseHandle: kernel32.func('bool __stdcall CloseHandle(HANDLE handle)'),
|
|
108
115
|
structs: { POINT, RECT, INPUT, BITMAPINFOHEADER },
|
|
109
116
|
};
|
|
110
117
|
try {
|
|
@@ -134,6 +141,10 @@ const KEYEVENTF_KEYUP = 0x0002;
|
|
|
134
141
|
const KEYEVENTF_UNICODE = 0x0004;
|
|
135
142
|
const MAX_COORD = 65535;
|
|
136
143
|
const SW_RESTORE = 9;
|
|
144
|
+
const MOUSEEVENTF_HWHEEL = 0x01000;
|
|
145
|
+
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
|
|
146
|
+
const PW_RENDERFULLCONTENT = 0x00000002;
|
|
147
|
+
const MAX_CAPTURE_PIXELS = 40_000_000;
|
|
137
148
|
|
|
138
149
|
/** Behaviour defaults; the host may override them through cordis.patch.yml config. */
|
|
139
150
|
const DEFAULTS = {
|
|
@@ -142,18 +153,35 @@ const DEFAULTS = {
|
|
|
142
153
|
moveSteps: 16,
|
|
143
154
|
keyDelayMs: [12, 34],
|
|
144
155
|
clickSettleMs: [60, 140],
|
|
156
|
+
zoomFactor: 2,
|
|
157
|
+
zoomMaxPixels: 24_000_000,
|
|
158
|
+
maxWaitMs: 10_000,
|
|
159
|
+
allowLaunch: true,
|
|
145
160
|
};
|
|
146
161
|
const CFG = { ...DEFAULTS };
|
|
147
162
|
|
|
148
163
|
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
149
164
|
const between = ([min, max]) => min + Math.random() * Math.max(0, max - min);
|
|
150
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Raw `tools.register()` does not validate arguments the way `defineTool` does,
|
|
168
|
+
* so every coordinate has to be checked here — otherwise a missing number turns
|
|
169
|
+
* into a NaN deep inside Buffer.alloc instead of a message the model can act on.
|
|
170
|
+
*/
|
|
171
|
+
function requireNumber(value, name) {
|
|
172
|
+
const n = Number(value);
|
|
173
|
+
if (!Number.isFinite(n)) throw new Error(`${name} must be a finite number (received ${JSON.stringify(value)})`);
|
|
174
|
+
return n;
|
|
175
|
+
}
|
|
176
|
+
|
|
151
177
|
/** Clamp a pointer target into the virtual screen so a bad coordinate cannot fly off. */
|
|
152
178
|
function clampPoint(A, x, y) {
|
|
153
179
|
const screen = virtualScreen(A);
|
|
180
|
+
const px = Math.round(requireNumber(x, 'x'));
|
|
181
|
+
const py = Math.round(requireNumber(y, 'y'));
|
|
154
182
|
return {
|
|
155
|
-
x: Math.min(Math.max(
|
|
156
|
-
y: Math.min(Math.max(
|
|
183
|
+
x: Math.min(Math.max(px, screen.x), screen.x + Math.max(0, screen.width - 1)),
|
|
184
|
+
y: Math.min(Math.max(py, screen.y), screen.y + Math.max(0, screen.height - 1)),
|
|
157
185
|
};
|
|
158
186
|
}
|
|
159
187
|
|
|
@@ -213,30 +241,37 @@ function toAbsolute(A, x, y) {
|
|
|
213
241
|
};
|
|
214
242
|
}
|
|
215
243
|
|
|
216
|
-
/**
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
244
|
+
/** Pull BGRA rows out of a selected bitmap. */
|
|
245
|
+
function readBits(A, dc, bmp, width, height) {
|
|
246
|
+
if (width * height > MAX_CAPTURE_PIXELS) throw new Error(`capture too large: ${width}x${height}`);
|
|
247
|
+
const bmi = {
|
|
248
|
+
biSize: A.koffi.sizeof(A.structs.BITMAPINFOHEADER),
|
|
249
|
+
biWidth: width, biHeight: -height, biPlanes: 1, biBitCount: 32,
|
|
250
|
+
biCompression: 0, biSizeImage: 0, biXPelsPerMeter: 0, biYPelsPerMeter: 0,
|
|
251
|
+
biClrUsed: 0, biClrImportant: 0,
|
|
252
|
+
};
|
|
253
|
+
const buffer = Buffer.alloc(width * height * 4);
|
|
254
|
+
const lines = A.GetDIBits(dc, bmp, 0, height, buffer, bmi, 0);
|
|
255
|
+
if (lines !== height) throw new Error(`GetDIBits returned ${lines}/${height} lines`);
|
|
256
|
+
return { width, height, data: buffer };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** BitBlt a screen rectangle (virtual-screen coordinates) into a fresh memory bitmap. */
|
|
260
|
+
function captureRect(A, sx, sy, width, height) {
|
|
261
|
+
const left = Math.round(requireNumber(sx, 'x'));
|
|
262
|
+
const top = Math.round(requireNumber(sy, 'y'));
|
|
263
|
+
const w = Math.max(1, Math.round(requireNumber(width, 'width')));
|
|
264
|
+
const h = Math.max(1, Math.round(requireNumber(height, 'height')));
|
|
221
265
|
const srcDc = A.GetDC(null);
|
|
222
266
|
if (!srcDc) throw new Error('GetDC returned null');
|
|
223
267
|
const memDc = A.CreateCompatibleDC(srcDc);
|
|
224
|
-
const bmp = A.CreateCompatibleBitmap(srcDc,
|
|
268
|
+
const bmp = A.CreateCompatibleBitmap(srcDc, w, h);
|
|
225
269
|
const old = A.SelectObject(memDc, bmp);
|
|
226
270
|
try {
|
|
227
|
-
if (!A.BitBlt(memDc, 0, 0,
|
|
271
|
+
if (!A.BitBlt(memDc, 0, 0, w, h, srcDc, left, top, SRCCOPY | CAPTUREBLT)) {
|
|
228
272
|
throw new Error('BitBlt failed');
|
|
229
273
|
}
|
|
230
|
-
|
|
231
|
-
biSize: A.koffi.sizeof(A.structs.BITMAPINFOHEADER),
|
|
232
|
-
biWidth: width, biHeight: -height, biPlanes: 1, biBitCount: 32,
|
|
233
|
-
biCompression: 0, biSizeImage: 0, biXPelsPerMeter: 0, biYPelsPerMeter: 0,
|
|
234
|
-
biClrUsed: 0, biClrImportant: 0,
|
|
235
|
-
};
|
|
236
|
-
const buffer = Buffer.alloc(width * height * 4);
|
|
237
|
-
const lines = A.GetDIBits(memDc, bmp, 0, height, buffer, bmi, 0);
|
|
238
|
-
if (lines !== height) throw new Error(`GetDIBits returned ${lines}/${height} lines`);
|
|
239
|
-
return { width, height, data: buffer };
|
|
274
|
+
return readBits(A, memDc, bmp, w, h);
|
|
240
275
|
} finally {
|
|
241
276
|
A.SelectObject(memDc, old);
|
|
242
277
|
A.DeleteObject(bmp);
|
|
@@ -245,6 +280,74 @@ function captureScreen(A) {
|
|
|
245
280
|
}
|
|
246
281
|
}
|
|
247
282
|
|
|
283
|
+
/** Grab the whole virtual screen as raw BGRA rows. */
|
|
284
|
+
function captureScreen(A) {
|
|
285
|
+
const screen = virtualScreen(A);
|
|
286
|
+
return captureRect(A, screen.x, screen.y, screen.width, screen.height);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Capture one window. PrintWindow asks the window to render itself into our DC
|
|
291
|
+
* (so occluded windows still come out right); when the window refuses, fall back
|
|
292
|
+
* to blitting its on-screen rectangle.
|
|
293
|
+
*/
|
|
294
|
+
function captureWindow(A, hwnd) {
|
|
295
|
+
const rect = { left: 0, top: 0, right: 0, bottom: 0 };
|
|
296
|
+
if (!A.GetWindowRect(hwnd, rect)) throw new Error(`GetWindowRect failed for handle ${hwnd}`);
|
|
297
|
+
const width = Math.max(1, rect.right - rect.left);
|
|
298
|
+
const height = Math.max(1, rect.bottom - rect.top);
|
|
299
|
+
const bounds = { x: rect.left, y: rect.top, width, height };
|
|
300
|
+
if (width * height > MAX_CAPTURE_PIXELS) throw new Error(`window too large to capture: ${width}x${height}`);
|
|
301
|
+
const winDc = A.GetWindowDC(hwnd);
|
|
302
|
+
if (!winDc) throw new Error(`GetWindowDC returned null for handle ${hwnd}`);
|
|
303
|
+
const memDc = A.CreateCompatibleDC(winDc);
|
|
304
|
+
const bmp = A.CreateCompatibleBitmap(winDc, width, height);
|
|
305
|
+
const old = A.SelectObject(memDc, bmp);
|
|
306
|
+
try {
|
|
307
|
+
const printed = A.PrintWindow(hwnd, memDc, PW_RENDERFULLCONTENT);
|
|
308
|
+
if (!printed) {
|
|
309
|
+
const screenDc = A.GetDC(null);
|
|
310
|
+
if (!screenDc) throw new Error('GetDC returned null');
|
|
311
|
+
try {
|
|
312
|
+
if (!A.BitBlt(memDc, 0, 0, width, height, screenDc, bounds.x, bounds.y, SRCCOPY | CAPTUREBLT)) {
|
|
313
|
+
throw new Error('window capture failed (PrintWindow and BitBlt both refused)');
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
A.ReleaseDC(null, screenDc);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { ...readBits(A, memDc, bmp, width, height), bounds, printed: Boolean(printed) };
|
|
320
|
+
} finally {
|
|
321
|
+
A.SelectObject(memDc, old);
|
|
322
|
+
A.DeleteObject(bmp);
|
|
323
|
+
A.DeleteDC(memDc);
|
|
324
|
+
A.ReleaseDC(hwnd, winDc);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Nearest-neighbour upscale: crisp pixels, which is what reading small UI text needs. */
|
|
329
|
+
function scaleBgra({ width, height, data }, factor) {
|
|
330
|
+
const scale = Math.max(1, Math.floor(factor));
|
|
331
|
+
if (scale === 1) return { width, height, data };
|
|
332
|
+
const ow = width * scale;
|
|
333
|
+
const oh = height * scale;
|
|
334
|
+
if (ow * oh > MAX_CAPTURE_PIXELS) throw new Error(`zoomed image too large: ${ow}x${oh}`);
|
|
335
|
+
const out = Buffer.alloc(ow * oh * 4);
|
|
336
|
+
for (let y = 0; y < oh; y += 1) {
|
|
337
|
+
const rowBase = Math.min(height - 1, Math.floor(y / scale)) * width;
|
|
338
|
+
const target = y * ow;
|
|
339
|
+
for (let x = 0; x < ow; x += 1) {
|
|
340
|
+
const s = (rowBase + Math.min(width - 1, Math.floor(x / scale))) * 4;
|
|
341
|
+
const t = (target + x) * 4;
|
|
342
|
+
out[t] = data[s];
|
|
343
|
+
out[t + 1] = data[s + 1];
|
|
344
|
+
out[t + 2] = data[s + 2];
|
|
345
|
+
out[t + 3] = data[s + 3];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { width: ow, height: oh, data: out };
|
|
349
|
+
}
|
|
350
|
+
|
|
248
351
|
const CRC_TABLE = (() => {
|
|
249
352
|
const table = new Uint32Array(256);
|
|
250
353
|
for (let n = 0; n < 256; n += 1) {
|
|
@@ -439,25 +542,30 @@ function clickMouse(A, button = 'left', double = false) {
|
|
|
439
542
|
}
|
|
440
543
|
}
|
|
441
544
|
|
|
442
|
-
/** Scroll the wheel; positive is up. */
|
|
443
|
-
function scrollWheel(A, amount) {
|
|
444
|
-
const delta = Math.round(
|
|
545
|
+
/** Scroll the wheel; positive is up (or right on the horizontal axis). */
|
|
546
|
+
function scrollWheel(A, amount, axis = 'vertical') {
|
|
547
|
+
const delta = Math.round(requireNumber(amount, 'amount'));
|
|
445
548
|
if (!delta) return;
|
|
446
|
-
|
|
549
|
+
const flag = String(axis).toLowerCase() === 'horizontal' ? MOUSEEVENTF_HWHEEL : MOUSEEVENTF_WHEEL;
|
|
550
|
+
sendInput(A, mouseInput({ data: delta, flags: flag }));
|
|
447
551
|
}
|
|
448
552
|
|
|
449
|
-
/**
|
|
450
|
-
function
|
|
553
|
+
/** Encode one frame to PNG on disk, then prune old captures. */
|
|
554
|
+
function savePng(frame, directory, prefix = 'screen') {
|
|
451
555
|
const target = directory || SCREEN_DIR;
|
|
452
556
|
mkdirSync(target, { recursive: true });
|
|
453
|
-
const frame = captureScreen(A);
|
|
454
557
|
const png = encodePng(frame);
|
|
455
|
-
const path = join(target,
|
|
558
|
+
const path = join(target, `${prefix}-${Date.now()}.png`);
|
|
456
559
|
writeFileSync(path, png);
|
|
457
560
|
pruneScreens(target);
|
|
458
561
|
return { path, width: frame.width, height: frame.height, bytes: png.length };
|
|
459
562
|
}
|
|
460
563
|
|
|
564
|
+
/** Screen capture to a PNG on disk, keeping only the newest screenshots. */
|
|
565
|
+
function screenshot(A, directory) {
|
|
566
|
+
return savePng(captureScreen(A), directory);
|
|
567
|
+
}
|
|
568
|
+
|
|
461
569
|
/** Retention is best effort: a locked or unreadable file must not fail the capture. */
|
|
462
570
|
function pruneScreens(directory) {
|
|
463
571
|
const keep = Number(CFG.keepScreenshots);
|
|
@@ -485,6 +593,84 @@ function cursorPosition(A) {
|
|
|
485
593
|
return { x: point.x, y: point.y };
|
|
486
594
|
}
|
|
487
595
|
|
|
596
|
+
/** Executable file name behind a process id, or '' when it cannot be read. */
|
|
597
|
+
function exeName(A, pid) {
|
|
598
|
+
if (!pid) return '';
|
|
599
|
+
const handle = A.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
|
600
|
+
if (!handle) return '';
|
|
601
|
+
try {
|
|
602
|
+
const size = [512];
|
|
603
|
+
const buffer = Buffer.alloc(512 * 2);
|
|
604
|
+
if (!A.QueryFullProcessImageNameW(handle, 0, buffer, size)) return '';
|
|
605
|
+
const full = buffer.toString('utf16le', 0, Math.max(0, size[0]) * 2);
|
|
606
|
+
return full.split(/[\\/]/).pop() || full;
|
|
607
|
+
} finally {
|
|
608
|
+
A.CloseHandle(handle);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Press, travel and release like a hand: the button stays down for the whole
|
|
614
|
+
* path so drag targets that watch hover state still see it.
|
|
615
|
+
*/
|
|
616
|
+
async function dragMouse(A, fromX, fromY, toX, toY, humanize) {
|
|
617
|
+
const start = clampPoint(A, fromX, fromY);
|
|
618
|
+
const end = clampPoint(A, toX, toY);
|
|
619
|
+
await moveMouseHuman(A, start.x, start.y, humanize);
|
|
620
|
+
if (humanize) await sleep(between([40, 90]));
|
|
621
|
+
sendInput(A, mouseInput({ flags: MOUSEEVENTF_LEFTDOWN }));
|
|
622
|
+
try {
|
|
623
|
+
if (humanize) {
|
|
624
|
+
const steps = Math.max(6, Math.min(CFG.moveSteps * 2, Math.round(Math.hypot(end.x - start.x, end.y - start.y) / 35)));
|
|
625
|
+
for (let i = 1; i <= steps; i += 1) {
|
|
626
|
+
const t = i / steps;
|
|
627
|
+
const eased = t * t * (3 - 2 * t);
|
|
628
|
+
moveMouse(A, start.x + (end.x - start.x) * eased, start.y + (end.y - start.y) * eased);
|
|
629
|
+
await sleep(8 + Math.random() * 10);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
moveMouse(A, end.x, end.y);
|
|
633
|
+
if (humanize) await sleep(between([50, 110]));
|
|
634
|
+
} finally {
|
|
635
|
+
sendInput(A, mouseInput({ flags: MOUSEEVENTF_LEFTUP }));
|
|
636
|
+
}
|
|
637
|
+
return { from: start, to: end };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Sleep, and optionally poll until a window whose title contains `title` exists. */
|
|
641
|
+
async function waitFor(A, { ms, title, timeoutMs }) {
|
|
642
|
+
const budget = Math.max(0, Math.min(timeoutMs === undefined ? CFG.maxWaitMs : requireNumber(timeoutMs, 'timeoutMs'), CFG.maxWaitMs));
|
|
643
|
+
const started = Date.now();
|
|
644
|
+
const sleepBudget = Math.min(ms === undefined ? 0 : Math.max(0, requireNumber(ms, 'ms')), budget);
|
|
645
|
+
if (sleepBudget > 0) await sleep(sleepBudget);
|
|
646
|
+
if (!title) return { waitedMs: Date.now() - started, matched: false, timedOut: false };
|
|
647
|
+
const needle = String(title).toLowerCase();
|
|
648
|
+
while (Date.now() - started < budget) {
|
|
649
|
+
if (listWindows(A).some((w) => w.title.toLowerCase().includes(needle))) {
|
|
650
|
+
return { waitedMs: Date.now() - started, matched: true, timedOut: false };
|
|
651
|
+
}
|
|
652
|
+
await sleep(150);
|
|
653
|
+
}
|
|
654
|
+
return { waitedMs: Date.now() - started, matched: false, timedOut: true };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Resolve an executable name or path without ever going through a shell. */
|
|
658
|
+
function resolveExe(name) {
|
|
659
|
+
const raw = String(name || '').trim();
|
|
660
|
+
if (!raw) throw new Error('empty executable name');
|
|
661
|
+
if (/[\\/]/.test(raw)) {
|
|
662
|
+
if (!existsSync(raw)) throw new Error(`executable not found: ${raw}`);
|
|
663
|
+
return raw;
|
|
664
|
+
}
|
|
665
|
+
const exe = raw.toLowerCase().endsWith('.exe') ? raw : `${raw}.exe`;
|
|
666
|
+
const dirs = (process.env.PATH || '').split(';').filter(Boolean);
|
|
667
|
+
for (const dir of dirs) {
|
|
668
|
+
const candidate = join(dir, exe);
|
|
669
|
+
if (existsSync(candidate)) return candidate;
|
|
670
|
+
}
|
|
671
|
+
throw new Error(`executable not found on PATH: ${raw} (pass an absolute path instead)`);
|
|
672
|
+
}
|
|
673
|
+
|
|
488
674
|
/**
|
|
489
675
|
* JSON-Schema helpers for raw `tools.register()` definitions. The host registry
|
|
490
676
|
* requires every tool to carry `output { schema, render }` and a lossless-JSON
|
|
@@ -501,6 +687,35 @@ const num = (description) => ({ type: 'number', description });
|
|
|
501
687
|
const bool = (description) => ({ type: 'boolean', description });
|
|
502
688
|
const text = (value) => [{ type: 'text', text: String(value) }];
|
|
503
689
|
|
|
690
|
+
/** Reusable output-schema fragments so every object node declares its fields. */
|
|
691
|
+
const point = (description) => ({
|
|
692
|
+
type: 'object',
|
|
693
|
+
description,
|
|
694
|
+
properties: { x: num('X in virtual-screen pixels.'), y: num('Y in virtual-screen pixels.') },
|
|
695
|
+
});
|
|
696
|
+
const rect = (description) => ({
|
|
697
|
+
type: 'object',
|
|
698
|
+
description,
|
|
699
|
+
properties: {
|
|
700
|
+
x: num('Left in virtual-screen pixels.'),
|
|
701
|
+
y: num('Top in virtual-screen pixels.'),
|
|
702
|
+
width: num('Width in pixels.'),
|
|
703
|
+
height: num('Height in pixels.'),
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
const windowItem = (description) => ({
|
|
707
|
+
type: 'object',
|
|
708
|
+
description,
|
|
709
|
+
properties: {
|
|
710
|
+
handle: { type: 'integer', description: 'Window handle; pass it to cua_focus_window.' },
|
|
711
|
+
title: str('Window title.'),
|
|
712
|
+
pid: { type: 'integer', description: 'Owner process id.' },
|
|
713
|
+
active: bool('Is the foreground window.'),
|
|
714
|
+
minimized: bool('Is minimized.'),
|
|
715
|
+
bounds: rect('Window rect on the virtual screen.'),
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
|
|
504
719
|
const TOOL_DECLS = [
|
|
505
720
|
{
|
|
506
721
|
name: 'cua_screenshot',
|
|
@@ -519,6 +734,7 @@ const TOOL_DECLS = [
|
|
|
519
734
|
}),
|
|
520
735
|
render: (_args, v) => text(`Screenshot saved: ${v.path} (${v.width}x${v.height}, ${v.bytes} bytes). Read this file to view it.`),
|
|
521
736
|
},
|
|
737
|
+
readOnly: true,
|
|
522
738
|
},
|
|
523
739
|
{
|
|
524
740
|
name: 'cua_mouse_move',
|
|
@@ -571,12 +787,13 @@ const TOOL_DECLS = [
|
|
|
571
787
|
},
|
|
572
788
|
{
|
|
573
789
|
name: 'cua_scroll',
|
|
574
|
-
description: 'Turn the real mouse wheel at the pointer position; positive scrolls up.',
|
|
790
|
+
description: 'Turn the real mouse wheel at the pointer position; positive scrolls up (or right on the horizontal axis).',
|
|
575
791
|
parameters: obj({
|
|
576
792
|
amount: num('Wheel delta (120 per notch).'),
|
|
793
|
+
axis: { type: 'string', enum: ['vertical', 'horizontal'], description: 'Scroll axis; defaults to vertical.' },
|
|
577
794
|
}, ['amount']),
|
|
578
795
|
run: (A, args) => {
|
|
579
|
-
scrollWheel(A, args.amount);
|
|
796
|
+
scrollWheel(A, args.amount, args.axis);
|
|
580
797
|
return { ok: true };
|
|
581
798
|
},
|
|
582
799
|
out: {
|
|
@@ -623,13 +840,14 @@ const TOOL_DECLS = [
|
|
|
623
840
|
run: (A) => ({ windows: listWindows(A) }),
|
|
624
841
|
out: {
|
|
625
842
|
schema: obj({
|
|
626
|
-
windows: { type: 'array', items:
|
|
843
|
+
windows: { type: 'array', items: windowItem('One visible window.'), description: 'Visible top-level windows.' },
|
|
627
844
|
}),
|
|
628
845
|
render: (_args, v) => text(
|
|
629
846
|
`Found ${v.windows.length} window(s):\n` + v.windows.map((w) =>
|
|
630
847
|
`#${w.handle}${w.active ? ' [active]' : ''}${w.minimized ? ' [minimized]' : ''} "${w.title}" pid=${w.pid} ${w.bounds.width}x${w.bounds.height} @ (${w.bounds.x},${w.bounds.y})`
|
|
631
848
|
).join('\n')),
|
|
632
849
|
},
|
|
850
|
+
readOnly: true,
|
|
633
851
|
},
|
|
634
852
|
{
|
|
635
853
|
name: 'cua_focus_window',
|
|
@@ -638,7 +856,7 @@ const TOOL_DECLS = [
|
|
|
638
856
|
handle: num('Window handle from cua_windows.'),
|
|
639
857
|
}, ['handle']),
|
|
640
858
|
run: (A, args) => {
|
|
641
|
-
const hwnd = A.koffi.address(args.handle);
|
|
859
|
+
const hwnd = A.koffi.address(requireNumber(args.handle, 'handle'));
|
|
642
860
|
A.ShowWindow(hwnd, SW_RESTORE);
|
|
643
861
|
A.SetForegroundWindow(hwnd);
|
|
644
862
|
A.BringWindowToTop(hwnd);
|
|
@@ -658,10 +876,183 @@ const TOOL_DECLS = [
|
|
|
658
876
|
schema: obj({
|
|
659
877
|
x: num('Pointer X.'),
|
|
660
878
|
y: num('Pointer Y.'),
|
|
661
|
-
screen:
|
|
879
|
+
screen: rect('Virtual-screen geometry.'),
|
|
662
880
|
}),
|
|
663
881
|
render: (_args, v) => text(`Cursor at (${v.x}, ${v.y}); virtual screen: ${JSON.stringify(v.screen)}.`),
|
|
664
882
|
},
|
|
883
|
+
readOnly: true,
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: 'cua_zoom',
|
|
887
|
+
description:
|
|
888
|
+
'Capture a screen region and upscale it so small text, icons and menu items become readable. Coordinates are virtual-screen pixels.',
|
|
889
|
+
parameters: obj({
|
|
890
|
+
x: num('Region left in virtual-screen pixels.'),
|
|
891
|
+
y: num('Region top in virtual-screen pixels.'),
|
|
892
|
+
width: num('Region width in pixels.'),
|
|
893
|
+
height: num('Region height in pixels.'),
|
|
894
|
+
factor: num('Integer upscale factor (default 2, max 4).'),
|
|
895
|
+
directory: str('Optional output directory for the PNG.'),
|
|
896
|
+
}, ['x', 'y', 'width', 'height']),
|
|
897
|
+
run: (A, args) => {
|
|
898
|
+
const screen = virtualScreen(A);
|
|
899
|
+
const x = Math.max(screen.x, Math.round(args.x));
|
|
900
|
+
const y = Math.max(screen.y, Math.round(args.y));
|
|
901
|
+
const right = Math.min(screen.x + screen.width, Math.round(args.x + args.width));
|
|
902
|
+
const bottom = Math.min(screen.y + screen.height, Math.round(args.y + args.height));
|
|
903
|
+
const w = right - x;
|
|
904
|
+
const h = bottom - y;
|
|
905
|
+
if (w <= 0 || h <= 0) throw new Error('zoom region falls outside the virtual screen');
|
|
906
|
+
let factor = Math.max(1, Math.min(4, Math.floor(Number(args.factor) || CFG.zoomFactor)));
|
|
907
|
+
while (factor > 1 && w * factor * h * factor > CFG.zoomMaxPixels) factor -= 1;
|
|
908
|
+
const scaled = scaleBgra(captureRect(A, x, y, w, h), factor);
|
|
909
|
+
const saved = savePng(scaled, args.directory, 'zoom');
|
|
910
|
+
return { ...saved, factor, source: { x, y, width: w, height: h } };
|
|
911
|
+
},
|
|
912
|
+
out: {
|
|
913
|
+
schema: obj({
|
|
914
|
+
path: str('Absolute path of the written PNG.'),
|
|
915
|
+
width: { type: 'integer', description: 'Saved width in pixels.' },
|
|
916
|
+
height: { type: 'integer', description: 'Saved height in pixels.' },
|
|
917
|
+
bytes: { type: 'integer', description: 'PNG size in bytes.' },
|
|
918
|
+
factor: { type: 'integer', description: 'Upscale factor actually used.' },
|
|
919
|
+
source: rect('Captured region in virtual-screen pixels.'),
|
|
920
|
+
}),
|
|
921
|
+
render: (_args, v) => text(
|
|
922
|
+
`Zoom saved: ${v.path} — ${v.source.width}x${v.source.height} at (${v.source.x},${v.source.y}) scaled x${v.factor} to ${v.width}x${v.height}.`),
|
|
923
|
+
},
|
|
924
|
+
readOnly: true,
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: 'cua_screenshot_window',
|
|
928
|
+
description:
|
|
929
|
+
'Capture one window by handle. Uses PrintWindow so occluded windows still render correctly, with a screen blit fallback.',
|
|
930
|
+
parameters: obj({
|
|
931
|
+
handle: num('Window handle from cua_windows.'),
|
|
932
|
+
directory: str('Optional output directory for the PNG.'),
|
|
933
|
+
}, ['handle']),
|
|
934
|
+
run: (A, args) => {
|
|
935
|
+
const hwnd = A.koffi.address(requireNumber(args.handle, 'handle'));
|
|
936
|
+
const frame = captureWindow(A, hwnd);
|
|
937
|
+
const saved = savePng(frame, args.directory, 'window');
|
|
938
|
+
return { ...saved, bounds: frame.bounds, printed: frame.printed };
|
|
939
|
+
},
|
|
940
|
+
out: {
|
|
941
|
+
schema: obj({
|
|
942
|
+
path: str('Absolute path of the written PNG.'),
|
|
943
|
+
width: { type: 'integer', description: 'Image width in pixels.' },
|
|
944
|
+
height: { type: 'integer', description: 'Image height in pixels.' },
|
|
945
|
+
bytes: { type: 'integer', description: 'PNG size in bytes.' },
|
|
946
|
+
printed: bool('True when PrintWindow rendered it; false means screen fallback.'),
|
|
947
|
+
bounds: rect('Window rect on the virtual screen.'),
|
|
948
|
+
}),
|
|
949
|
+
render: (_args, v) => text(
|
|
950
|
+
`Window captured (${v.printed ? 'PrintWindow' : 'screen fallback'}): ${v.path} — ${v.width}x${v.height} at (${v.bounds.x},${v.bounds.y}).`),
|
|
951
|
+
},
|
|
952
|
+
readOnly: true,
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
name: 'cua_drag',
|
|
956
|
+
description:
|
|
957
|
+
'Drag with the real left button held down: press at fromX/fromY, travel, release at toX/toY. Use it for sliders, selections, window moves and drag-and-drop.',
|
|
958
|
+
parameters: obj({
|
|
959
|
+
fromX: num('Press X in virtual-screen pixels.'),
|
|
960
|
+
fromY: num('Press Y in virtual-screen pixels.'),
|
|
961
|
+
toX: num('Release X in virtual-screen pixels.'),
|
|
962
|
+
toY: num('Release Y in virtual-screen pixels.'),
|
|
963
|
+
}, ['fromX', 'fromY', 'toX', 'toY']),
|
|
964
|
+
run: async (A, args) => {
|
|
965
|
+
const moved = await dragMouse(A, args.fromX, args.fromY, args.toX, args.toY, CFG.humanize);
|
|
966
|
+
return { ok: true, ...moved };
|
|
967
|
+
},
|
|
968
|
+
out: {
|
|
969
|
+
schema: obj({
|
|
970
|
+
ok: bool('Always true.'),
|
|
971
|
+
from: point('Actual press point after clamping.'),
|
|
972
|
+
to: point('Actual release point after clamping.'),
|
|
973
|
+
}),
|
|
974
|
+
render: (_args, v) => text(`Dragged (${v.from.x}, ${v.from.y}) -> (${v.to.x}, ${v.to.y}).`),
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
name: 'cua_wait',
|
|
979
|
+
description:
|
|
980
|
+
'Wait for the desktop to settle: sleep for a number of milliseconds and/or poll until a window whose title contains the given text exists.',
|
|
981
|
+
parameters: obj({
|
|
982
|
+
ms: num('Milliseconds to sleep first (default 0).'),
|
|
983
|
+
title: str('Optional window-title substring to wait for, case-insensitive.'),
|
|
984
|
+
timeoutMs: num('Upper bound for the wait; capped by the plugin config.'),
|
|
985
|
+
}),
|
|
986
|
+
run: async (A, args) => waitFor(A, args),
|
|
987
|
+
out: {
|
|
988
|
+
schema: obj({
|
|
989
|
+
waitedMs: { type: 'integer', description: 'Actual time spent waiting.' },
|
|
990
|
+
matched: bool('True when the title appeared.'),
|
|
991
|
+
timedOut: bool('True when the title never appeared in time.'),
|
|
992
|
+
}),
|
|
993
|
+
render: (_args, v) => text(
|
|
994
|
+
v.matched ? `Window appeared after ${v.waitedMs} ms.`
|
|
995
|
+
: v.timedOut ? `Timed out after ${v.waitedMs} ms waiting for the window.`
|
|
996
|
+
: `Waited ${v.waitedMs} ms.`),
|
|
997
|
+
},
|
|
998
|
+
readOnly: true,
|
|
999
|
+
timeoutMs: 15_000,
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
name: 'cua_app_list',
|
|
1003
|
+
description: 'List visible top-level windows with their process executable name, so you can tell apps apart and pick a launch target.',
|
|
1004
|
+
parameters: obj({}),
|
|
1005
|
+
run: (A) => ({ apps: listWindows(A).map((w) => ({ ...w, process: exeName(A, w.pid) })) }),
|
|
1006
|
+
out: {
|
|
1007
|
+
schema: obj({
|
|
1008
|
+
apps: {
|
|
1009
|
+
type: 'array',
|
|
1010
|
+
items: {
|
|
1011
|
+
type: 'object',
|
|
1012
|
+
description: 'One window with its process executable.',
|
|
1013
|
+
properties: {
|
|
1014
|
+
handle: { type: 'integer', description: 'Window handle.' },
|
|
1015
|
+
title: str('Window title.'),
|
|
1016
|
+
pid: { type: 'integer', description: 'Owner process id.' },
|
|
1017
|
+
process: str('Process executable name, e.g. notepad.exe.'),
|
|
1018
|
+
active: bool('Is the foreground window.'),
|
|
1019
|
+
minimized: bool('Is minimized.'),
|
|
1020
|
+
bounds: rect('Window rect on the virtual screen.'),
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
description: 'Windows with process names.',
|
|
1024
|
+
},
|
|
1025
|
+
}),
|
|
1026
|
+
render: (_args, v) => text(
|
|
1027
|
+
`${v.apps.length} app window(s):\n` + v.apps.map((a) =>
|
|
1028
|
+
`#${a.handle} ${a.process || '?'} (pid ${a.pid})${a.active ? ' [active]' : ''}${a.minimized ? ' [minimized]' : ''} "${a.title}"`).join('\n')),
|
|
1029
|
+
},
|
|
1030
|
+
readOnly: true,
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
name: 'cua_app_launch',
|
|
1034
|
+
description:
|
|
1035
|
+
'Launch an application by executable name (resolved on PATH) or absolute path. Spawns directly without a shell, so no quoting games.',
|
|
1036
|
+
parameters: obj({
|
|
1037
|
+
exe: str('Executable name such as "notepad" / "calc", or an absolute .exe path.'),
|
|
1038
|
+
args: { type: 'array', items: { type: 'string' }, description: 'Command-line arguments.' },
|
|
1039
|
+
}, ['exe']),
|
|
1040
|
+
run: (A, args) => {
|
|
1041
|
+
if (CFG.allowLaunch !== true) throw new Error('cua_app_launch is disabled by config (allowLaunch: false)');
|
|
1042
|
+
const exe = resolveExe(args.exe);
|
|
1043
|
+
const argv = Array.isArray(args.args) ? args.args.map(String) : [];
|
|
1044
|
+
const child = spawn(exe, argv, { detached: true, stdio: 'ignore', windowsHide: false });
|
|
1045
|
+
child.unref();
|
|
1046
|
+
return { ok: true, exe, pid: child.pid ?? 0 };
|
|
1047
|
+
},
|
|
1048
|
+
out: {
|
|
1049
|
+
schema: obj({
|
|
1050
|
+
ok: bool('Always true.'),
|
|
1051
|
+
exe: str('Resolved executable path.'),
|
|
1052
|
+
pid: { type: 'integer', description: 'Process id of the launched app.' },
|
|
1053
|
+
}),
|
|
1054
|
+
render: (_args, v) => text(`Launched ${v.exe} (pid ${v.pid}).`),
|
|
1055
|
+
},
|
|
665
1056
|
},
|
|
666
1057
|
];
|
|
667
1058
|
|
|
@@ -675,9 +1066,12 @@ export const inject = ['tools'];
|
|
|
675
1066
|
* Config knobs (all optional, from this bundle's cordis.patch.yml):
|
|
676
1067
|
* - `humanize` (default true): move the pointer along a smoothstep path and
|
|
677
1068
|
* type/click at a human cadence instead of teleporting instantly.
|
|
678
|
-
* - `keepScreenshots` (default 30): newest
|
|
1069
|
+
* - `keepScreenshots` (default 30): newest captures kept per directory.
|
|
679
1070
|
* - `moveSteps` (default 16), `keyDelayMs` ([12, 34]), `clickSettleMs` ([60, 140]):
|
|
680
1071
|
* fine-tuning for the humanized cadence.
|
|
1072
|
+
* - `zoomFactor` (default 2), `zoomMaxPixels` (default 24e6): `cua_zoom` limits.
|
|
1073
|
+
* - `maxWaitMs` (default 10000): ceiling for `cua_wait`.
|
|
1074
|
+
* - `allowLaunch` (default true): set false to disable `cua_app_launch`.
|
|
681
1075
|
*/
|
|
682
1076
|
export function apply(ctx, config = {}) {
|
|
683
1077
|
for (const key of Object.keys(DEFAULTS)) {
|
|
@@ -689,6 +1083,9 @@ export function apply(ctx, config = {}) {
|
|
|
689
1083
|
description: decl.description,
|
|
690
1084
|
parameters: decl.parameters,
|
|
691
1085
|
output: decl.out,
|
|
1086
|
+
presentCall: (args) => ({ card: 'generic', title: decl.name, kind: 'other', rawInput: args ?? {} }),
|
|
1087
|
+
...decl.readOnly === true ? { isConcurrencySafe: () => true } : {},
|
|
1088
|
+
...decl.timeoutMs !== undefined ? { timeoutMs: decl.timeoutMs } : {},
|
|
692
1089
|
async execute(args = {}) {
|
|
693
1090
|
return decl.run(loadNative(), args ?? {});
|
|
694
1091
|
},
|
package/package.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-win-cua",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Windows computer-use for DSH: real mouse and keyboard input through Win32 SendInput, screen capture through GDI BitBlt, window control. No external driver.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"author": "WJPGKZ",
|
|
9
|
+
"homepage": "https://github.com/WJPGKZ/dsh-win-cua#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/WJPGKZ/dsh-win-cua.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/WJPGKZ/dsh-win-cua/issues"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
8
20
|
"keywords": [
|
|
9
21
|
"dsh",
|
|
10
22
|
"dsh-plugin",
|
|
@@ -32,4 +44,4 @@
|
|
|
32
44
|
"node": ">=20"
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
|
-
}
|
|
47
|
+
}
|