@rynx-ai/cli 0.1.10

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 (47) hide show
  1. package/dist/agent-file.d.ts +13 -0
  2. package/dist/agent-file.js +61 -0
  3. package/dist/browser-cli-args.d.ts +28 -0
  4. package/dist/browser-cli-args.js +181 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +11 -0
  7. package/dist/client.d.ts +4 -0
  8. package/dist/client.js +2 -0
  9. package/dist/commands/agent.d.ts +1 -0
  10. package/dist/commands/agent.js +55 -0
  11. package/dist/commands/app-distribution.d.ts +6 -0
  12. package/dist/commands/app-distribution.js +98 -0
  13. package/dist/commands/browser.d.ts +1 -0
  14. package/dist/commands/browser.js +273 -0
  15. package/dist/commands/cleanup.d.ts +1 -0
  16. package/dist/commands/cleanup.js +24 -0
  17. package/dist/commands/emulator.d.ts +1 -0
  18. package/dist/commands/emulator.js +23 -0
  19. package/dist/commands/errors.d.ts +5 -0
  20. package/dist/commands/errors.js +10 -0
  21. package/dist/commands/index.d.ts +9 -0
  22. package/dist/commands/index.js +9 -0
  23. package/dist/commands/plugin.d.ts +6 -0
  24. package/dist/commands/plugin.js +289 -0
  25. package/dist/commands/runtime.d.ts +1 -0
  26. package/dist/commands/runtime.js +136 -0
  27. package/dist/commands/skills.d.ts +10 -0
  28. package/dist/commands/skills.js +80 -0
  29. package/dist/control-client.d.ts +163 -0
  30. package/dist/control-client.js +1028 -0
  31. package/dist/control-endpoint.d.ts +29 -0
  32. package/dist/control-endpoint.js +121 -0
  33. package/dist/desktop-browser-host-client.d.ts +44 -0
  34. package/dist/desktop-browser-host-client.js +430 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +2 -0
  37. package/dist/legacy-adapter.d.ts +7 -0
  38. package/dist/legacy-adapter.js +63 -0
  39. package/dist/run-cli.d.ts +1 -0
  40. package/dist/run-cli.js +62 -0
  41. package/dist/usage.d.ts +1 -0
  42. package/dist/usage.js +56 -0
  43. package/dist/version.d.ts +1 -0
  44. package/dist/version.js +8 -0
  45. package/package.json +52 -0
  46. package/skills/rynx-cli/SKILL.md +99 -0
  47. package/skills/rynx-cli/agents/openai.yaml +4 -0
@@ -0,0 +1,29 @@
1
+ export interface DaemonControlEndpoint {
2
+ origin: string;
3
+ pid: number;
4
+ managementToken: string;
5
+ distribution?: "app" | "standalone";
6
+ productVersion?: string;
7
+ buildId?: string;
8
+ daemonLifecycle?: "follow-app" | "resident" | "standalone";
9
+ managementProtocol?: {
10
+ current: number;
11
+ minimumCompatible: number;
12
+ };
13
+ }
14
+ export interface ResolveDaemonControlEndpointOptions {
15
+ signal?: AbortSignal;
16
+ probe?: boolean;
17
+ }
18
+ /**
19
+ * Read the endpoint published by the App- or supervisor-owned daemon.
20
+ *
21
+ * This package deliberately does not start a daemon. Starting one is a
22
+ * distribution/lifecycle concern; a read-only client must not silently create
23
+ * a second owner while trying to inspect status.
24
+ */
25
+ export declare function resolveDaemonControlEndpoint(options?: ResolveDaemonControlEndpointOptions): Promise<DaemonControlEndpoint | null>;
26
+ /**
27
+ * Resolve a live resident daemon without taking ownership of its lifecycle.
28
+ */
29
+ export declare function ensureDaemonControlEndpoint(options?: ResolveDaemonControlEndpointOptions): Promise<DaemonControlEndpoint>;
@@ -0,0 +1,121 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { rynxHome } from "@rynx-ai/core";
4
+ const PROBE_TIMEOUT_MS = 1_000;
5
+ /**
6
+ * Read the endpoint published by the App- or supervisor-owned daemon.
7
+ *
8
+ * This package deliberately does not start a daemon. Starting one is a
9
+ * distribution/lifecycle concern; a read-only client must not silently create
10
+ * a second owner while trying to inspect status.
11
+ */
12
+ export async function resolveDaemonControlEndpoint(options = {}) {
13
+ options.signal?.throwIfAborted();
14
+ const endpoint = await readControlEndpoint();
15
+ if (!endpoint || !isProcessAlive(endpoint.pid))
16
+ return null;
17
+ if (options.probe === false)
18
+ return endpoint;
19
+ return await isHealthy(endpoint.origin, options.signal) ? endpoint : null;
20
+ }
21
+ /**
22
+ * Resolve a live resident daemon without taking ownership of its lifecycle.
23
+ */
24
+ export async function ensureDaemonControlEndpoint(options = {}) {
25
+ const endpoint = await resolveDaemonControlEndpoint(options);
26
+ if (endpoint)
27
+ return endpoint;
28
+ throw new Error("Rynx daemon is unavailable; open Rynx App or start the standalone daemon, then retry");
29
+ }
30
+ async function readControlEndpoint() {
31
+ try {
32
+ const parsed = JSON.parse(await readFile(path.join(rynxHome(), "control.json"), "utf8"));
33
+ if (typeof parsed.origin !== "string" ||
34
+ typeof parsed.pid !== "number" ||
35
+ !Number.isInteger(parsed.pid) ||
36
+ parsed.pid <= 0 ||
37
+ typeof parsed.managementToken !== "string" ||
38
+ parsed.managementToken.length < 32 ||
39
+ parsed.managementToken.length > 256) {
40
+ return null;
41
+ }
42
+ const origin = new URL(parsed.origin);
43
+ if ((origin.protocol !== "http:" && origin.protocol !== "https:") ||
44
+ origin.username ||
45
+ origin.password ||
46
+ origin.search ||
47
+ origin.hash ||
48
+ !isLoopbackHostname(origin.hostname)) {
49
+ return null;
50
+ }
51
+ const endpoint = {
52
+ origin: origin.toString().replace(/\/$/, ""),
53
+ pid: parsed.pid,
54
+ managementToken: parsed.managementToken,
55
+ };
56
+ if (parsed.distribution === "app" || parsed.distribution === "standalone") {
57
+ endpoint.distribution = parsed.distribution;
58
+ }
59
+ if (boundedMetadata(parsed.productVersion))
60
+ endpoint.productVersion = parsed.productVersion;
61
+ if (boundedMetadata(parsed.buildId))
62
+ endpoint.buildId = parsed.buildId;
63
+ if (parsed.daemonLifecycle === "follow-app" ||
64
+ parsed.daemonLifecycle === "resident" ||
65
+ parsed.daemonLifecycle === "standalone") {
66
+ endpoint.daemonLifecycle = parsed.daemonLifecycle;
67
+ }
68
+ if (validManagementProtocol(parsed.managementProtocol)) {
69
+ endpoint.managementProtocol = parsed.managementProtocol;
70
+ }
71
+ return endpoint;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ function validManagementProtocol(value) {
78
+ if (!value || typeof value !== "object" || Array.isArray(value))
79
+ return false;
80
+ const record = value;
81
+ return (typeof record.current === "number" &&
82
+ Number.isInteger(record.current) &&
83
+ record.current > 0 &&
84
+ typeof record.minimumCompatible === "number" &&
85
+ Number.isInteger(record.minimumCompatible) &&
86
+ record.minimumCompatible > 0 &&
87
+ record.minimumCompatible <= record.current);
88
+ }
89
+ function boundedMetadata(value) {
90
+ return typeof value === "string"
91
+ && value.length > 0
92
+ && value.length <= 256
93
+ && value.trim() === value
94
+ && !/[\u0000-\u001f\u007f]/.test(value);
95
+ }
96
+ async function isHealthy(origin, signal) {
97
+ try {
98
+ const response = await fetch(`${origin}/health`, {
99
+ signal: signal
100
+ ? AbortSignal.any([signal, AbortSignal.timeout(PROBE_TIMEOUT_MS)])
101
+ : AbortSignal.timeout(PROBE_TIMEOUT_MS),
102
+ });
103
+ return response.ok;
104
+ }
105
+ catch {
106
+ signal?.throwIfAborted();
107
+ return false;
108
+ }
109
+ }
110
+ function isLoopbackHostname(hostname) {
111
+ return hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
112
+ }
113
+ function isProcessAlive(pid) {
114
+ try {
115
+ process.kill(pid, 0);
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
@@ -0,0 +1,44 @@
1
+ import { type DesktopBrowserHostCommandFrame, type DesktopBrowserHostErrorCode, type DesktopBrowserHostEvent, type DesktopBrowserHostLeaseFrame, type DesktopBrowserHostPresentation, type DesktopBrowserHostSurfaceCapability, type DesktopBrowserHostSurfaceFrame } from "@rynx-ai/protocol/desktop-browser-host";
2
+ export interface ResidentDesktopBrowserHostConnectOptions {
3
+ hostInstanceId: string;
4
+ /** Advertise only handlers that are installed in this main-process Host. */
5
+ capabilities: {
6
+ semanticPageBinding: boolean;
7
+ presentation: DesktopBrowserHostPresentation;
8
+ surface: DesktopBrowserHostSurfaceCapability;
9
+ };
10
+ /**
11
+ * Process-private endpoint supplied by the daemon that spawned an embedded
12
+ * Browser Host. It avoids publishing the management credential through argv
13
+ * or environment variables while the daemon endpoint is still starting.
14
+ */
15
+ endpoint?: {
16
+ origin: string;
17
+ managementToken: string;
18
+ };
19
+ signal?: AbortSignal;
20
+ }
21
+ export interface ResidentDesktopBrowserHostFailure {
22
+ code: DesktopBrowserHostErrorCode;
23
+ message: string;
24
+ }
25
+ /**
26
+ * Process-private lease used by Electron main. The management credential is
27
+ * resolved and consumed in this package and never returned to the app renderer.
28
+ */
29
+ export interface ResidentDesktopBrowserHostCommandRequest {
30
+ readonly frame: DesktopBrowserHostCommandFrame;
31
+ /** Aborts at the daemon-supplied absolute deadline or on lease loss. */
32
+ readonly signal: AbortSignal;
33
+ reply(result: unknown): Promise<void>;
34
+ reject(failure: ResidentDesktopBrowserHostFailure): Promise<void>;
35
+ }
36
+ export interface ResidentDesktopBrowserHostConnection extends AsyncIterable<ResidentDesktopBrowserHostCommandRequest> {
37
+ readonly lease: DesktopBrowserHostLeaseFrame;
38
+ readonly closed: Promise<void>;
39
+ emit(event: DesktopBrowserHostEvent): Promise<void>;
40
+ emitSurfaceFrame(frame: Omit<DesktopBrowserHostSurfaceFrame, "leaseId">): Promise<void>;
41
+ close(): Promise<void>;
42
+ }
43
+ /** Open the sole resident daemon's authenticated loopback Desktop Host lease. */
44
+ export declare function connectResidentDesktopBrowserHost(options: ResidentDesktopBrowserHostConnectOptions): Promise<ResidentDesktopBrowserHostConnection>;
@@ -0,0 +1,430 @@
1
+ import { DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS, DESKTOP_BROWSER_HOST_PATH, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION, encodeDesktopBrowserHostSurfaceFrame, parseDesktopBrowserHostClientFrame, parseDesktopBrowserHostServerFrame, } from "@rynx-ai/protocol/desktop-browser-host";
2
+ import { WebSocket } from "ws";
3
+ import { ensureDaemonControlEndpoint } from "./control-endpoint.js";
4
+ const CONNECT_TIMEOUT_MS = 5_000;
5
+ const MAX_BUFFERED_BYTES = DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES + DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES;
6
+ /** Open the sole resident daemon's authenticated loopback Desktop Host lease. */
7
+ export async function connectResidentDesktopBrowserHost(options) {
8
+ options.signal?.throwIfAborted();
9
+ const hello = parseDesktopBrowserHostClientFrame({
10
+ type: "desktop.browser.host.hello",
11
+ protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
12
+ hostInstanceId: options.hostInstanceId,
13
+ capabilities: {
14
+ semanticPageBinding: options.capabilities.semanticPageBinding,
15
+ presentation: options.capabilities.presentation,
16
+ surface: options.capabilities.surface,
17
+ },
18
+ });
19
+ if (hello.type !== "desktop.browser.host.hello") {
20
+ throw new Error("Desktop Browser Host hello parser returned the wrong frame");
21
+ }
22
+ const endpoint = options.endpoint
23
+ ?? await ensureDaemonControlEndpoint({ signal: options.signal });
24
+ assertDesktopBrowserHostEndpoint(endpoint);
25
+ const url = new URL(DESKTOP_BROWSER_HOST_PATH, `${endpoint.origin}/`);
26
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
27
+ const socket = new WebSocket(url, {
28
+ headers: { "x-rynx-management-token": endpoint.managementToken },
29
+ handshakeTimeout: CONNECT_TIMEOUT_MS,
30
+ maxPayload: DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES,
31
+ perMessageDeflate: false,
32
+ });
33
+ const queue = new BoundedCommandQueue(DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS);
34
+ let lease;
35
+ let leaseResolve;
36
+ let leaseReject;
37
+ const leaseReady = new Promise((resolve, reject) => {
38
+ leaseResolve = resolve;
39
+ leaseReject = reject;
40
+ });
41
+ let closedResolve;
42
+ const closed = new Promise((resolve) => {
43
+ closedResolve = resolve;
44
+ });
45
+ let terminalError;
46
+ let eventSequence = 0;
47
+ let closing = false;
48
+ let writerTail = Promise.resolve();
49
+ const activeRequests = new Map();
50
+ const abortActiveRequests = (error) => {
51
+ for (const request of activeRequests.values()) {
52
+ if (request.timer)
53
+ clearTimeout(request.timer);
54
+ request.settled = true;
55
+ request.controller.abort(error);
56
+ }
57
+ activeRequests.clear();
58
+ };
59
+ const fail = (error) => {
60
+ terminalError ??= error;
61
+ if (!lease)
62
+ leaseReject(error);
63
+ queue.close(error);
64
+ abortActiveRequests(error);
65
+ };
66
+ const failConnection = (error, code, reason) => {
67
+ fail(error);
68
+ if (socket.readyState < WebSocket.CLOSING)
69
+ socket.close(code, reason);
70
+ };
71
+ const enqueueWrite = (operation) => {
72
+ const current = writerTail.then(async () => {
73
+ if (terminalError)
74
+ throw terminalError;
75
+ await operation();
76
+ });
77
+ writerTail = current.catch(() => undefined);
78
+ return current.catch((error) => {
79
+ const failure = asError(error, "Desktop Browser Host output failed");
80
+ failConnection(failure, 1011, "host output failed");
81
+ throw failure;
82
+ });
83
+ };
84
+ const onAbort = () => {
85
+ const error = abortError(options.signal?.reason);
86
+ failConnection(error, 1000, "aborted");
87
+ };
88
+ options.signal?.addEventListener("abort", onAbort, { once: true });
89
+ socket.on("open", () => {
90
+ void enqueueWrite(() => sendJson(socket, hello));
91
+ });
92
+ socket.on("message", (raw, isBinary) => {
93
+ if (isBinary) {
94
+ failConnection(new Error("Desktop Browser Host daemon sent an unexpected binary frame"), 1003, "binary frame unsupported");
95
+ return;
96
+ }
97
+ let frame;
98
+ try {
99
+ const bytes = rawDataBytes(raw);
100
+ if (bytes.byteLength === 0 || bytes.byteLength > DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES) {
101
+ throw new Error("Desktop Browser Host daemon frame exceeded its bound");
102
+ }
103
+ frame = parseDesktopBrowserHostServerFrame(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
104
+ }
105
+ catch (error) {
106
+ failConnection(asError(error, "Desktop Browser Host daemon frame was invalid"), 1007, "invalid daemon frame");
107
+ return;
108
+ }
109
+ if (!lease) {
110
+ if (frame.type !== "desktop.browser.host.lease") {
111
+ failConnection(new Error("Desktop Browser Host daemon did not begin with a lease"), 1002, "lease required");
112
+ return;
113
+ }
114
+ if (frame.capabilities.semanticPageBinding &&
115
+ !options.capabilities.semanticPageBinding) {
116
+ failConnection(new Error("Desktop Browser Host daemon enabled an unadvertised Page binding capability"), 1002, "invalid lease capabilities");
117
+ return;
118
+ }
119
+ if (frame.capabilities.surface === "binary-v1" &&
120
+ options.capabilities.surface !== "binary-v1") {
121
+ failConnection(new Error("Desktop Browser Host daemon enabled an unadvertised Surface capability"), 1002, "invalid lease capabilities");
122
+ return;
123
+ }
124
+ lease = frame;
125
+ leaseResolve(frame);
126
+ return;
127
+ }
128
+ if (frame.type !== "desktop.browser.host.command" || frame.leaseId !== lease.leaseId) {
129
+ failConnection(new Error("Desktop Browser Host daemon sent a frame outside the active lease"), 1002, "lease mismatch");
130
+ return;
131
+ }
132
+ if (!queue.push(frame)) {
133
+ failConnection(new Error("Desktop Browser Host command capacity exceeded"), 1013, "command capacity exceeded");
134
+ }
135
+ });
136
+ socket.on("error", (error) => {
137
+ failConnection(new Error("Desktop Browser Host connection failed", { cause: error }), 1011, "connection failed");
138
+ });
139
+ socket.once("close", (code, reason) => {
140
+ options.signal?.removeEventListener("abort", onAbort);
141
+ if (!terminalError && !closing) {
142
+ terminalError = new Error(`Desktop Browser Host lease closed (${code})${reason.length > 0 ? `: ${reason.toString("utf8")}` : ""}`);
143
+ }
144
+ if (!lease && terminalError)
145
+ leaseReject(terminalError);
146
+ queue.close(terminalError);
147
+ abortActiveRequests(terminalError ?? new Error("Desktop Browser Host lease closed"));
148
+ closedResolve();
149
+ });
150
+ try {
151
+ lease = await abortableTimeout(leaseReady, CONNECT_TIMEOUT_MS, options.signal);
152
+ }
153
+ catch (error) {
154
+ if (socket.readyState < WebSocket.CLOSING)
155
+ socket.close(1000, "lease failed");
156
+ throw error;
157
+ }
158
+ const activeLease = lease;
159
+ const settle = async (command, value) => {
160
+ const request = activeRequests.get(command.commandId);
161
+ if (!request || request.settled) {
162
+ throw new Error("Desktop Browser Host command is already settled or stale");
163
+ }
164
+ if (command.leaseId !== activeLease.leaseId ||
165
+ request.controller.signal.aborted ||
166
+ Date.now() >= command.deadlineUnixMilliseconds) {
167
+ expireRequest(command, request);
168
+ throw new Error("Desktop Browser Host command deadline expired before settlement");
169
+ }
170
+ request.settled = true;
171
+ if (request.timer)
172
+ clearTimeout(request.timer);
173
+ activeRequests.delete(command.commandId);
174
+ await enqueueWrite(async () => {
175
+ const frame = parseDesktopBrowserHostClientFrame(value);
176
+ if (frame.type !== "desktop.browser.host.reply") {
177
+ throw new Error("Desktop Browser Host reply parser returned the wrong frame");
178
+ }
179
+ await sendJson(socket, frame);
180
+ });
181
+ };
182
+ const expireRequest = (command, request) => {
183
+ if (request.settled)
184
+ return;
185
+ request.settled = true;
186
+ if (request.timer)
187
+ clearTimeout(request.timer);
188
+ activeRequests.delete(command.commandId);
189
+ const error = new Error(`Desktop Browser Host ${command.command.method} deadline expired`);
190
+ request.controller.abort(error);
191
+ if (isMutatingCommand(command)) {
192
+ failConnection(error, 1000, "command deadline expired");
193
+ }
194
+ };
195
+ const requestIterator = queue[Symbol.asyncIterator]();
196
+ const claimedIterator = {
197
+ next: async () => {
198
+ const next = await requestIterator.next();
199
+ if (next.done)
200
+ return { done: true, value: undefined };
201
+ const command = next.value;
202
+ if (activeRequests.has(command.commandId)) {
203
+ const error = new Error("Desktop Browser Host daemon repeated a commandId");
204
+ failConnection(error, 1002, "duplicate command");
205
+ throw error;
206
+ }
207
+ const controller = new AbortController();
208
+ const remaining = command.deadlineUnixMilliseconds - Date.now();
209
+ const request = {
210
+ controller,
211
+ settled: false,
212
+ };
213
+ request.timer = setTimeout(() => expireRequest(command, request), Math.max(1, remaining));
214
+ request.timer.unref?.();
215
+ activeRequests.set(command.commandId, request);
216
+ if (remaining <= 0) {
217
+ expireRequest(command, request);
218
+ throw new Error("Desktop Browser Host received an expired command");
219
+ }
220
+ return {
221
+ done: false,
222
+ value: {
223
+ frame: command,
224
+ signal: controller.signal,
225
+ reply: (result) => settle(command, {
226
+ type: "desktop.browser.host.reply",
227
+ protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
228
+ leaseId: activeLease.leaseId,
229
+ commandId: command.commandId,
230
+ ok: true,
231
+ result,
232
+ }),
233
+ reject: (failure) => settle(command, {
234
+ type: "desktop.browser.host.reply",
235
+ protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
236
+ leaseId: activeLease.leaseId,
237
+ commandId: command.commandId,
238
+ ok: false,
239
+ error: failure,
240
+ }),
241
+ },
242
+ };
243
+ },
244
+ };
245
+ return {
246
+ lease: activeLease,
247
+ closed,
248
+ [Symbol.asyncIterator]: () => claimedIterator,
249
+ emit: (event) => enqueueWrite(async () => {
250
+ const nextSequence = eventSequence + 1;
251
+ const frame = parseDesktopBrowserHostClientFrame({
252
+ type: "desktop.browser.host.event",
253
+ protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
254
+ leaseId: activeLease.leaseId,
255
+ eventSequence: nextSequence,
256
+ event,
257
+ });
258
+ if (frame.type !== "desktop.browser.host.event") {
259
+ throw new Error("Desktop Browser Host event parser returned the wrong frame");
260
+ }
261
+ await sendJson(socket, frame);
262
+ eventSequence = nextSequence;
263
+ }),
264
+ emitSurfaceFrame: (frame) => enqueueWrite(async () => {
265
+ if (activeLease.capabilities.surface !== "binary-v1") {
266
+ throw new Error("Desktop Browser Host Surface was not negotiated");
267
+ }
268
+ const encoded = encodeDesktopBrowserHostSurfaceFrame({
269
+ ...frame,
270
+ leaseId: activeLease.leaseId,
271
+ });
272
+ await sendBinary(socket, encoded);
273
+ }),
274
+ close: async () => {
275
+ if (closing)
276
+ return closed;
277
+ closing = true;
278
+ queue.close();
279
+ abortActiveRequests(new Error("Desktop Browser Host stopped"));
280
+ if (socket.readyState < WebSocket.CLOSING)
281
+ socket.close(1000, "host stopped");
282
+ if (socket.readyState === WebSocket.CLOSED)
283
+ closedResolve();
284
+ await closed;
285
+ },
286
+ };
287
+ }
288
+ async function sendBinary(socket, bytes) {
289
+ if (socket.readyState !== WebSocket.OPEN)
290
+ throw new Error("Desktop Browser Host lease is closed");
291
+ if (bytes.byteLength === 0 || bytes.byteLength > DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES) {
292
+ throw new Error("Desktop Browser Host binary frame exceeded its bound");
293
+ }
294
+ if (socket.bufferedAmount + bytes.byteLength > MAX_BUFFERED_BYTES) {
295
+ throw new Error("Desktop Browser Host output buffer exceeded its bound");
296
+ }
297
+ await new Promise((resolve, reject) => {
298
+ socket.send(bytes, { binary: true, compress: false }, (error) => {
299
+ if (error)
300
+ reject(error);
301
+ else
302
+ resolve();
303
+ });
304
+ });
305
+ }
306
+ async function sendJson(socket, value) {
307
+ if (socket.readyState !== WebSocket.OPEN)
308
+ throw new Error("Desktop Browser Host lease is closed");
309
+ const bytes = Buffer.from(JSON.stringify(value), "utf8");
310
+ if (bytes.byteLength === 0 || bytes.byteLength > DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES) {
311
+ throw new Error("Desktop Browser Host client frame exceeded its bound");
312
+ }
313
+ if (socket.bufferedAmount + bytes.byteLength > MAX_BUFFERED_BYTES) {
314
+ throw new Error("Desktop Browser Host output buffer exceeded its bound");
315
+ }
316
+ await new Promise((resolve, reject) => {
317
+ socket.send(bytes, { binary: false, compress: false }, (error) => {
318
+ if (error)
319
+ reject(error);
320
+ else
321
+ resolve();
322
+ });
323
+ });
324
+ }
325
+ class BoundedCommandQueue {
326
+ capacity;
327
+ values = [];
328
+ waiters = [];
329
+ ended = false;
330
+ error;
331
+ constructor(capacity) {
332
+ this.capacity = capacity;
333
+ }
334
+ push(value) {
335
+ if (this.ended)
336
+ return false;
337
+ const waiter = this.waiters.shift();
338
+ if (waiter) {
339
+ waiter.resolve({ done: false, value });
340
+ return true;
341
+ }
342
+ if (this.values.length >= this.capacity)
343
+ return false;
344
+ this.values.push(value);
345
+ return true;
346
+ }
347
+ close(error) {
348
+ if (this.ended)
349
+ return;
350
+ this.ended = true;
351
+ this.error = error;
352
+ // Commands are capabilities of this exact socket lease. Never drain a
353
+ // buffered command after abort/disconnect, even if it was parsed earlier.
354
+ this.values.length = 0;
355
+ for (const waiter of this.waiters.splice(0)) {
356
+ if (error)
357
+ waiter.reject(error);
358
+ else
359
+ waiter.resolve({ done: true, value: undefined });
360
+ }
361
+ }
362
+ [Symbol.asyncIterator]() {
363
+ return {
364
+ next: () => {
365
+ const value = this.values.shift();
366
+ if (value)
367
+ return Promise.resolve({ done: false, value });
368
+ if (this.ended) {
369
+ return this.error
370
+ ? Promise.reject(this.error)
371
+ : Promise.resolve({ done: true, value: undefined });
372
+ }
373
+ return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
374
+ },
375
+ };
376
+ }
377
+ }
378
+ function rawDataBytes(data) {
379
+ if (Buffer.isBuffer(data))
380
+ return data;
381
+ if (data instanceof ArrayBuffer)
382
+ return new Uint8Array(data);
383
+ if (Array.isArray(data))
384
+ return Buffer.concat(data);
385
+ throw new Error("Desktop Browser Host received an unsupported WebSocket payload");
386
+ }
387
+ function isMutatingCommand(command) {
388
+ return command.command.method !== "browser.snapshot" &&
389
+ command.command.method !== "browser.cdp-endpoint" &&
390
+ command.command.method !== "page.cdp-endpoint";
391
+ }
392
+ async function abortableTimeout(promise, timeoutMs, signal) {
393
+ signal?.throwIfAborted();
394
+ const timeout = AbortSignal.timeout(timeoutMs);
395
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
396
+ return await new Promise((resolve, reject) => {
397
+ const onAbort = () => reject(abortError(combined.reason));
398
+ combined.addEventListener("abort", onAbort, { once: true });
399
+ promise.then(resolve, reject).finally(() => combined.removeEventListener("abort", onAbort));
400
+ });
401
+ }
402
+ function abortError(reason) {
403
+ if (reason instanceof Error)
404
+ return reason;
405
+ return new Error("Desktop Browser Host connection was aborted", { cause: reason });
406
+ }
407
+ function asError(error, message) {
408
+ return error instanceof Error ? error : new Error(message, { cause: error });
409
+ }
410
+ function assertDesktopBrowserHostEndpoint(endpoint) {
411
+ if (endpoint.managementToken.length < 32 ||
412
+ endpoint.managementToken.length > 256 ||
413
+ endpoint.managementToken.trim() !== endpoint.managementToken ||
414
+ /[\u0000-\u001f\u007f]/.test(endpoint.managementToken)) {
415
+ throw new Error("resident daemon Desktop Browser Host credential is invalid");
416
+ }
417
+ const origin = endpoint.origin;
418
+ const url = new URL(origin);
419
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
420
+ if ((url.protocol !== "http:" && url.protocol !== "https:") ||
421
+ url.username ||
422
+ url.password ||
423
+ url.pathname !== "/" ||
424
+ url.search ||
425
+ url.hash ||
426
+ url.origin !== origin ||
427
+ (host !== "localhost" && host !== "127.0.0.1" && host !== "::1")) {
428
+ throw new Error("resident daemon Desktop Browser Host endpoint is not loopback");
429
+ }
430
+ }
@@ -0,0 +1,2 @@
1
+ export { runCli } from "./run-cli.js";
2
+ export { USAGE } from "./usage.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { runCli } from "./run-cli.js";
2
+ export { USAGE } from "./usage.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Temporary bridge for commands whose daemon-owned mutation has no management
3
+ * RPC yet. It crosses the package boundary as a subprocess, never by importing
4
+ * daemon implementation modules into the CLI process.
5
+ */
6
+ export declare function runLegacyDaemonCli(args: readonly string[]): Promise<number>;
7
+ export declare function resolveLegacyDaemonCli(env?: NodeJS.ProcessEnv, moduleUrl?: string): string | null;