@touchcastllc/napster-companion-api-dev 1.0.0-alpha.74 → 1.0.0-alpha.77

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.
@@ -20,6 +20,14 @@ export interface EdgeMcpBridgeOptions {
20
20
  * false to manage the agent's function list yourself. Default: `true`.
21
21
  */
22
22
  registerInlineFunctions?: boolean;
23
+ /**
24
+ * Read tools from a same-origin child iframe's `document.modelContext` instead of
25
+ * this window's. Used by the persistent-agent shell: the avatar + bridge live in the
26
+ * top-level document while the SITE (and its tools) live in the wrapped iframe. The
27
+ * bridge re-discovers on each iframe `load` (navigation) and routes tool calls into
28
+ * it (`executeTool` runs in the iframe's realm, so the site operates itself).
29
+ */
30
+ iframe?: HTMLIFrameElement;
23
31
  }
24
32
  export declare class EdgeMcpBridge {
25
33
  private mc;
@@ -27,12 +35,25 @@ export declare class EdgeMcpBridge {
27
35
  private onToolChange;
28
36
  private onResourceUpdated;
29
37
  private onResourceListChanged;
38
+ private onIframeLoad;
30
39
  private readonly seededUris;
31
40
  private lastInlineSig;
32
41
  private sessionReady;
33
42
  private disposed;
34
43
  private readonly options;
35
44
  constructor(options: EdgeMcpBridgeOptions);
45
+ /** The document whose `modelContext` we read — the target iframe's, or this window's. */
46
+ private targetDoc;
47
+ /**
48
+ * Switch the bridge's target to a different iframe (or back to this window's document)
49
+ * at runtime, then re-discover the model context there. Used by the persistent-agent
50
+ * shell's frame-on-navigate mode: the site starts UN-framed — its WebMCP tools live on
51
+ * the top-level `document.modelContext` — and only gets wrapped in an iframe on the first
52
+ * navigation, at which point the bridge must re-target from the top-level document to the
53
+ * new iframe. Detaches the old iframe's `load` listener, installs one on the new iframe,
54
+ * and calls `reattach()` (which polls, so a not-yet-loaded iframe still binds once ready).
55
+ */
56
+ setIframe(iframe: HTMLIFrameElement | null): void;
36
57
  /**
37
58
  * Discover `document.modelContext` in the page, cache its tools, and wire up
38
59
  * the `toolchange` / `resourceupdated` listeners. Returns the model context,
@@ -40,6 +61,15 @@ export declare class EdgeMcpBridge {
40
61
  * failure is silent and non-fatal.
41
62
  */
42
63
  attach(): Promise<EdgeMcpModelContext | null>;
64
+ /** Remove listeners from the current model context and drop the ref (no state reset). */
65
+ private unbind;
66
+ /**
67
+ * Re-discover and re-bind after the target iframe navigated to a NEW document (its old
68
+ * `document.modelContext` is dead). Keeps `sessionReady` (the avatar session in the parent
69
+ * survives), but resets the per-document dedupe so tools + resources re-register for the
70
+ * new page.
71
+ */
72
+ private reattach;
43
73
  /** Attach to a known model context: cache tools and subscribe to events. */
44
74
  private bind;
45
75
  /**
@@ -1,14 +1,21 @@
1
1
  import type { EdgeMcpModelContext } from "./edge-mcp-types";
2
- /** Read `document.modelContext`, falling back to the deprecated navigator alias. */
3
- export declare function getModelContext(): EdgeMcpModelContext | null;
2
+ /**
3
+ * Read `modelContext` from a document, falling back to the deprecated navigator alias.
4
+ *
5
+ * `doc` defaults to the ambient `document`. Pass a same-origin child frame's
6
+ * `contentDocument` to read ITS surface instead (the shell targets the wrapped
7
+ * iframe this way). Cross-origin access throws — caught and returned as `null`.
8
+ */
9
+ export declare function getModelContext(doc?: Document | null): EdgeMcpModelContext | null;
4
10
  /**
5
11
  * Discover `document.modelContext`, polling briefly if it is not present yet.
6
12
  *
7
13
  * Resolves with the model context if found, or `null` after `timeoutMs` if no
8
14
  * WebMCP surface shows up. Never rejects.
9
15
  *
10
- * @param timeoutMs How long to wait for a late-installing polyfill. Default
11
- * 1500ms — long enough to cover normal hydration ordering,
12
- * short enough not to delay the SDK noticeably.
16
+ * @param timeoutMs How long to wait for a late-installing polyfill. Default 1500ms.
17
+ * @param getDoc Optional getter for the document to read (re-evaluated each poll,
18
+ * since a targeted iframe's `contentDocument` changes as it navigates).
19
+ * Omit to use the ambient `document`.
13
20
  */
14
- export declare function discoverModelContext(timeoutMs?: number): Promise<EdgeMcpModelContext | null>;
21
+ export declare function discoverModelContext(timeoutMs?: number, getDoc?: () => Document | null): Promise<EdgeMcpModelContext | null>;
@@ -0,0 +1,102 @@
1
+ import type { NapsterCompanionApiConfig, NapsterCompanionApiInstance } from "../types";
2
+ export interface ShellOptions {
3
+ /**
4
+ * Returns a fresh connection token. The site implements this (typically a POST to
5
+ * its own endpoint that mints a token server-side). Required.
6
+ */
7
+ getToken: () => Promise<string>;
8
+ /**
9
+ * Persist the session across page navigation by wrapping the current page in a
10
+ * same-origin iframe on connect (needed for multi-page apps). Default: `false` —
11
+ * the avatar mounts inline in the current page and a full page navigation ends the
12
+ * session (a normal in-page widget). Turn on only when you need cross-navigation
13
+ * persistence.
14
+ */
15
+ persist?: boolean;
16
+ /**
17
+ * Defer the iframe wrap until the user first navigates (requires `persist`). Instead of
18
+ * wrapping the current page on connect, the landing page stays fully native — no iframe —
19
+ * and the avatar mounts in the top-level document. The shell intercepts the first in-site
20
+ * link click and, if the target is allowed (not matched by `persistConfig`), wraps the site
21
+ * into an iframe THEN, loading the clicked page, so the session persists from that point on.
22
+ *
23
+ * Only anchor clicks are caught. Programmatic navigation (`location.assign`, `location.href=`),
24
+ * address-bar/bookmark loads, modified/`_blank` clicks, and links matched by `persistConfig`
25
+ * fall through to a normal top-level navigation, which ends the session (as a plain widget would).
26
+ *
27
+ * Default: `false` (wrap eagerly on connect). Ignored unless `persist` is on.
28
+ */
29
+ frameOnNavigate?: boolean;
30
+ /**
31
+ * The ONLY URLs that break out of the iframe (open at the top window and end the session).
32
+ * A link is opened inside the iframe unless it matches this denylist — use it for URLs that
33
+ * can't be framed, e.g. an identity / SSO subdomain, or same-origin paths that redirect
34
+ * off-site. Everything not listed (including other cross-origin links) navigates in the iframe.
35
+ */
36
+ persistConfig?: {
37
+ /**
38
+ * Hostnames to force out of the iframe. Matched as the exact host OR a parent domain —
39
+ * e.g. `"napster.com"` also matches `"identity.napster.com"`.
40
+ */
41
+ domains?: string[];
42
+ /**
43
+ * URLs to force out of the iframe: an absolute `"https://…"` (exact or prefix match), or a
44
+ * same-origin path prefix like `"/signout"`.
45
+ */
46
+ urls?: string[];
47
+ };
48
+ /**
49
+ * SECOND persistence variant (no iframe). When a session is active and the user navigates
50
+ * (a normal full page load), automatically start a NEW session on the next page — so the
51
+ * avatar "follows" the user across pages. Each page is a fresh connection (the backend can
52
+ * resume the conversation by externalClientId). The intent is remembered in sessionStorage
53
+ * and cleared when the user ends the session. Ignored when `persist` (the iframe variant)
54
+ * is on. Default: `false`.
55
+ */
56
+ reconnectOnNavigation?: boolean;
57
+ /** When `persist` is on, the URL to load in the iframe. Default: the current page (`location.href`). */
58
+ siteUrl?: string;
59
+ /** Element to build the launcher/iframe into. Default: `document.body`. */
60
+ mount?: HTMLElement;
61
+ /** Avatar config forwarded to `init()`. `mountContainer`/`layout` are managed by the shell. */
62
+ avatar?: Partial<NapsterCompanionApiConfig>;
63
+ /** The iframe `allow` attribute (Permissions Policy for the site). Default: `"autoplay; clipboard-write"`. */
64
+ iframeAllow?: string;
65
+ /** Launcher button label. Default: `"Talk to an agent"`. */
66
+ launcherLabel?: string;
67
+ /** Start a session immediately instead of showing a launcher. Default: `false`. */
68
+ autoConnect?: boolean;
69
+ /**
70
+ * While a session is active, mirror the iframe's path into the address bar (so inner
71
+ * pages stay shareable/bookmarkable) and follow parent back/forward into the iframe.
72
+ * Same-origin only. Default: `true`.
73
+ */
74
+ syncHistory?: boolean;
75
+ /** Log shell lifecycle to the console. Default: `false`. */
76
+ debug?: boolean;
77
+ }
78
+ export interface ShellController {
79
+ /** `true` if this call built the launcher (top-level); `false` if it no-oped (framed content). */
80
+ isShell: boolean;
81
+ /** The live avatar instance once connected, else `null`. */
82
+ getInstance(): NapsterCompanionApiInstance | null;
83
+ /** The site iframe while a session is active, else `null`. */
84
+ getIframe(): HTMLIFrameElement | null;
85
+ /** Start a session: connect the avatar (and, with `persist`, wrap the current page in the iframe). */
86
+ connect(): Promise<void>;
87
+ /** End the session (and, if wrapped, unwrap back to the plain site). */
88
+ disconnect(): void;
89
+ /** Remove the launcher and any active session/iframe. */
90
+ destroy(): void;
91
+ }
92
+ type InitFn = (token: string, config?: Partial<NapsterCompanionApiConfig>) => Promise<NapsterCompanionApiInstance>;
93
+ /**
94
+ * Build the lazy persistent-agent shell. Returns a {@link ShellController}.
95
+ *
96
+ * A no-op (returns a controller with `isShell: false`) when run inside the shell
97
+ * iframe (framed content), so the same snippet is safe to include on every page.
98
+ *
99
+ * `init` is the SDK's `init` method, wired by the SDK.
100
+ */
101
+ export declare function mountShell(init: InitFn, options: ShellOptions): ShellController;
102
+ export {};
@@ -335,6 +335,13 @@ export interface NapsterCompanionApiConfig {
335
335
  functions?: CompanionFunction[];
336
336
  /** Enable debug logging throughout the SDK. When enabled, detailed logs will be output to the console. */
337
337
  debug?: boolean;
338
+ /**
339
+ * Read WebMCP tools from a same-origin child iframe's `document.modelContext` instead of
340
+ * this window's. Set by the persistent-agent shell (`mountShell` with `persist`) so the
341
+ * avatar in the top-level document operates the SITE running inside the wrapped iframe.
342
+ * Leave unset for a normal same-page integration.
343
+ */
344
+ edgeMcpIframe?: HTMLIFrameElement;
338
345
  /** Analytics configuration controls analytics delivery and tracking. */
339
346
  analytics?: AnalyticsConfig;
340
347
  /** Lifecycle callbacks. All are optional. */
@@ -372,6 +379,14 @@ export interface NapsterCompanionApiInstance {
372
379
  avatarIsVisible: () => boolean;
373
380
  /** Tear down the SDK, remove DOM nodes and clean up resources. */
374
381
  destroy: () => void;
382
+ /**
383
+ * Re-target the Edge MCP bridge at runtime: read the site's WebMCP tools from the given
384
+ * same-origin iframe's `document.modelContext`, or pass `null` to read them from this
385
+ * window's document. Used by the persistent-agent shell's frame-on-navigate mode, where
386
+ * the site is only wrapped in an iframe on the first navigation. No-op if the bridge is
387
+ * inactive.
388
+ */
389
+ setEdgeMcpIframe: (iframe: HTMLIFrameElement | null) => void;
375
390
  /**
376
391
  * The current session identifier. Available after a connection is established
377
392
  * and reset to `undefined` once the connection is closed.
@@ -456,6 +471,12 @@ export interface NapsterCompanionApiSDK {
456
471
  * Returns a Promise that resolves to a controllable `NapsterCompanionApiInstance`.
457
472
  */
458
473
  init(token: string, config?: Partial<NapsterCompanionApiConfig>): Promise<NapsterCompanionApiInstance>;
474
+ /**
475
+ * Mount a persistent-agent shell for multi-page apps: renders the site in a
476
+ * same-origin iframe with the avatar in the top-level shell, so the agent
477
+ * session survives navigation. No-op when run as the framed content.
478
+ */
479
+ mountShell(options: import("../shell/mount-shell").ShellOptions): import("../shell/mount-shell").ShellController;
459
480
  /** The SDK version string (useful for diagnostics). */
460
481
  version: string;
461
482
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@touchcastllc/napster-companion-api-dev",
3
- "version": "1.0.0-alpha.74",
3
+ "version": "1.0.0-alpha.77",
4
4
  "keywords": [
5
5
  "napster",
6
6
  "companion-api",