@moxxy/plugin-computer-control 0.41.2 → 0.42.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.
Files changed (269) hide show
  1. package/bin/win32-x64/moxxy-computer.exe +0 -0
  2. package/bin/win32-x64/moxxy-computer.exe.json +1 -1
  3. package/dist/backend/access.d.ts +129 -0
  4. package/dist/backend/access.d.ts.map +1 -0
  5. package/dist/backend/access.js +158 -0
  6. package/dist/backend/access.js.map +1 -0
  7. package/dist/backend/app-hints.d.ts +14 -0
  8. package/dist/backend/app-hints.d.ts.map +1 -0
  9. package/dist/backend/app-hints.js +30 -0
  10. package/dist/backend/app-hints.js.map +1 -0
  11. package/dist/backend/backend.d.ts +70 -0
  12. package/dist/backend/backend.d.ts.map +1 -0
  13. package/dist/backend/backend.js +466 -0
  14. package/dist/backend/backend.js.map +1 -0
  15. package/dist/backend/rpc.d.ts +1438 -0
  16. package/dist/backend/rpc.d.ts.map +1 -0
  17. package/dist/backend/rpc.js +75 -0
  18. package/dist/backend/rpc.js.map +1 -0
  19. package/dist/backend/turn-controls.d.ts +20 -0
  20. package/dist/backend/turn-controls.d.ts.map +1 -0
  21. package/dist/backend/turn-controls.js +115 -0
  22. package/dist/backend/turn-controls.js.map +1 -0
  23. package/dist/contract/guidance.d.ts +12 -0
  24. package/dist/contract/guidance.d.ts.map +1 -0
  25. package/dist/contract/guidance.js +42 -0
  26. package/dist/contract/guidance.js.map +1 -0
  27. package/dist/contract/image.d.ts +29 -0
  28. package/dist/contract/image.d.ts.map +1 -0
  29. package/dist/contract/image.js +46 -0
  30. package/dist/contract/image.js.map +1 -0
  31. package/dist/contract/keys.d.ts +15 -0
  32. package/dist/contract/keys.d.ts.map +1 -0
  33. package/dist/contract/keys.js +118 -0
  34. package/dist/contract/keys.js.map +1 -0
  35. package/dist/contract/outcome.d.ts +61 -0
  36. package/dist/contract/outcome.d.ts.map +1 -0
  37. package/dist/contract/outcome.js +63 -0
  38. package/dist/contract/outcome.js.map +1 -0
  39. package/dist/contract/progress.d.ts +27 -0
  40. package/dist/contract/progress.d.ts.map +1 -0
  41. package/dist/contract/progress.js +36 -0
  42. package/dist/contract/progress.js.map +1 -0
  43. package/dist/contract/tools.d.ts +338 -0
  44. package/dist/contract/tools.d.ts.map +1 -0
  45. package/dist/contract/tools.js +195 -0
  46. package/dist/contract/tools.js.map +1 -0
  47. package/dist/contract/untrusted.d.ts +3 -0
  48. package/dist/contract/untrusted.d.ts.map +1 -0
  49. package/dist/contract/untrusted.js +8 -0
  50. package/dist/contract/untrusted.js.map +1 -0
  51. package/dist/helper/artifact.d.ts +41 -0
  52. package/dist/helper/artifact.d.ts.map +1 -0
  53. package/dist/helper/artifact.js +189 -0
  54. package/dist/helper/artifact.js.map +1 -0
  55. package/dist/helper/protocol.d.ts +80 -0
  56. package/dist/helper/protocol.d.ts.map +1 -0
  57. package/dist/helper/protocol.js +49 -0
  58. package/dist/helper/protocol.js.map +1 -0
  59. package/dist/helper/transport.d.ts +43 -0
  60. package/dist/helper/transport.d.ts.map +1 -0
  61. package/dist/{windows → helper}/transport.js +45 -19
  62. package/dist/helper/transport.js.map +1 -0
  63. package/dist/index.d.ts +9 -22
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +35 -42
  66. package/dist/index.js.map +1 -1
  67. package/dist/jev/ladder.d.ts +32 -0
  68. package/dist/jev/ladder.d.ts.map +1 -0
  69. package/dist/jev/ladder.js +66 -0
  70. package/dist/jev/ladder.js.map +1 -0
  71. package/dist/jev/memory.d.ts +11 -0
  72. package/dist/jev/memory.d.ts.map +1 -0
  73. package/dist/jev/memory.js +16 -0
  74. package/dist/jev/memory.js.map +1 -0
  75. package/dist/jev/run.d.ts +93 -0
  76. package/dist/jev/run.d.ts.map +1 -0
  77. package/dist/jev/run.js +403 -0
  78. package/dist/jev/run.js.map +1 -0
  79. package/dist/jev/train.d.ts +97 -0
  80. package/dist/jev/train.d.ts.map +1 -0
  81. package/dist/jev/train.js +28 -0
  82. package/dist/jev/train.js.map +1 -0
  83. package/dist/linux/profile.d.ts +6 -0
  84. package/dist/linux/profile.d.ts.map +1 -0
  85. package/dist/linux/profile.js +21 -0
  86. package/dist/linux/profile.js.map +1 -0
  87. package/dist/macos/profile.d.ts +5 -0
  88. package/dist/macos/profile.d.ts.map +1 -0
  89. package/dist/macos/profile.js +17 -0
  90. package/dist/macos/profile.js.map +1 -0
  91. package/dist/preview/controller.d.ts +127 -0
  92. package/dist/preview/controller.d.ts.map +1 -0
  93. package/dist/preview/controller.js +203 -0
  94. package/dist/preview/controller.js.map +1 -0
  95. package/dist/preview/surface.d.ts +9 -0
  96. package/dist/preview/surface.d.ts.map +1 -0
  97. package/dist/preview/surface.js +50 -0
  98. package/dist/preview/surface.js.map +1 -0
  99. package/dist/windows/maintenance.d.ts +1 -1
  100. package/dist/windows/maintenance.d.ts.map +1 -1
  101. package/dist/windows/maintenance.js +6 -5
  102. package/dist/windows/maintenance.js.map +1 -1
  103. package/dist/windows/profile.d.ts +5 -0
  104. package/dist/windows/profile.d.ts.map +1 -0
  105. package/dist/windows/profile.js +17 -0
  106. package/dist/windows/profile.js.map +1 -0
  107. package/learned/README.md +26 -0
  108. package/learned/cases/com.apple.calculator.json +395 -0
  109. package/learned/cases/com.apple.finder.json +115 -0
  110. package/learned/cases/com.apple.safari.json +112 -0
  111. package/learned/cases/com.apple.systempreferences.json +255 -0
  112. package/learned/com.apple.calculator-eace95fc.json +334 -0
  113. package/learned/com.apple.finder-27cf6ce8.json +260 -0
  114. package/learned/com.apple.safari-7cd9df4f.json +143 -0
  115. package/learned/com.apple.systempreferences-02cf0b8b.json +535 -0
  116. package/package.json +13 -7
  117. package/scripts/promote-learned.mjs +8 -0
  118. package/scripts/summarize-trial.mjs +68 -0
  119. package/scripts/train-learned.mjs +64 -0
  120. package/skills/computer-apps/blender.md +29 -0
  121. package/skills/computer-apps/browsers.md +50 -0
  122. package/skills/computer-apps/design-tools.md +37 -0
  123. package/skills/computer-apps/finder.md +23 -0
  124. package/skills/computer-apps/office.md +46 -0
  125. package/skills/computer-apps/video-editors.md +44 -0
  126. package/skills/computer-control.md +154 -191
  127. package/src/backend/access.test.ts +158 -0
  128. package/src/backend/access.ts +168 -0
  129. package/src/backend/app-hints.test.ts +59 -0
  130. package/src/backend/app-hints.ts +39 -0
  131. package/src/backend/backend.test.ts +725 -0
  132. package/src/backend/backend.ts +482 -0
  133. package/src/backend/contract-helper.fixture.mjs +138 -0
  134. package/src/backend/helper.fixture.ts +38 -0
  135. package/src/backend/rpc.ts +90 -0
  136. package/src/backend/turn-controls.test.ts +178 -0
  137. package/src/backend/turn-controls.ts +120 -0
  138. package/src/contract/guidance.test.ts +75 -0
  139. package/src/contract/guidance.ts +48 -0
  140. package/src/contract/image.test.ts +74 -0
  141. package/src/contract/image.ts +54 -0
  142. package/src/contract/keys.test.ts +103 -0
  143. package/src/contract/keys.ts +117 -0
  144. package/src/contract/outcome.test.ts +71 -0
  145. package/src/contract/outcome.ts +72 -0
  146. package/src/contract/progress.test.ts +56 -0
  147. package/src/contract/progress.ts +43 -0
  148. package/src/contract/tools.test.ts +210 -0
  149. package/src/contract/tools.ts +212 -0
  150. package/src/contract/untrusted.test.ts +20 -0
  151. package/src/contract/untrusted.ts +8 -0
  152. package/src/helper/artifact.test.ts +131 -0
  153. package/src/helper/artifact.ts +182 -0
  154. package/src/helper/protocol.test.ts +29 -0
  155. package/src/helper/protocol.ts +50 -0
  156. package/src/{windows → helper}/transport.test.ts +76 -19
  157. package/src/{windows → helper}/transport.ts +54 -16
  158. package/src/index.test.ts +112 -0
  159. package/src/index.ts +36 -53
  160. package/src/jev/ladder.test.ts +113 -0
  161. package/src/jev/ladder.ts +88 -0
  162. package/src/jev/memory.ts +20 -0
  163. package/src/jev/run.test.ts +700 -0
  164. package/src/jev/run.ts +443 -0
  165. package/src/jev/train.test.ts +40 -0
  166. package/src/jev/train.ts +41 -0
  167. package/src/linux/helper.test.ts +481 -0
  168. package/src/linux/profile.ts +25 -0
  169. package/src/macos/helper.test.ts +988 -0
  170. package/src/macos/profile.ts +19 -0
  171. package/src/preview/controller.test.ts +308 -0
  172. package/src/preview/controller.ts +262 -0
  173. package/src/preview/surface.test.ts +68 -0
  174. package/src/preview/surface.ts +45 -0
  175. package/src/skill.test.ts +34 -0
  176. package/src/windows/maintenance.ts +8 -7
  177. package/src/windows/profile.ts +19 -0
  178. package/dist/shell.d.ts +0 -56
  179. package/dist/shell.d.ts.map +0 -1
  180. package/dist/shell.js +0 -189
  181. package/dist/shell.js.map +0 -1
  182. package/dist/temporary-files.d.ts +0 -2
  183. package/dist/temporary-files.d.ts.map +0 -1
  184. package/dist/temporary-files.js +0 -10
  185. package/dist/temporary-files.js.map +0 -1
  186. package/dist/tools/applescript.d.ts +0 -2
  187. package/dist/tools/applescript.d.ts.map +0 -1
  188. package/dist/tools/applescript.js +0 -50
  189. package/dist/tools/applescript.js.map +0 -1
  190. package/dist/tools/click.d.ts +0 -2
  191. package/dist/tools/click.d.ts.map +0 -1
  192. package/dist/tools/click.js +0 -58
  193. package/dist/tools/click.js.map +0 -1
  194. package/dist/tools/clipboard.d.ts +0 -2
  195. package/dist/tools/clipboard.d.ts.map +0 -1
  196. package/dist/tools/clipboard.js +0 -71
  197. package/dist/tools/clipboard.js.map +0 -1
  198. package/dist/tools/key.d.ts +0 -11
  199. package/dist/tools/key.d.ts.map +0 -1
  200. package/dist/tools/key.js +0 -143
  201. package/dist/tools/key.js.map +0 -1
  202. package/dist/tools/open.d.ts +0 -2
  203. package/dist/tools/open.d.ts.map +0 -1
  204. package/dist/tools/open.js +0 -91
  205. package/dist/tools/open.js.map +0 -1
  206. package/dist/tools/screenshot.d.ts +0 -2
  207. package/dist/tools/screenshot.d.ts.map +0 -1
  208. package/dist/tools/screenshot.js +0 -162
  209. package/dist/tools/screenshot.js.map +0 -1
  210. package/dist/tools/type.d.ts +0 -8
  211. package/dist/tools/type.d.ts.map +0 -1
  212. package/dist/tools/type.js +0 -65
  213. package/dist/tools/type.js.map +0 -1
  214. package/dist/windows/artifact.d.ts +0 -3
  215. package/dist/windows/artifact.d.ts.map +0 -1
  216. package/dist/windows/artifact.js +0 -57
  217. package/dist/windows/artifact.js.map +0 -1
  218. package/dist/windows/backend.d.ts +0 -11
  219. package/dist/windows/backend.d.ts.map +0 -1
  220. package/dist/windows/backend.js +0 -122
  221. package/dist/windows/backend.js.map +0 -1
  222. package/dist/windows/contracts.d.ts +0 -1627
  223. package/dist/windows/contracts.d.ts.map +0 -1
  224. package/dist/windows/contracts.js +0 -137
  225. package/dist/windows/contracts.js.map +0 -1
  226. package/dist/windows/control-service.d.ts +0 -12
  227. package/dist/windows/control-service.d.ts.map +0 -1
  228. package/dist/windows/control-service.js +0 -65
  229. package/dist/windows/control-service.js.map +0 -1
  230. package/dist/windows/guidance.d.ts +0 -3
  231. package/dist/windows/guidance.d.ts.map +0 -1
  232. package/dist/windows/guidance.js +0 -20
  233. package/dist/windows/guidance.js.map +0 -1
  234. package/dist/windows/protocol.d.ts +0 -9
  235. package/dist/windows/protocol.d.ts.map +0 -1
  236. package/dist/windows/protocol.js +0 -32
  237. package/dist/windows/protocol.js.map +0 -1
  238. package/dist/windows/transport.d.ts +0 -23
  239. package/dist/windows/transport.d.ts.map +0 -1
  240. package/dist/windows/transport.js.map +0 -1
  241. package/src/shell.test.ts +0 -186
  242. package/src/shell.ts +0 -213
  243. package/src/temporary-files.test.ts +0 -18
  244. package/src/temporary-files.ts +0 -9
  245. package/src/tools/applescript-serialize.test.ts +0 -74
  246. package/src/tools/applescript.ts +0 -53
  247. package/src/tools/click.ts +0 -60
  248. package/src/tools/clipboard.ts +0 -72
  249. package/src/tools/key.ts +0 -155
  250. package/src/tools/open.ts +0 -96
  251. package/src/tools/screenshot.test.ts +0 -137
  252. package/src/tools/screenshot.ts +0 -180
  253. package/src/tools/type.ts +0 -68
  254. package/src/tools.test.ts +0 -94
  255. package/src/windows/action-contracts.test.ts +0 -13
  256. package/src/windows/artifact.test.ts +0 -16
  257. package/src/windows/artifact.ts +0 -57
  258. package/src/windows/backend.test.ts +0 -41
  259. package/src/windows/backend.ts +0 -122
  260. package/src/windows/contracts.test.ts +0 -81
  261. package/src/windows/contracts.ts +0 -143
  262. package/src/windows/control-service.test.ts +0 -58
  263. package/src/windows/control-service.ts +0 -68
  264. package/src/windows/guidance.test.ts +0 -29
  265. package/src/windows/guidance.ts +0 -21
  266. package/src/windows/model-contract.test.ts +0 -37
  267. package/src/windows/protocol.ts +0 -27
  268. package/src/windows/text-contracts.test.ts +0 -14
  269. package/src/windows/window-typing.test.ts +0 -14
@@ -0,0 +1,112 @@
1
+ import { mkdtempSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import type { ToolDef } from '@moxxy/sdk';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+ import type { PlatformProfile } from './backend/backend.js';
7
+ import { computerTools } from './contract/tools.js';
8
+ import { createComputerControlPlugin } from './index.js';
9
+ import { linuxProfile } from './linux/profile.js';
10
+ import { macosProfile } from './macos/profile.js';
11
+ import { windowsProfile } from './windows/profile.js';
12
+ import { removeDirSync } from '@moxxy/vitest-preset/fs';
13
+
14
+ const directories: string[] = [];
15
+ afterEach(() => { for (const made of directories.splice(0)) removeDirSync(made); });
16
+
17
+ /** A profile whose helper files exist (or not) in a scratch directory; the helper is never started. */
18
+ function profile(manifest?: Record<string, unknown>, base: PlatformProfile = macosProfile): PlatformProfile {
19
+ const directory = mkdtempSync(join(tmpdir(), 'moxxy-computer-index-'));
20
+ directories.push(directory);
21
+ const helperPath = join(directory, 'moxxy-computer');
22
+ if (manifest) {
23
+ writeFileSync(helperPath, 'binary');
24
+ writeFileSync(`${helperPath}.json`, JSON.stringify(manifest));
25
+ }
26
+ return { ...base, helperPath };
27
+ }
28
+ const names = (tools: ReadonlyArray<ToolDef> | undefined) => (tools ?? []).map((tool) => tool.name).sort();
29
+ const ready = { protocolVersion: macosProfile.protocolVersion, architecture: 'universal', sha256: '0'.repeat(64) };
30
+ const context = { signal: new AbortController().signal } as never;
31
+
32
+ describe('createComputerControlPlugin', () => {
33
+ it('runs macOS on the native helper with the shared tool set and human controls', () => {
34
+ const plugin = createComputerControlPlugin('darwin', 'arm64', profile(ready));
35
+ expect(names(plugin.tools)).toEqual(Object.keys(computerTools).sort());
36
+ expect(names(plugin.tools)).not.toContain('computer_applescript');
37
+ expect(plugin.hooks?.onInit).toBeTypeOf('function');
38
+ expect(plugin.hooks?.onTurnEnd).toBeTypeOf('function');
39
+ expect(plugin.hooks?.onBeforeProviderCall).toBeTypeOf('function');
40
+ expect(plugin.surfaces?.map((surface) => surface.kind)).toEqual(['computer-preview']);
41
+ });
42
+
43
+ it('offers only computer_status, with the reason, when the macOS helper is missing', async () => {
44
+ const plugin = createComputerControlPlugin('darwin', 'arm64', profile());
45
+ expect(names(plugin.tools)).toEqual(['computer_status']);
46
+ expect(plugin.hooks).toBeUndefined();
47
+ const [status] = plugin.tools ?? [];
48
+ expect(await status?.handler({}, context)).toEqual({
49
+ platform: 'darwin', architecture: 'arm64', ready: false,
50
+ limitations: [expect.stringMatching(/helper is missing/)],
51
+ });
52
+ });
53
+
54
+ it('offers only computer_status when the helper speaks another protocol', async () => {
55
+ const plugin = createComputerControlPlugin('darwin', 'arm64', profile({ ...ready, protocolVersion: macosProfile.protocolVersion - 1 }));
56
+ expect(names(plugin.tools)).toEqual(['computer_status']);
57
+ const [status] = plugin.tools ?? [];
58
+ expect(await status?.handler({}, context)).toMatchObject({ ready: false, limitations: [expect.stringMatching(/protocol/)] });
59
+ });
60
+
61
+ it('runs Windows x64 on the same tool set, human controls and live view as macOS', () => {
62
+ const plugin = createComputerControlPlugin('win32', 'x64', profile({ ...ready, architecture: 'x64' }, windowsProfile));
63
+ expect(names(plugin.tools)).toEqual(Object.keys(computerTools).sort());
64
+ expect(names(plugin.tools)).not.toContain('computer_app_catalog');
65
+ expect(plugin.hooks?.onBeforeProviderCall).toBeTypeOf('function');
66
+ expect(plugin.surfaces?.map((surface) => surface.kind)).toEqual(['computer-preview']);
67
+ });
68
+
69
+ it('offers only computer_status, with the reason, when the Windows helper is missing', async () => {
70
+ const plugin = createComputerControlPlugin('win32', 'x64', profile(undefined, windowsProfile));
71
+ expect(names(plugin.tools)).toEqual(['computer_status']);
72
+ const [status] = plugin.tools ?? [];
73
+ expect(await status?.handler({}, context)).toEqual({
74
+ platform: 'win32', architecture: 'x64', ready: false,
75
+ limitations: [expect.stringMatching(/helper is missing.*full installer/s)],
76
+ });
77
+ });
78
+
79
+ it('speaks one protocol on both platforms', () => {
80
+ expect(windowsProfile).toMatchObject({ platform: 'win32', protocolVersion: macosProfile.protocolVersion });
81
+ expect(windowsProfile.helperPath.replaceAll('\\', '/')).toMatch(/bin\/win32-x64\/moxxy-computer\.exe$/);
82
+ });
83
+
84
+ it('runs Linux on the same tool set with a live view of pictures', () => {
85
+ const plugin = createComputerControlPlugin('linux', 'x64', profile({ ...ready, os: 'linux', architecture: 'x64' }, linuxProfile('x64')));
86
+ expect(names(plugin.tools)).toEqual(Object.keys(computerTools).sort());
87
+ expect(plugin.surfaces?.map((surface) => surface.kind)).toEqual(['computer-preview']);
88
+ expect(linuxProfile('x64')).toMatchObject({ platform: 'linux', protocolVersion: macosProfile.protocolVersion, previewCodecs: ['jpeg'] });
89
+ expect(linuxProfile('x64').helperPath.replaceAll('\\', '/')).toMatch(/bin\/linux-x64\/moxxy-computer$/);
90
+ expect(linuxProfile('arm64').helperPath.replaceAll('\\', '/')).toMatch(/bin\/linux-arm64\/moxxy-computer$/);
91
+ });
92
+
93
+ it('offers only computer_status, with the reason, when the Linux helper is missing', async () => {
94
+ const plugin = createComputerControlPlugin('linux', 'arm64', profile(undefined, linuxProfile('arm64')));
95
+ expect(names(plugin.tools)).toEqual(['computer_status']);
96
+ const [status] = plugin.tools ?? [];
97
+ expect(await status?.handler({}, context)).toMatchObject({ platform: 'linux', ready: false, limitations: [expect.stringMatching(/helper is missing/)] });
98
+ });
99
+
100
+ it.each([['linux', 'ia32'], ['freebsd', 'x64'], ['win32', 'arm64']] as const)('reports %s %s as unsupported through computer_status alone', async (platform, arch) => {
101
+ const plugin = createComputerControlPlugin(platform, arch);
102
+ expect(names(plugin.tools)).toEqual(['computer_status']);
103
+ const [status] = plugin.tools ?? [];
104
+ expect(await status?.handler({}, context)).toEqual({ platform, architecture: arch, ready: false, limitations: ['Unsupported platform or architecture'] });
105
+ });
106
+
107
+ it('asks before every tool on every platform', () => {
108
+ for (const plugin of [createComputerControlPlugin('darwin', 'arm64', profile(ready)), createComputerControlPlugin('win32', 'x64', profile({ ...ready, architecture: 'x64' }, windowsProfile)), createComputerControlPlugin('linux', 'ia32')]) {
109
+ for (const tool of plugin.tools ?? []) expect(tool.permission?.action, tool.name).toBe('prompt');
110
+ }
111
+ });
112
+ });
package/src/index.ts CHANGED
@@ -1,64 +1,47 @@
1
- import { definePlugin, defineTool, z, type Plugin, type ToolDef } from '@moxxy/sdk';
2
- import { WindowsBackend } from './windows/backend.js';
3
- import { IS_DARWIN } from './shell.js';
4
- import { applescriptTool } from './tools/applescript.js';
5
- import { clickTool } from './tools/click.js';
6
- import { clipboardTool } from './tools/clipboard.js';
7
- import { keyTool } from './tools/key.js';
8
- import { openTool } from './tools/open.js';
9
- import { screenshotTool } from './tools/screenshot.js';
10
- import { typeTool } from './tools/type.js';
1
+ import { definePlugin, defineTool, z, type Plugin } from '@moxxy/sdk';
2
+ import { ComputerBackend, type PlatformProfile } from './backend/backend.js';
3
+ import { helperProblem } from './helper/artifact.js';
4
+ import { linuxProfile } from './linux/profile.js';
5
+ import { macosProfile } from './macos/profile.js';
6
+ import { windowsProfile } from './windows/profile.js';
11
7
 
12
- export {
13
- applescriptTool,
14
- clickTool,
15
- clipboardTool,
16
- keyTool,
17
- openTool,
18
- screenshotTool,
19
- typeTool,
20
- };
8
+ const name = '@moxxy/plugin-computer-control';
21
9
 
22
- export const computerControlTools: ReadonlyArray<ToolDef> = [
23
- screenshotTool,
24
- clickTool,
25
- typeTool,
26
- keyTool,
27
- openTool,
28
- clipboardTool,
29
- applescriptTool,
30
- ];
31
-
32
- /**
33
- * `@moxxy/plugin-computer-control` — programmatic control of the host
34
- * computer (mouse, keyboard, screenshot, clipboard, app launching,
35
- * AppleScript escape hatch).
36
- *
37
- * macOS retains its system-binary backend; Windows x64 uses our bundled
38
- * native helper. Unsupported hosts expose status only.
39
- *
40
- * Every tool is `permission: 'prompt'`. There is intentionally no
41
- * "allow always" shortcut for these — granting blanket permission to
42
- * drive the user's screen + keyboard is exactly the wrong default.
43
- */
44
- export function createComputerControlPlugin(platform: NodeJS.Platform = process.platform, arch: string = process.arch): Plugin {
45
- const backend = platform === 'win32' && arch === 'x64' ? new WindowsBackend() : undefined;
10
+ /** The only tool of a host that cannot run Computer Use: it says why. */
11
+ function statusOnly(platform: NodeJS.Platform, architecture: string, limitation: string): Plugin {
46
12
  const status = defineTool({
47
13
  name: 'computer_status', description: 'Report Computer Use platform capabilities and limitations.',
48
14
  inputSchema: z.object({}).strict(), permission: { action: 'prompt' },
49
- handler: () => ({ platform, architecture: arch, ready: platform === 'darwin',
50
- limitations: platform === 'darwin' ? ['Requires Screen Recording and Accessibility permissions'] : ['Unsupported platform or architecture'] }),
51
- });
52
- return definePlugin({
53
- name: '@moxxy/plugin-computer-control', version: '0.0.0',
54
- tools: backend ? backend.tools() : platform === 'darwin' ? [...computerControlTools, status] : [status],
55
- ...(backend ? { hooks: backend.hooks } : {}),
15
+ handler: () => ({ platform, architecture, ready: false, limitations: [limitation] }),
56
16
  });
17
+ return definePlugin({ name, version: '0.0.0', tools: [status] });
18
+ }
19
+
20
+ function profileFor(platform: NodeJS.Platform, arch: string): PlatformProfile | undefined {
21
+ if (platform === 'darwin') return macosProfile;
22
+ if (platform === 'linux') return arch === 'x64' || arch === 'arm64' ? linuxProfile(arch) : undefined;
23
+ return platform === 'win32' && arch === 'x64' ? windowsProfile : undefined;
24
+ }
25
+
26
+ /**
27
+ * `@moxxy/plugin-computer-control` — operates the user's desktop applications
28
+ * through a bundled native helper: macOS (universal), Windows x64 and Linux
29
+ * (x64, arm64; X11 sessions). Other
30
+ * hosts, and a host whose helper is missing, expose `computer_status` only.
31
+ *
32
+ * Every tool is `permission: 'prompt'`; which apps may be controlled, and how
33
+ * far, is a separate grant recorded in the session log.
34
+ */
35
+ export function createComputerControlPlugin(
36
+ platform: NodeJS.Platform = process.platform, arch: string = process.arch, profile: PlatformProfile | undefined = profileFor(platform, arch),
37
+ ): Plugin {
38
+ if (!profile) return statusOnly(platform, arch, 'Unsupported platform or architecture');
39
+ const problem = helperProblem(profile.helperPath, profile.protocolVersion);
40
+ if (problem) return statusOnly(platform, arch, `${problem} ${profile.unavailableMessage}`);
41
+ const backend = new ComputerBackend(profile);
42
+ return definePlugin({ name, version: '0.0.0', tools: backend.tools(), hooks: backend.hooks, surfaces: backend.surfaces() });
57
43
  }
58
44
 
59
45
  export const computerControlPlugin = createComputerControlPlugin();
60
46
 
61
47
  export default computerControlPlugin;
62
-
63
- // Re-export for callers that want a runtime gate.
64
- export { IS_DARWIN };
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { AppElement } from '@moxxy/jev';
3
+ import { judge, rungs } from './ladder.js';
4
+
5
+ const field: AppElement = { key: 'w/name', index: 7, depth: 1, role: 'text field', title: 'Name', frame: { x: 100, y: 40, width: 200, height: 20 } };
6
+ const bare: AppElement = { key: 'w/bare', index: 8, depth: 1, role: 'button' };
7
+
8
+ describe('rungs', () => {
9
+ it('clicks the element, then the point in its middle', () => {
10
+ expect(rungs({ do: 'click', target: 'Name' }, field, 'super+a')).toEqual([
11
+ [{ action: 'click', element_index: 7, mouse_button: 'left', click_count: 1 }],
12
+ [{ action: 'click', x: 200, y: 50, mouse_button: 'left', click_count: 1 }],
13
+ ]);
14
+ expect(rungs({ do: 'click', target: 'x' }, bare, 'super+a')).toHaveLength(1);
15
+ });
16
+
17
+ it('sets a value directly, then by clicking the field, selecting what it holds and typing', () => {
18
+ expect(rungs({ do: 'set_value', target: 'Name', text: 'clip' }, field, 'ctrl+a')).toEqual([
19
+ [{ action: 'set_value', element_index: 7, value: 'clip' }],
20
+ [
21
+ { action: 'click', element_index: 7, mouse_button: 'left', click_count: 1 },
22
+ { action: 'press_key', key: 'ctrl+a', repeat: 1 },
23
+ { action: 'type_text', text: 'clip' },
24
+ ],
25
+ ]);
26
+ });
27
+
28
+ it('types into a described element, then into whatever a click on it focuses', () => {
29
+ expect(rungs({ do: 'type', target: 'Name', text: 'hi' }, field, 'super+a')).toEqual([
30
+ [{ action: 'type_text', element_index: 7, text: 'hi' }],
31
+ [{ action: 'click', element_index: 7, mouse_button: 'left', click_count: 1 }, { action: 'type_text', text: 'hi' }],
32
+ ]);
33
+ });
34
+
35
+ it('has one way to type into the focus, press a key or scroll', () => {
36
+ expect(rungs({ do: 'type', text: 'hi' }, undefined, 'super+a')).toEqual([[{ action: 'type_text', text: 'hi' }]]);
37
+ expect(rungs({ do: 'key', key: 'Return' }, undefined, 'super+a')).toEqual([[{ action: 'press_key', key: 'Return', repeat: 1 }]]);
38
+ expect(rungs({ do: 'scroll', target: 'list', direction: 'down' }, field, 'super+a')).toEqual([[{ action: 'scroll', element_index: 7, direction: 'down', pages: 1 }]]);
39
+ });
40
+ });
41
+
42
+ describe('judge', () => {
43
+ const delivered = { outcome: 'delivered' } as const;
44
+
45
+ it('goes on after an action that was delivered and changed the window', () => {
46
+ expect(judge({ step: { do: 'click', target: 'Save' }, result: delivered, changed: true })).toEqual({ verdict: 'done', verified: false });
47
+ });
48
+
49
+ it('trusts Jev on what the step was expected to show', () => {
50
+ const step = { do: 'click', target: 'Save', expect: 'a save dialog is open' } as const;
51
+ expect(judge({ step, result: delivered, changed: true, expected: 0.9 })).toEqual({ verdict: 'done', verified: true });
52
+ expect(judge({ step, result: delivered, changed: true, expected: 0.1 })).toMatchObject({ verdict: 'retry' });
53
+ // Jev undecided: a changed window is taken as progress, an unchanged one is not.
54
+ expect(judge({ step, result: delivered, changed: true, expected: 0.45 })).toEqual({ verdict: 'done', verified: false });
55
+ expect(judge({ step, result: delivered, changed: false, expected: 0.45 })).toMatchObject({ verdict: 'retry' });
56
+ });
57
+
58
+ it('takes typing for done once its text shows in the field: typing again would append it once more', () => {
59
+ const step = { do: 'type', target: 'the address field', text: 'olx.pl', expect: 'OLX opens' } as const;
60
+ expect(judge({ step, result: delivered, changed: true, expected: 0.1, typed: true })).toEqual({ verdict: 'done', verified: false });
61
+ expect(judge({ step, result: delivered, changed: false, typed: true })).toEqual({ verdict: 'done', verified: false });
62
+ expect(judge({ step, result: delivered, changed: true, expected: 0.9, typed: true })).toEqual({ verdict: 'done', verified: true });
63
+ expect(judge({ step, result: delivered, changed: true, expected: 0.1, typed: false })).toMatchObject({ verdict: 'retry' });
64
+ });
65
+
66
+ it('takes a key for done when another window came to the front: Jev cannot tell a new window from an old one', () => {
67
+ const step = { do: 'key', key: 'super+n', expect: 'a new window is open' } as const;
68
+ expect(judge({ step, result: delivered, changed: true, expected: 0.2, moved: true })).toEqual({ verdict: 'done', verified: false });
69
+ expect(judge({ step, result: delivered, changed: true, expected: 0.2, moved: false })).toMatchObject({ verdict: 'retry' });
70
+ expect(judge({ step: { do: 'click', target: 'Tab', expect: 'a new window is open' }, result: delivered, changed: true, expected: 0.2, moved: true })).toMatchObject({ verdict: 'retry' });
71
+ });
72
+
73
+ it('stops when only the screenshot changed after a click at a point: Jev cannot see what the click did, and another click may undo it', () => {
74
+ const step = { do: 'click', target: 'Sort', expect: 'the sort options show' } as const;
75
+ expect(judge({ step, result: delivered, changed: true, expected: 0.1, unseen: true })).toMatchObject({ verdict: 'stop', why: expect.stringMatching(/screenshot/) });
76
+ expect(judge({ step, result: delivered, changed: true, expected: 0.9, unseen: true })).toEqual({ verdict: 'done', verified: true });
77
+ });
78
+
79
+ // Canva creates the design before its editor loads: a second click would start a second one.
80
+ it('stops after a link was followed whose page has not come yet, instead of clicking again', () => {
81
+ const step = { do: 'click', target: 'YouTube thumbnail', expect: 'the editor opens' } as const;
82
+ const loading = { outcome: 'delivered', code: 'page_loading' } as const;
83
+ expect(judge({ step, result: loading, changed: false, expected: 0.1 })).toMatchObject({ verdict: 'stop', why: expect.stringMatching(/loading/) });
84
+ expect(judge({ step: { do: 'click', target: 'YouTube thumbnail' }, result: loading, changed: false })).toMatchObject({ verdict: 'stop' });
85
+ expect(judge({ step, result: loading, changed: true, expected: 0.9 })).toEqual({ verdict: 'done', verified: true });
86
+ });
87
+
88
+ it('tries another way after a click or a value that changed nothing', () => {
89
+ expect(judge({ step: { do: 'click', target: 'Save' }, result: delivered, changed: false })).toMatchObject({ verdict: 'retry' });
90
+ expect(judge({ step: { do: 'set_value', target: 'Name', text: 'a' }, result: delivered, changed: false })).toMatchObject({ verdict: 'retry' });
91
+ });
92
+
93
+ it('accepts a key or a scroll that changed nothing visible', () => {
94
+ expect(judge({ step: { do: 'key', key: 'Escape' }, result: delivered, changed: false })).toEqual({ verdict: 'done', verified: false });
95
+ expect(judge({ step: { do: 'scroll', target: 'list', direction: 'down' }, result: delivered, changed: false })).toEqual({ verdict: 'done', verified: false });
96
+ });
97
+
98
+ it('tries another way when the action did not go through', () => {
99
+ expect(judge({ step: { do: 'click', target: 'Save' }, result: { outcome: 'unsupported', code: 'unsupported_action' }, changed: false })).toMatchObject({ verdict: 'retry' });
100
+ expect(judge({ step: { do: 'click', target: 'Save' }, result: { outcome: 'blocked', code: 'hit_test_mismatch' }, changed: false })).toMatchObject({ verdict: 'retry' });
101
+ });
102
+
103
+ it('stops at once for what only the user or the main model can resolve', () => {
104
+ for (const code of ['user_stopped', 'user_intervened', 'screen_locked', 'permissions_not_granted', 'tier_insufficient', 'app_not_allowed', 'protected_path'] as const) {
105
+ expect(judge({ step: { do: 'click', target: 'Save' }, result: { outcome: 'blocked', code }, changed: false })).toMatchObject({ verdict: 'stop' });
106
+ }
107
+ });
108
+
109
+ it('takes another way when the element would not take the input: focus left the field, or something covers it', () => {
110
+ const step = { do: 'type' as const, target: 'the search field', text: 'x' };
111
+ expect(judge({ step, result: { outcome: 'blocked', code: 'target_blocked' }, changed: false })).toEqual({ verdict: 'retry', why: 'blocked (target_blocked)' });
112
+ });
113
+ });
@@ -0,0 +1,88 @@
1
+ import { assertDefined } from '@moxxy/sdk';
2
+ import type { ActionResult, ErrorCode } from '../contract/outcome.js';
3
+ import type { ComputerAction, RunStep } from '../contract/tools.js';
4
+ import type { AppElement } from '@moxxy/jev';
5
+
6
+ /** How sure Jev must be that the expected result shows, and below what it certainly does not. */
7
+ const HOLDS = 0.6;
8
+ const FAILS = 0.3;
9
+
10
+ /** What only the user or the main model can resolve; trying another way would not help. */
11
+ const FINAL: ReadonlySet<ErrorCode> = new Set<ErrorCode>([
12
+ 'user_stopped', 'user_intervened', 'screen_locked', 'permissions_not_granted', 'permissions_pending', 'tier_insufficient',
13
+ 'app_not_allowed', 'protected_path', 'system_key_combo', 'clipboard_not_granted', 'own_window',
14
+ ]);
15
+
16
+ const click = (target: { element_index: number } | { x: number; y: number }): ComputerAction => ({ action: 'click', ...target, mouse_button: 'left', click_count: 1 });
17
+
18
+ /** The ways to carry out a step on its element, best first. Each way is a short run of actions. */
19
+ export function rungs(step: RunStep, element: AppElement | undefined, selectAll: string): ComputerAction[][] {
20
+ const { key, text, direction } = step;
21
+ if (step.do === 'key') {
22
+ assertDefined(key, 'a key step has a key');
23
+ return [[{ action: 'press_key', key, repeat: 1 }]];
24
+ }
25
+ if (step.do === 'type' && !element) {
26
+ assertDefined(text, 'a type step has text');
27
+ return [[{ action: 'type_text', text }]];
28
+ }
29
+ assertDefined(element, `a ${step.do} step has an element`);
30
+ const { index, frame } = element;
31
+ const onElement = click({ element_index: index });
32
+ if (step.do === 'click') {
33
+ return [[onElement], ...(frame ? [[click({ x: Math.round(frame.x + frame.width / 2), y: Math.round(frame.y + frame.height / 2) })]] : [])];
34
+ }
35
+ if (step.do === 'scroll') {
36
+ assertDefined(direction, 'a scroll step has a direction');
37
+ return [[{ action: 'scroll', element_index: index, direction, pages: 1 }]];
38
+ }
39
+ assertDefined(text, `a ${step.do} step has text`);
40
+ if (step.do === 'type') return [[{ action: 'type_text', element_index: index, text }], [onElement, { action: 'type_text', text }]];
41
+ const typed: ComputerAction = text === '' ? { action: 'press_key', key: 'BackSpace', repeat: 1 } : { action: 'type_text', text };
42
+ return [[{ action: 'set_value', element_index: index, value: text }], [onElement, { action: 'press_key', key: selectAll, repeat: 1 }, typed]];
43
+ }
44
+
45
+ export type Verdict =
46
+ | { readonly verdict: 'done'; readonly verified: boolean }
47
+ | { readonly verdict: 'retry'; readonly why: string }
48
+ | { readonly verdict: 'stop'; readonly why: string };
49
+
50
+ export interface Attempt {
51
+ readonly step: RunStep;
52
+ readonly result: ActionResult;
53
+ /** Whether the window looks different than before the attempt. */
54
+ readonly changed: boolean;
55
+ /** Jev's probability that `step.expect` holds now; absent when the step expects nothing. */
56
+ readonly expected?: number;
57
+ /** A click at a point changed the screenshot and no element: what it did is outside what Jev is shown. */
58
+ readonly unseen?: boolean;
59
+ /** A type step's text shows in its element now: typing it again would append it once more. */
60
+ readonly typed?: boolean;
61
+ /** Another window is in front than before the attempt. */
62
+ readonly moved?: boolean;
63
+ }
64
+
65
+ const UNSEEN = 'the click changed the screenshot but no element, so its result cannot be checked here and it was not clicked again (another click may undo it). Read the screenshot: when it shows the result, continue with the single tools by x and y';
66
+
67
+ const LOADING = 'the click followed a link and the new page is still loading, so it was not clicked again (a second click can start the same thing twice). Look at the window again before the next action';
68
+
69
+ /** What one attempt at a step means for the run. */
70
+ export function judge({ step, result, changed, expected, unseen, typed, moved }: Attempt): Verdict {
71
+ if (result.outcome !== 'delivered') {
72
+ const why = `${result.outcome}${result.code ? ` (${result.code})` : ''}`;
73
+ return result.code && FINAL.has(result.code) ? { verdict: 'stop', why } : { verdict: 'retry', why };
74
+ }
75
+ // Doing these again would do them twice. A key that brought another window forward opened or switched one, which Jev,
76
+ // reading a single window, cannot tell from one that was there before.
77
+ if (typed || (moved && step.do === 'key')) return { verdict: 'done', verified: expected !== undefined && expected >= HOLDS };
78
+ if (expected !== undefined && expected >= HOLDS) return { verdict: 'done', verified: true };
79
+ if (result.code === 'page_loading') return { verdict: 'stop', why: LOADING };
80
+ if (expected !== undefined) {
81
+ if (unseen) return { verdict: 'stop', why: UNSEEN };
82
+ if (expected <= FAILS || !changed) return { verdict: 'retry', why: 'the expected result does not show' };
83
+ return { verdict: 'done', verified: false };
84
+ }
85
+ // A key or a scroll can be right and still change nothing visible; a click or a value cannot.
86
+ if (!changed && step.do !== 'key' && step.do !== 'scroll') return { verdict: 'retry', why: 'nothing changed' };
87
+ return { verdict: 'done', verified: false };
88
+ }
@@ -0,0 +1,20 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { RunMemory as SharedMemory, describeRoutes as describeShared, promote as promoteShared, type AppMemory } from '@moxxy/jev';
3
+ import { moxxyPath } from '@moxxy/sdk/server';
4
+ import { runStepShape } from '../contract/tools.js';
5
+
6
+ export { guess, labelOf, recall, sameWords, targetOf, type AppMemory, type LearnedRoute, type LearnedTarget, type Lesson } from '@moxxy/jev';
7
+
8
+ /** What the moxxy team learned and ships with the plugin, so a new install does not start from nothing. */
9
+ export const shippedLearned = fileURLToPath(new URL('../../learned', import.meta.url));
10
+
11
+ /** Computer Use's lessons: one file per app under `~/.moxxy/computer-use/learned`, routes of computer_run steps. */
12
+ export class RunMemory extends SharedMemory {
13
+ constructor(directory: string = moxxyPath('computer-use', 'learned'), now: () => number = Date.now, shipped?: string) {
14
+ super(directory, now, shipped, runStepShape);
15
+ }
16
+ }
17
+
18
+ export const describeRoutes = (memory: AppMemory): string | undefined => describeShared(memory, 'computer_run');
19
+
20
+ export const promote = (from: string, to: string): Promise<string[]> => promoteShared(from, to, runStepShape);