UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

200 lines (199 loc) 8.62 kB
import { Session, User } from '@supabase/supabase-js'; /** * Strip NUL characters (U+0000) from strings and object keys — Postgres rejects * them in jsonb and text (22P05). Walks the structure rather than * round-tripping JSON, which would also match escape text in legitimate content. */ export declare function stripNullBytes<T>(value: T): T; export interface AuthSession { access_token: string; refresh_token: string | null; device_id?: string; } interface DeviceData { user_id: string; device_name: string; capabilities: any; status: string; last_seen: string; } export declare class RemoteChannel { private client; private channel; /** Legacy listener, on its own public channel so a private-channel auth * failure can't take both transports down. Removed at the flip (009). */ private legacyChannel; private heartbeatInterval; private connectionCheckInterval; /** Device the heartbeat timer maintains; null = stopped, so re-arm is inert. */ private heartbeatDeviceId; private statusWriteChain; /** Tokens from the last setSession / TOKEN_REFRESHED, for setOffline(). */ private lastKnownSession; /** Set by unsubscribe(): suppresses status/heartbeat writes so they can't * land after setOffline()'s durable write. */ private shuttingDown; private deviceId; private deviceName; private onToolCall; private authListenerRegistered; /** False when presence publishing failed on an otherwise healthy channel; * the health check retries, since SUBSCRIBED won't fire again. */ private presenceTracked; /** Last capability value written (null = never), to avoid redundant writes. */ private transportCapableWritten; /** Re-entrancy guard: on a wedged socket each track() buffers for the full * 10s push timeout, so 10s health ticks would stack pushes. */ private isTrackingPresence; private lastDeviceStatus; private lastChannelState; private reconnectAttempt; private isRecreatingChannel; private joiningSince; private _user; get user(): User | null; initialize(url: string, key: string): void; setSession(session: AuthSession): Promise<{ error: any; }>; getSession(): Promise<{ data: { session: Session | null; }; error: any; }>; findDevice(deviceId: string): Promise<{ id: any; device_name: any; } | null>; updateDevice(deviceId: string, updates: any): Promise<{ data: any[] | null; error: import("@supabase/postgrest-js").PostgrestError | null; }>; createDevice(deviceData: DeviceData): Promise<{ data: any; error: null; }>; registerDevice(capabilities: any, currentDeviceId: string | undefined, deviceName: string, onToolCall: (payload: any) => void): Promise<void>; /** * Publish presence, retrying a non-'ok' result — track() resolves with a * status rather than rejecting, and absent presence reads as offline on the * server. `presenceTracked` lets the health check retry later. */ private trackPresenceWithRetry; private trackPresenceInner; /** * The complete `capabilities` JSONB value. One place only: every write * replaces the whole column, so a second literal would silently drop keys. */ private capabilitiesPayload; /** * Advertise (or withdraw) the broadcast capability. Only true while genuinely * reachable that way — the server uses it to pick a transport, to read absent * presence as offline, and to choose the sweep tier, so every change must * re-arm the heartbeat. */ private setTransportCapable; /** * Legacy postgres_changes listener on its own public channel. Best-effort: * failures are logged, never thrown. Removed at the flip (009). */ private createLegacyChannel; /** Tear down the legacy channel (best effort). */ private removeLegacyChannel; /** Create and subscribe the private channel (initial join and recreation). */ private createChannel; /** Hand a call to device.ts, observing the rejection — the handler is async * and an unhandled rejection terminates the process. */ private dispatchToolCall; /** * Handle a 'new_call' doorbell. It carries ids only; the row is fetched by * primary key and fed through the same handler as a postgres_changes * payload, so device.ts stays transport-agnostic. */ private onDoorbell; /** * Tell the server a result row is written. Fire-and-forget: a failed send * just falls back to the server's 10s recovery poll. MUST run only after * updateCallResult() resolves, so the server's fetch-by-id sees a terminal row. */ notifyResult(callId: string): Promise<void>; /** * Compact connection state for logs — e.g. "socket=open(1) ch=errored attempt=3". * readyState 1=OPEN (a 1 while joins keep failing = a half-open socket being reused), * 3=CLOSED, '-'=no socket. Reads realtime-js internals defensively; never throws. */ private connState; /** * Check if channel is connected, recreate if not. */ private checkConnectionHealth; /** * Run an async op but reject if it doesn't settle within `ms`, so a hung await * can't leave isRecreatingChannel stuck true and disable the watchdog. Mirrors * closeWithTimeout() in desktop-commander-integration.ts. */ private sleep; /** * Block until realtime-js has left the 'disconnecting' state it enters on * disconnect(), so the next subscribe() actually dials a socket instead of * hitting connect()'s early return. Bounded either way — worst case we cost * a recreate SOCKET_SETTLE_MAX_MS. */ private waitForSocketSettled; private withTimeout; /** * Recreate the channel by destroying old one and creating fresh instance. */ private recreateChannel; /** * Claim a call. True only when THIS update flipped the row pending -> * executing, which is what makes dual delivery safe across processes. * .eq('status','pending') makes it conditional; .select('id') makes the * result observable. On a transient DB error it returns true (execute * anyway), matching prior behaviour — so device.ts's in-memory guard is what * actually guarantees exactly-once within a process. */ markCallExecuting(callId: string): Promise<boolean>; updateCallResult(callId: string, status: string, result?: any, errorMessage?: string | null): Promise<void>; /** * Reachable by SOME transport — the private channel or, during the * transition, the independent legacy one. Gates the heartbeat and `status`: * asking only about the private channel starves last_seen for a device whose * legacy channel is fine, and the 45s sweep then blacks it out. * Collapses to a single check at the flip (009). */ private isReachable; /** * Set `status` from actual reachability. `status` is transport-agnostic (the * server filters on it), so it must not follow one channel's health — the * private channel's error path re-fires on every rejoin and would oscillate * the row against the heartbeat. Same predicate as the heartbeat gate. */ private syncReachabilityStatus; /** * Serialize the channel-callback status writes. They fire from un-awaited * callbacks, and inside recreateChannel() a teardown's 'offline' and the * fresh join's 'online' land ~100-300ms apart — unordered, 'offline' can win * and leave a healthy device undispatchable until the next heartbeat. * * Not the single writer: updateHeartbeat, registerDevice and setOffline's * subprocess write status directly, so this is not total ordering. */ private queueStatusWrite; /** * Heartbeat cadence for the tier this device is CURRENTLY in. Follows the * capability flag (what the server actually tiers its sweep on), not the * build — see LEGACY_HEARTBEAT_INTERVAL. */ private heartbeatIntervalMs; updateHeartbeat(deviceId: string): Promise<void>; startHeartbeat(deviceId: string): void; /** Arm (or re-arm) the last_seen timer at the current tier's cadence. */ private scheduleHeartbeat; stopHeartbeat(): void; setOnlineStatus(deviceId: string, status: 'online' | 'offline'): Promise<void>; setOffline(deviceId: string | undefined): Promise<void>; unsubscribe(): Promise<void>; } export {};