usecomputer 0.0.2 → 0.0.4

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +338 -0
  3. package/build.zig +1 -0
  4. package/dist/bridge-contract.test.js +124 -63
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +241 -46
  7. package/dist/cli-parsing.test.js +34 -11
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +328 -22
  10. package/dist/coord-map.d.ts +14 -0
  11. package/dist/coord-map.d.ts.map +1 -0
  12. package/dist/coord-map.js +75 -0
  13. package/dist/coord-map.test.d.ts +2 -0
  14. package/dist/coord-map.test.d.ts.map +1 -0
  15. package/dist/coord-map.test.js +157 -0
  16. package/dist/darwin-arm64/usecomputer.node +0 -0
  17. package/dist/darwin-x64/usecomputer.node +0 -0
  18. package/dist/debug-point-image.d.ts +8 -0
  19. package/dist/debug-point-image.d.ts.map +1 -0
  20. package/dist/debug-point-image.js +43 -0
  21. package/dist/debug-point-image.test.d.ts +2 -0
  22. package/dist/debug-point-image.test.d.ts.map +1 -0
  23. package/dist/debug-point-image.test.js +44 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/lib.d.ts +26 -0
  28. package/dist/lib.d.ts.map +1 -0
  29. package/dist/lib.js +88 -0
  30. package/dist/native-click-smoke.test.js +69 -29
  31. package/dist/native-lib.d.ts +59 -1
  32. package/dist/native-lib.d.ts.map +1 -1
  33. package/dist/terminal-table.d.ts +10 -0
  34. package/dist/terminal-table.d.ts.map +1 -0
  35. package/dist/terminal-table.js +55 -0
  36. package/dist/terminal-table.test.d.ts +2 -0
  37. package/dist/terminal-table.test.d.ts.map +1 -0
  38. package/dist/terminal-table.test.js +41 -0
  39. package/dist/types.d.ts +45 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +16 -4
  42. package/src/bridge-contract.test.ts +140 -69
  43. package/src/bridge.ts +293 -53
  44. package/src/cli-parsing.test.ts +61 -0
  45. package/src/cli.ts +401 -25
  46. package/src/coord-map.test.ts +178 -0
  47. package/src/coord-map.ts +105 -0
  48. package/src/debug-point-image.test.ts +50 -0
  49. package/src/debug-point-image.ts +69 -0
  50. package/src/index.ts +3 -1
  51. package/src/lib.ts +125 -0
  52. package/src/native-click-smoke.test.ts +81 -63
  53. package/src/native-lib.ts +39 -1
  54. package/src/terminal-table.test.ts +44 -0
  55. package/src/terminal-table.ts +88 -0
  56. package/src/types.ts +50 -0
  57. package/zig/src/lib.zig +1280 -163
  58. package/zig/src/scroll.zig +213 -0
  59. package/zig/src/window.zig +123 -0
package/src/bridge.ts CHANGED
@@ -1,37 +1,114 @@
1
- // Native bridge that maps typed TS calls to the Zig N-API command dispatcher.
1
+ // Native bridge that maps typed TS calls to direct Zig N-API methods.
2
2
 
3
3
  import { native, type NativeModule } from './native-lib.js'
4
+ import { z } from 'zod'
4
5
  import type {
5
6
  ClickInput,
7
+ DisplayInfo,
6
8
  DragInput,
9
+ NativeCommandResult,
10
+ NativeDataResult,
7
11
  Point,
8
12
  PressInput,
13
+ Region,
9
14
  ScreenshotInput,
10
15
  ScreenshotResult,
11
16
  ScrollInput,
12
17
  TypeInput,
13
18
  UseComputerBridge,
14
- DisplayInfo,
19
+ WindowInfo,
15
20
  } from './types.js'
16
21
 
22
+ const displayInfoSchema = z.object({
23
+ id: z.number(),
24
+ index: z.number(),
25
+ name: z.string(),
26
+ x: z.number(),
27
+ y: z.number(),
28
+ width: z.number(),
29
+ height: z.number(),
30
+ scale: z.number(),
31
+ isPrimary: z.boolean(),
32
+ })
33
+
34
+ const displayListSchema = z.array(displayInfoSchema)
35
+
36
+ const windowInfoSchema = z.object({
37
+ id: z.number(),
38
+ ownerPid: z.number(),
39
+ ownerName: z.string(),
40
+ title: z.string(),
41
+ x: z.number(),
42
+ y: z.number(),
43
+ width: z.number(),
44
+ height: z.number(),
45
+ desktopIndex: z.number(),
46
+ })
47
+
48
+ const windowListSchema = z.array(windowInfoSchema)
49
+
17
50
  const unavailableError =
18
51
  'Native backend is unavailable. Build it with `pnpm build:native` or `zig build` in usecomputer/.'
19
52
 
20
- function execute<T>({
21
- nativeModule,
22
- command,
23
- payload,
53
+ class NativeBridgeError extends Error {
54
+ readonly code?: string
55
+ readonly command?: string
56
+
57
+ constructor({
58
+ message,
59
+ code,
60
+ command,
61
+ }: {
62
+ message: string
63
+ code?: string
64
+ command?: string
65
+ }) {
66
+ super(message)
67
+ this.name = 'NativeBridgeError'
68
+ this.code = code
69
+ this.command = command
70
+ }
71
+ }
72
+
73
+ function unwrapCommand({
74
+ result,
75
+ fallbackCommand,
76
+ }: {
77
+ result: NativeCommandResult
78
+ fallbackCommand: string
79
+ }): Error | null {
80
+ if (result.ok) {
81
+ return null
82
+ }
83
+ const message = result.error?.message || `Native command failed: ${fallbackCommand}`
84
+ return new NativeBridgeError({
85
+ message,
86
+ code: result.error?.code,
87
+ command: result.error?.command || fallbackCommand,
88
+ })
89
+ }
90
+
91
+ function unwrapData<T>({
92
+ result,
93
+ fallbackCommand,
24
94
  }: {
25
- nativeModule: NativeModule
26
- command: string
27
- payload: unknown
95
+ result: NativeDataResult<T>
96
+ fallbackCommand: string
28
97
  }): Error | T {
29
- const response = nativeModule.execute(command, JSON.stringify(payload))
30
- const parsed = JSON.parse(response) as { ok: boolean; data?: T; error?: string }
31
- if (!parsed.ok) {
32
- return new Error(parsed.error || `Native command failed: ${command}`)
98
+ if (result.ok) {
99
+ if (result.data === undefined) {
100
+ return new NativeBridgeError({
101
+ message: `Native command returned no data: ${fallbackCommand}`,
102
+ command: fallbackCommand,
103
+ })
104
+ }
105
+ return result.data
33
106
  }
34
- return parsed.data as T
107
+ return new NativeBridgeError({
108
+ message: result.error?.message || `Native command failed: ${fallbackCommand}`,
109
+ code: result.error?.code,
110
+ command: result.error?.command || fallbackCommand,
111
+ })
35
112
  }
36
113
 
37
114
  function unavailableBridge(): UseComputerBridge {
@@ -52,6 +129,7 @@ function unavailableBridge(): UseComputerBridge {
52
129
  mouseUp: fail,
53
130
  mousePosition: fail,
54
131
  displayList: fail,
132
+ windowList: fail,
55
133
  clipboardGet: fail,
56
134
  clipboardSet: fail,
57
135
  }
@@ -64,91 +142,253 @@ export function createBridgeFromNative({ nativeModule }: { nativeModule: NativeM
64
142
 
65
143
  return {
66
144
  async screenshot(input: ScreenshotInput): Promise<ScreenshotResult> {
67
- const result = execute<ScreenshotResult>({ nativeModule, command: 'screenshot', payload: input })
145
+ const nativeInput: {
146
+ path: string | null
147
+ display: number | null
148
+ window: number | null
149
+ region: Region | null
150
+ annotate: boolean | null
151
+ } = {
152
+ path: input.path ?? null,
153
+ display: input.display ?? null,
154
+ window: input.window ?? null,
155
+ region: input.region ?? null,
156
+ annotate: input.annotate ?? null,
157
+ }
158
+
159
+ const result = unwrapData({
160
+ result: nativeModule.screenshot(nativeInput),
161
+ fallbackCommand: 'screenshot',
162
+ })
68
163
  if (result instanceof Error) {
69
164
  throw result
70
165
  }
71
- return result
166
+ const coordMap = [
167
+ result.captureX,
168
+ result.captureY,
169
+ result.captureWidth,
170
+ result.captureHeight,
171
+ result.imageWidth,
172
+ result.imageHeight,
173
+ ].join(',')
174
+ const hint = [
175
+ 'ALWAYS pass this exact coord map to click, hover, drag, and mouse move when using coordinates from this screenshot:',
176
+ `--coord-map "${coordMap}"`,
177
+ '',
178
+ 'Example:',
179
+ `usecomputer click -x 400 -y 220 --coord-map "${coordMap}"`,
180
+ ].join('\n')
181
+
182
+ return {
183
+ path: result.path,
184
+ desktopIndex: result.desktopIndex,
185
+ captureX: result.captureX,
186
+ captureY: result.captureY,
187
+ captureWidth: result.captureWidth,
188
+ captureHeight: result.captureHeight,
189
+ imageWidth: result.imageWidth,
190
+ imageHeight: result.imageHeight,
191
+ coordMap,
192
+ hint,
193
+ }
72
194
  },
73
195
  async click(input: ClickInput): Promise<void> {
74
- const result = execute<null>({ nativeModule, command: 'click', payload: input })
75
- if (result instanceof Error) {
76
- throw result
196
+ const nativeInput: { point: Point; button: 'left' | 'right' | 'middle' | null; count: number | null } = {
197
+ point: input.point,
198
+ button: input.button ?? null,
199
+ count: input.count ?? null,
200
+ }
201
+
202
+ const result = nativeModule.click(nativeInput)
203
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'click' })
204
+ if (maybeError instanceof Error) {
205
+ throw maybeError
77
206
  }
78
207
  },
79
208
  async typeText(input: TypeInput): Promise<void> {
80
- const result = execute<null>({ nativeModule, command: 'type-text', payload: input })
81
- if (result instanceof Error) {
82
- throw result
209
+ const nativeInput: { text: string; delayMs: number | null } = {
210
+ text: input.text,
211
+ delayMs: input.delayMs ?? null,
212
+ }
213
+
214
+ const result = nativeModule.typeText(nativeInput)
215
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'typeText' })
216
+ if (maybeError instanceof Error) {
217
+ throw maybeError
83
218
  }
84
219
  },
85
220
  async press(input: PressInput): Promise<void> {
86
- const result = execute<null>({ nativeModule, command: 'press', payload: input })
87
- if (result instanceof Error) {
88
- throw result
221
+ const nativeInput: { key: string; count: number | null; delayMs: number | null } = {
222
+ key: input.key,
223
+ count: input.count ?? null,
224
+ delayMs: input.delayMs ?? null,
225
+ }
226
+
227
+ const result = nativeModule.press(nativeInput)
228
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'press' })
229
+ if (maybeError instanceof Error) {
230
+ throw maybeError
89
231
  }
90
232
  },
91
233
  async scroll(input: ScrollInput): Promise<void> {
92
- const result = execute<null>({ nativeModule, command: 'scroll', payload: input })
93
- if (result instanceof Error) {
94
- throw result
234
+ const nativeInput: { direction: string; amount: number; at: Point | null } = {
235
+ direction: input.direction,
236
+ amount: input.amount,
237
+ at: input.at ?? null,
238
+ }
239
+
240
+ const result = nativeModule.scroll(nativeInput)
241
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'scroll' })
242
+ if (maybeError instanceof Error) {
243
+ throw maybeError
95
244
  }
96
245
  },
97
246
  async drag(input: DragInput): Promise<void> {
98
- const result = execute<null>({ nativeModule, command: 'drag', payload: input })
99
- if (result instanceof Error) {
100
- throw result
247
+ const nativeInput: {
248
+ from: Point
249
+ to: Point
250
+ durationMs: number | null
251
+ button: 'left' | 'right' | 'middle' | null
252
+ } = {
253
+ from: input.from,
254
+ to: input.to,
255
+ durationMs: input.durationMs ?? null,
256
+ button: input.button ?? null,
257
+ }
258
+
259
+ const result = nativeModule.drag(nativeInput)
260
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'drag' })
261
+ if (maybeError instanceof Error) {
262
+ throw maybeError
101
263
  }
102
264
  },
103
265
  async hover(input: Point): Promise<void> {
104
- const result = execute<null>({ nativeModule, command: 'hover', payload: input })
105
- if (result instanceof Error) {
106
- throw result
266
+ const result = nativeModule.hover(input)
267
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'hover' })
268
+ if (maybeError instanceof Error) {
269
+ throw maybeError
107
270
  }
108
271
  },
109
272
  async mouseMove(input: Point): Promise<void> {
110
- const result = execute<null>({ nativeModule, command: 'mouse-move', payload: input })
111
- if (result instanceof Error) {
112
- throw result
273
+ const result = nativeModule.mouseMove(input)
274
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'mouseMove' })
275
+ if (maybeError instanceof Error) {
276
+ throw maybeError
113
277
  }
114
278
  },
115
279
  async mouseDown(input: { button: 'left' | 'right' | 'middle' }): Promise<void> {
116
- const result = execute<null>({ nativeModule, command: 'mouse-down', payload: input })
117
- if (result instanceof Error) {
118
- throw result
280
+ const result = nativeModule.mouseDown({ button: input.button ?? null })
281
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'mouseDown' })
282
+ if (maybeError instanceof Error) {
283
+ throw maybeError
119
284
  }
120
285
  },
121
286
  async mouseUp(input: { button: 'left' | 'right' | 'middle' }): Promise<void> {
122
- const result = execute<null>({ nativeModule, command: 'mouse-up', payload: input })
123
- if (result instanceof Error) {
124
- throw result
287
+ const result = nativeModule.mouseUp({ button: input.button ?? null })
288
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'mouseUp' })
289
+ if (maybeError instanceof Error) {
290
+ throw maybeError
125
291
  }
126
292
  },
127
293
  async mousePosition(): Promise<Point> {
128
- const result = execute<Point>({ nativeModule, command: 'mouse-position', payload: {} })
294
+ const result = unwrapData({
295
+ result: nativeModule.mousePosition(),
296
+ fallbackCommand: 'mousePosition',
297
+ })
129
298
  if (result instanceof Error) {
130
299
  throw result
131
300
  }
132
301
  return result
133
302
  },
134
303
  async displayList(): Promise<DisplayInfo[]> {
135
- const result = execute<DisplayInfo[]>({ nativeModule, command: 'display-list', payload: {} })
136
- if (result instanceof Error) {
137
- throw result
304
+ const payload = unwrapData({
305
+ result: nativeModule.displayList(),
306
+ fallbackCommand: 'displayList',
307
+ })
308
+ if (payload instanceof Error) {
309
+ throw payload
138
310
  }
139
- return result
311
+
312
+ let parsedJson: unknown
313
+ try {
314
+ parsedJson = JSON.parse(payload)
315
+ } catch (e) {
316
+ throw new NativeBridgeError({
317
+ message: 'Native displayList returned invalid JSON',
318
+ command: 'displayList',
319
+ code: 'INVALID_NATIVE_JSON',
320
+ })
321
+ }
322
+
323
+ const parsed = displayListSchema.safeParse(parsedJson)
324
+ if (!parsed.success) {
325
+ throw new NativeBridgeError({
326
+ message: 'Native displayList returned invalid payload shape',
327
+ command: 'displayList',
328
+ code: 'INVALID_NATIVE_PAYLOAD',
329
+ })
330
+ }
331
+
332
+ return parsed.data.map((display) => {
333
+ return {
334
+ id: display.id,
335
+ index: display.index,
336
+ name: display.name,
337
+ x: display.x,
338
+ y: display.y,
339
+ width: display.width,
340
+ height: display.height,
341
+ scale: display.scale,
342
+ isPrimary: display.isPrimary,
343
+ }
344
+ })
345
+ },
346
+ async windowList(): Promise<WindowInfo[]> {
347
+ const payload = unwrapData({
348
+ result: nativeModule.windowList(),
349
+ fallbackCommand: 'windowList',
350
+ })
351
+ if (payload instanceof Error) {
352
+ throw payload
353
+ }
354
+
355
+ let parsedJson: unknown
356
+ try {
357
+ parsedJson = JSON.parse(payload)
358
+ } catch {
359
+ throw new NativeBridgeError({
360
+ message: 'Native windowList returned invalid JSON',
361
+ command: 'windowList',
362
+ code: 'INVALID_NATIVE_JSON',
363
+ })
364
+ }
365
+
366
+ const parsed = windowListSchema.safeParse(parsedJson)
367
+ if (!parsed.success) {
368
+ throw new NativeBridgeError({
369
+ message: 'Native windowList returned invalid payload shape',
370
+ command: 'windowList',
371
+ code: 'INVALID_NATIVE_PAYLOAD',
372
+ })
373
+ }
374
+
375
+ return parsed.data
140
376
  },
141
377
  async clipboardGet(): Promise<string> {
142
- const result = execute<{ text: string }>({ nativeModule, command: 'clipboard-get', payload: {} })
378
+ const result = unwrapData({
379
+ result: nativeModule.clipboardGet(),
380
+ fallbackCommand: 'clipboardGet',
381
+ })
143
382
  if (result instanceof Error) {
144
383
  throw result
145
384
  }
146
- return result.text
385
+ return result
147
386
  },
148
387
  async clipboardSet(input: { text: string }): Promise<void> {
149
- const result = execute<null>({ nativeModule, command: 'clipboard-set', payload: input })
150
- if (result instanceof Error) {
151
- throw result
388
+ const result = nativeModule.clipboardSet(input)
389
+ const maybeError = unwrapCommand({ result, fallbackCommand: 'clipboardSet' })
390
+ if (maybeError instanceof Error) {
391
+ throw maybeError
152
392
  }
153
393
  },
154
394
  }
@@ -0,0 +1,61 @@
1
+ // Parser tests for goke CLI options and flags.
2
+
3
+ import { describe, expect, test } from 'vitest'
4
+ import { createCli } from './cli.js'
5
+
6
+ describe('usecomputer cli parsing', () => {
7
+ test('parses click options with typed defaults', () => {
8
+ const cli = createCli()
9
+ const parsed = cli.parse(['node', 'usecomputer', 'click', '100,200', '--count', '2'], { run: false })
10
+ expect(parsed.args[0]).toBe('100,200')
11
+ expect(parsed.options.count).toBe(2)
12
+ expect(parsed.options.button).toBe('left')
13
+ })
14
+
15
+ test('parses screenshot options', () => {
16
+ const cli = createCli()
17
+ const parsed = cli.parse(['node', 'usecomputer', 'screenshot', './shot.png', '--display', '2', '--region', '0,0,120,80'], {
18
+ run: false,
19
+ })
20
+ expect(parsed.args[0]).toBe('./shot.png')
21
+ expect(parsed.options.display).toBe(2)
22
+ expect(parsed.options.region).toBe('0,0,120,80')
23
+ })
24
+
25
+ test('parses coord-map option for click and mouse move', () => {
26
+ const clickCli = createCli()
27
+ const clickParsed = clickCli.parse(['node', 'usecomputer', 'click', '-x', '100', '-y', '200', '--coord-map', '0,0,1600,900,1568,882'], {
28
+ run: false,
29
+ })
30
+
31
+ const moveCli = createCli()
32
+ const moveParsed = moveCli.parse(['node', 'usecomputer', 'mouse', 'move', '-x', '100', '-y', '200', '--coord-map', '0,0,1600,900,1568,882'], {
33
+ run: false,
34
+ })
35
+
36
+ expect(clickParsed.options.coordMap).toBe('0,0,1600,900,1568,882')
37
+ expect(moveParsed.options.coordMap).toBe('0,0,1600,900,1568,882')
38
+ })
39
+
40
+ test('parses debug-point options', () => {
41
+ const cli = createCli()
42
+ const parsed = cli.parse([
43
+ 'node',
44
+ 'usecomputer',
45
+ 'debug-point',
46
+ '-x',
47
+ '210',
48
+ '-y',
49
+ '560',
50
+ '--coord-map',
51
+ '0,0,1720,1440,1568,1313',
52
+ '--output',
53
+ './tmp/debug-point.png',
54
+ ], { run: false })
55
+
56
+ expect(parsed.options.coordMap).toBe('0,0,1720,1440,1568,1313')
57
+ expect(parsed.options.output).toBe('./tmp/debug-point.png')
58
+ expect(parsed.options.x).toBe(210)
59
+ expect(parsed.options.y).toBe(560)
60
+ })
61
+ })