@myagentroam/node 0.13.11 → 0.13.12

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 (41) hide show
  1. package/dist/connection/direct-transfer-service.js +60 -0
  2. package/dist/connection/endpoints.js +16 -0
  3. package/dist/connector.js +298 -10
  4. package/dist/ctags-binary.js +5 -1
  5. package/dist/managed-binaries.js +8 -0
  6. package/dist/marctl/package.json +1 -1
  7. package/dist/remote-desktop/desktop-byte-stream.js +128 -0
  8. package/dist/remote-desktop/desktop-error.js +35 -0
  9. package/dist/remote-desktop/desktop-service-core.js +384 -0
  10. package/dist/remote-desktop/direct-desktop-service.js +46 -0
  11. package/dist/remote-desktop/linux-managed-binaries.js +46 -0
  12. package/dist/remote-desktop/macos-managed-binaries.js +19 -0
  13. package/dist/remote-desktop/macos-remote-desktop-runtime.js +121 -0
  14. package/dist/remote-desktop/macvnc-managed-process.js +196 -0
  15. package/dist/remote-desktop/pooled-desktop-attachment.js +62 -0
  16. package/dist/remote-desktop/relay-desktop-service.js +40 -0
  17. package/dist/remote-desktop/remote-desktop-attachment.js +87 -0
  18. package/dist/remote-desktop/rfb-backend-handshake.js +98 -0
  19. package/dist/remote-desktop/rfb-client-gate.js +318 -0
  20. package/dist/remote-desktop/rfb-frontend-handshake.js +38 -0
  21. package/dist/remote-desktop/rfb-handshake-io.js +91 -0
  22. package/dist/remote-desktop/rfb-input-release.js +77 -0
  23. package/dist/remote-desktop/standard-rfb-attachment.js +40 -0
  24. package/dist/remote-desktop/tigervnc-attachment.js +68 -0
  25. package/dist/remote-desktop/tigervnc-backend-pool.js +88 -0
  26. package/dist/remote-desktop/tigervnc-cleanup-channel.js +129 -0
  27. package/dist/remote-desktop/tigervnc-managed-process.js +274 -0
  28. package/dist/remote-desktop/tigervnc-rfb-session.js +190 -0
  29. package/dist/remote-desktop/ultravnc-config.js +35 -0
  30. package/dist/remote-desktop/ultravnc-managed-process.js +180 -0
  31. package/dist/remote-desktop/vnc-password.js +15 -0
  32. package/dist/remote-desktop/windows-managed-binaries.js +18 -0
  33. package/dist/remote-desktop/windows-remote-desktop-runtime.js +126 -0
  34. package/dist/remote-desktop/x11-desktop-topology.js +82 -0
  35. package/dist/remote-desktop/x11-remote-desktop-runtime.js +99 -0
  36. package/dist/remote-desktop/x11-topology-source.js +205 -0
  37. package/dist/remote-desktop/x11-topology.js +88 -0
  38. package/dist/ripgrep-binary.js +5 -1
  39. package/dist/service/node-control-channel.js +7 -1
  40. package/dist/service/node-control-message-service.js +11 -0
  41. package/package.json +6 -6
@@ -0,0 +1,205 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { isAbsolute } from 'node:path';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { z } from 'zod';
5
+ import { RemoteDesktopError } from './desktop-error.js';
6
+ const coordinate = z.number().int().min(-0x80000000).max(0x7fffffff);
7
+ const dimension = z.number().int().min(1).max(65535);
8
+ const identifier = z.number().int().min(1).max(0xffffffff);
9
+ const outputSchema = z
10
+ .object({
11
+ id: identifier,
12
+ name: z.string().min(1).max(256),
13
+ x: coordinate,
14
+ y: coordinate,
15
+ width: dimension,
16
+ height: dimension,
17
+ rotation: z.number().int().min(1).max(63),
18
+ transform: z.array(coordinate).length(9)
19
+ })
20
+ .strict();
21
+ const snapshotSchema = z
22
+ .object({
23
+ type: z.literal('snapshot'),
24
+ width: dimension,
25
+ height: dimension,
26
+ monitors: z
27
+ .array(z
28
+ .object({
29
+ id: identifier,
30
+ name: z.string().min(1).max(256),
31
+ primary: z.boolean(),
32
+ x: coordinate,
33
+ y: coordinate,
34
+ width: dimension,
35
+ height: dimension,
36
+ mmWidth: z.number().int().min(0).max(0x7fffffff),
37
+ mmHeight: z.number().int().min(0).max(0x7fffffff),
38
+ outputs: z.array(outputSchema).min(1).max(64)
39
+ })
40
+ .strict())
41
+ .min(1)
42
+ .max(64)
43
+ })
44
+ .strict();
45
+ const eventSchema = z.discriminatedUnion('type', [
46
+ snapshotSchema,
47
+ z
48
+ .object({
49
+ type: z.literal('invalidated'),
50
+ resetOutput: identifier.optional(),
51
+ resetAll: z.literal(true).optional()
52
+ })
53
+ .strict()
54
+ ]);
55
+ const maxLineBytes = 256 * 1024;
56
+ /** Owns one native XRandR subscription, not the X11 session or any control lease. */
57
+ export class X11TopologySource {
58
+ child;
59
+ options;
60
+ #exit;
61
+ #initial;
62
+ #resolveInitial;
63
+ #rejectInitial;
64
+ #line = Buffer.alloc(0);
65
+ #waitingSnapshot = true;
66
+ #ready = false;
67
+ #stopping = false;
68
+ #failed = false;
69
+ #stopPromise;
70
+ constructor(child, options) {
71
+ this.child = child;
72
+ this.options = options;
73
+ this.#initial = new Promise((resolve, reject) => {
74
+ this.#resolveInitial = resolve;
75
+ this.#rejectInitial = reject;
76
+ });
77
+ this.#exit = new Promise((resolve) => child.once('close', () => {
78
+ this.#fail();
79
+ resolve();
80
+ }));
81
+ child.on('error', () => this.#fail());
82
+ child.stdout.on('error', () => this.#fail());
83
+ child.stdout.on('end', () => this.#fail());
84
+ child.stdout.on('close', () => this.#fail());
85
+ child.stdin.on('error', () => this.#fail());
86
+ child.stderr.resume();
87
+ child.stdout.on('data', (chunk) => this.#receive(chunk));
88
+ }
89
+ static async start(options, signal) {
90
+ if (process.platform !== 'linux' ||
91
+ !isAbsolute(options.binaryPath) ||
92
+ !isAbsolute(options.xAuthority) ||
93
+ !options.display ||
94
+ signal.aborted)
95
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
96
+ let child;
97
+ try {
98
+ child = spawn(options.binaryPath, [], {
99
+ env: { PATH: '/usr/bin:/bin', DISPLAY: options.display, XAUTHORITY: options.xAuthority },
100
+ stdio: ['pipe', 'pipe', 'pipe']
101
+ });
102
+ }
103
+ catch {
104
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
105
+ }
106
+ const source = new X11TopologySource(child, options);
107
+ const deadline = AbortSignal.any([signal, AbortSignal.timeout(10_000)]);
108
+ const abort = () => source.#fail();
109
+ deadline.addEventListener('abort', abort, { once: true });
110
+ if (deadline.aborted)
111
+ abort();
112
+ try {
113
+ await source.#initial;
114
+ if (source.#failed)
115
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
116
+ return source;
117
+ }
118
+ catch {
119
+ await source.stop();
120
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
121
+ }
122
+ finally {
123
+ deadline.removeEventListener('abort', abort);
124
+ }
125
+ }
126
+ stop() {
127
+ if (this.#stopPromise)
128
+ return this.#stopPromise;
129
+ this.#stopping = true;
130
+ this.child.stdin.end();
131
+ this.#stopPromise = (async () => {
132
+ if (await Promise.race([
133
+ this.#exit.then(() => false),
134
+ delay(5_000, undefined, { ref: false }).then(() => true)
135
+ ])) {
136
+ this.child.kill('SIGKILL');
137
+ if (await Promise.race([
138
+ this.#exit.then(() => false),
139
+ delay(5_000, undefined, { ref: false }).then(() => true)
140
+ ]))
141
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
142
+ }
143
+ })();
144
+ return this.#stopPromise;
145
+ }
146
+ #receive(chunk) {
147
+ if (this.#failed || this.#stopping)
148
+ return;
149
+ let offset = 0;
150
+ while (offset < chunk.length && !this.#failed && !this.#stopping) {
151
+ const newline = chunk.indexOf(10, offset);
152
+ const end = newline < 0 ? chunk.length : newline;
153
+ if (this.#line.length + end - offset > maxLineBytes)
154
+ return this.#fail();
155
+ this.#line = Buffer.concat([this.#line, chunk.subarray(offset, end)]);
156
+ if (newline < 0)
157
+ return;
158
+ try {
159
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(this.#line);
160
+ const event = eventSchema.parse(JSON.parse(text));
161
+ this.#line = Buffer.alloc(0);
162
+ if (event.type === 'invalidated') {
163
+ if (this.#waitingSnapshot)
164
+ return this.#fail();
165
+ this.#waitingSnapshot = true;
166
+ this.options.onInvalidated({
167
+ ...(event.resetOutput === undefined ? {} : { resetOutput: event.resetOutput }),
168
+ ...(event.resetAll === undefined ? {} : { resetAll: event.resetAll })
169
+ });
170
+ }
171
+ else {
172
+ if (!this.#waitingSnapshot)
173
+ return this.#fail();
174
+ this.#waitingSnapshot = false;
175
+ this.options.onSnapshot(event);
176
+ if (!this.#ready) {
177
+ this.#ready = true;
178
+ this.#resolveInitial();
179
+ }
180
+ }
181
+ }
182
+ catch {
183
+ this.#fail();
184
+ }
185
+ offset = newline + 1;
186
+ }
187
+ }
188
+ #fail() {
189
+ if (this.#failed || this.#stopping)
190
+ return;
191
+ this.#failed = true;
192
+ try {
193
+ if (!this.#ready)
194
+ this.#rejectInitial(new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE'));
195
+ else
196
+ this.options.onUnavailable();
197
+ }
198
+ catch {
199
+ // A consumer failure must not prevent the owned native process stopping.
200
+ }
201
+ finally {
202
+ void this.stop().catch(() => { });
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,88 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { RemoteDesktopError } from './desktop-error.js';
3
+ /** Tracks layout and display identities for one X11 session, independently of VNC processes. */
4
+ export class X11Topology {
5
+ desktopGeneration;
6
+ #identities = new Map();
7
+ #revision = 0;
8
+ #layout;
9
+ constructor(desktopGeneration) {
10
+ this.desktopGeneration = desktopGeneration;
11
+ if (!desktopGeneration)
12
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
13
+ }
14
+ /** The owner must fence attachments on every invalidation before accepting a new layout. */
15
+ invalidate(event) {
16
+ this.#layout = undefined;
17
+ if (event.resetAll)
18
+ this.#identities.clear();
19
+ if (event.resetOutput !== undefined) {
20
+ for (const [key, identity] of this.#identities) {
21
+ if (identity.outputs.includes(event.resetOutput))
22
+ this.#identities.delete(key);
23
+ }
24
+ }
25
+ }
26
+ /** Lost subscriptions cannot prove which monitor identities survived. */
27
+ unavailable() {
28
+ this.invalidate({ resetAll: true });
29
+ }
30
+ accept(snapshot) {
31
+ if (this.#layout)
32
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
33
+ const monitors = [...snapshot.monitors].sort((a, b) => a.id - b.id);
34
+ if (new Set(monitors.map((monitor) => monitor.id)).size !== monitors.length ||
35
+ monitors.filter((monitor) => monitor.primary).length > 1 ||
36
+ (monitors.length > 1 && !monitors.some((monitor) => monitor.primary)) ||
37
+ monitors.some((monitor) => monitor.x < 0 ||
38
+ monitor.y < 0 ||
39
+ monitor.x + monitor.width > snapshot.width ||
40
+ monitor.y + monitor.height > snapshot.height ||
41
+ new Set(monitor.outputs.map((output) => output.id)).size !== monitor.outputs.length))
42
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
43
+ const active = new Set(monitors.map((monitor) => monitor.id));
44
+ for (const key of this.#identities.keys())
45
+ if (!active.has(key))
46
+ this.#identities.delete(key);
47
+ const displays = monitors.map((monitor) => {
48
+ const outputs = monitor.outputs.map((output) => output.id).sort((a, b) => a - b);
49
+ let identity = this.#identities.get(monitor.id);
50
+ if (!identity ||
51
+ identity.outputs.length !== outputs.length ||
52
+ identity.outputs.some((output, index) => output !== outputs[index])) {
53
+ identity = { displayId: randomUUID(), outputs };
54
+ this.#identities.set(monitor.id, identity);
55
+ }
56
+ const bounds = { x: monitor.x, y: monitor.y, width: monitor.width, height: monitor.height };
57
+ return {
58
+ displayId: identity.displayId,
59
+ name: monitor.name,
60
+ primary: monitor.primary || monitors.length === 1,
61
+ nativeBounds: bounds,
62
+ framebufferBounds: { ...bounds },
63
+ nativeToFramebuffer: [1, 0, 0, 1, 0, 0]
64
+ };
65
+ });
66
+ // A notification may hide a transient change even when the new snapshot
67
+ // matches the old one. Never let grants from before that fence revive.
68
+ this.#revision++;
69
+ this.#layout = {
70
+ desktopGeneration: this.desktopGeneration,
71
+ topologyRevision: this.#revision,
72
+ framebuffer: { width: snapshot.width, height: snapshot.height },
73
+ displays
74
+ };
75
+ return structuredClone(this.#layout);
76
+ }
77
+ describe() {
78
+ if (!this.#layout)
79
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
80
+ return structuredClone(this.#layout);
81
+ }
82
+ /** A mismatch must end the attachment, never adapt input using stale geometry. */
83
+ matchesFramebuffer(width, height) {
84
+ return (!!this.#layout &&
85
+ this.#layout.framebuffer.width === width &&
86
+ this.#layout.framebuffer.height === height);
87
+ }
88
+ }
@@ -1,5 +1,6 @@
1
1
  import { dirname, resolve } from 'node:path';
2
2
  import { createRequire } from 'node:module';
3
+ import { managedBinariesPlatformDirectory } from './managed-binaries.js';
3
4
  export const BUNDLED_RIPGREP_VERSION = '15.1.0';
4
5
  const TARGETS = {
5
6
  'linux-x64': ['linux-x64', 'rg'],
@@ -18,7 +19,10 @@ export function bundledRipgrepPath(platform = process.platform, architecture = p
18
19
  const target = TARGETS[`${platform}-${architecture}`];
19
20
  if (target === undefined)
20
21
  throw new Error('RIPGREP_PLATFORM_UNSUPPORTED');
21
- return resolve(bundledRipgrepDirectory(packageRoot), ...target);
22
+ const directory = packageRoot === undefined
23
+ ? resolve(managedBinariesPlatformDirectory(platform, architecture), 'vendor/ripgrep')
24
+ : bundledRipgrepDirectory(packageRoot);
25
+ return resolve(directory, ...target);
22
26
  }
23
27
  export function prependBundledRipgrepToPath(environment, executable, platform = process.platform) {
24
28
  const key = platform === 'win32' ? 'Path' : 'PATH';
@@ -7,6 +7,7 @@ const MAX_PREAUTH_BYTES = 8 * 1024 * 1024;
7
7
  export class NodeControlChannel {
8
8
  options;
9
9
  socket;
10
+ inboundMuted = false;
10
11
  authenticated = false;
11
12
  preauthFrames = 0;
12
13
  preauthBytes = 0;
@@ -25,6 +26,7 @@ export class NodeControlChannel {
25
26
  this.socket.readyState === WebSocket.CONNECTING)))
26
27
  return;
27
28
  this.options.onConnecting(this.attempts);
29
+ this.inboundMuted = false;
28
30
  const socket = new WebSocket(nodeConnectEndpoint(config.serverUrl));
29
31
  this.socket = socket;
30
32
  socket.once('open', () => {
@@ -40,7 +42,7 @@ export class NodeControlChannel {
40
42
  socket.close(4000, 'NODE_CREDENTIAL_MISSING');
41
43
  });
42
44
  socket.on('message', (raw) => {
43
- if (this.socket === socket)
45
+ if (this.socket === socket && !this.inboundMuted)
44
46
  this.options.onMessage(raw.toString());
45
47
  });
46
48
  socket.once('close', (code, reason) => {
@@ -61,6 +63,10 @@ export class NodeControlChannel {
61
63
  connected() {
62
64
  return this.authenticated && this.socket?.readyState === WebSocket.OPEN;
63
65
  }
66
+ /** Stop admitting new work while allowing final owned cleanup events to be sent. */
67
+ muteInbound() {
68
+ this.inboundMuted = true;
69
+ }
64
70
  connection() {
65
71
  return this.socket;
66
72
  }
@@ -55,6 +55,17 @@ export class NodeControlMessageService {
55
55
  if (envelope.type.startsWith('terminal.direct.') &&
56
56
  this.options.handleTerminalDirect?.(envelope) === true)
57
57
  return;
58
+ if (envelope.type === 'desktop.ready') {
59
+ if (this.options.handleDesktopDirect?.(envelope) !== true)
60
+ this.options.handleDesktopRelay?.(envelope);
61
+ return;
62
+ }
63
+ if (envelope.type.startsWith('desktop.direct.') &&
64
+ this.options.handleDesktopDirect?.(envelope) === true)
65
+ return;
66
+ if (envelope.type.startsWith('desktop.relay.') &&
67
+ this.options.handleDesktopRelay?.(envelope) === true)
68
+ return;
58
69
  if (envelope.type === 'usage.ack') {
59
70
  this.options.usageAck?.(envelope.payload);
60
71
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.13.11",
3
+ "version": "0.13.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/myagentroam/mar.git",
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "@anthropic-ai/claude-agent-sdk": "0.3.233",
39
39
  "@modelcontextprotocol/sdk": "1.32.0",
40
- "@myagentroam/binaries": "0.10.2",
40
+ "@myagentroam/binaries": "0.10.3",
41
41
  "@opencode-ai/sdk": "1.18.20",
42
42
  "minimatch": "^10.2.6",
43
43
  "node-datachannel": "0.33.4",
@@ -49,14 +49,14 @@
49
49
  "turndown-plugin-gfm": "^1.0.2",
50
50
  "ws": "^8.21.3",
51
51
  "zod": "4.4.3",
52
- "@myagentroam/agent": "0.13.11",
53
- "@myagentroam/protocol": "0.13.11"
52
+ "@myagentroam/agent": "0.13.12",
53
+ "@myagentroam/protocol": "0.13.12"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/turndown": "^5.0.6",
57
57
  "@types/ws": "^8.18.1",
58
- "@myagentroam/marctl": "0.13.11",
59
- "@myagentroam/code-navigation": "0.11.1"
58
+ "@myagentroam/code-navigation": "0.11.1",
59
+ "@myagentroam/marctl": "0.13.12"
60
60
  },
61
61
  "exports": {
62
62
  "./config": {