@boldvideo/bold-js 1.6.1 → 1.8.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # @boldvideo/bold-js
2
2
 
3
+ ## 1.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 065d7ea: Transform all API responses to use camelCase (TypeScript/JavaScript convention)
8
+
9
+ **Breaking Changes:**
10
+
11
+ - **All response types now use camelCase** (was snake_case). API responses are transformed at the SDK boundary.
12
+ - `video_id` → `videoId`
13
+ - `timestamp_end` → `timestampEnd`
14
+ - `playback_id` → `playbackId`
15
+ - `input_tokens` → `inputTokens`
16
+ - `conversation_id` → `conversationId`
17
+
18
+ **Internal:**
19
+
20
+ - Added `camelizeKeys` utility for deep snake_case → camelCase transformation
21
+ - Applied transformation in `jsonRequest()` and `parseSSE()` at the transport boundary
22
+
23
+ ## 1.7.0
24
+
25
+ ### Minor Changes
26
+
27
+ - e6fb51b: Align SDK with Bold AI API v1 specification
28
+
29
+ **Breaking Changes:**
30
+
31
+ - Video-scoped chat: pass `videoId` in options instead of as separate arg: `bold.ai.chat({ videoId, prompt })`
32
+ - `Source.timestamp_end` and `Source.playback_id` are now required (were optional)
33
+ - `AIUsage` now uses `input_tokens`/`output_tokens` (was `prompt_tokens`/`completion_tokens`/`total_tokens`)
34
+ - Removed `TopicInput` type - `RecommendationsOptions.topics` now only accepts `string[]`
35
+ - `RecommendationsOptions.context` is now `AIContextMessage[]` (was `string`)
36
+
37
+ **New:**
38
+
39
+ - `bold.ai.chat(opts)` - Single method for all chat (pass `videoId` to scope to a video)
40
+ - `bold.ai.recommendations(opts)` - AI-powered video recommendations (replaces `recommend`)
41
+ - `ChatOptions.videoId` - Scope chat to a specific video
42
+ - `ChatOptions.currentTime` - Pass current playback position for context
43
+
44
+ **Deprecated (still work, will be removed in v2):**
45
+
46
+ - `bold.ai.ask()` → use `bold.ai.chat()`
47
+ - `bold.ai.coach()` → use `bold.ai.chat()`
48
+ - `bold.ai.recommend()` → use `bold.ai.recommendations()`
49
+ - `AskOptions` type → use `ChatOptions`
50
+ - `RecommendOptions` type → use `RecommendationsOptions`
51
+ - `RecommendResponse` type → use `RecommendationsResponse`
52
+
53
+ **Type Changes:**
54
+
55
+ - Added `Source.id` field (chunk identifier)
56
+ - Added `conversation_id` and `video_id` to `message_start` event
57
+ - Added `conversation_id`, `recommendations`, `guidance` to `message_complete` event
58
+ - Simplified `clarification` event to include `content` field
59
+ - Removed legacy event types: `token`, `answer`, `complete`
60
+
3
61
  ## 1.6.1
4
62
 
5
63
  ### Patch Changes
package/README.md CHANGED
@@ -44,8 +44,8 @@ const bold = createClient('your-api-key');
44
44
  // Fetch videos
45
45
  const videos = await bold.videos.list();
46
46
 
47
- // Get AI-powered recommendations
48
- const recs = await bold.ai.recommend({
47
+ // AI-powered recommendations
48
+ const recs = await bold.ai.recommendations({
49
49
  topics: ['sales', 'negotiation'],
50
50
  stream: false
51
51
  });
@@ -92,15 +92,47 @@ const settings = await bold.settings();
92
92
 
93
93
  All AI methods support both streaming (default) and non-streaming modes.
94
94
 
95
- ### Recommend
95
+ ### Chat
96
+
97
+ Library-wide conversational AI for deep Q&A across your entire video library.
98
+
99
+ ```typescript
100
+ // Streaming (default)
101
+ const stream = await bold.ai.chat({ prompt: 'How do I price my SaaS?' });
102
+
103
+ for await (const event of stream) {
104
+ if (event.type === 'text_delta') process.stdout.write(event.delta);
105
+ if (event.type === 'sources') console.log('Sources:', event.sources);
106
+ }
107
+
108
+ // Non-streaming
109
+ const response = await bold.ai.chat({
110
+ prompt: 'What are the best closing techniques?',
111
+ stream: false
112
+ });
113
+ console.log(response.content);
114
+ ```
115
+
116
+ **Options:**
117
+
118
+ | Parameter | Type | Description |
119
+ |-----------|------|-------------|
120
+ | `prompt` | `string` | The user's question (required) |
121
+ | `stream` | `boolean` | `true` (default) for SSE, `false` for JSON |
122
+ | `videoId` | `string` | If provided, scope to this video instead of whole library |
123
+ | `currentTime` | `number` | Current playback position (only with `videoId`) |
124
+ | `conversationId` | `string` | Pass to continue existing conversation |
125
+ | `collectionId` | `string` | Filter to a specific collection |
126
+ | `tags` | `string[]` | Filter by tags |
127
+
128
+ ### Recommendations
96
129
 
97
130
  Get AI-powered video recommendations based on topics — ideal for personalized learning paths, exam prep, and content discovery.
98
131
 
99
132
  ```typescript
100
133
  // Streaming (default)
101
- const stream = await bold.ai.recommend({
134
+ const stream = await bold.ai.recommendations({
102
135
  topics: ['contract law', 'ethics', 'client management'],
103
- context: 'I failed these topics on my certification exam'
104
136
  });
105
137
 
106
138
  for await (const event of stream) {
@@ -116,7 +148,7 @@ for await (const event of stream) {
116
148
  }
117
149
 
118
150
  // Non-streaming
119
- const response = await bold.ai.recommend({
151
+ const response = await bold.ai.recommendations({
120
152
  topics: ['sales', 'marketing'],
121
153
  stream: false
122
154
  });
@@ -128,38 +160,17 @@ console.log(response.recommendations);
128
160
 
129
161
  | Parameter | Type | Description |
130
162
  |-----------|------|-------------|
131
- | `topics` | `string[]` \| `string` | Topics to find content for (required) |
163
+ | `topics` | `string[]` | Topics to find content for (required) |
132
164
  | `stream` | `boolean` | `true` (default) for SSE, `false` for JSON |
133
165
  | `limit` | `number` | Max videos per topic (default: 5, max: 20) |
134
166
  | `collectionId` | `string` | Filter to a specific collection |
135
167
  | `tags` | `string[]` | Filter by tags |
136
168
  | `includeGuidance` | `boolean` | Include AI learning path narrative (default: true) |
137
- | `context` | `string` | User context for personalized guidance |
138
-
139
- ### Coach / Ask
140
-
141
- Library-wide RAG assistant for answering questions across your entire video library.
142
-
143
- ```typescript
144
- // Streaming
145
- const stream = await bold.ai.coach({ prompt: 'How do I price my SaaS?' });
146
-
147
- for await (const event of stream) {
148
- if (event.type === 'text_delta') process.stdout.write(event.delta);
149
- if (event.type === 'sources') console.log('Sources:', event.sources);
150
- }
151
-
152
- // Non-streaming
153
- const response = await bold.ai.ask({
154
- prompt: 'What are the best closing techniques?',
155
- stream: false
156
- });
157
- console.log(response.content);
158
- ```
169
+ | `context` | `AIContextMessage[]` | Previous conversation turns for follow-ups |
159
170
 
160
171
  ### Search
161
172
 
162
- Semantic search with light synthesis.
173
+ Fast semantic search with a brief AI-generated summary.
163
174
 
164
175
  ```typescript
165
176
  const stream = await bold.ai.search({
@@ -174,12 +185,13 @@ for await (const event of stream) {
174
185
  }
175
186
  ```
176
187
 
177
- ### Chat
188
+ ### Video-Scoped Chat
178
189
 
179
- Video-scoped conversation for Q&A about a specific video.
190
+ Chat about a specific video by passing `videoId`. Uses only that video's transcript as context.
180
191
 
181
192
  ```typescript
182
- const stream = await bold.ai.chat('video-id', {
193
+ const stream = await bold.ai.chat({
194
+ videoId: 'video-id',
183
195
  prompt: 'What is discussed at the 5 minute mark?'
184
196
  });
185
197
 
@@ -187,8 +199,9 @@ for await (const event of stream) {
187
199
  if (event.type === 'text_delta') process.stdout.write(event.delta);
188
200
  }
189
201
 
190
- // With playback context - helps AI understand what viewer just watched
191
- const stream = await bold.ai.chat('video-id', {
202
+ // With playback context (coming soon)
203
+ const stream = await bold.ai.chat({
204
+ videoId: 'video-id',
192
205
  prompt: 'What does she mean by that?',
193
206
  currentTime: 847 // seconds
194
207
  });
@@ -245,8 +258,10 @@ import type {
245
258
  Settings,
246
259
  AIEvent,
247
260
  AIResponse,
248
- RecommendOptions,
249
- RecommendResponse,
261
+ ChatOptions,
262
+ SearchOptions,
263
+ RecommendationsOptions,
264
+ RecommendationsResponse,
250
265
  Recommendation,
251
266
  Source
252
267
  } from '@boldvideo/bold-js';
@@ -254,6 +269,49 @@ import type {
254
269
 
255
270
  ---
256
271
 
272
+ ## Migration from v1.6.x
273
+
274
+ ### Breaking: Response types now use camelCase
275
+
276
+ All API responses are now transformed to use idiomatic TypeScript/JavaScript naming:
277
+
278
+ ```typescript
279
+ // Before (v1.6.x)
280
+ source.video_id
281
+ source.timestamp_end
282
+ source.playback_id
283
+ usage.input_tokens
284
+ event.conversation_id
285
+
286
+ // After (v1.7.0)
287
+ source.videoId
288
+ source.timestampEnd
289
+ source.playbackId
290
+ usage.inputTokens
291
+ event.conversationId
292
+ ```
293
+
294
+ ### Method Changes
295
+
296
+ | Old | New | Notes |
297
+ |-----|-----|-------|
298
+ | `bold.ai.ask(opts)` | `bold.ai.chat(opts)` | `ask()` still works but is deprecated |
299
+ | `bold.ai.coach(opts)` | `bold.ai.chat(opts)` | `coach()` still works but is deprecated |
300
+ | `bold.ai.chat(videoId, opts)` | `bold.ai.chat({ videoId, ...opts })` | Pass `videoId` in options |
301
+ | `bold.ai.recommend(opts)` | `bold.ai.recommendations(opts)` | `recommend()` still works but is deprecated |
302
+
303
+ ### Type Renames
304
+
305
+ | Old Type | New Type |
306
+ |----------|----------|
307
+ | `AskOptions` | `ChatOptions` |
308
+ | `RecommendOptions` | `RecommendationsOptions` |
309
+ | `RecommendResponse` | `RecommendationsResponse` |
310
+
311
+ The old types are still exported as aliases for backward compatibility.
312
+
313
+ ---
314
+
257
315
  ## Related Links
258
316
 
259
317
  - **[Bold API Documentation](https://docs.boldvideo.io/docs/api)**
package/dist/index.cjs CHANGED
@@ -221,6 +221,24 @@ function basicInfos() {
221
221
  };
222
222
  }
223
223
 
224
+ // src/util/camelize.ts
225
+ var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
226
+ var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
227
+ function camelizeKeys(input) {
228
+ if (Array.isArray(input)) {
229
+ return input.map((item) => camelizeKeys(item));
230
+ }
231
+ if (!isPlainObject(input)) {
232
+ return input;
233
+ }
234
+ const out = {};
235
+ for (const [rawKey, value] of Object.entries(input)) {
236
+ const key = snakeToCamel(rawKey);
237
+ out[key] = camelizeKeys(value);
238
+ }
239
+ return out;
240
+ }
241
+
224
242
  // src/lib/ai.ts
225
243
  async function* parseSSE(response) {
226
244
  const reader = response.body?.getReader();
@@ -248,9 +266,10 @@ async function* parseSSE(response) {
248
266
  if (!json)
249
267
  continue;
250
268
  try {
251
- const event = JSON.parse(json);
269
+ const raw = JSON.parse(json);
270
+ const event = camelizeKeys(raw);
252
271
  yield event;
253
- if (event.type === "complete" || event.type === "error") {
272
+ if (event.type === "message_complete" || event.type === "error") {
254
273
  await reader.cancel();
255
274
  return;
256
275
  }
@@ -298,24 +317,33 @@ async function jsonRequest(path, body, config) {
298
317
  if (!response.ok) {
299
318
  throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
300
319
  }
301
- return response.json();
320
+ const raw = await response.json();
321
+ return camelizeKeys(raw);
302
322
  }
303
323
  function createAI(config) {
304
- async function ask(options) {
305
- const path = options.conversationId ? `ai/ask/${options.conversationId}` : "ai/ask";
324
+ async function chat(options) {
325
+ const isVideoScoped = !!options.videoId;
326
+ const basePath = isVideoScoped ? `ai/videos/${options.videoId}/chat` : "ai/chat";
327
+ const path = options.conversationId ? `${basePath}/${options.conversationId}` : basePath;
306
328
  const body = { prompt: options.prompt };
307
329
  if (options.collectionId)
308
330
  body.collection_id = options.collectionId;
309
331
  if (options.tags)
310
332
  body.tags = options.tags;
333
+ if (isVideoScoped && options.currentTime !== void 0) {
334
+ body.current_time = options.currentTime;
335
+ }
311
336
  if (options.stream === false) {
312
337
  body.stream = false;
313
338
  return jsonRequest(path, body, config);
314
339
  }
315
340
  return streamRequest(path, body, config);
316
341
  }
342
+ async function ask(options) {
343
+ return chat(options);
344
+ }
317
345
  async function coach(options) {
318
- return ask(options);
346
+ return chat(options);
319
347
  }
320
348
  async function search(options) {
321
349
  const path = "ai/search";
@@ -336,19 +364,8 @@ function createAI(config) {
336
364
  }
337
365
  return streamRequest(path, body, config);
338
366
  }
339
- async function chat(videoId, options) {
340
- const path = options.conversationId ? `ai/videos/${videoId}/chat/${options.conversationId}` : `ai/videos/${videoId}/chat`;
341
- const body = { prompt: options.prompt };
342
- if (options.currentTime !== void 0)
343
- body.current_time = options.currentTime;
344
- if (options.stream === false) {
345
- body.stream = false;
346
- return jsonRequest(path, body, config);
347
- }
348
- return streamRequest(path, body, config);
349
- }
350
- async function recommend(options) {
351
- const path = "ai/recommend";
367
+ async function recommendations(options) {
368
+ const path = "ai/recommendations";
352
369
  const body = { topics: options.topics };
353
370
  if (options.limit)
354
371
  body.limit = options.limit;
@@ -366,11 +383,15 @@ function createAI(config) {
366
383
  }
367
384
  return streamRequest(path, body, config);
368
385
  }
386
+ async function recommend(options) {
387
+ return recommendations(options);
388
+ }
369
389
  return {
390
+ chat,
370
391
  ask,
371
392
  coach,
372
393
  search,
373
- chat,
394
+ recommendations,
374
395
  recommend
375
396
  };
376
397
  }
package/dist/index.d.ts CHANGED
@@ -182,70 +182,65 @@ type Settings = {
182
182
  * Source citation from AI responses
183
183
  */
184
184
  interface Source {
185
- video_id: string;
185
+ id: string;
186
+ videoId: string;
186
187
  title: string;
187
- timestamp: number;
188
- timestamp_end?: number;
189
188
  text: string;
190
- playback_id?: string;
189
+ timestamp: number;
190
+ timestampEnd: number;
191
+ playbackId: string;
191
192
  speaker?: string;
192
193
  }
193
194
  /**
194
195
  * Token usage statistics
195
196
  */
196
197
  interface AIUsage {
197
- prompt_tokens: number;
198
- completion_tokens: number;
199
- total_tokens: number;
198
+ inputTokens: number;
199
+ outputTokens: number;
200
200
  }
201
201
  /**
202
202
  * SSE event types for AI streaming responses
203
203
  */
204
204
  type AIEvent = {
205
205
  type: "message_start";
206
- id: string;
207
- model?: string;
206
+ conversationId?: string;
207
+ videoId?: string;
208
208
  } | {
209
209
  type: "sources";
210
210
  sources: Source[];
211
- query?: string;
212
211
  } | {
213
212
  type: "text_delta";
214
213
  delta: string;
215
- } | {
216
- type: "token";
217
- content: string;
218
- } | {
219
- type: "answer";
220
- content: string;
221
- response_id?: string;
222
- context?: AIContextMessage[];
223
214
  } | {
224
215
  type: "clarification";
216
+ content: string;
225
217
  questions: string[];
226
218
  } | {
227
219
  type: "recommendations";
228
220
  recommendations: Recommendation[];
229
221
  } | {
230
222
  type: "message_complete";
223
+ conversationId?: string;
231
224
  content: string;
232
225
  sources: Source[];
233
226
  usage?: AIUsage;
234
227
  context?: AIContextMessage[];
235
- } | {
236
- type: "complete";
228
+ recommendations?: Recommendation[];
229
+ guidance?: string;
237
230
  } | {
238
231
  type: "error";
239
232
  code: string;
240
233
  message: string;
241
234
  retryable: boolean;
242
- details?: Record<string, unknown>;
243
235
  };
244
236
  /**
245
- * Non-streaming AI response
237
+ * Non-streaming AI response for /ai/chat, /ai/videos/:id/chat, and /ai/search
246
238
  */
247
239
  interface AIResponse {
248
- id: string;
240
+ conversationId?: string;
241
+ videoId?: string;
242
+ /** @deprecated Use conversationId instead. Will be removed in v2. */
243
+ id?: string;
249
244
  content: string;
250
245
  sources: Source[];
251
246
  usage: AIUsage;
@@ -253,15 +248,31 @@ interface AIResponse {
253
248
  context?: AIContextMessage[];
254
249
  }
255
250
  /**
256
- * Options for bold.ai.ask() and bold.ai.coach()
251
+ * Options for bold.ai.chat()
252
+ *
253
+ * If `videoId` is provided, scopes chat to that video (hits /ai/videos/:id/chat).
254
+ * Otherwise, searches your entire library (hits /ai/chat).
257
255
  */
258
- interface AskOptions {
256
+ interface ChatOptions {
259
257
  prompt: string;
260
258
  stream?: boolean;
261
259
  conversationId?: string;
262
260
  collectionId?: string;
263
261
  tags?: string[];
262
+ /**
263
+ * If provided, scope chat to a specific video instead of the whole library.
264
+ */
265
+ videoId?: string;
266
+ /**
267
+ * Current playback position in seconds. Only used when videoId is set.
268
+ * Helps AI understand what the viewer just watched.
269
+ */
270
+ currentTime?: number;
264
271
  }
272
+ /**
273
+ * @deprecated Use ChatOptions instead
274
+ */
275
+ type AskOptions = ChatOptions;
265
276
  /**
266
277
  * Conversation message for AI context
267
278
  */
@@ -281,26 +292,13 @@ interface SearchOptions {
281
292
  tags?: string[];
282
293
  context?: AIContextMessage[];
283
294
  }
284
- /**
285
- * Options for bold.ai.chat()
286
- *
287
- * conversationId: Pass to continue an existing conversation (multi-turn chat).
288
- * If omitted, a new conversation is created. The id is returned in the
289
- * message_start event - capture it to pass to subsequent requests.
290
- */
291
- interface ChatOptions {
292
- prompt: string;
293
- stream?: boolean;
294
- conversationId?: string;
295
- currentTime?: number;
296
- }
297
295
  /**
298
296
  * A recommended video with relevance score
299
297
  */
300
298
  interface RecommendationVideo {
301
- video_id: string;
299
+ videoId: string;
302
300
  title: string;
303
- playback_id: string;
301
+ playbackId: string;
304
302
  relevance: number;
305
303
  reason: string;
306
304
  }
@@ -309,57 +307,74 @@ interface RecommendationVideo {
309
307
  */
310
308
  interface Recommendation {
311
309
  topic: string;
312
- position: number;
313
310
  videos: RecommendationVideo[];
314
311
  }
315
312
  /**
316
- * Topic input format for recommendations
313
+ * Options for bold.ai.recommendations()
317
314
  */
318
- type TopicInput = string | {
319
- q: string;
320
- priority?: number;
321
- };
322
- /**
323
- * Options for bold.ai.recommend()
324
- */
325
- interface RecommendOptions {
326
- topics: TopicInput[] | string;
315
+ interface RecommendationsOptions {
316
+ topics: string[];
327
317
  stream?: boolean;
328
318
  limit?: number;
329
319
  collectionId?: string;
330
320
  tags?: string[];
331
321
  includeGuidance?: boolean;
332
- context?: string;
322
+ context?: AIContextMessage[];
333
323
  }
334
324
  /**
335
- * Non-streaming response for recommend endpoint
325
+ * @deprecated Use RecommendationsOptions instead
336
326
  */
337
- interface RecommendResponse {
338
- id: string;
327
+ type RecommendOptions = RecommendationsOptions;
328
+ /**
329
+ * Non-streaming response for recommendations endpoint
330
+ */
331
+ interface RecommendationsResponse {
339
332
  recommendations: Recommendation[];
340
333
  guidance: string;
341
334
  sources: Source[];
335
+ context?: AIContextMessage[];
336
+ usage?: AIUsage;
342
337
  }
338
+ /**
339
+ * @deprecated Use RecommendationsResponse instead
340
+ */
341
+ type RecommendResponse = RecommendationsResponse;
343
342
 
344
343
  /**
345
344
  * AI client interface for type-safe method overloading
346
345
  */
347
346
  interface AIClient {
348
347
  /**
349
- * Ask - Library-wide RAG assistant
348
+ * Chat - Conversational AI for Q&A
349
+ *
350
+ * If `videoId` is provided, scopes to that video. Otherwise searches your entire library.
350
351
  *
351
352
  * @example
352
- * // Streaming (default)
353
- * const stream = await bold.ai.ask({ prompt: "How do I price my SaaS?" });
353
+ * // Library-wide Q&A
354
+ * const stream = await bold.ai.chat({ prompt: "How do I price my SaaS?" });
354
355
  * for await (const event of stream) {
355
356
  * if (event.type === "text_delta") process.stdout.write(event.delta);
356
357
  * }
357
358
  *
358
359
  * @example
360
+ * // Video-scoped Q&A
361
+ * const stream = await bold.ai.chat({ videoId: "vid_xyz", prompt: "What does she mean?" });
362
+ *
363
+ * @example
359
364
  * // Non-streaming
360
- * const response = await bold.ai.ask({ prompt: "How do I price my SaaS?", stream: false });
365
+ * const response = await bold.ai.chat({ prompt: "How do I price my SaaS?", stream: false });
361
366
  * console.log(response.content);
362
367
  */
368
+ chat(options: ChatOptions & {
369
+ stream: false;
370
+ }): Promise<AIResponse>;
371
+ chat(options: ChatOptions & {
372
+ stream?: true;
373
+ }): Promise<AsyncIterable<AIEvent>>;
374
+ chat(options: ChatOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
375
+ /**
376
+ * @deprecated Use chat() instead. Will be removed in a future version.
377
+ */
363
378
  ask(options: AskOptions & {
364
379
  stream: false;
365
380
  }): Promise<AIResponse>;
@@ -368,7 +383,7 @@ interface AIClient {
368
383
  }): Promise<AsyncIterable<AIEvent>>;
369
384
  ask(options: AskOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
370
385
  /**
371
- * Coach - Alias for ask() (Library-wide RAG assistant)
386
+ * @deprecated Use chat() instead. Will be removed in a future version.
372
387
  */
373
388
  coach(options: AskOptions & {
374
389
  stream: false;
@@ -394,36 +409,30 @@ interface AIClient {
394
409
  }): Promise<AsyncIterable<AIEvent>>;
395
410
  search(options: SearchOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
396
411
  /**
397
- * Chat - Video-scoped conversation
398
- *
399
- * @example
400
- * const stream = await bold.ai.chat("video-id", { prompt: "What is discussed at 5 minutes?" });
401
- * for await (const event of stream) {
402
- * if (event.type === "text_delta") process.stdout.write(event.delta);
403
- * }
404
- */
405
- chat(videoId: string, options: ChatOptions & {
406
- stream: false;
407
- }): Promise<AIResponse>;
408
- chat(videoId: string, options: ChatOptions & {
409
- stream?: true;
410
- }): Promise<AsyncIterable<AIEvent>>;
411
- chat(videoId: string, options: ChatOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
412
- /**
413
- * Recommend - AI-powered video recommendations
412
+ * Recommendations - AI-powered video recommendations
414
413
  *
415
414
  * @example
416
415
  * // Streaming (default)
417
- * const stream = await bold.ai.recommend({ topics: ["sales", "negotiation"] });
416
+ * const stream = await bold.ai.recommendations({ topics: ["sales", "negotiation"] });
418
417
  * for await (const event of stream) {
419
418
  * if (event.type === "recommendations") console.log(event.recommendations);
420
419
  * }
421
420
  *
422
421
  * @example
423
422
  * // Non-streaming
424
- * const response = await bold.ai.recommend({ topics: ["sales"], stream: false });
423
+ * const response = await bold.ai.recommendations({ topics: ["sales"], stream: false });
425
424
  * console.log(response.guidance);
426
425
  */
426
+ recommendations(options: RecommendationsOptions & {
427
+ stream: false;
428
+ }): Promise<RecommendationsResponse>;
429
+ recommendations(options: RecommendationsOptions & {
430
+ stream?: true;
431
+ }): Promise<AsyncIterable<AIEvent>>;
432
+ recommendations(options: RecommendationsOptions): Promise<AsyncIterable<AIEvent> | RecommendationsResponse>;
433
+ /**
434
+ * @deprecated Use recommendations() instead. Will be removed in a future version.
435
+ */
427
436
  recommend(options: RecommendOptions & {
428
437
  stream: false;
429
438
  }): Promise<RecommendResponse>;
@@ -475,4 +484,4 @@ declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
475
484
  */
476
485
  declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
477
486
 
478
- export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AskOptions, AssistantConfig, ChatOptions, ClientOptions, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, MenuItem, Playlist, Portal, PortalDisplay, PortalLayout, PortalNavigation, PortalTheme, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, SearchOptions, Settings, Source, ThemeColors, ThemeConfig, TopicInput, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, createClient };
487
+ export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AskOptions, AssistantConfig, ChatOptions, ClientOptions, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, MenuItem, Playlist, Portal, PortalDisplay, PortalLayout, PortalNavigation, PortalTheme, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, SearchOptions, Settings, Source, ThemeColors, ThemeConfig, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, createClient };
package/dist/index.js CHANGED
@@ -183,6 +183,24 @@ function basicInfos() {
183
183
  };
184
184
  }
185
185
 
186
+ // src/util/camelize.ts
187
+ var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
188
+ var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
189
+ function camelizeKeys(input) {
190
+ if (Array.isArray(input)) {
191
+ return input.map((item) => camelizeKeys(item));
192
+ }
193
+ if (!isPlainObject(input)) {
194
+ return input;
195
+ }
196
+ const out = {};
197
+ for (const [rawKey, value] of Object.entries(input)) {
198
+ const key = snakeToCamel(rawKey);
199
+ out[key] = camelizeKeys(value);
200
+ }
201
+ return out;
202
+ }
203
+
186
204
  // src/lib/ai.ts
187
205
  async function* parseSSE(response) {
188
206
  const reader = response.body?.getReader();
@@ -210,9 +228,10 @@ async function* parseSSE(response) {
210
228
  if (!json)
211
229
  continue;
212
230
  try {
213
- const event = JSON.parse(json);
231
+ const raw = JSON.parse(json);
232
+ const event = camelizeKeys(raw);
214
233
  yield event;
215
- if (event.type === "complete" || event.type === "error") {
234
+ if (event.type === "message_complete" || event.type === "error") {
216
235
  await reader.cancel();
217
236
  return;
218
237
  }
@@ -260,24 +279,33 @@ async function jsonRequest(path, body, config) {
260
279
  if (!response.ok) {
261
280
  throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
262
281
  }
263
- return response.json();
282
+ const raw = await response.json();
283
+ return camelizeKeys(raw);
264
284
  }
265
285
  function createAI(config) {
266
- async function ask(options) {
267
- const path = options.conversationId ? `ai/ask/${options.conversationId}` : "ai/ask";
286
+ async function chat(options) {
287
+ const isVideoScoped = !!options.videoId;
288
+ const basePath = isVideoScoped ? `ai/videos/${options.videoId}/chat` : "ai/chat";
289
+ const path = options.conversationId ? `${basePath}/${options.conversationId}` : basePath;
268
290
  const body = { prompt: options.prompt };
269
291
  if (options.collectionId)
270
292
  body.collection_id = options.collectionId;
271
293
  if (options.tags)
272
294
  body.tags = options.tags;
295
+ if (isVideoScoped && options.currentTime !== void 0) {
296
+ body.current_time = options.currentTime;
297
+ }
273
298
  if (options.stream === false) {
274
299
  body.stream = false;
275
300
  return jsonRequest(path, body, config);
276
301
  }
277
302
  return streamRequest(path, body, config);
278
303
  }
304
+ async function ask(options) {
305
+ return chat(options);
306
+ }
279
307
  async function coach(options) {
280
- return ask(options);
308
+ return chat(options);
281
309
  }
282
310
  async function search(options) {
283
311
  const path = "ai/search";
@@ -298,19 +326,8 @@ function createAI(config) {
298
326
  }
299
327
  return streamRequest(path, body, config);
300
328
  }
301
- async function chat(videoId, options) {
302
- const path = options.conversationId ? `ai/videos/${videoId}/chat/${options.conversationId}` : `ai/videos/${videoId}/chat`;
303
- const body = { prompt: options.prompt };
304
- if (options.currentTime !== void 0)
305
- body.current_time = options.currentTime;
306
- if (options.stream === false) {
307
- body.stream = false;
308
- return jsonRequest(path, body, config);
309
- }
310
- return streamRequest(path, body, config);
311
- }
312
- async function recommend(options) {
313
- const path = "ai/recommend";
329
+ async function recommendations(options) {
330
+ const path = "ai/recommendations";
314
331
  const body = { topics: options.topics };
315
332
  if (options.limit)
316
333
  body.limit = options.limit;
@@ -328,11 +345,15 @@ function createAI(config) {
328
345
  }
329
346
  return streamRequest(path, body, config);
330
347
  }
348
+ async function recommend(options) {
349
+ return recommendations(options);
350
+ }
331
351
  return {
352
+ chat,
332
353
  ask,
333
354
  coach,
334
355
  search,
335
- chat,
356
+ recommendations,
336
357
  recommend
337
358
  };
338
359
  }
package/llms.txt CHANGED
@@ -14,11 +14,11 @@ const videos = await bold.videos.list();
14
14
  const video = await bold.videos.get('video-id');
15
15
 
16
16
  // AI-powered recommendations
17
- const recs = await bold.ai.recommend({ topics: ['sales', 'negotiation'], stream: false });
17
+ const recs = await bold.ai.recommendations({ topics: ['sales', 'negotiation'], stream: false });
18
18
  console.log(recs.guidance);
19
19
 
20
20
  // AI streaming
21
- const stream = await bold.ai.coach({ prompt: 'How do I price my SaaS?' });
21
+ const stream = await bold.ai.chat({ prompt: 'How do I price my SaaS?' });
22
22
  for await (const event of stream) {
23
23
  if (event.type === 'text_delta') process.stdout.write(event.delta);
24
24
  }
@@ -43,11 +43,14 @@ for await (const event of stream) {
43
43
 
44
44
  All AI methods return `AsyncIterable<AIEvent>` (streaming) or `Promise<AIResponse>` (non-streaming).
45
45
 
46
- - `bold.ai.recommend(options)` - AI-powered video recommendations based on topics
47
- - `bold.ai.coach(options)` - Library-wide RAG assistant (alias: `ask`)
48
- - `bold.ai.ask(options)` - Library-wide RAG assistant
46
+ - `bold.ai.chat(options)` - Conversational AI for Q&A (pass `videoId` to scope to a video)
49
47
  - `bold.ai.search(options)` - Semantic search with synthesis
50
- - `bold.ai.chat(videoId, options)` - Video-scoped Q&A conversation
48
+ - `bold.ai.recommendations(options)` - AI-powered video recommendations based on topics
49
+
50
+ **Deprecated aliases** (still work but will be removed):
51
+ - `bold.ai.ask(options)` → use `chat()`
52
+ - `bold.ai.coach(options)` → use `chat()`
53
+ - `bold.ai.recommend(options)` → use `recommendations()`
51
54
 
52
55
  ### Analytics
53
56
 
@@ -56,26 +59,14 @@ All AI methods return `AsyncIterable<AIEvent>` (streaming) or `Promise<AIRespons
56
59
 
57
60
  ## AI Options
58
61
 
59
- ### RecommendOptions
60
-
61
- ```typescript
62
- {
63
- topics: string[] | string; // Topics to find content for (required)
64
- stream?: boolean; // Default: true
65
- limit?: number; // Max videos per topic (default: 5, max: 20)
66
- collectionId?: string; // Filter to collection
67
- tags?: string[]; // Filter by tags
68
- includeGuidance?: boolean; // Include AI learning path narrative (default: true)
69
- context?: string; // User context for personalization
70
- }
71
- ```
72
-
73
- ### AskOptions / CoachOptions
62
+ ### ChatOptions
74
63
 
75
64
  ```typescript
76
65
  {
77
66
  prompt: string; // Question to ask (required)
78
67
  stream?: boolean; // Default: true
68
+ videoId?: string; // If set, scope to this video
69
+ currentTime?: number; // Current playback position (with videoId)
79
70
  conversationId?: string; // Continue existing conversation
80
71
  collectionId?: string; // Filter to collection
81
72
  tags?: string[]; // Filter by tags
@@ -96,14 +87,17 @@ All AI methods return `AsyncIterable<AIEvent>` (streaming) or `Promise<AIRespons
96
87
  }
97
88
  ```
98
89
 
99
- ### ChatOptions
90
+ ### RecommendationsOptions
100
91
 
101
92
  ```typescript
102
93
  {
103
- prompt: string; // Question about the video (required)
94
+ topics: string[]; // Topics to find content for (required)
104
95
  stream?: boolean; // Default: true
105
- conversationId?: string; // Continue existing conversation
106
- currentTime?: number; // Current playback position in seconds
96
+ limit?: number; // Max videos per topic (default: 5, max: 20)
97
+ collectionId?: string; // Filter to collection
98
+ tags?: string[]; // Filter by tags
99
+ includeGuidance?: boolean; // Include AI learning path narrative (default: true)
100
+ context?: AIContextMessage[]; // Previous conversation turns for follow-ups
107
101
  }
108
102
  ```
109
103
 
@@ -113,8 +107,10 @@ Key types exported:
113
107
 
114
108
  - `Video`, `Playlist`, `Settings`, `Portal`
115
109
  - `AIEvent`, `AIResponse`, `Source`, `AIUsage`
116
- - `AskOptions`, `SearchOptions`, `ChatOptions`
117
- - `RecommendOptions`, `RecommendResponse`, `Recommendation`, `RecommendationVideo`, `TopicInput`
110
+ - `ChatOptions`, `SearchOptions`
111
+ - `RecommendationsOptions`, `RecommendationsResponse`, `Recommendation`, `RecommendationVideo`
112
+
113
+ All response types use camelCase (e.g., `source.videoId`, `source.timestampEnd`, `usage.inputTokens`).
118
114
 
119
115
  ## Links
120
116
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@boldvideo/bold-js",
3
3
  "license": "MIT",
4
- "version": "1.6.1",
4
+ "version": "1.8.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",