@llblab/pi-telegram 0.7.0 → 0.7.1

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/AGENTS.md CHANGED
@@ -133,7 +133,8 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
133
133
  - Shared inline-keyboard structure belongs to `keyboard`; application-control button labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue` while core queue mechanics stay in `queue`
134
134
  - Inbound files may become π image inputs or configured attachment-handler text before queueing; outbound files must flow through `telegram_attach`
135
135
  - Inbound attachment handlers and command-backed outbound handlers use command templates as the standard integration contract; built-in outbound buttons use inline keyboards plus callback routing because no external command execution is needed
136
- - Telegram prompt-template commands are discovered from π slash commands with `source: "prompt"`; π template names are mapped to Bot API-compatible aliases (`fix-tests` → `/fix_tests`), aliases that conflict with built-in bridge commands or hidden shortcuts are not registered/displayed, and the bridge expands template files before queueing because extension-originated `sendUserMessage()` bypasses π's interactive template expansion
136
+ - Telegram prompt-template commands are discovered from π slash commands with `source: "prompt"`; π template names are mapped to Bot API-compatible aliases (`fix-tests` → `/fix_tests`), aliases that conflict with built-in bridge commands or hidden shortcuts are not displayed, prompt-template aliases stay out of the Telegram bot command menu, and the bridge expands template files before queueing because extension-originated `sendUserMessage()` bypasses π's interactive template expansion
137
+ - Unknown callback data not owned by pi-telegram prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) may be forwarded as `[callback] <data>` after built-in handlers decline it; external extensions should follow `docs/callback-namespaces.md` and must not poll the same bot independently
137
138
  - Command templates stay compact and shell-free: no `command` field, no shell execution, inline defaults are allowed as `{name=default}`, `template` may be a string or an ordered composition array, only `args`/`defaults` inherit into leaves, top-level `timeout` wraps composed sequences, stdout pipes to the next step's stdin by default, and multi-step work should use `template: [...]` rather than provider-specific fields; `pipe` is only a legacy local alias
138
139
  - Command-template documentation examples should use portable executable placeholders such as `/path/to/stt` and `/path/to/tts`, not host-local skill paths or machine-specific install locations
139
140
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1: Layered Callback Interop
4
+
5
+ - `[Callback Interop]` Unknown Telegram inline-button callback data that does not belong to pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) is now forwarded to π as `[callback] <data>` after assistant-button, queue-menu, and app-menu handlers decline it. `docs/callback-namespaces.md` defines the shared callback namespace standard for layered extensions. Impact: layered π extensions can namespace and handle their own Telegram inline buttons without polling the same bot or forking pi-telegram.
6
+ - `[Prompt Templates]` Prompt-template aliases stay visible only inside `/start` and are no longer registered in the Telegram bot command menu. Impact: reusable π workflows remain discoverable without making Telegram's global command menu noisy.
7
+ - `[Package]` Bumped package metadata to `0.7.1` through npm and kept the lockfile in sync.
8
+
3
9
  ## 0.7.0: Unified App Menu & Command Template Hardening
4
10
 
5
11
  - `[Commands]` Visible Telegram bot command menu now exposes `/start`, `/compact`, `/next`, `/continue`, `/abort`, and `/stop`; `/help`, `/status`, `/model`, `/thinking`, and `/queue` remain hidden compatibility shortcuts. `/start`, `/help`, and `/status` open one unified app menu containing command help, status rows, and inline controls. Command emoji are centralized as fixed adornments in the commands domain and reused by matching menu buttons (`🤖` model, `🧠` thinking); `/next` uses `⏩` and `/continue` uses `▶️`. `/continue` enqueues a priority Telegram-owned `continue` prompt instead of forcing the next queued item or requiring π to be idle. Impact: the visible command surface is cleaner while existing operator muscle memory still works and skills can react to queued `continue` prompts.
package/README.md CHANGED
@@ -84,7 +84,7 @@ Use these inside the Telegram DM with your bot:
84
84
  - **`/stop`**: Abort the active run and clear all waiting Telegram queue items.
85
85
  - **`/abort`**: Abort the active run without touching the queue.
86
86
 
87
- Prompt-template commands: π prompt templates are mapped to Telegram-safe aliases (`fix-tests.md` becomes `/fix_tests`) and shown as compact command-only rows between the built-in commands and status rows in `/start`, then registered in the Telegram bot command menu unless the mapped name conflicts with a built-in Telegram bridge command or hidden shortcut. Sending `/template_name args` from Telegram expands the matching π prompt-template file and queues the expanded prompt like normal Telegram work.
87
+ Prompt-template commands: π prompt templates are mapped to Telegram-safe aliases (`fix-tests.md` becomes `/fix_tests`) and shown as compact command-only rows between the built-in commands and status rows in `/start`. They are not registered in the Telegram bot command menu, keeping the bot menu focused on bridge controls. Sending `/template_name args` from Telegram expands the matching π prompt-template file and queues the expanded prompt like normal Telegram work.
88
88
 
89
89
  Hidden compatibility shortcuts: `/help` and `/status` open the same main application menu, `/model` opens the model section, `/thinking` opens the thinking section, and `/queue` opens the queue section. They are intentionally not shown in the bot command menu.
90
90
 
@@ -190,7 +190,7 @@ List the main risks first.
190
190
  <!-- telegram_button: OK -->
191
191
  ```
192
192
 
193
- Button prompts are routed back into the normal Telegram queue as prompt turns. Keep the opening comment unclosed until the body-ending `-->` for body-form buttons. Closed heads must use `prompt="..."` or the colon shorthand to create a button. Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
193
+ Button prompts are routed back into the normal Telegram queue as prompt turns. Keep the opening comment unclosed until the body-ending `-->` for body-form buttons. Closed heads must use `prompt="..."` or the colon shorthand to create a button. Unknown inline-button callbacks that do not belong to pi-telegram are forwarded to π as `[callback] <data>` so other extensions can namespace and handle their own Telegram buttons without polling the bot themselves; see the [Callback Namespace Standard](./docs/callback-namespaces.md). Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
194
194
 
195
195
  ## Streaming
196
196
 
package/docs/README.md CHANGED
@@ -9,3 +9,4 @@ Living index of project documentation in `/docs`.
9
9
  - [attachment-handlers.md](./attachment-handlers.md) — Local `pi-telegram` attachment-handler config, placeholders, and fallbacks
10
10
  - [outbound-handlers.md](./outbound-handlers.md) — Local `pi-telegram` outbound-handler config, voice/button markup, artifact outputs, and callback routing
11
11
  - [locks.md](./locks.md) — Shared `locks.json` standard for singleton extension ownership
12
+ - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
@@ -117,7 +117,7 @@ Dispatch is gated by:
117
117
 
118
118
  This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Post-agent-end dispatch retries are scheduled through a session-bound deferred dispatcher that activates on session start, cancels timers on session shutdown, and skips callbacks from older generations before they touch `ExtensionContext`. Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, and `/queue` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
119
119
 
120
- `/start` opens the main application menu: visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections. The Queue button includes the current queued-item count. Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens. Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source. Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, are registered in the Telegram bot command menu when the mapped command does not conflict with built-in bridge commands or hidden shortcuts, and expand before queueing because `ExtensionAPI.sendUserMessage()` intentionally bypasses π prompt-template expansion for extension-originated messages. Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard; model-menu scope and pagination controls sit directly under that top row before model choices, and tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`. `menu-model` owns model-menu state, scoped model pages, model callback planning, page-picker rendering, and model-menu rendering while `model` owns core model identity/switching semantics. `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering. `menu-status` owns status-menu payloads, status callback handling, and status-message rendering. `menu-queue` owns queue-menu UI only: queue items are rendered under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order, numbered, and marked with `⚡` for priority prompts or `📎` for prompts with attachments. An empty queue renders bold message text plus the top Main menu button, not a disabled empty-state button. Selecting an item opens a submenu that displays the full queued prompt text with Back, priority toggle, and Cancel. If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
120
+ `/start` opens the main application menu: visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections. The Queue button includes the current queued-item count. Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens. Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source. Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` intentionally bypasses π prompt-template expansion for extension-originated messages. Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard; model-menu scope and pagination controls sit directly under that top row before model choices, and tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`. `menu-model` owns model-menu state, scoped model pages, model callback planning, page-picker rendering, and model-menu rendering while `model` owns core model identity/switching semantics. `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering. `menu-status` owns status-menu payloads, status callback handling, and status-message rendering. `menu-queue` owns queue-menu UI only: queue items are rendered under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order, numbered, and marked with `⚡` for priority prompts or `📎` for prompts with attachments. An empty queue renders bold message text plus the top Main menu button, not a disabled empty-state button. Selecting an item opens a submenu that displays the full queued prompt text with Back, priority toggle, and Cancel. If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
121
121
 
122
122
  ### Abort Behavior
123
123
 
@@ -160,7 +160,7 @@ Telegram prompt responses use explicit delivery context to attach outbound text,
160
160
 
161
161
  Outbound files are sent only after the active Telegram turn completes, must be staged through the `telegram_attach` tool, are staged atomically per tool call, are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`, and use file-backed multipart blobs so large sends do not require preloading whole files into memory.
162
162
 
163
- Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes body text, `text="..."`, or colon shorthand through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, the default command timeout applies automatically, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools.
163
+ Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes body text, `text="..."`, or colon shorthand through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, the default command timeout applies automatically, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it, giving layered extensions a simple namespaced button channel without separate polling; layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools.
164
164
 
165
165
  ## Interactive Controls
166
166
 
@@ -0,0 +1,36 @@
1
+ # Callback Namespace Standard
2
+
3
+ Telegram `callback_data` is one bot-wide namespace. Any extension that creates inline buttons for a bot shared with `pi-telegram` must use namespaced callback data.
4
+
5
+ ## Format
6
+
7
+ ```text
8
+ <namespace>:<action>[:<payload>]
9
+ ```
10
+
11
+ Examples:
12
+
13
+ ```text
14
+ vividfish:approve:123
15
+ vividfish:deny:123
16
+ myext:page:2
17
+ ```
18
+
19
+ ## Rules
20
+
21
+ - Use a stable extension-owned namespace, preferably the package or extension name without scope punctuation.
22
+ - Keep the namespace lowercase ASCII: `a-z`, `0-9`, `_`, `-`.
23
+ - Do not use `pi-telegram` owned prefixes: `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`.
24
+ - Keep the full `callback_data` within Telegram's 64-byte limit.
25
+ - Put only opaque ids or small enum values in payloads; do not store secrets, full prompts, or large state.
26
+ - Treat callbacks as untrusted input. Validate namespace, action, and payload before executing side effects.
27
+
28
+ ## pi-telegram fallback
29
+
30
+ If `pi-telegram` receives callback data that is not owned by its built-in prefixes and no built-in handler consumes it, it forwards the click to π as:
31
+
32
+ ```text
33
+ [callback] <callback_data>
34
+ ```
35
+
36
+ Layered extensions may intercept that message and handle their own namespace. If no extension handles it, the assistant may see the fallback message and should tell the user the callback was not handled and the environment may be misconfigured.
package/index.ts CHANGED
@@ -297,6 +297,7 @@ export default function (pi: Pi.ExtensionAPI) {
297
297
  getThinkingLevel,
298
298
  setThinkingLevel,
299
299
  setModel,
300
+ sendUserMessage,
300
301
  isIdle,
301
302
  hasPendingMessages,
302
303
  compact,
@@ -259,7 +259,9 @@ function getTelegramAttachmentCompositionStepTimeout(
259
259
  startedAt,
260
260
  );
261
261
  const stepTimeout = getTelegramAttachmentHandlerConfiguredTimeout(step);
262
- return stepTimeout === undefined ? remaining : Math.min(stepTimeout, remaining);
262
+ return stepTimeout === undefined
263
+ ? remaining
264
+ : Math.min(stepTimeout, remaining);
263
265
  }
264
266
 
265
267
  function getTelegramAttachmentHandlerKind(
@@ -300,7 +302,9 @@ async function executeTelegramAttachmentHandlerInvocation(
300
302
  const result = await deps.execCommand(invocation.command, invocation.args, {
301
303
  cwd,
302
304
  timeout,
303
- ...(typeof handler === "object" && handler.retry !== undefined ? { retry: handler.retry } : {}),
305
+ ...(typeof handler === "object" && handler.retry !== undefined
306
+ ? { retry: handler.retry }
307
+ : {}),
304
308
  ...(stdin !== undefined ? { stdin } : {}),
305
309
  });
306
310
  if (result.code !== 0)
@@ -210,7 +210,12 @@ export async function execCommandTemplate(
210
210
  options: CommandTemplateExecOptions = {},
211
211
  ): Promise<CommandTemplateExecResult> {
212
212
  const maxAttempts = options.retry ?? 1;
213
- let lastResult: CommandTemplateExecResult = { stdout: "", stderr: "", code: 1, killed: false };
213
+ let lastResult: CommandTemplateExecResult = {
214
+ stdout: "",
215
+ stderr: "",
216
+ code: 1,
217
+ killed: false,
218
+ };
214
219
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
215
220
  const result = await execCommandTemplateOnce(command, args, options);
216
221
  if (result.code === 0) return result;
package/lib/commands.ts CHANGED
@@ -61,84 +61,61 @@ function formatTelegramBotCommandDescription(
61
61
  return `${formatTelegramCommandEmojiPrefix(command)}${description}`;
62
62
  }
63
63
 
64
- export const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [
65
- {
66
- command: "start",
67
- description: formatTelegramBotCommandDescription(
68
- "start",
69
- "Open menu / Pair bridge",
70
- ),
71
- },
72
- {
73
- command: "compact",
74
- description: formatTelegramBotCommandDescription(
75
- "compact",
76
- "Compact current session",
77
- ),
78
- },
79
- {
80
- command: "next",
81
- description: formatTelegramBotCommandDescription(
82
- "next",
83
- "Force next turn",
84
- ),
85
- },
86
- {
87
- command: "continue",
88
- description: formatTelegramBotCommandDescription(
89
- "continue",
90
- "Queue continue prompt",
91
- ),
92
- },
93
- {
94
- command: "abort",
95
- description: formatTelegramBotCommandDescription("abort", "Abort π"),
96
- },
97
- {
98
- command: "stop",
99
- description: formatTelegramBotCommandDescription(
100
- "stop",
101
- "Abort π & Clear queue",
102
- ),
103
- },
104
- ];
64
+ export const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] =
65
+ [
66
+ {
67
+ command: "start",
68
+ description: formatTelegramBotCommandDescription(
69
+ "start",
70
+ "Open menu / Pair bridge",
71
+ ),
72
+ },
73
+ {
74
+ command: "compact",
75
+ description: formatTelegramBotCommandDescription(
76
+ "compact",
77
+ "Compact current session",
78
+ ),
79
+ },
80
+ {
81
+ command: "next",
82
+ description: formatTelegramBotCommandDescription(
83
+ "next",
84
+ "Force next turn",
85
+ ),
86
+ },
87
+ {
88
+ command: "continue",
89
+ description: formatTelegramBotCommandDescription(
90
+ "continue",
91
+ "Queue continue prompt",
92
+ ),
93
+ },
94
+ {
95
+ command: "abort",
96
+ description: formatTelegramBotCommandDescription("abort", "Abort π"),
97
+ },
98
+ {
99
+ command: "stop",
100
+ description: formatTelegramBotCommandDescription(
101
+ "stop",
102
+ "Abort π & Clear queue",
103
+ ),
104
+ },
105
+ ];
105
106
 
106
107
  export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS;
107
108
 
108
- const TELEGRAM_MAX_BOT_COMMANDS = 100;
109
- const TELEGRAM_BOT_COMMAND_DESCRIPTION_LIMIT = 256;
110
-
111
- function truncateTelegramBotCommandDescription(description: string): string {
112
- if (description.length <= TELEGRAM_BOT_COMMAND_DESCRIPTION_LIMIT) return description;
113
- return `${description.slice(0, TELEGRAM_BOT_COMMAND_DESCRIPTION_LIMIT - 1)}…`;
114
- }
115
-
116
- export function buildTelegramBotCommands(
117
- promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
118
- ): TelegramBotCommandDefinition[] {
119
- const remainingSlots = TELEGRAM_MAX_BOT_COMMANDS - TELEGRAM_BUILTIN_BOT_COMMANDS.length;
120
- const templateCommands = promptTemplates.slice(0, remainingSlots).map((template) => ({
121
- command: template.command,
122
- description: truncateTelegramBotCommandDescription(
123
- `🧩 ${template.description?.trim() || "Prompt template"}`,
124
- ),
125
- }));
126
- return [...TELEGRAM_BUILTIN_BOT_COMMANDS, ...templateCommands];
127
- }
128
-
129
109
  export interface TelegramBotCommandRegistrationDeps {
130
110
  setMyCommands: (
131
111
  commands: readonly TelegramBotCommandDefinition[],
132
112
  ) => Promise<unknown>;
133
- getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
134
113
  }
135
114
 
136
115
  export async function registerTelegramBotCommands(
137
116
  deps: TelegramBotCommandRegistrationDeps,
138
117
  ): Promise<void> {
139
- await deps.setMyCommands(
140
- buildTelegramBotCommands(deps.getPromptTemplateCommands?.()),
141
- );
118
+ await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
142
119
  }
143
120
 
144
121
  export function createTelegramBotCommandRegistrar(
@@ -267,7 +244,10 @@ const TELEGRAM_RESERVED_COMMAND_NAME_SET = new Set<string>(
267
244
  export function isTelegramReservedCommandName(
268
245
  commandName: string | undefined,
269
246
  ): commandName is TelegramReservedCommandName {
270
- return commandName !== undefined && TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName);
247
+ return (
248
+ commandName !== undefined &&
249
+ TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName)
250
+ );
271
251
  }
272
252
 
273
253
  export type TelegramCommandAction =
@@ -600,8 +580,10 @@ export function buildTelegramAppMenuHtml(
600
580
  statusHtml: string,
601
581
  promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
602
582
  ): string {
603
- const promptTemplateHtml = buildTelegramPromptTemplateMenuHtml(promptTemplates);
604
- if (!promptTemplateHtml) return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${statusHtml}`;
583
+ const promptTemplateHtml =
584
+ buildTelegramPromptTemplateMenuHtml(promptTemplates);
585
+ if (!promptTemplateHtml)
586
+ return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${statusHtml}`;
605
587
  return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
606
588
  }
607
589
 
@@ -764,7 +746,7 @@ export async function handleTelegramCompactCommand(
764
746
  deps.isCompactionInProgress()
765
747
  ) {
766
748
  await deps.sendTextReply(
767
- "Cannot compact while π or the Telegram queue is busy. Wait for queued turns to finish or send /stop first.",
749
+ "Cannot compact while π or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
768
750
  );
769
751
  return;
770
752
  }
@@ -923,7 +905,6 @@ export function createTelegramCommandHandlerTargetRuntime<
923
905
  setAllowedUserId: deps.setAllowedUserId,
924
906
  registerBotCommands: createTelegramBotCommandRegistrar({
925
907
  setMyCommands: deps.setMyCommands,
926
- getPromptTemplateCommands: deps.getPromptTemplateCommands,
927
908
  }),
928
909
  persistConfig: deps.persistConfig,
929
910
  sendTextReply: commandTargetRuntime.sendTextReply,
@@ -955,7 +936,11 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
955
936
  const firstMessage = messages[0];
956
937
  if (!firstMessage) return;
957
938
  const command = parseTelegramCommand(deps.extractRawText(messages));
958
- const handled = await deps.handleCommand(command?.name, firstMessage, ctx);
939
+ const handled = await deps.handleCommand(
940
+ command?.name,
941
+ firstMessage,
942
+ ctx,
943
+ );
959
944
  if (handled) return;
960
945
  if (command?.name && deps.expandPromptTemplateCommand) {
961
946
  const expanded = deps.expandPromptTemplateCommand(
@@ -964,7 +949,10 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
964
949
  );
965
950
  if (expanded !== undefined) {
966
951
  await deps.enqueueTurn(
967
- [deps.replaceMessageText(firstMessage, expanded), ...messages.slice(1)],
952
+ [
953
+ deps.replaceMessageText(firstMessage, expanded),
954
+ ...messages.slice(1),
955
+ ],
968
956
  ctx,
969
957
  );
970
958
  return;
package/lib/menu-queue.ts CHANGED
@@ -24,11 +24,13 @@ function getTelegramQueueItemPromptText<Context>(
24
24
  item: Queue.TelegramQueueItem<Context>,
25
25
  ): string {
26
26
  if (item.kind !== "prompt") return item.statusSummary;
27
- return item.content
28
- .filter((block) => block.type === "text")
29
- .map((block) => block.text)
30
- .join("\n")
31
- .trim() || item.statusSummary;
27
+ return (
28
+ item.content
29
+ .filter((block) => block.type === "text")
30
+ .map((block) => block.text)
31
+ .join("\n")
32
+ .trim() || item.statusSummary
33
+ );
32
34
  }
33
35
  function toTelegramQueueMenuItems<Context>(
34
36
  items: readonly Queue.TelegramQueueItem<Context>[],
@@ -476,7 +476,9 @@ async function runVoiceReplyCommand(
476
476
  {
477
477
  cwd: options.cwd,
478
478
  timeout: options.timeout,
479
- ...(typeof config === "object" && config.retry !== undefined ? { retry: config.retry } : {}),
479
+ ...(typeof config === "object" && config.retry !== undefined
480
+ ? { retry: config.retry }
481
+ : {}),
480
482
  ...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
481
483
  },
482
484
  );
package/lib/prompts.ts CHANGED
@@ -15,6 +15,7 @@ Inbound context:
15
15
  - \`[telegram]\` marks Telegram-originated messages.
16
16
  - \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
17
17
  - \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains attachment-handler stdout such as transcriptions or extracted text for those attachments.
18
+ - Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
18
19
 
19
20
  Telegram-visible output:
20
21
  - Telegram is often phone-width; prefer narrow table columns because wide monospace tables can become unreadable.
package/lib/queue.ts CHANGED
@@ -817,11 +817,20 @@ export interface TelegramAgentEndHookRuntimeDeps<
817
817
  ) => void;
818
818
  clearPreview: (chatId: number) => Promise<void>;
819
819
  setPreviewPendingText: (text: string) => void;
820
- finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["finalizeMarkdownPreview"];
821
- sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["sendMarkdownReply"];
820
+ finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<
821
+ TTurn,
822
+ TReplyMarkup
823
+ >["finalizeMarkdownPreview"];
824
+ sendMarkdownReply: TelegramAgentEndRuntimeDeps<
825
+ TTurn,
826
+ TReplyMarkup
827
+ >["sendMarkdownReply"];
822
828
  sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
823
829
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
824
- planOutboundReply?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["planOutboundReply"];
830
+ planOutboundReply?: TelegramAgentEndRuntimeDeps<
831
+ TTurn,
832
+ TReplyMarkup
833
+ >["planOutboundReply"];
825
834
  sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
826
835
  }
827
836
 
package/lib/replies.ts CHANGED
@@ -391,7 +391,9 @@ export function dedupSendTextReply(
391
391
  options?: { parseMode?: "HTML" },
392
392
  ) => Promise<number | undefined> {
393
393
  return async (chatId, replyToMessageId, text, options) => {
394
- const effectiveReplyTo = dedup.shouldReply(replyToMessageId) ? replyToMessageId : undefined;
394
+ const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
395
+ ? replyToMessageId
396
+ : undefined;
395
397
  return inner(chatId, effectiveReplyTo, text, options);
396
398
  };
397
399
  }
@@ -412,7 +414,9 @@ export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
412
414
  options?: { replyMarkup?: TReplyMarkup },
413
415
  ) => Promise<number | undefined> {
414
416
  return async (chatId, replyToMessageId, markdown, options) => {
415
- const effectiveReplyTo = dedup.shouldReply(replyToMessageId) ? replyToMessageId : undefined;
417
+ const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
418
+ ? replyToMessageId
419
+ : undefined;
416
420
  return inner(chatId, effectiveReplyTo, markdown, options);
417
421
  };
418
422
  }
package/lib/routing.ts CHANGED
@@ -71,11 +71,14 @@ export interface TelegramInboundRouteRuntimeDeps<
71
71
  text: string,
72
72
  ) => Promise<number | undefined>;
73
73
  setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
74
- getCommands: () => Parameters<typeof PromptTemplates.getTelegramPromptTemplateCommands>[0];
74
+ getCommands: () => Parameters<
75
+ typeof PromptTemplates.getTelegramPromptTemplateCommands
76
+ >[0];
75
77
  downloadFile: Media.DownloadTelegramMessageFilesDeps["downloadFile"];
76
78
  getThinkingLevel: () => Model.ThinkingLevel;
77
79
  setThinkingLevel: (level: Model.ThinkingLevel) => void;
78
80
  setModel: (model: TModel) => Promise<boolean>;
81
+ sendUserMessage?: (message: string) => void;
79
82
  isIdle: (ctx: TContext) => boolean;
80
83
  hasPendingMessages: (ctx: TContext) => boolean;
81
84
  compact: (
@@ -89,6 +92,21 @@ export interface TelegramInboundRouteRuntimeDeps<
89
92
  ) => void;
90
93
  }
91
94
 
95
+ const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
96
+ "menu:",
97
+ "model:",
98
+ "queue:",
99
+ "status:",
100
+ "tgbtn:",
101
+ "thinking:",
102
+ ] as const;
103
+
104
+ function isTelegramOwnedCallbackData(data: string): boolean {
105
+ return TELEGRAM_OWNED_CALLBACK_PREFIXES.some((prefix) =>
106
+ data.startsWith(prefix),
107
+ );
108
+ }
109
+
92
110
  export function createTelegramInboundRouteRuntime<
93
111
  TUpdate extends Updates.TelegramUpdateFlow & {
94
112
  message?: TMessage;
@@ -167,6 +185,16 @@ export function createTelegramInboundRouteRuntime<
167
185
  }
168
186
  const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
169
187
  if (handledByQueue) return;
188
+ const callbackData = query.data;
189
+ if (
190
+ deps.sendUserMessage &&
191
+ callbackData &&
192
+ !isTelegramOwnedCallbackData(callbackData)
193
+ ) {
194
+ deps.sendUserMessage(`[callback] ${callbackData}`);
195
+ await deps.answerCallbackQuery(query.id);
196
+ return;
197
+ }
170
198
  await menuCallbackHandler(query, ctx);
171
199
  };
172
200
  const promptTurnBuilder = Turns.createTelegramPromptTurnRuntimeBuilder<
@@ -206,7 +234,9 @@ export function createTelegramInboundRouteRuntime<
206
234
  deps.queueMutationRuntime.append(continueTurn, ctx);
207
235
  deps.dispatchNextQueuedTelegramTurn(ctx);
208
236
  };
209
- const reservedCommandNames = new Set(Commands.TELEGRAM_RESERVED_COMMAND_NAMES);
237
+ const reservedCommandNames = new Set(
238
+ Commands.TELEGRAM_RESERVED_COMMAND_NAMES,
239
+ );
210
240
  const getPromptTemplateCommands = () =>
211
241
  PromptTemplates.getTelegramPromptTemplateCommands(
212
242
  deps.getCommands(),
package/lib/status.ts CHANGED
@@ -409,13 +409,16 @@ export function buildTelegramStatusBarText(
409
409
  return `${label} ${theme.fg("muted", "disconnected")}`;
410
410
  if (!state.paired)
411
411
  return `${label} ${theme.fg("warning", "awaiting pairing")}`;
412
- const queued = state.queuedStatus ? theme.fg("muted", state.queuedStatus) : "";
412
+ const queued = state.queuedStatus
413
+ ? theme.fg("muted", state.queuedStatus)
414
+ : "";
413
415
  if (state.compactionInProgress) {
414
416
  return `${label} ${theme.fg("accent", "compacting")}${queued}`;
415
417
  }
416
418
  if (state.processing) {
417
419
  const processingStatus = state.processingStatus ?? "processing";
418
- const processingToken = processingStatus === "active" ? "warning" : "accent";
420
+ const processingToken =
421
+ processingStatus === "active" ? "warning" : "accent";
419
422
  return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
420
423
  }
421
424
  return `${label} ${theme.fg("success", "connected")}`;
package/lib/updates.ts CHANGED
@@ -26,8 +26,18 @@ export type TelegramReactionType =
26
26
  | TelegramReactionTypeEmoji
27
27
  | TelegramReactionTypeNonEmoji;
28
28
 
29
- export const TELEGRAM_PRIORITY_REACTION_EMOJIS = ["👍", "⚡", "❤", "🕊"] as const;
30
- export const TELEGRAM_REMOVAL_REACTION_EMOJIS = ["👎", "👻", "💔", "💩"] as const;
29
+ export const TELEGRAM_PRIORITY_REACTION_EMOJIS = [
30
+ "👍",
31
+ "⚡",
32
+ "❤",
33
+ "🕊",
34
+ ] as const;
35
+ export const TELEGRAM_REMOVAL_REACTION_EMOJIS = [
36
+ "👎",
37
+ "👻",
38
+ "💔",
39
+ "💩",
40
+ ] as const;
31
41
 
32
42
  export interface TelegramUpdateDeletion {
33
43
  deleted_business_messages?: { message_ids?: unknown };
@@ -66,7 +76,9 @@ function hasAddedTelegramReactionEmoji(
66
76
  newEmojis: Set<string>,
67
77
  candidates: readonly string[],
68
78
  ): boolean {
69
- return candidates.some((emoji) => !oldEmojis.has(emoji) && newEmojis.has(emoji));
79
+ return candidates.some(
80
+ (emoji) => !oldEmojis.has(emoji) && newEmojis.has(emoji),
81
+ );
70
82
  }
71
83
 
72
84
  export function extractDeletedTelegramMessageIds(
@@ -626,7 +638,14 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
626
638
  deps.ctx,
627
639
  );
628
640
  }
629
- if (!hasAddedTelegramReactionEmoji(oldEmojis, newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS)) return;
641
+ if (
642
+ !hasAddedTelegramReactionEmoji(
643
+ oldEmojis,
644
+ newEmojis,
645
+ TELEGRAM_PRIORITY_REACTION_EMOJIS,
646
+ )
647
+ )
648
+ return;
630
649
  deps.prioritizeQueuedTelegramTurnByMessageId(
631
650
  reactionUpdate.message_id,
632
651
  deps.ctx,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
- "description": "Better Telegram DM bridge extension for pi",
5
+ "description": "Better Telegram DM bridge extension for π",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "pi-package",