@touchcastllc/napster-companion-api-dev 1.6.0-alpha.9 → 2.0.0-alpha.1

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.
@@ -10,6 +10,14 @@ export interface MicGateController {
10
10
  export interface MicGateOptions {
11
11
  /** Element the recovery card mounts into on denial. */
12
12
  root: HTMLElement;
13
+ /**
14
+ * True when the SDK's own click-to-start button is in play. Gates the
15
+ * 30s dismiss-timer below: with no button for an ignored prompt to fall
16
+ * back to, there's nothing useful for the attempt to collapse into, so
17
+ * this waits on the browser's own prompt indefinitely instead of forcing
18
+ * "entry" and firing onDismissed.
19
+ */
20
+ buttonMode: boolean;
13
21
  /**
14
22
  * The microphone is available. Fires once for an immediate grant, and
15
23
  * again after each successful "Check again" retry. May return a promise —
@@ -19,6 +27,20 @@ export interface MicGateOptions {
19
27
  onGranted: () => void | Promise<void>;
20
28
  /** Reported once, on the FIRST failure only — never on a refused retry. */
21
29
  onError?: (error: Error) => void;
30
+ /**
31
+ * Fires when a native permission prompt is ignored for 30s and the attempt
32
+ * collapses back to the entry point — first attempt or retry. Only possible
33
+ * when buttonMode is true (see its own comment); without a button there is
34
+ * no 30s timer and this never fires. Distinct from onError: a dismiss is
35
+ * not a failure, it is a clean, silent return to idle, so nothing on this
36
+ * channel may reach the host's own onError.
37
+ *
38
+ * Exists because gateOnMicrophone()'s own promise resolves identically for
39
+ * every outcome (it returns a bare { dismiss } handle), so a caller awaiting
40
+ * it cannot otherwise tell a dismissed attempt from a live one — button mode
41
+ * latched a fake "connected" instance on exactly that ambiguity.
42
+ */
43
+ onDismissed?: () => void;
22
44
  /** Fires immediately before the recovery card mounts. */
23
45
  onBeforeRecovery?: () => void;
24
46
  /**
@@ -28,6 +50,13 @@ export interface MicGateOptions {
28
50
  * detached root, and must not drive the session that replaced it.
29
51
  */
30
52
  isStale?: () => boolean;
53
+ /**
54
+ * Which avatar shape to render the Permission States recovery card as —
55
+ * threaded straight through to createMicPermission(). See
56
+ * MicPermissionOptions.view's own comment for why this can't be inferred
57
+ * from CSS cascade.
58
+ */
59
+ view: string | undefined;
31
60
  }
32
61
  /**
33
62
  * Gate a session on microphone access (CA-622): try once, and on denial keep
@@ -3,4 +3,6 @@ export type { RootState } from "./store";
3
3
  export { setOnFeaturesUpdateCallback } from "./middleware/featuresUpdateMiddleware";
4
4
  export * from "./selectors";
5
5
  export { setSessionId, setTransport, setStopInteraction, setMuted, setAutoMicActive, setVolume, setAvatarSpeaking, setScreenSharing, setUserTalking, setConnectionToken, setCloseConnectionHandler, setSessionTerminatedByServer, resetAvatar, } from "./slices/avatarSlice";
6
+ export { setSessionStatus, resetSession } from "./slices/sessionSlice";
7
+ export type { SessionStatus } from "./slices/sessionSlice";
6
8
  export { setFeatures, resetFeatures, setDebugMode } from "./slices/appSlice";
@@ -1,6 +1,7 @@
1
1
  import { RootState } from "./index";
2
2
  import { FeatureConfig } from "../types";
3
3
  import type { Transport } from "../services/transport";
4
+ import type { SessionStatus } from "./slices/sessionSlice";
4
5
  /**
5
6
  * Avatar state selectors
6
7
  */
@@ -14,6 +15,10 @@ export declare const selectConnectionToken: (state: RootState) => string | undef
14
15
  export declare const selectStopInteraction: (state: RootState) => boolean;
15
16
  export declare const selectCloseConnectionHandler: (state: RootState) => (() => void) | undefined;
16
17
  export declare const selectSessionTerminatedByServer: (state: RootState) => boolean;
18
+ /**
19
+ * Session-lifecycle selectors (CA-834)
20
+ */
21
+ export declare const selectSessionStatus: (state: RootState) => SessionStatus;
17
22
  /**
18
23
  * App state selectors
19
24
  */
@@ -0,0 +1,7 @@
1
+ import type { SessionStatus } from "../../types";
2
+ export type { SessionStatus };
3
+ export interface SessionSliceState {
4
+ status: SessionStatus;
5
+ }
6
+ export declare const setSessionStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<SessionStatus, "session/setSessionStatus">, resetSession: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"session/resetSession">;
7
+ export declare const sessionReducer: import("redux").Reducer<SessionSliceState>;
@@ -1,10 +1,12 @@
1
1
  export declare const store: import("@reduxjs/toolkit").EnhancedStore<{
2
2
  avatar: import("./slices/avatarSlice").AvatarState;
3
3
  app: import("./slices/appSlice").AppState;
4
+ session: import("./slices/sessionSlice").SessionSliceState;
4
5
  }, import("redux").UnknownAction, import("@reduxjs/toolkit").Tuple<[import("redux").StoreEnhancer<{
5
6
  dispatch: import("redux-thunk").ThunkDispatch<{
6
7
  avatar: import("./slices/avatarSlice").AvatarState;
7
8
  app: import("./slices/appSlice").AppState;
9
+ session: import("./slices/sessionSlice").SessionSliceState;
8
10
  }, undefined, import("redux").UnknownAction>;
9
11
  }>, import("redux").StoreEnhancer]>>;
10
12
  export type RootState = ReturnType<typeof store.getState>;
@@ -117,6 +117,16 @@ export type FunctionCallOutputCommand = {
117
117
  export type DataChannelCommand = SendMessageCommand | CancelCommand | SetSettingsCommand | StartVideoCommand | StopVideoCommand | FunctionCallOutputCommand;
118
118
  export declare const maxInactiveTimeoutDuration = 180000;
119
119
  export declare const defaultCountdownDuration = 30;
120
+ /**
121
+ * The session-lifecycle state (CA-834). `muted` is a separate, orthogonal
122
+ * flag (read via `getMutedState()`/`onAvatarReady`, not part of this union) —
123
+ * it composes with `readyAudio`/`readyVideo` rather than adding its own
124
+ * members here. `permissionDismissed` fires when the user never answers the
125
+ * browser's native microphone prompt within 30s (micGate.ts's acquire()) —
126
+ * immediately followed by `entry`, collapsing the UI back to the Entry
127
+ * Point bubble.
128
+ */
129
+ export type SessionStatus = "entry" | "loading" | "permissionPending" | "permissionDismissed" | "permissionBlocked" | "readyAudio" | "readyVideo";
120
130
  export interface EventMessage {
121
131
  event: string;
122
132
  data?: {
@@ -126,6 +136,16 @@ export interface EventMessage {
126
136
  content?: string;
127
137
  role?: string;
128
138
  };
139
+ /**
140
+ * Present on `avatar_state_changed` events for a deferred-video session
141
+ * (CA-593/CA-834): the server sends `ready` twice, `details.mode`
142
+ * distinguishing the first (audio-only) from the second (audio+video).
143
+ */
144
+ details?: {
145
+ mode?: string;
146
+ stage?: string;
147
+ video_type?: string;
148
+ };
129
149
  [key: string]: unknown;
130
150
  };
131
151
  }
@@ -136,7 +156,20 @@ export interface EventMessage {
136
156
  * (for example `inactiveTimeout.duration`).
137
157
  */
138
158
  export interface FeatureConfig {
139
- /** Background removal feature. When enabled, the avatar background should be removed. */
159
+ /**
160
+ * Background removal (green-screen cutout) feature.
161
+ *
162
+ * @deprecated `enabled` is no longer settable — it's driven entirely by
163
+ * `style.view` (CA-834, manager feedback): `"silhouette"` turns it on,
164
+ * any other view turns it off, unconditionally. `enableFeature`/
165
+ * `disableFeature`/`updateFeatureConfig` accept calls for this feature
166
+ * without erroring, but silently resync `enabled` back to whatever the
167
+ * current view dictates rather than applying the requested value. Kept
168
+ * on `FeatureConfig` only so existing config objects that set it don't
169
+ * become a type error; the value itself is ignored on the way in and
170
+ * only meaningful as a read of the current derived state (e.g. via
171
+ * `onFeaturesUpdate`).
172
+ */
140
173
  backgroundRemoval?: {
141
174
  enabled: boolean;
142
175
  };
@@ -165,18 +198,6 @@ export interface FeatureConfig {
165
198
  text?: string;
166
199
  color?: string;
167
200
  };
168
- /** Loader overlay shown while the SDK/Avatar is loading. */
169
- showSDKLoader?: {
170
- enabled: boolean;
171
- /** Optional background color for the loader overlay. */
172
- bgColor?: string;
173
- /** Optional color for the loader animation. */
174
- color?: string;
175
- /** Optional loader animation type. */
176
- type?: "spinner" | "pulse";
177
- /** Optional CSS class name(s) to add to the loader spinner element. */
178
- className?: string;
179
- };
180
201
  /** Document Picture-in-Picture. When enabled, the avatar automatically
181
202
  * opens in a PiP window when the user switches browser tabs (Chrome/Edge 116+). */
182
203
  pictureInPicture?: {
@@ -206,6 +227,18 @@ export interface FeatureConfig {
206
227
  /** Mouth opening ratio threshold. Default 0.015. */
207
228
  talkingThreshold?: number;
208
229
  };
230
+ /** The "big card" placeholder (blurred companion photo + spinner) shown
231
+ * behind the entry/loading/permission-pending/permission-blocked states,
232
+ * and the zoom-in grow animation played when the session becomes ready.
233
+ * Disable when the embedding app renders its own start/loading screen
234
+ * and would otherwise have to fight this with CSS. Defaults to
235
+ * `enabled: true`. The MicPermission recovery card itself (the actual
236
+ * "Allow microphone access" / "Check again" UI) and the ready video/
237
+ * canvas are unaffected — this only controls the decorative chrome
238
+ * behind/around them. */
239
+ loadingIndicator?: {
240
+ enabled: boolean;
241
+ };
209
242
  }
210
243
  /**
211
244
  * Configuration for the face tracking service.
@@ -271,25 +304,60 @@ export interface FaceTrackingController {
271
304
  /** Full teardown: stop + dispose model + null all refs. */
272
305
  destroy(): void;
273
306
  }
274
- export interface avatarStyleConfig {
307
+ export interface StyleConfig {
275
308
  /** Avatar visual style: "round" | "rectangle" | "silhouette" (Default: "round").
276
309
  *
277
310
  * Use "rectangle" when applying custom styling that requires a rectangular container.
278
311
  */
279
312
  view: "round" | "rectangle" | "silhouette";
280
- /** Border width in pixels. */
281
- borderWidth?: CSSStyleDeclaration["borderWidth"];
282
- /** Border color as a CSS color string. */
283
- borderColor?: CSSStyleDeclaration["borderColor"];
284
- /** Border style as a CSS border-style string (e.g., "solid", "dashed"). */
285
- borderStyle?: CSSStyleDeclaration["borderStyle"];
313
+ /**
314
+ * Brand accent color, any CSS color string. Default: `"#be369d"`. Replaces
315
+ * that default everywhere it's currently used as the SDK's one accent hue:
316
+ * the control bar's live-mic button fill, the mic-permission recovery
317
+ * card's buttons/links (`--np-mic-permission-accent`), and the loading
318
+ * photo bubble's own ring. Does NOT affect the muted-mic button (a fixed
319
+ * warning color, not brand-tied) or `persistence.navigationProgress`'s
320
+ * own color (set separately — see {@link NavigationProgressOptions}).
321
+ */
322
+ accentColor?: string;
323
+ /** CSS class name(s) added to the SDK root container. Useful for theming. */
324
+ className?: string;
325
+ }
326
+ export interface LayoutConfig {
327
+ /**
328
+ * Layout mode.
329
+ *
330
+ * - `"fixed"` (default): the avatar floats over the page as a corner widget,
331
+ * anchored to the viewport via `position`. Always mounts to
332
+ * `document.body`, ignoring `mountContainer` if set.
333
+ * - `"inline"`: the avatar renders inside `mountContainer` and fills it. Size
334
+ * and shape come from how you style that container; `position` is ignored.
335
+ * When the container has no explicit height, an internal 4:5 ratio is used
336
+ * so the avatar is never invisible.
337
+ */
338
+ type?: "fixed" | "inline";
339
+ /** Position of the avatar on screen. Meaningful only when `type` is
340
+ * `"fixed"` (the default). See `Position` enum. */
341
+ position?: Position;
342
+ /**
343
+ * Mount target for the SDK. Provide an actual HTMLElement or a selector
344
+ * string; defaults to `document.body` when omitted. Meaningful only under
345
+ * `type: "inline"` — `type: "fixed"` always drops this and mounts to
346
+ * `document.body` regardless of what's passed here, since a floating,
347
+ * viewport-anchored widget has no host container to size or position
348
+ * against.
349
+ */
350
+ mountContainer?: HTMLElement | string | null;
351
+ }
352
+ export interface AgentConfig {
353
+ /**
354
+ * The agent's photo. Single source of truth for every avatar photo shown
355
+ * in the widget — the Entry Point / button card, the loading placeholder,
356
+ * and the mic-permission recovery card. Omitted → each of those falls back
357
+ * to its own bundled placeholder.
358
+ */
359
+ photoUrl?: string;
286
360
  }
287
- /**
288
- * CSS style object that works with vanilla JS.
289
- * Compatible with CSSStyleDeclaration.
290
- * Allows both string and number values for CSS properties.
291
- */
292
- export type StyleObject = Partial<CSSStyleDeclaration>;
293
361
  /**
294
362
  * Styling for the navigation progress bar — see
295
363
  * {@link PersistenceOptions.navigationProgress}. Both map onto CSS custom properties,
@@ -408,7 +476,7 @@ export interface ButtonConfig {
408
476
  enabled?: boolean;
409
477
  /**
410
478
  * Host-owned trigger — an element, or a selector resolved once at `init()`.
411
- * When set the SDK renders no card of its own and `label`, `avatarUrl` and
479
+ * When set the SDK renders no card of its own and `label` and
412
480
  * `description` are ignored. The SDK never moves or reparents the node; it
413
481
  * sets `data-np-state`, toggles `disabled`, and hides it while a session is
414
482
  * live, restoring it on end.
@@ -416,10 +484,17 @@ export interface ButtonConfig {
416
484
  element?: HTMLElement | string;
417
485
  /** Card text. Default: `"Talk to an agent"`. Ignored when `element` is set. */
418
486
  label?: string;
419
- /** Optional companion picture on the card. Ignored when `element` is set. */
420
- avatarUrl?: string;
421
487
  /** Supporting line under the label (Figma entry-point card). Ignored when `element` is set. */
422
488
  description?: string;
489
+ /**
490
+ * Which edge of the host container the SDK-rendered trigger card docks
491
+ * to, for `layout.type: "inline"`. Default: `"right"`. Ignored when `element`
492
+ * is set (a host-owned trigger keeps whatever position the host already
493
+ * gave it) and has no effect in fixed layout either — there, idle's own
494
+ * shrink-to-fit sizing (Button.css) already shrinks the root to exactly
495
+ * the card's own size, leaving no room inside it to dock toward an edge.
496
+ */
497
+ alignment?: "left" | "right";
423
498
  }
424
499
  /**
425
500
  * Returned by `init()` in button mode. Nothing is connected when you receive it:
@@ -469,44 +544,16 @@ export interface NapsterCompanionApiConfig {
469
544
  * session presents is a separate, runtime-changeable axis.
470
545
  */
471
546
  modality?: "video";
472
- /** Position of the avatar on screen. See `Position` enum. */
473
- position?: Position;
474
- /** CSS class name(s) to add to the SDK root container. Useful for theming. */
475
- className?: string;
476
- /** Inline style object applied to the SDK root container. It supports only vanilla JS style objects. */
477
- style?: StyleObject;
478
- /** Avatar visual style configuration. */
479
- avatarStyle?: avatarStyleConfig;
547
+ /** Placement: layout mode, screen position, and (for inline layout) the
548
+ * mount target. See {@link LayoutConfig}. */
549
+ layout?: LayoutConfig;
550
+ /** Visual style: avatar shape, brand accent color, and a CSS class for the
551
+ * root container. See {@link StyleConfig}. */
552
+ style?: StyleConfig;
553
+ /** The agent's identity — currently just its photo. See {@link AgentConfig}. */
554
+ agent?: AgentConfig;
480
555
  /** Per-feature configuration object to toggle features and set options. */
481
556
  features?: FeatureConfig;
482
- /**
483
- * Mount target for the SDK. Provide an actual HTMLElement or a selector
484
- * string; defaults to `document.body` when omitted.
485
- */
486
- mountContainer?: HTMLElement | string | null;
487
- /**
488
- * Layout mode.
489
- *
490
- * - `"fixed"` (default): the avatar floats over the page as a corner widget,
491
- * anchored to the viewport via `position`. This is the original behavior.
492
- * - `"inline"`: the avatar renders inside `mountContainer` and fills it. Size
493
- * and shape come from how you style that container; `position` is ignored.
494
- * When the container has no explicit height, an internal 4:5 ratio is used
495
- * so the avatar is never invisible.
496
- */
497
- layout?: "fixed" | "inline";
498
- /**
499
- * AI companion function definitions for function calling support.
500
- *
501
- * Functions are configured server-side and baked into the connection at
502
- * session-creation time (the connection API / token). To register tools
503
- * mid-session from the page, use the WebMCP bridge, which pushes them as
504
- * `set_settings.inline_functions` after the session reaches "ready".
505
- *
506
- * @deprecated The SDK no longer sends these over the data channel — supplying
507
- * them here has no effect. Configure functions via the connection API instead.
508
- */
509
- functions?: CompanionFunction[];
510
557
  /** Enable debug logging throughout the SDK. When enabled, detailed logs will be output to the console. */
511
558
  debug?: boolean;
512
559
  /** @internal */
@@ -526,7 +573,7 @@ export interface NapsterCompanionApiConfig {
526
573
  * Keep the session alive across page navigation. When enabled, the page is wrapped in
527
574
  * a same-origin iframe so navigation happens inside it while the avatar stays in the
528
575
  * top document, which never reloads.
529
- * When on, `mountContainer` is ignored — the avatar must live in the top document.
576
+ * When on, `layout.mountContainer` is ignored — the avatar must live in the top document.
530
577
  */
531
578
  persistence?: PersistenceOptions;
532
579
  /** Click-to-start configuration. Omit for the default connect-immediately behaviour. */
@@ -542,6 +589,13 @@ export interface NapsterCompanionApiConfig {
542
589
  onAvatarReady?: (isReady?: boolean) => void;
543
590
  /** Called when the avatar inactivity status changes; receives the new status. */
544
591
  onInactivityStatusChange?: (isInactive: boolean) => void;
592
+ /**
593
+ * Called whenever the internal session-lifecycle status changes (CA-834);
594
+ * receives the new status. Intended for hosts that want to drive their own
595
+ * UI off the exact same state the SDK tracks internally, rather than
596
+ * inferring it from `onAvatarReady`/`onError` alone.
597
+ */
598
+ onSessionStatusChange?: (status: SessionStatus) => void;
545
599
  /** Called when the SDK is destroyed via the public API. */
546
600
  onDestroy?: () => void;
547
601
  /** Called whenever feature configuration is changed at runtime. */
@@ -571,14 +625,12 @@ export interface NapsterCompanionApiInstance {
571
625
  * and reset to `undefined` once the connection is closed.
572
626
  */
573
627
  readonly sessionId: string | undefined;
574
- /** Update the inline style object applied to the SDK root container. */
575
- updateStyles: (styles: NapsterCompanionApiConfig["style"]) => void;
576
628
  /** Set the position of the SDK on screen (see `Position`). */
577
629
  setPosition: (position: Position) => void;
578
630
  /** Clear any programmatic position and revert to the configured/default position. */
579
631
  clearPosition: () => void;
580
- /** Update avatar style configuration (e.g., view: round/silhouette/rectangle). */
581
- updateAvatarStyle: (newAvatarStyle: Partial<NonNullable<NapsterCompanionApiConfig["avatarStyle"]>>) => void;
632
+ /** Update style configuration (e.g., view: round/silhouette/rectangle). */
633
+ updateStyle: (newStyle: Partial<NonNullable<NapsterCompanionApiConfig["style"]>>) => void;
582
634
  /** Enable a named feature (one of the keys from `FeatureConfig`).
583
635
  *
584
636
  * e.g. `enableFeature("disclaimer")`
@@ -661,7 +713,14 @@ export interface NapsterCompanionApiSDK {
661
713
  };
662
714
  }): Promise<NapsterCompanionApiController>;
663
715
  /**
664
- * Initialize the SDK and connect immediately: resolves once the session is up.
716
+ * Initialize the SDK and connect immediately.
717
+ *
718
+ * The returned promise resolves as soon as the widget has mounted and
719
+ * rendered — before the microphone prompt, before `getToken()`, before any
720
+ * connection exists. Sending a command (`sendCommand`, `enableFeature`,
721
+ * etc.) right after `await init()` can silently drop it if the session
722
+ * isn't actually connected yet. Use {@link NapsterCompanionApiConfig.onAvatarReady}
723
+ * (or `onSessionStatusChange`) to know when the session itself is up.
665
724
  *
666
725
  * `getToken` is called by the SDK itself once the microphone permission has
667
726
  * settled, so the connection token is always fresh when signaling starts. A
@@ -92,6 +92,11 @@ declare class DebugLogger {
92
92
  * Log cross-page persistence information (iframe wrap/unwrap, break-out, history sync).
93
93
  */
94
94
  persistence(...args: unknown[]): void;
95
+ /**
96
+ * Log SessionStatus transitions (CA-834): entry/loading/permissionPending/
97
+ * permissionDismissed/permissionBlocked/readyAudio/readyVideo.
98
+ */
99
+ session(...args: unknown[]): void;
95
100
  }
96
101
  /**
97
102
  * Global debug logger instance
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The wrap div's own literal class, shared by every `wrapWithGlassFrame`
3
+ * consumer — exported so callers that need to rebuild a wrap's className
4
+ * wholesale (e.g. Avatar/index.ts's `computeControlContainerClass()`, which
5
+ * recomputes `controlWrapEl`'s full class list on every show/hide toggle
6
+ * rather than merging via classList) read it from one place instead of
7
+ * repeating the string literal, which could otherwise drift from this file's
8
+ * own copy.
9
+ */
10
+ export declare const GLASS_WRAP_CLASS = "np_companion-glass-wrap";
11
+ export interface WrapWithGlassFrameOptions {
12
+ /**
13
+ * Extra class(es) for the wrap div — where a caller's own page-position
14
+ * CSS lives, if it needs any (e.g. the control-bar pill's own
15
+ * bottom/left/transform/hover-collapse). Page position was already a
16
+ * separate, per-block concern before this helper existed and stays one.
17
+ */
18
+ wrapClassName?: string;
19
+ }
20
+ /**
21
+ * Wraps `target` (already fully built and styled by its own content
22
+ * builder) in the SDK's one shared wrap div (class `np_companion-glass-wrap`
23
+ * plus any `wrapClassName`) and returns it.
24
+ *
25
+ * Used to carry the translucent glass-frame ring every card in this SDK
26
+ * once rendered (avatar shape, control-bar pill, Entry Point card,
27
+ * MicPermission card) — removed per manager feedback ("треба прибрати всі
28
+ * скляні бордери, не тільки навколо шейпів"), so this now just wraps;
29
+ * kept as the one place every caller's page-position class lands, since
30
+ * that part of the contract is unrelated to the frame that used to live
31
+ * here.
32
+ */
33
+ export declare function wrapWithGlassFrame(target: HTMLElement, options?: WrapWithGlassFrameOptions): HTMLDivElement;
@@ -1,6 +1,7 @@
1
1
  import { FeatureConfig } from "../types";
2
2
  export * from "./classnames";
3
3
  export * from "./domFactory";
4
+ export * from "./glassFrame";
4
5
  export * from "./greenscreen";
5
6
  export * from "./svg";
6
7
  export * from "./sendCommand";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@touchcastllc/napster-companion-api-dev",
3
- "version": "1.6.0-alpha.9",
3
+ "version": "2.0.0-alpha.1",
4
4
  "keywords": [
5
5
  "napster",
6
6
  "companion-api",