@boldvideo/bold-js 1.16.0 → 1.18.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,61 @@
1
1
  # @boldvideo/bold-js
2
2
 
3
+ ## 1.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 48d8b40: Add Community API for posts, comments, and reactions
8
+
9
+ New `bold.community` namespace with full CRUD operations:
10
+
11
+ **Posts:**
12
+
13
+ - `bold.community.posts.list(opts?)` - List posts with category/limit/offset filters
14
+ - `bold.community.posts.get(id, viewerId?)` - Get single post with comments
15
+ - `bold.community.posts.create(viewerId, data)` - Create a post (markdown supported)
16
+ - `bold.community.posts.update(viewerId, id, data)` - Update post (owner/admin only)
17
+ - `bold.community.posts.delete(viewerId, id)` - Delete post (owner/admin only)
18
+ - `bold.community.posts.react(viewerId, id)` - Toggle reaction (like/unlike)
19
+
20
+ **Comments:**
21
+
22
+ - `bold.community.comments.create(viewerId, postId, data)` - Create comment with optional `parentId` for nested replies
23
+ - `bold.community.comments.delete(viewerId, id)` - Delete comment (owner/admin only)
24
+ - `bold.community.comments.react(viewerId, id)` - Toggle reaction
25
+
26
+ **New Types:**
27
+
28
+ - `Post`, `PostAuthor`, `Comment`, `ReactionResponse`
29
+ - `ListPostsOptions`, `CreatePostData`, `UpdatePostData`, `CreateCommentData`
30
+ - `CommunityAPIError` - Error class for Community API operations
31
+
32
+ ## 1.17.0
33
+
34
+ ### Minor Changes
35
+
36
+ - 563730d: Add full options support to `videos.list()` for tag, collectionId, viewerId, and page filtering
37
+
38
+ **New features:**
39
+
40
+ - Filter videos by `tag` and `collectionId` on both endpoints
41
+ - Include watch progress with `viewerId` parameter
42
+ - Access paginated `/videos` endpoint using `page` parameter
43
+
44
+ **Usage:**
45
+
46
+ ```typescript
47
+ // Latest videos with filters
48
+ await bold.videos.list({ limit: 20, tag: "sales", collectionId: "col_123" });
49
+
50
+ // Include viewer watch progress
51
+ await bold.videos.list({ limit: 20, viewerId: "viewer_123" });
52
+
53
+ // Paginated index (uses /videos endpoint)
54
+ await bold.videos.list({ page: 2, tag: "sales" });
55
+ ```
56
+
57
+ **Breaking:** None. Existing `bold.videos.list()` and `bold.videos.list(12)` calls work unchanged.
58
+
3
59
  ## 1.16.0
4
60
 
5
61
  ### Minor Changes
package/README.md CHANGED
@@ -59,9 +59,27 @@ console.log(recs.guidance);
59
59
  ### Videos
60
60
 
61
61
  ```typescript
62
- // List latest videos
62
+ // List latest videos (default: 12)
63
63
  const videos = await bold.videos.list();
64
64
 
65
+ // With limit (backwards compatible)
66
+ const videos = await bold.videos.list(20);
67
+
68
+ // With filters
69
+ const videos = await bold.videos.list({
70
+ limit: 20,
71
+ tag: 'sales',
72
+ collectionId: 'col_123',
73
+ viewerId: 'viewer_123' // Include watch progress
74
+ });
75
+
76
+ // Paginated index (uses /videos endpoint)
77
+ const videos = await bold.videos.list({
78
+ page: 2,
79
+ tag: 'sales',
80
+ collectionId: 'col_123'
81
+ });
82
+
65
83
  // Get a single video by ID or slug
66
84
  const video = await bold.videos.get('video-id');
67
85
  const videoBySlug = await bold.videos.get('my-video-slug');
@@ -160,6 +178,69 @@ console.log(`Completed ${meta.completed} of ${meta.total} videos`);
160
178
 
161
179
  ---
162
180
 
181
+ ## Community API
182
+
183
+ Build community features with posts, comments, and reactions. All write operations require a `viewerId` (the viewer performing the action).
184
+
185
+ ### Posts
186
+
187
+ ```typescript
188
+ // List posts (optionally filter by category)
189
+ const { data: posts } = await bold.community.posts.list({
190
+ category: 'announcements',
191
+ limit: 20,
192
+ offset: 0,
193
+ viewerId: 'viewer-uuid' // Include viewerReacted in response
194
+ });
195
+
196
+ // Get a single post with comments
197
+ const { data: post } = await bold.community.posts.get('post-id', 'viewer-uuid');
198
+
199
+ // Create a post (requires viewerId)
200
+ const { data: newPost } = await bold.community.posts.create('viewer-uuid', {
201
+ content: 'Hello community! **Markdown** supported.',
202
+ category: 'general'
203
+ });
204
+
205
+ // Update a post (owner or admin only)
206
+ await bold.community.posts.update('viewer-uuid', 'post-id', {
207
+ content: 'Updated content'
208
+ });
209
+
210
+ // Delete a post (owner or admin only)
211
+ await bold.community.posts.delete('viewer-uuid', 'post-id');
212
+
213
+ // React to a post (toggle like/unlike)
214
+ const reaction = await bold.community.posts.react('viewer-uuid', 'post-id');
215
+ console.log(reaction.reacted, reaction.reactionsCount);
216
+ ```
217
+
218
+ ### Comments
219
+
220
+ ```typescript
221
+ // Create a comment on a post
222
+ const { data: comment } = await bold.community.comments.create(
223
+ 'viewer-uuid',
224
+ 'post-id',
225
+ { content: 'Great post!' }
226
+ );
227
+
228
+ // Reply to a comment (nested)
229
+ const { data: reply } = await bold.community.comments.create(
230
+ 'viewer-uuid',
231
+ 'post-id',
232
+ { content: 'I agree!', parentId: 'parent-comment-id' }
233
+ );
234
+
235
+ // Delete a comment (owner or admin only)
236
+ await bold.community.comments.delete('viewer-uuid', 'comment-id');
237
+
238
+ // React to a comment (toggle)
239
+ const reaction = await bold.community.comments.react('viewer-uuid', 'comment-id');
240
+ ```
241
+
242
+ ---
243
+
163
244
  ## AI Methods
164
245
 
165
246
  All AI methods support both streaming (default) and non-streaming modes.
@@ -357,7 +438,19 @@ import type {
357
438
  Viewer,
358
439
  ViewerProgress,
359
440
  ViewerLookupParams,
360
- ListProgressOptions
441
+ ListProgressOptions,
442
+ ListVideosOptions,
443
+ ListVideosLatestOptions,
444
+ ListVideosIndexOptions,
445
+ // Community API
446
+ Post,
447
+ PostAuthor,
448
+ Comment,
449
+ ReactionResponse,
450
+ ListPostsOptions,
451
+ CreatePostData,
452
+ UpdatePostData,
453
+ CreateCommentData
361
454
  } from '@boldvideo/bold-js';
362
455
  ```
363
456
 
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ CommunityAPIError: () => CommunityAPIError,
33
34
  DEFAULT_API_BASE_URL: () => DEFAULT_API_BASE_URL,
34
35
  DEFAULT_INTERNAL_API_BASE_URL: () => DEFAULT_INTERNAL_API_BASE_URL,
35
36
  ViewerAPIError: () => ViewerAPIError,
@@ -38,7 +39,7 @@ __export(src_exports, {
38
39
  module.exports = __toCommonJS(src_exports);
39
40
 
40
41
  // src/lib/client.ts
41
- var import_axios2 = __toESM(require("axios"), 1);
42
+ var import_axios3 = __toESM(require("axios"), 1);
42
43
 
43
44
  // src/util/camelize.ts
44
45
  var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
@@ -64,6 +65,15 @@ function camelizeKeys(input, options = {}) {
64
65
  }
65
66
 
66
67
  // src/lib/fetchers.ts
68
+ function toQuery(params) {
69
+ const qs = new URLSearchParams();
70
+ for (const [k, v] of Object.entries(params)) {
71
+ if (v !== void 0 && v !== null)
72
+ qs.set(k, String(v));
73
+ }
74
+ const s = qs.toString();
75
+ return s ? `?${s}` : "";
76
+ }
67
77
  async function get(client, url) {
68
78
  try {
69
79
  const res = await client.get(url);
@@ -90,14 +100,44 @@ function fetchSettings(client) {
90
100
  };
91
101
  }
92
102
  function fetchVideos(client) {
93
- return async (videoLimit = 12) => {
103
+ return async (arg = 12) => {
94
104
  try {
105
+ if (typeof arg === "number") {
106
+ return await get(
107
+ client,
108
+ `videos/latest${toQuery({ limit: arg })}`
109
+ );
110
+ }
111
+ const opts = arg;
112
+ const hasPage = "page" in opts && opts.page !== void 0;
113
+ if (hasPage && ("limit" in opts || "viewerId" in opts)) {
114
+ throw new Error(
115
+ "videos.list(): cannot use `page` with `limit` or `viewerId` (these belong to different endpoints)"
116
+ );
117
+ }
118
+ if (hasPage) {
119
+ const { page, tag: tag2, collectionId: collectionId2 } = opts;
120
+ return await get(
121
+ client,
122
+ `videos${toQuery({
123
+ page,
124
+ tag: tag2,
125
+ collection_id: collectionId2
126
+ })}`
127
+ );
128
+ }
129
+ const { limit, tag, collectionId, viewerId } = opts;
95
130
  return await get(
96
131
  client,
97
- `videos/latest?limit=${videoLimit}`
132
+ `videos/latest${toQuery({
133
+ limit: limit ?? 12,
134
+ tag,
135
+ collection_id: collectionId,
136
+ viewer_id: viewerId
137
+ })}`
98
138
  );
99
139
  } catch (error) {
100
- console.error(`Error fetching videos with limit: ${videoLimit}`, error);
140
+ console.error(`Error fetching videos`, error);
101
141
  throw error;
102
142
  }
103
143
  };
@@ -291,6 +331,188 @@ function saveProgress(client) {
291
331
  };
292
332
  }
293
333
 
334
+ // src/lib/community.ts
335
+ var import_axios2 = require("axios");
336
+ var CommunityAPIError = class extends Error {
337
+ constructor(method, url, error) {
338
+ var __super = (...args) => {
339
+ super(...args);
340
+ };
341
+ if (error instanceof import_axios2.AxiosError) {
342
+ const status = error.response?.status;
343
+ const message = error.response?.data?.error || error.message;
344
+ __super(`${method} ${url} failed (${status}): ${message}`);
345
+ this.status = status;
346
+ this.originalError = error;
347
+ } else if (error instanceof Error) {
348
+ __super(`${method} ${url} failed: ${error.message}`);
349
+ this.originalError = error;
350
+ } else {
351
+ __super(`${method} ${url} failed: ${String(error)}`);
352
+ }
353
+ this.name = "CommunityAPIError";
354
+ }
355
+ };
356
+ function requireViewerId(viewerId) {
357
+ if (!viewerId)
358
+ throw new Error("Viewer ID is required (X-Viewer-ID)");
359
+ }
360
+ function viewerHeaders(viewerId) {
361
+ return viewerId ? { "X-Viewer-ID": viewerId } : void 0;
362
+ }
363
+ function toQuery2(params) {
364
+ const qs = new URLSearchParams();
365
+ for (const [k, v] of Object.entries(params)) {
366
+ if (v !== void 0 && v !== null)
367
+ qs.set(k, String(v));
368
+ }
369
+ const s = qs.toString();
370
+ return s ? `?${s}` : "";
371
+ }
372
+ async function get3(client, url, viewerId) {
373
+ try {
374
+ const res = await client.get(url, { headers: viewerHeaders(viewerId) });
375
+ return camelizeKeys(res.data);
376
+ } catch (error) {
377
+ throw new CommunityAPIError("GET", url, error);
378
+ }
379
+ }
380
+ async function post2(client, url, data, viewerId) {
381
+ try {
382
+ const res = await client.post(url, data, { headers: viewerHeaders(viewerId) });
383
+ return camelizeKeys(res.data);
384
+ } catch (error) {
385
+ throw new CommunityAPIError("POST", url, error);
386
+ }
387
+ }
388
+ async function put(client, url, data, viewerId) {
389
+ try {
390
+ const res = await client.put(url, data, { headers: viewerHeaders(viewerId) });
391
+ return camelizeKeys(res.data);
392
+ } catch (error) {
393
+ throw new CommunityAPIError("PUT", url, error);
394
+ }
395
+ }
396
+ async function del(client, url, viewerId) {
397
+ try {
398
+ const res = await client.delete(url, { headers: viewerHeaders(viewerId) });
399
+ return camelizeKeys(res.data);
400
+ } catch (error) {
401
+ throw new CommunityAPIError("DELETE", url, error);
402
+ }
403
+ }
404
+ function listPosts(client) {
405
+ return async (opts = {}) => {
406
+ return get3(
407
+ client,
408
+ `community/posts${toQuery2({
409
+ category: opts.category,
410
+ limit: opts.limit,
411
+ offset: opts.offset
412
+ })}`,
413
+ opts.viewerId
414
+ );
415
+ };
416
+ }
417
+ function getPost(client) {
418
+ return async (id, viewerId) => {
419
+ if (!id)
420
+ throw new Error("Post ID is required");
421
+ return get3(client, `community/posts/${id}`, viewerId);
422
+ };
423
+ }
424
+ function createPost(client) {
425
+ return async (viewerId, data) => {
426
+ requireViewerId(viewerId);
427
+ if (!data?.content)
428
+ throw new Error("Post content is required");
429
+ return post2(
430
+ client,
431
+ "community/posts",
432
+ { post: { content: data.content, category: data.category } },
433
+ viewerId
434
+ );
435
+ };
436
+ }
437
+ function updatePost(client) {
438
+ return async (viewerId, id, data) => {
439
+ requireViewerId(viewerId);
440
+ if (!id)
441
+ throw new Error("Post ID is required");
442
+ const body = {};
443
+ if (data.content !== void 0)
444
+ body.content = data.content;
445
+ if (data.category !== void 0)
446
+ body.category = data.category;
447
+ return put(
448
+ client,
449
+ `community/posts/${id}`,
450
+ { post: body },
451
+ viewerId
452
+ );
453
+ };
454
+ }
455
+ function deletePost(client) {
456
+ return async (viewerId, id) => {
457
+ requireViewerId(viewerId);
458
+ if (!id)
459
+ throw new Error("Post ID is required");
460
+ return del(client, `community/posts/${id}`, viewerId);
461
+ };
462
+ }
463
+ function reactToPost(client) {
464
+ return async (viewerId, id) => {
465
+ requireViewerId(viewerId);
466
+ if (!id)
467
+ throw new Error("Post ID is required");
468
+ return post2(
469
+ client,
470
+ `community/posts/${id}/react`,
471
+ void 0,
472
+ viewerId
473
+ );
474
+ };
475
+ }
476
+ function createComment(client) {
477
+ return async (viewerId, postId, data) => {
478
+ requireViewerId(viewerId);
479
+ if (!postId)
480
+ throw new Error("Post ID is required");
481
+ if (!data?.content)
482
+ throw new Error("Comment content is required");
483
+ const body = { content: data.content };
484
+ if (data.parentId)
485
+ body.parent_id = data.parentId;
486
+ return post2(
487
+ client,
488
+ `community/posts/${postId}/comments`,
489
+ { comment: body },
490
+ viewerId
491
+ );
492
+ };
493
+ }
494
+ function deleteComment(client) {
495
+ return async (viewerId, id) => {
496
+ requireViewerId(viewerId);
497
+ if (!id)
498
+ throw new Error("Comment ID is required");
499
+ return del(client, `community/comments/${id}`, viewerId);
500
+ };
501
+ }
502
+ function reactToComment(client) {
503
+ return async (viewerId, id) => {
504
+ requireViewerId(viewerId);
505
+ if (!id)
506
+ throw new Error("Comment ID is required");
507
+ return post2(
508
+ client,
509
+ `community/comments/${id}/react`,
510
+ void 0,
511
+ viewerId
512
+ );
513
+ };
514
+ }
515
+
294
516
  // src/util/throttle.ts
295
517
  var throttle = (fn, delay) => {
296
518
  let wait = false;
@@ -592,7 +814,7 @@ function createClient(apiKey, options = {}) {
592
814
  };
593
815
  let apiClient;
594
816
  try {
595
- apiClient = import_axios2.default.create(apiClientOptions);
817
+ apiClient = import_axios3.default.create(apiClientOptions);
596
818
  } catch (error) {
597
819
  console.error("Error creating API client", error);
598
820
  throw error;
@@ -624,12 +846,28 @@ function createClient(apiKey, options = {}) {
624
846
  saveProgress: saveProgress(apiClient)
625
847
  },
626
848
  ai: createAI(aiConfig),
849
+ community: {
850
+ posts: {
851
+ list: listPosts(apiClient),
852
+ get: getPost(apiClient),
853
+ create: createPost(apiClient),
854
+ update: updatePost(apiClient),
855
+ delete: deletePost(apiClient),
856
+ react: reactToPost(apiClient)
857
+ },
858
+ comments: {
859
+ create: createComment(apiClient),
860
+ delete: deleteComment(apiClient),
861
+ react: reactToComment(apiClient)
862
+ }
863
+ },
627
864
  trackEvent: trackEvent(apiClient, userId, { debug }),
628
865
  trackPageView: trackPageView(apiClient, userId, { debug })
629
866
  };
630
867
  }
631
868
  // Annotate the CommonJS export names for ESM import in node:
632
869
  0 && (module.exports = {
870
+ CommunityAPIError,
633
871
  DEFAULT_API_BASE_URL,
634
872
  DEFAULT_INTERNAL_API_BASE_URL,
635
873
  ViewerAPIError,
package/dist/index.d.ts CHANGED
@@ -473,6 +473,136 @@ type ProgressListMeta = {
473
473
  /** Number of in-progress videos */
474
474
  inProgress: number;
475
475
  };
476
+ /**
477
+ * Options for listing videos from /videos/latest endpoint
478
+ */
479
+ type ListVideosLatestOptions = {
480
+ /** Max videos to return (default: 12) */
481
+ limit?: number;
482
+ /** Filter by tag */
483
+ tag?: string;
484
+ /** Filter to videos in a specific collection */
485
+ collectionId?: string;
486
+ /** Viewer UUID for watch progress */
487
+ viewerId?: string;
488
+ };
489
+ /**
490
+ * Options for listing videos from /videos (index) endpoint with pagination
491
+ */
492
+ type ListVideosIndexOptions = {
493
+ /** Page number for pagination */
494
+ page?: number;
495
+ /** Filter by tag */
496
+ tag?: string;
497
+ /** Filter to videos in a specific collection */
498
+ collectionId?: string;
499
+ };
500
+ /**
501
+ * Combined options for bold.videos.list()
502
+ *
503
+ * If `page` is provided, uses /videos (index) endpoint.
504
+ * Otherwise, uses /videos/latest endpoint.
505
+ *
506
+ * Note: `page` and `limit`/`viewerId` are mutually exclusive.
507
+ */
508
+ type ListVideosOptions = (ListVideosLatestOptions & {
509
+ page?: never;
510
+ }) | (ListVideosIndexOptions & {
511
+ limit?: never;
512
+ viewerId?: never;
513
+ });
514
+ /**
515
+ * Author information for posts and comments
516
+ */
517
+ type PostAuthor = {
518
+ id: string;
519
+ name: string;
520
+ email?: string;
521
+ isAdmin: boolean;
522
+ };
523
+ /**
524
+ * A comment on a community post
525
+ */
526
+ type Comment = {
527
+ id: string;
528
+ content: string;
529
+ /** Nesting depth (0 = top-level) */
530
+ depth: number;
531
+ reactionsCount: number;
532
+ /** Whether current viewer has reacted (only present when X-Viewer-ID header sent) */
533
+ viewerReacted?: boolean;
534
+ viewer: PostAuthor;
535
+ /** Nested replies */
536
+ replies: Comment[];
537
+ createdAt: string;
538
+ updatedAt: string;
539
+ };
540
+ /**
541
+ * A community post
542
+ */
543
+ type Post = {
544
+ id: string;
545
+ content: string;
546
+ category: string;
547
+ pinned: boolean;
548
+ pinnedAt?: string | null;
549
+ reactionsCount: number;
550
+ commentsCount: number;
551
+ /** Whether current viewer has reacted (only present when X-Viewer-ID header sent) */
552
+ viewerReacted?: boolean;
553
+ viewer: PostAuthor;
554
+ /** Comments (only present on single post fetch) */
555
+ comments?: Comment[];
556
+ createdAt: string;
557
+ updatedAt: string;
558
+ };
559
+ /**
560
+ * Response from react endpoints (toggle)
561
+ */
562
+ type ReactionResponse = {
563
+ reacted: boolean;
564
+ reactionsCount: number;
565
+ };
566
+ /**
567
+ * Options for listing community posts
568
+ */
569
+ type ListPostsOptions = {
570
+ /** Filter by category (e.g., "announcements", "general") */
571
+ category?: string;
572
+ /** Max posts to return (default: 20) */
573
+ limit?: number;
574
+ /** Pagination offset */
575
+ offset?: number;
576
+ /** Viewer ID to include viewerReacted in response */
577
+ viewerId?: string;
578
+ };
579
+ /**
580
+ * Data for creating a community post
581
+ */
582
+ type CreatePostData = {
583
+ /** Post content (supports markdown) */
584
+ content: string;
585
+ /** Category for the post */
586
+ category?: string;
587
+ };
588
+ /**
589
+ * Data for updating a community post
590
+ */
591
+ type UpdatePostData = {
592
+ /** Post content (supports markdown) */
593
+ content?: string;
594
+ /** Category for the post */
595
+ category?: string;
596
+ };
597
+ /**
598
+ * Data for creating a comment
599
+ */
600
+ type CreateCommentData = {
601
+ /** Comment content */
602
+ content: string;
603
+ /** Parent comment ID for replies */
604
+ parentId?: string;
605
+ };
476
606
 
477
607
  /**
478
608
  * AI client interface for type-safe method overloading
@@ -609,7 +739,7 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
609
739
  data: Settings;
610
740
  }>;
611
741
  videos: {
612
- list: (videoLimit?: number) => Promise<{
742
+ list: (arg?: number | ListVideosOptions) => Promise<{
613
743
  data: Video[];
614
744
  }>;
615
745
  get: (id: string) => Promise<{
@@ -655,6 +785,35 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
655
785
  }>;
656
786
  };
657
787
  ai: AIClient;
788
+ community: {
789
+ posts: {
790
+ list: (opts?: ListPostsOptions) => Promise<{
791
+ data: Post[];
792
+ }>;
793
+ get: (id: string, viewerId?: string | undefined) => Promise<{
794
+ data: Post;
795
+ }>;
796
+ create: (viewerId: string, data: CreatePostData) => Promise<{
797
+ data: Post;
798
+ }>;
799
+ update: (viewerId: string, id: string, data: UpdatePostData) => Promise<{
800
+ data: Post;
801
+ }>;
802
+ delete: (viewerId: string, id: string) => Promise<{
803
+ data?: unknown;
804
+ }>;
805
+ react: (viewerId: string, id: string) => Promise<ReactionResponse>;
806
+ };
807
+ comments: {
808
+ create: (viewerId: string, postId: string, data: CreateCommentData) => Promise<{
809
+ data: Comment;
810
+ }>;
811
+ delete: (viewerId: string, id: string) => Promise<{
812
+ data?: unknown;
813
+ }>;
814
+ react: (viewerId: string, id: string) => Promise<ReactionResponse>;
815
+ };
816
+ };
658
817
  trackEvent: (video: any, event: Event) => void;
659
818
  trackPageView: (title: string) => void;
660
819
  };
@@ -668,4 +827,13 @@ declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
668
827
  */
669
828
  declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
670
829
 
671
- export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AnalyticsProvider, AskOptions, AssistantConfig, ChatOptions, Citation, ClientOptions, Conversation, ConversationMessage, ConversationMetadata, CreateViewerData, CustomRedirect, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, ListProgressOptions, MenuItem, Playlist, Portal, PortalDisplay, PortalHero, PortalLayout, PortalNavigation, PortalTheme, ProgressListMeta, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, SaveProgressData, SearchOptions, Segment, Settings, Source, ThemeColors, ThemeConfig, UpdateViewerData, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, Viewer, ViewerAPIError, ViewerLookupParams, ViewerProgress, createClient };
830
+ /**
831
+ * Error thrown by Community API operations
832
+ */
833
+ declare class CommunityAPIError extends Error {
834
+ readonly status?: number;
835
+ readonly originalError?: Error;
836
+ constructor(method: string, url: string, error: unknown);
837
+ }
838
+
839
+ export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AnalyticsProvider, AskOptions, AssistantConfig, ChatOptions, Citation, ClientOptions, Comment, CommunityAPIError, Conversation, ConversationMessage, ConversationMetadata, CreateCommentData, CreatePostData, CreateViewerData, CustomRedirect, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, ListPostsOptions, ListProgressOptions, ListVideosIndexOptions, ListVideosLatestOptions, ListVideosOptions, MenuItem, Playlist, Portal, PortalDisplay, PortalHero, PortalLayout, PortalNavigation, PortalTheme, Post, PostAuthor, ProgressListMeta, ReactionResponse, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, SaveProgressData, SearchOptions, Segment, Settings, Source, ThemeColors, ThemeConfig, UpdatePostData, UpdateViewerData, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, Viewer, ViewerAPIError, ViewerLookupParams, ViewerProgress, createClient };
package/dist/index.js CHANGED
@@ -25,6 +25,15 @@ function camelizeKeys(input, options = {}) {
25
25
  }
26
26
 
27
27
  // src/lib/fetchers.ts
28
+ function toQuery(params) {
29
+ const qs = new URLSearchParams();
30
+ for (const [k, v] of Object.entries(params)) {
31
+ if (v !== void 0 && v !== null)
32
+ qs.set(k, String(v));
33
+ }
34
+ const s = qs.toString();
35
+ return s ? `?${s}` : "";
36
+ }
28
37
  async function get(client, url) {
29
38
  try {
30
39
  const res = await client.get(url);
@@ -51,14 +60,44 @@ function fetchSettings(client) {
51
60
  };
52
61
  }
53
62
  function fetchVideos(client) {
54
- return async (videoLimit = 12) => {
63
+ return async (arg = 12) => {
55
64
  try {
65
+ if (typeof arg === "number") {
66
+ return await get(
67
+ client,
68
+ `videos/latest${toQuery({ limit: arg })}`
69
+ );
70
+ }
71
+ const opts = arg;
72
+ const hasPage = "page" in opts && opts.page !== void 0;
73
+ if (hasPage && ("limit" in opts || "viewerId" in opts)) {
74
+ throw new Error(
75
+ "videos.list(): cannot use `page` with `limit` or `viewerId` (these belong to different endpoints)"
76
+ );
77
+ }
78
+ if (hasPage) {
79
+ const { page, tag: tag2, collectionId: collectionId2 } = opts;
80
+ return await get(
81
+ client,
82
+ `videos${toQuery({
83
+ page,
84
+ tag: tag2,
85
+ collection_id: collectionId2
86
+ })}`
87
+ );
88
+ }
89
+ const { limit, tag, collectionId, viewerId } = opts;
56
90
  return await get(
57
91
  client,
58
- `videos/latest?limit=${videoLimit}`
92
+ `videos/latest${toQuery({
93
+ limit: limit ?? 12,
94
+ tag,
95
+ collection_id: collectionId,
96
+ viewer_id: viewerId
97
+ })}`
59
98
  );
60
99
  } catch (error) {
61
- console.error(`Error fetching videos with limit: ${videoLimit}`, error);
100
+ console.error(`Error fetching videos`, error);
62
101
  throw error;
63
102
  }
64
103
  };
@@ -252,6 +291,188 @@ function saveProgress(client) {
252
291
  };
253
292
  }
254
293
 
294
+ // src/lib/community.ts
295
+ import { AxiosError as AxiosError2 } from "axios";
296
+ var CommunityAPIError = class extends Error {
297
+ constructor(method, url, error) {
298
+ var __super = (...args) => {
299
+ super(...args);
300
+ };
301
+ if (error instanceof AxiosError2) {
302
+ const status = error.response?.status;
303
+ const message = error.response?.data?.error || error.message;
304
+ __super(`${method} ${url} failed (${status}): ${message}`);
305
+ this.status = status;
306
+ this.originalError = error;
307
+ } else if (error instanceof Error) {
308
+ __super(`${method} ${url} failed: ${error.message}`);
309
+ this.originalError = error;
310
+ } else {
311
+ __super(`${method} ${url} failed: ${String(error)}`);
312
+ }
313
+ this.name = "CommunityAPIError";
314
+ }
315
+ };
316
+ function requireViewerId(viewerId) {
317
+ if (!viewerId)
318
+ throw new Error("Viewer ID is required (X-Viewer-ID)");
319
+ }
320
+ function viewerHeaders(viewerId) {
321
+ return viewerId ? { "X-Viewer-ID": viewerId } : void 0;
322
+ }
323
+ function toQuery2(params) {
324
+ const qs = new URLSearchParams();
325
+ for (const [k, v] of Object.entries(params)) {
326
+ if (v !== void 0 && v !== null)
327
+ qs.set(k, String(v));
328
+ }
329
+ const s = qs.toString();
330
+ return s ? `?${s}` : "";
331
+ }
332
+ async function get3(client, url, viewerId) {
333
+ try {
334
+ const res = await client.get(url, { headers: viewerHeaders(viewerId) });
335
+ return camelizeKeys(res.data);
336
+ } catch (error) {
337
+ throw new CommunityAPIError("GET", url, error);
338
+ }
339
+ }
340
+ async function post2(client, url, data, viewerId) {
341
+ try {
342
+ const res = await client.post(url, data, { headers: viewerHeaders(viewerId) });
343
+ return camelizeKeys(res.data);
344
+ } catch (error) {
345
+ throw new CommunityAPIError("POST", url, error);
346
+ }
347
+ }
348
+ async function put(client, url, data, viewerId) {
349
+ try {
350
+ const res = await client.put(url, data, { headers: viewerHeaders(viewerId) });
351
+ return camelizeKeys(res.data);
352
+ } catch (error) {
353
+ throw new CommunityAPIError("PUT", url, error);
354
+ }
355
+ }
356
+ async function del(client, url, viewerId) {
357
+ try {
358
+ const res = await client.delete(url, { headers: viewerHeaders(viewerId) });
359
+ return camelizeKeys(res.data);
360
+ } catch (error) {
361
+ throw new CommunityAPIError("DELETE", url, error);
362
+ }
363
+ }
364
+ function listPosts(client) {
365
+ return async (opts = {}) => {
366
+ return get3(
367
+ client,
368
+ `community/posts${toQuery2({
369
+ category: opts.category,
370
+ limit: opts.limit,
371
+ offset: opts.offset
372
+ })}`,
373
+ opts.viewerId
374
+ );
375
+ };
376
+ }
377
+ function getPost(client) {
378
+ return async (id, viewerId) => {
379
+ if (!id)
380
+ throw new Error("Post ID is required");
381
+ return get3(client, `community/posts/${id}`, viewerId);
382
+ };
383
+ }
384
+ function createPost(client) {
385
+ return async (viewerId, data) => {
386
+ requireViewerId(viewerId);
387
+ if (!data?.content)
388
+ throw new Error("Post content is required");
389
+ return post2(
390
+ client,
391
+ "community/posts",
392
+ { post: { content: data.content, category: data.category } },
393
+ viewerId
394
+ );
395
+ };
396
+ }
397
+ function updatePost(client) {
398
+ return async (viewerId, id, data) => {
399
+ requireViewerId(viewerId);
400
+ if (!id)
401
+ throw new Error("Post ID is required");
402
+ const body = {};
403
+ if (data.content !== void 0)
404
+ body.content = data.content;
405
+ if (data.category !== void 0)
406
+ body.category = data.category;
407
+ return put(
408
+ client,
409
+ `community/posts/${id}`,
410
+ { post: body },
411
+ viewerId
412
+ );
413
+ };
414
+ }
415
+ function deletePost(client) {
416
+ return async (viewerId, id) => {
417
+ requireViewerId(viewerId);
418
+ if (!id)
419
+ throw new Error("Post ID is required");
420
+ return del(client, `community/posts/${id}`, viewerId);
421
+ };
422
+ }
423
+ function reactToPost(client) {
424
+ return async (viewerId, id) => {
425
+ requireViewerId(viewerId);
426
+ if (!id)
427
+ throw new Error("Post ID is required");
428
+ return post2(
429
+ client,
430
+ `community/posts/${id}/react`,
431
+ void 0,
432
+ viewerId
433
+ );
434
+ };
435
+ }
436
+ function createComment(client) {
437
+ return async (viewerId, postId, data) => {
438
+ requireViewerId(viewerId);
439
+ if (!postId)
440
+ throw new Error("Post ID is required");
441
+ if (!data?.content)
442
+ throw new Error("Comment content is required");
443
+ const body = { content: data.content };
444
+ if (data.parentId)
445
+ body.parent_id = data.parentId;
446
+ return post2(
447
+ client,
448
+ `community/posts/${postId}/comments`,
449
+ { comment: body },
450
+ viewerId
451
+ );
452
+ };
453
+ }
454
+ function deleteComment(client) {
455
+ return async (viewerId, id) => {
456
+ requireViewerId(viewerId);
457
+ if (!id)
458
+ throw new Error("Comment ID is required");
459
+ return del(client, `community/comments/${id}`, viewerId);
460
+ };
461
+ }
462
+ function reactToComment(client) {
463
+ return async (viewerId, id) => {
464
+ requireViewerId(viewerId);
465
+ if (!id)
466
+ throw new Error("Comment ID is required");
467
+ return post2(
468
+ client,
469
+ `community/comments/${id}/react`,
470
+ void 0,
471
+ viewerId
472
+ );
473
+ };
474
+ }
475
+
255
476
  // src/util/throttle.ts
256
477
  var throttle = (fn, delay) => {
257
478
  let wait = false;
@@ -585,11 +806,27 @@ function createClient(apiKey, options = {}) {
585
806
  saveProgress: saveProgress(apiClient)
586
807
  },
587
808
  ai: createAI(aiConfig),
809
+ community: {
810
+ posts: {
811
+ list: listPosts(apiClient),
812
+ get: getPost(apiClient),
813
+ create: createPost(apiClient),
814
+ update: updatePost(apiClient),
815
+ delete: deletePost(apiClient),
816
+ react: reactToPost(apiClient)
817
+ },
818
+ comments: {
819
+ create: createComment(apiClient),
820
+ delete: deleteComment(apiClient),
821
+ react: reactToComment(apiClient)
822
+ }
823
+ },
588
824
  trackEvent: trackEvent(apiClient, userId, { debug }),
589
825
  trackPageView: trackPageView(apiClient, userId, { debug })
590
826
  };
591
827
  }
592
828
  export {
829
+ CommunityAPIError,
593
830
  DEFAULT_API_BASE_URL,
594
831
  DEFAULT_INTERNAL_API_BASE_URL,
595
832
  ViewerAPIError,
package/llms.txt CHANGED
@@ -55,12 +55,35 @@ interface ClientOptions {
55
55
  All content methods return `Promise<{ data: T }>`.
56
56
 
57
57
  - `bold.settings(videoLimit?)` - Channel settings, menus, featured playlists
58
- - `bold.videos.list(limit?)` - List videos (default limit: 12)
58
+ - `bold.videos.list()` - List latest videos (default limit: 12)
59
+ - `bold.videos.list(limit)` - List with numeric limit (backwards compatible)
60
+ - `bold.videos.list(options)` - List with full options (see ListVideosOptions)
59
61
  - `bold.videos.get(id)` - Get video by ID or slug
60
62
  - `bold.videos.search(query)` - Search videos
61
63
  - `bold.playlists.list()` - List playlists
62
64
  - `bold.playlists.get(id)` - Get playlist with videos
63
65
 
66
+ ### ListVideosOptions
67
+
68
+ ```typescript
69
+ // For /videos/latest endpoint (default)
70
+ {
71
+ limit?: number; // Max videos (default: 12)
72
+ tag?: string; // Filter by tag
73
+ collectionId?: string; // Filter to collection
74
+ viewerId?: string; // Include watch progress
75
+ }
76
+
77
+ // For /videos (index) endpoint - use when you need pagination
78
+ {
79
+ page?: number; // Page number
80
+ tag?: string;
81
+ collectionId?: string;
82
+ }
83
+ ```
84
+
85
+ Note: If `page` is provided, SDK uses `/videos` (index); otherwise uses `/videos/latest`.
86
+
64
87
  ## Viewers API
65
88
 
66
89
  Manage external users and track video progress. Returns `Promise<{ data: T }>` (consistent with other SDK methods).
@@ -88,6 +111,33 @@ const { data: progress, meta } = await bold.viewers.listProgress(viewer.id, { co
88
111
  console.log(`Completed ${meta.completed} of ${meta.total} videos`);
89
112
  ```
90
113
 
114
+ ## Community API
115
+
116
+ Build community features with posts, comments, and reactions. Write operations require `viewerId`.
117
+
118
+ ### Posts
119
+
120
+ - `bold.community.posts.list(opts?)` - List posts with optional filters
121
+ - `bold.community.posts.get(id, viewerId?)` - Get post with comments
122
+ - `bold.community.posts.create(viewerId, data)` - Create a post
123
+ - `bold.community.posts.update(viewerId, id, data)` - Update post (owner/admin)
124
+ - `bold.community.posts.delete(viewerId, id)` - Delete post (owner/admin)
125
+ - `bold.community.posts.react(viewerId, id)` - Toggle reaction
126
+
127
+ ### Comments
128
+
129
+ - `bold.community.comments.create(viewerId, postId, data)` - Create comment
130
+ - `bold.community.comments.delete(viewerId, id)` - Delete comment (owner/admin)
131
+ - `bold.community.comments.react(viewerId, id)` - Toggle reaction
132
+
133
+ ```typescript
134
+ // Example
135
+ const { data: posts } = await bold.community.posts.list({ category: 'general', limit: 20 });
136
+ await bold.community.posts.create(viewerId, { content: 'Hello!', category: 'general' });
137
+ await bold.community.posts.react(viewerId, posts[0].id);
138
+ await bold.community.comments.create(viewerId, posts[0].id, { content: 'Great post!' });
139
+ ```
140
+
91
141
  ## AI Methods
92
142
 
93
143
  All AI methods return `AsyncIterable<AIEvent>` (streaming) or `Promise<AIResponse>` (non-streaming).
@@ -252,9 +302,44 @@ type AIEvent =
252
302
  }
253
303
  ```
254
304
 
305
+ ### Post
306
+
307
+ ```typescript
308
+ {
309
+ id: string;
310
+ content: string; // Markdown content
311
+ category: string;
312
+ pinned: boolean;
313
+ pinnedAt?: string | null;
314
+ reactionsCount: number;
315
+ commentsCount: number;
316
+ viewerReacted?: boolean; // Only with X-Viewer-ID
317
+ viewer: PostAuthor;
318
+ comments?: Comment[]; // Only on single post fetch
319
+ createdAt: string;
320
+ updatedAt: string;
321
+ }
322
+ ```
323
+
324
+ ### Comment
325
+
326
+ ```typescript
327
+ {
328
+ id: string;
329
+ content: string;
330
+ depth: number; // Nesting level (0 = top-level)
331
+ reactionsCount: number;
332
+ viewerReacted?: boolean;
333
+ viewer: PostAuthor;
334
+ replies: Comment[]; // Nested replies
335
+ createdAt: string;
336
+ updatedAt: string;
337
+ }
338
+ ```
339
+
255
340
  ### Other types exported
256
341
 
257
- `Playlist`, `Settings`, `Portal`, `MenuItem`, `AIResponse`, `AIUsage`, `AIContextMessage`, `Conversation`, `ConversationMessage`, `RecommendationsResponse`, `Viewer`, `ViewerProgress`, `ViewerLookupParams`, `ListProgressOptions`
342
+ `Playlist`, `Settings`, `Portal`, `MenuItem`, `AIResponse`, `AIUsage`, `AIContextMessage`, `Conversation`, `ConversationMessage`, `RecommendationsResponse`, `Viewer`, `ViewerProgress`, `ViewerLookupParams`, `ListProgressOptions`, `ListVideosOptions`, `ListVideosLatestOptions`, `ListVideosIndexOptions`, `Post`, `PostAuthor`, `Comment`, `ReactionResponse`, `ListPostsOptions`, `CreatePostData`, `UpdatePostData`, `CreateCommentData`, `CommunityAPIError`
258
343
 
259
344
  ## Error Handling
260
345
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@boldvideo/bold-js",
3
3
  "license": "MIT",
4
- "version": "1.16.0",
4
+ "version": "1.18.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",