@moxxy/plugin-computer-control 0.39.0 → 0.41.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 (69) hide show
  1. package/bin/win32-x64/moxxy-computer.exe +0 -0
  2. package/bin/win32-x64/moxxy-computer.exe.json +1 -0
  3. package/dist/index.d.ts +3 -5
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +19 -11
  6. package/dist/index.js.map +1 -1
  7. package/dist/temporary-files.d.ts +2 -0
  8. package/dist/temporary-files.d.ts.map +1 -0
  9. package/dist/temporary-files.js +10 -0
  10. package/dist/temporary-files.js.map +1 -0
  11. package/dist/tools/screenshot.d.ts.map +1 -1
  12. package/dist/tools/screenshot.js +22 -35
  13. package/dist/tools/screenshot.js.map +1 -1
  14. package/dist/windows/artifact.d.ts +3 -0
  15. package/dist/windows/artifact.d.ts.map +1 -0
  16. package/dist/windows/artifact.js +57 -0
  17. package/dist/windows/artifact.js.map +1 -0
  18. package/dist/windows/backend.d.ts +11 -0
  19. package/dist/windows/backend.d.ts.map +1 -0
  20. package/dist/windows/backend.js +122 -0
  21. package/dist/windows/backend.js.map +1 -0
  22. package/dist/windows/contracts.d.ts +1627 -0
  23. package/dist/windows/contracts.d.ts.map +1 -0
  24. package/dist/windows/contracts.js +137 -0
  25. package/dist/windows/contracts.js.map +1 -0
  26. package/dist/windows/control-service.d.ts +12 -0
  27. package/dist/windows/control-service.d.ts.map +1 -0
  28. package/dist/windows/control-service.js +65 -0
  29. package/dist/windows/control-service.js.map +1 -0
  30. package/dist/windows/guidance.d.ts +3 -0
  31. package/dist/windows/guidance.d.ts.map +1 -0
  32. package/dist/windows/guidance.js +20 -0
  33. package/dist/windows/guidance.js.map +1 -0
  34. package/dist/windows/maintenance.d.ts +7 -0
  35. package/dist/windows/maintenance.d.ts.map +1 -0
  36. package/dist/windows/maintenance.js +22 -0
  37. package/dist/windows/maintenance.js.map +1 -0
  38. package/dist/windows/protocol.d.ts +9 -0
  39. package/dist/windows/protocol.d.ts.map +1 -0
  40. package/dist/windows/protocol.js +32 -0
  41. package/dist/windows/protocol.js.map +1 -0
  42. package/dist/windows/transport.d.ts +23 -0
  43. package/dist/windows/transport.d.ts.map +1 -0
  44. package/dist/windows/transport.js +150 -0
  45. package/dist/windows/transport.js.map +1 -0
  46. package/package.json +10 -5
  47. package/skills/computer-control.md +82 -11
  48. package/src/index.ts +20 -11
  49. package/src/temporary-files.test.ts +18 -0
  50. package/src/temporary-files.ts +9 -0
  51. package/src/tools/screenshot.ts +23 -37
  52. package/src/windows/action-contracts.test.ts +13 -0
  53. package/src/windows/artifact.test.ts +16 -0
  54. package/src/windows/artifact.ts +57 -0
  55. package/src/windows/backend.test.ts +41 -0
  56. package/src/windows/backend.ts +122 -0
  57. package/src/windows/contracts.test.ts +81 -0
  58. package/src/windows/contracts.ts +143 -0
  59. package/src/windows/control-service.test.ts +58 -0
  60. package/src/windows/control-service.ts +68 -0
  61. package/src/windows/guidance.test.ts +29 -0
  62. package/src/windows/guidance.ts +21 -0
  63. package/src/windows/maintenance.ts +19 -0
  64. package/src/windows/model-contract.test.ts +37 -0
  65. package/src/windows/protocol.ts +27 -0
  66. package/src/windows/text-contracts.test.ts +14 -0
  67. package/src/windows/transport.test.ts +106 -0
  68. package/src/windows/transport.ts +139 -0
  69. package/src/windows/window-typing.test.ts +14 -0
@@ -0,0 +1,139 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { JsonLineDecoder } from './protocol.js';
4
+ import { MAX_FRAME_BYTES, PROTOCOL_VERSION, responseSchema, controlStateSchema, controlCommandSchema, type ControlState } from './contracts.js';
5
+
6
+ interface Pending {
7
+ id: string;
8
+ resolve(value: unknown): void;
9
+ reject(error: Error): void;
10
+ dispose(): void;
11
+ waiting(value: boolean): void;
12
+ }
13
+
14
+ /** One serial, non-retrying connection to a disposable native helper. */
15
+ export class HelperTransport {
16
+ private readonly child: ChildProcessWithoutNullStreams;
17
+ private readonly exited: Promise<void>;
18
+ private pending: Pending | undefined;
19
+ private stopped = false;
20
+ private userStopped = false;
21
+ private queue: Promise<unknown> = Promise.resolve();
22
+ private stderrBytes = 0;
23
+ get closed(): boolean { return this.stopped; }
24
+ get stoppedByUser(): boolean { return this.userStopped; }
25
+
26
+ constructor(command: string, args: string[], private readonly timeoutMs = 15_000,
27
+ private readonly onState: (state: ControlState) => void = () => undefined) {
28
+ this.child = spawn(command, args, { shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
29
+ this.exited = new Promise((resolve) => this.child.once('close', () => resolve()));
30
+ const decoder = new JsonLineDecoder(MAX_FRAME_BYTES);
31
+ this.child.on('error', () => this.fail(new Error('Computer Use helper cannot start; reinstall the extension.')));
32
+ this.child.stdin.on('error', () => this.fail(new Error('Computer Use pipe closed; action not retried.')));
33
+ this.child.on('close', (code) => {
34
+ if (code === 20) this.userStopped = true;
35
+ this.fail(new Error(this.userStopped
36
+ ? 'Computer Use stopped by user; action not retried.'
37
+ : `Computer Use helper exited (${code}); action not retried.`));
38
+ });
39
+ this.child.stdout.on('data', (bytes: Buffer) => {
40
+ try {
41
+ for (const frame of decoder.push(bytes)) {
42
+ const event = controlStateSchema.safeParse(frame);
43
+ if (event.success) {
44
+ const pending = this.pending;
45
+ if (!pending || event.data.id !== pending.id) throw new Error('Unmatched control state');
46
+ pending.waiting(event.data.state === 'waiting_for_focus' || event.data.state === 'paused_by_user');
47
+ this.onState(event.data);
48
+ continue;
49
+ }
50
+ const response = responseSchema.parse(frame);
51
+ const pending = this.pending;
52
+ if (!pending || pending.id !== response.id) throw new Error('Unmatched response');
53
+ this.pending = undefined;
54
+ pending.dispose();
55
+ if (response.ok) pending.resolve(response.result);
56
+ else pending.reject(new Error(`${response.error.code}: ${response.error.message}`));
57
+ }
58
+ } catch {
59
+ this.fail(new Error('Computer Use protocol mismatch or invalid response; update the extension. Action not retried.'));
60
+ }
61
+ });
62
+ this.child.stdout.on('end', () => {
63
+ try { decoder.finish(); } catch { this.fail(new Error('Computer Use incomplete response; action not retried.')); }
64
+ });
65
+ // Never echo native stderr: it can contain application text or secrets.
66
+ this.child.stderr.on('data', (bytes: Buffer) => {
67
+ this.stderrBytes += bytes.length;
68
+ if (this.stderrBytes > 64_000) this.fail(new Error('Computer Use diagnostic output limit exceeded'));
69
+ });
70
+ }
71
+
72
+ request(method: string, params: unknown, signal: AbortSignal): Promise<unknown> {
73
+ const next = this.queue.then(() => this.send(method, params, signal));
74
+ this.queue = next.catch(() => undefined);
75
+ return next;
76
+ }
77
+
78
+ /** Control uses the reader side of the helper, never its blocked request queue. */
79
+ control(command: 'pause' | 'resume' | 'stop'): void {
80
+ controlCommandSchema.parse(command);
81
+ if (this.stopped) throw new Error('Computer Use connection closed');
82
+ if (command === 'stop') {
83
+ this.userStopped = true;
84
+ this.fail(new Error('Computer Use stopped by user; action not retried.'));
85
+ return;
86
+ }
87
+ this.child.stdin.write(JSON.stringify({version: PROTOCOL_VERSION, control: command}) + '\n');
88
+ }
89
+
90
+ private send(method: string, params: unknown, signal: AbortSignal): Promise<unknown> {
91
+ if (this.stopped) return Promise.reject(new Error('Computer Use connection closed; observe again with a new connection.'));
92
+ if (signal.aborted) return Promise.reject(new Error('Computer Use cancelled'));
93
+ const id = randomUUID();
94
+ const frame = JSON.stringify({ version: PROTOCOL_VERSION, id, method, params }) + '\n';
95
+ if (Buffer.byteLength(frame) > MAX_FRAME_BYTES) return Promise.reject(new Error('Computer Use request limit exceeded'));
96
+ return new Promise((resolve, reject) => {
97
+ const abort = () => this.fail(new Error('Computer Use cancelled; action not retried.'));
98
+ let remaining = this.timeoutMs;
99
+ let started = performance.now();
100
+ let waiting = false;
101
+ const timeout = () => this.fail(new Error('Computer Use timed out; action not retried. Observe again.'));
102
+ let timer = setTimeout(timeout, remaining);
103
+ signal.addEventListener('abort', abort, { once: true });
104
+ this.pending = { id, resolve, reject, waiting: (value) => {
105
+ if (value === waiting) return;
106
+ waiting = value;
107
+ if (waiting) {
108
+ clearTimeout(timer);
109
+ remaining = Math.max(0, remaining - (performance.now() - started));
110
+ } else {
111
+ started = performance.now();
112
+ timer = setTimeout(timeout, remaining);
113
+ }
114
+ }, dispose: () => {
115
+ clearTimeout(timer);
116
+ signal.removeEventListener('abort', abort);
117
+ } };
118
+ this.child.stdin.write(frame);
119
+ });
120
+ }
121
+
122
+ private fail(error: Error): void {
123
+ if (this.stopped) return;
124
+ this.stopped = true;
125
+ const pending = this.pending;
126
+ this.pending = undefined;
127
+ if (pending) { pending.dispose(); pending.reject(error); }
128
+ // Closing stdin is the graceful shutdown signal (native watchdog releases input).
129
+ this.child.stdin.end();
130
+ const kill = setTimeout(() => this.child.kill('SIGKILL'), 500);
131
+ kill.unref();
132
+ void this.exited.then(() => clearTimeout(kill));
133
+ }
134
+
135
+ async close(): Promise<void> {
136
+ this.fail(new Error('Computer Use connection closed'));
137
+ await this.exited;
138
+ }
139
+ }
@@ -0,0 +1,14 @@
1
+ import { expect, it } from 'vitest';
2
+ import { createComputerControlPlugin } from '../index.js';
3
+
4
+ it('exposes explicit observed-window typing on Windows without weakening control-targeted typing', () => {
5
+ const tools=createComputerControlPlugin('win32','x64').tools ?? [];
6
+ const window=tools.find(tool=>tool.name==='computer_type_window');
7
+ expect(window).toBeDefined();
8
+ expect(window?.permission?.action).toBe('prompt');
9
+ expect(window?.inputSchema.safeParse({windowId:'w',observationId:'o',text:'hello'}).success).toBe(true);
10
+ expect(window?.inputSchema.safeParse({windowId:'w',text:'hello'}).success).toBe(false);
11
+ const control=tools.find(tool=>tool.name==='computer_type');
12
+ expect(control?.inputSchema.safeParse({windowId:'w',observationId:'o',text:'hello'}).success).toBe(false);
13
+ expect(createComputerControlPlugin('darwin','arm64').tools?.some(tool=>tool.name==='computer_type_window')).toBe(false);
14
+ });