@boldvideo/bold-js 1.15.2 → 1.17.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/dist/index.js CHANGED
@@ -4,9 +4,10 @@ import axios from "axios";
4
4
  // src/util/camelize.ts
5
5
  var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
6
6
  var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
7
- function camelizeKeys(input) {
7
+ function camelizeKeys(input, options = {}) {
8
+ const preserve = new Set(options.preserveKeys ?? []);
8
9
  if (Array.isArray(input)) {
9
- return input.map((item) => camelizeKeys(item));
10
+ return input.map((item) => camelizeKeys(item, options));
10
11
  }
11
12
  if (!isPlainObject(input)) {
12
13
  return input;
@@ -14,12 +15,25 @@ function camelizeKeys(input) {
14
15
  const out = {};
15
16
  for (const [rawKey, value] of Object.entries(input)) {
16
17
  const key = snakeToCamel(rawKey);
17
- out[key] = camelizeKeys(value);
18
+ if (preserve.has(rawKey) || preserve.has(key)) {
19
+ out[key] = value;
20
+ continue;
21
+ }
22
+ out[key] = camelizeKeys(value, options);
18
23
  }
19
24
  return out;
20
25
  }
21
26
 
22
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
+ }
23
37
  async function get(client, url) {
24
38
  try {
25
39
  const res = await client.get(url);
@@ -46,14 +60,44 @@ function fetchSettings(client) {
46
60
  };
47
61
  }
48
62
  function fetchVideos(client) {
49
- return async (videoLimit = 12) => {
63
+ return async (arg = 12) => {
50
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;
51
90
  return await get(
52
91
  client,
53
- `videos/latest?limit=${videoLimit}`
92
+ `videos/latest${toQuery({
93
+ limit: limit ?? 12,
94
+ tag,
95
+ collection_id: collectionId,
96
+ viewer_id: viewerId
97
+ })}`
54
98
  );
55
99
  } catch (error) {
56
- console.error(`Error fetching videos with limit: ${videoLimit}`, error);
100
+ console.error(`Error fetching videos`, error);
57
101
  throw error;
58
102
  }
59
103
  };
@@ -99,6 +143,154 @@ function fetchPlaylist(client) {
99
143
  };
100
144
  }
101
145
 
146
+ // src/lib/viewers.ts
147
+ import { AxiosError } from "axios";
148
+ var VIEWER_CAMELIZE_OPTIONS = { preserveKeys: ["traits"] };
149
+ var ViewerAPIError = class extends Error {
150
+ constructor(method, url, error) {
151
+ var __super = (...args) => {
152
+ super(...args);
153
+ };
154
+ if (error instanceof AxiosError) {
155
+ const status = error.response?.status;
156
+ const message = error.response?.data?.error || error.message;
157
+ __super(`${method} ${url} failed (${status}): ${message}`);
158
+ this.status = status;
159
+ this.originalError = error;
160
+ } else if (error instanceof Error) {
161
+ __super(`${method} ${url} failed: ${error.message}`);
162
+ this.originalError = error;
163
+ } else {
164
+ __super(`${method} ${url} failed: ${String(error)}`);
165
+ }
166
+ this.name = "ViewerAPIError";
167
+ }
168
+ };
169
+ async function get2(client, url, options) {
170
+ try {
171
+ const res = await client.get(url);
172
+ return camelizeKeys(res.data, options);
173
+ } catch (error) {
174
+ throw new ViewerAPIError("GET", url, error);
175
+ }
176
+ }
177
+ async function post(client, url, data, options) {
178
+ try {
179
+ const res = await client.post(url, data);
180
+ return camelizeKeys(res.data, options);
181
+ } catch (error) {
182
+ throw new ViewerAPIError("POST", url, error);
183
+ }
184
+ }
185
+ async function patch(client, url, data, options) {
186
+ try {
187
+ const res = await client.patch(url, data);
188
+ return camelizeKeys(res.data, options);
189
+ } catch (error) {
190
+ throw new ViewerAPIError("PATCH", url, error);
191
+ }
192
+ }
193
+ function fetchViewers(client) {
194
+ return async () => {
195
+ return get2(client, "viewers", VIEWER_CAMELIZE_OPTIONS);
196
+ };
197
+ }
198
+ function fetchViewer(client) {
199
+ return async (id) => {
200
+ if (!id)
201
+ throw new Error("Viewer ID is required");
202
+ return get2(client, `viewers/${id}`, VIEWER_CAMELIZE_OPTIONS);
203
+ };
204
+ }
205
+ function lookupViewer(client) {
206
+ return async (params) => {
207
+ const qs = new URLSearchParams();
208
+ if ("externalId" in params && params.externalId) {
209
+ qs.set("external_id", params.externalId);
210
+ }
211
+ if ("email" in params && params.email) {
212
+ qs.set("email", params.email);
213
+ }
214
+ if (!qs.toString()) {
215
+ throw new Error("Either externalId or email is required");
216
+ }
217
+ return get2(client, `viewers/lookup?${qs.toString()}`, VIEWER_CAMELIZE_OPTIONS);
218
+ };
219
+ }
220
+ function createViewer(client) {
221
+ return async (data) => {
222
+ if (!data.name)
223
+ throw new Error("Viewer name is required");
224
+ return post(client, "viewers", {
225
+ viewer: {
226
+ name: data.name,
227
+ email: data.email,
228
+ external_id: data.externalId,
229
+ traits: data.traits
230
+ }
231
+ }, VIEWER_CAMELIZE_OPTIONS);
232
+ };
233
+ }
234
+ function updateViewer(client) {
235
+ return async (id, data) => {
236
+ if (!id)
237
+ throw new Error("Viewer ID is required");
238
+ const body = {};
239
+ if (data.name !== void 0)
240
+ body.name = data.name;
241
+ if (data.email !== void 0)
242
+ body.email = data.email;
243
+ if (data.externalId !== void 0)
244
+ body.external_id = data.externalId;
245
+ if (data.traits !== void 0)
246
+ body.traits = data.traits;
247
+ return patch(client, `viewers/${id}`, { viewer: body }, VIEWER_CAMELIZE_OPTIONS);
248
+ };
249
+ }
250
+ function fetchViewerProgress(client) {
251
+ return async (viewerId, options) => {
252
+ if (!viewerId)
253
+ throw new Error("Viewer ID is required");
254
+ const params = new URLSearchParams();
255
+ if (options?.completed !== void 0)
256
+ params.set("completed", String(options.completed));
257
+ if (options?.collectionId)
258
+ params.set("collection_id", options.collectionId);
259
+ const query = params.toString();
260
+ const url = query ? `viewers/${viewerId}/progress?${query}` : `viewers/${viewerId}/progress`;
261
+ return get2(client, url);
262
+ };
263
+ }
264
+ function fetchProgress(client) {
265
+ return async (viewerId, videoId) => {
266
+ if (!viewerId)
267
+ throw new Error("Viewer ID is required");
268
+ if (!videoId)
269
+ throw new Error("Video ID is required");
270
+ return get2(client, `viewers/${viewerId}/progress/${videoId}`);
271
+ };
272
+ }
273
+ function saveProgress(client) {
274
+ return async (viewerId, videoId, data) => {
275
+ if (!viewerId)
276
+ throw new Error("Viewer ID is required");
277
+ if (!videoId)
278
+ throw new Error("Video ID is required");
279
+ if (!Number.isFinite(data.currentTime) || data.currentTime < 0) {
280
+ throw new Error("currentTime must be a non-negative number");
281
+ }
282
+ if (!Number.isFinite(data.duration) || data.duration <= 0) {
283
+ throw new Error("duration must be a positive number");
284
+ }
285
+ return post(client, `viewers/${viewerId}/progress/${videoId}`, {
286
+ progress: {
287
+ current_time: data.currentTime,
288
+ duration: data.duration
289
+ }
290
+ });
291
+ };
292
+ }
293
+
102
294
  // src/util/throttle.ts
103
295
  var throttle = (fn, delay) => {
104
296
  let wait = false;
@@ -421,6 +613,16 @@ function createClient(apiKey, options = {}) {
421
613
  list: fetchPlaylists(apiClient),
422
614
  get: fetchPlaylist(apiClient)
423
615
  },
616
+ viewers: {
617
+ list: fetchViewers(apiClient),
618
+ get: fetchViewer(apiClient),
619
+ lookup: lookupViewer(apiClient),
620
+ create: createViewer(apiClient),
621
+ update: updateViewer(apiClient),
622
+ listProgress: fetchViewerProgress(apiClient),
623
+ getProgress: fetchProgress(apiClient),
624
+ saveProgress: saveProgress(apiClient)
625
+ },
424
626
  ai: createAI(aiConfig),
425
627
  trackEvent: trackEvent(apiClient, userId, { debug }),
426
628
  trackPageView: trackPageView(apiClient, userId, { debug })
@@ -429,5 +631,6 @@ function createClient(apiKey, options = {}) {
429
631
  export {
430
632
  DEFAULT_API_BASE_URL,
431
633
  DEFAULT_INTERNAL_API_BASE_URL,
634
+ ViewerAPIError,
432
635
  createClient
433
636
  };
@@ -0,0 +1,393 @@
1
+ ---
2
+ title: "docs: Enhance llms.txt for AI consumption"
3
+ type: docs
4
+ date: 2026-01-22
5
+ ---
6
+
7
+ # docs: Enhance llms.txt for AI consumption
8
+
9
+ ## Overview
10
+
11
+ Rewrite the existing `llms.txt` file to be crystal clear for LLMs to understand and use the Bold JS SDK. The current file (121 lines) covers basics but lacks critical information LLMs need to generate correct code.
12
+
13
+ ## Problem Statement / Motivation
14
+
15
+ LLMs reading the current `llms.txt` will generate broken code because:
16
+
17
+ 1. **Missing `{ data: T }` wrapper** - Content methods return `{ data: Video[] }`, not `Video[]`. Every generated code snippet will fail to access data.
18
+ 2. **Incomplete AIEvent types** - Only `text_delta` shown; 5 other event types undocumented
19
+ 3. **No error handling guidance** - LLMs cannot generate production-ready code
20
+ 4. **`getConversation()` missing** - Multi-turn conversation flow incomplete
21
+ 5. **Segment type absent** - Cannot work with sources/citations correctly
22
+ 6. **Browser-only tracking undocumented** - Node.js users will hit runtime errors
23
+
24
+ The llmstxt.org specification recommends keeping files under 10KB while being comprehensive. Our current file is ~4KB with significant gaps.
25
+
26
+ ## Proposed Solution
27
+
28
+ Rewrite `llms.txt` to ~8KB with these sections:
29
+
30
+ 1. **Header** - SDK name, purpose, installation
31
+ 2. **Quick Start** - Working example showing `{ data }` destructuring
32
+ 3. **Client Configuration** - `createClient()` with `ClientOptions`
33
+ 4. **Content Methods** - Videos, Playlists, Settings with return types
34
+ 5. **AI Methods** - All methods including `getConversation()`
35
+ 6. **AI Streaming** - Complete `AIEvent` union with all 6 types
36
+ 7. **Core Types** - `Video`, `Segment`, `Recommendation` inline
37
+ 8. **Error Handling** - HTTP errors, streaming errors, common issues
38
+ 9. **Analytics** - Browser-only warning, supported events
39
+ 10. **Links** - GitHub, npm, API docs
40
+
41
+ ## Technical Approach
42
+
43
+ ### File Structure
44
+
45
+ ```
46
+ llms.txt (~8KB)
47
+ ├── # Bold Video JavaScript SDK (H1)
48
+ ├── ## Installation
49
+ ├── ## Quick Start (with correct { data } pattern)
50
+ ├── ## Client Configuration
51
+ │ ├── createClient(apiKey, options?)
52
+ │ └── ClientOptions type inline
53
+ ├── ## Content Methods
54
+ │ ├── bold.settings()
55
+ │ ├── bold.videos.list(limit?)
56
+ │ ├── bold.videos.get(id) // Note: accepts ID or slug
57
+ │ ├── bold.videos.search(query)
58
+ │ ├── bold.playlists.list()
59
+ │ └── bold.playlists.get(id)
60
+ ├── ## AI Methods
61
+ │ ├── bold.ai.chat(options)
62
+ │ ├── bold.ai.search(options)
63
+ │ ├── bold.ai.recommendations(options)
64
+ │ ├── bold.ai.getConversation(id) // NEW
65
+ │ └── Deprecated aliases note
66
+ ├── ## AI Streaming Events (AIEvent)
67
+ │ ├── message_start
68
+ │ ├── sources
69
+ │ ├── text_delta
70
+ │ ├── recommendations
71
+ │ ├── message_complete
72
+ │ └── error
73
+ ├── ## AI Options
74
+ │ ├── ChatOptions
75
+ │ ├── SearchOptions
76
+ │ └── RecommendationsOptions
77
+ ├── ## Core Types
78
+ │ ├── Video (inline definition)
79
+ │ ├── Segment (inline definition)
80
+ │ ├── Recommendation (inline definition)
81
+ │ └── Others listed by name
82
+ ├── ## Error Handling
83
+ │ ├── HTTP Errors (401, 403, 429, 500)
84
+ │ ├── Streaming Errors (AIEvent error type)
85
+ │ └── Common Issues
86
+ ├── ## Analytics (Browser Only)
87
+ │ ├── trackEvent(video, event)
88
+ │ └── trackPageView(title)
89
+ └── ## Links
90
+ ```
91
+
92
+ ### Key Changes
93
+
94
+ | Section | Current | Enhanced |
95
+ |---------|---------|----------|
96
+ | Quick Start | `await bold.videos.list()` | `const { data } = await bold.videos.list()` |
97
+ | AI Methods | 3 methods | 4 methods (add `getConversation`) |
98
+ | AIEvent | Mentioned | Full 6-type union with fields |
99
+ | Types | Listed names | Video, Segment, Recommendation inline |
100
+ | Errors | None | HTTP codes + streaming errors |
101
+ | Analytics | 2 lines | Browser-only warning + details |
102
+
103
+ ## Acceptance Criteria
104
+
105
+ ### Content Requirements
106
+
107
+ - [x] Quick Start shows `{ data }` destructuring pattern
108
+ - [x] `ClientOptions` type shown with `baseURL`, `debug`, `headers`
109
+ - [x] `videos.get(id)` documents ID or slug acceptance
110
+ - [x] `videos.list(limit?)` shows optional limit parameter
111
+ - [x] `getConversation(id)` documented under AI Methods
112
+ - [x] All 6 AIEvent types shown with their fields
113
+ - [x] `Segment` type shown inline (replaces deprecated `Source`)
114
+ - [x] Error handling section with HTTP codes and streaming errors
115
+ - [x] Analytics section marked "Browser Only"
116
+ - [x] File size under 10KB (actual: 6.6KB)
117
+
118
+ ### Code Examples Must Work
119
+
120
+ - [x] Client initialization compiles
121
+ - [x] Video listing with `{ data }` works
122
+ - [x] AI streaming loop handles all event types
123
+ - [x] Error handling pattern catches both sync and async errors
124
+
125
+ ### Format Requirements
126
+
127
+ - [x] Follows llmstxt.org Markdown conventions
128
+ - [x] H1 title, H2 sections, H3 subsections
129
+ - [x] Code blocks with `typescript` syntax highlighting
130
+ - [x] Consistent terminology (use "Segment" not "Source")
131
+
132
+ ## Success Metrics
133
+
134
+ 1. **LLM Code Generation Accuracy** - Generated code runs without immediate type/property errors
135
+ 2. **File Size** - Under 10KB (target: ~8KB)
136
+ 3. **Section Coverage** - All 10 planned sections present
137
+ 4. **Zero Missing Criticals** - No undocumented critical paths
138
+
139
+ ## Dependencies & Risks
140
+
141
+ ### Dependencies
142
+ - None - this is documentation only
143
+
144
+ ### Risks
145
+ | Risk | Mitigation |
146
+ |------|------------|
147
+ | File too large (>10KB) | Keep types minimal; link to types.ts for full definitions |
148
+ | Breaking existing LLM workflows | Maintain same structure/sections; additions only |
149
+ | Missing edge cases | SpecFlow analysis identified gaps; review against it |
150
+
151
+ ## Implementation Checklist
152
+
153
+ ### llms.txt
154
+
155
+ ```markdown
156
+ # Bold Video JavaScript SDK
157
+
158
+ > TypeScript client for the Bold Video API. Fetch videos, playlists, settings. Stream AI responses. Track analytics.
159
+
160
+ ## Installation
161
+
162
+ npm install @boldvideo/bold-js
163
+
164
+ ## Quick Start
165
+
166
+ import { createClient } from '@boldvideo/bold-js';
167
+
168
+ const bold = createClient('your-api-key');
169
+
170
+ // Content methods return { data: T }
171
+ const { data: videos } = await bold.videos.list();
172
+ const { data: video } = await bold.videos.get('video-id-or-slug');
173
+
174
+ // AI streaming (default)
175
+ const stream = await bold.ai.chat({ prompt: 'How do I price my SaaS?' });
176
+ for await (const event of stream) {
177
+ switch (event.type) {
178
+ case 'text_delta': process.stdout.write(event.delta); break;
179
+ case 'sources': console.log('Found:', event.sources.length); break;
180
+ case 'message_complete': console.log('Done:', event.conversationId); break;
181
+ case 'error': console.error(event.message); break;
182
+ }
183
+ }
184
+
185
+ // AI non-streaming
186
+ const response = await bold.ai.recommendations({
187
+ topics: ['sales', 'negotiation'],
188
+ stream: false
189
+ });
190
+ console.log(response.guidance);
191
+
192
+ ## Client Configuration
193
+
194
+ createClient(apiKey: string, options?: ClientOptions)
195
+
196
+ interface ClientOptions {
197
+ baseURL?: string; // Default: 'https://app.boldvideo.io/api/v1/'
198
+ debug?: boolean; // Log requests (default: false)
199
+ headers?: Record<string, string>; // Additional headers
200
+ }
201
+
202
+ ## Content Methods
203
+
204
+ All content methods return Promise<{ data: T }>.
205
+
206
+ - bold.settings(videoLimit?) - Channel settings, menus, featured playlists
207
+ - bold.videos.list(limit?) - List videos (default: 12)
208
+ - bold.videos.get(id) - Get video by ID or slug
209
+ - bold.videos.search(query) - Search videos
210
+ - bold.playlists.list() - List playlists
211
+ - bold.playlists.get(id) - Get playlist with videos
212
+
213
+ ## AI Methods
214
+
215
+ All AI methods return AsyncIterable<AIEvent> (streaming) or Promise<AIResponse> (non-streaming).
216
+ Default is streaming (stream: true).
217
+
218
+ - bold.ai.chat(options: ChatOptions) - Conversational Q&A (library-wide or video-scoped)
219
+ - bold.ai.search(options: SearchOptions) - Semantic search with AI summary
220
+ - bold.ai.recommendations(options: RecommendationsOptions) - Topic-based video recommendations
221
+ - bold.ai.getConversation(id: string) - Retrieve conversation history by ID
222
+
223
+ Deprecated aliases (still work):
224
+ - bold.ai.ask() -> use chat()
225
+ - bold.ai.coach() -> use chat()
226
+ - bold.ai.recommend() -> use recommendations()
227
+
228
+ ## AI Streaming Events
229
+
230
+ type AIEvent =
231
+ | { type: "message_start"; conversationId?: string; videoId?: string }
232
+ | { type: "sources"; sources: Segment[] }
233
+ | { type: "text_delta"; delta: string }
234
+ | { type: "recommendations"; recommendations: Recommendation[] }
235
+ | { type: "message_complete";
236
+ conversationId?: string;
237
+ content: string;
238
+ citations: Segment[];
239
+ responseType: "answer" | "clarification";
240
+ usage?: AIUsage;
241
+ context?: AIContextMessage[];
242
+ recommendations?: Recommendation[];
243
+ guidance?: string }
244
+ | { type: "error"; code: string; message: string; retryable: boolean }
245
+
246
+ ## AI Options
247
+
248
+ ### ChatOptions
249
+ {
250
+ prompt: string; // Required
251
+ stream?: boolean; // Default: true
252
+ videoId?: string; // Scope to specific video
253
+ currentTime?: number; // Playback position (with videoId)
254
+ conversationId?: string; // Continue conversation
255
+ collectionId?: string; // Filter to collection
256
+ tags?: string[]; // Filter by tags
257
+ }
258
+
259
+ ### SearchOptions
260
+ {
261
+ prompt: string; // Required
262
+ stream?: boolean; // Default: true
263
+ limit?: number; // Max results
264
+ collectionId?: string;
265
+ videoId?: string; // Search within video
266
+ tags?: string[];
267
+ context?: AIContextMessage[];
268
+ }
269
+
270
+ ### RecommendationsOptions
271
+ {
272
+ topics: string[]; // Required (max: 10)
273
+ stream?: boolean; // Default: true
274
+ limit?: number; // Max per topic (default: 5, max: 20)
275
+ collectionId?: string;
276
+ tags?: string[];
277
+ includeGuidance?: boolean; // Default: true
278
+ context?: AIContextMessage[];
279
+ }
280
+
281
+ ## Core Types
282
+
283
+ ### Video
284
+ {
285
+ id: string;
286
+ slug?: string;
287
+ title: string;
288
+ description: string | null;
289
+ duration: number;
290
+ publishedAt: string;
291
+ playbackId: string;
292
+ streamUrl: string;
293
+ thumbnail: string;
294
+ tags?: string[];
295
+ metaData: { title: string; description: string; image: string | null };
296
+ chapters?: string;
297
+ attachments?: VideoAttachment[];
298
+ transcript?: { text: string; json: any };
299
+ }
300
+
301
+ ### Segment (for sources/citations)
302
+ {
303
+ id: string;
304
+ videoId: string;
305
+ title: string;
306
+ text: string; // Transcript excerpt
307
+ timestamp: number; // Start seconds
308
+ timestampEnd: number; // End seconds
309
+ playbackId: string;
310
+ speaker?: string;
311
+ cited?: boolean;
312
+ }
313
+
314
+ ### Recommendation
315
+ {
316
+ topic: string;
317
+ videos: Array<{
318
+ videoId: string;
319
+ title: string;
320
+ playbackId: string;
321
+ relevance: number;
322
+ reason: string;
323
+ }>;
324
+ }
325
+
326
+ ### Other types exported
327
+ Playlist, Settings, Portal, MenuItem, AIResponse, AIUsage, AIContextMessage,
328
+ Conversation, ConversationMessage, RecommendationsResponse
329
+
330
+ ## Error Handling
331
+
332
+ ### HTTP Errors
333
+ SDK throws on non-2xx responses:
334
+ - 401: Invalid API key
335
+ - 403: Forbidden (check permissions)
336
+ - 429: Rate limited (retry with backoff)
337
+ - 500: Server error (retry)
338
+
339
+ try {
340
+ const { data } = await bold.videos.list();
341
+ } catch (error) {
342
+ if (error.response?.status === 401) {
343
+ console.error('Invalid API key');
344
+ }
345
+ }
346
+
347
+ ### Streaming Errors
348
+ Handle error events in stream:
349
+
350
+ for await (const event of stream) {
351
+ if (event.type === 'error') {
352
+ console.error(`[${event.code}] ${event.message}`);
353
+ if (event.retryable) { /* retry logic */ }
354
+ }
355
+ }
356
+
357
+ ## Analytics (Browser Only)
358
+
359
+ These methods require browser globals (window, document, navigator).
360
+ Do not use in Node.js.
361
+
362
+ - bold.trackEvent(video, event) - Track video playback
363
+ video: { id, title, duration }
364
+ event: DOM Event (play, pause, timeupdate, loadedmetadata)
365
+ Note: timeupdate throttled to 5 seconds
366
+
367
+ - bold.trackPageView(title: string) - Track page views
368
+
369
+ ## Links
370
+
371
+ - GitHub: https://github.com/boldvideo/bold-js
372
+ - npm: https://www.npmjs.com/package/@boldvideo/bold-js
373
+ - API Docs: https://docs.boldvideo.io/docs/api
374
+ - Types: https://github.com/boldvideo/bold-js/blob/main/src/lib/types.ts
375
+ ```
376
+
377
+ ## References & Research
378
+
379
+ ### Internal References
380
+ - Current llms.txt: `/llms.txt`
381
+ - Types source: `/src/lib/types.ts`
382
+ - Client factory: `/src/lib/client.ts`
383
+ - AI methods: `/src/lib/ai.ts`
384
+ - README: `/README.md`
385
+
386
+ ### External References
387
+ - [llmstxt.org specification](https://llmstxt.org/) - Official format guide
388
+ - [llms.txt Best Practices (Rankability)](https://www.rankability.com/guides/llms-txt-best-practices/) - Implementation guide
389
+ - [Mintlify llms.txt](https://www.mintlify.com/blog/simplifying-docs-with-llms-txt) - Platform adoption examples
390
+
391
+ ### Industry Adoption
392
+ - Over 844,000 websites use llms.txt (BuiltWith, Oct 2025)
393
+ - Anthropic, Cloudflare, Stripe all use this format