@ai-matrx/desktop-protocol 0.1.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.
@@ -0,0 +1,1309 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ DesktopProtocolError: () => DesktopProtocolError,
24
+ RECONNECT_ALARM_ATTEMPTS: () => import_realtime.RECONNECT_ALARM_ATTEMPTS,
25
+ createDesktopClient: () => createDesktopClient,
26
+ isDesktopProtocolError: () => isDesktopProtocolError
27
+ });
28
+ module.exports = __toCommonJS(client_exports);
29
+
30
+ // src/client/client.ts
31
+ var import_realtime = require("@ai-matrx/realtime");
32
+
33
+ // src/error.ts
34
+ var DesktopProtocolError = class extends Error {
35
+ code;
36
+ retryable;
37
+ data;
38
+ constructor(code, message, options = {}) {
39
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
40
+ this.name = "DesktopProtocolError";
41
+ this.code = code;
42
+ this.retryable = options.retryable ?? false;
43
+ this.data = options.data;
44
+ }
45
+ /** The wire body (ProtocolErrorBody) — what a core or relay sends for this error. */
46
+ toBody() {
47
+ return this.data === void 0 ? { code: this.code, message: this.message, retryable: this.retryable } : { code: this.code, message: this.message, retryable: this.retryable, data: this.data };
48
+ }
49
+ };
50
+ function isDesktopProtocolError(value) {
51
+ return value instanceof DesktopProtocolError || value instanceof Error && value.name === "DesktopProtocolError" && "code" in value;
52
+ }
53
+
54
+ // src/frame.ts
55
+ var FRAME_HEADER_BYTES = 16;
56
+ var FRAME_VERSION = 1;
57
+ var FrameChannel = {
58
+ /** core→client: pty output, fs.read_stream bytes */
59
+ DATA: 0,
60
+ /** core→client: stderr of a non-pty process (reserved for exec.spawn in v1.1) */
61
+ STDERR: 1,
62
+ /** client→core: keystrokes / stdin to sid */
63
+ STDIN: 2,
64
+ /** core→client: serialized terminal screen (attach when since_seq is outside the ring) */
65
+ SNAPSHOT: 3,
66
+ /** relay↔device hop only: a JSON envelope carried inside a binary frame so the relay can route
67
+ * by cid without parsing JSON. Never seen by clients. */
68
+ ENVELOPE: 255
69
+ };
70
+ var U32_MAX = 4294967295;
71
+ var CHANNELS = new Set(Object.values(FrameChannel));
72
+ function frameError(message) {
73
+ return new DesktopProtocolError("PROTOCOL_ERROR", message);
74
+ }
75
+ function checkU32(name, value) {
76
+ if (!Number.isInteger(value) || value < 0 || value > U32_MAX) throw frameError(`frame ${name} is not a u32: ${value}`);
77
+ }
78
+ function assertFrameHeader(h) {
79
+ if (h.version !== FRAME_VERSION) throw frameError(`unsupported frame version ${String(h.version)}`);
80
+ if (!Number.isInteger(h.flags) || h.flags < 0 || h.flags > 3) throw frameError(`reserved frame flag bits set: ${h.flags}`);
81
+ if (!CHANNELS.has(h.channel)) throw frameError(`unknown frame channel ${String(h.channel)}`);
82
+ checkU32("cid", h.cid);
83
+ checkU32("sid", h.sid);
84
+ checkU32("seq", h.seq);
85
+ }
86
+ function encodeFrame(h, payload) {
87
+ assertFrameHeader(h);
88
+ const out = new Uint8Array(FRAME_HEADER_BYTES + payload.byteLength);
89
+ const v = new DataView(out.buffer);
90
+ v.setUint8(0, h.version);
91
+ v.setUint8(1, h.flags);
92
+ v.setUint8(2, h.channel);
93
+ v.setUint8(3, 0);
94
+ v.setUint32(4, h.cid);
95
+ v.setUint32(8, h.sid);
96
+ v.setUint32(12, h.seq);
97
+ out.set(payload, FRAME_HEADER_BYTES);
98
+ return out;
99
+ }
100
+ function decodeFrame(buf) {
101
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
102
+ if (bytes.byteLength < FRAME_HEADER_BYTES) throw frameError(`short frame: ${bytes.byteLength} bytes`);
103
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
104
+ if (v.getUint8(3) !== 0) throw frameError("reserved frame byte 3 is not 0");
105
+ const header = {
106
+ version: v.getUint8(0),
107
+ flags: v.getUint8(1),
108
+ channel: v.getUint8(2),
109
+ cid: v.getUint32(4),
110
+ sid: v.getUint32(8),
111
+ seq: v.getUint32(12)
112
+ };
113
+ assertFrameHeader(header);
114
+ return { header, payload: bytes.subarray(FRAME_HEADER_BYTES) };
115
+ }
116
+
117
+ // src/schema/envelope.ts
118
+ var import_zod3 = require("zod");
119
+
120
+ // src/schema/errors.ts
121
+ var import_zod2 = require("zod");
122
+
123
+ // src/schema/primitives.ts
124
+ var import_zod = require("zod");
125
+ var PROTOCOL_MAJOR = 1;
126
+ var PROTOCOL_MINOR = 0;
127
+ var ProtocolVersion = import_zod.z.string().regex(/^[1-9][0-9]{0,2}\.[0-9]{1,3}$/);
128
+ var RequestId = import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
129
+ var ResourceId = import_zod.z.string().regex(/^(pty|watch)_[A-Za-z0-9]{16,40}$/);
130
+ var DeviceId = import_zod.z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
131
+ var AbsPath = import_zod.z.string().min(1).max(4096).regex(/^(\/|[A-Za-z]:\/)[^\\\u0000]*$/);
132
+ var u32 = import_zod.z.int().min(0).max(4294967295);
133
+ var nonNegInt = import_zod.z.int().min(0);
134
+ var epochMs = import_zod.z.int().min(0);
135
+
136
+ // src/schema/errors.ts
137
+ var ErrorCode = import_zod2.z.enum([
138
+ "AUTH_REQUIRED",
139
+ "AUTH_INVALID",
140
+ "AUTH_EXPIRED",
141
+ "AUTH_FORBIDDEN",
142
+ // signed in, but not this device's owner
143
+ "AUTH_DEVICE_REVOKED",
144
+ // kill switch
145
+ "PERMISSION_DENIED_OS",
146
+ "NOT_FOUND",
147
+ "CONFLICT",
148
+ "INVALID_PARAMS",
149
+ "RESOURCE_BUSY",
150
+ "RESOURCE_GONE",
151
+ "CANCELLED",
152
+ "TIMEOUT",
153
+ "PLATFORM_UNSUPPORTED",
154
+ "RATE_LIMITED",
155
+ "INTERNAL",
156
+ "PROTOCOL_VERSION_UNSUPPORTED",
157
+ // NEW: hello refused, carries data.upgrade
158
+ "PROTOCOL_ERROR",
159
+ // NEW: malformed envelope/frame, credit overrun, unknown sid
160
+ "UNKNOWN_OPERATION",
161
+ // NEW: cap.op not served by this core version
162
+ "DEVICE_OFFLINE",
163
+ // NEW: relay has no live device socket (retryable)
164
+ "PAYLOAD_TOO_LARGE"
165
+ // NEW: message or frame above limits.max_*_bytes
166
+ ]);
167
+ var ErrorReason = import_zod2.z.enum([
168
+ "not_a_directory",
169
+ "is_a_directory",
170
+ "directory_not_empty",
171
+ "already_exists",
172
+ "stale_write",
173
+ // fs.write if_match_mtime_ms mismatch → CONFLICT
174
+ "binary_content",
175
+ // utf8 read of non-UTF-8 bytes → INVALID_PARAMS
176
+ "not_utf8_boundary",
177
+ "patch_no_match",
178
+ "patch_ambiguous",
179
+ "resource_exited",
180
+ "resource_expired",
181
+ "control_taken",
182
+ // another client of the same user took control
183
+ "window_exhausted"
184
+ ]);
185
+ var ProtocolErrorBody = import_zod2.z.strictObject({
186
+ code: ErrorCode,
187
+ message: import_zod2.z.string().max(300),
188
+ // short, human, never a stack trace or a token
189
+ retryable: import_zod2.z.boolean(),
190
+ data: import_zod2.z.strictObject({
191
+ reason: ErrorReason.optional(),
192
+ cap: import_zod2.z.string().max(32).optional(),
193
+ op: import_zod2.z.string().max(64).optional(),
194
+ path: AbsPath.optional(),
195
+ settings_url: import_zod2.z.string().max(512).optional(),
196
+ // PERMISSION_DENIED_OS deep link
197
+ retry_after_ms: nonNegInt.optional(),
198
+ limit_bytes: nonNegInt.optional(),
199
+ upgrade: import_zod2.z.strictObject({
200
+ min_version: ProtocolVersion,
201
+ max_version: ProtocolVersion,
202
+ download_url: import_zod2.z.url().optional()
203
+ }).optional()
204
+ }).optional()
205
+ });
206
+
207
+ // src/schema/envelope.ts
208
+ var ClientType = import_zod3.z.enum(["screen", "web", "extension", "server", "test"]);
209
+ var Limits = import_zod3.z.strictObject({
210
+ max_message_bytes: import_zod3.z.int().min(65536),
211
+ max_frame_payload_bytes: import_zod3.z.int().min(4096),
212
+ default_window_bytes: import_zod3.z.int().min(4096),
213
+ max_streams_per_client: import_zod3.z.int().min(1),
214
+ max_resources_per_user: import_zod3.z.int().min(1)
215
+ });
216
+ var OpDescriptor = import_zod3.z.strictObject({ cap: import_zod3.z.string().max(32), op: import_zod3.z.string().max(64), since: ProtocolVersion });
217
+ var Welcome = import_zod3.z.strictObject({
218
+ protocol_version: ProtocolVersion,
219
+ // negotiated = min(client, core) within the same major
220
+ session_id: import_zod3.z.string().min(8).max(64),
221
+ client_id: import_zod3.z.string().min(8).max(64),
222
+ // this connection's identity in the Session Manager
223
+ device_id: DeviceId,
224
+ device_name: import_zod3.z.string().max(128),
225
+ app_version: import_zod3.z.string().max(32),
226
+ platform: import_zod3.z.enum(["darwin", "win32", "linux"]),
227
+ ops: import_zod3.z.array(OpDescriptor),
228
+ limits: Limits,
229
+ server_time_ms: epochMs
230
+ });
231
+ var HelloMsg = import_zod3.z.strictObject({
232
+ type: import_zod3.z.literal("hello"),
233
+ id: RequestId,
234
+ // answered by response{id, result: Welcome} or error{id}
235
+ protocolVersion: ProtocolVersion,
236
+ clientType: ClientType,
237
+ clientVersion: import_zod3.z.string().min(1).max(32)
238
+ });
239
+ var RequestMsg = import_zod3.z.strictObject({
240
+ id: RequestId,
241
+ type: import_zod3.z.literal("request"),
242
+ cap: import_zod3.z.string().min(1).max(32),
243
+ op: import_zod3.z.string().min(1).max(64),
244
+ params: import_zod3.z.unknown()
245
+ // validated by the Router against OPS[cap.op].params — once, there
246
+ });
247
+ var ResponseMsg = import_zod3.z.strictObject({ id: RequestId, type: import_zod3.z.literal("response"), result: import_zod3.z.unknown() });
248
+ var ErrorMsg = import_zod3.z.strictObject({ id: RequestId, type: import_zod3.z.literal("error"), error: ProtocolErrorBody });
249
+ var StreamMeta = import_zod3.z.discriminatedUnion("kind", [
250
+ /** Binds request id → numeric sid; binary frames for this stream follow. */
251
+ import_zod3.z.strictObject({
252
+ kind: import_zod3.z.literal("open"),
253
+ sid: u32,
254
+ window_bytes: import_zod3.z.int().min(4096),
255
+ resource_id: ResourceId.optional()
256
+ }),
257
+ /** Frames after this (channel SNAPSHOT) are a serialized screen; live DATA resumes after. */
258
+ import_zod3.z.strictObject({ kind: import_zod3.z.literal("snapshot"), cols: import_zod3.z.int().min(1), rows: import_zod3.z.int().min(1) }),
259
+ /** Frames [from_seq, to_seq] are gone from the ring; a snapshot follows. */
260
+ import_zod3.z.strictObject({ kind: import_zod3.z.literal("gap"), from_seq: u32, to_seq: u32 })
261
+ ]);
262
+ var StreamMsg = import_zod3.z.strictObject({ id: RequestId, type: import_zod3.z.literal("stream"), seq: u32, meta: StreamMeta });
263
+ var EndReason = import_zod3.z.enum(["completed", "cancelled", "detached", "resource_exited", "error"]);
264
+ var EndMsg = import_zod3.z.strictObject({ id: RequestId, type: import_zod3.z.literal("end"), seq: u32, reason: EndReason.optional() });
265
+ var CancelMsg = import_zod3.z.strictObject({ id: RequestId, type: import_zod3.z.literal("cancel") });
266
+ var CreditMsg = import_zod3.z.strictObject({ id: RequestId, type: import_zod3.z.literal("credit"), n: import_zod3.z.int().min(1).max(2147483647) });
267
+ var EventMsg = import_zod3.z.strictObject({
268
+ type: import_zod3.z.literal("event"),
269
+ cap: import_zod3.z.string().min(1).max(32),
270
+ name: import_zod3.z.string().min(1).max(64),
271
+ payload: import_zod3.z.unknown(),
272
+ // validated against EVENTS[cap.name]
273
+ resourceId: ResourceId.optional()
274
+ });
275
+ var RelayMsg = import_zod3.z.discriminatedUnion("op", [
276
+ /** Fresh access token before the current one expires (relay closes 4008 at exp + 60 s). */
277
+ import_zod3.z.strictObject({ type: import_zod3.z.literal("relay"), op: import_zod3.z.literal("reauth"), token: import_zod3.z.string().min(16).max(8192) })
278
+ ]);
279
+ var PING_TEXT = '{"type":"ping"}';
280
+ var PONG_TEXT = '{"type":"pong"}';
281
+ var ClientToCoreMsg = import_zod3.z.discriminatedUnion("type", [HelloMsg, RequestMsg, CancelMsg, CreditMsg]);
282
+ var CoreToClientMsg = import_zod3.z.discriminatedUnion("type", [ResponseMsg, ErrorMsg, StreamMsg, EndMsg, CreditMsg, EventMsg]);
283
+ var CloseCode = {
284
+ NORMAL: 1e3,
285
+ GOING_AWAY: 1001,
286
+ // relay deploy / app quit — reconnect immediately
287
+ AUTH_INVALID: 4001,
288
+ AUTH_FORBIDDEN: 4003,
289
+ DEVICE_NOT_FOUND: 4004,
290
+ AUTH_EXPIRED: 4008,
291
+ // no reauth before exp + 60 s
292
+ REPLACED: 4009,
293
+ // a newer device socket for the same device took over
294
+ DEVICE_REVOKED: 4010,
295
+ // kill switch — do NOT reconnect
296
+ PROTOCOL_ERROR: 4400,
297
+ UPGRADE_REQUIRED: 4426
298
+ };
299
+
300
+ // src/schema/frame.ts
301
+ var import_zod4 = require("zod");
302
+ var FrameHeader = import_zod4.z.strictObject({
303
+ version: import_zod4.z.literal(FRAME_VERSION),
304
+ flags: import_zod4.z.int().min(0).max(3),
305
+ channel: import_zod4.z.union([import_zod4.z.literal(0), import_zod4.z.literal(1), import_zod4.z.literal(2), import_zod4.z.literal(3), import_zod4.z.literal(255)]),
306
+ cid: u32,
307
+ sid: u32,
308
+ seq: u32
309
+ });
310
+ var DEFAULT_WINDOW_BYTES = 262144;
311
+ var DEFAULT_MAX_FRAME_PAYLOAD_BYTES = 65536;
312
+
313
+ // src/schema/sysinfo.ts
314
+ var import_zod5 = require("zod");
315
+ var OsPermissionName = import_zod5.z.enum([
316
+ "full_disk_access",
317
+ "accessibility",
318
+ "screen_recording",
319
+ "microphone",
320
+ "camera",
321
+ "automation",
322
+ "contacts",
323
+ "calendars",
324
+ "photos",
325
+ "location",
326
+ "local_network",
327
+ "bluetooth",
328
+ "notifications"
329
+ ]);
330
+ var SysinfoGetParams = import_zod5.z.strictObject({});
331
+ var SysinfoGetResult = import_zod5.z.strictObject({
332
+ device_id: DeviceId,
333
+ device_name: import_zod5.z.string().max(128),
334
+ hostname: import_zod5.z.string().max(255),
335
+ platform: import_zod5.z.enum(["darwin", "win32", "linux"]),
336
+ os_version: import_zod5.z.string().max(64),
337
+ arch: import_zod5.z.enum(["arm64", "x64"]),
338
+ app_version: import_zod5.z.string().max(32),
339
+ protocol_version: ProtocolVersion,
340
+ user: import_zod5.z.strictObject({ username: import_zod5.z.string().max(128), shell: import_zod5.z.string().max(512) }),
341
+ paths: import_zod5.z.strictObject({
342
+ home: AbsPath,
343
+ desktop: AbsPath,
344
+ documents: AbsPath,
345
+ downloads: AbsPath,
346
+ app_data: AbsPath,
347
+ temp: AbsPath
348
+ }),
349
+ cpu: import_zod5.z.strictObject({ model: import_zod5.z.string().max(128), cores: import_zod5.z.int().min(1) }),
350
+ memory: import_zod5.z.strictObject({ total_bytes: nonNegInt, free_bytes: nonNegInt }),
351
+ uptime_seconds: nonNegInt,
352
+ on_battery: import_zod5.z.boolean().nullable(),
353
+ sleep_prevented: import_zod5.z.boolean(),
354
+ // true while any remote session holds the power blocker
355
+ permissions: import_zod5.z.array(
356
+ import_zod5.z.strictObject({
357
+ name: OsPermissionName,
358
+ state: import_zod5.z.enum(["granted", "denied", "not_determined", "not_applicable"]),
359
+ settings_url: import_zod5.z.string().max(512).nullable()
360
+ })
361
+ )
362
+ });
363
+
364
+ // src/schema/fs.ts
365
+ var import_zod6 = require("zod");
366
+ var FsKind = import_zod6.z.enum(["file", "dir", "symlink"]);
367
+ var FsEntry = import_zod6.z.strictObject({
368
+ name: import_zod6.z.string().max(1024),
369
+ path: AbsPath,
370
+ kind: FsKind,
371
+ size: nonNegInt,
372
+ mtime: import_zod6.z.number().min(0),
373
+ // seconds, float — daemon parity
374
+ mtime_ms: epochMs,
375
+ mode: import_zod6.z.int().min(0).max(4095),
376
+ target: AbsPath.nullable(),
377
+ // symlink target, never followed
378
+ hidden: import_zod6.z.boolean()
379
+ });
380
+ var FsListParams = import_zod6.z.strictObject({
381
+ path: AbsPath,
382
+ recursive: import_zod6.z.boolean().default(false),
383
+ depth: import_zod6.z.int().min(1).max(32).default(1),
384
+ pattern: import_zod6.z.string().max(256).optional(),
385
+ // glob on name
386
+ limit: import_zod6.z.int().min(1).max(5e3).default(500),
387
+ page_token: import_zod6.z.string().max(256).optional(),
388
+ show_hidden: import_zod6.z.boolean().default(false),
389
+ sort: import_zod6.z.enum(["name", "mtime", "size", "kind"]).default("kind")
390
+ });
391
+ var FsListResult = import_zod6.z.strictObject({
392
+ entries: import_zod6.z.array(FsEntry),
393
+ truncated: import_zod6.z.boolean(),
394
+ next_page_token: import_zod6.z.string().max(256).nullable(),
395
+ scanned: nonNegInt
396
+ });
397
+ var FsStatParams = import_zod6.z.strictObject({ path: AbsPath });
398
+ var FsStatResult = FsEntry;
399
+ var FsEncoding = import_zod6.z.enum(["utf8", "base64"]);
400
+ var FsReadParams = import_zod6.z.strictObject({
401
+ path: AbsPath,
402
+ encoding: FsEncoding.default("utf8"),
403
+ offset: nonNegInt.default(0),
404
+ limit: import_zod6.z.int().min(1).max(4194304).default(262144)
405
+ });
406
+ var FsReadResult = import_zod6.z.strictObject({
407
+ content: import_zod6.z.string(),
408
+ encoding: FsEncoding,
409
+ size: nonNegInt,
410
+ offset: nonNegInt,
411
+ next_offset: nonNegInt,
412
+ truncated: import_zod6.z.boolean(),
413
+ mime: import_zod6.z.string().max(128).nullable(),
414
+ mtime_ms: epochMs
415
+ // pass back as if_match_mtime_ms to guard a later write
416
+ });
417
+ var FsReadStreamParams = import_zod6.z.strictObject({
418
+ path: AbsPath,
419
+ offset: nonNegInt.default(0),
420
+ length: nonNegInt.optional(),
421
+ // absent = to EOF
422
+ window_bytes: import_zod6.z.int().min(4096).max(16777216).optional()
423
+ });
424
+ var FsReadStreamResult = import_zod6.z.strictObject({ size: nonNegInt, mime: import_zod6.z.string().max(128).nullable(), mtime_ms: epochMs });
425
+ var FsWriteParams = import_zod6.z.strictObject({
426
+ path: AbsPath,
427
+ content: import_zod6.z.string().max(5592406),
428
+ // ≤ 4 MiB of bytes after base64 decode
429
+ encoding: FsEncoding.default("utf8"),
430
+ mode: import_zod6.z.int().min(0).max(4095).optional(),
431
+ create_parents: import_zod6.z.boolean().default(false),
432
+ /** Optimistic concurrency: CONFLICT/stale_write if the file changed since this mtime. */
433
+ if_match_mtime_ms: epochMs.optional(),
434
+ /** Refuse to replace an existing file (CONFLICT/already_exists). */
435
+ exclusive: import_zod6.z.boolean().default(false)
436
+ });
437
+ var FsWriteResult = import_zod6.z.strictObject({ path: AbsPath, size: nonNegInt, mtime_ms: epochMs, created: import_zod6.z.boolean() });
438
+ var FsMkdirParams = import_zod6.z.strictObject({ path: AbsPath, parents: import_zod6.z.boolean().default(false) });
439
+ var FsMkdirResult = import_zod6.z.strictObject({ path: AbsPath, created: import_zod6.z.boolean() });
440
+ var FsMoveParams = import_zod6.z.strictObject({ from_path: AbsPath, to_path: AbsPath, overwrite: import_zod6.z.boolean().default(false) });
441
+ var FsMoveResult = import_zod6.z.strictObject({ from_path: AbsPath, to_path: AbsPath });
442
+ var FsDeleteParams = import_zod6.z.strictObject({
443
+ path: AbsPath,
444
+ recursive: import_zod6.z.boolean().default(false),
445
+ to_trash: import_zod6.z.boolean().default(true)
446
+ });
447
+ var FsDeleteResult = import_zod6.z.strictObject({ deleted: import_zod6.z.literal(true), trashed_to: AbsPath.nullable() });
448
+ var FsWatchParams = import_zod6.z.strictObject({
449
+ path: AbsPath,
450
+ recursive: import_zod6.z.boolean().default(false),
451
+ ignore: import_zod6.z.array(import_zod6.z.string().max(256)).max(64).default([]),
452
+ debounce_ms: import_zod6.z.int().min(0).max(5e3).default(50)
453
+ });
454
+ var FsWatchResult = import_zod6.z.strictObject({ resource_id: ResourceId, path: AbsPath });
455
+ var FsPatchParams = import_zod6.z.strictObject({
456
+ path: AbsPath,
457
+ edits: import_zod6.z.array(
458
+ import_zod6.z.strictObject({
459
+ old_text: import_zod6.z.string(),
460
+ new_text: import_zod6.z.string(),
461
+ replace_all: import_zod6.z.boolean().default(false)
462
+ })
463
+ ).min(1).max(256),
464
+ create_if_missing: import_zod6.z.boolean().default(false)
465
+ });
466
+ var FsPatchResult = import_zod6.z.strictObject({ path: AbsPath, applied: nonNegInt, size: nonNegInt, mtime_ms: epochMs });
467
+ var FsSearchPathsParams = import_zod6.z.strictObject({
468
+ pattern: import_zod6.z.string().min(1).max(512),
469
+ cwd: AbsPath,
470
+ hidden: import_zod6.z.boolean().default(false),
471
+ max_results: import_zod6.z.int().min(1).max(5e3).default(100)
472
+ });
473
+ var FsSearchPathsResult = import_zod6.z.strictObject({ paths: import_zod6.z.array(AbsPath), truncated: import_zod6.z.boolean() });
474
+ var FsSearchContentParams = import_zod6.z.strictObject({
475
+ query: import_zod6.z.string().min(1).max(1024),
476
+ cwd: AbsPath,
477
+ regex: import_zod6.z.boolean().default(true),
478
+ case_sensitive: import_zod6.z.boolean().default(false),
479
+ include: import_zod6.z.array(import_zod6.z.string().max(256)).max(64).optional(),
480
+ exclude: import_zod6.z.array(import_zod6.z.string().max(256)).max(64).optional(),
481
+ max_results: import_zod6.z.int().min(1).max(5e3).default(100)
482
+ });
483
+ var FsSearchContentResult = import_zod6.z.strictObject({
484
+ results: import_zod6.z.array(
485
+ import_zod6.z.strictObject({
486
+ path: AbsPath,
487
+ lines: import_zod6.z.string(),
488
+ line_number: import_zod6.z.int().min(1),
489
+ submatches: import_zod6.z.array(import_zod6.z.strictObject({ match: import_zod6.z.string(), start: nonNegInt, end: nonNegInt }))
490
+ })
491
+ ),
492
+ truncated: import_zod6.z.boolean()
493
+ });
494
+ var FsChange = import_zod6.z.strictObject({
495
+ type: import_zod6.z.enum(["created", "modified", "deleted", "moved"]),
496
+ path: AbsPath,
497
+ to_path: AbsPath.optional(),
498
+ kind: import_zod6.z.enum(["file", "dir", "symlink", "unknown"])
499
+ });
500
+ var FsChangedEvent = import_zod6.z.strictObject({ changes: import_zod6.z.array(FsChange).min(1).max(1e3), overflow: import_zod6.z.boolean() });
501
+
502
+ // src/schema/exec.ts
503
+ var import_zod7 = require("zod");
504
+ var Signal = import_zod7.z.enum(["SIGINT", "SIGTERM", "SIGKILL", "SIGHUP", "SIGQUIT", "SIGTSTP", "SIGCONT", "SIGUSR1", "SIGUSR2"]);
505
+ var cols = import_zod7.z.int().min(2).max(1e3);
506
+ var rows = import_zod7.z.int().min(1).max(500);
507
+ var ExecPtyStartParams = import_zod7.z.strictObject({
508
+ cols,
509
+ rows,
510
+ cwd: AbsPath.optional(),
511
+ // default: home
512
+ shell: import_zod7.z.string().max(512).optional(),
513
+ // default: user's login shell (PowerShell on Windows)
514
+ args: import_zod7.z.array(import_zod7.z.string().max(4096)).max(64).optional(),
515
+ env: import_zod7.z.record(import_zod7.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(128), import_zod7.z.string().max(32768)).optional(),
516
+ name: import_zod7.z.string().max(64).optional(),
517
+ // tab label
518
+ window_bytes: import_zod7.z.int().min(4096).max(16777216).optional()
519
+ });
520
+ var PtyInfo = import_zod7.z.strictObject({
521
+ resource_id: ResourceId,
522
+ pid: import_zod7.z.int().min(1),
523
+ shell: import_zod7.z.string().max(512),
524
+ cwd: AbsPath,
525
+ // live, from OSC 7 / process cwd
526
+ title: import_zod7.z.string().max(256),
527
+ name: import_zod7.z.string().max(64).nullable(),
528
+ cols,
529
+ rows,
530
+ state: import_zod7.z.enum(["running", "exited"]),
531
+ exit_code: import_zod7.z.int().nullable(),
532
+ exit_signal: Signal.nullable(),
533
+ last_seq: u32,
534
+ created_at_ms: epochMs,
535
+ controller_client_id: import_zod7.z.string().max(64).nullable(),
536
+ // null = orphaned (in grace period)
537
+ attached_clients: nonNegInt,
538
+ orphan_expires_at_ms: epochMs.nullable()
539
+ });
540
+ var ExecPtyStartResult = PtyInfo;
541
+ var ExecPtyWriteParams = import_zod7.z.strictObject({ resource_id: ResourceId, data: import_zod7.z.string().min(1).max(65536) });
542
+ var ExecPtyWriteResult = import_zod7.z.strictObject({ written: nonNegInt });
543
+ var ExecPtyResizeParams = import_zod7.z.strictObject({ resource_id: ResourceId, cols, rows });
544
+ var ExecPtyResizeResult = import_zod7.z.strictObject({ cols, rows });
545
+ var ExecPtySignalParams = import_zod7.z.strictObject({ resource_id: ResourceId, signal: Signal });
546
+ var ExecPtySignalResult = import_zod7.z.strictObject({ delivered: import_zod7.z.boolean() });
547
+ var ExecPtyKillParams = import_zod7.z.strictObject({ resource_id: ResourceId, grace_ms: import_zod7.z.int().min(0).max(3e4).default(2e3) });
548
+ var ExecPtyKillResult = import_zod7.z.strictObject({ exit_code: import_zod7.z.int().nullable(), exit_signal: Signal.nullable() });
549
+ var ExecListParams = import_zod7.z.strictObject({});
550
+ var ExecListResult = import_zod7.z.strictObject({ sessions: import_zod7.z.array(PtyInfo) });
551
+ var ExecRunParams = import_zod7.z.strictObject({
552
+ command: import_zod7.z.string().min(1).max(131072),
553
+ cwd: AbsPath.optional(),
554
+ cwd_key: import_zod7.z.string().max(128).optional(),
555
+ env: import_zod7.z.record(import_zod7.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(128), import_zod7.z.string().max(32768)).optional(),
556
+ stdin: import_zod7.z.string().max(1048576).optional(),
557
+ timeout: import_zod7.z.int().min(1).max(3600).default(60),
558
+ // seconds, daemon parity
559
+ max_output_bytes: import_zod7.z.int().min(1024).max(8388608).default(1048576)
560
+ });
561
+ var ExecRunResult = import_zod7.z.strictObject({
562
+ exit_code: import_zod7.z.int(),
563
+ stdout: import_zod7.z.string(),
564
+ stderr: import_zod7.z.string(),
565
+ cwd: AbsPath,
566
+ duration_ms: nonNegInt,
567
+ timed_out: import_zod7.z.boolean(),
568
+ output_truncated: import_zod7.z.boolean()
569
+ });
570
+ var ExecExitedEvent = import_zod7.z.strictObject({ exit_code: import_zod7.z.int().nullable(), exit_signal: Signal.nullable() });
571
+ var ExecTitleEvent = import_zod7.z.strictObject({ title: import_zod7.z.string().max(256) });
572
+ var ExecCwdEvent = import_zod7.z.strictObject({ cwd: AbsPath });
573
+
574
+ // src/schema/session.ts
575
+ var import_zod8 = require("zod");
576
+ var SessionAttachParams = import_zod8.z.strictObject({
577
+ resource_id: ResourceId,
578
+ mode: import_zod8.z.enum(["control", "read_only"]).default("control"),
579
+ since_seq: u32.optional(),
580
+ window_bytes: import_zod8.z.int().min(4096).max(16777216).optional()
581
+ });
582
+ var ResourceInfo = import_zod8.z.discriminatedUnion("kind", [
583
+ import_zod8.z.strictObject({ kind: import_zod8.z.literal("pty"), pty: PtyInfo }),
584
+ import_zod8.z.strictObject({
585
+ kind: import_zod8.z.literal("watch"),
586
+ resource_id: ResourceId,
587
+ path: AbsPath,
588
+ orphan_expires_at_ms: epochMs.nullable()
589
+ })
590
+ ]);
591
+ var SessionAttachResult = ResourceInfo;
592
+ var SessionDetachParams = import_zod8.z.strictObject({ resource_id: ResourceId });
593
+ var SessionDetachResult = import_zod8.z.strictObject({ orphan_expires_at_ms: epochMs.nullable() });
594
+ var SessionReleaseParams = import_zod8.z.strictObject({ resource_id: ResourceId });
595
+ var SessionReleaseResult = import_zod8.z.strictObject({ released: import_zod8.z.literal(true) });
596
+ var SessionListParams = import_zod8.z.strictObject({});
597
+ var SessionListResult = import_zod8.z.strictObject({ resources: import_zod8.z.array(ResourceInfo) });
598
+ var SessionControlChangedEvent = import_zod8.z.strictObject({ controller_client_id: import_zod8.z.string().max(64).nullable(), your_mode: import_zod8.z.enum(["control", "read_only"]) });
599
+
600
+ // src/schema/relay.ts
601
+ var import_zod9 = require("zod");
602
+ var RelayDeviceStatusEvent = import_zod9.z.strictObject({
603
+ online: import_zod9.z.boolean(),
604
+ since_ms: epochMs,
605
+ app_version: import_zod9.z.string().max(32).nullable(),
606
+ protocol_version: ProtocolVersion.nullable()
607
+ });
608
+ var RelayAuthExpiringEvent = import_zod9.z.strictObject({ expires_at_ms: epochMs });
609
+ var RelayCallRequest = import_zod9.z.strictObject({
610
+ cap: import_zod9.z.string().min(1).max(32),
611
+ op: import_zod9.z.string().min(1).max(64),
612
+ params: import_zod9.z.unknown(),
613
+ timeout_ms: import_zod9.z.int().min(100).max(37e5).default(7e4)
614
+ });
615
+ var RelayCallResponse = import_zod9.z.discriminatedUnion("ok", [
616
+ import_zod9.z.strictObject({ ok: import_zod9.z.literal(true), result: import_zod9.z.unknown() }),
617
+ import_zod9.z.strictObject({ ok: import_zod9.z.literal(false), error: ProtocolErrorBody })
618
+ ]);
619
+
620
+ // src/schema/registry.ts
621
+ var FS_ERR = ["NOT_FOUND", "PERMISSION_DENIED_OS", "INVALID_PARAMS", "RESOURCE_BUSY", "INTERNAL"];
622
+ var OPS = {
623
+ "sysinfo.get": { params: SysinfoGetParams, result: SysinfoGetResult, kind: "unary", since: "1.0", errors: ["INTERNAL"] },
624
+ "fs.list": { params: FsListParams, result: FsListResult, kind: "unary", since: "1.0", errors: FS_ERR },
625
+ "fs.stat": { params: FsStatParams, result: FsStatResult, kind: "unary", since: "1.0", errors: FS_ERR },
626
+ "fs.read": { params: FsReadParams, result: FsReadResult, kind: "unary", since: "1.0", errors: [...FS_ERR, "PAYLOAD_TOO_LARGE"] },
627
+ "fs.read_stream": { params: FsReadStreamParams, result: FsReadStreamResult, kind: "stream", since: "1.0", errors: [...FS_ERR, "CANCELLED"] },
628
+ "fs.write": { params: FsWriteParams, result: FsWriteResult, kind: "unary", since: "1.0", errors: [...FS_ERR, "CONFLICT", "PAYLOAD_TOO_LARGE"] },
629
+ "fs.mkdir": { params: FsMkdirParams, result: FsMkdirResult, kind: "unary", since: "1.0", errors: [...FS_ERR, "CONFLICT"] },
630
+ "fs.move": { params: FsMoveParams, result: FsMoveResult, kind: "unary", since: "1.0", errors: [...FS_ERR, "CONFLICT"] },
631
+ "fs.delete": { params: FsDeleteParams, result: FsDeleteResult, kind: "unary", since: "1.0", errors: FS_ERR },
632
+ "fs.watch": { params: FsWatchParams, result: FsWatchResult, kind: "resource", since: "1.0", errors: [...FS_ERR, "RATE_LIMITED"] },
633
+ "fs.patch": { params: FsPatchParams, result: FsPatchResult, kind: "unary", since: "1.0", errors: [...FS_ERR, "CONFLICT"] },
634
+ "fs.search_paths": { params: FsSearchPathsParams, result: FsSearchPathsResult, kind: "unary", since: "1.0", errors: [...FS_ERR, "TIMEOUT"] },
635
+ "fs.search_content": { params: FsSearchContentParams, result: FsSearchContentResult, kind: "unary", since: "1.0", errors: [...FS_ERR, "TIMEOUT"] },
636
+ "exec.pty.start": { params: ExecPtyStartParams, result: ExecPtyStartResult, kind: "resource", since: "1.0", errors: ["INVALID_PARAMS", "NOT_FOUND", "RATE_LIMITED", "PLATFORM_UNSUPPORTED", "INTERNAL"] },
637
+ "exec.pty.write": { params: ExecPtyWriteParams, result: ExecPtyWriteResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN", "INVALID_PARAMS"] },
638
+ "exec.pty.resize": { params: ExecPtyResizeParams, result: ExecPtyResizeResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN"] },
639
+ "exec.pty.signal": { params: ExecPtySignalParams, result: ExecPtySignalResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN", "PLATFORM_UNSUPPORTED"] },
640
+ "exec.pty.kill": { params: ExecPtyKillParams, result: ExecPtyKillResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN"] },
641
+ "exec.list": { params: ExecListParams, result: ExecListResult, kind: "unary", since: "1.0", errors: ["INTERNAL"] },
642
+ "exec.run": { params: ExecRunParams, result: ExecRunResult, kind: "unary", since: "1.0", errors: ["INVALID_PARAMS", "NOT_FOUND", "TIMEOUT", "CANCELLED", "INTERNAL"] },
643
+ "session.attach": { params: SessionAttachParams, result: SessionAttachResult, kind: "stream", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN", "RESOURCE_BUSY"] },
644
+ "session.detach": { params: SessionDetachParams, result: SessionDetachResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE"] },
645
+ "session.release": { params: SessionReleaseParams, result: SessionReleaseResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN"] },
646
+ "session.list": { params: SessionListParams, result: SessionListResult, kind: "unary", since: "1.0", errors: ["INTERNAL"] }
647
+ };
648
+ var EVENTS = {
649
+ "fs.changed": FsChangedEvent,
650
+ "exec.exited": ExecExitedEvent,
651
+ "exec.title": ExecTitleEvent,
652
+ "exec.cwd": ExecCwdEvent,
653
+ "session.control_changed": SessionControlChangedEvent,
654
+ "relay.device_status": RelayDeviceStatusEvent,
655
+ "relay.auth_expiring": RelayAuthExpiringEvent
656
+ };
657
+
658
+ // src/client/client.ts
659
+ var WS_OPEN = 1;
660
+ var TERMINAL_CLOSE = /* @__PURE__ */ new Map([
661
+ [CloseCode.DEVICE_REVOKED, ["AUTH_DEVICE_REVOKED", "This device was revoked"]],
662
+ [CloseCode.AUTH_FORBIDDEN, ["AUTH_FORBIDDEN", "Not this device's owner"]],
663
+ [CloseCode.DEVICE_NOT_FOUND, ["NOT_FOUND", "Device not found"]],
664
+ [CloseCode.REPLACED, ["CONFLICT", "Replaced by a newer connection"]],
665
+ [CloseCode.UPGRADE_REQUIRED, ["PROTOCOL_VERSION_UNSUPPORTED", "Protocol upgrade required"]]
666
+ ]);
667
+ var defaultTimers = {
668
+ setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
669
+ clearTimeout: (handle) => globalThis.clearTimeout(handle),
670
+ now: () => Date.now()
671
+ };
672
+ function defaultWebSocket() {
673
+ return (url, protocols) => {
674
+ const Ctor = globalThis.WebSocket;
675
+ if (!Ctor) {
676
+ throw new DesktopProtocolError("PLATFORM_UNSUPPORTED", "No global WebSocket \u2014 pass options.webSocket");
677
+ }
678
+ return new Ctor(url, protocols);
679
+ };
680
+ }
681
+ function capOf(op) {
682
+ const dot = op.indexOf(".");
683
+ return { cap: op.slice(0, dot), op: op.slice(dot + 1) };
684
+ }
685
+ function fromBody(body) {
686
+ return body.data === void 0 ? new DesktopProtocolError(body.code, body.message, { retryable: body.retryable }) : new DesktopProtocolError(body.code, body.message, { retryable: body.retryable, data: body.data });
687
+ }
688
+ function toBytes(data) {
689
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
690
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
691
+ return null;
692
+ }
693
+ function createDesktopClient(options) {
694
+ const timers = options.timers ?? defaultTimers;
695
+ const makeSocket = options.webSocket ?? defaultWebSocket();
696
+ const heartbeatMs = options.heartbeatMs ?? 25e3;
697
+ const requestTimeoutMs = options.requestTimeoutMs ?? 7e4;
698
+ const protocolVersion = options.protocolVersion ?? `${PROTOCOL_MAJOR}.${PROTOCOL_MINOR}`;
699
+ const backoff = (0, import_realtime.createBackoff)({ ...options.backoff, now: timers.now });
700
+ const diagnose = options.onDiagnostic ?? ((d) => {
701
+ console.warn("[desktop-protocol]", d.kind, d);
702
+ });
703
+ let state = {
704
+ status: "idle",
705
+ welcome: null,
706
+ attempts: 0,
707
+ retryInMs: null,
708
+ lastError: null,
709
+ lastCloseCode: null
710
+ };
711
+ const listeners = /* @__PURE__ */ new Set();
712
+ const readyWaiters = /* @__PURE__ */ new Set();
713
+ const pending = /* @__PURE__ */ new Map();
714
+ const streams = /* @__PURE__ */ new Map();
715
+ const bySid = /* @__PURE__ */ new Map();
716
+ const eventHandlers = /* @__PURE__ */ new Map();
717
+ let socket = null;
718
+ let socketGeneration = 0;
719
+ let nextId = 0;
720
+ let helloId = null;
721
+ let reconnectTimer = null;
722
+ let heartbeatTimer = null;
723
+ let lastHeardAt = 0;
724
+ let closedByUser = false;
725
+ const newId = (prefix) => `${prefix}${(++nextId).toString(36)}`;
726
+ function setState(patch) {
727
+ state = { ...state, ...patch };
728
+ for (const listener of [...listeners]) {
729
+ try {
730
+ listener();
731
+ } catch (error) {
732
+ diagnose({ kind: "handler_threw", where: "subscribe listener", error });
733
+ }
734
+ }
735
+ }
736
+ function sendJson(message) {
737
+ if (!socket || socket.readyState !== WS_OPEN) return false;
738
+ socket.send(JSON.stringify(message));
739
+ return true;
740
+ }
741
+ function connect() {
742
+ if (state.status === "connecting" || state.status === "handshaking" || state.status === "open") return;
743
+ closedByUser = false;
744
+ if (reconnectTimer !== null) {
745
+ timers.clearTimeout(reconnectTimer);
746
+ reconnectTimer = null;
747
+ }
748
+ void openSocket();
749
+ }
750
+ async function openSocket() {
751
+ const generation = ++socketGeneration;
752
+ setState({ status: "connecting", retryInMs: null });
753
+ let ws;
754
+ try {
755
+ const url = typeof options.url === "function" ? await options.url() : options.url;
756
+ const protocols = options.protocols ? await options.protocols() : [];
757
+ if (generation !== socketGeneration || closedByUser) return;
758
+ ws = makeSocket(url, protocols);
759
+ } catch (error) {
760
+ if (generation !== socketGeneration) return;
761
+ const err = error instanceof DesktopProtocolError ? error : new DesktopProtocolError("INTERNAL", "Could not open the connection", { retryable: true, cause: error });
762
+ onSocketGone(generation, null, err);
763
+ return;
764
+ }
765
+ socket = ws;
766
+ ws.binaryType = "arraybuffer";
767
+ ws.onopen = () => {
768
+ if (generation !== socketGeneration) return;
769
+ lastHeardAt = timers.now();
770
+ helloId = newId("h");
771
+ setState({ status: "handshaking" });
772
+ sendJson({ type: "hello", id: helloId, protocolVersion, clientType: options.clientType, clientVersion: options.clientVersion });
773
+ scheduleHeartbeat(generation);
774
+ };
775
+ ws.onmessage = (event) => {
776
+ if (generation !== socketGeneration) return;
777
+ lastHeardAt = timers.now();
778
+ onMessage(event.data);
779
+ };
780
+ ws.onerror = () => {
781
+ };
782
+ ws.onclose = (event) => {
783
+ if (generation !== socketGeneration) return;
784
+ onSocketGone(generation, event.code, null);
785
+ };
786
+ }
787
+ function scheduleHeartbeat(generation) {
788
+ if (heartbeatTimer !== null) timers.clearTimeout(heartbeatTimer);
789
+ heartbeatTimer = timers.setTimeout(() => {
790
+ heartbeatTimer = null;
791
+ if (generation !== socketGeneration || !socket) return;
792
+ const silentMs = timers.now() - lastHeardAt;
793
+ if (silentMs >= heartbeatMs * 2) {
794
+ diagnose({ kind: "heartbeat_timeout", silentMs });
795
+ const dead = socket;
796
+ onSocketGone(generation, null, new DesktopProtocolError("TIMEOUT", "Connection went silent", { retryable: true }));
797
+ try {
798
+ dead.close(CloseCode.NORMAL, "heartbeat timeout");
799
+ } catch {
800
+ }
801
+ return;
802
+ }
803
+ if (socket.readyState === WS_OPEN) socket.send(PING_TEXT);
804
+ scheduleHeartbeat(generation);
805
+ }, heartbeatMs);
806
+ }
807
+ function onSocketGone(generation, code, error) {
808
+ if (generation !== socketGeneration) return;
809
+ socketGeneration++;
810
+ if (socket) {
811
+ socket.onopen = socket.onmessage = socket.onclose = socket.onerror = null;
812
+ }
813
+ socket = null;
814
+ helloId = null;
815
+ if (heartbeatTimer !== null) {
816
+ timers.clearTimeout(heartbeatTimer);
817
+ heartbeatTimer = null;
818
+ }
819
+ const terminal = code === null ? void 0 : TERMINAL_CLOSE.get(code);
820
+ const lost = error ?? (terminal ? new DesktopProtocolError(terminal[0], terminal[1]) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
821
+ for (const [id, p] of pending) {
822
+ pending.delete(id);
823
+ p.cleanup();
824
+ p.reject(lost);
825
+ }
826
+ bySid.clear();
827
+ for (const [id, s] of streams) {
828
+ if (s.resourceId !== null && terminal === void 0 && !closedByUser) {
829
+ s.sid = null;
830
+ s.reattaching = false;
831
+ continue;
832
+ }
833
+ streams.delete(id);
834
+ endStream(s, null, lost);
835
+ }
836
+ if (closedByUser || terminal) {
837
+ setState({ status: "closed", welcome: null, lastError: closedByUser ? state.lastError : lost, lastCloseCode: code, retryInMs: null });
838
+ rejectReadyWaiters(lost);
839
+ return;
840
+ }
841
+ const delay = backoff.nextDelayMs();
842
+ if (backoff.isAlarming()) diagnose({ kind: "reconnect_alarm", attempts: backoff.attempts() });
843
+ setState({ status: "reconnecting", welcome: null, attempts: backoff.attempts(), retryInMs: delay, lastError: lost, lastCloseCode: code });
844
+ reconnectTimer = timers.setTimeout(() => {
845
+ reconnectTimer = null;
846
+ if (!closedByUser) void openSocket();
847
+ }, delay);
848
+ }
849
+ function rejectReadyWaiters(error) {
850
+ for (const w of [...readyWaiters]) {
851
+ readyWaiters.delete(w);
852
+ w.reject(error);
853
+ }
854
+ }
855
+ function close() {
856
+ closedByUser = true;
857
+ if (reconnectTimer !== null) {
858
+ timers.clearTimeout(reconnectTimer);
859
+ reconnectTimer = null;
860
+ }
861
+ const cancelled = new DesktopProtocolError("CANCELLED", "Client closed");
862
+ const ws = socket;
863
+ if (ws) {
864
+ onSocketGone(socketGeneration, CloseCode.NORMAL, cancelled);
865
+ try {
866
+ ws.close(CloseCode.NORMAL, "client closed");
867
+ } catch {
868
+ }
869
+ } else {
870
+ for (const [id, s] of streams) {
871
+ streams.delete(id);
872
+ endStream(s, null, cancelled);
873
+ }
874
+ setState({ status: "closed", retryInMs: null });
875
+ rejectReadyWaiters(cancelled);
876
+ }
877
+ }
878
+ function onMessage(data) {
879
+ if (typeof data === "string") {
880
+ if (data === PONG_TEXT) return;
881
+ let json;
882
+ try {
883
+ json = JSON.parse(data);
884
+ } catch {
885
+ diagnose({ kind: "malformed_message", detail: "not JSON" });
886
+ return;
887
+ }
888
+ const parsed = CoreToClientMsg.safeParse(json);
889
+ if (!parsed.success) {
890
+ diagnose({ kind: "malformed_message", detail: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ") });
891
+ return;
892
+ }
893
+ onEnvelope(parsed.data);
894
+ return;
895
+ }
896
+ const bytes = toBytes(data);
897
+ if (bytes === null) {
898
+ diagnose({ kind: "malformed_message", detail: "binary message is not an ArrayBuffer/view (set binaryType)" });
899
+ return;
900
+ }
901
+ let frame;
902
+ try {
903
+ frame = decodeFrame(bytes);
904
+ } catch (error) {
905
+ diagnose({ kind: "malformed_message", detail: error instanceof Error ? error.message : String(error) });
906
+ return;
907
+ }
908
+ onFrame(frame.header, frame.payload);
909
+ }
910
+ function onEnvelope(msg) {
911
+ switch (msg.type) {
912
+ case "response": {
913
+ if (msg.id === helloId) return onWelcome(msg.result);
914
+ const s = streams.get(msg.id);
915
+ if (s) return onStreamResult(s, msg.result);
916
+ const p = pending.get(msg.id);
917
+ if (!p) return diagnose({ kind: "unknown_request", id: msg.id, type: msg.type });
918
+ pending.delete(msg.id);
919
+ p.cleanup();
920
+ p.resolve(msg.result);
921
+ return;
922
+ }
923
+ case "error": {
924
+ const error = fromBody(msg.error);
925
+ if (msg.id === helloId) return onHelloRefused(error);
926
+ const s = streams.get(msg.id);
927
+ if (s) {
928
+ if (s.reattaching && s.resourceId !== null) diagnose({ kind: "reattach_failed", resourceId: s.resourceId, error });
929
+ streams.delete(msg.id);
930
+ if (s.sid !== null) bySid.delete(s.sid);
931
+ return endStream(s, null, error);
932
+ }
933
+ const p = pending.get(msg.id);
934
+ if (!p) return diagnose({ kind: "unknown_request", id: msg.id, type: msg.type });
935
+ pending.delete(msg.id);
936
+ p.cleanup();
937
+ p.reject(error);
938
+ return;
939
+ }
940
+ case "stream": {
941
+ const s = streams.get(msg.id);
942
+ if (!s) return diagnose({ kind: "unknown_request", id: msg.id, type: msg.type });
943
+ onStreamMeta(s, msg.meta);
944
+ return;
945
+ }
946
+ case "end": {
947
+ const s = streams.get(msg.id);
948
+ if (!s) return diagnose({ kind: "unknown_request", id: msg.id, type: msg.type });
949
+ void s.chain.then(() => {
950
+ streams.delete(s.id);
951
+ if (s.sid !== null) bySid.delete(s.sid);
952
+ endStream(s, msg.reason ?? "completed", null);
953
+ });
954
+ return;
955
+ }
956
+ case "credit": {
957
+ const s = streams.get(msg.id);
958
+ if (!s) return diagnose({ kind: "unknown_request", id: msg.id, type: msg.type });
959
+ s.sendWindow += msg.n;
960
+ flushSend(s);
961
+ return;
962
+ }
963
+ case "event": {
964
+ const key = `${msg.cap}.${msg.name}`;
965
+ const schema = EVENTS[key];
966
+ if (!schema) return diagnose({ kind: "malformed_message", detail: `unknown event ${key}` });
967
+ const payload = schema.safeParse(msg.payload);
968
+ if (!payload.success) return diagnose({ kind: "malformed_message", detail: `event ${key}: invalid payload` });
969
+ if (key === "relay.auth_expiring" && options.getToken) {
970
+ options.getToken().then(reauth, (error) => diagnose({ kind: "reauth_failed", error }));
971
+ }
972
+ for (const handler of [...eventHandlers.get(key) ?? []]) {
973
+ try {
974
+ handler(payload.data, msg.resourceId);
975
+ } catch (error) {
976
+ diagnose({ kind: "handler_threw", where: `event ${key}`, error });
977
+ }
978
+ }
979
+ return;
980
+ }
981
+ }
982
+ }
983
+ function onWelcome(result) {
984
+ const welcome = Welcome.safeParse(result);
985
+ if (!welcome.success) {
986
+ onHelloRefused(new DesktopProtocolError("PROTOCOL_ERROR", "Malformed welcome"));
987
+ return;
988
+ }
989
+ helloId = null;
990
+ backoff.markConnected();
991
+ setState({ status: "open", welcome: welcome.data, attempts: 0, retryInMs: null, lastError: null });
992
+ for (const w of [...readyWaiters]) {
993
+ readyWaiters.delete(w);
994
+ w.resolve(welcome.data);
995
+ }
996
+ for (const s of [...streams.values()]) {
997
+ if (s.sid === null && s.resourceId !== null && !s.ended) reattach(s, welcome.data);
998
+ }
999
+ }
1000
+ function onHelloRefused(error) {
1001
+ const ws = socket;
1002
+ onSocketGone(socketGeneration, error.code === "PROTOCOL_VERSION_UNSUPPORTED" ? CloseCode.UPGRADE_REQUIRED : null, error);
1003
+ try {
1004
+ ws?.close(CloseCode.NORMAL, "hello refused");
1005
+ } catch {
1006
+ }
1007
+ }
1008
+ function onStreamResult(s, result) {
1009
+ if (s.reattaching) {
1010
+ s.reattaching = false;
1011
+ safeCall("onReattached", () => s.handlers.onReattached?.());
1012
+ return;
1013
+ }
1014
+ const rid = result?.resource_id;
1015
+ if (typeof rid === "string") s.resourceId = rid;
1016
+ if (!s.resultSettled) {
1017
+ s.resultSettled = true;
1018
+ s.resolveResult(result);
1019
+ }
1020
+ }
1021
+ function onStreamMeta(s, meta) {
1022
+ if (meta.kind === "open") {
1023
+ if (s.sid !== null) bySid.delete(s.sid);
1024
+ s.sid = meta.sid;
1025
+ s.windowBytes = meta.window_bytes;
1026
+ if (meta.resource_id !== void 0) s.resourceId = meta.resource_id;
1027
+ bySid.set(meta.sid, s);
1028
+ flushSend(s);
1029
+ } else {
1030
+ s.expectSeq = null;
1031
+ }
1032
+ safeCall("onMeta", () => s.handlers.onMeta?.(meta));
1033
+ }
1034
+ function onFrame(header, payload) {
1035
+ const s = bySid.get(header.sid);
1036
+ if (!s) {
1037
+ diagnose({ kind: "unknown_stream", sid: header.sid });
1038
+ return;
1039
+ }
1040
+ const isData = header.channel === FrameChannel.DATA || header.channel === FrameChannel.STDERR;
1041
+ const isSnapshot = header.channel === FrameChannel.SNAPSHOT;
1042
+ if (!isData && !isSnapshot) {
1043
+ diagnose({ kind: "malformed_message", detail: `channel ${header.channel} is not client-bound` });
1044
+ return;
1045
+ }
1046
+ if (isData) {
1047
+ if (s.expectSeq !== null && header.seq !== s.expectSeq) {
1048
+ diagnose({ kind: "seq_gap", id: s.id, expected: s.expectSeq, received: header.seq });
1049
+ }
1050
+ s.expectSeq = header.seq + 1;
1051
+ }
1052
+ const bytes = payload.slice();
1053
+ const handler = isData ? s.handlers.onData : s.handlers.onSnapshot;
1054
+ s.chain = s.chain.then(async () => {
1055
+ if (s.ended) return;
1056
+ try {
1057
+ await handler?.(bytes, header);
1058
+ } catch (error) {
1059
+ diagnose({ kind: "handler_threw", where: isData ? "onData" : "onSnapshot", error });
1060
+ }
1061
+ if (isData) s.lastSeq = header.seq;
1062
+ grantCredit(s, bytes.byteLength);
1063
+ });
1064
+ }
1065
+ function grantCredit(s, consumed) {
1066
+ s.consumedUncredited += consumed;
1067
+ if (s.consumedUncredited >= Math.max(1, Math.floor(s.windowBytes / 4))) {
1068
+ if (sendJson({ type: "credit", id: s.id, n: s.consumedUncredited })) s.consumedUncredited = 0;
1069
+ }
1070
+ }
1071
+ function flushSend(s) {
1072
+ if (s.sid === null || !socket || socket.readyState !== WS_OPEN) return;
1073
+ const maxPayload = state.welcome?.limits.max_frame_payload_bytes ?? DEFAULT_MAX_FRAME_PAYLOAD_BYTES;
1074
+ while (s.sendQueue.length > 0 && s.sendWindow > 0) {
1075
+ const head = s.sendQueue[0];
1076
+ const n = Math.min(head.byteLength, s.sendWindow, maxPayload);
1077
+ const chunk = head.subarray(0, n);
1078
+ if (n === head.byteLength) s.sendQueue.shift();
1079
+ else s.sendQueue[0] = head.subarray(n);
1080
+ s.sendWindow -= n;
1081
+ s.sendSeq += 1;
1082
+ socket.send(encodeFrame({ version: FRAME_VERSION, flags: 0, channel: FrameChannel.STDIN, cid: 0, sid: s.sid, seq: s.sendSeq }, chunk));
1083
+ }
1084
+ }
1085
+ function endStream(s, reason, error) {
1086
+ if (s.ended) return;
1087
+ s.ended = true;
1088
+ s.sendQueue = [];
1089
+ if (!s.resultSettled) {
1090
+ s.resultSettled = true;
1091
+ s.rejectResult(error ?? new DesktopProtocolError("INTERNAL", "Stream ended before its result"));
1092
+ }
1093
+ if (error) s.rejectDone(error);
1094
+ else s.resolveDone(reason ?? "completed");
1095
+ }
1096
+ function reattach(s, welcome) {
1097
+ const id = newId("a");
1098
+ streams.delete(s.id);
1099
+ s.id = id;
1100
+ s.reattaching = true;
1101
+ s.expectSeq = s.lastSeq > 0 ? s.lastSeq + 1 : null;
1102
+ s.consumedUncredited = 0;
1103
+ s.sendWindow = welcome.limits.default_window_bytes;
1104
+ s.sendSeq = 0;
1105
+ streams.set(id, s);
1106
+ const params = { resource_id: s.resourceId, mode: s.options.mode ?? "control" };
1107
+ if (s.lastSeq > 0) params.since_seq = s.lastSeq;
1108
+ if (s.options.windowBytes !== void 0) params.window_bytes = s.options.windowBytes;
1109
+ sendJson({ id, type: "request", cap: "session", op: "attach", params });
1110
+ }
1111
+ function safeCall(where, fn) {
1112
+ try {
1113
+ fn();
1114
+ } catch (error) {
1115
+ diagnose({ kind: "handler_threw", where, error });
1116
+ }
1117
+ }
1118
+ function ready(opts = {}) {
1119
+ if (state.status === "open" && state.welcome) return Promise.resolve(state.welcome);
1120
+ if (state.status === "closed") {
1121
+ return Promise.reject(state.lastError ?? new DesktopProtocolError("CANCELLED", "Client is closed"));
1122
+ }
1123
+ if (state.status === "idle") connect();
1124
+ return new Promise((resolve, reject) => {
1125
+ const waiter = {
1126
+ resolve: (w) => {
1127
+ done();
1128
+ resolve(w);
1129
+ },
1130
+ reject: (e) => {
1131
+ done();
1132
+ reject(e);
1133
+ }
1134
+ };
1135
+ const timeoutMs = opts.timeoutMs ?? requestTimeoutMs;
1136
+ const timer = timers.setTimeout(() => waiter.reject(new DesktopProtocolError("DEVICE_OFFLINE", "Device not reachable", { retryable: true })), timeoutMs);
1137
+ const onAbort = () => waiter.reject(new DesktopProtocolError("CANCELLED", "Aborted"));
1138
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
1139
+ function done() {
1140
+ readyWaiters.delete(waiter);
1141
+ timers.clearTimeout(timer);
1142
+ opts.signal?.removeEventListener("abort", onAbort);
1143
+ }
1144
+ readyWaiters.add(waiter);
1145
+ });
1146
+ }
1147
+ async function request(op, params, opts = {}) {
1148
+ if (OPS[op].kind !== "unary") {
1149
+ throw new DesktopProtocolError("INVALID_PARAMS", `${op} is a ${OPS[op].kind} op \u2014 use stream()`);
1150
+ }
1151
+ await ready(opts);
1152
+ const id = newId("r");
1153
+ return new Promise((resolve, reject) => {
1154
+ const timeoutMs = opts.timeoutMs ?? requestTimeoutMs;
1155
+ const timer = timers.setTimeout(() => {
1156
+ pending.delete(id);
1157
+ cleanup();
1158
+ sendJson({ id, type: "cancel" });
1159
+ reject(new DesktopProtocolError("TIMEOUT", `${op} timed out`, { retryable: true }));
1160
+ }, timeoutMs);
1161
+ const onAbort = () => {
1162
+ pending.delete(id);
1163
+ cleanup();
1164
+ sendJson({ id, type: "cancel" });
1165
+ reject(new DesktopProtocolError("CANCELLED", "Aborted"));
1166
+ };
1167
+ function cleanup() {
1168
+ timers.clearTimeout(timer);
1169
+ opts.signal?.removeEventListener("abort", onAbort);
1170
+ }
1171
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
1172
+ pending.set(id, { kind: "unary", op, resolve, reject, cleanup });
1173
+ if (!sendJson({ id, type: "request", ...capOf(op), params })) {
1174
+ pending.delete(id);
1175
+ cleanup();
1176
+ reject(new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
1177
+ }
1178
+ });
1179
+ }
1180
+ function stream(op, params, handlers = {}, opts = {}) {
1181
+ let resolveResult;
1182
+ let rejectResult;
1183
+ let resolveDone;
1184
+ let rejectDone;
1185
+ const result = new Promise((res, rej) => {
1186
+ resolveResult = res;
1187
+ rejectResult = rej;
1188
+ });
1189
+ const done = new Promise((res, rej) => {
1190
+ resolveDone = res;
1191
+ rejectDone = rej;
1192
+ });
1193
+ result.catch(() => void 0);
1194
+ done.catch(() => void 0);
1195
+ const s = {
1196
+ op,
1197
+ handlers,
1198
+ options: opts,
1199
+ id: newId("s"),
1200
+ sid: null,
1201
+ resourceId: null,
1202
+ lastSeq: 0,
1203
+ expectSeq: 1,
1204
+ windowBytes: opts.windowBytes ?? DEFAULT_WINDOW_BYTES,
1205
+ consumedUncredited: 0,
1206
+ sendWindow: DEFAULT_WINDOW_BYTES,
1207
+ sendSeq: 0,
1208
+ sendQueue: [],
1209
+ chain: Promise.resolve(),
1210
+ resultSettled: false,
1211
+ resolveResult,
1212
+ rejectResult,
1213
+ resolveDone,
1214
+ rejectDone,
1215
+ ended: false,
1216
+ reattaching: false
1217
+ };
1218
+ const start = async () => {
1219
+ let welcome;
1220
+ try {
1221
+ welcome = await ready(opts);
1222
+ } catch (error) {
1223
+ endStream(s, null, error);
1224
+ return;
1225
+ }
1226
+ if (s.ended) return;
1227
+ s.sendWindow = welcome.limits.default_window_bytes;
1228
+ if (opts.windowBytes === void 0) s.windowBytes = welcome.limits.default_window_bytes;
1229
+ streams.set(s.id, s);
1230
+ const wire = { ...params };
1231
+ if (opts.windowBytes !== void 0 && "window_bytes" in OPS[op].params.shape) wire.window_bytes = opts.windowBytes;
1232
+ if (!sendJson({ id: s.id, type: "request", ...capOf(op), params: wire })) {
1233
+ streams.delete(s.id);
1234
+ endStream(s, null, new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
1235
+ }
1236
+ };
1237
+ void start();
1238
+ opts.signal?.addEventListener("abort", () => handle.cancel(), { once: true });
1239
+ const encoder = new TextEncoder();
1240
+ const handle = {
1241
+ get id() {
1242
+ return s.id;
1243
+ },
1244
+ result,
1245
+ done,
1246
+ get resourceId() {
1247
+ return s.resourceId;
1248
+ },
1249
+ get lastSeq() {
1250
+ return s.lastSeq;
1251
+ },
1252
+ write(data) {
1253
+ if (s.ended) throw new DesktopProtocolError("RESOURCE_GONE", "Stream has ended");
1254
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
1255
+ if (bytes.byteLength === 0) return;
1256
+ s.sendQueue.push(bytes);
1257
+ flushSend(s);
1258
+ },
1259
+ cancel() {
1260
+ if (s.ended) return;
1261
+ if (!sendJson({ id: s.id, type: "cancel" })) {
1262
+ streams.delete(s.id);
1263
+ endStream(s, "cancelled", null);
1264
+ }
1265
+ },
1266
+ async detach() {
1267
+ if (s.ended) return;
1268
+ if (s.resourceId === null) throw new DesktopProtocolError("INVALID_PARAMS", "Only resource streams can be detached");
1269
+ const resourceId = s.resourceId;
1270
+ streams.delete(s.id);
1271
+ if (s.sid !== null) bySid.delete(s.sid);
1272
+ endStream(s, "detached", null);
1273
+ await request("session.detach", { resource_id: resourceId });
1274
+ }
1275
+ };
1276
+ return handle;
1277
+ }
1278
+ function on(name, handler) {
1279
+ let set = eventHandlers.get(name);
1280
+ if (!set) {
1281
+ set = /* @__PURE__ */ new Set();
1282
+ eventHandlers.set(name, set);
1283
+ }
1284
+ set.add(handler);
1285
+ return () => {
1286
+ set.delete(handler);
1287
+ };
1288
+ }
1289
+ function reauth(token) {
1290
+ sendJson({ type: "relay", op: "reauth", token });
1291
+ }
1292
+ return {
1293
+ connect,
1294
+ close,
1295
+ getState: () => state,
1296
+ subscribe(listener) {
1297
+ listeners.add(listener);
1298
+ return () => {
1299
+ listeners.delete(listener);
1300
+ };
1301
+ },
1302
+ ready,
1303
+ request,
1304
+ stream,
1305
+ on,
1306
+ reauth
1307
+ };
1308
+ }
1309
+ //# sourceMappingURL=client.cjs.map