@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.
- package/LICENSE +21 -0
- package/dist/approval.d.ts +37 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +75 -0
- package/dist/approval.js.map +1 -0
- package/dist/channel/approval-prompt.d.ts +20 -0
- package/dist/channel/approval-prompt.d.ts.map +1 -0
- package/dist/channel/approval-prompt.js +34 -0
- package/dist/channel/approval-prompt.js.map +1 -0
- package/dist/channel/callback-handler.d.ts +32 -0
- package/dist/channel/callback-handler.d.ts.map +1 -0
- package/dist/channel/callback-handler.js +212 -0
- package/dist/channel/callback-handler.js.map +1 -0
- package/dist/channel/frame-pump.d.ts +58 -0
- package/dist/channel/frame-pump.d.ts.map +1 -0
- package/dist/channel/frame-pump.js +159 -0
- package/dist/channel/frame-pump.js.map +1 -0
- package/dist/channel/html.d.ts +24 -0
- package/dist/channel/html.d.ts.map +1 -0
- package/dist/channel/html.js +62 -0
- package/dist/channel/html.js.map +1 -0
- package/dist/channel/pairing-handler.d.ts +66 -0
- package/dist/channel/pairing-handler.d.ts.map +1 -0
- package/dist/channel/pairing-handler.js +147 -0
- package/dist/channel/pairing-handler.js.map +1 -0
- package/dist/channel/permission-prompt.d.ts +21 -0
- package/dist/channel/permission-prompt.d.ts.map +1 -0
- package/dist/channel/permission-prompt.js +36 -0
- package/dist/channel/permission-prompt.js.map +1 -0
- package/dist/channel/slash-handler.d.ts +47 -0
- package/dist/channel/slash-handler.d.ts.map +1 -0
- package/dist/channel/slash-handler.js +214 -0
- package/dist/channel/slash-handler.js.map +1 -0
- package/dist/channel/text-handler.d.ts +43 -0
- package/dist/channel/text-handler.d.ts.map +1 -0
- package/dist/channel/text-handler.js +116 -0
- package/dist/channel/text-handler.js.map +1 -0
- package/dist/channel/turn-runner.d.ts +40 -0
- package/dist/channel/turn-runner.d.ts.map +1 -0
- package/dist/channel/turn-runner.js +67 -0
- package/dist/channel/turn-runner.js.map +1 -0
- package/dist/channel/typing-indicator.d.ts +14 -0
- package/dist/channel/typing-indicator.d.ts.map +1 -0
- package/dist/channel/typing-indicator.js +29 -0
- package/dist/channel/typing-indicator.js.map +1 -0
- package/dist/channel/voice-handler.d.ts +42 -0
- package/dist/channel/voice-handler.d.ts.map +1 -0
- package/dist/channel/voice-handler.js +138 -0
- package/dist/channel/voice-handler.js.map +1 -0
- package/dist/channel.d.ts +118 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +478 -0
- package/dist/channel.js.map +1 -0
- package/dist/channel.test-d.d.ts +2 -0
- package/dist/channel.test-d.d.ts.map +1 -0
- package/dist/channel.test-d.js +5 -0
- package/dist/channel.test-d.js.map +1 -0
- package/dist/format.d.ts +38 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +237 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +37 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +42 -0
- package/dist/keys.js.map +1 -0
- package/dist/pair-flow.d.ts +18 -0
- package/dist/pair-flow.d.ts.map +1 -0
- package/dist/pair-flow.js +119 -0
- package/dist/pair-flow.js.map +1 -0
- package/dist/pairing.d.ts +90 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +97 -0
- package/dist/pairing.js.map +1 -0
- package/dist/permission.d.ts +25 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +49 -0
- package/dist/permission.js.map +1 -0
- package/dist/render.d.ts +68 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +439 -0
- package/dist/render.js.map +1 -0
- package/dist/setup-wizard.d.ts +21 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +140 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +91 -0
- package/src/approval.test.ts +103 -0
- package/src/approval.ts +86 -0
- package/src/channel/approval-prompt.ts +46 -0
- package/src/channel/callback-handler.test.ts +188 -0
- package/src/channel/callback-handler.ts +256 -0
- package/src/channel/frame-pump.ts +176 -0
- package/src/channel/html.test.ts +40 -0
- package/src/channel/html.ts +60 -0
- package/src/channel/pairing-handler.ts +181 -0
- package/src/channel/permission-prompt.ts +51 -0
- package/src/channel/slash-handler.ts +259 -0
- package/src/channel/text-handler.test.ts +222 -0
- package/src/channel/text-handler.ts +174 -0
- package/src/channel/turn-runner.ts +102 -0
- package/src/channel/typing-indicator.ts +31 -0
- package/src/channel/voice-handler.test.ts +328 -0
- package/src/channel/voice-handler.ts +198 -0
- package/src/channel/voice-reply.test.ts +223 -0
- package/src/channel.test-d.ts +7 -0
- package/src/channel.ts +564 -0
- package/src/format.test.ts +159 -0
- package/src/format.ts +257 -0
- package/src/index.ts +347 -0
- package/src/keys.ts +52 -0
- package/src/pair-flow.ts +131 -0
- package/src/pairing.test.ts +113 -0
- package/src/pairing.ts +184 -0
- package/src/permission.test.ts +56 -0
- package/src/permission.ts +61 -0
- package/src/render.test.ts +350 -0
- package/src/render.ts +522 -0
- package/src/setup-wizard.ts +178 -0
- 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
|
package/dist/keys.js.map
ADDED
|
@@ -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"}
|
package/dist/pairing.js
ADDED
|
@@ -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"}
|
package/dist/render.d.ts
ADDED
|
@@ -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"}
|