@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
200 lines (199 loc) • 8.62 kB
TypeScript
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 {};