@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
package/dist/keys.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Vault keys + token validation shared by the channel, its subcommands, and the
3
+ * interactive setup wizard. Kept in their own module so the wizard / pair-flow
4
+ * helpers can import them without pulling in the plugin's full index.
5
+ */
6
+ /** Vault key the plugin uses for the Bot API token. */
7
+ export const TELEGRAM_TOKEN_KEY = 'telegram_bot_token';
8
+ /** Vault key the plugin uses for the paired chat id. */
9
+ export const TELEGRAM_AUTHORIZED_CHAT_KEY = 'telegram_authorized_chat_id';
10
+ /**
11
+ * Vault key for the voice-replies preference. Single-paired-chat model, so a
12
+ * simple present-flag (`'1'` = on, absent = off) keyed like the other telegram
13
+ * keys, toggled from the paired chat via `/voice`.
14
+ */
15
+ export const TELEGRAM_VOICE_REPLIES_KEY = 'telegram_voice_replies';
16
+ /** Regex validating a Telegram bot token (`<digits>:<22+ url-safe>`). */
17
+ export const TELEGRAM_TOKEN_RE = /^\d+:[A-Za-z0-9_-]{20,}$/;
18
+ /** Read the persisted voice-replies preference (`'1'` → on). */
19
+ export async function loadVoiceReplies(vault) {
20
+ return (await vault.get(TELEGRAM_VOICE_REPLIES_KEY)) === '1';
21
+ }
22
+ /** Persist the voice-replies preference: store `'1'` when on, remove it when off. */
23
+ export async function saveVoiceReplies(vault, on) {
24
+ if (on)
25
+ await vault.set(TELEGRAM_VOICE_REPLIES_KEY, '1');
26
+ else
27
+ await vault.delete(TELEGRAM_VOICE_REPLIES_KEY);
28
+ }
29
+ /**
30
+ * Parse a stored chat-id (the vault keeps it as a string). Returns `null` for a
31
+ * missing OR corrupt value rather than letting `Number(x)` yield `NaN` — a
32
+ * silent NaN serializes to `null` in status output (masking the corruption) and
33
+ * would otherwise be passed to `api.sendMessage(NaN, …)` as a real target,
34
+ * producing an opaque Telegram 400 instead of a clear "not paired" signal.
35
+ */
36
+ export function parseChatId(raw) {
37
+ if (raw == null || raw === '')
38
+ return null;
39
+ const n = Number(raw);
40
+ return Number.isFinite(n) ? n : null;
41
+ }
42
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,uDAAuD;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AACvD,wDAAwD;AACxD,MAAM,CAAC,MAAM,4BAA4B,GAAG,6BAA6B,CAAC;AAC1E;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AACnE,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAS5D,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAkC;IACvE,OAAO,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,KAAK,GAAG,CAAC;AAC/D,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA6C,EAC7C,EAAW;IAEX,IAAI,EAAE;QAAE,MAAM,KAAK,CAAC,GAAG,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;;QACpD,MAAM,KAAK,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,GAA8B;IACxD,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type ChannelSubcommandContext } from '@moxxy/sdk';
2
+ /**
3
+ * Drive the host-issued QR pairing flow end-to-end in a terminal — the SAME
4
+ * mechanism the desktop Channels panel uses, just rendered inline here.
5
+ *
6
+ * Steps:
7
+ * 1. Build a TelegramChannel from the subcommand ctx and wire the session's
8
+ * permission resolver.
9
+ * 2. Subscribe to "paired" BEFORE starting the bot so a fast scan can't race us.
10
+ * 3. Start the bot in pairing mode — it mints a code and publishes a
11
+ * `t.me/<bot>?start=<code>` deep link as `channel.requestUrl`.
12
+ * 4. Render that deep link as a scannable QR (+ the plain link).
13
+ * 5. The user scans / opens the link and taps START (or sends the 6 digits);
14
+ * the bot authorizes that chat and `onPaired` fires.
15
+ * 6. Hand off SIGINT to keep the bot running until the user Ctrl-Cs.
16
+ */
17
+ export declare function runPairFlow(ctx: ChannelSubcommandContext): Promise<number>;
18
+ //# sourceMappingURL=pair-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pair-flow.d.ts","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0B,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAQnF;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6FhF"}
@@ -0,0 +1,119 @@
1
+ import { log, outro, spinner } from '@clack/prompts';
2
+ import QRCode from 'qrcode';
3
+ import { exitAfterPairRequested } from '@moxxy/sdk';
4
+ import { TelegramChannel } from './channel.js';
5
+ // Tiny zero-dep ANSI dim helper, so this flow stays inside the plugin.
6
+ const ANSI = process.stdout.isTTY && !process.env.NO_COLOR;
7
+ const dim = (s) => (ANSI ? `\x1b[2m${s}\x1b[22m` : s);
8
+ /**
9
+ * Drive the host-issued QR pairing flow end-to-end in a terminal — the SAME
10
+ * mechanism the desktop Channels panel uses, just rendered inline here.
11
+ *
12
+ * Steps:
13
+ * 1. Build a TelegramChannel from the subcommand ctx and wire the session's
14
+ * permission resolver.
15
+ * 2. Subscribe to "paired" BEFORE starting the bot so a fast scan can't race us.
16
+ * 3. Start the bot in pairing mode — it mints a code and publishes a
17
+ * `t.me/<bot>?start=<code>` deep link as `channel.requestUrl`.
18
+ * 4. Render that deep link as a scannable QR (+ the plain link).
19
+ * 5. The user scans / opens the link and taps START (or sends the 6 digits);
20
+ * the bot authorizes that chat and `onPaired` fires.
21
+ * 6. Hand off SIGINT to keep the bot running until the user Ctrl-Cs.
22
+ */
23
+ export async function runPairFlow(ctx) {
24
+ const session = ctx.session;
25
+ const channel = new TelegramChannel({
26
+ vault: ctx.deps.vault,
27
+ token: ctx.deps.options?.['token'] ?? undefined,
28
+ logger: ctx.deps.logger,
29
+ });
30
+ session.setPermissionResolver(channel.permissionResolver);
31
+ // Subscribe BEFORE start so the first scan can't fire before us.
32
+ let pairedResolve = null;
33
+ const paired = new Promise((resolve) => {
34
+ pairedResolve = resolve;
35
+ });
36
+ const unsubscribe = channel.onPaired((chatId) => {
37
+ pairedResolve?.(chatId);
38
+ pairedResolve = null;
39
+ });
40
+ outro(dim('opening pairing window...'));
41
+ // `pair: true` opens the host-issued QR window and resolves the bot's deep link.
42
+ const handle = await channel.start({ session, pair: true });
43
+ // From here on we own the bot lifecycle. Any failure path needs to
44
+ // call stopBot() before returning.
45
+ const stopBot = async () => {
46
+ unsubscribe();
47
+ try {
48
+ await handle.stop('wizard');
49
+ }
50
+ catch {
51
+ /* ignore */
52
+ }
53
+ };
54
+ // Already paired (no window was opened): there's nothing to scan. Tell the
55
+ // user how to re-pair rather than hanging on a pairing that can't happen.
56
+ if (channel.connected) {
57
+ log.info('This bot is already paired. Run `moxxy channels telegram unpair` first to pair a different chat.');
58
+ await stopBot();
59
+ return 0;
60
+ }
61
+ const url = channel.requestUrl;
62
+ if (!url) {
63
+ log.error('Could not resolve the bot link — check the token is valid and the network is reachable.');
64
+ await stopBot();
65
+ return 1;
66
+ }
67
+ await printPairQr(url);
68
+ log.info('Scan the QR with your phone (or open the link and tap START) — your chat pairs automatically.');
69
+ // Graceful Ctrl-C while waiting (or once running): stop the bot and exit.
70
+ let stopping = false;
71
+ const shutdown = async () => {
72
+ if (stopping)
73
+ return;
74
+ stopping = true;
75
+ await stopBot();
76
+ await session.close('SIGINT').catch(() => undefined);
77
+ process.exit(0);
78
+ };
79
+ const onSignal = () => void shutdown();
80
+ process.once('SIGINT', onSignal);
81
+ process.once('SIGTERM', onSignal);
82
+ const spin = spinner();
83
+ spin.start('Waiting for you to pair in Telegram...');
84
+ const chatId = await paired;
85
+ spin.stop(`Paired ✓ — chat ${chatId} is authorized.`);
86
+ if (exitAfterPairRequested(ctx)) {
87
+ // Orchestrated pairing (`moxxy onboard`): hand control back — the caller
88
+ // starts the bot under its own service afterwards. Our SIGINT handlers
89
+ // would `process.exit` the orchestrator, so drop them first.
90
+ process.removeListener('SIGINT', onSignal);
91
+ process.removeListener('SIGTERM', onSignal);
92
+ await stopBot();
93
+ return 0;
94
+ }
95
+ log.info('Bot is running. Press Ctrl+C to stop.');
96
+ try {
97
+ // Only reached if the bot stops on its own (a signal path exits via
98
+ // shutdown()); remove our handlers so they don't outlive this flow.
99
+ await handle.running;
100
+ return 0;
101
+ }
102
+ finally {
103
+ process.removeListener('SIGINT', onSignal);
104
+ process.removeListener('SIGTERM', onSignal);
105
+ }
106
+ }
107
+ /** Render the pairing deep link as a scannable terminal QR + the plain link. */
108
+ async function printPairQr(url) {
109
+ let qr = '';
110
+ try {
111
+ qr = await QRCode.toString(url, { type: 'terminal', small: true });
112
+ }
113
+ catch {
114
+ qr = '';
115
+ }
116
+ // CLI surface — intentional stdout.
117
+ console.log(['', qr, ` link: ${url}`, ''].join('\n'));
118
+ }
119
+ //# sourceMappingURL=pair-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pair-flow.js","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Pairing state machine — one mechanism, host-issued QR code.
3
+ *
4
+ * A control surface (the desktop "Channels" panel, or `moxxy channels telegram
5
+ * pair` in a terminal) opens a window with `beginHostIssuedPairing`: the 6-digit
6
+ * code is generated UP FRONT so the surface can embed it in a
7
+ * `t.me/<bot>?start=<code>` deep link and render that as a QR. The user scans /
8
+ * opens the link and taps START — the bot receives `/start <code>` — or simply
9
+ * sends the 6 digits as a message. `submitChatCode` matches the presented code
10
+ * and authorizes that chat.
11
+ *
12
+ * Why this single direction (it replaced an older bot-issues-a-code /
13
+ * paste-in-the-terminal flow): it works identically whether the surface is a GUI
14
+ * with no terminal (the desktop) or a terminal (the `pair` command renders the
15
+ * QR inline) — one affordance everywhere, zero manual code entry — while keeping
16
+ * the "prove you can see the host's screen" security property (the code only
17
+ * ever lived on the surface).
18
+ *
19
+ * The transitions live in `@moxxy/channel-kit` (host-code pairing, generic over
20
+ * the peer id — shared with future channels); this module keeps the
21
+ * Telegram-shaped state/decision surface and maps the kit's semantic action
22
+ * kinds to Telegram's user-facing messages.
23
+ */
24
+ export type PairingPhase = 'idle' | 'awaiting-host-code' | 'paired' | 'expired';
25
+ export interface PairingState {
26
+ phase: PairingPhase;
27
+ code: string | null;
28
+ pendingChatId: number | null;
29
+ expiresAt: number | null;
30
+ authorizedChatId: number | null;
31
+ }
32
+ export interface PairingDecision {
33
+ readonly state: PairingState;
34
+ readonly action: {
35
+ kind: 'reject';
36
+ message: string;
37
+ } | {
38
+ kind: 'paired';
39
+ chatId: number;
40
+ } | {
41
+ kind: 'still-paired';
42
+ chatId: number;
43
+ } | {
44
+ kind: 'mismatch';
45
+ message: string;
46
+ } | {
47
+ kind: 'expired';
48
+ message: string;
49
+ } | {
50
+ kind: 'not-pending';
51
+ message: string;
52
+ };
53
+ }
54
+ export declare function createPairingState(opts?: {
55
+ authorizedChatId?: number | null;
56
+ }): PairingState;
57
+ /**
58
+ * Open a host-issued pairing window. The code is generated immediately so the
59
+ * surface can embed it in the deep link / QR it shows the user.
60
+ *
61
+ * No TTL by default: the window's lifetime is the channel process's lifetime
62
+ * while unpaired (the surface tears it down by stopping the channel), and the
63
+ * security boundary is "could see the host's screen", not a clock. A caller may
64
+ * still pass a `ttlMs` to bound it.
65
+ */
66
+ export declare function beginHostIssuedPairing(state: PairingState, code?: string, now?: number, ttlMs?: number | null): {
67
+ state: PairingState;
68
+ code: string;
69
+ };
70
+ /**
71
+ * Called when the bot receives a BARE `/start` (no code payload) — i.e. the user
72
+ * opened the chat manually rather than via the pairing deep link. The code path
73
+ * for a `/start <code>` payload (and plain-message codes) is `submitChatCode`.
74
+ *
75
+ * Behavior depends on phase:
76
+ * - paired (same chat) → still-paired (already authorized; greet)
77
+ * - paired (other chat) → reject (the bot is owned by someone else)
78
+ * - awaiting-host-code → reject with a nudge to use the QR / send the code
79
+ * - idle / expired → reject with a nudge to start pairing
80
+ */
81
+ export declare function handleStart(state: PairingState, chatId: number): PairingDecision;
82
+ /**
83
+ * Called when a chat PRESENTS a host-issued code — either as the payload of a
84
+ * `/start <code>` deep link or as a plain 6-digit message. On match the chat is
85
+ * authorized.
86
+ */
87
+ export declare function submitChatCode(state: PairingState, chatId: number, rawCode: string, now?: number): PairingDecision;
88
+ export declare function isAuthorized(state: PairingState, chatId: number): boolean;
89
+ export declare function clearPairing(state: PairingState): PairingState;
90
+ //# sourceMappingURL=pairing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,YAAY,GACpB,MAAM,GAGN,oBAAoB,GACpB,QAAQ,GACR,SAAS,CAAC;AAEd,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EACX;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACnC;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAClC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GACxC;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACrC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACpC;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AA8BD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,YAAY,CAOhG;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,EACnB,IAAI,GAAE,MAAsB,EAC5B,GAAG,GAAE,MAAmB,EACxB,KAAK,GAAE,MAAM,GAAG,IAAW,GAC1B;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAGvC;AA6BD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,CAGhF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAmB,GACvB,eAAe,CAGjB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAE9D"}
@@ -0,0 +1,97 @@
1
+ import { randomCode } from '@moxxy/plugin-vault';
2
+ import { clearHostCodePairing, createHostCodeState, greetPeer, isPeerAuthorized, openHostCodeWindow, submitPeerCode, } from '@moxxy/channel-kit';
3
+ const MSG_FOREIGN_CHAT = 'This bot is paired with a different chat. Access denied.';
4
+ const MSG_WINDOW_OPEN_HINT = 'Scan the QR (or open the link) shown in moxxy, or send the 6-digit code it shows, to finish pairing.';
5
+ const MSG_NO_WINDOW = 'No pairing window is open. Start pairing from the moxxy desktop Channels panel, or run `moxxy channels telegram pair`.';
6
+ const MSG_NOT_PENDING = 'No pairing window is open.';
7
+ const MSG_EXPIRED = 'Pairing window expired. Start the channel again to retry.';
8
+ const MSG_MISMATCH = "Code didn't match. Check the digits shown in moxxy and try again.";
9
+ function toKit(state) {
10
+ return {
11
+ phase: state.phase,
12
+ code: state.code,
13
+ expiresAt: state.expiresAt,
14
+ authorizedPeer: state.authorizedChatId,
15
+ };
16
+ }
17
+ function fromKit(state) {
18
+ return {
19
+ phase: state.phase,
20
+ code: state.code,
21
+ pendingChatId: null,
22
+ expiresAt: state.expiresAt,
23
+ authorizedChatId: state.authorizedPeer,
24
+ };
25
+ }
26
+ export function createPairingState(opts = {}) {
27
+ return fromKit(createHostCodeState({
28
+ // Preserve the historical truthy check (a 0 chat id is not a real chat).
29
+ authorizedPeer: opts.authorizedChatId ? opts.authorizedChatId : null,
30
+ }));
31
+ }
32
+ /**
33
+ * Open a host-issued pairing window. The code is generated immediately so the
34
+ * surface can embed it in the deep link / QR it shows the user.
35
+ *
36
+ * No TTL by default: the window's lifetime is the channel process's lifetime
37
+ * while unpaired (the surface tears it down by stopping the channel), and the
38
+ * security boundary is "could see the host's screen", not a clock. A caller may
39
+ * still pass a `ttlMs` to bound it.
40
+ */
41
+ export function beginHostIssuedPairing(state, code = randomCode(6), now = Date.now(), ttlMs = null) {
42
+ const opened = openHostCodeWindow(toKit(state), { code, now, ttlMs });
43
+ return { state: fromKit(opened.state), code: opened.code };
44
+ }
45
+ /** Map a kit action to the Telegram-worded decision, reusing `state` when the
46
+ * machine didn't transition. */
47
+ function mapAction(state, kitState, action) {
48
+ switch (action.kind) {
49
+ case 'paired':
50
+ return { state: fromKit(kitState), action: { kind: 'paired', chatId: action.peer } };
51
+ case 'still-paired':
52
+ return { state, action: { kind: 'still-paired', chatId: action.peer } };
53
+ case 'rejected-foreign-peer':
54
+ return { state, action: { kind: 'reject', message: MSG_FOREIGN_CHAT } };
55
+ case 'window-open-hint':
56
+ return { state, action: { kind: 'reject', message: MSG_WINDOW_OPEN_HINT } };
57
+ case 'no-window':
58
+ return { state, action: { kind: 'reject', message: MSG_NO_WINDOW } };
59
+ case 'not-pending':
60
+ return { state, action: { kind: 'not-pending', message: MSG_NOT_PENDING } };
61
+ case 'expired':
62
+ return { state: fromKit(kitState), action: { kind: 'expired', message: MSG_EXPIRED } };
63
+ case 'mismatch':
64
+ return { state, action: { kind: 'mismatch', message: MSG_MISMATCH } };
65
+ }
66
+ }
67
+ /**
68
+ * Called when the bot receives a BARE `/start` (no code payload) — i.e. the user
69
+ * opened the chat manually rather than via the pairing deep link. The code path
70
+ * for a `/start <code>` payload (and plain-message codes) is `submitChatCode`.
71
+ *
72
+ * Behavior depends on phase:
73
+ * - paired (same chat) → still-paired (already authorized; greet)
74
+ * - paired (other chat) → reject (the bot is owned by someone else)
75
+ * - awaiting-host-code → reject with a nudge to use the QR / send the code
76
+ * - idle / expired → reject with a nudge to start pairing
77
+ */
78
+ export function handleStart(state, chatId) {
79
+ const decision = greetPeer(toKit(state), chatId);
80
+ return mapAction(state, decision.state, decision.action);
81
+ }
82
+ /**
83
+ * Called when a chat PRESENTS a host-issued code — either as the payload of a
84
+ * `/start <code>` deep link or as a plain 6-digit message. On match the chat is
85
+ * authorized.
86
+ */
87
+ export function submitChatCode(state, chatId, rawCode, now = Date.now()) {
88
+ const decision = submitPeerCode(toKit(state), chatId, rawCode, now);
89
+ return mapAction(state, decision.state, decision.action);
90
+ }
91
+ export function isAuthorized(state, chatId) {
92
+ return isPeerAuthorized(toKit(state), chatId);
93
+ }
94
+ export function clearPairing(state) {
95
+ return fromKit(clearHostCodePairing(toKit(state)));
96
+ }
97
+ //# sourceMappingURL=pairing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { PendingToolCall, PermissionContext, PermissionDecision, PermissionResolver } from '@moxxy/sdk';
2
+ export interface PendingPermission {
3
+ readonly callId: string;
4
+ readonly call: PendingToolCall;
5
+ readonly ctx: PermissionContext;
6
+ readonly resolve: (decision: PermissionDecision) => void;
7
+ }
8
+ /**
9
+ * Permission resolver that defers each tool call to an external decider (the
10
+ * Telegram bot, which renders an inline keyboard and waits for a callback).
11
+ * The Telegram channel registers a callback handler that invokes
12
+ * `resolvePending(callId, decision)` when the user clicks a button.
13
+ */
14
+ export declare class TelegramPermissionResolver implements PermissionResolver {
15
+ readonly name = "telegram";
16
+ private readonly pending;
17
+ private readonly sessionAllows;
18
+ private deciderFn;
19
+ setDecider(fn: (call: PendingToolCall, ctx: PermissionContext) => Promise<void>): void;
20
+ check(call: PendingToolCall, ctx: PermissionContext): Promise<PermissionDecision>;
21
+ /** Called by the bot when a permission callback button is clicked. */
22
+ resolvePending(callId: string, decision: PermissionDecision): boolean;
23
+ abortAll(reason?: string): void;
24
+ }
25
+ //# sourceMappingURL=permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE7G,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC1D;AAED;;;;;GAKG;AACH,qBAAa,0BAA2B,YAAW,kBAAkB;IACnE,QAAQ,CAAC,IAAI,cAAc;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;IAChE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,SAAS,CAED;IAEhB,UAAU,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAIhF,KAAK,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAkBvF,sEAAsE;IACtE,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO;IAQrE,QAAQ,CAAC,MAAM,SAAmB,GAAG,IAAI;CAM1C"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Permission resolver that defers each tool call to an external decider (the
3
+ * Telegram bot, which renders an inline keyboard and waits for a callback).
4
+ * The Telegram channel registers a callback handler that invokes
5
+ * `resolvePending(callId, decision)` when the user clicks a button.
6
+ */
7
+ export class TelegramPermissionResolver {
8
+ name = 'telegram';
9
+ pending = new Map();
10
+ sessionAllows = new Set();
11
+ deciderFn = null;
12
+ setDecider(fn) {
13
+ this.deciderFn = fn;
14
+ }
15
+ async check(call, ctx) {
16
+ if (this.sessionAllows.has(call.name)) {
17
+ return { mode: 'allow_session', reason: 'allow_session previously granted' };
18
+ }
19
+ if (!this.deciderFn) {
20
+ return { mode: 'deny', reason: 'no decider attached (bot not running)' };
21
+ }
22
+ const decision = await new Promise((resolve) => {
23
+ this.pending.set(call.callId, { callId: call.callId, call, ctx, resolve });
24
+ this.deciderFn(call, ctx).catch((err) => {
25
+ this.pending.delete(call.callId);
26
+ resolve({ mode: 'deny', reason: err instanceof Error ? err.message : String(err) });
27
+ });
28
+ });
29
+ if (decision.mode === 'allow_session')
30
+ this.sessionAllows.add(call.name);
31
+ return decision;
32
+ }
33
+ /** Called by the bot when a permission callback button is clicked. */
34
+ resolvePending(callId, decision) {
35
+ const pending = this.pending.get(callId);
36
+ if (!pending)
37
+ return false;
38
+ this.pending.delete(callId);
39
+ pending.resolve(decision);
40
+ return true;
41
+ }
42
+ abortAll(reason = 'channel closed') {
43
+ for (const pending of this.pending.values()) {
44
+ pending.resolve({ mode: 'deny', reason });
45
+ }
46
+ this.pending.clear();
47
+ }
48
+ }
49
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,MAAM,OAAO,0BAA0B;IAC5B,IAAI,GAAG,UAAU,CAAC;IACV,OAAO,GAAG,IAAI,GAAG,EAA6B,CAAC;IAC/C,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;IAC3C,SAAS,GAEN,IAAI,CAAC;IAEhB,UAAU,CAAC,EAAoE;QAC7E,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAqB,EAAE,GAAsB;QACvD,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,kCAAkC,EAAE,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,uCAAuC,EAAE,CAAC;QAC3E,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,OAAO,CAAqB,CAAC,OAAO,EAAE,EAAE;YACjE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3E,IAAI,CAAC,SAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACvC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACjC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACtF,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,IAAI,QAAQ,CAAC,IAAI,KAAK,eAAe;YAAE,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,sEAAsE;IACtE,cAAc,CAAC,MAAc,EAAE,QAA4B;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,QAAQ,CAAC,MAAM,GAAG,gBAAgB;QAChC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5C,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,68 @@
1
+ import type { MoxxyEvent } from '@moxxy/sdk';
2
+ export interface RenderedFrame {
3
+ /** Pre-formatted HTML activity block (blockquote of tool calls). */
4
+ readonly activityHtml: string;
5
+ /** Assistant body in original markdown — caller converts to Telegram HTML. */
6
+ readonly body: string;
7
+ /** Pre-formatted HTML error line. */
8
+ readonly errorHtml: string;
9
+ /**
10
+ * Pre-formatted HTML diff block(s) for any file-diff tool results in
11
+ * this turn. Each is a summary line plus a `<pre><code
12
+ * class="language-diff">…</code></pre>` fence; Telegram highlights the
13
+ * `+`/`-` lines for the `diff` language. Empty string when no file was
14
+ * edited. Composed after the body — see `composeFrame`.
15
+ */
16
+ readonly diffHtml: string;
17
+ /** True if any of the above changed since the previous frame. */
18
+ readonly hasUpdate: boolean;
19
+ }
20
+ export declare class TurnRenderer {
21
+ private chunks;
22
+ /** In-flight + completed tool calls keyed by callId, in arrival order. */
23
+ private tools;
24
+ private skillBanner;
25
+ private notices;
26
+ private finalAssistant;
27
+ private errorLine;
28
+ /** Pre-formatted HTML diff blocks, one per file-diff tool result, in arrival order. */
29
+ private diffBlocks;
30
+ private lastFrame;
31
+ accept(event: MoxxyEvent): RenderedFrame;
32
+ /**
33
+ * Compose the current frame. `collapse` (set on the final flush) folds a
34
+ * long tool-activity trace into an expandable box so the finished message
35
+ * leads with the answer; during streaming it's left false so the activity
36
+ * stays fully visible while work is in flight.
37
+ */
38
+ snapshot(opts?: {
39
+ collapse?: boolean;
40
+ }): RenderedFrame;
41
+ /**
42
+ * Format the tool/skill activity block. Returns null when there's
43
+ * nothing to show — keeps frames lean for chat-only turns.
44
+ *
45
+ * The block is rendered with HTML tags (`<b>`, `<code>`, `<i>`) so
46
+ * it survives `markdownToTelegramHtml` intact (the function escapes
47
+ * `<` `>` `&` in text segments but leaves existing tags alone via
48
+ * the fence/inline placeholder dance). Use `__inert__` markers so
49
+ * already-formed HTML doesn't get re-escaped.
50
+ *
51
+ * When `collapse` is set (final frame) and the trace ran long, fold it
52
+ * into a `<blockquote expandable>` topped by a one-line summary — the
53
+ * tool noise becomes a tap-to-open box and the assistant's answer leads.
54
+ */
55
+ private renderActivity;
56
+ reset(): void;
57
+ }
58
+ /**
59
+ * Split composed Telegram HTML into <=`limit`-char parts that are each
60
+ * independently valid `parse_mode=HTML`. Cuts are chosen so they never land
61
+ * inside a `<...>` tag or an `&...;` entity; if a block-level tag (e.g. a
62
+ * `<pre><code class="language-diff">` fence or a `<blockquote>`) is open at the
63
+ * boundary it is closed in the head part and reopened in the tail. For plain
64
+ * text with no open tags this reduces to the old newline-preferring behaviour
65
+ * and `parts.join('')` reconstructs the input exactly.
66
+ */
67
+ export declare function splitForTelegram(text: string, limit?: number): string[];
68
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAiB7C,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAyCD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,0EAA0E;IAC1E,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,SAAS,CAAuB;IACxC,uFAAuF;IACvF,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,SAAS,CAAM;IAEvB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa;IA6ExC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,GAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,aAAa;IAQ1D;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,cAAc;IA0BtB,KAAK,IAAI,IAAI;CAUd;AAkOD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAA+B,GAAG,MAAM,EAAE,CA0D/F"}