Skip to content

clutchcall/typescript-sdk

Repository files navigation

ClutchCall TypeScript SDK

The official TypeScript / JavaScript SDK for ClutchCall. Runs in Node, Bun, and modern browsers (the WASM core ships in the package).

ClutchCall is modality-oriented: every modality is its own subpath import, all riding the same MoQT substrate underneath. Pick the entry point that matches what you're building; you can mix them in one app.

Subpath Modality Status
clutchcall_connect/streams Live broadcasts + signed playback URLs GA
clutchcall_connect/robotics Robotics topic pub/sub (Zenoh-over-QUIC, ROS 2 CDR) GA
clutchcall_connect/games Games (rooms, state/input/event channels; Unity UPM drop-in) GA
clutchcall_connect/data MQTT-style typed pub/sub (topics + + / # filters, retained messages) GA
clutchcall_connect/voice Voice (calls + bidirectional audio bridge + agent attach) GA
clutchcall_connect/moqt Realtime tracks (audio/video/frame) GA
clutchcall_connect/agent Agent-runtime control channel GA
clutchcall_connect (root) Legacy voice surface (ClutchCallClient) — kept for backwards compat legacy

Installation

npm install clutchcall_connect
# or: pnpm add clutchcall_connect
# or: bun  add clutchcall_connect

Streams — watch a live broadcast

The Streams modality is the modality-first pattern. The control plane (Streams) mints a short-lived playback URL; the data plane (BroadcastViewer) opens it over WebTransport+MoQT and yields fMP4 chunks. The relay enforces the playback JWT for you.

import { Streams, BroadcastViewer } from "clutchcall_connect/streams";

const streams = new Streams({
  baseUrl: "https://app.clutchcall.dev",
  apiKey:  process.env.CLUTCHCALL_API_KEY!,
  orgId:   "org_abc",
});

const input = await streams.liveInputs.get({ id: "li_xyz" });
const { url, expiresAt } = await input.signedPlaybackUrl({ ttlSeconds: 3600 });

const viewer = await BroadcastViewer.open(url, {
  onChunk: (init, chunk) => mediaSourceBuffer.appendBuffer(chunk.data),
  onClose: (reason)      => console.log("closed:", reason),
});

A complete browser+MSE viewer is in examples/streams_viewer.ts. The matching skill for code generation is clutchcall-streams.

Streams — push a live broadcast

Mirror of the viewer pattern for the publisher side. The cleartext stream key is returned once, at create() / rotateStreamKey(); the BFF stores only the hash, so save it.

import { Streams, BroadcastPublisher } from "clutchcall_connect/streams";

const streams = new Streams({ baseUrl: BFF, apiKey: KEY, orgId: ORG });
const { input, streamKey } = await streams.liveInputs.create({
  name: "My Show",
});

const pub = await BroadcastPublisher.open({
  inputId:   input.external_input_id,
  streamKey,
  codecs:    { video: "avc1.42E01F", audio: "opus" },
});

pub.write(fmp4Init);             // CMAF init segment FIRST
pub.write(fmp4Segment);          // media segments
await pub.close("finished");

A browser-camera-to-broadcast example with MediaRecorder is in examples/streams_publisher.ts.

Robotics — telemetry + commands across a fleet

The robotics modality enforces the bidirectional teleop convention so telemetry and commands never collide on the same namespace. Payload bytes are opaque CDR — your DDS / rmw_zenoh stack's serializer goes on the wire unchanged, prefixed with the ROS 2 type name for cross-language subscribers.

import { Robotics } from "clutchcall_connect/robotics";

const r = new Robotics({
  relayHost: "relay.clutchcall.dev",
  token:     process.env.CLUTCHCALL_RELAY_TOKEN!,
  robotId:   "turtlebot-7",
});

// robot side — push odometry on robot/<id>
const odom = await r.publishTelemetry({
  topic:    "odom",
  typeName: "nav_msgs/msg/Odometry",
  qos:      { reliability: "reliable", depth: 10 },
});
odom.write(cdrBytes);

// cloud side — push cmd_vel on robot/<id>/ctl
const cmd = await r.publishCommand({
  topic:    "cmd_vel",
  typeName: "geometry_msgs/msg/Twist",
});
cmd.write(twistBytes);

A robot-side publisher example is in examples/robotics_telemetry.ts; a cloud-side command publisher is in examples/robotics_command.ts. The matching skill is clutchcall-robotics.

Games — multiplayer rooms over QUIC

The games modality is one client per (room, player) — or (room, server) for the authority — with three baked-in channels:

Channel Direction Lane
state server → all datagram (lossy)
input player → server datagram (lossy)
event any → any stream (reliable)
import { Games } from "clutchcall_connect/games";

// Player joining a room
const me = new Games({
  relayHost: "relay.clutchcall.dev",
  token:     await fetchRoomToken("duel-42", "alice"),
  roomId:    "duel-42",
  playerId:  "alice",
});

me.subscribeState(bytes => render(deserializeState(bytes)));
const input = await me.publishInput();
addEventListener("tick", () => input.write(serializeInput(localInput)));
// Authoritative server (no playerId)
const auth = new Games({ relayHost, token, roomId: "duel-42" });
const state = await auth.publishState({ tickHz: 30 });
await auth.subscribeInputs((pid, bytes) => applyInput(pid, deserializeInput(bytes)));
setInterval(() => state.write(serializeState(world)), 1000 / 30);

A browser client example is in examples/games_client.ts; a Node server loop in examples/games_server.ts. For Unity games, install the matching UPM package com.clutchcall.transport — a drop-in for com.unity.transport that speaks the same wire. The skill is clutchcall-games.

Data — MQTT-style typed pub/sub

The "if you'd reach for MQTT, reach for this instead" modality. Hierarchical topics, + / # wildcards, retained messages, all over the same QUIC relay mesh — no broker. The SDK opens one MoQT track per top-level topic segment and filters MQTT-style client-side.

import { Data } from "clutchcall_connect/data";

const data = new Data({
  relayHost: "relay.clutchcall.dev",
  token:     process.env.CLUTCHCALL_DATA_TOKEN!,
  clientId:  "device-7",
});

// publish — lossy by default; flip reliable for application events
await data.publish({
  topic:   "sensors/room1/temperature",
  payload: new TextEncoder().encode("23.5"),
});

// MQTT-style filter — + is one segment, # is the rest
await data.subscribe({ topicFilter: "sensors/+/temperature" }, (msg) => {
  console.log(msg.topic, "←", msg.fromClientId, "=", new TextDecoder().decode(msg.payload));
});

A device-side publisher (with retained boot state + alerts) is in examples/data_publisher.ts; a fleet dashboard with three MQTT filters is in examples/data_dashboard.ts. The matching skill is clutchcall-data.

Voice — call control + audio bridge

The modality-shaped voice surface. Two primitives baked in: Calls (control plane) and AudioBridge (data plane), with the voice/<sid>/{uplink,downlink} namespace convention enforced.

import { Voice } from "clutchcall_connect/voice";

const v    = new Voice({ baseUrl: BFF, apiKey: KEY, orgId: ORG });
const call = await v.calls.originate({
  to:      "+15551234567",
  from:    "+15558675309",
  trunkId: "trunk_main",
  agent:   "healthcare-assistant",
});

const bridge = await v.audioBridge.attach(call.sid, {
  codec: "opus",
  onUplink: (frame, tsUs) => myAsr.feed(frame),
});
myTts.onChunk(opus => bridge.publishDownlink(opus));

await call.hangup();

A browser-mic outbound caller example is in examples/voice_browser_caller.ts. The matching skill is clutchcall-voice.

Legacy voice surface

The original ClutchCallClient + ClutchCallAudioStream (at the root import) remains available for backwards compat. Set CLUTCHCALL_CREDENTIALS to your service-account JSON, then:

import { ClutchCallClient }      from "clutchcall_connect";
import { ClutchCallAudioStream } from "clutchcall_connect";

async function run() {
  const client = new ClutchCallClient("https://pbx.clutchcall.com");

  // Answer an inbound call and pipe its media into your bot.
  await client.answerIncomingCall("call_sid_123", "wss://my-bot.com/media");

  // Tap the raw audio if you want to do your own routing.
  const stream = new ClutchCallAudioStream();
  await stream.connect("wss://pbx.clutchcall.com/ai/session_789");
  stream.onAudio((pcm: Uint8Array) => {
    // Forward to OpenAI / Deepgram / your model of choice.
  });
}

run();

Native core

In Node, the SDK loads libclutchcall_core_ffi.{so,dylib,dll} via Node-API. In the browser, it loads the WASM build (clutchcall_core_cc.wasm) shipped in dist/. Build details: core-sdk.

Realtime tracks over MoQT (browser)

clutchcall_connect/moqt is the first-class browser MoqtClient — the twin of the native Python/Go/Rust/Java/.NET clients. It speaks MoQT directly over native WebTransport (no FFI, no custom framing), so browser tracks interoperate with the other SDKs and the relay's fan-out, and it auto-reconnects.

import { MoqtClient } from "clutchcall_connect/moqt";

const moqt = await MoqtClient.connect(
  "https://relay.clutchcall.dev/moq",        // relay runs on its own host, 443
  token,                                     // tenant token (optional in dev)
  (state) => console.log("moqt state", state),
);

// Binary frame track (robot telemetry / game state), per-frame priority.
const pub = moqt.publishFrame("robot-7", "telemetry", { capability: "pose" });
pub.write(BigInt(Math.round(performance.now() * 1000)), encodePose(pose), /*priority*/ 0);
moqt.subscribeFrame("robot-7", "telemetry", (tsUs, priority, data) => render(data));

publishAudio / subscribeAudio carry encoded audio; the helpers captureMicrophone (mic → Opus) and OpusPlayer (WebCodecs playback) wire a voice track end to end. onState: 0 Connecting · 1 Connected · 2 Reconnecting · 3 Closed · 4 Failed.

About

TypeScript SDK for ClutchCall

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors