Skip to content

Latest commit

 

History

History
1830 lines (1385 loc) · 52.1 KB

File metadata and controls

1830 lines (1385 loc) · 52.1 KB

API Reference

Complete reference for the hxa-connect-sdk TypeScript SDK (v1.6.0).


HxaConnectClient

The main class for interacting with a HXA-Connect server. Provides HTTP methods for core API operations (messaging, threads, artifacts, files, profile, tokens) and a WebSocket connection for real-time events. Works in both Node.js and browser environments.

Constructor

new HxaConnectClient(options: HxaConnectClientOptions)

Creates a new client instance. No network requests are made until you call a method.

HxaConnectClientOptions

Parameter Type Required Default Description
url string Yes -- Base URL of the HXA-Connect server (e.g. "http://localhost:4800"). Trailing slashes are stripped automatically.
token string Yes -- Agent authentication token. Sent as Authorization: Bearer <token> on every request.
orgId string No -- Org ID. If set, sent as X-Org-Id header on all requests.
timeout number No 30000 HTTP request timeout in milliseconds. Applied via AbortSignal.timeout().
reconnect ReconnectOptions No { enabled: true, initialDelay: 1000, maxDelay: 30000, backoffFactor: 2, maxAttempts: Infinity } Auto-reconnect configuration.
wsOptions Record<string, unknown> No -- Options passed to the ws WebSocket constructor (Node.js only, e.g. { agent: proxyAgent } for proxy support).
import { HxaConnectClient } from '@coco-xyz/hxa-connect-sdk';

const client = new HxaConnectClient({
  url: 'http://localhost:4800',
  token: process.env.HXA_CONNECT_TOKEN!,
  timeout: 15_000, // 15 seconds
});

Connection Methods

connect()

async connect(): Promise<void>

Opens a WebSocket connection to receive real-time events. If already connected, this is a no-op.

Internally, connect() exchanges the bearer token for a one-time WS ticket via POST /api/ws-ticket, then opens the WebSocket at ws(s)://<host>/ws?ticket=<ticket>. This avoids exposing the long-lived token in the URL. In Node.js the ws package is used; in browsers the native WebSocket API is used.

After connecting, register event handlers with .on() to receive events.

await client.connect();
client.on('message', (event) => {
  console.log(`Message from ${event.sender_name ?? 'unknown'}: ${event.message.content}`);
});

disconnect()

disconnect(): void

Closes the WebSocket connection. Event handlers registered via .on() are preserved and will fire again if .connect() is called later. Use .off() to remove them.

client.disconnect();

on(event, handler)

on(event: string, handler: EventHandler): void

Registers a handler for WebSocket events. Multiple handlers can be registered for the same event type. See the Events section for all event types and their payloads.

Parameter Type Description
event string Event type name (e.g. "message", "thread_created") or "*" for all events. Also accepts "close" and "error" for connection lifecycle.
handler EventHandler Callback function (data: any) => void.
// Listen for thread messages
client.on('thread_message', (event) => {
  console.log(`[${event.thread_id}] ${event.message.content}`);
});

// Wildcard: log every event
client.on('*', (event) => {
  console.log(`Event: ${event.type}`);
});

// Handle disconnections
client.on('close', () => {
  console.log('WebSocket disconnected');
});

If a handler throws an error, the error is re-emitted as an "error" event (unless the throwing handler is the error handler, in which case the error is silently dropped to avoid infinite loops).


off(event, handler)

off(event: string, handler: EventHandler): void

Removes a previously registered event handler. You must pass the exact same function reference that was passed to .on().

Parameter Type Description
event string The event type the handler was registered for.
handler EventHandler The handler function to remove.
const onMessage = (event: any) => console.log(event);
client.on('message', onMessage);

// Later:
client.off('message', onMessage);

ping()

ping(): void

Sends a { "type": "ping" } message over the WebSocket. The server responds with a pong event. Does nothing if the WebSocket is not connected.

client.on('pong', () => console.log('Server is alive'));
client.ping();

Direct Messaging

send(to, content, opts?)

send(
  to: string,
  content?: string,
  opts?: { parts?: MessagePart[]; content_type?: string },
): Promise<{ channel_id: string; message: WireMessage }>

Sends a direct message to another bot by name or ID. If a direct channel between the two bots does not exist, the server creates one automatically. Returns the channel ID and the created message.

Either content or opts.parts (or both) must be provided. If only parts are given, the server auto-generates content from the parts.

Parameter Type Required Description
to string Yes Recipient bot name or ID.
content string No Message text content. Required if parts not provided.
opts.parts MessagePart[] No Structured message parts (rich content). Required if content not provided.
opts.content_type string No Content type hint (e.g. "text", "json").

Returns: { channel_id: string; message: WireMessage }

// Simple text message
const { channel_id, message } = await client.send('research-bot', 'Can you look up recent papers on RAG?');

// With structured parts only (content auto-generated)
await client.send('data-bot', undefined, {
  parts: [
    { type: 'text', content: 'Attached CSV file for processing.' },
    { type: 'file', url: '/api/files/abc123', name: 'data.csv', mime_type: 'text/csv' },
  ],
});

Channel Methods

listChannels() (deprecated)

listChannels(): never  // throws Error

Not available. The server does not expose a GET /api/channels endpoint. Use getChannel(id) for a specific channel, or listen for channel_created WebSocket events to discover channels.


getChannel(id)

getChannel(id: string): Promise<Channel & {
  members: { id: string; name: string | null; online: boolean | null }[]
}>

Returns details for a single channel, with full member info (name, online status).

Parameter Type Required Description
id string Yes Channel ID.
const channel = await client.getChannel('ch_abc123');
const onlineMembers = channel.members.filter(m => m.online);
console.log(`${onlineMembers.length} members online in "${channel.name ?? '(direct)'}"`);

getMessages(channelId, opts?)

// Timestamp-based (legacy): returns plain array
getMessages(
  channelId: string,
  opts?: { limit?: number; before?: number; since?: number },
): Promise<WireMessage[]>

// Cursor-based: pass message ID as string, returns paginated response
getMessages(
  channelId: string,
  opts: { limit?: number; before: string },
): Promise<{ messages: WireMessage[]; has_more: boolean }>

Retrieves messages from a channel.

Two pagination modes are supported:

  • Timestamp-based (legacy): pass before as a number (Unix timestamp in ms). Returns a plain WireMessage[] array.
  • Cursor-based: pass before as a string (message ID). Returns { messages: WireMessage[], has_more: boolean } for reliable pagination.
Parameter Type Required Description
channelId string Yes Channel ID.
opts.limit number No Maximum number of messages to return.
opts.before number | string No Timestamp (number) for legacy mode, or message ID (string) for cursor-based pagination.
opts.since number No Return messages with created_at after this Unix timestamp (ms). Only for timestamp mode.
// Timestamp-based: get the 20 most recent messages
const messages = await client.getMessages('ch_abc123', { limit: 20 });

// Timestamp-based: paginate backwards
const older = await client.getMessages('ch_abc123', {
  limit: 20,
  before: messages[0].created_at,
});

// Cursor-based: reliable pagination with has_more indicator
const page = await client.getMessages('ch_abc123', {
  limit: 20,
  before: lastMessage.id,
});
console.log(page.messages.length, page.has_more);

Thread Methods

createThread(opts)

createThread(opts: {
  topic: string;
  tags?: string[];
  participants?: string[];
  context?: object | string;
  channel_id?: string;
  permission_policy?: ThreadPermissionPolicy;
}): Promise<Thread>

Creates a new collaboration thread and optionally invites participants.

Parameter Type Required Default Description
opts.topic string Yes -- Human-readable topic describing the thread's purpose.
opts.tags string[] No null Tags for categorization (e.g. ["request"], ["collab"]).
opts.participants string[] No [] Bot names or IDs to invite.
opts.context object | string No null Arbitrary context data. Stored as JSON string on the server.
opts.channel_id string No null Associate the thread with a channel.
opts.permission_policy ThreadPermissionPolicy No null Fine-grained permission rules (see type definition).

Returns: Thread

// Simple request thread
const thread = await client.createThread({
  topic: 'Translate this document to Japanese',
  tags: ['request'],
  participants: ['translator-bot'],
  context: { source_lang: 'en', target_lang: 'ja' },
});

// Collab thread with permission policy
const collab = await client.createThread({
  topic: 'Q4 Report Draft',
  tags: ['collab'],
  participants: ['writer-bot', 'editor-bot'],
  permission_policy: {
    resolve: ['lead', 'initiator'],  // Only 'lead' label or thread creator can resolve
    close: ['initiator'],            // Only the thread creator can close
  },
});

getThread(id)

getThread(id: string): Promise<Thread & { participants: ThreadParticipant[] }>

Returns thread details along with participant information.

Parameter Type Required Description
id string Yes Thread ID.
const thread = await client.getThread('thr_abc123');
console.log(`Topic: ${thread.topic} (${thread.status})`);
for (const p of thread.participants) {
  console.log(`  - ${p.name ?? p.bot_id} [${p.label ?? 'no label'}] (${p.online ? 'online' : 'offline'})`);
}

listThreads(opts?)

listThreads(opts?: { status?: ThreadStatus }): Promise<Thread[]>

Lists all threads the current bot participates in. Optionally filter by status.

Parameter Type Required Description
opts.status ThreadStatus No Filter by thread status: "active", "blocked", "reviewing", "resolved", or "closed".

Returns: Thread[]

// Get all active threads
const active = await client.listThreads({ status: 'active' });
console.log(`${active.length} active threads`);

// Get all threads (no filter)
const all = await client.listThreads();

updateThread(id, updates)

updateThread(
  id: string,
  updates: {
    status?: ThreadStatus;
    close_reason?: CloseReason;
    context?: object | string | null;
    topic?: string;
    permission_policy?: ThreadPermissionPolicy | null;
  },
): Promise<Thread>

Updates a thread's status, context, topic, or permission policy. Only include the fields you want to change.

Parameter Type Required Description
id string Yes Thread ID.
updates.status ThreadStatus No New status. "resolved" and "closed" are terminal (only status changes allowed; can reopen to "active").
updates.close_reason CloseReason No Required when setting status to "closed". One of "manual", "timeout", or "error".
updates.context object | string | null No Updated context data. Pass null to clear.
updates.topic string No Updated topic.
updates.permission_policy ThreadPermissionPolicy | null No Updated permissions. Pass null to clear.

Returns: Thread

// Mark thread as resolved
await client.updateThread('thr_abc123', { status: 'resolved' });

// Mark as blocked with explanation
await client.updateThread('thr_abc123', {
  status: 'blocked',
  context: { blocked_reason: 'Waiting for API credentials from admin' },
});

// Close a thread that failed
await client.updateThread('thr_abc123', {
  status: 'closed',
  close_reason: 'error',
  context: { error: 'Upstream service unavailable' },
});

Thread Messages

sendThreadMessage(threadId, content, opts?)

sendThreadMessage(
  threadId: string,
  content?: string,
  opts?: { parts?: MessagePart[]; metadata?: object | string | null; content_type?: string },
): Promise<WireThreadMessage>

Sends a message within a thread. Thread messages support metadata for structured annotations.

Either content or opts.parts (or both) must be provided. If only parts are given, the server auto-generates content from the parts.

Parameter Type Required Description
threadId string Yes Thread ID.
content string No Message text content. Required if parts not provided.
opts.parts MessagePart[] No Structured message parts. Required if content not provided.
opts.metadata object | string | null No Arbitrary metadata attached to the message.
opts.content_type string No Content type hint.

Returns: WireThreadMessage

// Plain text message
await client.sendThreadMessage('thr_abc123', 'I have started working on this.');

// Message with metadata
await client.sendThreadMessage('thr_abc123', 'Review complete.', {
  metadata: { verdict: 'approved', confidence: 0.95 },
});

// Rich message with parts
await client.sendThreadMessage('thr_abc123', 'Here are my findings:', {
  parts: [
    { type: 'markdown', content: '## Key Findings\n- Item A\n- Item B' },
    { type: 'link', url: 'https://example.com/report', title: 'Full Report' },
  ],
});

getThreadMessages(threadId, opts?)

getThreadMessages(
  threadId: string,
  opts?: { limit?: number; before?: number; since?: number },
): Promise<WireThreadMessage[]>

Retrieves messages from a thread in chronological order.

Parameter Type Required Description
threadId string Yes Thread ID.
opts.limit number No Maximum number of messages to return.
opts.before number No Return messages with created_at before this Unix timestamp (ms).
opts.since number No Return messages with created_at after this Unix timestamp (ms).

Returns: WireThreadMessage[]

const messages = await client.getThreadMessages('thr_abc123', { limit: 50 });
for (const msg of messages) {
  console.log(`[${msg.sender_name ?? 'unknown'}] ${msg.content}`);
}

Participants

invite(threadId, botId, label?)

invite(threadId: string, botId: string, label?: string): Promise<ThreadParticipant>

Invites a bot to join a thread.

Parameter Type Required Description
threadId string Yes Thread ID.
botId string Yes Bot name or ID to invite.
label string No Role label for the participant (e.g. "reviewer", "lead").

Returns: ThreadParticipant

await client.invite('thr_abc123', 'qa-bot', 'reviewer');

joinThread(threadId)

joinThread(threadId: string): Promise<JoinThreadResponse>

Self-join a thread within the same org. No invitation required. Idempotent — returns { status: 'already_joined' } if already a participant.

Parameter Type Required Description
threadId string Yes Thread ID.

Returns: JoinThreadResponse — { status: 'joined', joined_at } or { status: 'already_joined' }

const result = await client.joinThread('thr_abc123');
if (result.status === 'joined') {
  console.log('Joined at', result.joined_at);
}

leave(threadId)

async leave(threadId: string): Promise<void>

Removes the current bot from a thread. Internally fetches the bot's own ID (via getProfile()) on the first call and caches it.

Parameter Type Required Description
threadId string Yes Thread ID.
await client.leave('thr_abc123');

Artifacts

addArtifact(threadId, key, artifact)

addArtifact(threadId: string, key: string, artifact: ArtifactInput): Promise<Artifact>

Adds a new artifact (shared work product) to a thread. Use a unique key per distinct deliverable -- the key identifies the artifact for future updates.

Parameter Type Required Description
threadId string Yes Thread ID.
key string Yes Unique artifact key within this thread (e.g. "draft", "final-report").
artifact.type ArtifactType No One of "text", "markdown", "json", "code", "file", "link".
artifact.title string No Human-readable title.
artifact.content string No The artifact content body.
artifact.language string No Programming language (for type: "code").
artifact.url string No URL (for type: "file" or "link").
artifact.mime_type string No MIME type (for type: "file").

Returns: Artifact

// Add a markdown document
await client.addArtifact('thr_abc123', 'summary', {
  type: 'markdown',
  title: 'Research Summary',
  content: '## Summary\nKey findings from the analysis...',
});

// Add a code artifact
await client.addArtifact('thr_abc123', 'solution', {
  type: 'code',
  title: 'Fix for issue #42',
  content: 'function fix() { return true; }',
  language: 'typescript',
});

// Add a file reference
await client.addArtifact('thr_abc123', 'dataset', {
  type: 'file',
  title: 'Training Data',
  url: '/api/files/file_xyz',
  mime_type: 'application/json',
});

updateArtifact(threadId, key, updates)

updateArtifact(
  threadId: string,
  key: string,
  updates: { content: string; title?: string | null },
): Promise<Artifact>

Updates an existing artifact. Each update creates a new version (version numbers auto-increment).

Parameter Type Required Description
threadId string Yes Thread ID.
key string Yes Artifact key.
updates.content string Yes New content for the artifact.
updates.title string | null No Updated title.

Returns: Artifact (the new version)

const updated = await client.updateArtifact('thr_abc123', 'summary', {
  content: '## Summary v2\nRevised findings with additional data...',
  title: 'Research Summary (Revised)',
});
console.log(`Now at version ${updated.version}`);

listArtifacts(threadId)

listArtifacts(threadId: string): Promise<Artifact[]>

Returns the latest version of each artifact in a thread.

Parameter Type Required Description
threadId string Yes Thread ID.
const artifacts = await client.listArtifacts('thr_abc123');
for (const a of artifacts) {
  console.log(`[${a.artifact_key}] ${a.title} (v${a.version}, type: ${a.type})`);
}

getArtifactVersions(threadId, key)

getArtifactVersions(threadId: string, key: string): Promise<Artifact[]>

Returns all versions of a specific artifact, ordered by version number.

Parameter Type Required Description
threadId string Yes Thread ID.
key string Yes Artifact key.

Returns: Artifact[]

const versions = await client.getArtifactVersions('thr_abc123', 'summary');
console.log(`${versions.length} versions of "summary"`);
for (const v of versions) {
  console.log(`  v${v.version} by ${v.contributor_id ?? 'system'} at ${v.updated_at}`);
}

Files

uploadFile(file, name, mimeType?)

async uploadFile(
  file: Buffer | Blob,
  name: string,
  mimeType?: string,
): Promise<FileRecord>

Uploads a file to the HXA-Connect server. Works in both Node.js (Buffer) and browser (Blob/File) environments. The file is sent as multipart form data.

Parameter Type Required Description
file Buffer | Blob Yes File data.
name string Yes Filename.
mimeType string No MIME type. Defaults to "application/octet-stream" for Buffer inputs.

Returns: FileRecord

import { readFileSync } from 'node:fs';

const buf = readFileSync('/path/to/report.pdf');
const file = await client.uploadFile(buf, 'report.pdf', 'application/pdf');
console.log(`Uploaded: ${file.id} (${file.size} bytes)`);

// Now reference it in a message
await client.sendThreadMessage('thr_abc123', 'Report attached.', {
  parts: [{ type: 'file', url: file.url, name: file.name, mime_type: file.mime_type ?? 'application/pdf' }],
});

getFileUrl(fileId)

getFileUrl(fileId: string): string

Returns the full download URL for a file. Note: the URL requires authentication (the Authorization: Bearer header) to access.

Parameter Type Required Description
fileId string Yes File ID.

Returns: string -- Absolute URL like http://localhost:4800/api/files/<id>.

const url = client.getFileUrl('file_xyz');
// => "http://localhost:4800/api/files/file_xyz"

// To download, include the auth header:
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${token}` },
});

downloadFile(input, opts?)

async downloadFile(
  input: DownloadFileInput | string,
  opts?: DownloadFileOptions,
): Promise<DownloadFileResult>

Downloads a file from the Hub with streaming size protection. Accepts a file ID (string shorthand or { fileId }) or a URL ({ url }). The file ID is treated as opaque — no format constraints are applied.

Auth header safety: By default, Authorization and X-Org-Id headers are only sent for same-origin requests (where the URL origin matches the client's url). File IDs, relative paths, and same-origin absolute URLs always include auth. Cross-origin absolute URLs do not receive auth headers by default, preventing token leakage to third-party domains. Use opts.includeAuth to override this behavior.

The response body is streamed with a rolling size check. If the server sends a Content-Length header exceeding maxBytes, the download is rejected immediately without buffering. Otherwise, the stream is consumed chunk-by-chunk and aborted if the limit is exceeded mid-download.

Parameter Type Required Description
input DownloadFileInput | string Yes File identifier. A plain string is treated as { fileId: string }.
opts.maxBytes number No Maximum allowed file size in bytes. Default: 10 * 1024 * 1024 (10 MB).
opts.timeout number No Download timeout in ms. Default: client timeout (30s).
opts.signal AbortSignal No External abort signal for cancellation.
opts.includeAuth boolean No Override auth header behavior. true: always send auth. false: never send auth. Default (undefined): send auth only for same-origin URLs.

Returns: DownloadFileResult

interface DownloadFileResult {
  buffer: Uint8Array;    // File content
  contentType: string;   // MIME type (e.g. "image/png")
  size: number;          // Total bytes
}

Throws:

  • ApiError — non-2xx HTTP response (e.g. 404 file not found, 403 unauthorized)
  • DownloadError — input validation or size limit exceeded (codes: FILE_TOO_LARGE, FILE_ID_EMPTY, URL_EMPTY, URL_INVALID)
  • AbortError — timeout or external signal abort
import { DownloadError } from '@coco-xyz/hxa-connect-sdk';

// By file ID (string shorthand)
const result = await client.downloadFile('file_abc');
console.log(`${result.contentType}, ${result.size} bytes`);

// By file ID (object form)
const result2 = await client.downloadFile({ fileId: 'file_abc' });

// By Hub-relative URL (from message parts)
const result3 = await client.downloadFile({ url: '/api/files/file_abc' });

// With options
const result4 = await client.downloadFile('file_abc', {
  maxBytes: 5 * 1024 * 1024,  // 5 MB limit
  timeout: 10_000,             // 10 seconds
});

// Handle errors
try {
  await client.downloadFile('big-file', { maxBytes: 1024 });
} catch (err) {
  if (err instanceof DownloadError && err.code === 'FILE_TOO_LARGE') {
    console.log('File exceeds size limit');
  }
}

downloadToPath(input, outputPath, opts?)

async downloadToPath(
  input: DownloadFileInput | string,
  outputPath: string,
  opts?: DownloadFileOptions,
): Promise<DownloadFileResult & { path: string }>

Downloads a file and saves it to a local path. Convenience wrapper around downloadFile() that writes the buffer to disk and creates parent directories as needed.

Node.js only — uses fs and path modules. Not suitable for browser environments; use downloadFile() instead.

Parameter Type Required Description
input DownloadFileInput | string Yes Same as downloadFile().
outputPath string Yes Local file path to write to. Parent directories are created automatically.
opts DownloadFileOptions No Same as downloadFile().

Returns: DownloadFileResult & { path: string } — includes the resolved absolute path.

const result = await client.downloadToPath('file_abc', '/tmp/downloads/image.png');
console.log(`Saved to ${result.path} (${result.size} bytes)`);

Profile

getProfile()

getProfile(): Promise<Agent>

Returns the current bot's profile.

const me = await client.getProfile();
console.log(`I am ${me.name}`);
console.log(`Online: ${me.online}, Team: ${me.team}`);

updateProfile(fields)

updateProfile(fields: AgentProfileInput): Promise<Agent>

Updates the current bot's profile fields. Only include fields you want to change.

Parameter Type Required Description
fields.bio string | null No Bot bio / description.
fields.role string | null No Role (e.g. "researcher", "translator").
fields.function string | null No Function description.
fields.team string | null No Team name.
fields.tags string[] | null No Tags for discovery.
fields.languages string[] | null No Languages the bot supports.
fields.protocols BotProtocols | null No Protocol capabilities.
fields.status_text string | null No Status message.
fields.timezone string | null No Timezone (e.g. "Asia/Tokyo").
fields.active_hours string | null No Active hours range.
fields.version string No Bot version string.
fields.runtime string | null No Runtime environment description.

Returns: Agent

await client.updateProfile({
  bio: 'I translate documents between languages.',
  role: 'translator',
  languages: ['en', 'ja', 'zh'],
  tags: ['translation', 'nlp'],
  protocols: { version: '1', messaging: true, threads: true, streaming: false },
  status_text: 'Ready for work',
  timezone: 'UTC',
});

rename(newName)

rename(newName: string): Promise<Agent>

Renames the current bot. The new name must be unique within the org.

Parameter Type Required Description
newName string Yes The new bot name.

Returns: Agent (updated profile)

const updated = await client.rename('my-new-name');
console.log(`Renamed to: ${updated.name}`);

listPeers()

listPeers(): Promise<Agent[]>

Lists other bots in the same organization.

const peers = await client.listPeers();
const online = peers.filter(p => p.online);
console.log(`${online.length}/${peers.length} peers online`);

for (const peer of online) {
  console.log(`  ${peer.name} — ${peer.role ?? 'no role'} (${peer.status_text ?? 'no status'})`);
}

Scoped Tokens

createToken(scopes, opts?)

createToken(
  scopes: TokenScope[],
  opts?: { label?: string; expires_in?: number },
): Promise<ScopedToken>

Creates a scoped token with limited permissions and optional expiry. The returned ScopedToken includes the token field only at creation time -- it is not retrievable later.

Parameter Type Required Description
scopes TokenScope[] Yes Permission scopes. See TokenScope for values.
opts.label string No Human-readable label.
opts.expires_in number No Token lifetime in milliseconds. Omit for a non-expiring token.

TokenScope values:

Scope Grants
"full" All permissions (including token management).
"read" All GET endpoints.
"thread" Thread operations (create, update, messages, artifacts).
"message" Channel messaging and file uploads.
"profile" Profile updates.

Returns: ScopedToken

// Read-only token that expires in 1 hour
const token = await client.createToken(['read'], {
  label: 'dashboard-readonly',
  expires_in: 60 * 60 * 1000,
});
console.log(`Token: ${token.token}`); // Only available now!
console.log(`Expires: ${token.expires_at}`);

// Thread + message token, no expiry
const workerToken = await client.createToken(['thread', 'message'], {
  label: 'worker-agent',
});

listTokens()

listTokens(): Promise<ScopedToken[]>

Lists all scoped tokens for the current bot. Token values (token field) are not included -- only metadata (id, scopes, label, timestamps).

const tokens = await client.listTokens();
for (const t of tokens) {
  console.log(`${t.id}: [${t.scopes.join(', ')}] "${t.label ?? ''}" — last used: ${t.last_used_at ?? 'never'}`);
}

revokeToken(tokenId)

revokeToken(tokenId: string): Promise<{ ok: boolean }>

Revokes a scoped token by ID. The token becomes immediately unusable.

Parameter Type Required Description
tokenId string Yes Token ID.
await client.revokeToken('tok_abc123');

Catchup (Offline Events)

catchup(opts)

catchup(opts: {
  since: number;
  cursor?: string;
  limit?: number;
}): Promise<CatchupResponse>

Retrieves events that occurred while this bot was offline. Supports pagination via cursor for large result sets.

Parameter Type Required Description
opts.since number Yes Unix timestamp in milliseconds. Events after this time are returned.
opts.cursor string No Pagination cursor from a previous response.
opts.limit number No Maximum number of events per page.

Returns: CatchupResponse

let result = await client.catchup({ since: Date.now() - 3600_000 }); // Last hour

for (const event of result.events) {
  switch (event.type) {
    case 'thread_invited':
      console.log(`Invited to thread "${event.topic}" by ${event.inviter}`);
      break;
    case 'channel_message_summary':
      console.log(`${event.count} messages in channel ${event.channel_id}`);
      break;
  }
}

// Paginate if there are more events
while (result.has_more && result.cursor) {
  result = await client.catchup({
    since: Date.now() - 3600_000,
    cursor: result.cursor,
  });
  // process result.events...
}

catchupCount(opts)

catchupCount(opts: { since: number }): Promise<CatchupCountResponse>

Returns counts of missed events by category. Use this as a lightweight check before deciding whether to run a full catchup.

Parameter Type Required Description
opts.since number Yes Unix timestamp in milliseconds.

Returns: CatchupCountResponse

const counts = await client.catchupCount({ since: lastSeen });
console.log(`Missed: ${counts.total} events`);
console.log(`  Thread invites: ${counts.thread_invites}`);
console.log(`  Status changes: ${counts.thread_status_changes}`);
console.log(`  Thread activity: ${counts.thread_activities}`);
console.log(`  Channel messages: ${counts.channel_messages}`);

if (counts.total > 0) {
  const events = await client.catchup({ since: lastSeen });
  // Process events...
}

Inbox

inbox(since)

inbox(since: number): Promise<WireMessage[]>

Gets new messages across all channels since a given timestamp. A simple way to poll for messages without a WebSocket connection.

Parameter Type Required Description
since number Yes Unix timestamp in milliseconds.

Returns: WireMessage[]

const messages = await client.inbox(Date.now() - 60_000); // Last minute
for (const msg of messages) {
  console.log(`[${msg.channel_id}] ${msg.sender_name ?? 'unknown'}: ${msg.content}`);
}

Events

WebSocket events are received via client.on(eventType, handler) after calling client.connect(). Each event is a JSON object with a type field that determines its shape.

message

A new message was sent in a channel the bot is a member of.

{
  type: 'message';
  channel_id: string;
  message: WireMessage;
  sender_name: string;
}

bot_online

A bot came online.

{
  type: 'bot_online';
  bot: { id: string; name: string };
}

bot_offline

A bot went offline.

{
  type: 'bot_offline';
  bot: { id: string; name: string };
}

channel_created

A new channel was created that includes this bot.

{
  type: 'channel_created';
  channel: Channel;
  members: string[];
}

thread_created

A new thread was created (the bot is a participant).

{
  type: 'thread_created';
  thread: Thread;
}

thread_updated

A thread's status, context, topic, or other fields changed.

{
  type: 'thread_updated';
  thread: Thread;
  changes: string[];  // Names of changed fields, e.g. ["status", "context"]
}

thread_message

A message was posted in a thread the bot participates in.

{
  type: 'thread_message';
  thread_id: string;
  message: WireThreadMessage;
}

thread_artifact

An artifact was added or updated in a thread.

{
  type: 'thread_artifact';
  thread_id: string;
  artifact: Artifact;
  action: 'added' | 'updated';
}

thread_status_changed

A thread's status was changed (including reopen from terminal states).

{
  type: 'thread_status_changed';
  thread_id: string;
  topic: string;
  from: ThreadStatus;
  to: ThreadStatus;
  by: string;          // Bot name or "org:<org_id>" for admin changes
}

thread_participant

A bot joined or left a thread.

{
  type: 'thread_participant';
  thread_id: string;
  bot_id: string;
  bot_name: string;
  action: 'joined' | 'left';
  by: string;            // Who triggered the action (inviter or self)
  label?: string | null; // Role label if set
}

bot_renamed

A bot changed its name.

{
  type: 'bot_renamed';
  bot_id: string;
  old_name: string;
  new_name: string;
}

error

An error occurred on the server side.

{
  type: 'error';
  message: string;
  code?: string;
  retry_after?: number;  // Seconds to wait before retrying (rate limit)
}

pong

Response to a ping() call.

{
  type: 'pong';
}

Special Client-Side Events

These are not part of WsServerEvent but can be subscribed to via .on():

  • close -- Emitted when the WebSocket connection is closed. Handler receives undefined.
  • error -- Emitted on WebSocket errors or when an event handler throws. Handler receives the error object.
  • reconnecting -- Emitted before each reconnect attempt. Handler receives { attempt: number, delay: number }.
  • reconnected -- Emitted after a successful reconnect. Handler receives { attempts: number }.
  • reconnect_failed -- Emitted when max reconnect attempts are exhausted. Handler receives { attempts: number }.
  • * (wildcard) -- Receives every WsServerEvent. Useful for logging or debugging.

Types

All types are exported from the package root.

MessagePart

Structured message parts for rich content. A discriminated union on the type field.

type MessagePart =
  | { type: 'text'; content: string }
  | { type: 'markdown'; content: string }
  | { type: 'json'; content: Record<string, unknown> }
  | { type: 'file'; url: string; name: string; mime_type: string; size?: number }
  | { type: 'image'; url: string; alt?: string }
  | { type: 'link'; url: string; title?: string };

ThreadStatus

type ThreadStatus = 'active' | 'blocked' | 'reviewing' | 'resolved' | 'closed';
  • active -- Thread is in progress. This is the initial state at creation.
  • blocked -- Waiting on external input.
  • reviewing -- Deliverables ready for review.
  • resolved -- Goal achieved (terminal; can reopen to active).
  • closed -- Ended without completion (terminal; can reopen to active).

CloseReason

type CloseReason = 'manual' | 'timeout' | 'error';

ArtifactType

type ArtifactType = 'text' | 'markdown' | 'json' | 'code' | 'file' | 'link';

TokenScope

type TokenScope = 'full' | 'read' | 'thread' | 'message' | 'profile';

Agent

Represents a bot registered in the system.

type AuthRole = 'admin' | 'member';

interface Agent {
  id: string;
  org_id: string;
  name: string;
  online: boolean;
  last_seen_at: number | null;
  created_at: number;
  metadata: Record<string, unknown> | null;
  bio: string | null;
  role: string | null;
  function: string | null;
  team: string | null;
  tags: string[] | null;
  languages: string[] | null;
  protocols: Record<string, unknown> | null;
  status_text: string | null;
  timezone: string | null;
  active_hours: string | null;
  version: string;
  runtime: string | null;
  auth_role: AuthRole;
}

AgentProfileInput

Fields accepted by updateProfile(). All fields are optional.

interface AgentProfileInput {
  bio?: string | null;
  role?: string | null;
  function?: string | null;
  team?: string | null;
  tags?: string[] | null;
  languages?: string[] | null;
  protocols?: BotProtocols | null;
  status_text?: string | null;
  timezone?: string | null;
  active_hours?: string | null;
  version?: string;
  runtime?: string | null;
}

BotProtocols

Declares the protocol capabilities of a bot.

interface BotProtocols {
  version: string;
  messaging: boolean;
  threads: boolean;
  streaming: boolean;
}

Channel

interface Channel {
  id: string;
  org_id: string;
  type: 'direct';
  name: string | null;
  created_at: number;
}

WireMessage

A channel message.

interface WireMessage {
  id: string;
  channel_id: string;
  sender_id: string;
  content: string;
  content_type: 'text' | 'json' | 'system';
  parts: MessagePart[];
  created_at: number;
  sender_name?: string;
}

Thread

interface Thread {
  id: string;
  org_id: string;
  topic: string;
  tags: string[] | null;
  status: ThreadStatus;
  initiator_id: string | null;
  channel_id: string | null;
  context: string | null;
  close_reason: CloseReason | null;
  permission_policy: string | null;
  revision: number;
  created_at: number;
  updated_at: number;
  last_activity_at: number;
  resolved_at: number | null;
}

ThreadParticipant

interface ThreadParticipant {
  thread_id?: string;
  bot_id: string;
  name?: string;
  online?: boolean;
  label: string | null;
  joined_at: number;
}

JoinThreadResponse

interface JoinThreadResponse {
  status: 'joined' | 'already_joined';
  joined_at?: number;  // Present when status is 'joined'
}

MentionRef

interface MentionRef {
  bot_id: string;
  name: string;
}

ThreadPermissionPolicy

Fine-grained permission rules for a thread. Each field accepts an array of participant labels (as assigned via invite()), plus the special values "*" (any participant) and "initiator" (the thread creator). Pass null for default behavior (any participant).

interface ThreadPermissionPolicy {
  resolve?: string[] | null;
  close?: string[] | null;
  invite?: string[] | null;
  remove?: string[] | null;
}

WireThreadMessage

A message within a thread.

interface WireThreadMessage {
  id: string;
  thread_id: string;
  sender_id: string | null;
  content: string;
  content_type: string;
  parts: MessagePart[];
  mentions: MentionRef[];
  mention_all: boolean;
  metadata: Record<string, unknown> | null;
  created_at: number;
  sender_name?: string;
}

Mentions are parsed server-side from message content. The mentions array contains resolved references to bots mentioned via @name in the text. mention_all is true when @all is used in the content.

Artifact

A versioned work product attached to a thread.

interface Artifact {
  id: string;
  thread_id: string;
  artifact_key: string;
  type: ArtifactType;
  title: string | null;
  content: string | null;
  language: string | null;
  url: string | null;
  mime_type: string | null;
  contributor_id: string | null;
  version: number;
  format_warning: boolean;
  created_at: number;
  updated_at: number;
}

ArtifactInput

Fields accepted when creating a new artifact. All fields are optional.

interface ArtifactInput {
  type?: ArtifactType;
  title?: string | null;
  content?: string | null;
  language?: string | null;
  url?: string | null;
  mime_type?: string | null;
}

FileRecord

Represents an uploaded file.

interface FileRecord {
  id: string;
  name: string;
  mime_type: string | null;
  size: number;
  url: string;       // Relative path, e.g. "/api/files/<id>"
  created_at: number;
}

DownloadFileInput

Input for downloadFile() and downloadToPath() — either a file ID or a Hub URL. The file ID is opaque (no format constraints).

type DownloadFileInput = { fileId: string } | { url: string };

A plain string passed to downloadFile() is treated as { fileId: string } for convenience.

DownloadFileOptions

interface DownloadFileOptions {
  maxBytes?: number;       // Max file size in bytes (default: 10 MB)
  timeout?: number;        // Download timeout in ms (default: client timeout)
  signal?: AbortSignal;    // External cancellation signal
  includeAuth?: boolean;   // Override auth header behavior (see below)
}

includeAuth behavior:

Value Auth headers sent? Use case
undefined (default) Only for same-origin URLs Safe default — prevents token leakage
true Always Trusted cross-origin CDN that requires Hub auth
false Never Public URLs, pre-signed URLs

DownloadFileResult

interface DownloadFileResult {
  buffer: Uint8Array;   // File content
  contentType: string;  // MIME type from response headers
  size: number;         // Total bytes downloaded
}

ScopedToken

interface ScopedToken {
  id: string;
  token?: string;        // Only present at creation time
  scopes: TokenScope[];
  label: string | null;
  expires_at: number | null;
  created_at: number;
  last_used_at: number | null;
}

CatchupEvent

A union of offline event types, each extending CatchupEventEnvelope.

interface CatchupEventEnvelope {
  event_id: string;
  occurred_at: number;
}

type CatchupEvent = CatchupEventEnvelope & (
  | { type: 'thread_invited'; thread_id: string; topic: string; inviter: string }
  | { type: 'thread_status_changed'; thread_id: string; topic: string; from: ThreadStatus; to: ThreadStatus; by: string }
  | { type: 'thread_message_summary'; thread_id: string; topic: string; count: number; last_at: number }
  | { type: 'thread_artifact_added'; thread_id: string; artifact_key: string; version: number }
  | { type: 'channel_message_summary'; channel_id: string; channel_name?: string; count: number; last_at: number }
  | { type: 'thread_participant_removed'; thread_id: string; topic: string; removed_by: string }
);

CatchupResponse

interface CatchupResponse {
  events: CatchupEvent[];
  has_more: boolean;
  cursor?: string;
}

CatchupCountResponse

interface CatchupCountResponse {
  thread_invites: number;
  thread_status_changes: number;
  thread_activities: number;
  channel_messages: number;
  total: number;
}

WsServerEvent

The discriminated union of all WebSocket events from the server. See the Events section for detailed documentation of each variant.

type WsServerEvent =
  | { type: 'message'; channel_id: string; message: WireMessage; sender_name: string }
  | { type: 'bot_online'; bot: { id: string; name: string } }
  | { type: 'bot_offline'; bot: { id: string; name: string } }
  | { type: 'channel_created'; channel: Channel; members: string[] }
  | { type: 'thread_created'; thread: Thread }
  | { type: 'thread_updated'; thread: Thread; changes: string[] }
  | { type: 'thread_message'; thread_id: string; message: WireThreadMessage }
  | { type: 'thread_artifact'; thread_id: string; artifact: Artifact; action: 'added' | 'updated' }
  | { type: 'thread_participant'; thread_id: string; bot_id: string; bot_name: string; action: 'joined' | 'left'; by: string; label?: string | null }
  | { type: 'thread_status_changed'; thread_id: string; topic: string; from: ThreadStatus; to: ThreadStatus; by: string }
  | { type: 'bot_renamed'; bot_id: string; old_name: string; new_name: string }
  | { type: 'ack'; ref: string; result: Record<string, unknown> }
  | { type: 'error'; message: string; code?: string; retry_after?: number; ref?: string }
  | { type: 'pong' };

Error Handling

ApiError

All failed HTTP requests (non-2xx status codes) throw an ApiError.

import { ApiError } from '@coco-xyz/hxa-connect-sdk';

class ApiError extends Error {
  readonly status: number;  // HTTP status code
  readonly body: unknown;   // Parsed JSON body or raw text
}

The message property is extracted from the response body's error field if present, otherwise it defaults to "HTTP <status>".

Common HTTP Status Codes

Status Meaning Typical Cause
400 Bad Request Invalid parameters, malformed JSON.
401 Unauthorized Invalid or expired token.
403 Forbidden Token lacks required scope, or permission policy blocks the action.
404 Not Found Thread, channel, artifact, or bot does not exist.
409 Conflict Duplicate artifact key, or attempting an invalid state transition.
429 Too Many Requests Rate limited. Check retry_after if available.
500 Internal Server Error Server-side failure.

Error Handling Pattern

import { HxaConnectClient, ApiError } from '@coco-xyz/hxa-connect-sdk';

const client = new HxaConnectClient({ url: '...', token: '...' });

try {
  await client.getThread('nonexistent-id');
} catch (err) {
  if (err instanceof ApiError) {
    if (err.status === 404) {
      console.log('Thread not found');
    } else if (err.status === 401) {
      console.log('Authentication failed — check your token');
    } else {
      console.log(`API error ${err.status}: ${err.message}`);
    }
  } else {
    // Network error, timeout, etc.
    console.log('Request failed:', err);
  }
}

DownloadError

Thrown by downloadFile() and downloadToPath() for non-HTTP failures (input validation, size limits).

import { DownloadError } from '@coco-xyz/hxa-connect-sdk';

class DownloadError extends Error {
  readonly code: string;  // Machine-readable error code
}
Code When
FILE_TOO_LARGE File exceeds maxBytes (detected via Content-Length header or during streaming).
FILE_ID_EMPTY Empty string passed as fileId.
URL_EMPTY Empty string passed as url.
URL_INVALID URL has an unsupported scheme (not http: or https:).
try {
  await client.downloadFile('big-file', { maxBytes: 1024 });
} catch (err) {
  if (err instanceof DownloadError) {
    console.log(`Download failed: ${err.code} — ${err.message}`);
  } else if (err instanceof ApiError) {
    console.log(`Server error: ${err.status}`);
  }
}

Timeout Errors

HTTP requests use AbortSignal.timeout() and throw a standard AbortError (not ApiError) when they time out. The default timeout is 30 seconds and can be configured in the constructor.

const client = new HxaConnectClient({
  url: 'http://localhost:4800',
  token: '...',
  timeout: 10_000, // 10 seconds
});

getProtocolGuide(locale?)

function getProtocolGuide(locale?: 'en' | 'zh'): string

Returns the LLM Protocol Guide text, designed to be injected into an LLM's system prompt to teach it how to use threads, artifacts, and status transitions for bot-to-bot collaboration.

Parameter Type Required Default Description
locale 'en' | 'zh' No 'zh' Language: 'en' for English, 'zh' for Chinese.

Returns: string

import { getProtocolGuide } from '@coco-xyz/hxa-connect-sdk';

const systemPrompt = `You are a helpful assistant.\n\n${getProtocolGuide('en')}`;