@happyrobot-ai/sdk 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -998,6 +998,13 @@ app.post("/api/voice-token", async (req, res) => {
998
998
  });
999
999
  res.json(result); // { url, token, room_name, run_id }
1000
1000
  });
1001
+
1002
+ app.post("/api/live-session/:sessionId/listen-token", async (req, res) => {
1003
+ const result = await client.voice.createToken({
1004
+ session_id: req.params.sessionId,
1005
+ });
1006
+ res.json(result); // Hidden, subscribe-only observer token
1007
+ });
1001
1008
  ```
1002
1009
 
1003
1010
  ### Browser-side: `HappyRobotVoiceClient`
@@ -1044,6 +1051,28 @@ await connection.unmute();
1044
1051
  await connection.disconnect();
1045
1052
  ```
1046
1053
 
1054
+ To listen to an existing call without publishing audio or affecting the AI
1055
+ agent, create a token with `session_id` and use `listen()` in the browser:
1056
+
1057
+ ```ts
1058
+ const { url, token } = await fetch(
1059
+ `/api/live-session/${sessionId}/listen-token`,
1060
+ { method: "POST" }
1061
+ ).then((r) => r.json());
1062
+
1063
+ const voice = new HappyRobotVoiceClient({ url, token });
1064
+ const listener = await voice.listen();
1065
+
1066
+ // If the browser later blocks playback, call this from a click handler:
1067
+ await listener.startAudio();
1068
+
1069
+ // Stop listening without affecting the call:
1070
+ await listener.disconnect();
1071
+ ```
1072
+
1073
+ Set `should_takeover: true` when creating a session token to retain the
1074
+ existing takeover behavior, which removes the AI agent from the call.
1075
+
1047
1076
  ### `HappyRobotClient` (server-side)
1048
1077
 
1049
1078
  List the voices available to the current workspace in its cluster. Use a
@@ -1059,17 +1088,19 @@ for (const voice of voices) {
1059
1088
 
1060
1089
  `voice.id` is the value used in workflow `agent.voices` configuration.
1061
1090
 
1062
- | Method | Description |
1063
- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
1064
- | `client.voice.list({ language? })` | List voices available to the current workspace. `language` accepts prefixes like `en` or locales like `en-GB`. |
1065
- | `client.voice.createToken({ workflow_id, data?, env?, ttl_seconds? })` | Create a LiveKit token for voice calls. `ttl_seconds` defaults to 21600 (min 60, max 86400). |
1091
+ | Method | Description |
1092
+ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
1093
+ | `client.voice.list({ language? })` | List voices available to the current workspace. `language` accepts prefixes like `en` or locales like `en-GB`. |
1094
+ | `client.voice.createToken({ workflow_id, data?, env?, ttl_seconds? })` | Create a LiveKit token for a new voice call. |
1095
+ | `client.voice.createToken({ session_id, should_takeover?, ttl_seconds? })` | Observe a live session, or take it over when `should_takeover` is true. |
1066
1096
 
1067
1097
  ### `HappyRobotVoiceClient` (browser-side)
1068
1098
 
1069
- | Method | Description |
1070
- | ------------------------------------------- | -------------------------------------------------------------- |
1071
- | `new HappyRobotVoiceClient({ url, token })` | Create client with LiveKit credentials |
1072
- | `voice.connect(handlers?)` | Connect to room, enable microphone — returns `VoiceConnection` |
1099
+ | Method | Description |
1100
+ | ------------------------------------------- | ----------------------------------------------------------------------------------- |
1101
+ | `new HappyRobotVoiceClient({ url, token })` | Create client with LiveKit credentials |
1102
+ | `voice.connect(handlers?)` | Connect and enable the microphone — returns `VoiceConnection` |
1103
+ | `voice.listen(handlers?)` | Connect without microphone access or publishing — returns `VoiceListenerConnection` |
1073
1104
 
1074
1105
  ### `VoiceConnection`
1075
1106
 
@@ -1081,6 +1112,14 @@ for (const voice of voices) {
1081
1112
  | `connection.isMuted()` | Check mute state |
1082
1113
  | `connection.room` | Underlying LiveKit `Room` instance for advanced use cases |
1083
1114
 
1115
+ ### `VoiceListenerConnection`
1116
+
1117
+ | Method | Description |
1118
+ | ----------------------- | --------------------------------------------------------- |
1119
+ | `listener.disconnect()` | Stop listening without affecting the call |
1120
+ | `listener.startAudio()` | Resume audio playback from a browser user gesture |
1121
+ | `listener.room` | Underlying LiveKit `Room` instance for advanced use cases |
1122
+
1084
1123
  ### Room Events
1085
1124
 
1086
1125
  | Handler | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyrobot-ai/sdk",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "TypeScript SDK for the HappyRobot Public API",
5
5
  "main": "./index.js",
6
6
  "module": "./index.mjs",
@@ -12,6 +12,7 @@ export interface ApiKeyInfo {
12
12
  prefix: string;
13
13
  lastFour: string;
14
14
  createdAt: string;
15
+ expiresAt: string | null;
15
16
  lastUsedAt: string | null;
16
17
  revokedAt: string | null;
17
18
  }
@@ -21,6 +21,14 @@
21
21
  * should_takeover: true,
22
22
  * });
23
23
  * ```
24
+ *
25
+ * @example Silently listen to an in-progress call
26
+ * ```ts
27
+ * const { url, token } = await client.voice.createToken({
28
+ * session_id: "the-live-session-id",
29
+ * });
30
+ * // In the browser: await new HappyRobotVoiceClient({ url, token }).listen()
31
+ * ```
24
32
  */
25
33
  import type { HttpClient } from "../core/http";
26
34
  import type { CreateVoiceTokenBody, CreateVoiceTokenResponse, ListVoicesQuery, ListVoicesResponse } from "../types/voice.types";
@@ -35,9 +43,9 @@ export declare class VoiceResource {
35
43
  * Call this from your server, then pass the returned `url` and `token`
36
44
  * to the browser where they can be used to initialize `HappyRobotVoiceClient`.
37
45
  *
38
- * Provide `workflow_id` to start a new call, or `session_id` (with the
39
- * required `should_takeover: true`) to take over an in-progress call, which
40
- * drops the AI agent.
46
+ * Provide `workflow_id` to start a new call, or `session_id` to silently
47
+ * observe an in-progress call. Set `should_takeover: true` to take over the
48
+ * call and drop the AI agent.
41
49
  */
42
50
  createToken(body: CreateVoiceTokenBody): Promise<CreateVoiceTokenResponse>;
43
51
  }
@@ -22,6 +22,14 @@
22
22
  * should_takeover: true,
23
23
  * });
24
24
  * ```
25
+ *
26
+ * @example Silently listen to an in-progress call
27
+ * ```ts
28
+ * const { url, token } = await client.voice.createToken({
29
+ * session_id: "the-live-session-id",
30
+ * });
31
+ * // In the browser: await new HappyRobotVoiceClient({ url, token }).listen()
32
+ * ```
25
33
  */
26
34
  Object.defineProperty(exports, "__esModule", { value: true });
27
35
  exports.VoiceResource = void 0;
@@ -48,9 +56,9 @@ class VoiceResource {
48
56
  * Call this from your server, then pass the returned `url` and `token`
49
57
  * to the browser where they can be used to initialize `HappyRobotVoiceClient`.
50
58
  *
51
- * Provide `workflow_id` to start a new call, or `session_id` (with the
52
- * required `should_takeover: true`) to take over an in-progress call, which
53
- * drops the AI agent.
59
+ * Provide `workflow_id` to start a new call, or `session_id` to silently
60
+ * observe an in-progress call. Set `should_takeover: true` to take over the
61
+ * call and drop the AI agent.
54
62
  */
55
63
  async createToken(body) {
56
64
  return this.http.request({
@@ -30,8 +30,8 @@ type CreateVoiceTokenBase = {
30
30
  * POST /voice/tokens request body.
31
31
  *
32
32
  * Provide exactly one of `workflow_id` (start a new call) or `session_id`
33
- * (join an in-progress call). `should_takeover` is only available alongside
34
- * `session_id`.
33
+ * (observe or take over an in-progress call). `should_takeover` is only
34
+ * available alongside `session_id`.
35
35
  */
36
36
  export type CreateVoiceTokenBody = (CreateVoiceTokenBase & {
37
37
  /** Workflow UUID or slug to start a new call with. */
@@ -40,17 +40,16 @@ export type CreateVoiceTokenBody = (CreateVoiceTokenBase & {
40
40
  should_takeover?: never;
41
41
  }) | (CreateVoiceTokenBase & {
42
42
  /**
43
- * ID of an in-progress session to take over. Generates a token for the
44
- * session's existing LiveKit room.
43
+ * ID of an in-progress session to observe or take over. Generates a token
44
+ * for the session's existing LiveKit room.
45
45
  */
46
46
  session_id: string;
47
47
  workflow_id?: never;
48
48
  /**
49
- * Must be `true`. Explicit opt-in confirming the joining participant
50
- * takes over the call and the AI agent drops. Joining a live call
51
- * without taking over is not supported.
49
+ * Defaults to false, creating a hidden subscribe-only observer while the
50
+ * AI agent remains active. Set to true to take over and drop the agent.
52
51
  */
53
- should_takeover: true;
52
+ should_takeover?: boolean;
54
53
  });
55
54
  /** POST /voice/tokens response. */
56
55
  export interface CreateVoiceTokenResponse {
package/voice/index.d.ts CHANGED
@@ -18,5 +18,5 @@
18
18
  * ```
19
19
  */
20
20
  export { HappyRobotVoiceClient } from "../voice-client";
21
- export type { VoiceConnectionHandlers, VoiceConnection } from "../voice-client";
21
+ export type { VoiceClientConfig, VoiceConnection, VoiceConnectionHandlers, VoiceListenerConnection, } from "../voice-client";
22
22
  export type { CreateVoiceTokenBody, CreateVoiceTokenResponse, ListVoicesQuery, ListVoicesResponse, Voice, } from "../types/voice.types";
package/voice-client.d.ts CHANGED
@@ -67,6 +67,14 @@ export interface VoiceConnection {
67
67
  /** The underlying LiveKit Room instance for advanced use cases. */
68
68
  readonly room: Room;
69
69
  }
70
+ export interface VoiceListenerConnection {
71
+ /** Stop listening and disconnect from the room. */
72
+ disconnect(): Promise<void>;
73
+ /** Resume audio playback after a browser autoplay block. Call from a user gesture. */
74
+ startAudio(): Promise<void>;
75
+ /** The underlying LiveKit Room instance for advanced use cases. */
76
+ readonly room: Room;
77
+ }
70
78
  export interface VoiceClientConfig {
71
79
  /** LiveKit WebSocket URL from `client.voice.createToken()`. */
72
80
  url: string;
@@ -84,4 +92,12 @@ export declare class HappyRobotVoiceClient {
84
92
  * Pass event handlers to react to room lifecycle events and remote tracks.
85
93
  */
86
94
  connect(handlers?: VoiceConnectionHandlers): Promise<VoiceConnection>;
95
+ /**
96
+ * Silently listen to a live call without requesting microphone access.
97
+ *
98
+ * This must be used with a session observer token returned by
99
+ * `client.voice.createToken({ session_id })`.
100
+ */
101
+ listen(handlers?: VoiceConnectionHandlers): Promise<VoiceListenerConnection>;
102
+ private connectToRoom;
87
103
  }
package/voice-client.js CHANGED
@@ -47,6 +47,25 @@ class HappyRobotVoiceClient {
47
47
  * Pass event handlers to react to room lifecycle events and remote tracks.
48
48
  */
49
49
  async connect(handlers = {}) {
50
+ return this.connectToRoom(handlers, true);
51
+ }
52
+ /**
53
+ * Silently listen to a live call without requesting microphone access.
54
+ *
55
+ * This must be used with a session observer token returned by
56
+ * `client.voice.createToken({ session_id })`.
57
+ */
58
+ async listen(handlers = {}) {
59
+ const connection = await this.connectToRoom(handlers, false);
60
+ return {
61
+ disconnect: connection.disconnect,
62
+ startAudio: () => connection.room.startAudio(),
63
+ get room() {
64
+ return connection.room;
65
+ },
66
+ };
67
+ }
68
+ async connectToRoom(handlers, enableMicrophone) {
50
69
  // Create Room with HappyRobot defaults (matching platform use-webcall.tsx)
51
70
  const room = new livekit_client_1.Room({
52
71
  adaptiveStream: true,
@@ -106,9 +125,17 @@ class HappyRobotVoiceClient {
106
125
  });
107
126
  // ── Connect ──
108
127
  try {
128
+ if (!enableMicrophone) {
129
+ await room.startAudio().catch(() => undefined);
130
+ }
109
131
  await room.prepareConnection(this.url, this.token);
110
132
  await room.connect(this.url, this.token);
111
- await room.localParticipant.setMicrophoneEnabled(true);
133
+ if (enableMicrophone) {
134
+ await room.localParticipant.setMicrophoneEnabled(true);
135
+ }
136
+ else {
137
+ await room.startAudio().catch(() => undefined);
138
+ }
112
139
  }
113
140
  catch (error) {
114
141
  // Clean up on connection failure