@moxxy/plugin-telegram 0.27.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/dist/approval.d.ts +37 -0
  3. package/dist/approval.d.ts.map +1 -0
  4. package/dist/approval.js +75 -0
  5. package/dist/approval.js.map +1 -0
  6. package/dist/channel/approval-prompt.d.ts +20 -0
  7. package/dist/channel/approval-prompt.d.ts.map +1 -0
  8. package/dist/channel/approval-prompt.js +34 -0
  9. package/dist/channel/approval-prompt.js.map +1 -0
  10. package/dist/channel/callback-handler.d.ts +32 -0
  11. package/dist/channel/callback-handler.d.ts.map +1 -0
  12. package/dist/channel/callback-handler.js +212 -0
  13. package/dist/channel/callback-handler.js.map +1 -0
  14. package/dist/channel/frame-pump.d.ts +58 -0
  15. package/dist/channel/frame-pump.d.ts.map +1 -0
  16. package/dist/channel/frame-pump.js +159 -0
  17. package/dist/channel/frame-pump.js.map +1 -0
  18. package/dist/channel/html.d.ts +24 -0
  19. package/dist/channel/html.d.ts.map +1 -0
  20. package/dist/channel/html.js +62 -0
  21. package/dist/channel/html.js.map +1 -0
  22. package/dist/channel/pairing-handler.d.ts +66 -0
  23. package/dist/channel/pairing-handler.d.ts.map +1 -0
  24. package/dist/channel/pairing-handler.js +147 -0
  25. package/dist/channel/pairing-handler.js.map +1 -0
  26. package/dist/channel/permission-prompt.d.ts +21 -0
  27. package/dist/channel/permission-prompt.d.ts.map +1 -0
  28. package/dist/channel/permission-prompt.js +36 -0
  29. package/dist/channel/permission-prompt.js.map +1 -0
  30. package/dist/channel/slash-handler.d.ts +47 -0
  31. package/dist/channel/slash-handler.d.ts.map +1 -0
  32. package/dist/channel/slash-handler.js +214 -0
  33. package/dist/channel/slash-handler.js.map +1 -0
  34. package/dist/channel/text-handler.d.ts +43 -0
  35. package/dist/channel/text-handler.d.ts.map +1 -0
  36. package/dist/channel/text-handler.js +116 -0
  37. package/dist/channel/text-handler.js.map +1 -0
  38. package/dist/channel/turn-runner.d.ts +40 -0
  39. package/dist/channel/turn-runner.d.ts.map +1 -0
  40. package/dist/channel/turn-runner.js +67 -0
  41. package/dist/channel/turn-runner.js.map +1 -0
  42. package/dist/channel/typing-indicator.d.ts +14 -0
  43. package/dist/channel/typing-indicator.d.ts.map +1 -0
  44. package/dist/channel/typing-indicator.js +29 -0
  45. package/dist/channel/typing-indicator.js.map +1 -0
  46. package/dist/channel/voice-handler.d.ts +42 -0
  47. package/dist/channel/voice-handler.d.ts.map +1 -0
  48. package/dist/channel/voice-handler.js +138 -0
  49. package/dist/channel/voice-handler.js.map +1 -0
  50. package/dist/channel.d.ts +118 -0
  51. package/dist/channel.d.ts.map +1 -0
  52. package/dist/channel.js +478 -0
  53. package/dist/channel.js.map +1 -0
  54. package/dist/channel.test-d.d.ts +2 -0
  55. package/dist/channel.test-d.d.ts.map +1 -0
  56. package/dist/channel.test-d.js +5 -0
  57. package/dist/channel.test-d.js.map +1 -0
  58. package/dist/format.d.ts +38 -0
  59. package/dist/format.d.ts.map +1 -0
  60. package/dist/format.js +237 -0
  61. package/dist/format.js.map +1 -0
  62. package/dist/index.d.ts +24 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +304 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/keys.d.ts +37 -0
  67. package/dist/keys.d.ts.map +1 -0
  68. package/dist/keys.js +42 -0
  69. package/dist/keys.js.map +1 -0
  70. package/dist/pair-flow.d.ts +18 -0
  71. package/dist/pair-flow.d.ts.map +1 -0
  72. package/dist/pair-flow.js +119 -0
  73. package/dist/pair-flow.js.map +1 -0
  74. package/dist/pairing.d.ts +90 -0
  75. package/dist/pairing.d.ts.map +1 -0
  76. package/dist/pairing.js +97 -0
  77. package/dist/pairing.js.map +1 -0
  78. package/dist/permission.d.ts +25 -0
  79. package/dist/permission.d.ts.map +1 -0
  80. package/dist/permission.js +49 -0
  81. package/dist/permission.js.map +1 -0
  82. package/dist/render.d.ts +68 -0
  83. package/dist/render.d.ts.map +1 -0
  84. package/dist/render.js +439 -0
  85. package/dist/render.js.map +1 -0
  86. package/dist/setup-wizard.d.ts +21 -0
  87. package/dist/setup-wizard.d.ts.map +1 -0
  88. package/dist/setup-wizard.js +140 -0
  89. package/dist/setup-wizard.js.map +1 -0
  90. package/package.json +91 -0
  91. package/src/approval.test.ts +103 -0
  92. package/src/approval.ts +86 -0
  93. package/src/channel/approval-prompt.ts +46 -0
  94. package/src/channel/callback-handler.test.ts +188 -0
  95. package/src/channel/callback-handler.ts +256 -0
  96. package/src/channel/frame-pump.ts +176 -0
  97. package/src/channel/html.test.ts +40 -0
  98. package/src/channel/html.ts +60 -0
  99. package/src/channel/pairing-handler.ts +181 -0
  100. package/src/channel/permission-prompt.ts +51 -0
  101. package/src/channel/slash-handler.ts +259 -0
  102. package/src/channel/text-handler.test.ts +222 -0
  103. package/src/channel/text-handler.ts +174 -0
  104. package/src/channel/turn-runner.ts +102 -0
  105. package/src/channel/typing-indicator.ts +31 -0
  106. package/src/channel/voice-handler.test.ts +328 -0
  107. package/src/channel/voice-handler.ts +198 -0
  108. package/src/channel/voice-reply.test.ts +223 -0
  109. package/src/channel.test-d.ts +7 -0
  110. package/src/channel.ts +564 -0
  111. package/src/format.test.ts +159 -0
  112. package/src/format.ts +257 -0
  113. package/src/index.ts +347 -0
  114. package/src/keys.ts +52 -0
  115. package/src/pair-flow.ts +131 -0
  116. package/src/pairing.test.ts +113 -0
  117. package/src/pairing.ts +184 -0
  118. package/src/permission.test.ts +56 -0
  119. package/src/permission.ts +61 -0
  120. package/src/render.test.ts +350 -0
  121. package/src/render.ts +522 -0
  122. package/src/setup-wizard.ts +178 -0
  123. package/src/subcommands.test.ts +153 -0
@@ -0,0 +1,159 @@
1
+ import { GrammyError } from 'grammy';
2
+ import { FramePump as StreamPump } from '@moxxy/channel-kit';
3
+ import { TurnRenderer, splitForTelegram } from '../render.js';
4
+ import { composeFrame, stripHtml } from './html.js';
5
+ /**
6
+ * Drives the throttled "compose snapshot → send/edit one message" loop
7
+ * for a turn. Owns the renderer and the Telegram-specific delivery —
8
+ * HTML parse-mode fallback + 4096-char message splitting — while the
9
+ * throttle/send-once-then-edit mechanics live in `@moxxy/channel-kit`'s
10
+ * {@link StreamPump} (one instance per turn).
11
+ *
12
+ * Lifecycle per turn:
13
+ * 1. `beginTurn(chatId)` resets state.
14
+ * 2. Renderer updates schedule edits via `scheduleEdit()`.
15
+ * 3. `flush(final)` drains the latest snapshot to Telegram.
16
+ * 4. `endTurn()` clears timers + chat binding.
17
+ */
18
+ export class FramePump {
19
+ editFrameMs;
20
+ logger;
21
+ bot = null;
22
+ renderer = new TurnRenderer();
23
+ pump = null;
24
+ constructor(opts) {
25
+ this.editFrameMs = opts.editFrameMs;
26
+ if (opts.logger)
27
+ this.logger = opts.logger;
28
+ }
29
+ attachBot(bot) {
30
+ this.bot = bot;
31
+ }
32
+ /** Renderer the channel feeds events into. Owned here so reset/snapshot
33
+ * cycles stay coordinated with the message-id state. */
34
+ get renderState() {
35
+ return this.renderer;
36
+ }
37
+ resetRenderer() {
38
+ this.renderer.reset();
39
+ }
40
+ beginTurn(chatId) {
41
+ this.renderer.reset();
42
+ this.pump = new StreamPump({
43
+ editFrameMs: this.editFrameMs,
44
+ // Only the FINAL frame collapses the activity trace into its expandable
45
+ // box — mid-stream frames keep it open so the user watches work land live.
46
+ frame: (final) => composeFrame(this.renderer.snapshot({ collapse: final })),
47
+ // The final flush must produce at least one message so the user isn't
48
+ // left with the typing indicator dangling.
49
+ emptyFinalText: '<i>(no output)</i>',
50
+ // The sink does final-only work (split-overflow tails below), so the
51
+ // final frame must reach it even when the text didn't change since the
52
+ // last streamed edit.
53
+ alwaysFlushFinal: true,
54
+ sink: {
55
+ send: (text, final) => this.sendFrame(chatId, text, final),
56
+ edit: (messageId, text, final) => this.editFrame(chatId, messageId, text, final),
57
+ },
58
+ });
59
+ }
60
+ endTurn() {
61
+ this.pump?.dispose();
62
+ this.pump = null;
63
+ }
64
+ scheduleEdit() {
65
+ this.pump?.scheduleEdit();
66
+ }
67
+ async flush(final) {
68
+ if (!this.bot || !this.pump)
69
+ return;
70
+ await this.pump.flush(final);
71
+ }
72
+ /** First real content of this turn — send (don't edit a placeholder). On the
73
+ * final frame, overflow beyond Telegram's message limit goes out as
74
+ * follow-up messages. */
75
+ async sendFrame(chatId, text, final) {
76
+ if (!this.bot)
77
+ return null;
78
+ const parts = splitForTelegram(text);
79
+ const sent = await this.safeSend(chatId, parts[0]);
80
+ if (final && parts.length > 1) {
81
+ for (const tail of parts.slice(1)) {
82
+ await this.safeSend(chatId, tail);
83
+ }
84
+ }
85
+ return sent;
86
+ }
87
+ async editFrame(chatId, messageId, text, final) {
88
+ if (!this.bot)
89
+ return;
90
+ const parts = splitForTelegram(text);
91
+ await this.safeEdit(chatId, messageId, parts[0]);
92
+ if (final && parts.length > 1) {
93
+ for (const tail of parts.slice(1)) {
94
+ await this.safeSend(chatId, tail);
95
+ }
96
+ }
97
+ }
98
+ /**
99
+ * `text` is already Telegram-flavored HTML (produced by
100
+ * `composeFrame`). Try HTML; on parse-entity errors, strip tags and
101
+ * send plain text so the message still lands instead of looping on
102
+ * the same edit forever.
103
+ */
104
+ async safeEdit(chatId, messageId, text) {
105
+ try {
106
+ await this.bot.api.editMessageText(chatId, messageId, text, {
107
+ parse_mode: 'HTML',
108
+ link_preview_options: { is_disabled: true },
109
+ });
110
+ return;
111
+ }
112
+ catch (err) {
113
+ if (err instanceof GrammyError && err.description?.includes('not modified'))
114
+ return;
115
+ if (err instanceof GrammyError && /can't parse entities|Bad Request: can't parse/i.test(err.description ?? '')) {
116
+ try {
117
+ await this.bot.api.editMessageText(chatId, messageId, stripHtml(text));
118
+ return;
119
+ }
120
+ catch (plainErr) {
121
+ if (plainErr instanceof GrammyError && plainErr.description?.includes('not modified'))
122
+ return;
123
+ this.logger?.warn('editMessageText plain-fallback failed', { err: String(plainErr) });
124
+ return;
125
+ }
126
+ }
127
+ this.logger?.warn('editMessageText failed', { err: String(err) });
128
+ }
129
+ }
130
+ /**
131
+ * Send a new message (first frame of a turn or split-tail).
132
+ * Returns the new message_id on success so callers can set
133
+ * messageId for future edits.
134
+ */
135
+ async safeSend(chatId, text) {
136
+ try {
137
+ const sent = await this.bot.api.sendMessage(chatId, text, {
138
+ parse_mode: 'HTML',
139
+ link_preview_options: { is_disabled: true },
140
+ });
141
+ return sent.message_id;
142
+ }
143
+ catch (err) {
144
+ if (err instanceof GrammyError && /can't parse entities|Bad Request: can't parse/i.test(err.description ?? '')) {
145
+ try {
146
+ const sent = await this.bot.api.sendMessage(chatId, stripHtml(text));
147
+ return sent.message_id;
148
+ }
149
+ catch (plainErr) {
150
+ this.logger?.warn('sendMessage plain-fallback failed', { err: String(plainErr) });
151
+ return null;
152
+ }
153
+ }
154
+ this.logger?.warn('sendMessage failed', { err: String(err) });
155
+ return null;
156
+ }
157
+ }
158
+ }
159
+ //# sourceMappingURL=frame-pump.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-pump.js","sourceRoot":"","sources":["../../src/channel/frame-pump.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { RenderedFrame } from '../render.js';
2
+ /**
3
+ * Compose one Telegram message string from the renderer's structured
4
+ * snapshot. The activity block is pre-formatted HTML (the renderer
5
+ * emits `<blockquote>`, `<code>`, `<b>`, `<i>` directly so it can
6
+ * style tool calls without going through the markdown converter). The
7
+ * assistant body is plain Markdown from the model — convert it through
8
+ * `markdownToTelegramHtml` so `**bold**`, `` `code` ``, ```code blocks```,
9
+ * `[links](…)`, and list bullets render natively in Telegram.
10
+ *
11
+ * The error line is also pre-formatted HTML (small, controlled).
12
+ */
13
+ export declare function composeFrame(snap: RenderedFrame): string;
14
+ /** Strip every HTML tag for plain-text fallback when Telegram rejects
15
+ * our parse_mode=HTML payload (rare — usually a malformed entity in
16
+ * user-supplied content). Keeps text content intact.
17
+ *
18
+ * Entities are decoded in a single pass with `&amp;` LAST: decoding it first
19
+ * would double-decode (`&amp;lt;` → `&lt;` → `<`). Numeric entities
20
+ * (`&#39;`, `&#x27;`) are handled too, since escaped user/code content can
21
+ * carry them and they'd otherwise leak literally into the fallback message. */
22
+ export declare function stripHtml(html: string): string;
23
+ export declare function truncate(s: string, n: number): string;
24
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/channel/html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAWxD;AAED;;;;;;;gFAOgF;AAChF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS9C;AAaD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,62 @@
1
+ import { markdownToTelegramHtml } from '../format.js';
2
+ /**
3
+ * Compose one Telegram message string from the renderer's structured
4
+ * snapshot. The activity block is pre-formatted HTML (the renderer
5
+ * emits `<blockquote>`, `<code>`, `<b>`, `<i>` directly so it can
6
+ * style tool calls without going through the markdown converter). The
7
+ * assistant body is plain Markdown from the model — convert it through
8
+ * `markdownToTelegramHtml` so `**bold**`, `` `code` ``, ```code blocks```,
9
+ * `[links](…)`, and list bullets render natively in Telegram.
10
+ *
11
+ * The error line is also pre-formatted HTML (small, controlled).
12
+ */
13
+ export function composeFrame(snap) {
14
+ const parts = [];
15
+ if (snap.activityHtml)
16
+ parts.push(snap.activityHtml);
17
+ if (snap.body)
18
+ parts.push(markdownToTelegramHtml(snap.body));
19
+ // File-diff blocks are already Telegram HTML (summary line + a
20
+ // <pre><code class="language-diff"> fence), so they skip the markdown
21
+ // converter. Append after the body; the frame pump's `splitForTelegram`
22
+ // handles the 4000-char cap, sending overflow as follow-up messages.
23
+ if (snap.diffHtml)
24
+ parts.push(snap.diffHtml);
25
+ if (snap.errorHtml)
26
+ parts.push(snap.errorHtml);
27
+ return parts.join('\n\n');
28
+ }
29
+ /** Strip every HTML tag for plain-text fallback when Telegram rejects
30
+ * our parse_mode=HTML payload (rare — usually a malformed entity in
31
+ * user-supplied content). Keeps text content intact.
32
+ *
33
+ * Entities are decoded in a single pass with `&amp;` LAST: decoding it first
34
+ * would double-decode (`&amp;lt;` → `&lt;` → `<`). Numeric entities
35
+ * (`&#39;`, `&#x27;`) are handled too, since escaped user/code content can
36
+ * carry them and they'd otherwise leak literally into the fallback message. */
37
+ export function stripHtml(html) {
38
+ return html
39
+ .replace(/<\/?[a-z][^>]*>/gi, '')
40
+ .replace(/&#(\d+);/g, (_, dec) => safeFromCodePoint(parseInt(dec, 10)))
41
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => safeFromCodePoint(parseInt(hex, 16)))
42
+ .replace(/&lt;/g, '<')
43
+ .replace(/&gt;/g, '>')
44
+ .replace(/&quot;/g, '"')
45
+ .replace(/&amp;/g, '&');
46
+ }
47
+ /** Decode a numeric entity's code point, leaving an out-of-range/invalid value
48
+ * untouched (return empty) rather than throwing. */
49
+ function safeFromCodePoint(cp) {
50
+ if (!Number.isFinite(cp) || cp < 0 || cp > 0x10ffff)
51
+ return '';
52
+ try {
53
+ return String.fromCodePoint(cp);
54
+ }
55
+ catch {
56
+ return '';
57
+ }
58
+ }
59
+ export function truncate(s, n) {
60
+ return s.length <= n ? s : s.slice(0, n) + '…';
61
+ }
62
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/channel/html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAGtD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,IAAmB;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,CAAC,YAAY;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7D,+DAA+D;IAC/D,sEAAsE;IACtE,wEAAwE;IACxE,qEAAqE;IACrE,IAAI,IAAI,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;gFAOgF;AAChF,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI;SACR,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;SAChC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,GAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;SAC9E,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC,EAAE,GAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;SACtF,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED;qDACqD;AACrD,SAAS,iBAAiB,CAAC,EAAU;IACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,QAAQ;QAAE,OAAO,EAAE,CAAC;IAC/D,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,CAAS,EAAE,CAAS;IAC3C,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC;AACjD,CAAC"}
@@ -0,0 +1,66 @@
1
+ import type { Bot, Context } from 'grammy';
2
+ import type { VaultStore } from '@moxxy/plugin-vault';
3
+ import { type PairingState } from '../pairing.js';
4
+ /** Result returned by `confirmChatCode`. */
5
+ export type PairingConfirmResult = {
6
+ ok: true;
7
+ chatId: number;
8
+ } | {
9
+ ok: false;
10
+ reason: 'mismatch' | 'expired' | 'not-pending' | 'no-window';
11
+ message: string;
12
+ };
13
+ export interface PairingHandlerOptions {
14
+ readonly vault: VaultStore;
15
+ readonly logger?: {
16
+ info?(msg: string, meta?: Record<string, unknown>): void;
17
+ warn(msg: string, meta?: Record<string, unknown>): void;
18
+ };
19
+ }
20
+ /**
21
+ * Owns the pairing state machine + the bot-side `/start` handler for the
22
+ * host-issued QR pairing flow (see {@link beginHostWindow}). Keeps the bot
23
+ * reference (re-)settable so the TelegramChannel can wire it up after
24
+ * construction.
25
+ */
26
+ export declare class PairingHandler {
27
+ private state;
28
+ private bot;
29
+ private readonly opts;
30
+ private readonly pairedListeners;
31
+ constructor(opts: PairingHandlerOptions);
32
+ attachBot(bot: Bot | null): void;
33
+ loadAuthorized(): Promise<void>;
34
+ isAuthorized(chatId: number): boolean;
35
+ phase(): PairingState['phase'];
36
+ /**
37
+ * Open a host-issued pairing window and return the code to embed in the
38
+ * `t.me/<bot>?start=<code>` deep link / QR the control surface renders. The
39
+ * user proves ownership by presenting THIS code back to the bot (deep-link tap
40
+ * or a plain message), which `handleStartCommand` / `confirmChatCode`
41
+ * validate. Returns the generated code.
42
+ */
43
+ beginHostWindow(): string;
44
+ unpair(): void;
45
+ /**
46
+ * Subscribe to "a chat just became authorized" — fires once each time pairing
47
+ * completes. The channel uses this to publish its "connected" connect-state so
48
+ * a watching control surface can swap the QR for a "✓ Connected" affordance,
49
+ * and the `pair` terminal command uses it to know pairing finished. Returns an
50
+ * unsubscribe function.
51
+ */
52
+ onPaired(listener: (chatId: number) => void): () => void;
53
+ /**
54
+ * A chat presented a code (deep-link `/start <code>` payload or a plain 6-digit
55
+ * message). On match the chat is authorized, persisted to the vault, and
56
+ * greeted. Returns a structured result the caller can branch on.
57
+ */
58
+ confirmChatCode(chatId: number, rawCode: string): Promise<PairingConfirmResult>;
59
+ /** Persist + greet + notify on a 'paired' decision; map every decision kind to
60
+ * a {@link PairingConfirmResult}. */
61
+ private applyConfirmDecision;
62
+ private emitPaired;
63
+ /** Bot-side `/start` handler. */
64
+ handleStartCommand(ctx: Context): Promise<void>;
65
+ }
66
+ //# sourceMappingURL=pairing-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing-handler.d.ts","sourceRoot":"","sources":["../../src/channel/pairing-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAQL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAKvB,4CAA4C;AAC5C,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KACzD,CAAC;CACH;AAED;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuC;gBAE3D,IAAI,EAAE,qBAAqB;IAIvC,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;IAI1B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAWrC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIrC,KAAK,IAAI,YAAY,CAAC,OAAO,CAAC;IAI9B;;;;;;OAMG;IACH,eAAe,IAAI,MAAM;IAMzB,MAAM,IAAI,IAAI;IAId;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAKrF;0CACsC;YACxB,oBAAoB;IA4BlC,OAAO,CAAC,UAAU;IAUlB,iCAAiC;IAC3B,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAgCtD"}
@@ -0,0 +1,147 @@
1
+ import { beginHostIssuedPairing, clearPairing, createPairingState, handleStart, isAuthorized, submitChatCode, } from '../pairing.js';
2
+ import { parseChatId, TELEGRAM_AUTHORIZED_CHAT_KEY } from '../keys.js';
3
+ const AUTHORIZED_CHAT_KEY = TELEGRAM_AUTHORIZED_CHAT_KEY;
4
+ /**
5
+ * Owns the pairing state machine + the bot-side `/start` handler for the
6
+ * host-issued QR pairing flow (see {@link beginHostWindow}). Keeps the bot
7
+ * reference (re-)settable so the TelegramChannel can wire it up after
8
+ * construction.
9
+ */
10
+ export class PairingHandler {
11
+ state = createPairingState();
12
+ bot = null;
13
+ opts;
14
+ pairedListeners = new Set();
15
+ constructor(opts) {
16
+ this.opts = opts;
17
+ }
18
+ attachBot(bot) {
19
+ this.bot = bot;
20
+ }
21
+ async loadAuthorized() {
22
+ const authorizedRaw = await this.opts.vault.get(AUTHORIZED_CHAT_KEY);
23
+ const authorizedChatId = parseChatId(authorizedRaw);
24
+ if (authorizedChatId == null && authorizedRaw) {
25
+ this.opts.logger?.warn('telegram pairing: stored chat id is not a number — treating as unpaired', {
26
+ raw: authorizedRaw,
27
+ });
28
+ }
29
+ this.state = createPairingState({ authorizedChatId });
30
+ }
31
+ isAuthorized(chatId) {
32
+ return isAuthorized(this.state, chatId);
33
+ }
34
+ phase() {
35
+ return this.state.phase;
36
+ }
37
+ /**
38
+ * Open a host-issued pairing window and return the code to embed in the
39
+ * `t.me/<bot>?start=<code>` deep link / QR the control surface renders. The
40
+ * user proves ownership by presenting THIS code back to the bot (deep-link tap
41
+ * or a plain message), which `handleStartCommand` / `confirmChatCode`
42
+ * validate. Returns the generated code.
43
+ */
44
+ beginHostWindow() {
45
+ const { state, code } = beginHostIssuedPairing(this.state);
46
+ this.state = state;
47
+ return code;
48
+ }
49
+ unpair() {
50
+ this.state = clearPairing(this.state);
51
+ }
52
+ /**
53
+ * Subscribe to "a chat just became authorized" — fires once each time pairing
54
+ * completes. The channel uses this to publish its "connected" connect-state so
55
+ * a watching control surface can swap the QR for a "✓ Connected" affordance,
56
+ * and the `pair` terminal command uses it to know pairing finished. Returns an
57
+ * unsubscribe function.
58
+ */
59
+ onPaired(listener) {
60
+ this.pairedListeners.add(listener);
61
+ return () => this.pairedListeners.delete(listener);
62
+ }
63
+ /**
64
+ * A chat presented a code (deep-link `/start <code>` payload or a plain 6-digit
65
+ * message). On match the chat is authorized, persisted to the vault, and
66
+ * greeted. Returns a structured result the caller can branch on.
67
+ */
68
+ async confirmChatCode(chatId, rawCode) {
69
+ const decision = submitChatCode(this.state, chatId, rawCode);
70
+ return this.applyConfirmDecision(decision);
71
+ }
72
+ /** Persist + greet + notify on a 'paired' decision; map every decision kind to
73
+ * a {@link PairingConfirmResult}. */
74
+ async applyConfirmDecision(decision) {
75
+ this.state = decision.state;
76
+ const action = decision.action;
77
+ if (action.kind === 'paired') {
78
+ await this.opts.vault.set(AUTHORIZED_CHAT_KEY, String(action.chatId));
79
+ // Greet the chat that just got authorized so the user has a confirmation
80
+ // on the Telegram side too — symmetric with the success the surface shows.
81
+ if (this.bot) {
82
+ try {
83
+ await this.bot.api.sendMessage(action.chatId, '✅ Paired with moxxy. Send a prompt to begin.');
84
+ }
85
+ catch (err) {
86
+ this.opts.logger?.warn('pairing: greeting send failed', { err: String(err) });
87
+ }
88
+ }
89
+ this.emitPaired(action.chatId);
90
+ return { ok: true, chatId: action.chatId };
91
+ }
92
+ if (action.kind === 'still-paired')
93
+ return { ok: true, chatId: action.chatId };
94
+ if (action.kind === 'mismatch')
95
+ return { ok: false, reason: 'mismatch', message: action.message };
96
+ if (action.kind === 'expired')
97
+ return { ok: false, reason: 'expired', message: action.message };
98
+ if (action.kind === 'not-pending')
99
+ return { ok: false, reason: 'not-pending', message: action.message };
100
+ if (action.kind === 'reject')
101
+ return { ok: false, reason: 'not-pending', message: action.message };
102
+ return { ok: false, reason: 'mismatch', message: 'unexpected pairing state' };
103
+ }
104
+ emitPaired(chatId) {
105
+ for (const listener of this.pairedListeners) {
106
+ try {
107
+ listener(chatId);
108
+ }
109
+ catch (err) {
110
+ this.opts.logger?.warn('pairing paired-listener threw', { err: String(err) });
111
+ }
112
+ }
113
+ }
114
+ /** Bot-side `/start` handler. */
115
+ async handleStartCommand(ctx) {
116
+ const chatId = ctx.chat?.id;
117
+ if (!chatId)
118
+ return;
119
+ // Host-issued direction: the deep link the user scanned carries the code as
120
+ // the /start payload (grammy exposes it as `ctx.match`). Validate it here so
121
+ // "scan QR → tap START → paired" needs no typing. A bare /start (no payload —
122
+ // e.g. the user opened the chat manually) falls through to `handleStart`,
123
+ // which nudges them to use the link or send the digits (the message handler
124
+ // also accepts a bare 6-digit code).
125
+ if (this.state.phase === 'awaiting-host-code') {
126
+ const payload = typeof ctx.match === 'string' ? ctx.match.trim() : '';
127
+ if (payload) {
128
+ const result = await this.confirmChatCode(chatId, payload);
129
+ if (!result.ok && result.reason !== 'not-pending') {
130
+ await ctx.reply(result.message);
131
+ }
132
+ return;
133
+ }
134
+ }
135
+ const decision = handleStart(this.state, chatId);
136
+ this.state = decision.state;
137
+ const action = decision.action;
138
+ if (action.kind === 'still-paired') {
139
+ await ctx.reply('Welcome back! Send me a prompt.');
140
+ return;
141
+ }
142
+ if (action.kind === 'reject' || action.kind === 'expired') {
143
+ await ctx.reply(action.message);
144
+ }
145
+ }
146
+ }
147
+ //# sourceMappingURL=pairing-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing-handler.js","sourceRoot":"","sources":["../../src/channel/pairing-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type Bot } from 'grammy';
2
+ import type { PendingToolCall, PermissionContext } from '@moxxy/sdk';
3
+ import type { ClientSession as Session } from '@moxxy/sdk';
4
+ import type { TelegramPermissionResolver } from '../permission.js';
5
+ export interface PermissionPromptDeps {
6
+ readonly bot: Bot | null;
7
+ readonly chatId: number | null;
8
+ readonly session: Session | null;
9
+ readonly resolver: TelegramPermissionResolver;
10
+ readonly yolo: boolean;
11
+ readonly logger?: {
12
+ warn(msg: string, meta?: Record<string, unknown>): void;
13
+ };
14
+ }
15
+ /**
16
+ * Render an inline-keyboard permission prompt for a pending tool call.
17
+ * The decider promise resolves when the user clicks a button (routed
18
+ * back via the callback handler) or when the resolver aborts on stop.
19
+ */
20
+ export declare function askForPermission(call: PendingToolCall, ctx: PermissionContext, deps: PermissionPromptDeps): Promise<void>;
21
+ //# sourceMappingURL=permission-prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-prompt.d.ts","sourceRoot":"","sources":["../../src/channel/permission-prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAkB,MAAM,QAAQ,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAGnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC;CAC/E;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,eAAe,EACrB,GAAG,EAAE,iBAAiB,EACtB,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CA0Bf"}
@@ -0,0 +1,36 @@
1
+ import { InlineKeyboard } from 'grammy';
2
+ import { truncate } from './html.js';
3
+ /**
4
+ * Render an inline-keyboard permission prompt for a pending tool call.
5
+ * The decider promise resolves when the user clicks a button (routed
6
+ * back via the callback handler) or when the resolver aborts on stop.
7
+ */
8
+ export async function askForPermission(call, ctx, deps) {
9
+ if (!deps.bot || !deps.chatId || !deps.session)
10
+ return;
11
+ void ctx;
12
+ // YOLO short-circuit: resolve immediately without rendering a prompt.
13
+ // Mirrors the TUI's `/yolo` flag — once set, every tool call passes.
14
+ if (deps.yolo) {
15
+ deps.resolver.resolvePending(call.callId, { mode: 'allow', reason: 'yolo mode' });
16
+ return;
17
+ }
18
+ const keyboard = new InlineKeyboard()
19
+ .text('Allow once', `perm:${call.callId}:allow`)
20
+ .text('Allow session', `perm:${call.callId}:allow_session`)
21
+ .row()
22
+ .text('Deny', `perm:${call.callId}:deny`);
23
+ const description = deps.session.tools.get(call.name)?.description ?? '';
24
+ const summary = `🔐 Tool permission requested\n` +
25
+ `Tool: ${call.name}\n` +
26
+ (description ? `Desc: ${description}\n` : '') +
27
+ `Input: ${truncate(JSON.stringify(call.input), 300)}`;
28
+ try {
29
+ await deps.bot.api.sendMessage(deps.chatId, summary, { reply_markup: keyboard });
30
+ }
31
+ catch (err) {
32
+ deps.logger?.warn('permission send failed', { err: String(err) });
33
+ deps.resolver.resolvePending(call.callId, { mode: 'deny', reason: 'unable to render prompt' });
34
+ }
35
+ }
36
+ //# sourceMappingURL=permission-prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-prompt.js","sourceRoot":"","sources":["../../src/channel/permission-prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,cAAc,EAAE,MAAM,QAAQ,CAAC;AAIlD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAWrC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAqB,EACrB,GAAsB,EACtB,IAA0B;IAE1B,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO;IACvD,KAAK,GAAG,CAAC;IACT,sEAAsE;IACtE,qEAAqE;IACrE,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;QAClF,OAAO;IACT,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE;SAClC,IAAI,CAAC,YAAY,EAAE,QAAQ,IAAI,CAAC,MAAM,QAAQ,CAAC;SAC/C,IAAI,CAAC,eAAe,EAAE,QAAQ,IAAI,CAAC,MAAM,gBAAgB,CAAC;SAC1D,GAAG,EAAE;SACL,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,WAAW,IAAI,EAAE,CAAC;IACzE,MAAM,OAAO,GACX,gCAAgC;QAChC,SAAS,IAAI,CAAC,IAAI,IAAI;QACtB,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,UAAU,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAC;IACnF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,wBAAwB,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,EAAE,CAAC,CAAC;IACjG,CAAC;AACH,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { type Bot, type Context } from 'grammy';
2
+ import type { ClientSession as Session } from '@moxxy/sdk';
3
+ /**
4
+ * Install guidance shown when enabling `/voice` with no active synthesizer —
5
+ * mirrors the voice-handler's transcriber guidance wording, pointing at the TTS
6
+ * plugin instead of the STT one.
7
+ */
8
+ export declare const VOICE_NO_SYNTH_HINT = "No text-to-speech backend is configured yet, so replies stay text-only. Install one with `moxxy plugins install tts-openai` and run `moxxy login openai` (or set OPENAI_API_KEY) to enable spoken replies.";
9
+ export interface SlashState {
10
+ readonly session: Session | null;
11
+ readonly model: string | undefined;
12
+ readonly activeModelOverride: string | null;
13
+ readonly yolo: boolean;
14
+ /** Whether voice replies are currently enabled (backs `/voice status`). */
15
+ readonly voiceReplies: boolean;
16
+ }
17
+ export interface SlashCallbacks {
18
+ /** Toggle yolo and return its new value (so we can echo the right message). */
19
+ toggleYolo(): boolean;
20
+ /** Persist + apply the voice-replies preference (backs `/voice on|off`). */
21
+ setVoiceReplies(on: boolean): Promise<void>;
22
+ /** Apply a `session-action` result emitted from a registered command. */
23
+ performSessionAction(ctx: Context, action: 'new' | 'clear' | 'exit', notice: string | undefined): Promise<void>;
24
+ }
25
+ /**
26
+ * Slash-command dispatcher for the Telegram channel.
27
+ *
28
+ * First tries the shared `session.commands` registry — this is where
29
+ * the universal commands (/info, /clear, /new, /exit, /help) live, so
30
+ * Telegram gets them for free alongside any plugin-contributed
31
+ * commands without needing a switch case here.
32
+ *
33
+ * Falls through to channel-local cases for Telegram-specific UI
34
+ * (model/loop pickers as inline keyboards, /yolo toggle, /tools and
35
+ * /skills as text dumps).
36
+ */
37
+ export declare function runSlash(ctx: Context, text: string, state: SlashState, cb: SlashCallbacks): Promise<void>;
38
+ /**
39
+ * Push the union of registry commands + Telegram-local commands to
40
+ * Telegram so they appear in the chat's command menu (the "/" picker
41
+ * the official client shows). Best-effort: a network failure here
42
+ * doesn't block channel startup, the commands still work via text.
43
+ */
44
+ export declare function publishBotCommands(bot: Bot | null, session: Session | null, logger?: {
45
+ warn?(msg: string, meta?: Record<string, unknown>): void;
46
+ }): Promise<void>;
47
+ //# sourceMappingURL=slash-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slash-handler.d.ts","sourceRoot":"","sources":["../../src/channel/slash-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,OAAO,EAAkB,MAAM,QAAQ,CAAC;AAEhE,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,+MAC8K,CAAC;AAE/M,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,UAAU,IAAI,OAAO,CAAC;IACtB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,yEAAyE;IACzE,oBAAoB,CAClB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,EAChC,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,cAAc,GACjB,OAAO,CAAC,IAAI,CAAC,CAsFf;AAiFD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,GAAG,GAAG,IAAI,EACf,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,MAAM,CAAC,EAAE;IAAE,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,GACpE,OAAO,CAAC,IAAI,CAAC,CA6Bf"}