@framers/agentos 0.1.111 → 0.1.112

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.
Files changed (44) hide show
  1. package/dist/voice/CallManager.d.ts.map +1 -1
  2. package/dist/voice/CallManager.js +9 -1
  3. package/dist/voice/CallManager.js.map +1 -1
  4. package/dist/voice/MediaStreamParser.d.ts +115 -6
  5. package/dist/voice/MediaStreamParser.d.ts.map +1 -1
  6. package/dist/voice/MediaStreamParser.js +44 -0
  7. package/dist/voice/MediaStreamParser.js.map +1 -1
  8. package/dist/voice/TelephonyStreamTransport.d.ts +112 -20
  9. package/dist/voice/TelephonyStreamTransport.d.ts.map +1 -1
  10. package/dist/voice/TelephonyStreamTransport.js +136 -30
  11. package/dist/voice/TelephonyStreamTransport.js.map +1 -1
  12. package/dist/voice/parsers/PlivoMediaStreamParser.d.ts +64 -6
  13. package/dist/voice/parsers/PlivoMediaStreamParser.d.ts.map +1 -1
  14. package/dist/voice/parsers/PlivoMediaStreamParser.js +67 -6
  15. package/dist/voice/parsers/PlivoMediaStreamParser.js.map +1 -1
  16. package/dist/voice/parsers/TelnyxMediaStreamParser.d.ts +55 -8
  17. package/dist/voice/parsers/TelnyxMediaStreamParser.d.ts.map +1 -1
  18. package/dist/voice/parsers/TelnyxMediaStreamParser.js +60 -9
  19. package/dist/voice/parsers/TelnyxMediaStreamParser.js.map +1 -1
  20. package/dist/voice/parsers/TwilioMediaStreamParser.d.ts +73 -11
  21. package/dist/voice/parsers/TwilioMediaStreamParser.d.ts.map +1 -1
  22. package/dist/voice/parsers/TwilioMediaStreamParser.js +81 -12
  23. package/dist/voice/parsers/TwilioMediaStreamParser.js.map +1 -1
  24. package/dist/voice/providers/plivo.d.ts +108 -12
  25. package/dist/voice/providers/plivo.d.ts.map +1 -1
  26. package/dist/voice/providers/plivo.js +106 -9
  27. package/dist/voice/providers/plivo.js.map +1 -1
  28. package/dist/voice/providers/telnyx.d.ts +110 -20
  29. package/dist/voice/providers/telnyx.d.ts.map +1 -1
  30. package/dist/voice/providers/telnyx.js +111 -20
  31. package/dist/voice/providers/telnyx.js.map +1 -1
  32. package/dist/voice/providers/twilio.d.ts +91 -13
  33. package/dist/voice/providers/twilio.d.ts.map +1 -1
  34. package/dist/voice/providers/twilio.js +94 -14
  35. package/dist/voice/providers/twilio.js.map +1 -1
  36. package/dist/voice/twiml.d.ts +70 -12
  37. package/dist/voice/twiml.d.ts.map +1 -1
  38. package/dist/voice/twiml.js +70 -12
  39. package/dist/voice/twiml.js.map +1 -1
  40. package/dist/voice/types.d.ts +142 -15
  41. package/dist/voice/types.d.ts.map +1 -1
  42. package/dist/voice/types.js +34 -3
  43. package/dist/voice/types.js.map +1 -1
  44. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TelnyxMediaStreamParser.d.ts","sourceRoot":"","sources":["../../../src/voice/parsers/TelnyxMediaStreamParser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEtF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,iBAAiB;IAC/D;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAyDhE;;;;;;;;;OASG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAIzD;;;;OAIG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAG1C"}
1
+ {"version":3,"file":"TelnyxMediaStreamParser.d.ts","sourceRoot":"","sources":["../../../src/voice/parsers/TelnyxMediaStreamParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEtF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,iBAAiB;IAC/D;;;;;;;;;;;;;;;;;OAiBG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,mBAAmB,GAAG,IAAI;IA6DhE;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAIzD;;;;;;;;OAQG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAG1C"}
@@ -1,14 +1,51 @@
1
+ /**
2
+ * @fileoverview Telnyx media stream WebSocket parser.
3
+ *
4
+ * ## Telnyx's asymmetric protocol
5
+ *
6
+ * Telnyx uses a fundamentally different approach than Twilio for inbound vs.
7
+ * outbound audio on the media stream WebSocket:
8
+ *
9
+ * - **Inbound** (phone -> server): JSON-encoded messages with `event`, `stream_id`,
10
+ * and `media.chunk` (base64 mu-law audio) fields.
11
+ * - **Outbound** (server -> phone): **Raw binary** WebSocket frames containing
12
+ * mu-law PCM bytes directly, with no JSON envelope whatsoever.
13
+ *
14
+ * This asymmetry means {@link formatOutgoing} returns the `Buffer` unchanged,
15
+ * while {@link parseIncoming} parses JSON and base64-decodes the audio payload.
16
+ *
17
+ * ## Field name mapping
18
+ *
19
+ * Telnyx uses snake_case field names that differ from Twilio's conventions.
20
+ * This parser normalises them to the shared {@link MediaStreamIncoming} shape:
21
+ *
22
+ * | Telnyx field | Normalised field |
23
+ * |----------------------|-------------------|
24
+ * | `stream_id` | `streamSid` |
25
+ * | `call_control_id` | `callSid` |
26
+ * | `media.chunk` | `payload` (Buffer)|
27
+ * | `media.track` | (used for filtering, not emitted) |
28
+ *
29
+ * ## DTMF limitation
30
+ *
31
+ * Telnyx does NOT deliver DTMF events over the media stream WebSocket.
32
+ * DTMF key-presses arrive as `call.dtmf.received` HTTP webhook events and
33
+ * must be handled by {@link TelnyxVoiceProvider.parseWebhookEvent} instead.
34
+ *
35
+ * @see {@link https://developers.telnyx.com/docs/voice/media-streaming}
36
+ * @module @framers/agentos/voice/parsers/TelnyxMediaStreamParser
37
+ */
1
38
  /**
2
39
  * Parses the Telnyx media stream WebSocket protocol.
3
40
  *
4
41
  * Telnyx sends JSON-encoded messages for stream lifecycle events (`start`,
5
- * `stop`) and audio chunks (`media`). Unlike Twilio, Telnyx does NOT deliver
6
- * DTMF events over the media stream WebSocket those arrive as HTTP webhooks
42
+ * `stop`) and audio chunks (`media`). Unlike Twilio, Telnyx does NOT deliver
43
+ * DTMF events over the media stream WebSocket -- those arrive as HTTP webhooks
7
44
  * to a separate endpoint and must be handled outside this parser.
8
45
  *
9
46
  * Outgoing audio is sent as a **raw binary Buffer** (mu-law PCM bytes without
10
47
  * any JSON envelope) because Telnyx accepts unframed binary WebSocket frames
11
- * directly. No explicit connection acknowledgment is needed after the
48
+ * directly. No explicit connection acknowledgment is needed after the
12
49
  * handshake.
13
50
  *
14
51
  * @see {@link https://developers.telnyx.com/docs/voice/media-streaming}
@@ -18,11 +55,15 @@ export class TelnyxMediaStreamParser {
18
55
  * Parse a raw WebSocket frame from Telnyx's media stream.
19
56
  *
20
57
  * Supported Telnyx event types:
21
- * - `start` stream established; `stream_id` maps to `streamSid`,
58
+ * - `start` -- stream established; `stream_id` maps to `streamSid`,
22
59
  * `call_control_id` maps to `callSid`.
23
- * - `media` audio chunk; `chunk` field contains base64-encoded mu-law
24
- * bytes; only `inbound` track frames are returned.
25
- * - `stop` — stream ended.
60
+ * - `media` -- audio chunk; `media.chunk` field contains base64-encoded mu-law
61
+ * bytes; only `inbound` track frames are returned (outbound echoes are
62
+ * discarded to prevent feedback loops).
63
+ * - `stop` -- stream ended (call terminated or stream explicitly closed).
64
+ *
65
+ * Any other event type (e.g., future Telnyx additions, DTMF attempts) is
66
+ * silently dropped by returning `null`.
26
67
  *
27
68
  * @param data - Raw WebSocket frame payload (JSON string or Buffer from Telnyx).
28
69
  * @returns Normalised {@link MediaStreamIncoming} event, or `null` for
@@ -38,12 +79,15 @@ export class TelnyxMediaStreamParser {
38
79
  return null;
39
80
  }
40
81
  const event = msg['event'];
82
+ // Telnyx uses `stream_id` where Twilio uses `streamSid`.
41
83
  const streamSid = msg['stream_id'];
42
84
  if (!event || !streamSid) {
43
85
  return null;
44
86
  }
45
87
  switch (event) {
46
88
  case 'start': {
89
+ // Telnyx uses `call_control_id` as the call-leg identifier,
90
+ // equivalent to Twilio's `callSid`.
47
91
  const callSid = msg['call_control_id'] ?? '';
48
92
  const result = {
49
93
  type: 'start',
@@ -56,10 +100,11 @@ export class TelnyxMediaStreamParser {
56
100
  const media = msg['media'];
57
101
  if (!media)
58
102
  return null;
59
- // Ignore outbound audio echoes from Telnyx.
103
+ // Ignore outbound audio echoes from Telnyx to prevent feedback.
60
104
  const track = media['track'];
61
105
  if (track === 'outbound')
62
106
  return null;
107
+ // Telnyx names its audio payload field `chunk` (not `payload` like Twilio).
63
108
  const chunk = media['chunk'];
64
109
  if (!chunk)
65
110
  return null;
@@ -81,7 +126,9 @@ export class TelnyxMediaStreamParser {
81
126
  /**
82
127
  * Encode mu-law audio for transmission back to Telnyx.
83
128
  *
84
- * Telnyx accepts raw binary WebSocket frames; no JSON wrapping is applied.
129
+ * Telnyx accepts raw binary WebSocket frames -- no JSON wrapping is needed.
130
+ * This is the key asymmetry in Telnyx's protocol: inbound is JSON, outbound
131
+ * is raw binary.
85
132
  *
86
133
  * @param audio - Raw mu-law PCM bytes to send to the caller.
87
134
  * @param _streamSid - Unused by Telnyx binary framing (accepted for interface
@@ -94,6 +141,10 @@ export class TelnyxMediaStreamParser {
94
141
  /**
95
142
  * No explicit connection acknowledgment is required by Telnyx.
96
143
  *
144
+ * Unlike Twilio, Telnyx does not need a `connected` handshake message
145
+ * before it starts sending media events.
146
+ *
147
+ * @param _streamSid - Unused (accepted for interface parity).
97
148
  * @returns Always `null`.
98
149
  */
99
150
  formatConnected(_streamSid) {
@@ -1 +1 @@
1
- {"version":3,"file":"TelnyxMediaStreamParser.js","sourceRoot":"","sources":["../../../src/voice/parsers/TelnyxMediaStreamParser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,uBAAuB;IAClC;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,IAAqB;QACjC,MAAM,GAAG,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAEpE,IAAI,GAA4B,CAAC;QACjC,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAuB,CAAC;QACjD,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAuB,CAAC;QAEzD,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,QAAQ,KAAK,EAAE,CAAC;YACd,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,OAAO,GAAI,GAAG,CAAC,iBAAiB,CAAwB,IAAI,EAAE,CAAC;gBACrE,MAAM,MAAM,GAAwB;oBAClC,IAAI,EAAE,OAAO;oBACb,SAAS;oBACT,OAAO;iBACR,CAAC;gBACF,OAAO,MAAM,CAAC;YAChB,CAAC;YAED,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAwC,CAAC;gBAClE,IAAI,CAAC,KAAK;oBAAE,OAAO,IAAI,CAAC;gBAExB,4CAA4C;gBAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAuB,CAAC;gBACnD,IAAI,KAAK,KAAK,UAAU;oBAAE,OAAO,IAAI,CAAC;gBAEtC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAuB,CAAC;gBACnD,IAAI,CAAC,KAAK;oBAAE,OAAO,IAAI,CAAC;gBAExB,MAAM,MAAM,GAAwB;oBAClC,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC;oBACrC,SAAS;iBACV,CAAC;gBACF,OAAO,MAAM,CAAC;YAChB,CAAC;YAED,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,MAAM,MAAM,GAAwB,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;gBAChE,OAAO,MAAM,CAAC;YAChB,CAAC;YAED;gBACE,OAAO,IAAI,CAAC;QAChB,CAAC;IACH,CAAC;IAED;;;;;;;;;OASG;IACH,cAAc,CAAC,KAAa,EAAE,UAAkB;QAC9C,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;OAIG;IACH,eAAe,CAAC,UAAkB;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
1
+ {"version":3,"file":"TelnyxMediaStreamParser.js","sourceRoot":"","sources":["../../../src/voice/parsers/TelnyxMediaStreamParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,uBAAuB;IAClC;;;;;;;;;;;;;;;;;OAiBG;IACH,aAAa,CAAC,IAAqB;QACjC,MAAM,GAAG,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAEpE,IAAI,GAA4B,CAAC;QACjC,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAuB,CAAC;QACjD,yDAAyD;QACzD,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAuB,CAAC;QAEzD,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,QAAQ,KAAK,EAAE,CAAC;YACd,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,4DAA4D;gBAC5D,oCAAoC;gBACpC,MAAM,OAAO,GAAI,GAAG,CAAC,iBAAiB,CAAwB,IAAI,EAAE,CAAC;gBACrE,MAAM,MAAM,GAAwB;oBAClC,IAAI,EAAE,OAAO;oBACb,SAAS;oBACT,OAAO;iBACR,CAAC;gBACF,OAAO,MAAM,CAAC;YAChB,CAAC;YAED,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAwC,CAAC;gBAClE,IAAI,CAAC,KAAK;oBAAE,OAAO,IAAI,CAAC;gBAExB,gEAAgE;gBAChE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAuB,CAAC;gBACnD,IAAI,KAAK,KAAK,UAAU;oBAAE,OAAO,IAAI,CAAC;gBAEtC,4EAA4E;gBAC5E,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAuB,CAAC;gBACnD,IAAI,CAAC,KAAK;oBAAE,OAAO,IAAI,CAAC;gBAExB,MAAM,MAAM,GAAwB;oBAClC,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC;oBACrC,SAAS;iBACV,CAAC;gBACF,OAAO,MAAM,CAAC;YAChB,CAAC;YAED,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,MAAM,MAAM,GAAwB,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;gBAChE,OAAO,MAAM,CAAC;YAChB,CAAC;YAED;gBACE,OAAO,IAAI,CAAC;QAChB,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,KAAa,EAAE,UAAkB;QAC9C,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;;;;OAQG;IACH,eAAe,CAAC,UAAkB;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
@@ -1,10 +1,64 @@
1
+ /**
2
+ * @fileoverview Twilio `<Connect><Stream>` WebSocket media stream parser.
3
+ *
4
+ * ## Twilio media stream protocol
5
+ *
6
+ * When a Twilio call executes the TwiML `<Connect><Stream url="wss://..." />`,
7
+ * Twilio opens a WebSocket to the specified URL and sends **all messages as
8
+ * JSON-encoded strings** (never raw binary). Each message has an `event` field
9
+ * and a `streamSid` field that together identify the event type and stream.
10
+ *
11
+ * ### Inbound JSON message shapes
12
+ *
13
+ * ```
14
+ * ┌─────────────────────────────────────────────────────────────────────┐
15
+ * │ event: "start" │
16
+ * │ streamSid: "MZxxx" │
17
+ * │ start: { callSid, accountSid, mediaFormat: { encoding, ... } } │
18
+ * ├─────────────────────────────────────────────────────────────────────┤
19
+ * │ event: "media" │
20
+ * │ streamSid: "MZxxx" │
21
+ * │ media: { track: "inbound"|"outbound", payload: "<base64>" } │
22
+ * │ sequenceNumber: 42 │
23
+ * ├─────────────────────────────────────────────────────────────────────┤
24
+ * │ event: "dtmf" │
25
+ * │ streamSid: "MZxxx" │
26
+ * │ dtmf: { digit: "5", duration: 500 } │
27
+ * ├─────────────────────────────────────────────────────────────────────┤
28
+ * │ event: "mark" │
29
+ * │ streamSid: "MZxxx" │
30
+ * │ mark: { name: "utterance-done" } │
31
+ * ├─────────────────────────────────────────────────────────────────────┤
32
+ * │ event: "stop" │
33
+ * │ streamSid: "MZxxx" │
34
+ * └─────────────────────────────────────────────────────────────────────┘
35
+ * ```
36
+ *
37
+ * ### Outbound audio format
38
+ *
39
+ * Audio sent back to Twilio must be wrapped in a JSON `media` envelope:
40
+ * ```json
41
+ * { "event": "media", "streamSid": "MZxxx", "media": { "payload": "<base64>" } }
42
+ * ```
43
+ *
44
+ * ### Connection acknowledgment
45
+ *
46
+ * Immediately after the WebSocket handshake, the server must send:
47
+ * ```json
48
+ * { "event": "connected", "protocol": "Call", "version": "1.0.0" }
49
+ * ```
50
+ * This tells Twilio the listener is ready to receive media.
51
+ *
52
+ * @see {@link https://www.twilio.com/docs/voice/twiml/stream}
53
+ * @module @framers/agentos/voice/parsers/TwilioMediaStreamParser
54
+ */
1
55
  import type { MediaStreamParser, MediaStreamIncoming } from '../MediaStreamParser.js';
2
56
  /**
3
57
  * Parses the Twilio `<Connect><Stream>` WebSocket media stream protocol.
4
58
  *
5
- * Twilio sends all messages as JSON-encoded strings. Outbound audio is
59
+ * Twilio sends all messages as JSON-encoded strings. Outbound audio is
6
60
  * wrapped in the same JSON envelope so Twilio can associate it with the
7
- * correct stream. An explicit `connected` acknowledgment is sent once
61
+ * correct stream. An explicit `connected` acknowledgment is sent once
8
62
  * immediately after the WebSocket handshake to signal that the listener is
9
63
  * ready to receive media.
10
64
  *
@@ -15,11 +69,15 @@ export declare class TwilioMediaStreamParser implements MediaStreamParser {
15
69
  * Parse a raw WebSocket frame from Twilio's media stream.
16
70
  *
17
71
  * Supported Twilio event types:
18
- * - `start` stream established, includes callSid
19
- * - `media` audio chunk (inbound track only; outbound chunks are ignored)
20
- * - `dtmf` DTMF keypress detected
21
- * - `stop` stream ended
22
- * - `mark` named synchronisation marker
72
+ * - `start` -- stream established, includes callSid and media format metadata.
73
+ * - `media` -- audio chunk (inbound track only; outbound echoes are discarded
74
+ * to prevent feedback loops).
75
+ * - `dtmf` -- DTMF keypress detected on the audio stream.
76
+ * - `stop` -- stream ended (call hangup or stream disconnect).
77
+ * - `mark` -- named synchronisation marker confirming playback reached a point.
78
+ *
79
+ * Messages with missing `event` or `streamSid` fields, malformed JSON,
80
+ * or unrecognised event types are silently dropped (return `null`).
23
81
  *
24
82
  * @param data - Raw WebSocket frame payload (always a JSON string from Twilio).
25
83
  * @returns Normalised {@link MediaStreamIncoming} event, or `null` for
@@ -30,20 +88,24 @@ export declare class TwilioMediaStreamParser implements MediaStreamParser {
30
88
  * Encode mu-law audio for transmission back to the Twilio stream.
31
89
  *
32
90
  * Twilio requires base64-encoded audio wrapped in a JSON `media` envelope
33
- * so it can route the audio to the correct stream.
91
+ * so it can route the audio to the correct stream by `streamSid`.
34
92
  *
35
93
  * @param audio - Raw mu-law PCM bytes to send to the caller.
36
94
  * @param streamSid - The stream identifier to include in the envelope.
37
- * @returns JSON string conforming to the Twilio media-out envelope format.
95
+ * @returns JSON string conforming to the Twilio media-out envelope format:
96
+ * `{ event: 'media', streamSid: '...', media: { payload: '<base64>' } }`
38
97
  */
39
98
  formatOutgoing(audio: Buffer, streamSid: string): string;
40
99
  /**
41
100
  * Generate the initial `connected` acknowledgment expected by Twilio
42
101
  * immediately after the WebSocket connection is established.
43
102
  *
44
- * @param _streamSid - Unused — Twilio does not require the stream ID in the
103
+ * Without this message, Twilio waits indefinitely for a response and
104
+ * eventually times out the stream connection.
105
+ *
106
+ * @param _streamSid - Unused -- Twilio does not require the stream ID in the
45
107
  * `connected` message, but the parameter is accepted for interface parity.
46
- * @returns JSON string with the `connected` envelope.
108
+ * @returns JSON string: `{ event: 'connected', protocol: 'Call', version: '1.0.0' }`
47
109
  */
48
110
  formatConnected(_streamSid: string): string;
49
111
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TwilioMediaStreamParser.d.ts","sourceRoot":"","sources":["../../../src/voice/parsers/TwilioMediaStreamParser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,qBAAa,uBAAwB,YAAW,iBAAiB;IAC/D;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,mBAAmB,GAAG,IAAI;IA+FhE;;;;;;;;;OASG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAQxD;;;;;;;OAOG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAO5C"}
1
+ {"version":3,"file":"TwilioMediaStreamParser.d.ts","sourceRoot":"","sources":["../../../src/voice/parsers/TwilioMediaStreamParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,qBAAa,uBAAwB,YAAW,iBAAiB;IAC/D;;;;;;;;;;;;;;;;;OAiBG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAsGhE;;;;;;;;;;OAUG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAQxD;;;;;;;;;;OAUG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAO5C"}
@@ -1,9 +1,63 @@
1
+ /**
2
+ * @fileoverview Twilio `<Connect><Stream>` WebSocket media stream parser.
3
+ *
4
+ * ## Twilio media stream protocol
5
+ *
6
+ * When a Twilio call executes the TwiML `<Connect><Stream url="wss://..." />`,
7
+ * Twilio opens a WebSocket to the specified URL and sends **all messages as
8
+ * JSON-encoded strings** (never raw binary). Each message has an `event` field
9
+ * and a `streamSid` field that together identify the event type and stream.
10
+ *
11
+ * ### Inbound JSON message shapes
12
+ *
13
+ * ```
14
+ * ┌─────────────────────────────────────────────────────────────────────┐
15
+ * │ event: "start" │
16
+ * │ streamSid: "MZxxx" │
17
+ * │ start: { callSid, accountSid, mediaFormat: { encoding, ... } } │
18
+ * ├─────────────────────────────────────────────────────────────────────┤
19
+ * │ event: "media" │
20
+ * │ streamSid: "MZxxx" │
21
+ * │ media: { track: "inbound"|"outbound", payload: "<base64>" } │
22
+ * │ sequenceNumber: 42 │
23
+ * ├─────────────────────────────────────────────────────────────────────┤
24
+ * │ event: "dtmf" │
25
+ * │ streamSid: "MZxxx" │
26
+ * │ dtmf: { digit: "5", duration: 500 } │
27
+ * ├─────────────────────────────────────────────────────────────────────┤
28
+ * │ event: "mark" │
29
+ * │ streamSid: "MZxxx" │
30
+ * │ mark: { name: "utterance-done" } │
31
+ * ├─────────────────────────────────────────────────────────────────────┤
32
+ * │ event: "stop" │
33
+ * │ streamSid: "MZxxx" │
34
+ * └─────────────────────────────────────────────────────────────────────┘
35
+ * ```
36
+ *
37
+ * ### Outbound audio format
38
+ *
39
+ * Audio sent back to Twilio must be wrapped in a JSON `media` envelope:
40
+ * ```json
41
+ * { "event": "media", "streamSid": "MZxxx", "media": { "payload": "<base64>" } }
42
+ * ```
43
+ *
44
+ * ### Connection acknowledgment
45
+ *
46
+ * Immediately after the WebSocket handshake, the server must send:
47
+ * ```json
48
+ * { "event": "connected", "protocol": "Call", "version": "1.0.0" }
49
+ * ```
50
+ * This tells Twilio the listener is ready to receive media.
51
+ *
52
+ * @see {@link https://www.twilio.com/docs/voice/twiml/stream}
53
+ * @module @framers/agentos/voice/parsers/TwilioMediaStreamParser
54
+ */
1
55
  /**
2
56
  * Parses the Twilio `<Connect><Stream>` WebSocket media stream protocol.
3
57
  *
4
- * Twilio sends all messages as JSON-encoded strings. Outbound audio is
58
+ * Twilio sends all messages as JSON-encoded strings. Outbound audio is
5
59
  * wrapped in the same JSON envelope so Twilio can associate it with the
6
- * correct stream. An explicit `connected` acknowledgment is sent once
60
+ * correct stream. An explicit `connected` acknowledgment is sent once
7
61
  * immediately after the WebSocket handshake to signal that the listener is
8
62
  * ready to receive media.
9
63
  *
@@ -14,11 +68,15 @@ export class TwilioMediaStreamParser {
14
68
  * Parse a raw WebSocket frame from Twilio's media stream.
15
69
  *
16
70
  * Supported Twilio event types:
17
- * - `start` stream established, includes callSid
18
- * - `media` audio chunk (inbound track only; outbound chunks are ignored)
19
- * - `dtmf` DTMF keypress detected
20
- * - `stop` stream ended
21
- * - `mark` named synchronisation marker
71
+ * - `start` -- stream established, includes callSid and media format metadata.
72
+ * - `media` -- audio chunk (inbound track only; outbound echoes are discarded
73
+ * to prevent feedback loops).
74
+ * - `dtmf` -- DTMF keypress detected on the audio stream.
75
+ * - `stop` -- stream ended (call hangup or stream disconnect).
76
+ * - `mark` -- named synchronisation marker confirming playback reached a point.
77
+ *
78
+ * Messages with missing `event` or `streamSid` fields, malformed JSON,
79
+ * or unrecognised event types are silently dropped (return `null`).
22
80
  *
23
81
  * @param data - Raw WebSocket frame payload (always a JSON string from Twilio).
24
82
  * @returns Normalised {@link MediaStreamIncoming} event, or `null` for
@@ -35,12 +93,14 @@ export class TwilioMediaStreamParser {
35
93
  }
36
94
  const event = msg['event'];
37
95
  const streamSid = msg['streamSid'];
96
+ // Both fields are required on every Twilio media stream message.
38
97
  if (!event || !streamSid) {
39
98
  return null;
40
99
  }
41
100
  switch (event) {
42
101
  case 'start': {
43
102
  const startPayload = msg['start'];
103
+ // callSid identifies the Twilio call leg this stream belongs to.
44
104
  const callSid = startPayload?.['callSid'] ?? '';
45
105
  const result = {
46
106
  type: 'start',
@@ -54,7 +114,9 @@ export class TwilioMediaStreamParser {
54
114
  const media = msg['media'];
55
115
  if (!media)
56
116
  return null;
57
- // Only process inbound audio outbound echoes must be discarded.
117
+ // Twilio sends both inbound and outbound audio on the same stream.
118
+ // Outbound echoes must be discarded to prevent feedback loops where
119
+ // the agent hears its own TTS output.
58
120
  const track = media['track'];
59
121
  if (track === 'outbound')
60
122
  return null;
@@ -79,6 +141,7 @@ export class TwilioMediaStreamParser {
79
141
  const digit = dtmf['digit'];
80
142
  if (!digit)
81
143
  return null;
144
+ // Twilio reports DTMF key-hold duration in milliseconds.
82
145
  const duration = typeof dtmf['duration'] === 'number'
83
146
  ? dtmf['duration']
84
147
  : undefined;
@@ -105,6 +168,8 @@ export class TwilioMediaStreamParser {
105
168
  return result;
106
169
  }
107
170
  default:
171
+ // Twilio may add new event types in the future; silently ignore them
172
+ // rather than throwing so existing deployments remain forward-compatible.
108
173
  return null;
109
174
  }
110
175
  }
@@ -112,11 +177,12 @@ export class TwilioMediaStreamParser {
112
177
  * Encode mu-law audio for transmission back to the Twilio stream.
113
178
  *
114
179
  * Twilio requires base64-encoded audio wrapped in a JSON `media` envelope
115
- * so it can route the audio to the correct stream.
180
+ * so it can route the audio to the correct stream by `streamSid`.
116
181
  *
117
182
  * @param audio - Raw mu-law PCM bytes to send to the caller.
118
183
  * @param streamSid - The stream identifier to include in the envelope.
119
- * @returns JSON string conforming to the Twilio media-out envelope format.
184
+ * @returns JSON string conforming to the Twilio media-out envelope format:
185
+ * `{ event: 'media', streamSid: '...', media: { payload: '<base64>' } }`
120
186
  */
121
187
  formatOutgoing(audio, streamSid) {
122
188
  return JSON.stringify({
@@ -129,9 +195,12 @@ export class TwilioMediaStreamParser {
129
195
  * Generate the initial `connected` acknowledgment expected by Twilio
130
196
  * immediately after the WebSocket connection is established.
131
197
  *
132
- * @param _streamSid - Unused — Twilio does not require the stream ID in the
198
+ * Without this message, Twilio waits indefinitely for a response and
199
+ * eventually times out the stream connection.
200
+ *
201
+ * @param _streamSid - Unused -- Twilio does not require the stream ID in the
133
202
  * `connected` message, but the parameter is accepted for interface parity.
134
- * @returns JSON string with the `connected` envelope.
203
+ * @returns JSON string: `{ event: 'connected', protocol: 'Call', version: '1.0.0' }`
135
204
  */
136
205
  formatConnected(_streamSid) {
137
206
  return JSON.stringify({
@@ -1 +1 @@
1
- {"version":3,"file":"TwilioMediaStreamParser.js","sourceRoot":"","sources":["../../../src/voice/parsers/TwilioMediaStreamParser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TwilioMediaStreamParser.js","sourceRoot":"","sources":["../../../src/voice/parsers/TwilioMediaStreamParser.ts"],"names":[],"mappings":"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"}
@@ -2,20 +2,78 @@
2
2
  * @fileoverview Plivo telephony provider for AgentOS voice calls.
3
3
  *
4
4
  * Implements {@link IVoiceCallProvider} using the Plivo Voice REST API v1.
5
- * Webhook verification uses HMAC-SHA256 with the v3 signature scheme.
5
+ *
6
+ * ## REST API contract
7
+ *
8
+ * | Operation | Method | Endpoint | Body format |
9
+ * |----------------|--------|----------------------------------------------|-------------|
10
+ * | Initiate call | POST | `/v1/Account/{authId}/Call/` | JSON |
11
+ * | Hangup call | DELETE | `/v1/Account/{authId}/Call/{callUuid}/` | (none) |
12
+ * | Play TTS | POST | `/v1/Account/{authId}/Call/{callUuid}/Speak/` | JSON |
13
+ *
14
+ * All requests use HTTP Basic authentication: `Authorization: Basic base64(authId:authToken)`.
15
+ *
16
+ * ### Hangup uses DELETE (not POST)
17
+ *
18
+ * Unlike Twilio (which POSTs `Status=completed`) and Telnyx (which POSTs to
19
+ * an `/actions/hangup` endpoint), Plivo uses the HTTP `DELETE` method on the
20
+ * Call resource to terminate an active call. This is a RESTful design choice
21
+ * where "deleting" the call resource means terminating the call.
22
+ *
23
+ * ## Webhook verification: HMAC-SHA256 + nonce (v3 scheme)
24
+ *
25
+ * Plivo's v3 webhook signature scheme:
26
+ *
27
+ * 1. Plivo generates a random `nonce` and includes it in the
28
+ * `X-Plivo-Signature-V3-Nonce` header.
29
+ * 2. The signed data is: `{fullRequestURL}{nonce}` (concatenated, no separator).
30
+ * 3. Compute `HMAC-SHA256(authToken, signedData)`.
31
+ * 4. Base64-encode the HMAC digest.
32
+ * 5. Compare the result with the `X-Plivo-Signature-V3` header.
33
+ *
34
+ * Note: Unlike Twilio's scheme, Plivo does NOT include the POST body in the
35
+ * signed data -- only the URL and nonce. The nonce prevents replay attacks.
36
+ *
37
+ * ## DTMF via `<GetDigits>` XML pattern
38
+ *
39
+ * Plivo delivers DTMF input through the `<GetDigits>` XML element callback,
40
+ * not through the media stream WebSocket. When a call executes:
41
+ *
42
+ * ```xml
43
+ * <GetDigits action="https://example.com/dtmf" method="POST" timeout="10">
44
+ * <Speak>Press 1 to confirm.</Speak>
45
+ * </GetDigits>
46
+ * ```
47
+ *
48
+ * Plivo POSTs the pressed digits to the `action` URL with a `Digits`
49
+ * parameter in the form-encoded body (e.g., `Digits=1&CallUUID=xxx`).
50
+ *
51
+ * ## Event mapping table
52
+ *
53
+ * | Plivo `CallStatus` | Normalised `kind` |
54
+ * |---------------------|----------------------|
55
+ * | `ringing` | `call-ringing` |
56
+ * | `in-progress` | `call-answered` |
57
+ * | `completed` | `call-completed` |
58
+ * | `busy` | `call-busy` |
59
+ * | `no-answer` | `call-no-answer` |
60
+ * | `failed` | `call-failed` |
61
+ * | (+ `Digits` param) | `call-dtmf` |
6
62
  *
7
63
  * @module @framers/agentos/voice/providers/plivo
8
64
  */
9
65
  import type { IVoiceCallProvider, InitiateCallInput, InitiateCallResult, HangupCallInput, PlayTtsInput } from '../IVoiceCallProvider.js';
10
66
  import type { WebhookContext, WebhookVerificationResult, WebhookParseResult } from '../types.js';
11
- /** Configuration for {@link PlivoVoiceProvider}. */
67
+ /**
68
+ * Configuration for {@link PlivoVoiceProvider}.
69
+ */
12
70
  export interface PlivoVoiceProviderConfig {
13
- /** Plivo Auth ID (account identifier). */
71
+ /** Plivo Auth ID (account identifier, used in API URLs and Basic auth). */
14
72
  authId: string;
15
- /** Plivo Auth Token. */
73
+ /** Plivo Auth Token (used for both API auth and webhook HMAC verification). */
16
74
  authToken: string;
17
75
  /**
18
- * Optional fetch override inject a mock in tests.
76
+ * Optional fetch implementation override -- inject a mock in tests.
19
77
  * Defaults to the global `fetch`.
20
78
  */
21
79
  fetchImpl?: typeof fetch;
@@ -35,42 +93,80 @@ export interface PlivoVoiceProviderConfig {
35
93
  * ```
36
94
  */
37
95
  export declare class PlivoVoiceProvider implements IVoiceCallProvider {
96
+ /** Provider identifier, always `'plivo'`. */
38
97
  readonly name: "plivo";
98
+ /** Immutable configuration snapshot. */
39
99
  private readonly config;
100
+ /** Base URL for the Plivo REST API v1. */
40
101
  private readonly baseUrl;
102
+ /** Pre-computed `Authorization: Basic ...` header value. */
41
103
  private readonly authHeader;
104
+ /** HTTP fetch implementation (injectable for testing). */
42
105
  private readonly fetch;
106
+ /**
107
+ * @param config - Plivo credentials and optional overrides.
108
+ */
43
109
  constructor(config: PlivoVoiceProviderConfig);
44
110
  /**
45
111
  * Verify an incoming Plivo webhook request using HMAC-SHA256 (v3 scheme).
46
112
  *
47
- * Plivo signs the concatenation of the full request URL and the nonce
48
- * header value. The resulting base64-encoded digest is compared against
49
- * the `x-plivo-signature-v3` header.
113
+ * ## Algorithm (step by step)
114
+ *
115
+ * 1. Extract the `X-Plivo-Signature-V3-Nonce` and `X-Plivo-Signature-V3` headers.
116
+ * 2. Build the signed data string: `{fullRequestURL}{nonce}`.
117
+ * Note: the POST body is NOT included in the signed data (unlike Twilio).
118
+ * 3. Compute `HMAC-SHA256(authToken, signedData)`.
119
+ * 4. Base64-encode the digest.
120
+ * 5. Compare with the `X-Plivo-Signature-V3` header.
121
+ *
122
+ * @param ctx - Raw webhook request context.
123
+ * @returns Verification result with `valid: true` if the signature matches.
50
124
  */
51
125
  verifyWebhook(ctx: WebhookContext): WebhookVerificationResult;
52
126
  /**
53
127
  * Parse a Plivo webhook body into normalized {@link NormalizedCallEvent}s.
54
128
  *
55
- * Supports both URL-encoded and JSON request bodies. Maps `CallStatus`
56
- * parameter values to call lifecycle events.
129
+ * Plivo sends most webhooks with URL-encoded bodies, but some callbacks
130
+ * may arrive as JSON. This parser handles both formats by inspecting
131
+ * whether the body starts with `{` (JSON) or not (form-encoded).
132
+ *
133
+ * Plivo uses two naming conventions for the same fields:
134
+ * - PascalCase (`CallUUID`, `CallStatus`, `Digits`) in URL callbacks.
135
+ * - snake_case (`call_uuid`, `call_status`) in some API responses.
136
+ * Both are checked for maximum compatibility.
137
+ *
138
+ * @param ctx - Raw webhook request context.
139
+ * @returns Parsed result containing zero or more normalized events.
57
140
  */
58
141
  parseWebhookEvent(ctx: WebhookContext): WebhookParseResult;
59
142
  /**
60
143
  * Initiate an outbound call via the Plivo Call API.
61
144
  *
62
- * POSTs a JSON body to `/Account/{authId}/Call/` with the caller, callee,
145
+ * POSTs a JSON body to `/v1/Account/{authId}/Call/` with the caller, callee,
63
146
  * and answer URL. Returns the `request_uuid` as the provider call ID.
147
+ *
148
+ * @param input - Call initiation parameters (from/to numbers, webhook URL).
149
+ * @returns Result containing the Plivo `request_uuid` on success.
150
+ * @throws Never throws; returns `{ success: false, error: '...' }` on failure.
64
151
  */
65
152
  initiateCall(input: InitiateCallInput): Promise<InitiateCallResult>;
66
153
  /**
67
154
  * Hang up an active call using the Plivo Call DELETE endpoint.
155
+ *
156
+ * Plivo uses HTTP `DELETE` to terminate a call (unlike Twilio's POST with
157
+ * `Status=completed` or Telnyx's POST to `/actions/hangup`). This is a
158
+ * RESTful convention where deleting the call resource ends the call.
159
+ *
160
+ * @param input - Contains the Plivo `call_uuid` to hang up.
68
161
  */
69
162
  hangupCall(input: HangupCallInput): Promise<void>;
70
163
  /**
71
164
  * Speak text into a live call using the Plivo Speak API.
72
165
  *
73
- * Defaults to `WOMAN` voice and `en-US` language when not specified.
166
+ * POSTs a JSON body to `/v1/Account/{authId}/Call/{callUuid}/Speak/`
167
+ * with the text, voice (default `'WOMAN'`), and language (default `'en-US'`).
168
+ *
169
+ * @param input - TTS parameters (text, optional voice, call ID).
74
170
  */
75
171
  playTts(input: PlayTtsInput): Promise<void>;
76
172
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plivo.d.ts","sourceRoot":"","sources":["../../../src/voice/providers/plivo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAEnB,MAAM,aAAa,CAAC;AAMrB,oDAAoD;AACpD,MAAM,WAAW,wBAAwB;IACvC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAMD;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,YAAW,kBAAkB;IAC3D,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IAEjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;gBAEzB,MAAM,EAAE,wBAAwB;IAU5C;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,yBAAyB;IAkB7D;;;;;OAKG;IACH,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,kBAAkB;IA8D1D;;;;;OAKG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA0BzE;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD;;;;OAIG;IACG,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBlD"}
1
+ {"version":3,"file":"plivo.d.ts","sourceRoot":"","sources":["../../../src/voice/providers/plivo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAIH,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAEnB,MAAM,aAAa,CAAC;AAMrB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAMD;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,YAAW,kBAAkB;IAC3D,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IAEjC,wCAAwC;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAElD,0CAA0C;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IAErC;;OAEG;gBACS,MAAM,EAAE,wBAAwB;IAW5C;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,yBAAyB;IAoB7D;;;;;;;;;;;;;;OAcG;IACH,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,kBAAkB;IAkE1D;;;;;;;;;OASG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA0BzE;;;;;;;;OAQG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD;;;;;;;OAOG;IACG,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBlD"}