@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/CHANGELOG.md +46 -0
- package/README.md +86 -2
- package/dist/index.cjs +212 -8
- package/dist/index.d.ts +163 -2
- package/dist/index.js +209 -6
- package/docs/plans/2026-01-22-docs-enhance-llms-txt-for-ai-consumption-plan.md +393 -0
- package/docs/plans/2026-01-23-feat-viewers-api-sdk-implementation-plan.md +380 -0
- package/llms.txt +83 -2
- package/package.json +1 -1
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|