@memberjunction/ai-bridge-webex 0.0.1 → 5.41.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/README.md CHANGED
@@ -1,45 +1,112 @@
1
1
  # @memberjunction/ai-bridge-webex
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/ai-bridge-webex`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**
3
+ The **Cisco Webex** Realtime Bridge driver in MemberJunction's Realtime Bridges program. It connects
4
+ the one realtime agent engine to a **Webex meeting**: bidirectional audio, a diarized participant roster,
5
+ participant mute, Webex meeting chat, and a **Meeting Controls** facilitator channel — all behind an
6
+ injectable Cisco Webex Meetings SDK seam so the driver builds and unit-tests with **no network and no real
7
+ Cisco Webex Meetings SDK**. It is a structural mirror of the reference
8
+ [`@memberjunction/ai-bridge-zoom`](../BridgeZoom/README.md) driver (and its sibling
9
+ [`@memberjunction/ai-bridge-teams`](../BridgeTeams/README.md)).
10
+
11
+ See the [Realtime Bridges Guide](../../../../guides/REALTIME_BRIDGES_GUIDE.md) and
12
+ [`/plans/realtime/realtime-bridges-architecture.md`](../../../../plans/realtime/realtime-bridges-architecture.md)
13
+ (§3 provider abstraction, §4b channels, §8 Cisco Webex capability row) for the full architecture.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @memberjunction/ai-bridge-webex
19
+ ```
20
+
21
+ ## What it provides
22
+
23
+ - **`WebexBridge`** — `@RegisterClass(BaseRealtimeBridge, 'WebexBridge')`. The `MJ: AI Bridge Providers`
24
+ row with `DriverClass = 'WebexBridge'` resolves to this driver via the `ClassFactory`. Implements the
25
+ four `BaseRealtimeBridge` abstracts (`Connect` / `Disconnect` / `SendMedia` / `OnMedia`) and the
26
+ capability-gated virtuals Webex supports (`GetParticipants`, `OnParticipantChange`), plus
27
+ `GetMeetingControlsEventSource` for the facilitator channel and a `PostChatMessage` helper.
28
+ - **`IWebexMeetingSdk`** — the **injectable seam** the driver depends on instead of the real SDK.
29
+ - **`WebexMeetingControlsEventSource`** — adapts the seam's roster / hand-raise / speaking / mute into
30
+ the bridge's `IBridgeMeetingControlsEventSource`, so the engine wires the Meeting Controls channel.
31
+
32
+ ## Capability coverage (the Cisco Webex seed row)
33
+
34
+ | Capability | Status |
35
+ |---|---|
36
+ | On-demand + scheduled + invite join | ✅ |
37
+ | Inbound routing | ✅ |
38
+ | Audio in / out | ✅ |
39
+ | Video in/out, Screen in/out (directional flags) | ✅ (transport carries them; models light audio first) |
40
+ | Speaker diarization (roster + per-speaker labels) | ✅ |
41
+ | Participant mute (Meeting Controls) | ✅ |
42
+ | Webex meeting chat | ✅ |
43
+ | Native raised-hand | ⚠️ Partial — wired where the Meetings SDK surfaces it; tolerant of it never firing |
44
+ | DTMF / call transfer / recording | ➖ not Webex-meeting features here — the gated base methods throw `BridgeCapabilityNotSupportedError` |
45
+
46
+ Capability gating is two-layer (defense-in-depth): the engine checks the provider's `SupportedFeatures`
47
+ first, and the driver re-asserts each flag with `RequireFeature` at the top of its overrides.
48
+
49
+ ### Webex vs. Zoom capability differences
50
+
51
+ Webex advertises everything Zoom does, **plus** `InviteJoin` and `InboundRouting` (the calendar-invite /
52
+ inbound-routing UX), and its native raised-hand is **partial** rather than fully reliable. Unlike Teams,
53
+ Webex does **not** advertise `NativeInvite`. Everything else (audio in/out, directional video/screen,
54
+ diarized roster, mute, meeting chat) maps one-to-one onto the Zoom reference driver.
55
+
56
+ ## The Cisco Webex Meetings SDK seam (`IWebexMeetingSdk`)
57
+
58
+ The driver never imports the real Webex SDK. It depends only on this minimal interface, named after
59
+ Cisco Webex Meetings SDK / xAPI / Webex bot concepts:
60
+
61
+ ```typescript
62
+ export interface IWebexMeetingSdk {
63
+ join(args: WebexJoinArgs): Promise<WebexJoinResult>;
64
+ leave(): Promise<void>;
65
+ sendAudioFrame(pcm: ArrayBuffer): void; // agent's voice out (outbound audio track)
66
+ onAudioFrame(cb: (frame: WebexAudioFrame) => void): void; // raw per-participant audio in (diarization)
67
+ onParticipantJoin(cb: (p: WebexParticipant) => void): void;
68
+ onParticipantLeave(cb: (id: string) => void): void;
69
+ onHandRaise(cb: (id: string, raised: boolean) => void): void; // ⚠️ partial on Webex
70
+ getParticipants(): Promise<WebexParticipant[]>;
71
+ postChatMessage(text: string): Promise<void>; // Webex meeting space chat
72
+ muteParticipant(participantId: string): Promise<void>;
73
+ onMeetingEnded(cb: () => void): void;
74
+ }
75
+ ```
76
+
77
+ ### Production binding (deployment TODO)
78
+
79
+ In production this is bound to the **Cisco Webex Meetings SDK** (the embedded-app / Web / mobile Meetings
80
+ SDK that joins the meeting and exposes media + the `members` roster) together with the **Webex bot
81
+ framework** (Webex Messaging API for the in-meeting space chat) and, where a Webex room device is involved,
82
+ the **xAPI** for participant actions. Supply a factory via the creation seam:
83
+
84
+ ```typescript
85
+ import { WebexBridge } from '@memberjunction/ai-bridge-webex';
86
+
87
+ // Once, where bridge drivers are configured:
88
+ // bridge.SetSdkFactory((config) => new RealWebexSdkAdapter(config));
89
+ // The adapter implements IWebexMeetingSdk over the real Webex SDK. The driver + its tests do not change.
90
+ ```
91
+
92
+ Out of the box, `WebexBridge` ships **without** the real SDK adapter — `Connect` throws an explicit
93
+ "bind the real Cisco Webex SDK" error until `SetSdkFactory` is called. Tests inject a `FakeWebexSdk`.
94
+
95
+ ## Usage (engine-driven)
96
+
97
+ The bridge is not used directly — `AIBridgeEngine.StartBridgeSession` (`@memberjunction/ai-bridge-server`)
98
+ resolves it from the provider's `DriverClass`, wires the transport seam to the injected
99
+ `IRealtimeSession`, and (when a channel host is supplied) wires the Meeting Controls channel from
100
+ `GetMeetingControlsEventSource`. See the bridge-server package and the guide's "Channel plane" section.
101
+
102
+ ## Testing
103
+
104
+ `FakeWebexSdk` (in `src/__tests__/`) is an in-memory `IWebexMeetingSdk` with drive helpers and capture
105
+ sinks. The suite covers connect/disconnect (incl. parsing the meeting number out of the join link),
106
+ audio in→`OnMedia` (speaker labels) and out→seam, participant join/leave → roster + event source, native
107
+ hand-raise → Meeting Controls perception, capability gating (a feature Webex lacks throws), and chat — all
108
+ with no network.
109
+
110
+ ```bash
111
+ cd packages/AI/Providers/BridgeWebex && npm run test
112
+ ```
@@ -0,0 +1,4 @@
1
+ export * from './webex-sdk.js';
2
+ export * from './webex-meeting-controls.js';
3
+ export * from './webex-bridge.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './webex-sdk.js';
2
+ export * from './webex-meeting-controls.js';
3
+ export * from './webex-bridge.js';
4
+ import { LoadWebexBridge } from './webex-bridge.js';
5
+ // Static reference so bundlers cannot tree-shake the @RegisterClass(BaseRealtimeBridge, 'WebexBridge')
6
+ // registration. Calling the no-op here keeps the driver resolvable by the engine's ClassFactory.
7
+ LoadWebexBridge();
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,uGAAuG;AACvG,iGAAiG;AACjG,eAAe,EAAE,CAAC"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @fileoverview `WebexBridge` — the **Cisco Webex** platform Realtime Bridge driver, connecting the
3
+ * one realtime agent engine to a **Webex meeting**. Implements the {@link BaseRealtimeBridge} contract
4
+ * against an injectable {@link IWebexMeetingSdk} seam so it builds + unit-tests with NO network and NO
5
+ * real Cisco Webex Meetings SDK (the Gemini `connectLiveSession` testability pattern). Structurally
6
+ * mirrors `ZoomBridge` / `TeamsBridge` (the reference drivers) — only the platform names + join-coordinate
7
+ * parsing differ.
8
+ *
9
+ * Webex capability coverage (per the §8 seed row): on-demand + scheduled + invite join, inbound routing,
10
+ * audio in/out (video/screen directional flags carried by transport), a diarized roster
11
+ * (`SpeakerDiarization`), participant mute + Webex meeting chat (via the Meeting Controls channel). Native
12
+ * raised-hand is ⚠️ partial over the Meetings SDK — wired where the platform surfaces it, tolerant of it
13
+ * never firing. Telephony features (DTMF / transfer / recording) are NOT Webex-meeting features here, so
14
+ * those virtual base methods keep throwing `BridgeCapabilityNotSupportedError`.
15
+ *
16
+ * @module @memberjunction/ai-bridge-webex
17
+ * @author MemberJunction.com
18
+ */
19
+ import { BaseRealtimeBridge, BridgeConnectResult, BridgeDisconnectReason, BridgeMediaFrame, BridgeMediaTrackKind, BridgeParticipantInfo, RealtimeBridgeContext, IBridgeMeetingControlsEventSource } from '@memberjunction/ai-bridge-base';
20
+ import { WebexMeetingSdkFactory } from './webex-sdk.js';
21
+ /**
22
+ * The `DriverClass` key {@link WebexBridge} registers under. A `MJ: AI Bridge Providers` row with
23
+ * `DriverClass = 'WebexBridge'` resolves to this driver via the `ClassFactory`.
24
+ */
25
+ export declare const WEBEX_BRIDGE_DRIVER_CLASS = "WebexBridge";
26
+ /**
27
+ * Realtime Bridge driver for **Cisco Webex meetings**.
28
+ *
29
+ * Construct the driver with the default constructor (the engine's `ClassFactory` path) — it lazily
30
+ * builds the Webex SDK from the {@link sdkFactory} at {@link Connect} time. Tests inject a `FakeWebexSdk`
31
+ * by overriding the factory via {@link SetSdkFactory} (the creation seam) before connecting.
32
+ *
33
+ * Registered via `@RegisterClass(BaseRealtimeBridge, 'WebexBridge')`.
34
+ */
35
+ export declare class WebexBridge extends BaseRealtimeBridge {
36
+ /** The live Webex SDK seam for this session, created at {@link Connect}. */
37
+ private sdk;
38
+ /** The bot's own member id in the joined meeting (set at {@link Connect}). */
39
+ private botParticipantId;
40
+ /** The inbound-media handler registered via {@link OnMedia}; raw audio frames are forwarded to it. */
41
+ private mediaHandler?;
42
+ /** The roster-change handler registered via {@link OnParticipantChange}. */
43
+ private participantHandler?;
44
+ /** The Meeting Controls event source for this session (only when diarization is supported). */
45
+ private meetingControls;
46
+ /**
47
+ * The SDK creation seam. Defaults to a factory that throws an explicit "bind the real Webex SDK"
48
+ * error, since this package ships WITHOUT the real SDK adapter (a deployment concern). Production
49
+ * sets a real factory via {@link SetSdkFactory}; tests inject a `FakeWebexSdk`.
50
+ */
51
+ private sdkFactory;
52
+ /**
53
+ * Sets the {@link WebexMeetingSdkFactory} this driver uses to construct its SDK seam at connect —
54
+ * the creation seam (mirroring Gemini's overridable `connectLiveSession`). Production binds the
55
+ * real Cisco Webex Meetings SDK adapter here; tests inject a `FakeWebexSdk`.
56
+ *
57
+ * @param factory The factory that builds the {@link IWebexMeetingSdk} for a session.
58
+ */
59
+ SetSdkFactory(factory: WebexMeetingSdkFactory): void;
60
+ /**
61
+ * Joins the Webex meeting and brings the bot online. Captures the capability context, builds the SDK
62
+ * from the {@link sdkFactory}, wires the inbound raw-audio path and meeting-ended callback, joins,
63
+ * and (when the provider diarizes) constructs + seeds the Meeting Controls event source.
64
+ *
65
+ * @param ctx The bridge context (features, provider name, the meeting link as `Address`, config).
66
+ * @returns The bot member + meeting (external connection) identifiers, persisted by the engine.
67
+ */
68
+ Connect(ctx: RealtimeBridgeContext): Promise<BridgeConnectResult>;
69
+ /**
70
+ * Leaves the Webex meeting and releases all SDK resources. Tolerant of teardown errors.
71
+ *
72
+ * @param _reason Why the disconnect happened (Webex teardown is uniform; the bot simply leaves).
73
+ */
74
+ Disconnect(_reason: BridgeDisconnectReason): Promise<void>;
75
+ /**
76
+ * Sends an outbound media frame into the meeting. Audio is fed to the SDK's outbound audio-track
77
+ * path; video/screen frames are gated by the directional capability flags (the transport carries
78
+ * them, and the SDK send is a deployment-time binding TODO — today the models emit audio).
79
+ *
80
+ * @param track The outbound track the frame targets.
81
+ * @param frame The media frame to send.
82
+ */
83
+ SendMedia(track: BridgeMediaTrackKind, frame: BridgeMediaFrame): void;
84
+ /**
85
+ * Registers the inbound-media handler. The driver forwards each raw per-participant audio frame
86
+ * (with its speaker label) to this handler; the engine routes it to `IRealtimeSession.SendInput`.
87
+ *
88
+ * @param handler Invoked with each inbound media frame.
89
+ */
90
+ OnMedia(handler: (frame: BridgeMediaFrame) => void): void;
91
+ /**
92
+ * Returns the current Webex member roster (gated by `SpeakerDiarization`). Re-asserts the flag
93
+ * (defense-in-depth) so even an engine-bypassing caller cannot pull a roster a disabled provider
94
+ * forbids.
95
+ *
96
+ * @returns The current members.
97
+ * @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled for the provider.
98
+ */
99
+ GetParticipants(): Promise<BridgeParticipantInfo[]>;
100
+ /**
101
+ * Registers a roster-change handler (gated by `SpeakerDiarization`). The driver fires it from the
102
+ * SDK's member join/leave stream with the full current roster.
103
+ *
104
+ * @param handler Invoked with the updated participant list on each change.
105
+ * @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled.
106
+ */
107
+ OnParticipantChange(handler: (participants: BridgeParticipantInfo[]) => void): void;
108
+ /**
109
+ * Returns the Webex Meeting Controls event source for this session (roster · hand-raise · speaking ·
110
+ * mute), or `null` when diarization is off (no roster to facilitate). The engine wires the Meeting
111
+ * Controls channel from this.
112
+ */
113
+ GetMeetingControlsEventSource(): IBridgeMeetingControlsEventSource | null;
114
+ /**
115
+ * Posts a message to the Webex meeting space chat. Exposed for the channel plane / turn-taking hybrid
116
+ * mode (the social-cost-free "raise hand"). Best-effort — a chat failure is logged, never fatal.
117
+ *
118
+ * @param text The chat message to post.
119
+ */
120
+ PostChatMessage(text: string): Promise<void>;
121
+ /** The bot's member id in the joined meeting (or `null` before {@link Connect}). */
122
+ get BotParticipantId(): string | null;
123
+ /** Wires the SDK's inbound raw-audio callback to a diarized inbound {@link BridgeMediaFrame}. */
124
+ private wireInboundAudio;
125
+ /**
126
+ * Wires the SDK's member join/leave + hand-raise streams ONCE (the driver is the single owner;
127
+ * the SDK seam is latest-handler-wins). Each roster change fans out to both the driver's own
128
+ * roster-change handler and the Meeting Controls source; hand-raise signals (⚠️ partial on Webex)
129
+ * feed the source.
130
+ */
131
+ private wireRoster;
132
+ /** Pulls the current roster from the SDK and fans it out to both the participant handler + Meeting Controls. */
133
+ private refreshRoster;
134
+ /** Handles the SDK's meeting-ended signal: surface an empty roster so the engine sees everyone gone. */
135
+ private handleMeetingEnded;
136
+ /** Builds the SDK join args from the bridge context (Address is the meeting link). */
137
+ private buildJoinArgs;
138
+ /**
139
+ * Extracts the Webex meeting number from a meeting link when present, else `undefined`. Webex meeting
140
+ * numbers are 9–11 digit strings carried in the join URL (e.g. as a `MTID`/`/j.php?MTID=...` query
141
+ * param or a trailing numeric segment of a personal-room / scheduled link). Tolerant — a non-matching
142
+ * address yields `undefined` so the SDK can resolve coordinates itself.
143
+ */
144
+ private parseMeetingNumber;
145
+ /** Returns the PCM bytes of an outbound audio frame, preferring the binary payload. */
146
+ private framePcm;
147
+ /** Decodes a base64 payload to an `ArrayBuffer` (Node `Buffer` fast path, `atob` fallback). */
148
+ private decodeBase64;
149
+ }
150
+ /**
151
+ * Tree-shaking-prevention loader. Modern bundlers cannot see the `@RegisterClass` dynamic registration
152
+ * of {@link WebexBridge} and may eliminate it. Import and call this no-op from a static code path
153
+ * (the package entry point does) so the `ClassFactory` can resolve `'WebexBridge'`.
154
+ */
155
+ export declare function LoadWebexBridge(): void;
156
+ //# sourceMappingURL=webex-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webex-bridge.d.ts","sourceRoot":"","sources":["../src/webex-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EAErB,qBAAqB,EACrB,iCAAiC,EACpC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAEH,sBAAsB,EAKzB,MAAM,aAAa,CAAC;AAGrB;;;GAGG;AACH,eAAO,MAAM,yBAAyB,gBAAgB,CAAC;AA8BvD;;;;;;;;GAQG;AACH,qBACa,WAAY,SAAQ,kBAAkB;IAC/C,4EAA4E;IAC5E,OAAO,CAAC,GAAG,CAAiC;IAE5C,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB,CAAuB;IAE/C,sGAAsG;IACtG,OAAO,CAAC,YAAY,CAAC,CAAoC;IAEzD,4EAA4E;IAC5E,OAAO,CAAC,kBAAkB,CAAC,CAAkD;IAE7E,+FAA+F;IAC/F,OAAO,CAAC,eAAe,CAAgD;IAEvE;;;;OAIG;IACH,OAAO,CAAC,UAAU,CAMhB;IAEF;;;;;;OAMG;IACI,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI;IAM3D;;;;;;;OAOG;IACU,OAAO,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA2B9E;;;;OAIG;IACU,UAAU,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvE;;;;;;;OAOG;IACI,SAAS,CAAC,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAuB5E;;;;;OAKG;IACI,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAMhE;;;;;;;OAOG;IACmB,eAAe,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;IASzE;;;;;;OAMG;IACa,mBAAmB,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,qBAAqB,EAAE,KAAK,IAAI,GAAG,IAAI;IAKnG;;;;OAIG;IACa,6BAA6B,IAAI,iCAAiC,GAAG,IAAI;IAMzF;;;;;OAKG;IACU,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWzD,oFAAoF;IACpF,IAAW,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAE3C;IAID,iGAAiG;IACjG,OAAO,CAAC,gBAAgB;IAWxB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAMlB,gHAAgH;YAClG,aAAa;IAU3B,wGAAwG;IACxG,OAAO,CAAC,kBAAkB;IAK1B,sFAAsF;IACtF,OAAO,CAAC,aAAa;IAYrB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAmB1B,uFAAuF;IACvF,OAAO,CAAC,QAAQ;IAUhB,+FAA+F;IAC/F,OAAO,CAAC,YAAY;CAevB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
@@ -0,0 +1,361 @@
1
+ /**
2
+ * @fileoverview `WebexBridge` — the **Cisco Webex** platform Realtime Bridge driver, connecting the
3
+ * one realtime agent engine to a **Webex meeting**. Implements the {@link BaseRealtimeBridge} contract
4
+ * against an injectable {@link IWebexMeetingSdk} seam so it builds + unit-tests with NO network and NO
5
+ * real Cisco Webex Meetings SDK (the Gemini `connectLiveSession` testability pattern). Structurally
6
+ * mirrors `ZoomBridge` / `TeamsBridge` (the reference drivers) — only the platform names + join-coordinate
7
+ * parsing differ.
8
+ *
9
+ * Webex capability coverage (per the §8 seed row): on-demand + scheduled + invite join, inbound routing,
10
+ * audio in/out (video/screen directional flags carried by transport), a diarized roster
11
+ * (`SpeakerDiarization`), participant mute + Webex meeting chat (via the Meeting Controls channel). Native
12
+ * raised-hand is ⚠️ partial over the Meetings SDK — wired where the platform surfaces it, tolerant of it
13
+ * never firing. Telephony features (DTMF / transfer / recording) are NOT Webex-meeting features here, so
14
+ * those virtual base methods keep throwing `BridgeCapabilityNotSupportedError`.
15
+ *
16
+ * @module @memberjunction/ai-bridge-webex
17
+ * @author MemberJunction.com
18
+ */
19
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
20
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
21
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
22
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
23
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
24
+ };
25
+ import { RegisterClass } from '@memberjunction/global';
26
+ import { LogError } from '@memberjunction/core';
27
+ import { BaseRealtimeBridge, } from '@memberjunction/ai-bridge-base';
28
+ import { WebexMeetingControlsEventSource } from './webex-meeting-controls.js';
29
+ /**
30
+ * The `DriverClass` key {@link WebexBridge} registers under. A `MJ: AI Bridge Providers` row with
31
+ * `DriverClass = 'WebexBridge'` resolves to this driver via the `ClassFactory`.
32
+ */
33
+ export const WEBEX_BRIDGE_DRIVER_CLASS = 'WebexBridge';
34
+ /**
35
+ * Maps a Webex member role to the bridge's {@link BridgeParticipantRole}. The bot (`IsSelf`) is
36
+ * surfaced as `'Agent'`; Webex hosts/cohosts map to host/co-host.
37
+ */
38
+ function mapParticipantRole(role, isSelf) {
39
+ if (isSelf) {
40
+ return 'Agent';
41
+ }
42
+ switch (role) {
43
+ case 'Host':
44
+ return 'Host';
45
+ case 'Cohost':
46
+ return 'CoHost';
47
+ default:
48
+ return 'Participant';
49
+ }
50
+ }
51
+ /** Maps a Webex member onto the bridge's {@link BridgeParticipantInfo}. */
52
+ function toBridgeParticipant(p) {
53
+ return {
54
+ ExternalId: p.ParticipantId,
55
+ DisplayName: p.DisplayName,
56
+ Role: mapParticipantRole(p.Role, p.IsSelf),
57
+ IsAgent: p.IsSelf === true,
58
+ };
59
+ }
60
+ /**
61
+ * Realtime Bridge driver for **Cisco Webex meetings**.
62
+ *
63
+ * Construct the driver with the default constructor (the engine's `ClassFactory` path) — it lazily
64
+ * builds the Webex SDK from the {@link sdkFactory} at {@link Connect} time. Tests inject a `FakeWebexSdk`
65
+ * by overriding the factory via {@link SetSdkFactory} (the creation seam) before connecting.
66
+ *
67
+ * Registered via `@RegisterClass(BaseRealtimeBridge, 'WebexBridge')`.
68
+ */
69
+ let WebexBridge = class WebexBridge extends BaseRealtimeBridge {
70
+ constructor() {
71
+ super(...arguments);
72
+ /** The live Webex SDK seam for this session, created at {@link Connect}. */
73
+ this.sdk = null;
74
+ /** The bot's own member id in the joined meeting (set at {@link Connect}). */
75
+ this.botParticipantId = null;
76
+ /** The Meeting Controls event source for this session (only when diarization is supported). */
77
+ this.meetingControls = null;
78
+ /**
79
+ * The SDK creation seam. Defaults to a factory that throws an explicit "bind the real Webex SDK"
80
+ * error, since this package ships WITHOUT the real SDK adapter (a deployment concern). Production
81
+ * sets a real factory via {@link SetSdkFactory}; tests inject a `FakeWebexSdk`.
82
+ */
83
+ this.sdkFactory = () => {
84
+ throw new Error('WebexBridge has no Cisco Webex SDK bound. Call WebexBridge.SetSdkFactory(...) with a factory ' +
85
+ 'that builds an IWebexMeetingSdk over the real Cisco Webex Meetings SDK / Webex bot framework, ' +
86
+ 'or inject a fake in tests.');
87
+ };
88
+ }
89
+ /**
90
+ * Sets the {@link WebexMeetingSdkFactory} this driver uses to construct its SDK seam at connect —
91
+ * the creation seam (mirroring Gemini's overridable `connectLiveSession`). Production binds the
92
+ * real Cisco Webex Meetings SDK adapter here; tests inject a `FakeWebexSdk`.
93
+ *
94
+ * @param factory The factory that builds the {@link IWebexMeetingSdk} for a session.
95
+ */
96
+ SetSdkFactory(factory) {
97
+ this.sdkFactory = factory;
98
+ }
99
+ // ── Abstract — every bridge MUST implement ───────────────────────────────────────
100
+ /**
101
+ * Joins the Webex meeting and brings the bot online. Captures the capability context, builds the SDK
102
+ * from the {@link sdkFactory}, wires the inbound raw-audio path and meeting-ended callback, joins,
103
+ * and (when the provider diarizes) constructs + seeds the Meeting Controls event source.
104
+ *
105
+ * @param ctx The bridge context (features, provider name, the meeting link as `Address`, config).
106
+ * @returns The bot member + meeting (external connection) identifiers, persisted by the engine.
107
+ */
108
+ async Connect(ctx) {
109
+ this.applyContext(ctx);
110
+ this.RequireFeature('AudioIn'); // a Webex meeting bridge requires bidirectional audio at minimum
111
+ this.RequireFeature('AudioOut');
112
+ this.sdk = this.sdkFactory(ctx.Configuration);
113
+ this.wireInboundAudio(this.sdk);
114
+ this.sdk.onMeetingEnded(() => this.handleMeetingEnded());
115
+ // Roster diarization is optional per provider; only stand up the Meeting Controls source when
116
+ // the platform advertises it (the engine also gates participant tracking on this flag).
117
+ if (this.features.SpeakerDiarization === true) {
118
+ this.meetingControls = new WebexMeetingControlsEventSource(this.sdk);
119
+ }
120
+ // The driver owns the single SDK subscription set and fans out to BOTH its own roster handler
121
+ // and the Meeting Controls source (the SDK seam is latest-handler-wins, so there is one owner).
122
+ this.wireRoster(this.sdk);
123
+ const result = await this.sdk.join(this.buildJoinArgs(ctx));
124
+ this.botParticipantId = result.BotParticipantId;
125
+ // Seed the initial roster now that the bot has joined.
126
+ await this.refreshRoster(this.sdk);
127
+ return { BotParticipantId: result.BotParticipantId, ExternalConnectionId: result.MeetingId };
128
+ }
129
+ /**
130
+ * Leaves the Webex meeting and releases all SDK resources. Tolerant of teardown errors.
131
+ *
132
+ * @param _reason Why the disconnect happened (Webex teardown is uniform; the bot simply leaves).
133
+ */
134
+ async Disconnect(_reason) {
135
+ const sdk = this.sdk;
136
+ this.sdk = null;
137
+ this.botParticipantId = null;
138
+ this.meetingControls = null;
139
+ this.mediaHandler = undefined;
140
+ this.participantHandler = undefined;
141
+ if (sdk) {
142
+ try {
143
+ await sdk.leave();
144
+ }
145
+ catch (err) {
146
+ LogError(`[WebexBridge] leave() failed: ${err instanceof Error ? err.message : String(err)}`);
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Sends an outbound media frame into the meeting. Audio is fed to the SDK's outbound audio-track
152
+ * path; video/screen frames are gated by the directional capability flags (the transport carries
153
+ * them, and the SDK send is a deployment-time binding TODO — today the models emit audio).
154
+ *
155
+ * @param track The outbound track the frame targets.
156
+ * @param frame The media frame to send.
157
+ */
158
+ SendMedia(track, frame) {
159
+ if (!this.sdk) {
160
+ return; // not connected — drop
161
+ }
162
+ switch (track) {
163
+ case 'audio-out': {
164
+ const pcm = this.framePcm(frame);
165
+ if (pcm) {
166
+ this.sdk.sendAudioFrame(pcm);
167
+ }
168
+ break;
169
+ }
170
+ case 'video-out':
171
+ case 'screen-out':
172
+ // Directional video/screen capability is declared; the SDK send binding lands with the
173
+ // first realtime model that emits these tracks (TODO at deployment). No-op for now.
174
+ break;
175
+ default:
176
+ // An inbound track was passed to SendMedia — ignore (defensive).
177
+ break;
178
+ }
179
+ }
180
+ /**
181
+ * Registers the inbound-media handler. The driver forwards each raw per-participant audio frame
182
+ * (with its speaker label) to this handler; the engine routes it to `IRealtimeSession.SendInput`.
183
+ *
184
+ * @param handler Invoked with each inbound media frame.
185
+ */
186
+ OnMedia(handler) {
187
+ this.mediaHandler = handler;
188
+ }
189
+ // ── Capability-gated virtuals Webex supports (gated by SupportedFeatures) ──────────
190
+ /**
191
+ * Returns the current Webex member roster (gated by `SpeakerDiarization`). Re-asserts the flag
192
+ * (defense-in-depth) so even an engine-bypassing caller cannot pull a roster a disabled provider
193
+ * forbids.
194
+ *
195
+ * @returns The current members.
196
+ * @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled for the provider.
197
+ */
198
+ async GetParticipants() {
199
+ this.RequireFeature('SpeakerDiarization');
200
+ if (!this.sdk) {
201
+ return [];
202
+ }
203
+ const participants = await this.sdk.getParticipants();
204
+ return participants.map(toBridgeParticipant);
205
+ }
206
+ /**
207
+ * Registers a roster-change handler (gated by `SpeakerDiarization`). The driver fires it from the
208
+ * SDK's member join/leave stream with the full current roster.
209
+ *
210
+ * @param handler Invoked with the updated participant list on each change.
211
+ * @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled.
212
+ */
213
+ OnParticipantChange(handler) {
214
+ this.RequireFeature('SpeakerDiarization');
215
+ this.participantHandler = handler;
216
+ }
217
+ /**
218
+ * Returns the Webex Meeting Controls event source for this session (roster · hand-raise · speaking ·
219
+ * mute), or `null` when diarization is off (no roster to facilitate). The engine wires the Meeting
220
+ * Controls channel from this.
221
+ */
222
+ GetMeetingControlsEventSource() {
223
+ return this.meetingControls;
224
+ }
225
+ // ── Webex-native surfaces (used by the channel plane / facilitator) ──────────────
226
+ /**
227
+ * Posts a message to the Webex meeting space chat. Exposed for the channel plane / turn-taking hybrid
228
+ * mode (the social-cost-free "raise hand"). Best-effort — a chat failure is logged, never fatal.
229
+ *
230
+ * @param text The chat message to post.
231
+ */
232
+ async PostChatMessage(text) {
233
+ if (!this.sdk) {
234
+ return;
235
+ }
236
+ try {
237
+ await this.sdk.postChatMessage(text);
238
+ }
239
+ catch (err) {
240
+ LogError(`[WebexBridge] postChatMessage failed: ${err instanceof Error ? err.message : String(err)}`);
241
+ }
242
+ }
243
+ /** The bot's member id in the joined meeting (or `null` before {@link Connect}). */
244
+ get BotParticipantId() {
245
+ return this.botParticipantId;
246
+ }
247
+ // ── internals ────────────────────────────────────────────────────────────────────
248
+ /** Wires the SDK's inbound raw-audio callback to a diarized inbound {@link BridgeMediaFrame}. */
249
+ wireInboundAudio(sdk) {
250
+ sdk.onAudioFrame((frame) => {
251
+ this.mediaHandler?.({
252
+ Track: 'audio-in',
253
+ Bytes: frame.Pcm,
254
+ SpeakerLabel: frame.ParticipantId,
255
+ TimestampMs: frame.TimestampMs ?? Date.now(),
256
+ });
257
+ });
258
+ }
259
+ /**
260
+ * Wires the SDK's member join/leave + hand-raise streams ONCE (the driver is the single owner;
261
+ * the SDK seam is latest-handler-wins). Each roster change fans out to both the driver's own
262
+ * roster-change handler and the Meeting Controls source; hand-raise signals (⚠️ partial on Webex)
263
+ * feed the source.
264
+ */
265
+ wireRoster(sdk) {
266
+ sdk.onParticipantJoin(() => void this.refreshRoster(sdk));
267
+ sdk.onParticipantLeave(() => void this.refreshRoster(sdk));
268
+ sdk.onHandRaise((id, raised) => this.meetingControls?.IngestHandRaise(id, raised));
269
+ }
270
+ /** Pulls the current roster from the SDK and fans it out to both the participant handler + Meeting Controls. */
271
+ async refreshRoster(sdk) {
272
+ try {
273
+ const participants = await sdk.getParticipants();
274
+ this.participantHandler?.(participants.map(toBridgeParticipant));
275
+ this.meetingControls?.IngestRoster(participants);
276
+ }
277
+ catch (err) {
278
+ LogError(`[WebexBridge] roster refresh failed: ${err instanceof Error ? err.message : String(err)}`);
279
+ }
280
+ }
281
+ /** Handles the SDK's meeting-ended signal: surface an empty roster so the engine sees everyone gone. */
282
+ handleMeetingEnded() {
283
+ this.participantHandler?.([]);
284
+ this.meetingControls?.IngestRoster([]);
285
+ }
286
+ /** Builds the SDK join args from the bridge context (Address is the meeting link). */
287
+ buildJoinArgs(ctx) {
288
+ const config = ctx.Configuration ?? {};
289
+ return {
290
+ MeetingLink: ctx.Address,
291
+ MeetingNumber: typeof config.MeetingNumber === 'string' ? config.MeetingNumber : this.parseMeetingNumber(ctx.Address),
292
+ BotDisplayName: typeof config.BotDisplayName === 'string' ? config.BotDisplayName : `${ctx.ProviderName} Agent`,
293
+ AccessToken: typeof config.AccessToken === 'string' ? config.AccessToken : undefined,
294
+ SiteUrl: typeof config.SiteUrl === 'string' ? config.SiteUrl : undefined,
295
+ };
296
+ }
297
+ /**
298
+ * Extracts the Webex meeting number from a meeting link when present, else `undefined`. Webex meeting
299
+ * numbers are 9–11 digit strings carried in the join URL (e.g. as a `MTID`/`/j.php?MTID=...` query
300
+ * param or a trailing numeric segment of a personal-room / scheduled link). Tolerant — a non-matching
301
+ * address yields `undefined` so the SDK can resolve coordinates itself.
302
+ */
303
+ parseMeetingNumber(address) {
304
+ if (!address) {
305
+ return undefined;
306
+ }
307
+ const decoded = (() => {
308
+ try {
309
+ return decodeURIComponent(address);
310
+ }
311
+ catch {
312
+ return address;
313
+ }
314
+ })();
315
+ // A 9–11 digit run is the Webex meeting number; prefer the longest match in the link.
316
+ const matches = decoded.match(/\d{9,11}/g);
317
+ if (!matches || matches.length === 0) {
318
+ return undefined;
319
+ }
320
+ return matches.reduce((longest, cur) => (cur.length > longest.length ? cur : longest));
321
+ }
322
+ /** Returns the PCM bytes of an outbound audio frame, preferring the binary payload. */
323
+ framePcm(frame) {
324
+ if (frame.Bytes) {
325
+ return frame.Bytes;
326
+ }
327
+ if (frame.Base64) {
328
+ return this.decodeBase64(frame.Base64);
329
+ }
330
+ return undefined;
331
+ }
332
+ /** Decodes a base64 payload to an `ArrayBuffer` (Node `Buffer` fast path, `atob` fallback). */
333
+ decodeBase64(base64) {
334
+ if (typeof Buffer !== 'undefined') {
335
+ const buf = Buffer.from(base64, 'base64');
336
+ // Copy into a fresh ArrayBuffer (Buffer.buffer is ArrayBufferLike under newer @types/node).
337
+ const out = new Uint8Array(buf.byteLength);
338
+ out.set(buf);
339
+ return out.buffer;
340
+ }
341
+ const binary = atob(base64);
342
+ const bytes = new Uint8Array(binary.length);
343
+ for (let i = 0; i < binary.length; i++) {
344
+ bytes[i] = binary.charCodeAt(i);
345
+ }
346
+ return bytes.buffer;
347
+ }
348
+ };
349
+ WebexBridge = __decorate([
350
+ RegisterClass(BaseRealtimeBridge, WEBEX_BRIDGE_DRIVER_CLASS)
351
+ ], WebexBridge);
352
+ export { WebexBridge };
353
+ /**
354
+ * Tree-shaking-prevention loader. Modern bundlers cannot see the `@RegisterClass` dynamic registration
355
+ * of {@link WebexBridge} and may eliminate it. Import and call this no-op from a static code path
356
+ * (the package entry point does) so the `ClassFactory` can resolve `'WebexBridge'`.
357
+ */
358
+ export function LoadWebexBridge() {
359
+ // Intentionally empty — referencing the module is what prevents tree-shaking.
360
+ }
361
+ //# sourceMappingURL=webex-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webex-bridge.js","sourceRoot":"","sources":["../src/webex-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Adapts the {@link IWebexMeetingSdk} member / hand-raise / speaking stream into an
3
+ * {@link IBridgeMeetingControlsEventSource} so the bridge engine can wire the **Meeting Controls**
4
+ * facilitator channel (roster · hand-raise queue · who's-speaking · mute) for a Webex session. This is
5
+ * the §4b "the bridge contributes a channel" pattern realized for Webex — no channel logic lives here,
6
+ * only the platform→channel signal mapping.
7
+ */
8
+ import { IBridgeMeetingControlsEventSource, BridgeMeetingParticipant, BridgeMeetingControlsCapability } from '@memberjunction/ai-bridge-base';
9
+ import { IWebexMeetingSdk, WebexParticipant } from './webex-sdk.js';
10
+ /** Maps one Webex member onto the channel's {@link BridgeMeetingParticipant} shape. */
11
+ export declare function toMeetingParticipant(p: WebexParticipant): BridgeMeetingParticipant;
12
+ /**
13
+ * Webex's adapter to the Meeting Controls channel's event source. It maintains a live roster and
14
+ * mirrors native hand-raise + diarized speaking signals, and actuates mute through the SDK.
15
+ *
16
+ * **The driver owns the SDK subscriptions** (the SDK seam is "latest handler wins", so it can have
17
+ * only one subscriber per event) and **feeds this source imperatively** via {@link IngestRoster} /
18
+ * {@link IngestHandRaise} / {@link IngestSpeaking}. This source only needs the SDK for the one *action*
19
+ * it actuates — {@link MuteParticipant}. The Meeting Controls channel subscribes the three `On*`
20
+ * streams in turn.
21
+ *
22
+ * The driver constructs ONE of these per session (only when the provider has `SpeakerDiarization`, i.e.
23
+ * there is a roster to facilitate) and returns it from `WebexBridge.GetMeetingControlsEventSource`.
24
+ */
25
+ export declare class WebexMeetingControlsEventSource implements IBridgeMeetingControlsEventSource {
26
+ private readonly sdk;
27
+ /** Live roster keyed by member id (lowercased), kept current by the driver's {@link IngestRoster}. */
28
+ private readonly roster;
29
+ private rosterHandler?;
30
+ private speakingHandler?;
31
+ private handRaiseHandler?;
32
+ /** Webex supports host/cohost-driven mute, so the facilitator `MuteParticipant` tool is offered. */
33
+ readonly Capabilities: ReadonlyArray<BridgeMeetingControlsCapability>;
34
+ /**
35
+ * @param sdk The Webex SDK seam used only to actuate {@link MuteParticipant} — all *perception* is
36
+ * fed in by the driver via the `Ingest*` methods so the SDK keeps a single subscriber per event.
37
+ */
38
+ constructor(sdk: IWebexMeetingSdk);
39
+ /**
40
+ * Replaces the roster with a fresh snapshot from the driver and emits it to the channel. Called by
41
+ * the driver after the initial join and on every member join/leave.
42
+ *
43
+ * @param participants The full current Webex roster.
44
+ */
45
+ IngestRoster(participants: WebexParticipant[]): void;
46
+ /**
47
+ * Forwards a native hand-raise/lower signal from the driver to the channel.
48
+ *
49
+ * ⚠️ Webex raised-hand is partial; this may never fire on clients/builds that don't surface the
50
+ * event over the Meetings SDK. The channel degrades gracefully (its software hand-raise still works).
51
+ *
52
+ * @param participantId The member whose hand changed.
53
+ * @param raised Whether the hand is now raised.
54
+ */
55
+ IngestHandRaise(participantId: string, raised: boolean): void;
56
+ /**
57
+ * Forwards a diarized speaking-set change from the driver to the channel.
58
+ *
59
+ * @param participantIds The ids currently speaking.
60
+ */
61
+ IngestSpeaking(participantIds: string[]): void;
62
+ /**
63
+ * Convenience alias for {@link IngestSpeaking}, kept because "notify" reads naturally at the
64
+ * driver's inbound-audio call site.
65
+ *
66
+ * @param participantIds The ids currently speaking.
67
+ */
68
+ NotifySpeaking(participantIds: string[]): void;
69
+ /** @inheritdoc */
70
+ OnRosterChange(handler: (participants: BridgeMeetingParticipant[]) => void): void;
71
+ /** @inheritdoc */
72
+ OnSpeakingChange(handler: (participantIds: string[]) => void): void;
73
+ /** @inheritdoc */
74
+ OnHandRaiseChange(handler: (participantId: string, raised: boolean) => void): void;
75
+ /** @inheritdoc */
76
+ MuteParticipant(participantId: string): Promise<void>;
77
+ private emitRoster;
78
+ private key;
79
+ }
80
+ //# sourceMappingURL=webex-meeting-controls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webex-meeting-controls.d.ts","sourceRoot":"","sources":["../src/webex-meeting-controls.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACH,iCAAiC,EACjC,wBAAwB,EACxB,+BAA+B,EAElC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAqBvF,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,gBAAgB,GAAG,wBAAwB,CAOlF;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,+BAAgC,YAAW,iCAAiC;IAezE,OAAO,CAAC,QAAQ,CAAC,GAAG;IAdhC,sGAAsG;IACtG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+C;IAEtE,OAAO,CAAC,aAAa,CAAC,CAAqD;IAC3E,OAAO,CAAC,eAAe,CAAC,CAAqC;IAC7D,OAAO,CAAC,gBAAgB,CAAC,CAAmD;IAE5E,oGAAoG;IACpG,SAAgB,YAAY,EAAE,aAAa,CAAC,+BAA+B,CAAC,CAAY;IAExF;;;OAGG;gBAC0B,GAAG,EAAE,gBAAgB;IAIlD;;;;;OAKG;IACI,YAAY,CAAC,YAAY,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAQ3D;;;;;;;;OAQG;IACI,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAIpE;;;;OAIG;IACI,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI;IAIrD;;;;;OAKG;IACI,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI;IAMrD,kBAAkB;IACX,cAAc,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,wBAAwB,EAAE,KAAK,IAAI,GAAG,IAAI;IAMxF,kBAAkB;IACX,gBAAgB,CAAC,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,IAAI;IAI1E,kBAAkB;IACX,iBAAiB,CAAC,OAAO,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAIzF,kBAAkB;IACL,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlE,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,GAAG;CAGd"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Adapts the {@link IWebexMeetingSdk} member / hand-raise / speaking stream into an
3
+ * {@link IBridgeMeetingControlsEventSource} so the bridge engine can wire the **Meeting Controls**
4
+ * facilitator channel (roster · hand-raise queue · who's-speaking · mute) for a Webex session. This is
5
+ * the §4b "the bridge contributes a channel" pattern realized for Webex — no channel logic lives here,
6
+ * only the platform→channel signal mapping.
7
+ */
8
+ /**
9
+ * Maps a Webex member role to the bridge's meeting-participant role. Webex has no distinct "Agent"
10
+ * role; the bot is flagged via `IsSelf` and surfaced as `'Agent'`. Webex hosts/cohosts map to the
11
+ * bridge's host/co-host roles (the closest facilitator-authority equivalents).
12
+ */
13
+ function mapRole(role, isSelf) {
14
+ if (isSelf) {
15
+ return 'Agent';
16
+ }
17
+ switch (role) {
18
+ case 'Host':
19
+ return 'Host';
20
+ case 'Cohost':
21
+ return 'CoHost';
22
+ default:
23
+ return 'Participant';
24
+ }
25
+ }
26
+ /** Maps one Webex member onto the channel's {@link BridgeMeetingParticipant} shape. */
27
+ export function toMeetingParticipant(p) {
28
+ return {
29
+ ParticipantId: p.ParticipantId,
30
+ DisplayName: p.DisplayName,
31
+ Role: mapRole(p.Role, p.IsSelf),
32
+ IsAgent: p.IsSelf === true,
33
+ };
34
+ }
35
+ /**
36
+ * Webex's adapter to the Meeting Controls channel's event source. It maintains a live roster and
37
+ * mirrors native hand-raise + diarized speaking signals, and actuates mute through the SDK.
38
+ *
39
+ * **The driver owns the SDK subscriptions** (the SDK seam is "latest handler wins", so it can have
40
+ * only one subscriber per event) and **feeds this source imperatively** via {@link IngestRoster} /
41
+ * {@link IngestHandRaise} / {@link IngestSpeaking}. This source only needs the SDK for the one *action*
42
+ * it actuates — {@link MuteParticipant}. The Meeting Controls channel subscribes the three `On*`
43
+ * streams in turn.
44
+ *
45
+ * The driver constructs ONE of these per session (only when the provider has `SpeakerDiarization`, i.e.
46
+ * there is a roster to facilitate) and returns it from `WebexBridge.GetMeetingControlsEventSource`.
47
+ */
48
+ export class WebexMeetingControlsEventSource {
49
+ /**
50
+ * @param sdk The Webex SDK seam used only to actuate {@link MuteParticipant} — all *perception* is
51
+ * fed in by the driver via the `Ingest*` methods so the SDK keeps a single subscriber per event.
52
+ */
53
+ constructor(sdk) {
54
+ this.sdk = sdk;
55
+ /** Live roster keyed by member id (lowercased), kept current by the driver's {@link IngestRoster}. */
56
+ this.roster = new Map();
57
+ /** Webex supports host/cohost-driven mute, so the facilitator `MuteParticipant` tool is offered. */
58
+ this.Capabilities = ['Mute'];
59
+ }
60
+ // ── Driver-fed perception (the driver owns the SDK subscriptions) ─────────────────
61
+ /**
62
+ * Replaces the roster with a fresh snapshot from the driver and emits it to the channel. Called by
63
+ * the driver after the initial join and on every member join/leave.
64
+ *
65
+ * @param participants The full current Webex roster.
66
+ */
67
+ IngestRoster(participants) {
68
+ this.roster.clear();
69
+ for (const p of participants) {
70
+ this.roster.set(this.key(p.ParticipantId), toMeetingParticipant(p));
71
+ }
72
+ this.emitRoster();
73
+ }
74
+ /**
75
+ * Forwards a native hand-raise/lower signal from the driver to the channel.
76
+ *
77
+ * ⚠️ Webex raised-hand is partial; this may never fire on clients/builds that don't surface the
78
+ * event over the Meetings SDK. The channel degrades gracefully (its software hand-raise still works).
79
+ *
80
+ * @param participantId The member whose hand changed.
81
+ * @param raised Whether the hand is now raised.
82
+ */
83
+ IngestHandRaise(participantId, raised) {
84
+ this.handRaiseHandler?.(participantId, raised);
85
+ }
86
+ /**
87
+ * Forwards a diarized speaking-set change from the driver to the channel.
88
+ *
89
+ * @param participantIds The ids currently speaking.
90
+ */
91
+ IngestSpeaking(participantIds) {
92
+ this.speakingHandler?.(participantIds);
93
+ }
94
+ /**
95
+ * Convenience alias for {@link IngestSpeaking}, kept because "notify" reads naturally at the
96
+ * driver's inbound-audio call site.
97
+ *
98
+ * @param participantIds The ids currently speaking.
99
+ */
100
+ NotifySpeaking(participantIds) {
101
+ this.IngestSpeaking(participantIds);
102
+ }
103
+ // ── IBridgeMeetingControlsEventSource ────────────────────────────────────────────
104
+ /** @inheritdoc */
105
+ OnRosterChange(handler) {
106
+ this.rosterHandler = handler;
107
+ // Emit the current roster immediately so a late subscriber is not blank.
108
+ this.emitRoster();
109
+ }
110
+ /** @inheritdoc */
111
+ OnSpeakingChange(handler) {
112
+ this.speakingHandler = handler;
113
+ }
114
+ /** @inheritdoc */
115
+ OnHandRaiseChange(handler) {
116
+ this.handRaiseHandler = handler;
117
+ }
118
+ /** @inheritdoc */
119
+ async MuteParticipant(participantId) {
120
+ await this.sdk.muteParticipant(participantId);
121
+ }
122
+ // ── internals ────────────────────────────────────────────────────────────────────
123
+ emitRoster() {
124
+ this.rosterHandler?.(Array.from(this.roster.values()));
125
+ }
126
+ key(participantId) {
127
+ return participantId.trim().toLowerCase();
128
+ }
129
+ }
130
+ //# sourceMappingURL=webex-meeting-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webex-meeting-controls.js","sourceRoot":"","sources":["../src/webex-meeting-controls.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH;;;;GAIG;AACH,SAAS,OAAO,CAAC,IAA0B,EAAE,MAA2B;IACpE,IAAI,MAAM,EAAE,CAAC;QACT,OAAO,OAAO,CAAC;IACnB,CAAC;IACD,QAAQ,IAAI,EAAE,CAAC;QACX,KAAK,MAAM;YACP,OAAO,MAAM,CAAC;QAClB,KAAK,QAAQ;YACT,OAAO,QAAQ,CAAC;QACpB;YACI,OAAO,aAAa,CAAC;IAC7B,CAAC;AACL,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,oBAAoB,CAAC,CAAmB;IACpD,OAAO;QACH,aAAa,EAAE,CAAC,CAAC,aAAa;QAC9B,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,KAAK,IAAI;KAC7B,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,+BAA+B;IAWxC;;;OAGG;IACH,YAA6B,GAAqB;QAArB,QAAG,GAAH,GAAG,CAAkB;QAdlD,sGAAsG;QACrF,WAAM,GAAG,IAAI,GAAG,EAAoC,CAAC;QAMtE,oGAAoG;QACpF,iBAAY,GAAmD,CAAC,MAAM,CAAC,CAAC;IAMnC,CAAC;IAEtD,qFAAqF;IAErF;;;;;OAKG;IACI,YAAY,CAAC,YAAgC;QAChD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;IACtB,CAAC;IAED;;;;;;;;OAQG;IACI,eAAe,CAAC,aAAqB,EAAE,MAAe;QACzD,IAAI,CAAC,gBAAgB,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IACnD,CAAC;IAED;;;;OAIG;IACI,cAAc,CAAC,cAAwB;QAC1C,IAAI,CAAC,eAAe,EAAE,CAAC,cAAc,CAAC,CAAC;IAC3C,CAAC;IAED;;;;;OAKG;IACI,cAAc,CAAC,cAAwB;QAC1C,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IACxC,CAAC;IAED,oFAAoF;IAEpF,kBAAkB;IACX,cAAc,CAAC,OAA2D;QAC7E,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;QAC7B,yEAAyE;QACzE,IAAI,CAAC,UAAU,EAAE,CAAC;IACtB,CAAC;IAED,kBAAkB;IACX,gBAAgB,CAAC,OAA2C;QAC/D,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC;IACnC,CAAC;IAED,kBAAkB;IACX,iBAAiB,CAAC,OAAyD;QAC9E,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC;IACpC,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,eAAe,CAAC,aAAqB;QAC9C,MAAM,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;IAClD,CAAC;IAED,oFAAoF;IAE5E,UAAU;QACd,IAAI,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;IAEO,GAAG,CAAC,aAAqB;QAC7B,OAAO,aAAa,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,CAAC;CACJ"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The injectable **Cisco Webex Meetings SDK seam** — the minimal set of operations
3
+ * {@link import('./webex-bridge.js').WebexBridge} needs from Webex, declared as an interface so the driver
4
+ * builds and unit-tests against an in-memory fake with **no network and no real Cisco Webex Meetings SDK
5
+ * / xAPI / Webex bot framework**.
6
+ *
7
+ * ## Production binding (TODO at deployment)
8
+ * In production this interface is bound to the **Cisco Webex Meetings SDK** (the embedded-app / Web /
9
+ * mobile Meetings SDK that joins a meeting and exposes media + roster) together with the **Webex bot
10
+ * framework** (Webex Messaging API, for posting to the meeting/space) and, where a Webex room device is
11
+ * involved, the **xAPI** (the device control surface for participant actions). The named operations map
12
+ * to the SDK as follows:
13
+ * - {@link join} / {@link leave} → the Webex Meetings SDK `Meeting.join()` / `Meeting.leave()` lifecycle
14
+ * (the bot/app joins via the meeting link, SIP address, or meeting number).
15
+ * - {@link sendAudioFrame} → the Meetings SDK outbound audio media track (the bot's voice into the meeting).
16
+ * - {@link onAudioFrame} → the Meetings SDK per-participant inbound audio media callback (the source of
17
+ * speaker labels for diarization — Webex attributes media to a `Member`).
18
+ * - {@link onParticipantJoin} / {@link onParticipantLeave} / {@link getParticipants} → the meeting's
19
+ * `members` collection + the `members:update` change events.
20
+ * - {@link onHandRaise} → the Webex raised-hand reaction. ⚠️ Partial: Webex surfaces raised hands in the
21
+ * meeting UI, but a clean per-participant *event* over the Meetings SDK is not guaranteed on every
22
+ * client/build; the adapter wires it where the platform exposes it and otherwise no-ops.
23
+ * - {@link postChatMessage} → the Webex Messaging API `POST /messages` against the meeting's associated
24
+ * space (the in-meeting chat thread).
25
+ * - {@link muteParticipant} → the meeting's `Member.mute` action / device xAPI mute (requires the bot be
26
+ * a host / cohost with the relevant privilege).
27
+ * - {@link onMeetingEnded} → the meeting's `meeting:ended` / `state` → `ENDED` notification.
28
+ *
29
+ * Binding the real SDK is a thin adapter that implements this interface; the driver and its tests do
30
+ * not change. **None of the Webex SDK types leak into this package.**
31
+ *
32
+ * See `/plans/realtime/realtime-bridges-architecture.md` §3, §8 (Cisco Webex row) and
33
+ * `/guides/REALTIME_BRIDGES_GUIDE.md`.
34
+ */
35
+ /** The role a Webex member holds in the meeting, normalized to the bridge's participant roles. */
36
+ export type WebexParticipantRole = 'Host' | 'Cohost' | 'Attendee';
37
+ /**
38
+ * One Webex meeting member as the seam reports it. Platform-native and minimal — the driver maps
39
+ * this onto `BridgeParticipantInfo` / `BridgeMeetingParticipant`.
40
+ */
41
+ export interface WebexParticipant {
42
+ /** The Webex member id (the meeting `Member.id`, stable for their presence). */
43
+ ParticipantId: string;
44
+ /** The member's display name as Webex reports it. */
45
+ DisplayName?: string;
46
+ /** The member's meeting role. */
47
+ Role: WebexParticipantRole;
48
+ /** Whether this member is the bridge's own bot (so the driver can exclude it from diarization addressing). */
49
+ IsSelf?: boolean;
50
+ }
51
+ /** One frame of raw per-participant audio the seam surfaces for diarization + the agent's "hearing". */
52
+ export interface WebexAudioFrame {
53
+ /** Raw PCM audio bytes for this frame. */
54
+ Pcm: ArrayBuffer;
55
+ /** The Webex member id this audio came from (the diarization speaker label). */
56
+ ParticipantId: string;
57
+ /** The member's display name at capture time, when known. */
58
+ DisplayName?: string;
59
+ /** Optional epoch-ms capture timestamp. */
60
+ TimestampMs?: number;
61
+ }
62
+ /** Arguments to {@link IWebexMeetingSdk.join} — what the bot needs to get into a Webex meeting. */
63
+ export interface WebexJoinArgs {
64
+ /** The Webex meeting link (the `https://*.webex.com/meet/...` / `.../join/...` URL). */
65
+ MeetingLink: string;
66
+ /**
67
+ * The parsed Webex meeting number / SIP address (the durable meeting coordinate behind the link),
68
+ * when resolved upstream. Used as a stable meeting identifier when the link is opaque.
69
+ */
70
+ MeetingNumber?: string;
71
+ /** The display name the bot appears as in the participant list. */
72
+ BotDisplayName: string;
73
+ /**
74
+ * The OAuth bearer / bot access token authorizing the bot's Webex Meetings + Messaging calls.
75
+ * Resolved upstream via MJ's credential system; never inline secrets.
76
+ */
77
+ AccessToken?: string;
78
+ /** The Webex site (org) the meeting belongs to, when joining cross-site. */
79
+ SiteUrl?: string;
80
+ }
81
+ /** The handles the seam returns after a successful {@link IWebexMeetingSdk.join}. */
82
+ export interface WebexJoinResult {
83
+ /** The bot's own member id in the joined meeting. */
84
+ BotParticipantId: string;
85
+ /** The Webex meeting id the bot joined (the durable external connection id). */
86
+ MeetingId: string;
87
+ }
88
+ /**
89
+ * The minimal Cisco Webex Meetings SDK surface the {@link import('./webex-bridge.js').WebexBridge}
90
+ * depends on. Production binds this to the real Webex Meetings SDK / Webex bot framework; tests
91
+ * inject a `FakeWebexSdk`.
92
+ */
93
+ export interface IWebexMeetingSdk {
94
+ /**
95
+ * Joins the Webex meeting and brings the bot online. Returns the bot's member id + the meeting id.
96
+ *
97
+ * @param args Join parameters (meeting link, meeting number, bot name, auth).
98
+ * @returns The bot member + meeting handles.
99
+ */
100
+ join(args: WebexJoinArgs): Promise<WebexJoinResult>;
101
+ /** Leaves the meeting (the bot's `Meeting.leave()`) and releases SDK resources. */
102
+ leave(): Promise<void>;
103
+ /**
104
+ * Sends one raw PCM audio frame as the bot's outbound audio (the agent's voice into the meeting),
105
+ * over the Webex Meetings SDK outbound audio media track.
106
+ *
107
+ * @param pcm The PCM audio bytes to send.
108
+ */
109
+ sendAudioFrame(pcm: ArrayBuffer): void;
110
+ /**
111
+ * Registers a callback for inbound raw per-participant audio frames (what the agent hears, carrying
112
+ * the speaker label for diarization). "Latest handler wins."
113
+ *
114
+ * @param cb Invoked with each inbound audio frame.
115
+ */
116
+ onAudioFrame(cb: (frame: WebexAudioFrame) => void): void;
117
+ /**
118
+ * Registers a callback fired when a member joins. "Latest handler wins."
119
+ *
120
+ * @param cb Invoked with the member who joined.
121
+ */
122
+ onParticipantJoin(cb: (participant: WebexParticipant) => void): void;
123
+ /**
124
+ * Registers a callback fired when a member leaves. "Latest handler wins."
125
+ *
126
+ * @param cb Invoked with the member id that left.
127
+ */
128
+ onParticipantLeave(cb: (participantId: string) => void): void;
129
+ /**
130
+ * Registers a callback for native raised-hand signals. "Latest handler wins."
131
+ *
132
+ * ⚠️ Webex raised-hand is partial over the Meetings SDK: a clean per-participant event is not
133
+ * guaranteed on every client/build. The adapter wires this where the platform exposes it and the
134
+ * driver tolerates the signal never firing.
135
+ *
136
+ * @param cb Invoked with the member id and whether the hand is now raised.
137
+ */
138
+ onHandRaise(cb: (participantId: string, raised: boolean) => void): void;
139
+ /**
140
+ * Returns the current member roster (including the bot).
141
+ *
142
+ * @returns The current members.
143
+ */
144
+ getParticipants(): Promise<WebexParticipant[]>;
145
+ /**
146
+ * Posts a message to the Webex meeting space chat (everyone).
147
+ *
148
+ * @param text The chat message text.
149
+ */
150
+ postChatMessage(text: string): Promise<void>;
151
+ /**
152
+ * Mutes a member (requires the bot be a host/cohost with the relevant privilege).
153
+ *
154
+ * @param participantId The member to mute.
155
+ */
156
+ muteParticipant(participantId: string): Promise<void>;
157
+ /**
158
+ * Registers a callback fired when the meeting ends (host ended / meeting state → ENDED).
159
+ * "Latest handler wins."
160
+ *
161
+ * @param cb Invoked when the meeting has ended.
162
+ */
163
+ onMeetingEnded(cb: () => void): void;
164
+ }
165
+ /**
166
+ * A factory that constructs an {@link IWebexMeetingSdk} for a session — the creation seam (mirroring
167
+ * Gemini's `connectLiveSession`). Production supplies a factory that builds the real Webex Meetings SDK /
168
+ * Webex bot adapter from resolved config; tests supply one that returns a `FakeWebexSdk`.
169
+ *
170
+ * @param config The resolved provider/session configuration (site, credential refs already resolved upstream).
171
+ * @returns The Webex SDK instance to drive the meeting with.
172
+ */
173
+ export type WebexMeetingSdkFactory = (config?: Record<string, unknown>) => IWebexMeetingSdk;
174
+ //# sourceMappingURL=webex-sdk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webex-sdk.d.ts","sourceRoot":"","sources":["../src/webex-sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,kGAAkG;AAClG,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC7B,gFAAgF;IAChF,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,8GAA8G;IAC9G,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wGAAwG;AACxG,MAAM,WAAW,eAAe;IAC5B,0CAA0C;IAC1C,GAAG,EAAE,WAAW,CAAC;IACjB,gFAAgF;IAChF,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,mGAAmG;AACnG,MAAM,WAAW,aAAa;IAC1B,wFAAwF;IACxF,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC5B,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;;;;OAKG;IACH,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAEpD,mFAAmF;IACnF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;;;;OAKG;IACH,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAAC;IAEvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI,CAAC;IAEzD;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,EAAE,CAAC,WAAW,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI,CAAC;IAErE;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAE9D;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IAExE;;;;OAIG;IACH,eAAe,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAE/C;;;;OAIG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7C;;;;OAIG;IACH,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtD;;;;;OAKG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,gBAAgB,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The injectable **Cisco Webex Meetings SDK seam** — the minimal set of operations
3
+ * {@link import('./webex-bridge.js').WebexBridge} needs from Webex, declared as an interface so the driver
4
+ * builds and unit-tests against an in-memory fake with **no network and no real Cisco Webex Meetings SDK
5
+ * / xAPI / Webex bot framework**.
6
+ *
7
+ * ## Production binding (TODO at deployment)
8
+ * In production this interface is bound to the **Cisco Webex Meetings SDK** (the embedded-app / Web /
9
+ * mobile Meetings SDK that joins a meeting and exposes media + roster) together with the **Webex bot
10
+ * framework** (Webex Messaging API, for posting to the meeting/space) and, where a Webex room device is
11
+ * involved, the **xAPI** (the device control surface for participant actions). The named operations map
12
+ * to the SDK as follows:
13
+ * - {@link join} / {@link leave} → the Webex Meetings SDK `Meeting.join()` / `Meeting.leave()` lifecycle
14
+ * (the bot/app joins via the meeting link, SIP address, or meeting number).
15
+ * - {@link sendAudioFrame} → the Meetings SDK outbound audio media track (the bot's voice into the meeting).
16
+ * - {@link onAudioFrame} → the Meetings SDK per-participant inbound audio media callback (the source of
17
+ * speaker labels for diarization — Webex attributes media to a `Member`).
18
+ * - {@link onParticipantJoin} / {@link onParticipantLeave} / {@link getParticipants} → the meeting's
19
+ * `members` collection + the `members:update` change events.
20
+ * - {@link onHandRaise} → the Webex raised-hand reaction. ⚠️ Partial: Webex surfaces raised hands in the
21
+ * meeting UI, but a clean per-participant *event* over the Meetings SDK is not guaranteed on every
22
+ * client/build; the adapter wires it where the platform exposes it and otherwise no-ops.
23
+ * - {@link postChatMessage} → the Webex Messaging API `POST /messages` against the meeting's associated
24
+ * space (the in-meeting chat thread).
25
+ * - {@link muteParticipant} → the meeting's `Member.mute` action / device xAPI mute (requires the bot be
26
+ * a host / cohost with the relevant privilege).
27
+ * - {@link onMeetingEnded} → the meeting's `meeting:ended` / `state` → `ENDED` notification.
28
+ *
29
+ * Binding the real SDK is a thin adapter that implements this interface; the driver and its tests do
30
+ * not change. **None of the Webex SDK types leak into this package.**
31
+ *
32
+ * See `/plans/realtime/realtime-bridges-architecture.md` §3, §8 (Cisco Webex row) and
33
+ * `/guides/REALTIME_BRIDGES_GUIDE.md`.
34
+ */
35
+ export {};
36
+ //# sourceMappingURL=webex-sdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webex-sdk.js","sourceRoot":"","sources":["../src/webex-sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG"}
package/package.json CHANGED
@@ -1,10 +1,35 @@
1
1
  {
2
2
  "name": "@memberjunction/ai-bridge-webex",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @memberjunction/ai-bridge-webex",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "type": "module",
4
+ "version": "5.41.0",
5
+ "description": "MemberJunction: Cisco Webex Realtime Bridge driver. Connects the realtime agent engine to a Webex meeting (audio in/out, diarized roster, participant mute, Webex meeting chat) via an injectable Cisco Webex Meetings SDK seam (Webex Meetings SDK / xAPI / Webex bot framework), and contributes a Meeting Controls facilitator channel.",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
11
+ "scripts": {
12
+ "start": "ts-node-dev src/index.ts",
13
+ "build": "tsc && tsc-alias -f",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest"
16
+ },
17
+ "author": "MemberJunction.com",
18
+ "license": "ISC",
19
+ "dependencies": {
20
+ "@memberjunction/core": "5.41.0",
21
+ "@memberjunction/global": "5.41.0",
22
+ "@memberjunction/core-entities": "5.41.0",
23
+ "@memberjunction/ai-bridge-base": "5.41.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "24.10.11",
27
+ "ts-node-dev": "^2.0.0",
28
+ "typescript": "^5.9.3",
29
+ "vitest": "^4.0.18"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/MemberJunction/MJ"
34
+ }
10
35
  }