@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,478 @@
1
+ import { Bot, GrammyError, HttpError, InputFile } from 'grammy';
2
+ import { newTurnId } from '@moxxy/core';
3
+ import { TurnCoordinator, deliverVoiceReply, resolveSecret } from '@moxxy/channel-kit';
4
+ import { TelegramPermissionResolver } from './permission.js';
5
+ import { TelegramApprovalResolver } from './approval.js';
6
+ import { FramePump } from './channel/frame-pump.js';
7
+ import { TypingIndicator } from './channel/typing-indicator.js';
8
+ import { PairingHandler } from './channel/pairing-handler.js';
9
+ import { askForPermission } from './channel/permission-prompt.js';
10
+ import { askForApproval } from './channel/approval-prompt.js';
11
+ import { publishBotCommands } from './channel/slash-handler.js';
12
+ import { handleCallback, } from './channel/callback-handler.js';
13
+ import { runUserTurn } from './channel/turn-runner.js';
14
+ import { handleTextMessage } from './channel/text-handler.js';
15
+ import { handleVoiceMessage } from './channel/voice-handler.js';
16
+ import { loadVoiceReplies, saveVoiceReplies } from './keys.js';
17
+ const TOKEN_KEY = 'telegram_bot_token';
18
+ /** Cap on the up-front `getMe` (bot identity → `t.me` connect link). It must
19
+ * never gate channel startup: if Telegram is slow/unreachable, we proceed
20
+ * without the link rather than wedge `start()` (and the readiness it unblocks). */
21
+ const BOT_IDENTITY_TIMEOUT_MS = 8_000;
22
+ export class TelegramChannel {
23
+ name = 'telegram';
24
+ permissionResolver;
25
+ approvalResolver;
26
+ opts;
27
+ bot = null;
28
+ // The resolved bot's `t.me/<botname>` link (from getMe at start), published as
29
+ // this channel's `requestUrl` connect value so control surfaces can render a
30
+ // QR / "open the bot" step. While a host-issued pairing window is open it
31
+ // carries the `?start=<code>` deep-link payload. Null until resolved (or if
32
+ // getMe failed).
33
+ botLink = null;
34
+ // The host-issued pairing code embedded in `botLink`'s `?start=` payload, set
35
+ // when `start()` opens a host pairing window (dedicated + unpaired). Null
36
+ // otherwise (terminal pairing, or already paired).
37
+ hostPairingCode = null;
38
+ // Listeners notified when this channel's connect-state changes (i.e. a chat
39
+ // just paired). The dedicated-runner host subscribes via the handle to
40
+ // re-publish the channel's status file. See `onConnectChange` on the handle.
41
+ connectListeners = new Set();
42
+ currentChatId = null;
43
+ // Last chat we ran a turn for — the target for mirroring turns this channel
44
+ // did NOT initiate (e.g. a web-surface action on a shared session).
45
+ lastChatId = null;
46
+ logUnsub = null;
47
+ session = null;
48
+ model;
49
+ activeModelOverride = null;
50
+ yolo = false;
51
+ // When true, the final assistant reply of each turn is also synthesized (via
52
+ // the session's active Synthesizer) and sent as a voice note. Persisted per
53
+ // paired chat in the vault (`telegram_voice_replies`), toggled with `/voice`.
54
+ voiceReplies = false;
55
+ // Single-flight turn state: `busy` guard, per-turn AbortController (so
56
+ // /cancel aborts only the current turn without poisoning the session-level
57
+ // signal other channels share), and the bounded own-turn-id set that
58
+ // mirrorForeignTurn filters on (invariant #8: filter event-log subscribers
59
+ // by turnId when multiplexing turns on one Session) rather than the coarse
60
+ // `busy` flag alone — so an assistant_message dispatched for our own turn
61
+ // AFTER `busy` flips false (async event ordering / RemoteSession replay)
62
+ // isn't re-mirrored as foreign.
63
+ turns = new TurnCoordinator();
64
+ // When a user clicks an approval option that needs text follow-up
65
+ // (e.g. plan-execute "Redraft with feedback"), we stash the
66
+ // approval+option pair and capture the user's NEXT message as the
67
+ // follow-up text — same mechanism the TUI uses, just over chat.
68
+ awaitingApprovalText = null;
69
+ handle = null;
70
+ framePump;
71
+ typing = new TypingIndicator();
72
+ pairing;
73
+ constructor(opts) {
74
+ this.opts = opts;
75
+ this.permissionResolver = new TelegramPermissionResolver();
76
+ this.approvalResolver = new TelegramApprovalResolver();
77
+ this.framePump = new FramePump({
78
+ editFrameMs: opts.editFrameMs ?? 1000,
79
+ ...(opts.logger ? { logger: opts.logger } : {}),
80
+ });
81
+ this.pairing = new PairingHandler({
82
+ vault: opts.vault,
83
+ ...(opts.logger ? { logger: opts.logger } : {}),
84
+ });
85
+ // A completed pairing flips this channel's connect-state to "connected";
86
+ // notify the host so it can swap the QR for a "✓ Connected" affordance.
87
+ this.pairing.onPaired(() => this.notifyConnectChange());
88
+ }
89
+ /** This channel's connect value (see {@link Channel.requestUrl}): the resolved
90
+ * bot's `t.me` link, surfaced by control surfaces as a QR / open-bot step.
91
+ * While a host pairing window is open it includes the `?start=<code>` payload. */
92
+ get requestUrl() {
93
+ return this.botLink;
94
+ }
95
+ /** Whether the "connect the other side" step is satisfied — i.e. a chat is
96
+ * paired (see {@link Channel.connected}). A host shows "✓ Connected" instead
97
+ * of the pairing QR once this is true. */
98
+ get connected() {
99
+ return this.pairing.phase() === 'paired';
100
+ }
101
+ notifyConnectChange() {
102
+ for (const listener of this.connectListeners) {
103
+ try {
104
+ listener();
105
+ }
106
+ catch (err) {
107
+ this.opts.logger?.warn?.('telegram connect-change listener threw', { err: String(err) });
108
+ }
109
+ }
110
+ }
111
+ async start(startOpts) {
112
+ if (this.handle)
113
+ return this.handle;
114
+ this.session = startOpts.session;
115
+ this.model = startOpts.model;
116
+ // Precedence mirrors Slack (and this channel's own `isAvailable` gate +
117
+ // error message): an explicit option wins, then the `MOXXY_TELEGRAM_TOKEN`
118
+ // env override, then the vault (the shared env→vault resolution in
119
+ // @moxxy/channel-kit). Without the env fallback a headless
120
+ // `moxxy channels start telegram` that set the env var would pass the
121
+ // availability check but then fail to boot — the gate and the channel must
122
+ // agree on where the token comes from.
123
+ const token = this.opts.token ??
124
+ (await resolveSecret(this.opts.vault, {
125
+ envVar: 'MOXXY_TELEGRAM_TOKEN',
126
+ vaultKey: TOKEN_KEY,
127
+ }));
128
+ if (!token) {
129
+ throw new Error(`Telegram bot token not found. Store one via vault_set('${TOKEN_KEY}', ...) or set MOXXY_TELEGRAM_TOKEN.`);
130
+ }
131
+ await this.pairing.loadAuthorized();
132
+ this.voiceReplies = await loadVoiceReplies(this.opts.vault);
133
+ // Open the single host-issued QR pairing window when a pairing surface asked
134
+ // for it (`pair`, the terminal `pair` command) OR when running GUI-supervised
135
+ // on a dedicated runner (the desktop) and no chat is paired yet. The code is
136
+ // minted now so we can embed it in the `t.me/<bot>?start=<code>` deep link
137
+ // below; the surface renders that as a QR, and pairing completes when the
138
+ // user taps START (or sends the digits) — no terminal round-trip. Only a
139
+ // headless start with neither signal still errors with a pairing hint.
140
+ const dedicated = startOpts.dedicated === true || process.env.MOXXY_DEDICATED_RUNNER === '1';
141
+ if (this.pairing.phase() !== 'paired') {
142
+ if (startOpts.pair || dedicated) {
143
+ this.hostPairingCode = this.pairing.beginHostWindow();
144
+ this.opts.logger?.info?.('telegram pairing window open');
145
+ }
146
+ else {
147
+ throw new Error('No Telegram chat is paired yet. Run `moxxy channels telegram pair` to pair, or start it from the desktop Channels panel.');
148
+ }
149
+ }
150
+ this.bot = new Bot(token);
151
+ this.framePump.attachBot(this.bot);
152
+ this.pairing.attachBot(this.bot);
153
+ this.permissionResolver.setDecider((call, ctx) => this.askForPermission(call, ctx));
154
+ this.approvalResolver.setDecider((id, request) => this.askForApproval(id, request));
155
+ // Register the approval resolver on the session so loop strategies
156
+ // (plan-execute) actually surface their plan-validation dialog on
157
+ // this channel. setApprovalResolver(null) on stop tears it down so
158
+ // headless code paths after channel close don't see a stale handler.
159
+ this.session.setApprovalResolver(this.approvalResolver);
160
+ // Mirror-to-both: when the session runs a turn this channel did NOT
161
+ // initiate (e.g. a user submitted a form on the co-attached web surface),
162
+ // post the assistant's prose into the last chat we served. Our OWN turns
163
+ // are rendered by the FramePump (gated by `busy`), so skip those.
164
+ this.logUnsub = this.session.log.subscribe((event) => this.mirrorForeignTurn(event));
165
+ this.bot.command('start', (ctx) => this.pairing.handleStartCommand(ctx));
166
+ this.bot.on('callback_query:data', (ctx) => this.dispatchCallback(ctx));
167
+ // grammy's built-in long-poller (Bot.handleUpdates) awaits each update
168
+ // handler before fetching the next getUpdates batch — so awaiting a
169
+ // whole user turn here would PARK the poll loop for the turn's lifetime.
170
+ // A mid-turn permission prompt waits for a `callback_query` click that
171
+ // grammy could never deliver while parked (deadlock), and `/cancel`
172
+ // would sit undelivered until the turn ends. We dispatch text/voice
173
+ // turns in the background (fire-and-track) so the poll loop stays free
174
+ // to deliver callback_query / permission clicks / `/cancel` mid-turn.
175
+ // Single-flight is preserved because runUserTurn sets `busy` true
176
+ // synchronously and the text/voice handlers reject overlapping turns.
177
+ this.bot.on('message:text', (ctx) => this.dispatchInBackground(this.handleText(ctx), 'text'));
178
+ // Voice notes (press-and-hold) and uploaded audio files. Both go
179
+ // through the same transcribe-then-runUserTurn pipeline; the
180
+ // handler picks the right one off `ctx.message`.
181
+ this.bot.on(['message:voice', 'message:audio'], (ctx) => this.dispatchInBackground(this.handleVoice(ctx, token), 'voice'));
182
+ // Surface the shared registry commands in Telegram's bot-command
183
+ // menu so users see /info, /clear, /new, /exit, /help next to the
184
+ // Telegram-local /model, /loop, /yolo, /tools, /skills, /cancel.
185
+ void publishBotCommands(this.bot, this.session, this.opts.logger);
186
+ this.bot.catch((err) => {
187
+ const e = err.error;
188
+ if (e instanceof GrammyError)
189
+ this.opts.logger?.warn('grammy error', { description: e.description });
190
+ else if (e instanceof HttpError)
191
+ this.opts.logger?.warn('http error', { message: e.message });
192
+ else
193
+ this.opts.logger?.warn('telegram error', { err: String(e) });
194
+ });
195
+ this.opts.logger?.info?.('telegram channel starting', {
196
+ paired: this.pairing.phase() === 'paired',
197
+ });
198
+ // Resolve the bot's own identity up front (getMe) so a control surface can
199
+ // show a `t.me/<botname>` connect step. grammy caches this on `bot.botInfo`,
200
+ // and `bot.start()` below reuses it instead of calling getMe again. Non-fatal
201
+ // AND non-blocking: a transient getMe failure must not block the channel from
202
+ // starting — and neither must a STALLED one. We bound it with a timeout so a
203
+ // slow/unreachable Telegram can't wedge `start()` (which gates bot polling,
204
+ // the web-surface co-attach, and the dedicated runner's status file). On
205
+ // timeout/error we proceed without the link; the poll loop surfaces a
206
+ // genuinely invalid token on its own. (We race a timer rather than pass
207
+ // grammy an AbortSignal — its `init(signal)` types the param against the
208
+ // `abort-controller` polyfill, not the global signal.)
209
+ const bot = this.bot;
210
+ const init = bot.init();
211
+ init.catch(() => undefined); // a late rejection (post-timeout) isn't unhandled
212
+ try {
213
+ await Promise.race([
214
+ init,
215
+ new Promise((_, reject) => {
216
+ const t = setTimeout(() => reject(new Error(`getMe timed out after ${BOT_IDENTITY_TIMEOUT_MS}ms`)), BOT_IDENTITY_TIMEOUT_MS);
217
+ t.unref?.();
218
+ }),
219
+ ]);
220
+ const username = bot.botInfo.username;
221
+ if (username) {
222
+ // Embed the host-issued code as the `?start=` deep-link payload so a scan
223
+ // → tap-START round-trips `/start <code>` back to us, pairing with zero
224
+ // typing. Plain `t.me/<bot>` once paired (or in terminal-pair mode).
225
+ this.botLink = this.hostPairingCode
226
+ ? `https://t.me/${username}?start=${this.hostPairingCode}`
227
+ : `https://t.me/${username}`;
228
+ }
229
+ }
230
+ catch (err) {
231
+ this.opts.logger?.warn?.('telegram: could not resolve bot identity (getMe)', {
232
+ error: err instanceof Error ? err.message : String(err),
233
+ });
234
+ }
235
+ const running = bot.start({ drop_pending_updates: false });
236
+ this.handle = {
237
+ running,
238
+ // Let the dedicated-runner host re-publish our status file when a chat
239
+ // pairs (connect-state → connected), so its panel swaps QR for "Connected".
240
+ onConnectChange: (listener) => {
241
+ this.connectListeners.add(listener);
242
+ return () => this.connectListeners.delete(listener);
243
+ },
244
+ stop: async (reason = 'shutdown') => {
245
+ // Abort the in-flight turn FIRST so the model loop stops generating
246
+ // tokens / executing side-effecting tools the moment the operator
247
+ // asks to shut down — otherwise (shared/remote Session) spend and
248
+ // tool calls continue to completion and only their output is discarded.
249
+ this.turns.abort(reason);
250
+ this.permissionResolver.abortAll(reason);
251
+ this.approvalResolver.abortAll(reason);
252
+ this.logUnsub?.();
253
+ this.logUnsub = null;
254
+ if (this.session)
255
+ this.session.setApprovalResolver(null);
256
+ this.framePump.endTurn();
257
+ this.typing.stop();
258
+ if (this.bot)
259
+ await this.bot.stop();
260
+ },
261
+ };
262
+ return this.handle;
263
+ }
264
+ pairingPhase() {
265
+ return this.pairing.phase();
266
+ }
267
+ unpair() {
268
+ this.pairing.unpair();
269
+ }
270
+ /**
271
+ * Subscribe to "a chat just paired" — fires once when the host-issued QR
272
+ * pairing completes. The `moxxy channels telegram pair` command uses this to
273
+ * print success and stop waiting; returns an unsubscribe function.
274
+ */
275
+ onPaired(listener) {
276
+ return this.pairing.onPaired(listener);
277
+ }
278
+ handleVoice(ctx, token) {
279
+ return handleVoiceMessage(ctx, {
280
+ session: this.session,
281
+ busy: this.turns.busy,
282
+ }, {
283
+ pairing: this.pairing,
284
+ token,
285
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
286
+ }, {
287
+ runUserTurn: (c, chatId, text) => this.runUserTurn(c, chatId, text),
288
+ });
289
+ }
290
+ handleText(ctx) {
291
+ return handleTextMessage(ctx, {
292
+ session: this.session,
293
+ model: this.model,
294
+ activeModelOverride: this.activeModelOverride,
295
+ yolo: this.yolo,
296
+ voiceReplies: this.voiceReplies,
297
+ busy: this.turns.busy,
298
+ turnController: this.turns.controller,
299
+ awaitingApprovalText: this.awaitingApprovalText,
300
+ handle: this.handle,
301
+ }, {
302
+ pairing: this.pairing,
303
+ approvalResolver: this.approvalResolver,
304
+ permissionResolver: this.permissionResolver,
305
+ framePump: this.framePump,
306
+ }, {
307
+ setAwaitingApprovalText: (state) => {
308
+ this.awaitingApprovalText = state;
309
+ },
310
+ toggleYolo: () => {
311
+ this.yolo = !this.yolo;
312
+ return this.yolo;
313
+ },
314
+ setYolo: (value) => {
315
+ this.yolo = value;
316
+ },
317
+ setVoiceReplies: (on) => this.setVoiceReplies(on),
318
+ runUserTurn: (c, chatId, text) => this.runUserTurn(c, chatId, text),
319
+ tryHostPair: (chatId, text) => this.tryHostPair(ctx, chatId, text),
320
+ });
321
+ }
322
+ /** Persist + apply the voice-replies preference (the `/voice` toggle). */
323
+ async setVoiceReplies(on) {
324
+ this.voiceReplies = on;
325
+ try {
326
+ await saveVoiceReplies(this.opts.vault, on);
327
+ }
328
+ catch (err) {
329
+ this.opts.logger?.warn('telegram voice-replies persist failed', { err: String(err) });
330
+ }
331
+ }
332
+ /**
333
+ * Speak the final assistant reply as a voice note, when enabled. Best-effort
334
+ * and fully isolated (never throws): synthesize via the session's active
335
+ * Synthesizer, transcode to OGG/Opus (or send plain audio when ffmpeg is
336
+ * unavailable), and deliver via grammy. The text reply already went out.
337
+ */
338
+ async sendVoiceReply(chatId, text) {
339
+ if (!this.voiceReplies || !this.bot || !this.session)
340
+ return;
341
+ const bot = this.bot;
342
+ const outcome = await deliverVoiceReply(this.session, text, {
343
+ send: async (audio, meta) => {
344
+ const file = new InputFile(audio, meta.filename);
345
+ if (meta.isVoiceNote)
346
+ await bot.api.sendVoice(chatId, file);
347
+ else
348
+ await bot.api.sendAudio(chatId, file);
349
+ },
350
+ });
351
+ if (outcome.status === 'failed') {
352
+ this.opts.logger?.warn('telegram voice reply failed', {
353
+ reason: outcome.reason,
354
+ ...(outcome.error ? { err: outcome.error } : {}),
355
+ });
356
+ }
357
+ }
358
+ /**
359
+ * Plain-message fallback for host-issued pairing: if a host window is open and
360
+ * an unauthorized chat sends exactly the 6-digit code, pair it (covers clients
361
+ * that don't auto-deliver the `?start=` deep-link payload). Returns true when
362
+ * the message was a pairing attempt we handled (paired, or a wrong-code reply)
363
+ * so the caller doesn't also emit the generic "not paired" rejection.
364
+ */
365
+ async tryHostPair(ctx, chatId, text) {
366
+ if (this.pairing.phase() !== 'awaiting-host-code')
367
+ return false;
368
+ const normalized = text.replace(/\s+/g, '');
369
+ if (!/^\d{6}$/.test(normalized))
370
+ return false;
371
+ const result = await this.pairing.confirmChatCode(chatId, normalized);
372
+ if (result.ok)
373
+ return true; // confirmChatCode greeted + fired onPaired
374
+ await ctx.reply(result.message);
375
+ return true;
376
+ }
377
+ /**
378
+ * Run a handler promise detached from the grammy poll loop. grammy awaits the
379
+ * value a middleware returns before fetching the next update batch; returning
380
+ * void here (instead of the handler promise) lets the loop keep delivering
381
+ * callback_query / `/cancel` while a turn runs. Errors are logged here because
382
+ * `bot.catch` only sees rejections of the AWAITED middleware chain.
383
+ */
384
+ dispatchInBackground(work, kind) {
385
+ void work.catch((err) => {
386
+ this.opts.logger?.warn('telegram handler failed', { kind, err: String(err) });
387
+ });
388
+ }
389
+ async runUserTurn(ctx, chatId, text) {
390
+ if (!this.session)
391
+ throw new Error('TelegramChannel.start() must be called first');
392
+ // Atomic single-flight guard: `begin` claims the slot synchronously BEFORE
393
+ // any await so a second turn dispatched concurrently (the poll loop is no
394
+ // longer parked on us) can't slip past the busy check in the text/voice
395
+ // handlers. If we are already busy, refuse rather than corrupt the
396
+ // single-instance per-turn state (framePump / currentChatId / controller).
397
+ // The turnId is minted here so the coordinator records it as an own-turn
398
+ // id — that's what mirrorForeignTurn filters on.
399
+ const lease = this.turns.begin(newTurnId());
400
+ if (!lease) {
401
+ await ctx.reply('I am still working on the previous prompt. Send /cancel to abort it.');
402
+ return;
403
+ }
404
+ this.currentChatId = chatId;
405
+ this.lastChatId = chatId;
406
+ const effectiveModel = this.activeModelOverride ?? this.model;
407
+ try {
408
+ await runUserTurn(ctx, {
409
+ session: this.session,
410
+ bot: this.bot,
411
+ framePump: this.framePump,
412
+ typing: this.typing,
413
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
414
+ onFinalReply: (finalText) => this.sendVoiceReply(chatId, finalText),
415
+ }, { chatId, text, model: effectiveModel, controller: lease.controller, turnId: lease.turnId });
416
+ }
417
+ finally {
418
+ lease.end();
419
+ this.currentChatId = null;
420
+ }
421
+ }
422
+ /**
423
+ * Post the assistant's prose for a turn this channel did not initiate. Gated
424
+ * by `!busy` (our own turns are rendered by the FramePump from the runUserTurn
425
+ * iterator) and by having served a chat at least once. Sent as plain text to
426
+ * avoid parse-mode pitfalls; the view itself lives on the web surface.
427
+ */
428
+ mirrorForeignTurn(event) {
429
+ // The coordinator skips turns THIS channel initiated, by turnId — robust to
430
+ // events that arrive after `busy` flips false (async ordering /
431
+ // RemoteSession replay), which the `busy` flag alone could mis-mirror as
432
+ // foreign (invariant #8) — and yields the trimmed assistant prose.
433
+ const text = this.turns.mirrorText(event);
434
+ if (text == null)
435
+ return;
436
+ if (!this.bot || this.lastChatId == null)
437
+ return;
438
+ void this.bot.api.sendMessage(this.lastChatId, text).catch((err) => {
439
+ this.opts.logger?.warn('telegram mirror failed', { err: String(err) });
440
+ });
441
+ }
442
+ askForPermission(call, ctx) {
443
+ return askForPermission(call, ctx, {
444
+ bot: this.bot,
445
+ chatId: this.currentChatId,
446
+ session: this.session,
447
+ resolver: this.permissionResolver,
448
+ yolo: this.yolo,
449
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
450
+ });
451
+ }
452
+ askForApproval(id, request) {
453
+ return askForApproval(id, request, {
454
+ bot: this.bot,
455
+ chatId: this.currentChatId,
456
+ resolver: this.approvalResolver,
457
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
458
+ });
459
+ }
460
+ dispatchCallback(ctx) {
461
+ return handleCallback(ctx, {
462
+ bot: this.bot,
463
+ session: this.session,
464
+ chatId: this.currentChatId,
465
+ permissionResolver: this.permissionResolver,
466
+ approvalResolver: this.approvalResolver,
467
+ pairing: this.pairing,
468
+ }, {
469
+ setAwaitingApprovalText: (state) => {
470
+ this.awaitingApprovalText = state;
471
+ },
472
+ setActiveModelOverride: (modelId) => {
473
+ this.activeModelOverride = modelId;
474
+ },
475
+ });
476
+ }
477
+ }
478
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=channel.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.test-d.d.ts","sourceRoot":"","sources":["../src/channel.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import { expectTypeOf } from 'vitest';
2
+ import {} from './channel.js';
3
+ // Compile-time assertion: TelegramChannel satisfies the Channel<TelegramStartOpts> contract.
4
+ expectTypeOf().toMatchTypeOf();
5
+ //# sourceMappingURL=channel.test-d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.test-d.js","sourceRoot":"","sources":["../src/channel.test-d.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,EAA0B,MAAM,cAAc,CAAC;AAEtD,6FAA6F;AAC7F,YAAY,EAAmB,CAAC,aAAa,EAA8B,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Convert the model's Markdown output into Telegram-flavoured HTML so
3
+ * the bot renders **bold**, `code`, ```code blocks```, [links](…),
4
+ * lists, and headings instead of dumping raw `**` and `#` characters
5
+ * into the chat.
6
+ *
7
+ * Why HTML and not MarkdownV2: MarkdownV2 requires escaping
8
+ * `_ * [ ] ( ) ~ \` > # + - = | { } . !` literally everywhere they
9
+ * AREN'T part of formatting — one stray `.` in a sentence breaks the
10
+ * whole message. HTML only needs three escapes (`< > &`) in text
11
+ * segments, and Telegram's HTML mode supports every tag we need
12
+ * (`<b>`, `<i>`, `<s>`, `<tg-spoiler>`, `<code>`, `<pre>`, `<a>`,
13
+ * `<blockquote>` / `<blockquote expandable>`).
14
+ *
15
+ * Telegram has no native headings or list elements, so:
16
+ * - `#`, `##`, `###` headings → bold text on their own line.
17
+ * - `- item` / `* item` bullets → `• item` (bullet glyph).
18
+ * - `1. item` numbered → kept as-is (the digit is fine).
19
+ *
20
+ * Beyond CommonMark we also map a few extensions so the model can lean
21
+ * on Telegram's richer surface without learning a new syntax:
22
+ * - `~~strike~~` → `<s>` strikethrough.
23
+ * - `||spoiler||` → `<tg-spoiler>` (tap-to-reveal hidden text).
24
+ * - `> [!note] Heading` → a titled, emoji-tagged callout blockquote.
25
+ * - `> [!details]- Title` → a COLLAPSED (expandable) callout — the
26
+ * load-bearing "hide the details" box. The
27
+ * trailing `-` forces collapsed, `+` forces
28
+ * open; some types (details/example/faq)
29
+ * collapse by default.
30
+ * - a plain `>` quote that runs long auto-collapses into an
31
+ * `<blockquote expandable>` so a wall of quoted text stays tidy.
32
+ *
33
+ * Code blocks are emitted as `<pre><code class="language-xxx">…</code></pre>`,
34
+ * inline code as `<code>…</code>`. Everything inside code is escaped
35
+ * but no formatting markers run.
36
+ */
37
+ export declare function markdownToTelegramHtml(md: string): string;
38
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAwFzD"}