Skip to content

TypeScript SDK — API reference

The hermes-mobile-client package is a client of the Hermes Mobile Adapter v1 protocol for apps and Node scripts. It knows nothing about the UI: only transport and callbacks. There are no runtime dependencies — the standard WebSocket is used.

Source: sdk/typescript/hermesClient.ts. Frame specification: ../../protocol/index.md.

Installation

npm i hermes-mobile-client

The package is built in two formats (the exports field):

// ESM — React Native, Expo, web, bundlers, modern Node
import { HermesClient } from 'hermes-mobile-client';

// CommonJS — plain Node scripts
const { HermesClient } = require('hermes-mobile-client');

Requirements and compatibility:

What Value
Node >= 22 (the engines field)
Platforms React Native / Expo, browser (web), Electron, Node
Dependencies none (the global WebSocket is used; in Node 22 it is built in)
Types .d.ts bundled (dist/esm/hermesClient.d.ts)
Package version 1.0.1

The token is a secret

token is the MOBILE_TOKEN on the agent owner's side. Do not embed it in public bundles (web/Electron) and do not commit it: the token grants access to the agent's sessions, tools and memory. On the web, serve it from your own backend.

Who configures access

The device list (allowlist) and pairing are configured by the agent owner (hermes pairing). The SDK does not set them: if a device is not approved, the gateway will close the connection with code 1008, and the client will move to the failed state with the auth_failed error (retrying is pointless).


Class HermesClient

Constructor

new HermesClient(options: ClientOptions)

ClientOptions:

Field Type Req. Default Meaning
url string yes — Gateway address, ws://host:8787/ws or wss://host/ws. The token is not passed in the URL.
token string yes — Application token (MOBILE_TOKEN). Sent only in the hello frame.
device DeviceInfo yes — The device as it is seen in Hermes (pairing, allowlist, cron).
user { id?: string; name?: string } no {} The user — carried in the hello frame.
handlers HermesClientHandlers no {} Event callbacks (see below).
maxBackoffMs number no 15000 Ceiling of the pause between reconnection attempts, ms.

There is no openTimeout in the TS SDK

Unlike the Python SDK (which has open_timeout), the TypeScript constructor does not accept openTimeout: the socket open timeout is delegated to the runtime's global WebSocket. There is no ClientOptions field with that name.

DeviceInfo:

Field Type Meaning
id string Device identifier (used in mobile:<id>).
name string Display name.
platform string ios / android / web / another label.
appVersion string Application version.

Properties and utility methods

Member Signature What it does
ready get ready(): boolean true if the state is connected.
setHandlers setHandlers(handlers: HermesClientHandlers): void Completes the current callbacks (merged over those already set). Lets you assign handlers after the constructor.
connect connect(): void Clears the manual-close flag and opens the connection. After open it sends hello. Returns void — readiness arrives in onReady/onState.
disconnect disconnect(): void Marks the close as manual, cancels reconnection, closes the socket, moves the state to idle.

Sending text, voice and utility frames

Method Signature Returns Frame Errors
sendText sendText(text: string, messageId?: string): string id of the sent message text Does not throw. Empty text is not validated — the server will reply with a bad_frame error in onError.
sendAudio sendAudio(base64: string, format: 'wav' \| 'ogg', transcript?: string, messageId?: string): string id audio Only the compiler constrains the type; there is no runtime format check. transcript is optional.
sendTyping sendTyping(): void — typing Does not throw.
acknowledge acknowledge(messageId: string): void — ack Acknowledgement of displaying/playing the message with the given id. Does not throw.
ping ping(): void — ping Sends ts = Date.now(). The pong reply is ignored. Does not throw.

If id is not passed, it is generated by makeId(). The adapter treats a repeat with the same id as a duplicate and ignores it — this is the idempotency of redelivery.

No connection — not an exception, but onError

With a closed socket, all send* methods do not throw, but call onError({ code: 'offline', message: 'Нет связи со шлюзом', fatal: false }). Check the state via client.ready before sending.

Prompt cards

answerPrompt(promptId: string, optionId?: string, text?: string): void

Sends the prompt frame. optionId is lowercased.

Parameter Req. Meaning
promptId yes id from onPrompt.
optionId no¹ For exec_approval — once\|session\|always\|deny; for slash_confirm — once\|always\|cancel; for clarify — c0..cN or other.
text no¹ Free-form answer (for clarify with other).

¹ At least one of optionId / text is required.

Throws Error if promptId is empty, or if neither optionId nor text is set.

An unknown option is replaced with a safe one

The adapter itself replaces an unrecognized value with deny (for command approval) or cancel (for a slash command). You cannot rely on the client — send a correct value.

Media

Method Signature Returns Frames
sendMedia sendMedia(kind: string, source: MediaSource, options?: MediaSendOptions): string id 1..N media frames (part/parts)
sendImage sendImage(source: MediaSource, options?: MediaSendOptions): string id media with kind=image
sendDocument sendDocument(source: MediaSource, options?: MediaSendOptions): string id media with kind=document
sendVoice sendVoice(source: MediaSource, options?: MediaSendOptions): string id media with kind=audio

MediaSource = string | Uint8Array | { path: string }:

  • string — base64 only (decoded);
  • Uint8Array — ready bytes;
  • { path: string } — a file path, read from disk (Node only; in the browser/RN it will throw Error).

MediaSendOptions:

Field Type Default Meaning
mime string by extension MIME type of the frame.
filename string — File name.
caption string — Caption.
messageId string makeId() Idempotency.
chunkBytes number 3 МБ (MEDIA_CHUNK_BYTES) Chunk size: the file is split into part/parts frames.

Notes:

  • kind for sendMedia is lowercased and must be one of image | video | document | audio — otherwise it throws Error. "Voice" is sent as audio (the sendVoice wrapper).
  • An empty file (0 bytes) → throws Error.
  • Large files are automatically split by chunkBytes (3 MB by default): the adapter reassembles the chunks into one file, so the size does not run into the limit of a single WS frame.
  • If mime/filename are absent, the MIME is guessed by extension (guessMime), otherwise application/octet-stream.

In TS a string is base64, not a path

In the TypeScript SDK, a string in source is always treated as base64. An example like client.sendVoice('/tmp/answer.ogg') with a path string (it appeared in the README) will not read the file, but decode the path as base64. For a file, use client.sendVoice({ path: '/tmp/answer.ogg' }).


Callbacks HermesClientHandlers

All callbacks are optional; an exception inside a callback is not caught by the SDK.

Callback Signature When it is called Argument
onState (state: ConnectionState, detail?: string) => void On every state change Current state; detail — text (e.g. попытка 2)
onReady (ready: ReadyPayload) => void The ready frame arrived (after hello) Session data
onMessage (message: AgentMessage) => void A message frame without edit A chunk of the agent's reply
onMessageEdit (message: AgentMessage) => void A message frame with edit: true An edit of a previously displayed message with the same id (streaming)
onTyping () => void A typing frame without stop — (the agent is typing)
onTypingStop () => void A typing frame with stop: true — (clear the indicator)
onAvatar (state: AvatarState) => void An avatar frame idle \| thinking \| speaking (default idle)
onPrompt (prompt: PromptPayload) => void A prompt frame A prompt card with options
onPromptDone (done: PromptDonePayload) => void A prompt_done frame The card is closed (an option was chosen or it expired)
onMedia (media: MediaPayload) => void A media frame, after all parts are assembled One ready file
onError (error: ProtocolError) => void An error frame, an auth_failed rejection, a disconnect (transport), or an offline send Code and message

Call order:

  • onState is called before the "content" callbacks on a state change: connecting → (connected and onReady) on the first session; on a disconnect reconnecting with detail, on rejection — failed followed by onError(auth_failed).
  • onMessageEdit is a separate branch: an edit frame goes to onMessageEdit, not to onMessage.
  • onMedia is called once per whole file: intermediate parts are assembled internally and not exposed.

Types and constants

States and version

Name Type Meaning
PROTOCOL_VERSION const number 1
AvatarState 'idle' \| 'thinking' \| 'speaking' Avatar state
ConnectionState 'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'failed' Connection state

ReadyPayload (the ready frame)

Field Type Meaning
v number Protocol version.
session string Session identifier.
device { id: string; name: string } The device as seen by the gateway.
avatar_url string Avatar URL for the WebView.
capabilities object (see below) Gateway capabilities.

capabilities:

Field Type Meaning
text boolean Text support.
voice boolean Voice support.
typing boolean "typing" indicator support.
avatar_states? AvatarState[] Available avatar states.
media_in? string[] Media types accepted by the agent.
media_out? string[] Media types from the agent.
prompts? string[] Card types.
edit? boolean Message edit support.
max_media_bytes? number File limit.
max_text_chars? number Text limit.

New fields are optional — an old gateway does not send them, check with ?. .

AgentMessage (the message frame)

Field Type Meaning
id string Agent message identifier.
text string Text of the chunk.
index number Chunk number (from chunk.index, default 0).
total number Total chunks (from chunk.total, default 1).
final boolean Last chunk / end of the reply.
edit? boolean true — an edit of an already displayed message with the same id.

PromptOption, PromptPayload, PromptDonePayload

PromptOption:

Field Type Meaning
id string Option identifier.
label string Button label.
style? string Style hint.

PromptPayload (the prompt frame):

Field Type Meaning
id string Card identifier.
kind string exec_approval | slash_confirm | clarify.
text string Question text.
options PromptOption[] Answer options.
session string The session the card belongs to.
timeout_seconds number How long to wait for an answer (seconds).

PromptDonePayload (the prompt_done frame):

Field Type Meaning
id string Card identifier.
option_id? string The option that was applied.
expired? boolean true — the card expired.

MediaPayload (the media frame)

Field Type Meaning
id string Message identifier.
kind string image | video | document | audio | voice | animation.
mime string MIME type.
filename string File name.
caption string Caption.
part number Part number (0-based).
parts number Total parts.
final boolean Last part.
size number File size, bytes.
data? string base64 of the whole file (all parts assembled), or absent.
url? string A link, if the file is served by link.
reply_to? string id of the message the media replies to.

For display: media.data ? \data:${media.mime};base64,${media.data}` : media.url`.

Other

ProtocolError:

Field Type Meaning
code string Stable code (auth_failed, transport, offline, bad_frame, …).
message string Text for the user.
fatal boolean true — retrying is pointless.

MEDIA_CHUNK_BYTES — const number, 3 * 1024 * 1024 (media chunk size).

Functions:

Function Signature Meaning
makeId (): string Message identifier (time in base36 + random suffix).
bytesToBase64 (bytes: Uint8Array): string Dependency-free base64 encoding of bytes.
base64ToBytes (input: string): Uint8Array base64 decoding (non-base64 characters are dropped, its own decoder — no need for atob/Buffer).

Which frames arrive in which callback

Server frame Callback
ready onReady (and onState('connected'))
message (edit not set/false) onMessage
message (edit: true) onMessageEdit
typing (without stop) onTyping
typing (stop: true) onTypingStop
avatar onAvatar
prompt onPrompt
prompt_done onPromptDone
media (after all parts are assembled) onMedia
error onError
pong ignored
unknown frame ignored (protocol requirement)
socket event onerror onError (code: 'transport', fatal: false)
close with code 1008 onState('failed') + onError (code: 'auth_failed', fatal: true)
send with a closed socket onError (code: 'offline', fatal: false)

Which methods send which frames

Method Frame(s) client → server
connect WS open + hello { t, v, token, device, user }
disconnect WS close
sendText text { t, id, text }
sendAudio audio { t, id, format, data, text? }
sendTyping typing { t }
acknowledge ack { t, id }
ping ping { t, ts }
answerPrompt prompt { t, prompt_id, option_id?, text? }
sendMedia / sendImage / sendDocument / sendVoice media { t, id, kind, mime, data, part, parts, filename?, caption? } (1..N frames)

Examples

Connecting and chatting

import { HermesClient } from 'hermes-mobile-client';

const client = new HermesClient({
  url: 'wss://agent.example.com/ws',
  token: process.env.MOBILE_TOKEN!,
  device: { id: 'app-1', name: 'Телефон', platform: 'android', appVersion: '1.0.0' },
  handlers: {
    onReady: (ready) => console.log('сессия', ready.session),
    onState: (state, detail) => console.log('связь:', state, detail ?? ''),
    onMessage: (m) => append(m.text, m.final),          // a chunk of the reply
    onMessageEdit: (m) => replace(m.id, m.text),         // streaming: redraw the bubble
    onAvatar: (state) => avatar.setState(state),         // idle | thinking | speaking
    onTyping: () => showTyping(),
    onTypingStop: () => hideTyping(),
    onError: (e) => console.warn(e.code, e.message, e.fatal),
  },
  maxBackoffMs: 15000,
});

client.connect();
client.sendText('привет, Hermes');

Command approval card

const client = new HermesClient({
  /* … */
  handlers: {
    onPrompt: (prompt) => {
      // prompt.options: [{ id, label, style }]
      renderCard(prompt.text, prompt.options, (optionId) => {
        client.answerPrompt(prompt.id, optionId);   // once | session | always | deny
      });
    },
    onPromptDone: (done) => {
      closeCard(done.id, done.expired ? 'истекла' : done.option_id ?? '');
    },
  },
});

client.connect();

// Clarifying question: a free-form answer together with 'other'
// client.answerPrompt(prompt.id, 'other', 'мой вариант');

Sending a photo and receiving media

const client = new HermesClient({
  /* … */
  handlers: {
    onMedia: (media) => {
      // media.data — base64 of the whole file; media.url — if the file is served by link
      const src = media.data
        ? `data:${media.mime};base64,${media.data}`
        : media.url;
      show(media.kind, src, media.filename, media.caption);
    },
  },
});

client.connect();

client.sendImage({ path: '/tmp/chart.png' }, { caption: 'график' });      // file (Node)
client.sendDocument(new Uint8Array(pdfBytes), { filename: 'отчёт.pdf' }); // bytes
client.sendVoice({ path: '/tmp/answer.ogg' });                // file → { path }
client.sendMedia('video', { path: '/tmp/clip.mp4' });