@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,214 @@
1
+ import { InlineKeyboard } from 'grammy';
2
+ import { isSelectableMode } from '@moxxy/sdk';
3
+ import { resolveVoiceToggle } from '@moxxy/channel-kit';
4
+ /**
5
+ * Install guidance shown when enabling `/voice` with no active synthesizer —
6
+ * mirrors the voice-handler's transcriber guidance wording, pointing at the TTS
7
+ * plugin instead of the STT one.
8
+ */
9
+ export 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.';
10
+ /**
11
+ * Slash-command dispatcher for the Telegram channel.
12
+ *
13
+ * First tries the shared `session.commands` registry — this is where
14
+ * the universal commands (/info, /clear, /new, /exit, /help) live, so
15
+ * Telegram gets them for free alongside any plugin-contributed
16
+ * commands without needing a switch case here.
17
+ *
18
+ * Falls through to channel-local cases for Telegram-specific UI
19
+ * (model/loop pickers as inline keyboards, /yolo toggle, /tools and
20
+ * /skills as text dumps).
21
+ */
22
+ export async function runSlash(ctx, text, state, cb) {
23
+ const session = state.session;
24
+ if (!session)
25
+ return;
26
+ const [head, ...rest] = text.split(/\s+/);
27
+ const name = head.slice(1);
28
+ const args = rest.join(' ');
29
+ // 1) Shared registry dispatch.
30
+ const registered = session.commands.get(name);
31
+ if (registered) {
32
+ try {
33
+ const result = await registered.handler({
34
+ channel: 'telegram',
35
+ sessionId: session.id,
36
+ args,
37
+ session,
38
+ });
39
+ if (result.kind === 'text') {
40
+ await ctx.reply(result.text);
41
+ }
42
+ else if (result.kind === 'session-action') {
43
+ await cb.performSessionAction(ctx, result.action, result.notice);
44
+ }
45
+ else if (result.kind === 'error') {
46
+ await ctx.reply(`error: ${result.message}`);
47
+ }
48
+ }
49
+ catch (err) {
50
+ await ctx.reply(`command /${name} failed: ${err instanceof Error ? err.message : String(err)}`);
51
+ }
52
+ return;
53
+ }
54
+ // 2) Channel-local cases.
55
+ switch (head) {
56
+ case '/model':
57
+ await renderModelPicker(ctx, session, state, args);
58
+ return;
59
+ case '/mode':
60
+ await renderModePicker(ctx, session);
61
+ return;
62
+ case '/yolo': {
63
+ const enabled = cb.toggleYolo();
64
+ await ctx.reply(enabled
65
+ ? '⚠ yolo mode ON — tool calls auto-approved for the rest of this session'
66
+ : 'yolo mode OFF — tool prompts will resume');
67
+ return;
68
+ }
69
+ case '/voice': {
70
+ const result = resolveVoiceToggle({
71
+ arg: args,
72
+ enabled: state.voiceReplies,
73
+ hasSynthesizer: session.synthesizers.tryGetActive() != null,
74
+ delivery: 'a voice note',
75
+ noSynthesizerHint: VOICE_NO_SYNTH_HINT,
76
+ });
77
+ if (result.persist)
78
+ await cb.setVoiceReplies(result.enabled);
79
+ await ctx.reply(result.reply);
80
+ return;
81
+ }
82
+ case '/tools': {
83
+ const list = session.tools
84
+ .list()
85
+ .map((t) => `${t.name} — ${t.description}`)
86
+ .join('\n');
87
+ await ctx.reply(list || '(no tools registered)');
88
+ return;
89
+ }
90
+ case '/skills': {
91
+ const list = session.skills
92
+ .list()
93
+ .map((s) => {
94
+ const triggers = s.frontmatter.triggers ?? [];
95
+ const triggerLine = triggers.length
96
+ ? `\n triggers: ${triggers.map((t) => `"${t}"`).join(', ')}`
97
+ : '';
98
+ return `${s.frontmatter.name} — ${s.frontmatter.description}${triggerLine}`;
99
+ })
100
+ .join('\n');
101
+ await ctx.reply(list || '(no skills discovered)');
102
+ return;
103
+ }
104
+ default:
105
+ await ctx.reply(`unknown command: ${head} (try /help)`);
106
+ }
107
+ }
108
+ /** Telegram inline keyboards get unwieldy past this many rows; cap the picker. */
109
+ const MODEL_PICKER_CAP = 30;
110
+ async function renderModelPicker(ctx, session, state, filter = '') {
111
+ const providers = session.providers.list();
112
+ if (providers.length === 0) {
113
+ await ctx.reply('no providers registered');
114
+ return;
115
+ }
116
+ const keyboard = new InlineKeyboard();
117
+ const providerName = session.providers.getActiveName() ?? '';
118
+ const activeModel = state.activeModelOverride ?? state.model ?? '';
119
+ // Same boot-time readiness set the TUI uses to flag unconfigured
120
+ // providers — set by `@moxxy/cli` at setup time. Providers that
121
+ // failed credential resolution get a "(not connected)" suffix
122
+ // and a tap on them surfaces the right setup command instead of
123
+ // a no-op switch.
124
+ const ready = session.readyProviders ?? new Set();
125
+ // `/model <substring>` narrows the list so a model that sorts past the
126
+ // keyboard cap is still reachable from chat (the cap below would otherwise
127
+ // silently hide it with no fallback).
128
+ const needle = filter.trim().toLowerCase();
129
+ // Flatten so we can both apply the substring filter and report how many the
130
+ // cap hides (vs. how many the filter excluded).
131
+ const all = providers.flatMap((p) => p.models.map((m) => ({ provider: p.name, model: m.id })));
132
+ const matched = needle
133
+ ? all.filter((e) => `${e.provider}::${e.model}`.toLowerCase().includes(needle))
134
+ : all;
135
+ if (matched.length === 0) {
136
+ await ctx.reply(`no model matches "${filter.trim()}" — try /model with no argument.`);
137
+ return;
138
+ }
139
+ const shown = matched.slice(0, MODEL_PICKER_CAP);
140
+ for (const e of shown) {
141
+ const isCurrent = providerName === e.provider && activeModel === e.model;
142
+ const connected = ready.has(e.provider);
143
+ const label = `${isCurrent ? '• ' : ''}${e.provider}: ${e.model}` +
144
+ (connected ? '' : ' (not connected)');
145
+ keyboard.text(label, `model:${e.provider}::${e.model}`).row();
146
+ }
147
+ const hidden = matched.length - shown.length;
148
+ const prompt = hidden > 0
149
+ ? `Pick a model (${hidden} more not shown — narrow with /model <text>):`
150
+ : 'Pick a model:';
151
+ await ctx.reply(prompt, { reply_markup: keyboard });
152
+ }
153
+ async function renderModePicker(ctx, session) {
154
+ // Special modes (e.g. collaborative, entered via /collab) are hidden from the
155
+ // picker the same way they are in the TUI. See ModeDef.special.
156
+ const modes = session.modes.list().filter(isSelectableMode);
157
+ if (modes.length === 0) {
158
+ await ctx.reply('no modes registered');
159
+ return;
160
+ }
161
+ const keyboard = new InlineKeyboard();
162
+ const activeModeName = (() => {
163
+ try {
164
+ return session.modes.getActive().name;
165
+ }
166
+ catch {
167
+ return '';
168
+ }
169
+ })();
170
+ for (const s of modes) {
171
+ const isCurrent = s.name === activeModeName;
172
+ keyboard.text(`${isCurrent ? '• ' : ''}${s.name}`, `mode:${s.name}`).row();
173
+ }
174
+ await ctx.reply('Pick a mode:', { reply_markup: keyboard });
175
+ }
176
+ /**
177
+ * Push the union of registry commands + Telegram-local commands to
178
+ * Telegram so they appear in the chat's command menu (the "/" picker
179
+ * the official client shows). Best-effort: a network failure here
180
+ * doesn't block channel startup, the commands still work via text.
181
+ */
182
+ export async function publishBotCommands(bot, session, logger) {
183
+ if (!session || !bot)
184
+ return;
185
+ const LOCAL = [
186
+ { command: 'model', description: 'Switch provider + model (inline keyboard)' },
187
+ { command: 'mode', description: 'Switch mode' },
188
+ { command: 'yolo', description: 'Toggle auto-approve mode' },
189
+ { command: 'voice', description: 'Toggle spoken voice replies' },
190
+ { command: 'tools', description: 'List the tools the active session can call' },
191
+ { command: 'skills', description: 'List the discovered skills' },
192
+ { command: 'cancel', description: 'Abort the current turn' },
193
+ ];
194
+ const shared = session.commands
195
+ .listForChannel('telegram')
196
+ .map((c) => ({ command: c.name, description: c.description }));
197
+ const seen = new Set(shared.map((c) => c.command));
198
+ const merged = [...shared, ...LOCAL.filter((c) => !seen.has(c.command))]
199
+ .sort((a, b) => a.command.localeCompare(b.command))
200
+ // Telegram caps descriptions at 256 chars and rejects empties.
201
+ .map((c) => ({
202
+ command: c.command,
203
+ description: (c.description || c.command).slice(0, 256),
204
+ }));
205
+ try {
206
+ await bot.api.setMyCommands(merged);
207
+ }
208
+ catch (err) {
209
+ logger?.warn?.('telegram setMyCommands failed', {
210
+ err: err instanceof Error ? err.message : String(err),
211
+ });
212
+ }
213
+ }
214
+ //# sourceMappingURL=slash-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slash-handler.js","sourceRoot":"","sources":["../../src/channel/slash-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,4MAA4M,CAAC;AAwB/M;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,GAAY,EACZ,IAAY,EACZ,KAAiB,EACjB,EAAkB;IAElB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE5B,+BAA+B;IAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,UAAU,EAAE,CAAC;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;gBACtC,OAAO,EAAE,UAAU;gBACnB,SAAS,EAAE,OAAO,CAAC,EAAE;gBACrB,IAAI;gBACJ,OAAO;aACR,CAAC,CAAC;YACH,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC3B,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC/B,CAAC;iBAAM,IAAI,MAAM,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;gBAC5C,MAAM,EAAE,CAAC,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YACnE,CAAC;iBAAM,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBACnC,MAAM,GAAG,CAAC,KAAK,CAAC,UAAU,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,CAAC,KAAK,CACb,YAAY,IAAI,YAAY,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/E,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IAED,0BAA0B;IAC1B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,MAAM,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACnD,OAAO;QACT,KAAK,OAAO;YACV,MAAM,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACrC,OAAO;QACT,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,OAAO,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC;YAChC,MAAM,GAAG,CAAC,KAAK,CACb,OAAO;gBACL,CAAC,CAAC,wEAAwE;gBAC1E,CAAC,CAAC,0CAA0C,CAC/C,CAAC;YACF,OAAO;QACT,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,MAAM,GAAG,kBAAkB,CAAC;gBAChC,GAAG,EAAE,IAAI;gBACT,OAAO,EAAE,KAAK,CAAC,YAAY;gBAC3B,cAAc,EAAE,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,IAAI,IAAI;gBAC3D,QAAQ,EAAE,cAAc;gBACxB,iBAAiB,EAAE,mBAAmB;aACvC,CAAC,CAAC;YACH,IAAI,MAAM,CAAC,OAAO;gBAAE,MAAM,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7D,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK;iBACvB,IAAI,EAAE;iBACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;iBAC1C,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,uBAAuB,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QACD,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM;iBACxB,IAAI,EAAE;iBACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;gBACT,MAAM,QAAQ,GAAG,CAAC,CAAC,WAAW,CAAC,QAAQ,IAAI,EAAE,CAAC;gBAC9C,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM;oBACjC,CAAC,CAAC,kBAAkB,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;oBAC9D,CAAC,CAAC,EAAE,CAAC;gBACP,OAAO,GAAG,CAAC,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,CAAC,WAAW,GAAG,WAAW,EAAE,CAAC;YAC9E,CAAC,CAAC;iBACD,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,wBAAwB,CAAC,CAAC;YAClD,OAAO;QACT,CAAC;QACD;YACE,MAAM,GAAG,CAAC,KAAK,CAAC,oBAAoB,IAAI,cAAc,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,KAAK,UAAU,iBAAiB,CAC9B,GAAY,EACZ,OAAgB,EAChB,KAAiB,EACjB,MAAM,GAAG,EAAE;IAEX,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IAC3C,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC3C,OAAO;IACT,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IACtC,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,CAAC;IAC7D,MAAM,WAAW,GAAG,KAAK,CAAC,mBAAmB,IAAI,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;IACnE,iEAAiE;IACjE,gEAAgE;IAChE,8DAA8D;IAC9D,gEAAgE;IAChE,kBAAkB;IAClB,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,IAAI,IAAI,GAAG,EAAU,CAAC;IAC1D,uEAAuE;IACvE,2EAA2E;IAC3E,sCAAsC;IACtC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,4EAA4E;IAC5E,gDAAgD;IAChD,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAClC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CACzD,CAAC;IACF,MAAM,OAAO,GAAG,MAAM;QACpB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC/E,CAAC,CAAC,GAAG,CAAC;IACR,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,GAAG,CAAC,KAAK,CAAC,qBAAqB,MAAM,CAAC,IAAI,EAAE,kCAAkC,CAAC,CAAC;QACtF,OAAO;IACT,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACjD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,YAAY,KAAK,CAAC,CAAC,QAAQ,IAAI,WAAW,KAAK,CAAC,CAAC,KAAK,CAAC;QACzE,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,KAAK,GACT,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,KAAK,EAAE;YACnD,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;QACxC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IAChE,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7C,MAAM,MAAM,GACV,MAAM,GAAG,CAAC;QACR,CAAC,CAAC,iBAAiB,MAAM,+CAA+C;QACxE,CAAC,CAAC,eAAe,CAAC;IACtB,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,GAAY,EAAE,OAAgB;IAC5D,8EAA8E;IAC9E,gEAAgE;IAChE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAC5D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACvC,OAAO;IACT,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IACtC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE;QAC3B,IAAI,CAAC;YACH,OAAO,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IACL,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC;QAC5C,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IAC7E,CAAC;IACD,MAAM,GAAG,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,GAAe,EACf,OAAuB,EACvB,MAAqE;IAErE,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG;QAAE,OAAO;IAC7B,MAAM,KAAK,GAAoD;QAC7D,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,2CAA2C,EAAE;QAC9E,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE;QAC/C,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;QAC5D,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,6BAA6B,EAAE;QAChE,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,4CAA4C,EAAE;QAC/E,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;QAChE,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,wBAAwB,EAAE;KAC7D,CAAC;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ;SAC5B,cAAc,CAAC,UAAU,CAAC;SAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;SACrE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACnD,+DAA+D;SAC9D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;KACxD,CAAC,CAAC,CAAC;IACN,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,IAAI,EAAE,CAAC,+BAA+B,EAAE;YAC9C,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACtD,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,43 @@
1
+ import type { Context } from 'grammy';
2
+ import type { ClientSession as Session } from '@moxxy/sdk';
3
+ import type { TelegramApprovalResolver } from '../approval.js';
4
+ import type { TelegramPermissionResolver } from '../permission.js';
5
+ import type { ChannelHandle } from '@moxxy/sdk';
6
+ import type { FramePump } from './frame-pump.js';
7
+ import type { AwaitingApprovalText } from './callback-handler.js';
8
+ import type { PairingHandler } from './pairing-handler.js';
9
+ export interface TextHandlerState {
10
+ readonly session: Session | null;
11
+ readonly model: string | undefined;
12
+ readonly activeModelOverride: string | null;
13
+ readonly yolo: boolean;
14
+ readonly voiceReplies: boolean;
15
+ readonly busy: boolean;
16
+ readonly turnController: AbortController | null;
17
+ readonly awaitingApprovalText: AwaitingApprovalText | null;
18
+ readonly handle: ChannelHandle | null;
19
+ }
20
+ export interface TextHandlerDeps {
21
+ readonly pairing: PairingHandler;
22
+ readonly approvalResolver: TelegramApprovalResolver;
23
+ readonly permissionResolver: TelegramPermissionResolver;
24
+ readonly framePump: FramePump;
25
+ }
26
+ export interface TextHandlerCallbacks {
27
+ readonly setAwaitingApprovalText: (state: AwaitingApprovalText | null) => void;
28
+ readonly toggleYolo: () => boolean;
29
+ readonly setYolo: (value: boolean) => void;
30
+ readonly setVoiceReplies: (on: boolean) => Promise<void>;
31
+ readonly runUserTurn: (ctx: Context, chatId: number, text: string) => Promise<void>;
32
+ /** Host-issued pairing fallback: try to pair an unauthorized chat whose message
33
+ * is the 6-digit code. Returns true when handled (so we skip the generic
34
+ * "not paired" reply). See {@link TelegramChannel.tryHostPair}. */
35
+ readonly tryHostPair: (chatId: number, text: string) => Promise<boolean>;
36
+ }
37
+ /**
38
+ * Top-level dispatch for inbound text messages: authorization gate,
39
+ * awaiting-approval-text capture, /cancel, slash routing, and finally
40
+ * the user-turn path.
41
+ */
42
+ export declare function handleTextMessage(ctx: Context, state: TextHandlerState, deps: TextHandlerDeps, cb: TextHandlerCallbacks): Promise<void>;
43
+ //# sourceMappingURL=text-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-handler.d.ts","sourceRoot":"","sources":["../../src/channel/text-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,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,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,cAAc,EAAE,eAAe,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,oBAAoB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,wBAAwB,CAAC;IACpD,QAAQ,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACxD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,uBAAuB,EAAE,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,eAAe,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF;;wEAEoE;IACpE,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,oBAAoB,GACvB,OAAO,CAAC,IAAI,CAAC,CAqEf"}
@@ -0,0 +1,116 @@
1
+ import { runSlash } from './slash-handler.js';
2
+ /**
3
+ * Top-level dispatch for inbound text messages: authorization gate,
4
+ * awaiting-approval-text capture, /cancel, slash routing, and finally
5
+ * the user-turn path.
6
+ */
7
+ export async function handleTextMessage(ctx, state, deps, cb) {
8
+ const chatId = ctx.chat?.id;
9
+ const text = ctx.message?.text;
10
+ if (!chatId || !text)
11
+ return;
12
+ if (!deps.pairing.isAuthorized(chatId)) {
13
+ // Host-issued pairing: a bare 6-digit message from an unpaired chat may be
14
+ // the code presented back to us. If so, pair and stop here.
15
+ if (await cb.tryHostPair(chatId, text))
16
+ return;
17
+ await ctx.reply('This bot is paired with a different chat (or not paired yet). Run `moxxy telegram pair` to (re-)pair.');
18
+ return;
19
+ }
20
+ // Capture awaiting-text BEFORE the busy guard so the user can answer
21
+ // an approval text prompt even while the strategy is technically
22
+ // still mid-turn (it's pending on us).
23
+ if (state.awaitingApprovalText) {
24
+ const { approvalId, optionId } = state.awaitingApprovalText;
25
+ cb.setAwaitingApprovalText(null);
26
+ const handled = deps.approvalResolver.resolvePendingWithText(approvalId, optionId, text);
27
+ if (handled) {
28
+ await ctx.reply(`✓ submitted (${optionId})`);
29
+ }
30
+ else {
31
+ await ctx.reply('that approval is no longer pending');
32
+ }
33
+ return;
34
+ }
35
+ // /cancel works even while busy; everything else routes through
36
+ // runSlash or the user-turn path.
37
+ if (text === '/cancel') {
38
+ if (state.turnController && !state.turnController.signal.aborted) {
39
+ state.turnController.abort('user cancel');
40
+ await ctx.reply('cancelling current turn…');
41
+ }
42
+ else {
43
+ await ctx.reply('nothing to cancel.');
44
+ }
45
+ return;
46
+ }
47
+ if (text.startsWith('/')) {
48
+ await runSlash(ctx, text, {
49
+ session: state.session,
50
+ model: state.model,
51
+ activeModelOverride: state.activeModelOverride,
52
+ yolo: state.yolo,
53
+ voiceReplies: state.voiceReplies,
54
+ }, {
55
+ toggleYolo: cb.toggleYolo,
56
+ setVoiceReplies: cb.setVoiceReplies,
57
+ performSessionAction: (c, action, notice) => performSessionAction(c, action, notice, state, deps, cb),
58
+ });
59
+ return;
60
+ }
61
+ if (state.busy) {
62
+ await ctx.reply('I am still working on the previous prompt. Send /cancel to abort it.');
63
+ return;
64
+ }
65
+ await cb.runUserTurn(ctx, chatId, text);
66
+ }
67
+ /**
68
+ * Channel-side handler for `session-action` outputs from registered
69
+ * commands. The TUI does the same thing; both channels translate the
70
+ * action into their own UI semantics (Telegram = reply text, Ink =
71
+ * setState + exit).
72
+ */
73
+ async function performSessionAction(ctx, action, notice, state, deps, cb) {
74
+ if (!state.session)
75
+ return;
76
+ if (action === 'exit') {
77
+ await ctx.reply(notice ?? 'closing Telegram channel');
78
+ if (state.handle)
79
+ await state.handle.stop('user /exit');
80
+ return;
81
+ }
82
+ if (action === 'clear') {
83
+ deps.framePump.resetRenderer();
84
+ if (notice)
85
+ await ctx.reply(`✓ ${notice}`);
86
+ return;
87
+ }
88
+ if (action === 'new') {
89
+ if (state.turnController && !state.turnController.signal.aborted) {
90
+ state.turnController.abort('user reset');
91
+ }
92
+ deps.framePump.resetRenderer();
93
+ cb.setYolo(false);
94
+ cb.setAwaitingApprovalText(null);
95
+ deps.approvalResolver.abortAll('session reset');
96
+ deps.permissionResolver.abortAll('session reset');
97
+ // Wipe the history at its source. On a RemoteSession `reset()` asks the
98
+ // runner to clear its authoritative log (and persisted JSONL) and
99
+ // re-sync every attached mirror; on a local Session it clears the
100
+ // EventLog + truncates the sidecar. A mirror-only `log.clear()` would
101
+ // be cosmetic AND desync this client, so only claim success when the
102
+ // reset actually happened.
103
+ try {
104
+ if (typeof state.session.reset === 'function')
105
+ await state.session.reset();
106
+ else
107
+ state.session.log.clear();
108
+ }
109
+ catch (err) {
110
+ await ctx.reply(`⚠ /new failed: ${err instanceof Error ? err.message : String(err)} — history NOT cleared`);
111
+ return;
112
+ }
113
+ await ctx.reply(`✓ ${notice ?? 'new session — conversation history cleared'}`);
114
+ }
115
+ }
116
+ //# sourceMappingURL=text-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-handler.js","sourceRoot":"","sources":["../../src/channel/text-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { Bot, Context } from 'grammy';
2
+ import type { newTurnId } from '@moxxy/core';
3
+ import type { ClientSession as Session } from '@moxxy/sdk';
4
+ import type { FramePump } from './frame-pump.js';
5
+ import type { TypingIndicator } from './typing-indicator.js';
6
+ export interface TurnRunnerLogger {
7
+ warn(msg: string, meta?: Record<string, unknown>): void;
8
+ }
9
+ export interface TurnRunnerDeps {
10
+ readonly session: Session;
11
+ readonly bot: Bot | null;
12
+ readonly framePump: FramePump;
13
+ readonly typing: TypingIndicator;
14
+ readonly logger?: TurnRunnerLogger;
15
+ /**
16
+ * Called once with the FINAL assistant text after it has been flushed to the
17
+ * chat (so the text reply always lands first). Backs the optional voice
18
+ * reply. Best-effort — its failure is logged and never breaks the text turn.
19
+ */
20
+ readonly onFinalReply?: (text: string) => Promise<void>;
21
+ }
22
+ export interface TurnRunnerOptions {
23
+ readonly chatId: number;
24
+ readonly text: string;
25
+ readonly model: string | undefined;
26
+ readonly controller: AbortController;
27
+ /** turnId for this turn. The channel mints it so it can also record it as an
28
+ * own-turn id (mirrorForeignTurn filters on those). */
29
+ readonly turnId: ReturnType<typeof newTurnId>;
30
+ }
31
+ /**
32
+ * Drive a single user turn end-to-end: kick off typing, subscribe the
33
+ * frame pump to session events, run the turn through `runTurn`, flush
34
+ * the final frame, and unwind state in `finally`.
35
+ *
36
+ * The controller is owned by the caller so /cancel can abort just this
37
+ * turn without poisoning the session-level signal.
38
+ */
39
+ export declare function runUserTurn(ctx: Context, deps: TurnRunnerDeps, opts: TurnRunnerOptions): Promise<void>;
40
+ //# sourceMappingURL=turn-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-runner.d.ts","sourceRoot":"","sources":["../../src/channel/turn-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAE3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzD;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC;4DACwD;IACxD,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,IAAI,CAAC,CAsDf"}
@@ -0,0 +1,67 @@
1
+ import { driveTurn, subscribeTurn } from '@moxxy/channel-kit';
2
+ /**
3
+ * Drive a single user turn end-to-end: kick off typing, subscribe the
4
+ * frame pump to session events, run the turn through `runTurn`, flush
5
+ * the final frame, and unwind state in `finally`.
6
+ *
7
+ * The controller is owned by the caller so /cancel can abort just this
8
+ * turn without poisoning the session-level signal.
9
+ */
10
+ export async function runUserTurn(ctx, deps, opts) {
11
+ const { session, bot, framePump, typing, logger, onFinalReply } = deps;
12
+ const { chatId, text, model, controller, turnId } = opts;
13
+ framePump.beginTurn(chatId);
14
+ // Kick off "typing…" right away so the user gets immediate feedback.
15
+ // Don't send an ellipsis placeholder message — the typing indicator
16
+ // IS the placeholder. The frame pump lazily sends the first real
17
+ // frame when there's content to display, then edits that message for
18
+ // every subsequent frame.
19
+ typing.start(bot, chatId);
20
+ // turnId is minted by the caller (the channel records it as an own-turn id).
21
+ // The frame-pump subscriber filters by it: `session.log` fans out to every
22
+ // listener; without this a concurrent turn driven by another channel
23
+ // (HTTP/runner) on the same Session would render into THIS chat. (AGENTS.md:
24
+ // filter event-log subscribers by turnId.)
25
+ const unsubscribe = subscribeTurn(session, turnId, (event) => {
26
+ const frame = framePump.renderState.accept(event);
27
+ if (frame.hasUpdate)
28
+ framePump.scheduleEdit();
29
+ });
30
+ try {
31
+ await driveTurn(session, { turnId, prompt: text, model, signal: controller.signal });
32
+ await framePump.flush(true);
33
+ // The text reply is now out. Speak the final assistant body if a voice
34
+ // reply is wired — isolated so a synth/transcode/transport failure can
35
+ // never break (or re-report) the already-delivered text turn.
36
+ if (onFinalReply) {
37
+ const finalText = framePump.renderState.snapshot().body;
38
+ if (finalText.trim()) {
39
+ try {
40
+ await onFinalReply(finalText);
41
+ }
42
+ catch (err) {
43
+ logger?.warn('telegram voice reply hook failed', {
44
+ err: err instanceof Error ? err.message : String(err),
45
+ });
46
+ }
47
+ }
48
+ }
49
+ }
50
+ catch (err) {
51
+ logger?.warn('telegram turn failed', {
52
+ err: err instanceof Error ? err.message : String(err),
53
+ });
54
+ try {
55
+ await ctx.reply(`Turn failed: ${err instanceof Error ? err.message : String(err)}`);
56
+ }
57
+ catch {
58
+ /* ignore */
59
+ }
60
+ }
61
+ finally {
62
+ typing.stop();
63
+ unsubscribe();
64
+ framePump.endTurn();
65
+ }
66
+ }
67
+ //# sourceMappingURL=turn-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-runner.js","sourceRoot":"","sources":["../../src/channel/turn-runner.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAgC9D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAY,EACZ,IAAoB,EACpB,IAAuB;IAEvB,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACvE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEzD,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC5B,qEAAqE;IACrE,oEAAoE;IACpE,iEAAiE;IACjE,qEAAqE;IACrE,0BAA0B;IAC1B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAE1B,6EAA6E;IAC7E,2EAA2E;IAC3E,qEAAqE;IACrE,6EAA6E;IAC7E,2CAA2C;IAC3C,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,KAAK,CAAC,SAAS;YAAE,SAAS,CAAC,YAAY,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QACrF,MAAM,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,uEAAuE;QACvE,uEAAuE;QACvE,8DAA8D;QAC9D,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,SAAS,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC;YACxD,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;gBACrB,IAAI,CAAC;oBACH,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;gBAChC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,EAAE,IAAI,CAAC,kCAAkC,EAAE;wBAC/C,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;qBACtD,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,IAAI,CAAC,sBAAsB,EAAE;YACnC,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACtD,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,CAAC,gBAAgB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,IAAI,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,SAAS,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Bot } from 'grammy';
2
+ /**
3
+ * Show a "typing…" indicator in the chat for the lifetime of a turn.
4
+ * Telegram clears the indicator ~5s after the last sendChatAction, so
5
+ * we re-fire every 4s. Best-effort — a single failure shouldn't crash
6
+ * the turn (we keep the interval going so transient network blips
7
+ * recover on the next tick).
8
+ */
9
+ export declare class TypingIndicator {
10
+ private timer;
11
+ start(bot: Bot | null, chatId: number): void;
12
+ stop(): void;
13
+ }
14
+ //# sourceMappingURL=typing-indicator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typing-indicator.d.ts","sourceRoot":"","sources":["../../src/channel/typing-indicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC;;;;;;GAMG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAA+C;IAE5D,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAY5C,IAAI,IAAI,IAAI;CAMb"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Show a "typing…" indicator in the chat for the lifetime of a turn.
3
+ * Telegram clears the indicator ~5s after the last sendChatAction, so
4
+ * we re-fire every 4s. Best-effort — a single failure shouldn't crash
5
+ * the turn (we keep the interval going so transient network blips
6
+ * recover on the next tick).
7
+ */
8
+ export class TypingIndicator {
9
+ timer = null;
10
+ start(bot, chatId) {
11
+ if (!bot)
12
+ return;
13
+ this.stop();
14
+ const fire = () => {
15
+ bot.api.sendChatAction(chatId, 'typing').catch(() => {
16
+ /* best-effort */
17
+ });
18
+ };
19
+ fire();
20
+ this.timer = setInterval(fire, 4_000);
21
+ }
22
+ stop() {
23
+ if (this.timer) {
24
+ clearInterval(this.timer);
25
+ this.timer = null;
26
+ }
27
+ }
28
+ }
29
+ //# sourceMappingURL=typing-indicator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typing-indicator.js","sourceRoot":"","sources":["../../src/channel/typing-indicator.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,OAAO,eAAe;IAClB,KAAK,GAA0C,IAAI,CAAC;IAE5D,KAAK,CAAC,GAAe,EAAE,MAAc;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,GAAS,EAAE;YACtB,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;gBAClD,iBAAiB;YACnB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QACF,IAAI,EAAE,CAAC;QACP,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,42 @@
1
+ import type { Context } from 'grammy';
2
+ import type { ClientSession as Session } from '@moxxy/sdk';
3
+ import type { PairingHandler } from './pairing-handler.js';
4
+ export interface VoiceHandlerState {
5
+ readonly session: Session | null;
6
+ readonly busy: boolean;
7
+ }
8
+ export interface VoiceHandlerDeps {
9
+ readonly pairing: PairingHandler;
10
+ /** Bot token, used to build the Telegram file-download URL. */
11
+ readonly token: string;
12
+ readonly logger?: {
13
+ warn(msg: string, meta?: Record<string, unknown>): void;
14
+ };
15
+ }
16
+ export interface VoiceHandlerCallbacks {
17
+ readonly runUserTurn: (ctx: Context, chatId: number, text: string) => Promise<void>;
18
+ /** Override the network fetch for tests. Defaults to global `fetch`. */
19
+ readonly fetchAudio?: (url: string, init?: {
20
+ signal?: AbortSignal;
21
+ }) => Promise<{
22
+ ok: boolean;
23
+ /** HTTP status — surfaced in the download-failure log for diagnostics. */
24
+ status?: number;
25
+ statusText?: string;
26
+ /** Content-Length header, when present — used to reject oversized bodies pre-buffer. */
27
+ headers?: {
28
+ get(name: string): string | null;
29
+ };
30
+ arrayBuffer(): Promise<ArrayBuffer>;
31
+ }>;
32
+ }
33
+ /**
34
+ * Handle inbound voice notes and uploaded audio files. Authorization
35
+ * gate matches the text path. If no Transcriber is registered on the
36
+ * session, reply with a one-time guidance message instead of silently
37
+ * swallowing the audio. Otherwise: download via Bot-API file URL,
38
+ * transcribe through the active Transcriber, echo a short "heard:"
39
+ * confirmation, then run a normal user turn with the transcript.
40
+ */
41
+ export declare function handleVoiceMessage(ctx: Context, state: VoiceHandlerState, deps: VoiceHandlerDeps, cb: VoiceHandlerCallbacks): Promise<void>;
42
+ //# sourceMappingURL=voice-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-handler.d.ts","sourceRoot":"","sources":["../../src/channel/voice-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAkB3D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,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,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAC5B,OAAO,CAAC;QACX,EAAE,EAAE,OAAO,CAAC;QACZ,0EAA0E;QAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,wFAAwF;QACxF,OAAO,CAAC,EAAE;YAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC;QAC/C,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;KACrC,CAAC,CAAC;CACJ;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,EACtB,EAAE,EAAE,qBAAqB,GACxB,OAAO,CAAC,IAAI,CAAC,CAuIf"}