@boldvideo/bold-js

中文文档
2.5.0发布于 2026/10/24MITTypeScriptES Module构建来源已验证近 30 天下载 2663
Bold Logo

@boldvid

原始使用文档
文档目录41 节
  1. Installation
  2. Quick Start
  3. API Reference
  4. Videos
  5. Secure playback
  6. Playlists
  7. Settings
  8. Viewers API
  9. Viewer Management
  10. Progress Tracking
  11. Notification Preferences
  12. Session Management
  13. Runtime Sessions
  14. Server-Side Session Management
  15. Community API
  16. Posts
  17. Comments
  18. Mention Inbox
  19. AI Methods
  20. Voice sessions
  21. Chat
  22. Recommendations
  23. Search
  24. AI interaction tracking
  25. Video-Scoped Chat
  26. Get Conversation History
  27. Rate an AI Answer
  28. Multi-turn Conversations
  29. Conversation Starters
  30. Analytics
  31. Explicit AI engagement (browser or server)
  32. Legacy playback and page views
  33. TypeScript
  34. Migration from v1.7.x
  35. Breaking: All response types now use camelCase
  36. Method Changes
  37. Type Renames
  38. Related Links
  39. Contributing
  40. Security
  41. License
Bold Logo

@boldvideo/bold-js

The JavaScript SDK for interacting with the Bold API, to power your own business video platform.

Bold JS npm

Follow @veryboldvideo Try Bold Video


Installation

npm install @boldvideo/bold-js

Quick Start

import { createClient } from '@boldvideo/bold-js';

const bold = createClient('your-api-key');

// Fetch videos
const videos = await bold.videos.list();

// AI-powered recommendations
const recs = await bold.ai.recommendations({ 
  topics: ['sales', 'negotiation'],
  stream: false 
});
console.log(recs.guidance);

API Reference

Videos

// List latest videos (default: 12)
const videos = await bold.videos.list();

// With limit (backwards compatible)
const videos = await bold.videos.list(20);

// With filters
const videos = await bold.videos.list({ 
  limit: 20,
  tag: 'sales',
  collectionId: 'col_123',
  viewerId: 'viewer_123'  // Include watch progress
});

// Paginated index (uses /videos endpoint)
const videos = await bold.videos.list({ 
  page: 2,
  tag: 'sales',
  collectionId: 'col_123'
});

// Get a single video by ID or slug
const video = await bold.videos.get('video-id');
const videoBySlug = await bold.videos.get('my-video-slug');

// Search videos
const results = await bold.videos.search('pricing strategies');

Secure playback

Videos with playbackPolicy: 'signed' require a playback token. Bare Mux stream and image URLs return 403 for these videos. Use the standalone helpers instead of building Mux URLs by hand:

import { createClient, streamUrl, muxPlayerProps, thumbnailUrl } from '@boldvideo/bold-js';

const bold = createClient('YOUR_API_KEY'); // Keep your tenant API key server-side.
const { data: video } = await bold.videos.get('video-id');
const src = streamUrl(video); // For an HLS player.
const playerProps = muxPlayerProps(video); // Spread into <MuxPlayer {...playerProps} />.
const poster = thumbnailUrl(video, { width: 640, height: 360, time: 0, fitMode: 'crop' });

streamUrl preserves the API's URL, falling back to a Mux HLS URL with any supplied playbackToken. muxPlayerProps returns just { playbackId } for public videos; signed videos also get available playback/storyboard tokens and an explicit poster from thumbnail. thumbnailUrl builds a Mux image URL for public videos. For signed videos it returns the stored public thumbnail unchanged and ignores all options: signed Mux images cannot be sized or seeked using query parameters.

The helpers accept Video, citations, search sources, recommendations, or your own object with the needed camelCase fields (PlaybackSource). AI sources carrying a token are treated as signed even without a policy; an explicit public policy takes precedence for player props and thumbnails. URL helpers return undefined when the required URL, playback ID, or signed thumbnail is missing; player props omit missing fields. They format supplied data, not validate credentials: a signed video without a token cannot play via a constructed bare URL. For a source without a thumbnail or storyboard token, re-fetch the full video to obtain the available fields.

Tokens last 12 hours. Video.playbackTokenExpiresAt is an optional nullable ISO8601 timestamp. Re-fetch the video with bold.videos.get() for fresh tokens; there is no refresh endpoint or automatic refresh timer. Token fields are null for public videos. Do not log tokens or tokenized URLs.

Playlists

// List all playlists
const playlists = await bold.playlists.list();

// Get a single playlist with videos
const playlist = await bold.playlists.get('playlist-id');

Settings

// Fetch channel settings, menus, and featured playlists
const settings = await bold.settings();

// Access portal hero configuration
if (settings.portal.hero.type === 'custom') {
  // Render custom hero section
}

// Menu items with external link handling
settings.menuItems.forEach(item => {
  // item.blank: true opens in a new window/tab
  // item.isExt: true indicates an external URL
  // item.icon: optional icon path (can be null)
});

Viewers API

Manage external users and track their video watch progress. Ideal for course platforms integrating with Bold Video.

Viewer Management

// Create a viewer (e.g., when user signs up)
const { data: viewer } = await bold.viewers.create({
  name: 'John Doe',
  externalId: 'user_123',  // Your platform's user ID
  email: '[email protected]',
  traits: { plan: 'pro', company_name: 'Acme Inc' }
});

// Find viewer by external ID (common for syncing users)
const { data: viewer } = await bold.viewers.lookup({ externalId: 'user_123' });

// Or find by email
const { data: viewer } = await bold.viewers.lookup({ email: '[email protected]' });

// Update viewer
await bold.viewers.update(viewer.id, { 
  traits: { plan: 'enterprise' }  // Note: traits are replaced, not merged
});

// List all viewers
const { data: viewers } = await bold.viewers.list();

Progress Tracking

// Save progress as video plays (call every 5-10 seconds)
await bold.viewers.saveProgress(viewerId, videoId, {
  currentTime: 120,  // seconds
  duration: 600      // total video duration
});

// Mark video complete by setting currentTime = duration
await bold.viewers.saveProgress(viewerId, videoId, {
  currentTime: 600,
  duration: 600
});

// Get progress for a specific video
const { data: progress } = await bold.viewers.getProgress(viewerId, videoId);
console.log(`${progress.percentage}% complete`);

// List all progress for a viewer (e.g., for a course dashboard)
const { data: progress, meta } = await bold.viewers.listProgress(viewerId, {
  collectionId: 'course-collection-id',  // Filter to a course
  completed: false  // Only in-progress videos
});
console.log(`Completed ${meta.completed} of ${meta.total} videos`);

Notification Preferences

Read or partially update a viewer's email and push notification channels with the tenant API-key client:

const preferences = await bold.notifications.getPreferences('viewer-uuid');
console.log(preferences.channels.email, preferences.channels.push);

// Only push is changed; the existing email preference is preserved.
await bold.notifications.updatePreferences('viewer-uuid', {
  channels: { push: true }
});

Session Management

Use BOLD Session Management as a headless password-sharing prevention layer. Your app still owns login, membership, product access, and staff/admin checks. Call BOLD only after your app has decided the viewer may access protected content.

Runtime Sessions

Runtime session calls use the customer's upstream JWT, not a BOLD API key. This factory is safe for browser/native runtime code when the upstream JWT is already available to that runtime.

import { createAuthClient } from '@boldvideo/bold-js';

const auth = createAuthClient({
  tenantSlug: 'hrtu',
  upstreamJwt: outsetaJwt
});

const result = await auth.sessions.create({
  deviceId: browserDeviceId,
  platform: 'web',
  userAgent: navigator.userAgent
});

let sessionId: string | undefined;

if ('challengeRequired' in result) {
  // Show an email-code challenge UI, then verify the code.
  const verified = await auth.challenges.verify(result.challengeId, '123456');
  sessionId = verified.sessionId ?? undefined;
} else if (result.sessionId) {
  sessionId = result.sessionId;
}

if (sessionId) {
  localStorage.setItem('bold_session_id', sessionId);
}

if (sessionId) {
  // Register the session for push notifications.
  await auth.notifications.registerDevice(sessionId, {
    provider: 'expo', // 'expo' | 'fcm' | 'apns'
    token: expoPushToken
  });

  const verification = await auth.sessions.verify(sessionId);
  if (!verification.valid) {
    // session_not_found, session_revoked, or session_expired
  }
}

For refreshed JWTs, pass an override per call:

await auth.sessions.verify(sessionId, { upstreamJwt: freshJwt });

Viewer self-management methods require policy support:

const { data: sessions } = await auth.sessions.list();
await auth.sessions.revoke(sessionId);
await auth.sessions.revokeOthers(currentSessionId);

When Session Management is not available for the account, device registration returns a typed error:

import { SessionManagementUnavailableError } from '@boldvideo/bold-js';

try {
  await auth.notifications.registerDevice(sessionId, {
    provider: 'fcm',
    token: fcmToken
  });
} catch (error) {
  if (error instanceof SessionManagementUnavailableError) {
    console.log(error.status, error.code, error.retryable);
  }
}

Server-Side Session Management

Customer servers can inspect BOLD device state, revoke sessions, and manage per-viewer device-limit overrides or exemptions by upstream external ID using the normal BOLD tenant API key. Keep this API key server-side. Do not put a BOLD tenant key or admin key in browser code.

import { createClient } from '@boldvideo/bold-js';

const bold = createClient(process.env.BOLD_TENANT_API_KEY!);

const { data: state } =
  await bold.sessionManagement.getViewerSessionManagementStateByExternalId(
    outsetaPersonUid
  );

if (state.canUpdateDeviceLimitOverride) {
  await bold.sessionManagement.setViewerDeviceLimitOverrideByExternalId(
    outsetaPersonUid,
    5
  );

  await bold.sessionManagement.clearViewerDeviceLimitOverrideByExternalId(
    outsetaPersonUid
  );
}

if (state.canUpdateSessionManagementExemption) {
  await bold.sessionManagement.setViewerSessionManagementExemptionByExternalId(
    outsetaPersonUid,
    true
  );
}

const { data: sessions } =
  await bold.sessionManagement.listViewerSessionsByExternalId(outsetaPersonUid);

for (const session of sessions) {
  if (session.travelVerdict === 'impossible_travel') {
    // Bold flagged this sign-in as physically implausible.
  }
}

await bold.sessionManagement.revokeAllViewerSessionsByExternalId(
  outsetaPersonUid,
  { reason: 'outseta:membership_changed' }
);

setViewerDeviceLimitOverrideByExternalId requires a positive safe integer. Use clearViewerDeviceLimitOverrideByExternalId for explicit clearing.

Each session item carries travelVerdict: 'impossible_travel' | null — Bold's authoritative impossible-travel signal, exposed by these admin-scoped session-management methods. It is a label only: the SDK never returns coordinates, the raw IP, or derived distance/speed. It is null when the sign-in was not flagged (and for backends that predate the field).

The server-side session-management methods cannot update JWT config, account feature flags, session TTL, or email template setup. Those remain BOLD operator controls.


Community API

Build community features with posts, comments, and reactions. All write operations require a viewerId (the viewer performing the action).

Posts

// List posts (optionally filter by category)
const { data: posts } = await bold.community.posts.list({ 
  category: 'announcements',
  limit: 20,
  offset: 0,
  viewerId: 'viewer-uuid'  // Include viewerReacted in response
});

// Get a single post with comments
const { data: post } = await bold.community.posts.get('post-id', 'viewer-uuid');

// Create a post and notify viewers by their customer external IDs.
const { data: newPost, mentions } = await bold.community.posts.create('viewer-uuid', {
  content: 'Hello community! **Markdown** supported.',
  category: 'general',
  mentions: ['customer-user-123']
});
console.log(mentions?.skipped); // malformed or unresolved external IDs

// Update a post (owner or admin only)
await bold.community.posts.update('viewer-uuid', 'post-id', {
  content: 'Updated content'
});

// Delete a post (owner or admin only)
await bold.community.posts.delete('viewer-uuid', 'post-id');

// Pin or unpin a post (server only). Use a community moderation key: a
// single-purpose key that can pin and unpin posts and nothing else, so it is
// safe to keep in your server's environment. Ask Bold support to issue one.
// Check that the acting member may moderate first; authorship is not enough.
const admin = createClient(process.env.BOLD_API_KEY, {
  adminApiKey: process.env.BOLD_COMMUNITY_MODERATION_KEY,
});
const { data: pinned } = await admin.community.posts.setPinned('post-id', true);
console.log(pinned.pinned, pinned.pinnedAt); // re-pinning keeps the original pinnedAt
await admin.community.posts.setPinned('post-id', false);

// React to a post (toggle like/unlike)
const reaction = await bold.community.posts.react('viewer-uuid', 'post-id');
console.log(reaction.reacted, reaction.reactionsCount);

Comments

// Create a comment on a post
const { data: comment } = await bold.community.comments.create(
  'viewer-uuid',
  'post-id',
  { content: 'Great post!', mentions: ['customer-user-456'] }
);

// Reply to a comment (nested)
const { data: reply } = await bold.community.comments.create(
  'viewer-uuid',
  'post-id',
  { content: 'I agree!', parentId: 'parent-comment-id' }
);

// Delete a comment (owner or admin only)
await bold.community.comments.delete('viewer-uuid', 'comment-id');

// React to a comment (toggle)
const reaction = await bold.community.comments.react('viewer-uuid', 'comment-id');

// Read nested reaction state on comments/replies (viewerId enables viewerHasReacted)
const { data: post } = await bold.community.posts.get('post-id', 'viewer-uuid');
for (const thread of post.comments.items ?? []) {
  console.log(thread.createdAt, thread.reactions.count, thread.reactions.viewerHasReacted);
}

Mention Inbox

const { data: mentionItems, meta } = await bold.community.mentions.list(
  'viewer-uuid',
  { page: 1, pageSize: 20 }
);

const { data: unread } =
  await bold.community.mentions.unreadCount('viewer-uuid');

await bold.community.mentions.markRead('viewer-uuid', {
  ids: mentionItems.map((mention) => mention.id)
});

// Or mark every unread mention as read.
await bold.community.mentions.markRead('viewer-uuid', { all: true });

The optional mentions result is returned on post/comment creation only when the request included a mentions array. Bold processes at most 25 supplied external IDs; malformed, unresolved, or beyond-cap IDs appear in skipped, while duplicates and self-mentions are omitted silently. A skipped ID never fails content creation.

Mention excerpt values are plain, unsanitized content. Escape or sanitize them before rendering as HTML.


AI Methods

Chat, search, and recommendations support both streaming (default) and non-streaming modes.

Voice sessions

Talk to a single video without managing WebRTC, microphone tracks, or caption fragments. Voice requires the account's AI and voice features, a video transcript, and a browser with microphone permission in a secure context (HTTPS, or localhost for development). The factory is synchronous and safe to call during server rendering; only start() uses browser APIs. No framework or additional dependencies are required.

For an operator-only page that tests an exact published npm release, see the voice acceptance harness. Its documented synthetic checks do not establish real-provider or Safari/iPhone acceptance.

Use the account settings to decide whether to show voice controls. Missing or false values keep voice hidden; the broker still enforces access when a session starts.

const settings = await bold.settings();
const voiceEnabled = settings.data.account.voice?.enabled === true;
const session = bold.ai.voice.createSession({
  videoId: 'lesson-slug',
  viewer: 'customer-123', // Optional viewer UUID or external ID
  onStatus: (status) => { statusLabel.textContent = status; },
  onCaptions: (turns) => {
    // Each turn has a stable id, speaker ('user' or 'assistant'), text,
    // startedAt/updatedAt in milliseconds, and seekable timestamp segments.
    captions.textContent = turns.map(turn => `${turn.speaker}: ${turn.text}`).join('\n');
  },
  onEnded: (reason) => console.log('Voice ended:', reason),
  onError: (error) => { errorLabel.textContent = error.message; },
});

// Start directly from a user gesture. Handle rejection even when using onError.
startButton.onclick = () => { void session.start().catch(() => {}); };
muteButton.onclick = () => session.setMuted(!session.muted);
endButton.onclick = () => { void session.end(); };

// Optional animation-frame polling: normalized RMS amplitudes, not volume settings.
const { input, output } = session.getAudioLevels();

// In your component unmount / video replacement callback, call session.dispose().

createSession(options) returns a single-use VoiceSession. Create a new handle to reconnect. The broker request happens inside start(), after microphone permission and SDP negotiation; callers do not supply SDP. Optional viewerProfile accepts a string or an object of string, number, and boolean values, forwarded without changing its keys. Viewer profiles require the account's viewers feature.

  • Readonly state: status, sessionId (null until the broker responds), muted.
  • Statuses: idle → connecting → live → ending → ended; remote shutdown may go directly to ended.
  • start(): Promise<void> resolves when live, or when cancelled by end()/dispose(). Check session.status if subsequent code requires a live connection. Setup failures reject and call onError. Concurrent starts share one attempt.
  • setMuted(boolean) disables microphone tracks locally, including when set before start.
  • setPlaybackState({ playing, currentTime }) mutes microphone and assistant audio while a video plays, restores the viewer's microphone mute choice when paused, and shares the video position with the assistant. Call on play, pause, ended, and seeked events (not every timeupdate). currentTime must be finite, non-negative seconds; context is deduplicated at whole-second precision. Pre-start updates are applied to new media immediately; only the latest context is sent when live. Calls after ending are ignored. This method does not operate your video player.
  • end(): Promise<void> stops microphone/audio immediately and waits up to three seconds for close acknowledgement. It is idempotent; completion does not confirm final billing.
  • dispose() silently releases everything immediately, including during setup or end. The SDK also disposes on pagehide; it cannot detect framework unmounts for you.
  • End reasons: user, idle, time, server, connection, error.

Connection setup has a 60-second deadline after microphone permission; cancelling also settles an unresolved permission prompt and stops any microphone tracks granted later. There are no automatic retries of paid session creation. The SDK enforces the server's maximum duration and idle limit while the page runs. Audible input/output or transcript deltas and video playback count as activity; incoming silent audio frames do not. Pausing the video restarts the idle window; playback never extends the maximum duration. Browser timers can be throttled or suspended, so backend limits remain authoritative for billing.

Broker failures are VoiceAPIError instances with optional status, code, retryable, retryAfter (raw HTTP header), resetsOn, and originalError. This includes voice_allowance_exceeded (402), voice_session_limit (409), and rate_limited (429). Microphone, playback, and WebRTC failures are ordinary Error instances. Provider error events call onError but do not automatically end a live session.

Captions and seek buttons. Fragments from the same speaker within 1,500ms extend the same turn, including when speakers overlap. turn.segments contains { type: 'text', text } and { type: 'timestamp', text, seconds } entries. Render a timestamp as a button whose click sets your video player's currentTime to seconds. Both m:ss and h:mm:ss are supported, including 0:00. Caption text is untrusted; use text nodes or your framework's escaped text rendering, not innerHTML. The pure helpers appendVoiceCaption(turns, speaker, delta, now) and splitVoiceTimestamps(text) are also exported. The SDK does not control your video player or inject an app-specific greeting.

If the provider requests a delegation, the SDK responds with a fixed English transcript-only instruction: “Only this video transcript is available. Answer from it, or say the video does not cover it.” This protocol response is also sent in non-English sessions; VoiceSessionOptions does not expose language or instruction overrides.

React cleanup. Keep the handle in a ref, create it in an effect, and start it from a click handler. Create a fresh handle for each mount/video (including Strict Mode's effect replay); do not start microphone capture in an effect.

const voiceRef = useRef<VoiceSession | null>(null);

useEffect(() => {
  const session = bold.ai.voice.createSession({
    videoId,
    onStatus: setStatus,
    onCaptions: setCaptions, // useState<readonly VoiceCaptionTurn[]>([])
    onError: (error) => setError(error.message),
  });
  voiceRef.current = session;
  return () => {
    session.dispose();
    voiceRef.current = null;
  };
}, [bold, videoId]); // Keep the bold client stable across renders.

// In a button handler:
const startVoice = () => { void voiceRef.current?.start().catch(() => {}); };

Import VoiceSession and VoiceCaptionTurn as types from @boldvideo/bold-js, and React hooks from react. Reset your displayed status/captions on video changes as needed.

Chat

Library-wide conversational AI for deep Q&A across your entire video library.

// Streaming (default)
const stream = await bold.ai.chat({ prompt: 'How do I price my SaaS?' });

for await (const event of stream) {
  if (event.type === 'text_delta') process.stdout.write(event.delta);
  if (event.type === 'sources') console.log('Sources:', event.sources);
}

// Non-streaming
const response = await bold.ai.chat({ 
  prompt: 'What are the best closing techniques?',
  stream: false 
});
console.log(response.content);

Options:

ParameterTypeDescription
promptstringThe user's question (required)
streambooleantrue (default) for SSE, false for JSON
videoIdstringIf provided, scope to this video instead of whole library
currentTimenumberCurrent playback position (only with videoId)
conversationIdstringPass to continue existing conversation
collectionIdstringFilter to a specific collection
tagsstring[]Filter by tags

Recommendations

Get AI-powered video recommendations based on topics — ideal for personalized learning paths, exam prep, and content discovery.

// Streaming (default)
const stream = await bold.ai.recommendations({ 
  topics: ['contract law', 'ethics', 'client management'],
});

for await (const event of stream) {
  if (event.type === 'recommendations') {
    event.recommendations.forEach(rec => {
      console.log(`${rec.topic}:`);
      rec.videos.forEach(v => console.log(`  - ${v.title} (${v.relevance})`));
    });
  }
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta); // AI guidance
  }
}

// Non-streaming
const response = await bold.ai.recommendations({ 
  topics: ['sales', 'marketing'],
  stream: false 
});
console.log(response.guidance);
console.log(response.recommendations);

Options:

ParameterTypeDescription
topicsstring[]Topics to find content for (required)
streambooleantrue (default) for SSE, false for JSON
limitnumberMax videos per topic (default: 5, max: 20)
collectionIdstringFilter to a specific collection
tagsstring[]Filter by tags
includeGuidancebooleanInclude AI learning path narrative (default: true)
contextAIContextMessage[]Previous conversation turns for follow-ups

Fast semantic search with a brief AI-generated summary.

const stream = await bold.ai.search({ 
  prompt: 'pricing strategies',
  limit: 10 
});

for await (const event of stream) {
  if (event.type === 'sources') {
    console.log(`Found ${event.sources.length} results`);
  }
}

AI interaction tracking

chat (library-wide or video-scoped), ask, coach, and search accept an optional requestId UUID, sent as request_id. Generate it once per intentional user action and reuse it when retrying that action. A new question, follow-up, or intentional repeated search gets a new UUID. The SDK does not generate IDs or retry requests automatically.

search also accepts searchMode: 'preview' | 'settled', sent as search_mode. Omitting it preserves the server's settled default. Use preview for interim searches and settled for the final submitted search; treat that final submission as a new action with its own UUID.

const action = {
  prompt: 'pricing strategies',
  requestId: crypto.randomUUID(),
  searchMode: 'settled' as const,
  stream: false as const,
};
const response = await bold.ai.search(action);
// If retrying this action, pass the same action.requestId.
console.log(response.interactionId);

JSON AIResponse and SSE message_complete events expose interactionId?: string | null (camelized from interaction_id). Search previews may return null; older servers may omit it. These IDs support analytics deduplication only, not response replay or model-call deduplication. Persistence is best-effort and asynchronous: receiving an ID is not a durable acknowledgement.

This does not change videos.search(), which calls /videos?query=…; the SDK does not wrap the separate keyword /search endpoint.

Chat, video chat, ask/coach, and AI search also accept optional attribution: channel: 'portal' | 'embed' | 'api' | 'mcp' | 'unknown', clientName: string, and clientVersion: string. These are sent as top-level channel, client_name, and client_version. Omitted fields stay omitted; the backend normalizes missing or malformed channel values to unknown. Attribution does not affect requestId deduplication. Set channel: 'portal' explicitly in portal server proxies; these labels are analytics metadata, not authorization signals.

Video-Scoped Chat

Chat about a specific video by passing videoId. Uses only that video's transcript as context.

const stream = await bold.ai.chat({ 
  videoId: 'video-id',
  prompt: 'What is discussed at the 5 minute mark?' 
});

for await (const event of stream) {
  if (event.type === 'text_delta') process.stdout.write(event.delta);
}

// With playback context (coming soon)
const stream = await bold.ai.chat({ 
  videoId: 'video-id',
  prompt: 'What does she mean by that?',
  currentTime: 847  // seconds
});

Get Conversation History

Retrieve a conversation by ID to display message history:

const conversation = await bold.ai.getConversation('550e8400-e29b-41d4-a716-446655440000');

console.log(`Created: ${conversation.createdAt}`);
for (const msg of conversation.messages) {
  console.log(`${msg.role}: ${msg.content}`);
}

Rate an AI Answer

Use the optional messageId from a non-streaming AIResponse or a streaming message_complete event to rate an assistant answer:

const answer = await bold.ai.chat({ prompt: 'How do I get started?', stream: false });
if (answer.messageId) {
  const feedback = await bold.ai.rateMessage(answer.messageId, {
    rating: 'down',
    reasons: ['too_short'],
    note: 'Please include an example.',
  });
  await bold.ai.clearRating(feedback.messageId);
}

rating is 'up' or 'down'. Optional reasons accepts 'wrong', 'not_in_video', 'too_short', 'off_topic', and 'other'; note is optional. rateMessage returns MessageFeedback with messageId, rating, reasons, and a nullable note. clearRating resolves without a value. Both throw AIAPIError with status 404 when the message is not on the caller's account.

Multi-turn Conversations

Use the context parameter for follow-up questions:

const first = await bold.ai.search({ 
  prompt: 'How do indie designers find clients?',
  stream: false 
});

const followUp = await bold.ai.search({
  prompt: 'What about cold outreach specifically?',
  context: first.context,
  stream: false
});

Conversation Starters

Fetch conversation starter prompts, resolved per collection when available and falling back to the account-level starters otherwise:

// Account-level starters (no collectionIds)
const { data: starters } = await bold.conversationStarters.list();

// Resolve starters for one or more collections
const { data: starters } = await bold.conversationStarters.list({
  collectionIds: ['collection-uuid-1', 'collection-uuid-2']
});

starters.forEach((starter) => {
  console.log(starter.text, starter.source, starter.collectionId);
});

Analytics

Explicit AI engagement (browser or server)

bold.trackEngagement(options: TrackEngagementOptions): Promise<void> sends POST /api/v1/event independently of legacy trackEvent and its throttle. Import TrackEngagementOptions from @boldvideo/bold-js.

SDK fieldWire fieldMeaning
eventnsource_open or video_progress
interactionIdinteraction_idRequired interaction UUID from an AI response
openIdplayback_idRequired new UUID per deliberate source open; not a Mux playbackId
videoIdvidRequired source video ID
viewer?viewerExisting viewer UUID or external ID
watchedSecondswatched_secondsRequired for video_progress, absent for source_open; cumulative elapsed playing wall-clock seconds for this open
// On a deliberate source open, skip if the response has no interaction ID.
if (response.interactionId) {
  const open = {
    interactionId: response.interactionId,
    openId: crypto.randomUUID(),
    videoId: source.videoId,
  };
  void bold.trackEngagement({ ...open, event: 'source_open' });

  // Later, after the caller has measured 12.5 seconds spent playing:
  void bold.trackEngagement({ ...open, event: 'video_progress', watchedSeconds: 12.5 });
}

source_open only establishes the open; progress requires that open. Maintain the counter in the caller, excluding paused/buffering time. Do not send absolute playhead positions, seek deltas, or per-sample increments. Retries/reordered progress use the same openId; the backend keeps the cumulative maximum. Generate a fresh openId when the user deliberately opens a source again.

Calls are best-effort and safe to fire and forget: transport errors are caught, and no automatic retry or shared throttle is applied. A resolved promise (including HTTP 202) is not a durable acknowledgement. The backend validates tenant, viewer, and source attribution; it retries capture/open races for at most 30 seconds and does not create placeholder interactions. Server proxies must derive API keys and viewer references from trusted tenant/session context, not browser claims.

Legacy playback and page views

Track video events and page views for analytics.

// Track video events (play, pause, complete, etc.)
bold.trackEvent({
  type: 'play',
  videoId: 'video-id',
  timestamp: 0
});

// Track page views
bold.trackPageView({
  path: '/videos/my-video',
  referrer: document.referrer
});

TypeScript

All types are exported for full TypeScript support:

import type { 
  Video, 
  Playlist, 
  Settings,
  Portal,
  PortalHero,
  MenuItem,
  AIEvent,
  AIResponse,
  ChatOptions,
  SearchOptions,
  RecommendationsOptions,
  RecommendationsResponse,
  Recommendation,
  Conversation,
  ConversationMessage,
  Source,
  Viewer,
  ViewerProgress,
  ViewerLookupParams,
  ListProgressOptions,
  ListVideosOptions,
  ListVideosLatestOptions,
  ListVideosIndexOptions,
  NotificationChannels,
  NotificationPreferencesResponse,
  NotificationChannelUpdate,
  UpdateNotificationPreferencesData,
  NotificationProvider,
  RegisterDeviceData,
  RegisterDeviceResponse,
  // Community API
  Post,
  PostAuthor,
  Comment,
  ReactionResponse,
  Mention,
  MentionSkippedReport,
  CommunityPostCreateResponse,
  CommunityCommentCreateResponse,
  ListPostsOptions,
  ListMentionsOptions,
  MentionsUnreadCountResponse,
  MarkMentionsReadData,
  MarkMentionsReadResponse,
  CreatePostData,
  UpdatePostData,
  CreateCommentData,
  // Conversation Starters
  ConversationStarter,
  ConversationStarterSource,
  ListConversationStartersOptions
} from '@boldvideo/bold-js';

Migration from v1.7.x

Breaking: All response types now use camelCase

All API responses (videos, playlists, settings, AI) are now transformed to use idiomatic TypeScript/JavaScript naming:

// Before (v1.7.x and earlier)
video.playback_id
video.published_at
video.stream_url
video.meta_data
settings.featured_playlists
settings.menu_items
settings.theme_config
playlist.is_private

// After (v1.8.0)
video.playbackId
video.publishedAt
video.streamUrl
video.metaData
settings.featuredPlaylists
settings.menuItems
settings.themeConfig
playlist.isPrivate

Method Changes

OldNewNotes
bold.ai.ask(opts)bold.ai.chat(opts)ask() still works but is deprecated
bold.ai.coach(opts)bold.ai.chat(opts)coach() still works but is deprecated
bold.ai.chat(videoId, opts)bold.ai.chat({ videoId, ...opts })Pass videoId in options
bold.ai.recommend(opts)bold.ai.recommendations(opts)recommend() still works but is deprecated

Type Renames

Old TypeNew Type
AskOptionsChatOptions
RecommendOptionsRecommendationsOptions
RecommendResponseRecommendationsResponse

The old types are still exported as aliases for backward compatibility.


Contributing

See CONTRIBUTING.md for details on how to contribute to this project.

Security

See SECURITY.md for security policies and reporting vulnerabilities.

License

MIT

版本与发布记录

@boldvideo/bold-js 共发布 73 个版本,首次发布于 2023/03/05;过去 12 个月发布了 52 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.5.0latest2026/10/2415 天前
2.4.12026/10/2118 天前
2.4.02026/10/2118 天前
2.3.02026/10/2118 天前
2.2.02026/10/1028 天前
2.1.02026/10/041 个月前

关于 @boldvideo/bold-js 的常见问题

@boldvideo/bold-js 是什么?

@boldvid

如何安装 @boldvideo/bold-js?

在项目目录运行 npm install @boldvideo/bold-js,或使用 pnpm add / yarn add / bun add。

@boldvideo/bold-js 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@boldvideo/bold-js 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@boldvideo/bold-js 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@boldvideo/bold-js 可以商用吗?

@boldvideo/bold-js 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@boldvideo/bold-js 的下载量有多少?

根据 npm 官方下载统计,@boldvideo/bold-js 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,663 次,日均约 89 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@boldvideo/bold-js 还在维护吗?

最新版本 2.5.0 发布于 2026/10/24(15 天前);至今共发布 73 个版本,其中过去 12 个月发布了 52 个;首次发布于 2023/03/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。