Skip to content

zidane/ipc/remote

The client side of remote zidane hosts, over WebSocket with saved servers and settings sync.

import { … } from 'zidane/ipc/remote'src/ipc/remote.ts

Use zidane/ipc/remote to connect to a zidane-server from Node 22 or later, or Bun. connectWsClient returns the same typed client as the unix-socket path, and the saved-hosts helpers keep a registry of the servers you connect to. It sits outside zidane/ipc, so an app loads none of it until remote hosts are on. Server clients covers errors, reconnection and settings sync.

TypeScript
import { connectWsClient } from 'zidane/ipc/remote'

const client = await connectWsClient({
  url: 'wss://zidane.example.com/',
  token: process.env.ZIDANE_HOST_TOKEN,
})
const sessions = await client.request('sessions.list', {})
console.log(sessions.length)
client.close()

Functions#

classifyWsFailureFunction#

A refused token stays refused until the user changes something: no retry loop. Another protocol version stays until one side is upgraded: a slow re-check.

TypeScript
function classifyWsFailure(err: unknown): HostFailureKind
Parameters
NameTypeDescription
errunknown

The error a connect attempt failed with.

HostFailureKind

auth for a refused token, identity for another host answering, protocol for a version mismatch, retry otherwise.

src/ipc/hosts/ws.ts:21

connectWsClientFunction#

Connects to a remote zidane server over WebSocket and completes the ipc.hello handshake. A refused upgrade rejects with HostAuthError for a 401 or 403, and HostBusyError for a 429. A hello from another host than expectHostId rejects with HostIdentityError. A version mismatch rejects with IpcVersionMismatchError and leaves the server running.

TypeScript
function connectWsClient(options: WsClientOptions): Promise<IpcClient>
Parameters
NameTypeDescription
optionsWsClientOptions

Server URL, token and connection settings.

Promise<IpcClient>

A typed client.

Example
import { connectWsClient } from 'zidane/ipc/remote'

const client = await connectWsClient({
  url: 'wss://zidane.example.com/',
  token: process.env.ZIDANE_HOST_TOKEN,
})
const sessions = await client.request('sessions.list', {})
console.log(sessions.length)
client.close()

src/ipc/transports/ws-client.ts:251

createSyncEngineFunction#

Creates a sync engine for this computer. Starts file watchers once a server is set to keep in sync.

TypeScript
function createSyncEngine(options: SyncEngineOptions): SyncEngine
Parameters
NameTypeDescription
optionsSyncEngineOptions

Paths, providers and timing for the engine.

src/chat/sync/engine.ts:139

createWsHostRegistryFunction#

The registry in dataDir, connecting over WebSocket.

TypeScript
function createWsHostRegistry(options: { dataDir: string }): HostRegistry
Parameters
NameTypeDescription
options{ dataDir: string }

Where the registry keeps its data.

HostRegistry

The host registry.

src/ipc/hosts/probe.ts:97

hostCaBundleFunction#

The runtime's roots, the OS store's and NODE_EXTRA_CA_CERTS's, read once. null when the OS store adds nothing (or can't be read): the runtime's default then applies as it is.

TypeScript
function hostCaBundle(): string[] | null
string[] | null

src/ipc/transports/tls-trust.ts:39

keepHostConnectedFunction#

Connects a host and keeps it connected: reconnects with backoff when the connection drops, unless the failure is classified as final.

TypeScript
function keepHostConnected(options: HostConnectionLoopOptions): HostConnectionLoop
Parameters
NameTypeDescription
optionsHostConnectionLoopOptions

How to connect, where to report, and the callbacks.

HostConnectionLoop

A handle to stop the loop or retry now.

src/ipc/hosts/connection-loop.ts:97

probeUnsavedWsHostFunction#

The add form's "test": the registry's own checks and probe over WebSocket, saving nothing.

TypeScript
function probeUnsavedWsHost(
  target: { token: string; url: string },
): Promise<HostProbeResult>
Parameters
NameTypeDescription
target{ token: string; url: string }

URL and token to try.

Promise<HostProbeResult>

The probe result.

src/ipc/hosts/ws.ts:34

probeWsHostFunction#

Connect, read the host's identity and daemon.info, disconnect. Never throws.

TypeScript
function probeWsHost(
  target: { timeoutMs?: number; token: string; url: string },
): Promise<HostProbeResult>
Parameters
NameTypeDescription
target{ timeoutMs?: number; token: string; url: string }

URL, token and an optional timeout in milliseconds. The timeout defaults to HOST_PROBE_TIMEOUT_MS.

Promise<HostProbeResult>

Who answered and how fast, or a classified failure.

src/ipc/hosts/probe.ts:59

startSavedHostsFunction#

Starts the connections of a client's saved servers and keeps the directory in step with the registry. Each saved server gets a directory entry and a connection loop. A connection whose host id differs from the saved one is closed and reported as identity.

TypeScript
function startSavedHosts(options: SavedHostsOptions): SavedHostsController
Parameters
NameTypeDescription
optionsSavedHostsOptions

Registry, directory and how to dial.

SavedHostsController

A controller over the saved servers.

src/ipc/hosts/saved-hosts.ts:108

startWsSavedHostsFunction#

Keeps the saved servers of a client connected over WebSocket. Uses the registry in dataDir unless registry is given, and dials with connectWsClient unless connect is given.

TypeScript
function startWsSavedHosts(options: WsSavedHostsOptions): SavedHostsController
Parameters
NameTypeDescription
optionsWsSavedHostsOptions

Data directory, host directory and callbacks.

SavedHostsController

A controller to list, add, remove, test and retry hosts, and to stop them all.

src/ipc/hosts/ws.ts:57

trustSystemCertificatesFunction#

Node: every TLS client of this process trusts hostCaBundle from now on (the desktop app's main process). Whether it applied.

TypeScript
function trustSystemCertificates(): boolean
boolean

src/ipc/transports/tls-trust.ts:51

Classes#

HostAuthErrorClass#

The server refused this client's credential: 401 (no valid token) or 403 (not allowed here). reason only when the server sent a code clients know.

TypeScript
class HostAuthError extends Error
TypeScript
new HostAuthError(
  url: string,
  status: 401 | 403,
  reason?: 'origin' | 'insecure-transport' | 'untrusted-proxy' | 'scope',
)
Parameters
NameTypeDescription
urlstring

The server URL.

status401 | 403

HTTP status of the refusal.

reason?'origin' | 'insecure-transport' | 'untrusted-proxy' | 'scope'

Refusal code the server sent, when any.

HostAuthError properties
NameTypeDescription
reason?readonly'origin' | 'insecure-transport' | 'untrusted-proxy' | 'scope'

Refusal code the server sent, when any.

statusreadonly401 | 403

HTTP status of the refusal.

urlreadonlystring

The server URL, scheme, host and path only.

src/ipc/transports/ws-client.ts:99

HostBusyErrorClass#

The server turned the upgrade down with a 429: too many open connections (per token, per address) or upgrades (per address). Transient.

TypeScript
class HostBusyError extends Error
TypeScript
new HostBusyError(url: string)
Parameters
NameTypeDescription
urlstring

The server URL.

src/ipc/transports/ws-client.ts:118

MissingHostTokenErrorClass#

No token is saved for this server (removed by hand, or another window removed it).

TypeScript
class MissingHostTokenError extends Error
TypeScript
new MissingHostTokenError()

src/ipc/hosts/saved-hosts.ts:41

Constants#

HOST_PROBE_TIMEOUT_MSConstant#

Bound on one probe: the upgrade, the handshake and daemon.info.

TypeScript
const HOST_PROBE_TIMEOUT_MS = 10_000

src/ipc/hosts/probe.ts:13

Interfaces#

HostConnectionLoopInterface#

Handle returned by keepHostConnected to stop the loop or retry at once.

TypeScript
interface HostConnectionLoop
HostConnectionLoop properties
NameTypeDescription
retry() => void

Try now (a manual retry, or after an auth fix).

stop() => void

Stops the loop, aborts a pending connect and closes the live client.

src/ipc/hosts/connection-loop.ts:81

HostConnectionLoopOptionsInterface#

Options for keepHostConnected: how to connect, where status goes, and the backoff bounds.

TypeScript
interface HostConnectionLoopOptions
HostConnectionLoopOptions properties
NameTypeDefaultDescription
classify?(err: unknown) => HostFailureKind

auth: the token was refused; identity: another host answered. Both are for good (no retry until retry()). protocol: another protocol version, re-checked every recheckMs. Default: every failure retries.

connect(options: { onClose: () => void; signal: AbortSignal }) => Promise<IpcClient>

One connect attempt. onClose fires when that connection drops (not on close()).

maxDelayMs?number30000

Upper backoff bound in milliseconds.

minDelayMs?number1000

Lower backoff bound in milliseconds (tests shrink it).

onConnected?(client: IpcClient) => void

Connected (also after a reconnect).

onDisconnected?() => void

The live connection dropped (a reconnect is scheduled).

onSignal?(signal: HostSignal) => void

Every signal of the live connection.

recheckMs?numberPROTOCOL_RECHECK_MS

Re-check interval for a protocol failure, in milliseconds (tests shrink it).

update(patch: HostConnectionPatch) => void

Receives each status change of the host.

src/ipc/hosts/connection-loop.ts:46

HostConnectionPatchInterface#

What the loop says about its host, applied by the owner to its directory entry.

TypeScript
interface HostConnectionPatch
HostConnectionPatch properties
NameTypeDescription
client?IpcClient | null

The live client, or null when there is none.

error?string

Why the last connect failed; undefined once connected.

serverName?string

The host's own name, from its ipc.hello.

status?HostConnectionStatus

Connection status to show.

src/ipc/hosts/connection-loop.ts:34

SavedHostsControllerInterface#

Handle returned by startSavedHosts to list, add, remove, rename, test and retry saved servers.

TypeScript
interface SavedHostsController
SavedHostsController properties
NameTypeDescription
add(input: { name?: string; token: string; url: string }) => Promise<HostEntry>

Verify, save, connect (in place when already saved). Throws a HostRegistryError.

list() => HostEntry[]

The saved hosts, read fresh (other windows share the file).

remove(id: string) => boolean

Removes a server from the registry and its entry from the directory. Returns whether it existed.

rename(id: string, name: string) => HostEntry

Renames a saved server and returns the updated entry.

retry(id: string) => void

Reconnect now (after an outage or a token fix).

retryAll() => void

Reconnect every saved server that is not connected now, its backoff reset (the computer woke up).

setUrl(id: string, url: string) => Promise<HostEntry>

Move a saved server to a new URL (its saved token; the same server must answer), then reconnect it in place. Throws a HostRegistryError.

stop() => void

Disconnects every saved server and removes their directory entries. The registry is not changed.

test(target: string | { id: string } | { token: string; url: string }) => Promise<HostProbeResult>

A saved server (by id), or an unsaved URL + token (the add form's test).

src/ipc/hosts/saved-hosts.ts:81

SavedHostsDirectoryInterface#

The client's host directory, as the saved servers see it. Keys are host ids.

TypeScript
interface SavedHostsDirectory
SavedHostsDirectory properties
NameTypeDescription
remove(key: string) => void

The server left the registry, or the saved servers stopped.

saved() => { key: string; url: string }[]

The saved servers it holds now, with their URL (a --host server excluded).

update(key: string, patch: HostConnectionPatch & { name?: string; serverVersion?: string }) => void

Applies a connection patch, a new name or a server version to the entry.

upsert(entry: HostEntry, token: string | undefined) => void

Add the entry, or reconfigure the one under its key in place (status connecting).

src/ipc/hosts/saved-hosts.ts:29

SavedHostsOptionsInterface#

Options for startSavedHosts: the registry, the host directory, the connect function and callbacks.

TypeScript
interface SavedHostsOptions
SavedHostsOptions properties
NameTypeDefaultDescription
classify?(err: unknown) => HostFailureKind

auth: refused for good. A missing token is always auth, another host answering always identity.

connect(entry: HostEntry, token: string, options: { onClose: () => void; signal: AbortSignal }) => Promise<IpcClient>

One connect attempt to a saved server, with its token read fresh.

directorySavedHostsDirectory

The client's host directory the entries are kept in.

maxDelayMs?number30000

Upper backoff bound in milliseconds.

minDelayMs?number1000

Lower backoff bound in milliseconds.

onConnected?(key: string, client: IpcClient) => void

Called when a saved server connects, also after a reconnect.

onDisconnected?(key: string) => void

Called when a saved server's connection drops.

onSignal?(key: string, signal: HostSignal) => void

Called with every signal of a connected saved server.

probeUnsaved?(target: { token: string; url: string }) => Promise<HostProbeResult>

The add form's test of an unsaved URL + token (probeUnsavedHost).

registryHostRegistry

The registry of saved servers.

skip?(entry: HostEntry) => boolean

Saved entries the directory already reaches another way (the TUI's --host server).

src/ipc/hosts/saved-hosts.ts:49

ServerSyncConfigInterface#

Saved sync settings for one server, kept in hosts-sync.json.

TypeScript
interface ServerSyncConfig
ServerSyncConfig properties
NameTypeDescription
categoriesSyncCategory[]

Categories sent to the server.

keysstring[]

Providers whose stored API key goes to this server.

lastError?string

Message of the last failed push.

lastSyncAt?number

Time of the last successful push in epoch ms.

modeSyncMode

Whether and how this computer syncs to the server.

pendingSyncCategory[]

Categories changed here and not pushed yet (offline queue).

resultsPartial<Record<SyncCategory, SyncPushResult>>

What the last push of each category did there.

sentPartial<Record<SyncCategory, string>>

Per category, a hash of what was last pushed (a restart finds what changed meanwhile).

src/chat/sync/local.ts:26

SyncClientInterface#

The part of a host connection sync uses (an IpcClient is one).

TypeScript
interface SyncClient
SyncClient properties
NameTypeDescription
hello{ sync?: boolean; syncCategories?: SyncCategory[] }

What the server announced when it connected. sync is set when it supports settings sync.

request((method: 'sync.status', params: Record<string, never>) => Promise<SyncStatus>) & ((method: 'sync.claim', params: { categories?: SyncCategory[]; deviceId: string; deviceName: string; keys?: string[]; takeover?: boolean }) => Promise<SyncClaimResult>) & ((method: 'sync.push', params: { category: SyncCategory; deviceId: string; payload: SyncPayload }) => Promise<SyncPushResult>) & ((method: 'sync.reset', params: { field: string }) => Promise<{ reset: boolean }>) & ((method: 'sync.release', params: { deviceId: string }) => Promise<{ released: boolean }>)

Sends a sync RPC to the server and resolves with its reply.

src/chat/sync/engine.ts:29

SyncEngineInterface#

Pushes this computer's settings to saved servers and tracks the state each one reports. Servers are identified by key.

TypeScript
interface SyncEngine
SyncEngine properties
NameTypeDescription
configure(key: string, patch: Partial<Pick<ServerSyncConfig, 'mode' | 'categories' | 'keys'>>) => Promise<void>

Change the mode, categories or opted-in keys; pushes what that calls for.

connected(key: string, client: SyncClient) => void

Reports that a connection to the server opened. Reads its record and pushes what its mode calls for.

deviceDeviceIdentity

Identity of this computer.

disconnected(key: string) => void

Reports that the connection to the server closed.

localInfo() => SyncLocalInfo

What this computer has that the UIs mention: API keys to opt in, extensions that can't sync, stdio MCP servers.

notifyLocalChange(category?: SyncCategory) => void

This computer changed something (the app's settings stream): check, push if it differs.

pushAll(key: string) => Promise<void>

Push every selected category now (whatever the mode, but off).

refresh(key: string) => Promise<void>

Read the server's record again.

removed(key: string) => void

The server was removed here: forget its sync config (the server keeps what it has).

reset(key: string, field: string) => Promise<void>

Drop an override on the server (the next push writes the field again).

serverChanged(key: string) => void

A server's sync:changed signal.

stop() => void

Stops the file watchers and pending pushes.

subscribe(listener: () => void) => () => void

Calls listener when any view changes. Returns a function that removes it.

takeOver(key: string) => Promise<void>

Become the source of a server another device syncs to, then push.

view(key: string) => SyncView

Returns what a UI shows for the server.

watching() => boolean

File watchers are running (some server keeps in sync).

src/chat/sync/engine.ts:77

SyncEngineOptionsInterface#

Options for createSyncEngine.

TypeScript
interface SyncEngineOptions
SyncEngineOptions properties
NameTypeDescription
dataDirstring

Directory holding the sync state files and the device identity.

debounceMs?number

Wait after a change before checking (a save is often several writes). Default 500 ms.

deviceName?string

Default: this computer's host name.

homestring

User home directory, where user-scope files and skills are read from.

prefixstring

Storage prefix, such as .zidane.

providers() => Readonly<Record<string, ProviderDescriptor>>

Returns the provider descriptors used to list API keys that can be opted in.

statePathstring

Path of the file holding the app's saved settings.

watch?boolean

File watchers (default on); off, only notifyLocalChange finds changes.

src/chat/sync/engine.ts:41

SyncLocalInfoInterface#

What this computer has that sync UIs mention.

TypeScript
interface SyncLocalInfo
SyncLocalInfo properties
NameTypeDescription
accountsLocalProviderAccounts[]

Every provider with an API key or a sign-in here (the GUI's accounts list).

keyProviders{ key: string; label: string }[]

Providers with an API key that can be opted in, with display labels.

localExtensionsstring[]

Names of extensions that cannot sync because they are not installed from npm or GitHub.

stdioMcpsstring[]

Names of stdio MCP servers, whose commands may be missing on the server.

src/chat/sync/engine.ts:113

SyncViewInterface#

What a UI shows for one server.

TypeScript
interface SyncView
SyncView properties
NameTypeDescription
busyboolean

A push or refresh is running.

configServerSyncConfig

Saved sync settings for the server.

conflictstring | null

Another device syncs to this server: its name.

connectedboolean

Whether a connection to the server is open.

statusSyncStatus | null

The server's record, when read.

supportedboolean

The server takes sync (ipc.hello.sync); false for an older server.

src/chat/sync/engine.ts:61

WsClientOptionsInterface#

Options for connectWsClient.

TypeScript
interface WsClientOptions
WsClientOptions properties
NameTypeDescription
appVersion?string

Client zidane version, surfaced in the host's diagnostics.

ca?readonly string[]

Certificate authorities (PEM) a wss:// server's certificate may chain to. Default: the runtime's roots and the OS store (hostCaBundle). Bun only: under Node the process default applies.

expectHostId?string

The host id a saved host answered with when it was added: a hello with any other id closes the connection and rejects with a HostIdentityError (nothing is read from that host).

heartbeatMs?number

$/ping interval (./client-core). Default WS_HEARTBEAT_MS; 0 disables.

onClose?() => void

The connection dropped (server exit, network error, a missed deadline or heartbeat). Not called for close().

openTimeoutMs?number

Bound on the upgrade (not the handshake, which has its own). Default WS_OPEN_TIMEOUT_MS.

serve?{ notification?: (method: string, params: unknown) => void; request: (method: string, params: unknown) => Promise<unknown> }

Answer the host's requests and notifications on this connection: a machine serving its host (docs/MACHINES.md). A throw becomes a JSON-RPC error (its code when it carries one). Default: a client refuses requests and reads only ipc.signal.

signal?AbortSignal

Abandon the connect (upgrade and handshake): it rejects with the signal's reason.

token?string

Bearer token for the server, see ./ws-auth.

tokenCarrier?WsTokenCarrier

How the token travels. Default: the Authorization header where the runtime's WebSocket can set one (Bun), else the subprotocol fallback.

urlstring

ws:// or wss:// URL of the server.

src/ipc/transports/ws-client.ts:39

Types#

WsSavedHostsOptionsType#

Options for startWsSavedHosts: SavedHostsOptions with the registry, dialer and classifier filled in. dataDir holds the saved-hosts registry file.

TypeScript
type WsSavedHostsOptions = Omit<SavedHostsOptions, 'registry' | 'connect' | 'classify' | 'probeUnsaved'> & { appVersion?: string; connect?: SavedHostsOptions['connect']; dataDir: string; registry?: SavedHostsOptions['registry'] }

src/ipc/hosts/ws.ts:42