@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,181 @@
1
+ import { BackoffOptions } from '@ai-matrx/realtime';
2
+ export { RECONNECT_ALARM_ATTEMPTS } from '@ai-matrx/realtime';
3
+ import { z } from 'zod';
4
+ import { W as Welcome, D as DesktopProtocolError, O as OpName, a as OpParams, b as OpResult, c as OPS, S as StreamMeta, E as EndReason, d as EVENTS, C as ClientType } from './registry-DTcTGycI.js';
5
+ export { i as isDesktopProtocolError } from './registry-DTcTGycI.js';
6
+ import { FrameHeader } from './frame.js';
7
+
8
+ /** The subset of the WHATWG WebSocket the client uses — browser, React Native, `ws` and Workers all fit. */
9
+ interface WebSocketLike {
10
+ readonly readyState: number;
11
+ binaryType: string;
12
+ send(data: string | ArrayBuffer | Uint8Array): void;
13
+ close(code?: number, reason?: string): void;
14
+ onopen: ((event: unknown) => void) | null;
15
+ onmessage: ((event: {
16
+ data: unknown;
17
+ }) => void) | null;
18
+ onclose: ((event: {
19
+ code: number;
20
+ reason: string;
21
+ }) => void) | null;
22
+ onerror: ((event: unknown) => void) | null;
23
+ }
24
+ type WebSocketFactory = (url: string, protocols: string[]) => WebSocketLike;
25
+ interface ClientTimers {
26
+ setTimeout(fn: () => void, ms: number): unknown;
27
+ clearTimeout(handle: unknown): void;
28
+ now(): number;
29
+ }
30
+ type DesktopClientStatus =
31
+ /** Constructed, connect() not called yet. */
32
+ "idle"
33
+ /** Socket opening. */
34
+ | "connecting"
35
+ /** Socket open, hello sent, waiting for Welcome. */
36
+ | "handshaking"
37
+ /** Welcome received — requests flow. */
38
+ | "open"
39
+ /** Dropped; a reconnect is scheduled (see `retryInMs`). */
40
+ | "reconnecting"
41
+ /** Closed for good: close() was called, or the device/server refused us (see `lastError`). */
42
+ | "closed";
43
+ interface DesktopClientState {
44
+ status: DesktopClientStatus;
45
+ welcome: Welcome | null;
46
+ /** Consecutive failed connection attempts since the last stable connection. */
47
+ attempts: number;
48
+ /** Delay before the scheduled reconnect, while `status === "reconnecting"`. */
49
+ retryInMs: number | null;
50
+ lastError: DesktopProtocolError | null;
51
+ lastCloseCode: number | null;
52
+ }
53
+ type DesktopClientDiagnostic = {
54
+ kind: "malformed_message";
55
+ detail: string;
56
+ } | {
57
+ kind: "unknown_request";
58
+ id: string;
59
+ type: string;
60
+ } | {
61
+ kind: "unknown_stream";
62
+ sid: number;
63
+ } | {
64
+ kind: "seq_gap";
65
+ id: string;
66
+ expected: number;
67
+ received: number;
68
+ } | {
69
+ kind: "heartbeat_timeout";
70
+ silentMs: number;
71
+ } | {
72
+ kind: "reconnect_alarm";
73
+ attempts: number;
74
+ } | {
75
+ kind: "reattach_failed";
76
+ resourceId: string;
77
+ error: DesktopProtocolError;
78
+ } | {
79
+ kind: "reauth_failed";
80
+ error: unknown;
81
+ } | {
82
+ kind: "handler_threw";
83
+ where: string;
84
+ error: unknown;
85
+ };
86
+ interface DesktopClientOptions {
87
+ /** wss://relay…/v1/devices/{id}/connect, or ws://127.0.0.1:2214x for the localhost transport. */
88
+ url: string | (() => string | Promise<string>);
89
+ /**
90
+ * WebSocket subprotocols, re-read on EVERY (re)connect so a refreshed token is used — e.g.
91
+ * `["matrx.v1", \`bearer.${jwt}\`]`. Browser tokens ride the subprotocol, never the URL.
92
+ */
93
+ protocols?: () => string[] | Promise<string[]>;
94
+ clientType: ClientType;
95
+ clientVersion: string;
96
+ /** Default: this package's PROTOCOL_MAJOR.PROTOCOL_MINOR. */
97
+ protocolVersion?: string;
98
+ /** Default: `globalThis.WebSocket`. */
99
+ webSocket?: WebSocketFactory;
100
+ /** Reconnect ladder — the shared @ai-matrx/realtime backoff (stability reset + jitter). */
101
+ backoff?: Omit<BackoffOptions, "now">;
102
+ /** Heartbeat interval (PING_TEXT). Two silent intervals = reconnect. Default 25 000. */
103
+ heartbeatMs?: number;
104
+ /** Unary request timeout. Default 70 000 (the relay's call default). */
105
+ requestTimeoutMs?: number;
106
+ /** Fresh access token for `relay.reauth` when the relay sends `relay.auth_expiring`. */
107
+ getToken?: () => Promise<string>;
108
+ /** Background problems — loud by contract; default logs to console.warn. */
109
+ onDiagnostic?: (diagnostic: DesktopClientDiagnostic) => void;
110
+ timers?: ClientTimers;
111
+ }
112
+ interface RequestOptions {
113
+ signal?: AbortSignal;
114
+ timeoutMs?: number;
115
+ }
116
+ type StreamOpName = {
117
+ [N in OpName]: (typeof OPS)[N]["kind"] extends "unary" ? never : N;
118
+ }[OpName];
119
+ type EventName = keyof typeof EVENTS;
120
+ type EventPayload<E extends EventName> = z.output<(typeof EVENTS)[E]>;
121
+ type EventHandler<E extends EventName> = (payload: EventPayload<E>, resourceId: string | undefined) => void;
122
+ interface StreamHandlers {
123
+ /** DATA / STDERR bytes. Credit is granted only after this returns (or its promise settles) — backpressure. */
124
+ onData?: (payload: Uint8Array, header: FrameHeader) => void | Promise<void>;
125
+ /** SNAPSHOT bytes (a serialized screen after a gap). */
126
+ onSnapshot?: (payload: Uint8Array, header: FrameHeader) => void | Promise<void>;
127
+ /** open / gap / snapshot announcements, including the ones after a reattach. */
128
+ onMeta?: (meta: StreamMeta) => void;
129
+ /** Called after a transparent reattach (the stream is live again on a new sid). */
130
+ onReattached?: () => void;
131
+ }
132
+ interface StreamOptions extends RequestOptions {
133
+ /** Receive window in payload bytes (default: the core's limits.default_window_bytes). */
134
+ windowBytes?: number;
135
+ /** Reattach mode for resource streams. Default "control". */
136
+ mode?: "control" | "read_only";
137
+ }
138
+ interface DesktopStream<N extends StreamOpName> {
139
+ /** The CURRENT request id carrying this stream (changes on reattach). */
140
+ readonly id: string;
141
+ /** The unary result that precedes the stream (PtyInfo, FsReadStreamResult, ResourceInfo…). */
142
+ readonly result: Promise<OpResult<N>>;
143
+ /** Settles when the stream ends for good: resolves with the end reason, rejects on error. */
144
+ readonly done: Promise<EndReason>;
145
+ /** The resource this stream belongs to (pty_…/watch_…), once known. Resource streams survive reconnects. */
146
+ readonly resourceId: string | null;
147
+ /** Seq of the last DATA frame delivered to onData — sent as since_seq on reattach. */
148
+ readonly lastSeq: number;
149
+ /** Send STDIN bytes (UTF-8 for strings), chunked and credit-gated. */
150
+ write(data: Uint8Array | string): void;
151
+ /** Cancel the request (core ends the stream with reason "cancelled"). */
152
+ cancel(): void;
153
+ /** Leave the resource running on the device and stop following it (session.detach). */
154
+ detach(): Promise<void>;
155
+ }
156
+ interface DesktopClient {
157
+ connect(): void;
158
+ /** Close for good (no reconnect). Pending work rejects with CANCELLED. */
159
+ close(): void;
160
+ getState(): DesktopClientState;
161
+ /** useSyncExternalStore-shaped. */
162
+ subscribe(listener: () => void): () => void;
163
+ /** Resolves with the Welcome once open (immediately if already open). */
164
+ ready(options?: RequestOptions): Promise<Welcome>;
165
+ request<N extends OpName>(op: N, params: OpParams<N>, options?: RequestOptions): Promise<OpResult<N>>;
166
+ stream<N extends StreamOpName>(op: N, params: OpParams<N>, handlers?: StreamHandlers, options?: StreamOptions): DesktopStream<N>;
167
+ on<E extends EventName>(name: E, handler: EventHandler<E>): () => void;
168
+ /** Hand the relay a fresh token without dropping the socket. */
169
+ reauth(token: string): void;
170
+ }
171
+
172
+ /**
173
+ * The reconnecting desktop-protocol client: hello → requests, streams with credit-based
174
+ * backpressure, events, transparent reattach of resource streams (terminals, watches) after a
175
+ * drop, heartbeat, and relay reauth. One instance per device connection. All state lives in the
176
+ * closure — no module-level state (dual ESM/CJS graphs would split it).
177
+ */
178
+
179
+ declare function createDesktopClient(options: DesktopClientOptions): DesktopClient;
180
+
181
+ export { type ClientTimers, type DesktopClient, type DesktopClientDiagnostic, type DesktopClientOptions, type DesktopClientState, type DesktopClientStatus, DesktopProtocolError, type DesktopStream, type EventHandler, type EventName, type EventPayload, OpName, OpParams, OpResult, type RequestOptions, type StreamHandlers, type StreamOpName, type StreamOptions, type WebSocketFactory, type WebSocketLike, createDesktopClient };