@llblab/pi-telegram 0.9.5 → 0.9.7

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
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## 1. Concept
13
13
 
14
- `pi-telegram` is a π extension that turns a Telegram DM into a session-local frontend for π, including text/file forwarding, streaming previews, queued follow-ups, model controls, and outbound attachment delivery.
14
+ `pi-telegram` is a Telegram Runtime Adapter for π: a session-local operator console that turns a private Telegram DM into a runtime surface for prompt intake, streaming previews, queue management, model/thinking/settings controls, inbound/outbound handler pipelines, voice/buttons, artifacts, and extension callback interop. Treat it as a Telegram membrane around π, not a narrow message pipe.
15
15
 
16
16
  ## 2. Identity & Naming Contract
17
17
 
@@ -28,7 +28,7 @@
28
28
  - `/tests/*.test.ts`: Domain-mirrored regression suites that follow the same flat naming as `/lib`
29
29
  - `/docs/README.md`: Documentation index for technical project docs
30
30
  - `/docs/architecture.md`: Runtime and subsystem overview for the bridge
31
- - `/README.md`: User-facing project entry point, install guide, and fork summary
31
+ - `/README.md`: User-facing project entry point. Keep its rhythm as install → connect → use → core features → docs, with vivid examples that explain the runtime adapter/operator-console model without duplicating full docs.
32
32
  - `/AGENTS.md`: Durable engineering and runtime conventions
33
33
  - `/BACKLOG.md`: Canonical open work
34
34
  - `/CHANGELOG.md`: Completed delivery history
@@ -122,6 +122,8 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
122
122
  - When Telegram-visible behavior changes, sync `README.md` and the relevant `/docs` entry in the same pass
123
123
  - When durable runtime constraints or repeat bug patterns emerge, record them here instead of burying them in changelog prose
124
124
  - When fork identity changes, keep `README.md`, package metadata, and docs aligned so the published package does not point back at stale upstream coordinates
125
+ - README positioning should emphasize `/start` as the primary Telegram operator menu and keep reaction shortcuts secondary. Reactions are useful queue affordances, but menu controls are the core CLI-to-Telegram bridge.
126
+ - Document configuration knobs without UI in the root README when they affect bootstrap, networking, or transport limits; currently this includes token env bootstrap, Node env proxy mode, and inbound/outbound size limits.
125
127
  - Work only inside this repository during development tasks; updating the installed Pi extension checkout is a separate manual operator step, not part of normal in-repo implementation work
126
128
 
127
129
  ## 8. Integration Protocols
package/BACKLOG.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Open Work
4
4
 
5
- - Implement Telegram Extension Sections Platform for the 0.10.0 line.
5
+ - [ ] Implement Telegram Extension Sections Platform for the 0.10.0 line.
6
6
  - Exit: Runtime registry, main-menu integration, `section:` callback routing, safe section context ports, diagnostics, docs, and at least one small demo/fixture prove ordinary pi extensions can add Telegram menu sections without owning a second poller.
7
- - Explore always-available outbound Telegram tools for queued artifacts and controls.
7
+ - [ ] Explore always-available outbound Telegram tools for queued artifacts and controls.
8
8
  - Priority: Low.
9
9
  - Idea: Provide tools such as `telegram_attach_file` and `telegram_attach_button` that can be called outside an active Telegram turn, using the paired chat/session as the delivery target when safe.
10
10
  - Exit: Design note defines active-turn versus ambient delivery semantics, safety constraints, failure modes, and whether the current `telegram_attach` contract should stay turn-scoped or gain an ambient companion.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.7: Bot API 10.0 Alignment
4
+
5
+ - `[Dependencies]` Migrated peer dependencies and imports from `@mariozechner/*` to `@earendil-works/*` (`pi-agent-core`, `pi-ai`, `pi-coding-agent`). Impact: the extension now tracks the new `@earendil-works` package scope; transitive `@mariozechner` packages remain in the lockfile until their upstreams migrate.
6
+ - `[Package]` Added `engines: { "node": ">=22.0.0" }` to document the supported Node expectation while keeping dev dependencies on `latest` for early-stage iteration. Impact: users know the minimum Node version without constraining the development dependency matrix prematurely.
7
+ - `[Polling]` Added `"guest_message"` to `TELEGRAM_ALLOWED_UPDATES` so the bot receives guest-mode updates. Impact: without this, guest mentions are silently ignored by Telegram.
8
+ - `[Telegram API]` Updated `sendMessageDraft` wrapper for Bot API 10.0 semantics: removed the empty-text guard, made `text` optional, and added optional `parse_mode`, `entities`, and `message_thread_id` parameters. Impact: preview can now show a "Thinking…" placeholder with empty text, and callers can pass rich formatting through `parse_mode` or `entities`.
9
+ - `[Telegram API]` Added `answerGuestQuery` to the API runtime for Bot API 10.0 Guest Mode support. Impact: callers can reply to guest queries in chats where the bot is not a member. Uses `InlineQueryResultArticle` as the result payload per Bot API 10.0 contract.
10
+ - `[Updates]` Extended inbound update routing to recognize `guest_message` updates. Added `getAuthorizedTelegramGuestMessage`, guest flow action, execution plan, runtime handler, and prompt enqueue support. Unauthorized guest queries receive an "Access denied." reply via `answerGuestQuery`. Guest turns customize the agent-end delivery to use `answerGuestQuery` instead of normal reply transport. Impact: the bridge can now receive and route guest-mode mentions in group chats while preserving the existing private-message authorization model.
11
+ - `[Runtime]` Added typing-loop skip for guest turns (`chatId === 0`) to avoid spurious `sendChatAction` errors in the status bar.
12
+ - `[Tests]` Added regression tests for empty-text draft delivery, undefined-text draft delivery, rich preview with `parse_mode` and `entities`, guest query answers, guest extraction, guest flow classification, guest execution plan, guest deny reply, and guest message routing through the runtime.
13
+ - `[Preview]` Updated `sendDraft` interface in `lib/preview.ts` to accept optional text and formatting options, keeping the preview pipeline aligned with the new API wrapper.
14
+
15
+ ## 0.9.6: Runtime Adapter Positioning
16
+
17
+ - `[Package]` Bumped package metadata to `0.9.6` and repositioned the package description from "Better Telegram DM bridge extension for π" to "Telegram Runtime Adapter for π". Impact: package metadata now reflects the runtime adapter/operator-console role rather than a narrow pipe metaphor.
18
+ - `[Telegram API]` Introduced `TELEGRAM_API_BASE` for the Bot API endpoint and documented native HTTP/HTTPS proxy operation through `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, and explicit `NODE_USE_ENV_PROXY=1` / `--use-env-proxy` enablement. Impact: users behind corporate proxies, local HTTP tunnels, or restricted networks get a zero-runtime-dependency proxy path without replacing native `fetch`; SOCKS5 remains outside the zero-dependency core.
19
+ - `[Dependencies]` Refreshed the lockfile transitive dependency set so `npm audit` clears current `fast-uri` and `fast-xml-builder` advisories inherited through development peer installs. Impact: the full `npm run validate` pipeline passes without changing runtime dependencies.
20
+ - `[README]` Restructured the user entrypoint around install → connect → use → core features → docs, then consolidated examples, terminology, proxy setup, `PI_CODING_AGENT_DIR`, and other environment-only configuration around the runtime-adapter/operator-console model. Impact: first-time users get a clearer path from installation to operation, while vivid examples and non-UI runtime knobs stay discoverable.
21
+ - `[Context]` Promoted the runtime-adapter/operator-console README rhythm, `/start` menu emphasis, and environment-only configuration rule into `AGENTS.md`. Impact: future documentation edits preserve the same positioning and env-knob coverage instead of drifting back toward a narrow bridge metaphor.
22
+
3
23
  ## 0.9.5: Telegram Delivery Resilience Hotfix
4
24
 
5
25
  - `[Preview Delivery]` Preview flush failures from Telegram transport errors such as `fetch failed` / `ECONNRESET` are now caught and recorded as runtime diagnostics instead of escaping from the preview pipeline. Impact: transient Telegram connectivity failures no longer crash the extension during streamed preview edits.
package/README.md CHANGED
@@ -2,24 +2,11 @@
2
2
 
3
3
  ![pi-telegram screenshot](screenshot.png)
4
4
 
5
- This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008460b6c1ca036d92322f69d87f626be0fc`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
5
+ **Telegram Runtime Adapter for π.**
6
6
 
7
- ## Start Here
7
+ `pi-telegram` turns a private Telegram DM into a session-local operator console for π. It admits work, preserves context, streams readable replies, keeps busy sessions usable through queues, lets other extensions share one bot, and turns assistant-authored intent into native Telegram artifacts.
8
8
 
9
- - [Project Context](./AGENTS.md)
10
- - [Open Backlog](./BACKLOG.md)
11
- - [Changelog](./CHANGELOG.md)
12
- - [Documentation](./docs/README.md)
13
-
14
- ## Key Features
15
-
16
- - **Telegram Controls**: `/start` opens the inline application menu with command help, available π prompt templates, status rows, model, thinking, and queue sections; the Status row reports `compacting` during Telegram `/compact`; `/stop`, `/abort`, `/next`, and `/continue` provide queue-clear, queue-preserve, force-next, and queued-resume semantics respectively; model-switch continuation turns still use the control lane when a restart needs to resume safely.
17
- - **Interactive UI**: Manage your session directly from Telegram. Inline buttons expose an application menu for switching models, choosing model pages from the pagination indicator, adjusting reasoning (thinking) levels, and inspecting or mutating the waiting queue; model scope/pagination controls stay at the top of the model menu, the Queue button shows the current item count, and command emoji are reused on matching controls such as model and thinking.
18
- - **In-flight Model Switching**: Change the active model mid-generation. The agent gracefully pauses, applies the new model, and restarts its response without losing context.
19
- - **Smart Message Queue**: Messages sent while the agent is busy are queued and previewed in the π status bar, and queued turns can be reprioritized or removed with Telegram reactions or the queue section of the inline application menu.
20
- - **Mobile-Optimized Rendering**: Tables and lists are formatted for narrow screens, table padding accounts for emoji grapheme and wide Unicode display width, and Telegram-originated runs prompt the assistant to prefer narrow table columns for phone readability. Markdown is correctly parsed and split to fit Telegram's limits without breaking HTML structures or code blocks, block spacing stays faithful to the original Markdown with readable heading separation, supported absolute links stay clickable, and unsupported link forms degrade safely.
21
- - **File Handling & Attachments**: Send images and files to the agent, transcribe or transform inbound text/media with configured inbound handlers, or ask π to generate and return artifacts. Inbound downloads and outbound attachments are size-limited by default, and outbound files are delivered automatically via the `telegram_attach` tool.
22
- - **Streaming Responses**: Closed Markdown blocks stream back as rich Telegram HTML while π is generating, and the still-growing tail stays readable until the final fully rendered reply lands.
9
+ This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
23
10
 
24
11
  ## Install
25
12
 
@@ -35,22 +22,16 @@ From git:
35
22
  pi install git:github.com/llblab/pi-telegram
36
23
  ```
37
24
 
38
- ## Configure
39
-
40
- ### Configuration Philosophy
41
-
42
- The extension intentionally keeps rich visual/TUI configuration minimal for now. Rich setup screens may arrive later, but they are not the main configuration surface yet.
25
+ ## Connect
43
26
 
44
- For advanced setup, ask an agent to read this `README.md` and the docs, then update `~/.pi/agent/telegram.json` for your workflow. Agents are good at small configuration changes, and this keeps the bridge simple while handler pipelines and operator preferences continue to evolve.
45
-
46
- ### 1. Telegram Bot
27
+ ### 1. Create a Telegram bot
47
28
 
48
29
  1. Open [@BotFather](https://t.me/BotFather)
49
30
  2. Run `/newbot`
50
31
  3. Pick a name and username
51
32
  4. Copy the bot token
52
33
 
53
- ### 2. Configure the extension in π
34
+ ### 2. Configure the bot token in π
54
35
 
55
36
  Start π, then run:
56
37
 
@@ -66,63 +47,97 @@ Paste your bot token when prompted. If a bot token is already saved in `~/.pi/ag
66
47
  /telegram-connect
67
48
  ```
68
49
 
69
- The bridge is session-local: only one π instance polls Telegram at a time. `/telegram-connect` records polling ownership in `~/.pi/agent/locks.json`; live ownership moves require confirmation, while `/new` and same-`cwd` process restarts resume automatically.
50
+ The adapter is session-local: only one π instance polls Telegram at a time. `/telegram-connect` records polling ownership in `~/.pi/agent/locks.json`; live ownership moves require confirmation, while `/new` and same-`cwd` process restarts resume automatically.
70
51
 
71
- ### 4. Pair your account from Telegram
52
+ ### 4. Pair your Telegram account
72
53
 
73
54
  1. Open the DM with your bot in Telegram
74
55
  2. Send `/start`
75
56
 
76
- The first user to message the bot becomes the exclusive owner of the bridge. The extension will only accept messages from this user.
57
+ The first user to message the bot becomes the exclusive owner of the adapter. Messages from other users are ignored.
77
58
 
78
- ## Usage
59
+ ### Environment-only configuration
79
60
 
80
- Once paired, simply chat with your bot in Telegram. All text, images, and files are forwarded to π.
61
+ Most day-to-day controls live in the Telegram menu or π commands. A few important runtime knobs intentionally stay in environment variables because they affect bootstrap, networking, or transport limits before a menu can help:
81
62
 
82
- ### Telegram Commands & Controls
63
+ - **Bot token bootstrap**: `/telegram-setup` can prefill from `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY` when no token is already saved.
64
+ - **HTTP/HTTPS proxy**: native `fetch` can use `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` when Node's environment proxy mode is enabled. Use `NODE_USE_ENV_PROXY=1` or start Node with `--use-env-proxy`. SOCKS5 is not part of the zero-dependency core. If you need it, run a local HTTP-to-SOCKS bridge or system tunnel and point `HTTP_PROXY` / `HTTPS_PROXY` at the HTTP endpoint.
65
+ - **Agent data root / temp location**: `PI_CODING_AGENT_DIR` changes the base agent directory used for `telegram.json`, locks, generated outbound-handler artifacts, and Telegram temp files. When unset, the adapter uses `~/.pi/agent`, so inbound Telegram files land in `~/.pi/agent/tmp/telegram`.
66
+ - **Inbound file limit**: `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES` changes the default 50 MiB Telegram download limit.
67
+ - **Outbound attachment limit**: `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` changes the default 50 MiB `telegram_attach` delivery limit.
83
68
 
84
- Use these inside the Telegram DM with your bot:
69
+ ## Use
85
70
 
86
- - **`/start`**: Pair the first Telegram user when needed, register bridge bot commands, and open the inline application menu with command help, available π prompt templates, status rows, and controls.
87
- - **`/compact`**: Start session compaction (only works when the session is idle).
88
- - **`/next`**: Dispatch the next queued turn (aborts π first if busy).
89
- - **`/continue`**: Enqueue a priority `continue` prompt. It waits like normal Telegram work when π is busy and can trigger prompt/skill handling that listens for `continue`.
90
- - **`/abort`**: Abort the active run without touching the queue.
91
- - **`/stop`**: Abort the active run and clear all waiting Telegram queue items.
71
+ Once paired, chat with your bot in Telegram. Text, images, files, replies, edits, media groups, and configured handler output are forwarded into π as Telegram-originated turns.
72
+
73
+ What it feels like:
92
74
 
93
- 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.
75
+ - Open `/start` and get a Telegram control panel for the running π session: status, prompt templates, model, thinking, settings, and queue.
76
+ - Fire off three tasks while π is busy. They become visible queue items instead of terminal noise.
77
+ - Open Queue from the menu, inspect waiting work, delete stale prompts, or move important work forward.
78
+ - Switch models from Telegram mid-run; the adapter schedules a safe continuation instead of tearing state apart.
79
+ - Send a voice note; an inbound handler transcribes it; π answers in the same chat.
80
+ - Drop a screenshot and ask, "what is broken here?" The image payload reaches π with the local file context.
81
+ - Ask for a generated file; when π calls `telegram_attach`, the artifact returns to Telegram with the next reply.
94
82
 
95
- Hidden compatibility shortcuts: `/help` and `/status` open the same main application menu, `/model` opens the model section, `/thinking` opens the thinking section, `/queue` opens the queue section, and `/settings` opens hidden bridge settings. Settings rows open detail submenus with Back plus green/black/yellow option controls such as On and Off for checkboxes. These shortcuts are intentionally not shown in the bot command menu.
83
+ ### Telegram controls
96
84
 
97
- Telegram command admission is explicit: `/compact`, `/queue`, `/settings`, `/stop`, `/abort`, `/next`, `/help`, `/start`, `/status`, `/model`, and `/thinking` execute immediately. `/continue` is a command shortcut that enqueues a priority Telegram prompt containing `continue`. Prompt-template commands expand before queueing and then follow normal prompt-queue rules. Synthetic model-switch continuation turns still enter the high-priority control lane so they can resume before normal queued prompts when π becomes safe to dispatch.
85
+ Use these inside the Telegram DM with your bot. The main entrypoint is `/start`: it opens the operator menu and exposes many of the important agent controls that normally live in the CLI, adapted for Telegram.
98
86
 
99
- ### Pi Commands
87
+ - **`/start`**: Pair the first Telegram user when needed, register bot commands, and open the inline application menu with command help, prompt-template commands, status rows, model controls, thinking controls, settings, and queue controls.
88
+ - **`/compact`**: Start session compaction when the session is idle.
89
+ - **`/next`**: Dispatch the next queued turn, aborting π first if needed.
90
+ - **`/continue`**: Enqueue a priority `continue` prompt.
91
+ - **`/abort`**: Abort the active run without touching the queue.
92
+ - **`/stop`**: Abort the active run and clear waiting Telegram queue items.
93
+
94
+ Hidden compatibility shortcuts: `/help` and `/status` open the main application menu, `/model` opens model controls, `/thinking` opens reasoning controls, `/queue` opens queue controls, and `/settings` opens bridge settings.
95
+
96
+ Prompt-template commands are discovered from π prompt templates, mapped to Telegram-safe aliases (`fix-tests.md` becomes `/fix_tests`), shown in `/start`, and expanded before queueing.
97
+
98
+ ### π commands
100
99
 
101
100
  Run these inside π, not Telegram:
102
101
 
103
102
  - **`/telegram-setup`**: Configure or update the Telegram bot token.
104
- - **`/telegram-status`**: Check bridge status, connection, polling, execution, queue, and recent redacted runtime/API failure events.
105
- - **`/telegram-settings`**: Open local bridge settings and toggle proactive push using the same `telegram.json` flag as the Telegram `/settings` menu.
106
- - **`/telegram-connect`**: Start polling Telegram updates in the current π session, acquire the singleton lock, or interactively move ownership here from another live instance.
103
+ - **`/telegram-connect`**: Start polling Telegram updates in the current π session and acquire the singleton lock.
104
+ - **`/telegram-settings`**: Open local settings and toggle proactive push using the same `telegram.json` flag as the Telegram `/settings` menu.
107
105
  - **`/telegram-disconnect`**: Stop polling in the current π session and release the singleton lock.
106
+ - **`/telegram-status`**: Inspect adapter status, connection, polling, execution, queue, and recent redacted runtime/API failure events.
107
+
108
+ ### Files and artifacts
109
+
110
+ Send files or images directly to the bot. Inbound downloads are saved under `<agent-dir>/tmp/telegram` and default to a 50 MiB limit. The agent dir is `~/.pi/agent` unless `PI_CODING_AGENT_DIR` overrides it.
111
+
112
+ If you ask π for a generated file, π can call the `telegram_attach` tool and the adapter sends the file with the next Telegram reply. Outbound attachments also default to a 50 MiB limit. Environment variables for both limits are listed in [Environment-only configuration](#environment-only-configuration).
108
113
 
109
- ### Queue, Reactions, and Media
114
+ ## Core features
110
115
 
111
- - If you send more Telegram messages while π is busy, they enter the default prompt queue and are processed in order.
112
- - Very long text messages that Telegram appears to split automatically are coalesced through a short conservative debounce and forwarded to π as one prompt when the first chunk is near Telegram's text limit, currently using a 3600-character threshold. Commands, bot messages, media groups, and normal short follow-ups are not coalesced.
113
- - `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` move a waiting prompt into the priority prompt queue, behind control actions but ahead of default prompts. Removing the last priority reaction sends it back to its normal queue position, and adding a priority reaction again gives it a fresh priority position.
114
- - `👎`, `👻`, `💔`, `💩`, and `🗑` remove a waiting turn from the queue. Telegram Bot API does not expose ordinary DM message-deletion events through the polling path used here, so queue removal is bound to removal reactions.
115
- - Reactions apply to any waiting Telegram turn, including text, voice, files, images, and media groups. For media groups, a reaction on any message in the group applies to the whole queued turn.
116
- - If you edit a Telegram message while it is still waiting in the queue, the queued turn is updated instead of creating a duplicate prompt. Edits after a turn has already started may not affect the active run.
117
- - Telegram replies to earlier text or caption messages are forwarded as `[reply]` context for normal prompts, while slash commands still parse from the new message text only.
118
- - Inbound images, albums, and files are saved to `~/.pi/agent/tmp/telegram`. Unhandled local file paths are included in the prompt, handled attachment output is injected into the prompt text, and inbound images are forwarded to π as image inputs. Inbound downloads default to a 50 MiB limit and can be adjusted with `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES`.
119
- - Queue reactions depend on Telegram delivering `message_reaction` updates for your bot and chat type.
116
+ ### Operator menu and controls
120
117
 
121
- ### Inbound Handlers
118
+ The inline application menu is the primary operator surface. It exposes status, prompt-template commands, model selection, thinking level selection, settings, and queue inspection/mutation: a Telegram-shaped subset of the important handles normally available from the CLI. A typical control loop stays inside Telegram: open `/start`, inspect status, jump into Queue, delete stale work, switch model, return to the main menu, and keep the π session running without touching the terminal.
122
119
 
123
- `telegram.json` can set `proactivePush: true` to send successful local non-Telegram final replies to the paired Telegram chat when no Telegram turn is active. Local prompt text is not sent because the bot does not own or mirror terminal user messages. The mode is off by default, can be toggled from the hidden `/settings` menu, persists across contexts until explicitly disabled or removed from config, is gated by the current Telegram lock owner, and skips aborted or failed turns.
120
+ ### Queue runtime
124
121
 
125
- `telegram.json` can define ordered `inboundHandlers` for Telegram → π preprocessing such as text translation, voice transcription, OCR, or PDF extraction. Matching handlers run before the Telegram turn enters the π queue. If a matching media/file handler fails, the next matching handler is tried as a fallback. Legacy `attachmentHandlers` still work as a deprecated compatibility alias and are appended after `inboundHandlers`.
122
+ Messages sent while π is busy enter the prompt queue and are processed in order. Control actions and model-switch continuation turns use higher-priority lanes so operational commands can resume before normal prompts.
123
+
124
+ The menu is the primary way to inspect and mutate the queue. Reactions are an extra shortcut when Telegram delivers `message_reaction` updates for the chat: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting work; `👎`, `👻`, `💔`, `💩`, and `🗑` remove it. The same rules apply to text, voice, files, images, and media groups.
125
+
126
+ ### Streaming and Telegram HTML rendering
127
+
128
+ Closed Markdown blocks stream back as rich Telegram HTML while π is generating. The growing tail stays conservative until the final rendered reply lands. Long replies are split below Telegram limits without intentionally breaking HTML structures, links, code blocks, blockquotes, lists, or code fences.
129
+
130
+ Rendering is phone-aware: tables and lists stay narrow, table padding accounts for emoji graphemes and wide Unicode display width, unsupported link forms degrade safely, and block spacing stays faithful to the original Markdown.
131
+
132
+ ### Media, replies, edits, and split text
133
+
134
+ Telegram replies to earlier text or caption messages are forwarded as `[reply]` context for normal prompts, while slash commands still parse from the new message text only. If a Telegram message is edited while still waiting in the queue, the queued turn is updated instead of duplicated. Very long text messages that Telegram appears to split automatically are coalesced through a conservative debounce when the first chunk is near Telegram's text limit.
135
+
136
+ ### Inbound handlers
137
+
138
+ `telegram.json` can define ordered `inboundHandlers` for Telegram → π preprocessing: text translation, voice transcription, OCR, PDF extraction, or any command-template pipeline. Matching handlers run before the turn enters the queue; failed handlers record diagnostics and fall back safely. Legacy `attachmentHandlers` still work as a deprecated compatibility alias appended after `inboundHandlers`.
139
+
140
+ A practical voice setup is simple: Telegram `.ogg` arrives, STT runs locally or through your chosen command, stdout is injected as `[outputs]`, and π receives the result as usable prompt context.
126
141
 
127
142
  ```json
128
143
  {
@@ -149,19 +164,9 @@ Run these inside π, not Telegram:
149
164
  }
150
165
  ```
151
166
 
152
- Matching supports optional `mime`, `type`, or `match`; `mime` can be used without `type`, and wildcards like `audio/*` or `text/*` are accepted. Raw Telegram text can match `type: "text"`, `mime: "text/plain"`, or `mime: "text/*"`; it is passed on stdin and as `{text}`, and non-empty stdout replaces the prompt text. Media/file handlers receive `{file}`, `{mime}`, and `{type}`; local attachments stay in the prompt under `[attachments] <directory>` with relative file entries, and successful media/file handler stdout is added under `[outputs]`. Attached `text/plain`/`text/*` files have a built-in fail-open reader that injects UTF-8 content into `[outputs]` when no configured handler produced output. Failed handlers record diagnostics and fall back safely. The portable command-template contract is documented in [`docs/command-templates.md`](./docs/command-templates.md); Telegram-specific inbound config is documented in [`docs/inbound-handlers.md`](./docs/inbound-handlers.md).
153
-
154
- ### Requesting Files
155
-
156
- If you ask π for a file or generated artifact (e.g., _"generate a shell script and attach it"_), π can call the `telegram_attach` tool, and the extension will send the file alongside its next Telegram reply. `telegram_attach` is the only π tool registered by `pi-telegram`; use it for ordinary files, not for Telegram-native voice or buttons. Outbound attachments default to a 50 MiB limit and can be adjusted with `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`.
157
-
158
- ### Assistant-Authored Outbound Actions
159
-
160
- Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not π tools; they are assistant-authored HTML comments that the bridge removes from Telegram text and handles after `agent_end`. Recognized blocks must start at column zero on a top-level line outside fenced code, quotes, and lists, so documentation examples remain literal. The agent writes normal Markdown; the extension owns voice generation, button markup, callback routing, and delivery.
161
-
162
- #### Voice
167
+ ### Outbound handlers, voice, and buttons
163
168
 
164
- Voice blocks synthesize their text and upload it as a native Telegram `sendVoice` OGG/Opus message. Use body form for multiline text, `text="..."` for explicit one-line text with optional attributes, and the colon shorthand for a one-line voice with no attributes. The spoken text may be a concise companion summary, but it does not have to follow that format; write what you want spoken and keep it TTS-friendly:
169
+ Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not π tools; they are assistant-authored HTML comments that the adapter removes from Telegram text and handles after `agent_end`. Recognized blocks must start at column zero on a top-level line outside fenced code, quotes, and lists.
165
170
 
166
171
  ```md
167
172
  Full technical answer stays readable as text.
@@ -170,27 +175,14 @@ Full technical answer stays readable as text.
170
175
  Text to synthesize as a Telegram voice message.
171
176
  -->
172
177
 
173
- <!-- telegram_voice lang=ru rate=+30% text="Short spoken companion summary." -->
174
-
175
- <!-- telegram_voice: Short spoken companion summary. -->
176
- ```
177
-
178
- Outbound `type: "text"` handlers can transform final text/Markdown before Telegram rendering and delivery, using stdin and `{text}` as input and non-empty stdout as replacement text. They are a good fit for machine translation, tone normalization, redaction, glossary expansion, or any other final text rewrite that should happen outside the agent prompt. The transform also applies when the bridge finalizes an already streamed rich preview, so Telegram may briefly show the pre-transform preview before the final edited message lands. Inline button labels are transformed too, while callback data and prompts stay unchanged.
179
-
180
- ```json
181
- {
182
- "outboundHandlers": [
183
- {
184
- "type": "text",
185
- "template": "/path/to/translate --lang {lang=ru} --text {text}"
186
- }
187
- ]
188
- }
178
+ <!-- telegram_button label="Show risks"
179
+ List the main risks first.
180
+ -->
189
181
  ```
190
182
 
191
- Outbound voice is disabled unless a matching `outboundHandlers[]` entry is configured. Multiple `telegram_voice` blocks in one reply are synthesized and sent independently, preserving each block's attributes. The bridge uses the same [command-template contract](./docs/command-templates.md) as inbound handlers: split the template into args, substitute placeholders, execute without a shell, and use stdout as the result channel for a single template.
183
+ Outbound `type: "text"` handlers can transform final text/Markdown before Telegram rendering and delivery. Outbound `type: "voice"` handlers can translate, synthesize, and convert hidden `telegram_voice` text into Telegram-native OGG/Opus voice through the same command-template contract used by inbound handlers.
192
184
 
193
- A composed voice setup can translate the hidden `telegram_voice` text, synthesize it, and convert MP3 to Telegram-native OGG/Opus in one pipeline. The bridge provides `{text}`, `{mp3}`, and `{ogg}` to every step; top-level `args`/`defaults` apply to all steps unless a step defines private values, the default command timeout applies automatically, and each step's stdout is passed to the next step's stdin by default. Use `"output": "ogg"` when the artifact path should come from the generated `{ogg}` value instead of final stdout:
185
+ A composed voice pipeline can translate, synthesize, and convert in one pass:
194
186
 
195
187
  ```json
196
188
  {
@@ -208,43 +200,36 @@ A composed voice setup can translate the hidden `telegram_voice` text, synthesiz
208
200
  }
209
201
  ```
210
202
 
211
- #### Buttons
203
+ The agent writes intent; the adapter owns transport. Text remains readable, voice becomes native Telegram media, and buttons route back as queued prompts.
212
204
 
213
- Button blocks attach inline quick replies to the final text. Use one independent `telegram_button` block per action. If the prompt should equal the label, use the colon shorthand. If the prompt differs, use the inline `prompt="..."` attribute for one-line prompts or the body form for multiline prompts:
205
+ ### Extension interop
214
206
 
215
- ```md
216
- I can continue.
207
+ Unknown inline-button callbacks are forwarded to π as `[callback] <data>` when they do not belong to pi-telegram, so other extensions can namespace and handle Telegram buttons without polling the bot themselves. Layered extensions that need synchronous update handling can register a runtime interceptor on the shared update registry.
217
208
 
218
- <!-- telegram_button label="Show risks"
219
- List the main risks first.
220
- -->
221
-
222
- <!-- telegram_button label=Continue prompt="Continue with the current plan." -->
209
+ ### Proactive push
223
210
 
224
- <!-- telegram_button: OK -->
225
- ```
211
+ `telegram.json` can set `proactivePush: true` to send successful local non-Telegram final replies to the paired Telegram chat when no Telegram turn is active. Local prompt text is not mirrored because the bot does not own terminal user messages. The mode is off by default and can be toggled from settings.
226
212
 
227
- 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). Layered extensions that need to react to Telegram updates synchronously inside their own runtime (for example, to resolve a blocking-tool approval Promise the moment a callback arrives) can register a runtime interceptor on the shared update registry; see [External Handlers](./docs/external-handlers.md). Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
213
+ ## Docs
228
214
 
229
- ## Streaming
230
-
231
- The extension streams assistant previews back to Telegram while π is generating.
232
-
233
- Rich previews are sent through editable messages because Telegram drafts are text-only. Closed top-level Markdown blocks can appear with formatting before the answer finishes, while the still-growing tail remains conservative and readable until the preview is replaced with the fully rendered Telegram HTML reply. Editable preview messages are also attached as replies to the source Telegram prompt when possible.
234
-
235
- ## Status bar
236
-
237
- The π status bar shows the current bridge state plus queued Telegram turns as compact previews. Busy labels distinguish states such as `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`. Telegram prompt guidance asks agents to keep tables, dense list items, and compact text blocks within about 37 visible cells when possible so mobile replies stay readable.
238
-
239
- ```text
240
- telegram active +3
241
- ```
215
+ - [Project Context](./AGENTS.md): durable engineering conventions and architecture constraints.
216
+ - [Open Backlog](./BACKLOG.md): planned work and known follow-ups.
217
+ - [Changelog](./CHANGELOG.md): completed delivery history.
218
+ - [Documentation Index](./docs/README.md): technical docs hub.
219
+ - [Architecture](./docs/architecture.md): runtime and subsystem overview.
220
+ - [Inbound Handlers](./docs/inbound-handlers.md): Telegram → π preprocessing.
221
+ - [Outbound Handlers](./docs/outbound-handlers.md): final text, voice, and artifact pipelines.
222
+ - [Command Templates](./docs/command-templates.md): portable command-template contract.
223
+ - [Callback Namespaces](./docs/callback-namespaces.md): callback interop for layered extensions.
224
+ - [External Handlers](./docs/external-handlers.md): shared update interception.
225
+ - [Extension Sections Draft](./docs/extension-sections.md): future Telegram extension sections platform.
226
+ - [Locks](./docs/locks.md): singleton polling ownership.
242
227
 
243
228
  ## Notes
244
229
 
245
- - Replies to Telegram prompts are sent as Telegram replies to the source message when possible; if the source message is unavailable, delivery falls back to a normal message
246
- - Long replies are split below Telegram's 4096 character limit without intentionally breaking Telegram HTML formatting; only the first split message is attached as a Telegram reply to the source prompt
247
- - Temporary inbound Telegram files are cleaned up on later session starts
230
+ - The extension intentionally keeps rich visual/TUI configuration minimal for now. For advanced setup, ask an agent to read this README and the docs, then update `~/.pi/agent/telegram.json` for your workflow.
231
+ - Replies to Telegram prompts are sent as Telegram replies to the source message when possible; if the source message is unavailable, delivery falls back to a normal message.
232
+ - Temporary inbound Telegram files are cleaned up on later session starts.
248
233
 
249
234
  ## License
250
235
 
@@ -24,7 +24,7 @@ Naming rule: because the repository already scopes this codebase to Telegram, ex
24
24
  Current runtime areas use these ownership boundaries:
25
25
 
26
26
  - `index.ts`: single composition root for live π/Telegram ports, session state, API-bound transport adapters, and status updates.
27
- - `api`: Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, and runtime error recording.
27
+ - `api`: Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording, and the `TELEGRAM_API_BASE` constant for the Bot API endpoint.
28
28
  - `config` / `setup`: persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, and config persistence.
29
29
  - `locks` / `polling`: singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, and poll-loop runtime wiring.
30
30
  - `updates` / `routing`: update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition.
@@ -114,7 +114,7 @@ unregister();
114
114
  Sections are registered by normal pi extensions:
115
115
 
116
116
  ```ts
117
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
117
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
118
118
  import { registerTelegramSection } from "@llblab/pi-telegram/lib/extension-sections.ts";
119
119
 
120
120
  export default function (pi: ExtensionAPI) {
package/index.ts CHANGED
@@ -148,6 +148,7 @@ export default function (pi: Pi.ExtensionAPI) {
148
148
  downloadFile: downloadTelegramBridgeFile,
149
149
  editMessageText: editTelegramMessageText,
150
150
  answerCallbackQuery,
151
+ answerGuestQuery,
151
152
  prepareTempDir,
152
153
  } = Api.createDefaultTelegramBridgeApiRuntime({
153
154
  getBotToken: configStore.getBotToken,
@@ -333,6 +334,7 @@ export default function (pi: Pi.ExtensionAPI) {
333
334
  updateStatus,
334
335
  dispatchNextQueuedTelegramTurn,
335
336
  answerCallbackQuery,
337
+ answerGuestQuery,
336
338
  sendTextReply,
337
339
  setMyCommands,
338
340
  getCommands,
@@ -494,6 +496,7 @@ export default function (pi: Pi.ExtensionAPI) {
494
496
  sendMarkdownReply,
495
497
  sendTextReply,
496
498
  sendQueuedAttachments: queuedAttachmentSender,
499
+ answerGuestQuery,
497
500
  planOutboundReply: outboundReplyPlanner,
498
501
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
499
502
  isCurrentOwner: lockOwnershipGuard.ownsContext,
package/lib/api.ts CHANGED
@@ -12,6 +12,8 @@ import { join, resolve } from "node:path";
12
12
  import { Readable, Transform } from "node:stream";
13
13
  import { pipeline } from "node:stream/promises";
14
14
 
15
+ export const TELEGRAM_API_BASE = "https://api.telegram.org";
16
+
15
17
  export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
16
18
 
17
19
  export function getTelegramInboundFileByteLimitFromEnv(
@@ -149,12 +151,25 @@ export interface TelegramMessageReactionUpdated {
149
151
  date: number;
150
152
  }
151
153
 
154
+ export interface TelegramGuestMessage {
155
+ message_id: number;
156
+ from?: TelegramUser;
157
+ chat: TelegramChat;
158
+ date: number;
159
+ text?: string;
160
+ caption?: string;
161
+ guest_query_id: string;
162
+ guest_bot_caller_user?: TelegramUser;
163
+ guest_bot_caller_chat?: TelegramChat;
164
+ }
165
+
152
166
  export interface TelegramUpdate {
153
167
  update_id: number;
154
168
  message?: TelegramMessage;
155
169
  edited_message?: TelegramMessage;
156
170
  callback_query?: TelegramCallbackQuery;
157
171
  message_reaction?: TelegramMessageReactionUpdated;
172
+ guest_message?: TelegramGuestMessage;
158
173
  deleted_business_messages?: { message_ids?: unknown };
159
174
  }
160
175
 
@@ -182,6 +197,15 @@ export type TelegramEditMessageTextBody = Record<string, unknown> & {
182
197
  parse_mode?: "HTML";
183
198
  };
184
199
 
200
+ export type TelegramSendMessageDraftBody = Record<string, unknown> & {
201
+ chat_id: number;
202
+ draft_id: number;
203
+ text?: string;
204
+ parse_mode?: string;
205
+ entities?: unknown[];
206
+ message_thread_id?: number;
207
+ };
208
+
185
209
  interface TelegramApiResponse<T> {
186
210
  ok: boolean;
187
211
  result?: T;
@@ -231,6 +255,7 @@ export interface TelegramApiClient {
231
255
  callbackQueryId: string,
232
256
  text?: string,
233
257
  ) => Promise<void>;
258
+ answerGuestQuery?: (guestQueryId: string, text?: string) => Promise<void>;
234
259
  }
235
260
 
236
261
  export interface TelegramBridgeApiRuntimeDeps {
@@ -273,7 +298,12 @@ export interface TelegramBridgeApiRuntime {
273
298
  sendMessageDraft: (
274
299
  chatId: number,
275
300
  draftId: number,
276
- text: string,
301
+ text?: string,
302
+ options?: {
303
+ parse_mode?: string;
304
+ entities?: unknown[];
305
+ message_thread_id?: number;
306
+ },
277
307
  ) => Promise<boolean>;
278
308
  sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
279
309
  editMessageText: (
@@ -283,6 +313,7 @@ export interface TelegramBridgeApiRuntime {
283
313
  callbackQueryId: string,
284
314
  text?: string,
285
315
  ) => Promise<void>;
316
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
286
317
  prepareTempDir: () => Promise<number>;
287
318
  }
288
319
 
@@ -513,7 +544,7 @@ export async function callTelegram<TResponse>(
513
544
  return callTelegramWithRetry(
514
545
  method,
515
546
  async () =>
516
- fetch(`https://api.telegram.org/bot${configuredBotToken}/${method}`, {
547
+ fetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
517
548
  method: "POST",
518
549
  headers: { "content-type": "application/json" },
519
550
  body: JSON.stringify(body),
@@ -532,9 +563,7 @@ export async function fetchTelegramBotIdentity(
532
563
  botToken: string,
533
564
  fetchImpl: typeof fetch = fetch,
534
565
  ): Promise<TelegramBotIdentityResponse> {
535
- const response = await fetchImpl(
536
- `https://api.telegram.org/bot${botToken}/getMe`,
537
- );
566
+ const response = await fetchImpl(`${TELEGRAM_API_BASE}/bot${botToken}/getMe`);
538
567
  return response.json() as Promise<TelegramBotIdentityResponse>;
539
568
  }
540
569
 
@@ -557,14 +586,11 @@ export async function callTelegramMultipart<TResponse>(
557
586
  form.set(key, value);
558
587
  }
559
588
  form.set(fileField, fileBlob, fileName);
560
- return fetch(
561
- `https://api.telegram.org/bot${configuredBotToken}/${method}`,
562
- {
563
- method: "POST",
564
- body: form,
565
- signal: options?.signal,
566
- },
567
- );
589
+ return fetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
590
+ method: "POST",
591
+ body: form,
592
+ signal: options?.signal,
593
+ });
568
594
  },
569
595
  options,
570
596
  );
@@ -591,7 +617,7 @@ export async function downloadTelegramFile(
591
617
  `${randomUUID()}-${sanitizeFileName(suggestedName)}`,
592
618
  );
593
619
  const response = await fetch(
594
- `https://api.telegram.org/file/bot${configuredBotToken}/${file.file_path}`,
620
+ `${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
595
621
  { signal: options?.signal },
596
622
  );
597
623
  if (!response.ok) {
@@ -730,13 +756,18 @@ export function createTelegramBridgeApiRuntime(
730
756
  }),
731
757
  "typing",
732
758
  ),
733
- sendMessageDraft: (chatId, draftId, text) => {
734
- if (text.length === 0) return Promise.resolve(false);
735
- return callRecorded<boolean>("sendMessageDraft", {
759
+ sendMessageDraft: (chatId, draftId, text, options) => {
760
+ const body: Record<string, unknown> = {
736
761
  chat_id: chatId,
737
762
  draft_id: draftId,
738
- text,
739
- });
763
+ };
764
+ if (text !== undefined) body.text = text;
765
+ if (options?.parse_mode !== undefined)
766
+ body.parse_mode = options.parse_mode;
767
+ if (options?.entities !== undefined) body.entities = options.entities;
768
+ if (options?.message_thread_id !== undefined)
769
+ body.message_thread_id = options.message_thread_id;
770
+ return callRecorded<boolean>("sendMessageDraft", body);
740
771
  },
741
772
  sendMessage: (body) =>
742
773
  callRecorded<TelegramSentMessage>("sendMessage", body),
@@ -753,6 +784,18 @@ export function createTelegramBridgeApiRuntime(
753
784
  answerCallbackQuery: (callbackQueryId, text) => {
754
785
  return deps.client.answerCallbackQuery(callbackQueryId, text);
755
786
  },
787
+ answerGuestQuery: (guestQueryId, text) => {
788
+ const body: Record<string, unknown> = { guest_query_id: guestQueryId };
789
+ if (text !== undefined) {
790
+ body.result = {
791
+ type: "article",
792
+ id: "1",
793
+ title: text.length > 64 ? text.slice(0, 61) + "..." : text,
794
+ input_message_content: { message_text: text },
795
+ };
796
+ }
797
+ return callRecorded<void>("answerGuestQuery", body);
798
+ },
756
799
  prepareTempDir: () =>
757
800
  prepareTelegramTempDir(deps.tempDir, deps.tempFileMaxAgeMs),
758
801
  };
package/lib/pi.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  type SessionStartEvent,
16
16
  type SlashCommandInfo,
17
17
  SettingsManager,
18
- } from "@mariozechner/pi-coding-agent";
18
+ } from "@earendil-works/pi-coding-agent";
19
19
 
20
20
  export type {
21
21
  AgentEndEvent,
package/lib/polling.ts CHANGED
@@ -20,6 +20,7 @@ export const TELEGRAM_ALLOWED_UPDATES = [
20
20
  "edited_message",
21
21
  "callback_query",
22
22
  "message_reaction",
23
+ "guest_message",
23
24
  ] as const;
24
25
 
25
26
  export function buildTelegramInitialSyncRequest(): {
package/lib/preview.ts CHANGED
@@ -61,7 +61,12 @@ export interface TelegramPreviewRuntimeDeps<
61
61
  sendDraft: (
62
62
  chatId: number,
63
63
  draftId: number,
64
- text: string,
64
+ text?: string,
65
+ options?: {
66
+ parse_mode?: string;
67
+ entities?: unknown[];
68
+ message_thread_id?: number;
69
+ },
65
70
  ) => Promise<unknown>;
66
71
  sendMessage: (
67
72
  chatId: number,
@@ -158,7 +163,12 @@ export interface TelegramPreviewControllerDeps<
158
163
  sendDraft: (
159
164
  chatId: number,
160
165
  draftId: number,
161
- text: string,
166
+ text?: string,
167
+ options?: {
168
+ parse_mode?: string;
169
+ entities?: unknown[];
170
+ message_thread_id?: number;
171
+ },
162
172
  ) => Promise<unknown>;
163
173
  sendMessage: (
164
174
  chatId: number,
package/lib/queue.ts CHANGED
@@ -66,6 +66,7 @@ export interface TelegramQueueItemBase {
66
66
  kind: TelegramQueueItemKind;
67
67
  chatId: number;
68
68
  replyToMessageId: number;
69
+ guestQueryId?: string;
69
70
  queueOrder: number;
70
71
  queueLane: TelegramQueueLane;
71
72
  laneOrder: number;
@@ -113,6 +114,7 @@ export interface TelegramActiveTurnStore<
113
114
  clear: () => void;
114
115
  getChatId: () => number | undefined;
115
116
  getReplyToMessageId: () => number | undefined;
117
+ getGuestQueryId: () => string | undefined;
116
118
  getSourceMessageIds: () => number[] | undefined;
117
119
  }
118
120
 
@@ -203,6 +205,7 @@ export function createTelegramActiveTurnStore<
203
205
  },
204
206
  getChatId: () => activeTurn?.chatId,
205
207
  getReplyToMessageId: () => activeTurn?.replyToMessageId,
208
+ getGuestQueryId: () => activeTurn?.guestQueryId,
206
209
  getSourceMessageIds: () => activeTurn?.sourceMessageIds,
207
210
  };
208
211
  }
@@ -785,6 +788,7 @@ export interface TelegramAgentEndRuntimeDeps<
785
788
  text: string,
786
789
  ) => Promise<unknown>;
787
790
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
791
+ answerGuestQuery?: (guestQueryId: string, text?: string) => Promise<void>;
788
792
  planOutboundReply?: (
789
793
  markdown: string,
790
794
  ) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
@@ -832,6 +836,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
832
836
  >["sendMarkdownReply"];
833
837
  sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
834
838
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
839
+ answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
835
840
  planOutboundReply?: TelegramAgentEndRuntimeDeps<
836
841
  TTurn,
837
842
  TReplyMarkup
@@ -952,6 +957,7 @@ export function createTelegramAgentEndHook<
952
957
  sendMarkdownReply: deps.sendMarkdownReply,
953
958
  sendTextReply: deps.sendTextReply,
954
959
  sendQueuedAttachments: deps.sendQueuedAttachments,
960
+ answerGuestQuery: deps.answerGuestQuery,
955
961
  planOutboundReply: deps.planOutboundReply,
956
962
  sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
957
963
  getDefaultChatId: deps.getDefaultChatId,
@@ -1007,6 +1013,25 @@ export async function handleTelegramAgentEndRuntime<
1007
1013
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1008
1014
  return;
1009
1015
  }
1016
+ if (turn.guestQueryId) {
1017
+ if (deps.isCurrentOwner && !deps.isCurrentOwner()) {
1018
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1019
+ return;
1020
+ }
1021
+ if (assistant.errorMessage) {
1022
+ await deps.answerGuestQuery?.(
1023
+ turn.guestQueryId,
1024
+ "Telegram bridge: π failed while processing the request.",
1025
+ );
1026
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1027
+ return;
1028
+ }
1029
+ if (finalText) {
1030
+ await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1031
+ }
1032
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1033
+ return;
1034
+ }
1010
1035
  if (endPlan.shouldClearPreview) {
1011
1036
  await deps.clearPreview(turn.chatId);
1012
1037
  }
package/lib/routing.ts CHANGED
@@ -17,6 +17,7 @@ import type { TelegramBridgeRuntime } from "./runtime.ts";
17
17
  import * as TextGroups from "./text-groups.ts";
18
18
  import * as Turns from "./turns.ts";
19
19
  import * as Updates from "./updates.ts";
20
+ import type { TelegramUser } from "./updates.ts";
20
21
 
21
22
  export type TelegramRoutedMessage = Updates.TelegramUpdateMessage &
22
23
  Media.TelegramMediaMessage &
@@ -80,6 +81,7 @@ export interface TelegramInboundRouteRuntimeDeps<
80
81
  callbackQueryId: string,
81
82
  text?: string,
82
83
  ) => Promise<void>;
84
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
83
85
  sendTextReply: (
84
86
  chatId: number,
85
87
  replyToMessageId: number,
@@ -364,6 +366,33 @@ export function createTelegramInboundRouteRuntime<
364
366
  ...deps.telegramQueueStore,
365
367
  updateStatus: deps.updateStatus,
366
368
  });
369
+ const handleAuthorizedTelegramGuestMessage = async (
370
+ guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
371
+ ctx: TContext,
372
+ ): Promise<void> => {
373
+ const text = guestMessage.text ?? "";
374
+ const order = deps.bridgeRuntime.queue.allocateItemOrder();
375
+ const guestTurn: Queue.PendingTelegramTurn = {
376
+ kind: "prompt",
377
+ chatId: 0,
378
+ replyToMessageId: 0,
379
+ guestQueryId: guestMessage.guest_query_id,
380
+ sourceMessageIds: [],
381
+ queueOrder: order,
382
+ queueLane: "default",
383
+ laneOrder: order,
384
+ queuedAttachments: [],
385
+ content: [{ type: "text", text }],
386
+ historyText: text,
387
+ statusSummary: Turns.truncateTelegramQueueSummary(text),
388
+ };
389
+ const items = deps.telegramQueueStore.getQueuedItems();
390
+ deps.telegramQueueStore.setQueuedItems(
391
+ Queue.appendTelegramQueueItem(items, guestTurn),
392
+ );
393
+ deps.updateStatus(ctx);
394
+ deps.dispatchNextQueuedTelegramTurn(ctx);
395
+ };
367
396
  return Updates.createTelegramPairedUpdateRuntime<TContext, TUpdate>({
368
397
  getAllowedUserId: deps.configStore.getAllowedUserId,
369
398
  setAllowedUserId: deps.configStore.setAllowedUserId,
@@ -377,9 +406,11 @@ export function createTelegramInboundRouteRuntime<
377
406
  prioritizeQueuedTelegramTurnByMessageId:
378
407
  deps.queueMutationRuntime.prioritizeByMessageId,
379
408
  answerCallbackQuery: deps.answerCallbackQuery,
409
+ answerGuestQuery: deps.answerGuestQuery,
380
410
  handleAuthorizedTelegramCallbackQuery: callbackHandler,
381
411
  sendTextReply: deps.sendTextReply,
382
412
  handleAuthorizedTelegramMessage: textDispatch.handleMessage,
383
413
  handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
414
+ handleAuthorizedTelegramGuestMessage,
384
415
  });
385
416
  }
package/lib/runtime.ts CHANGED
@@ -365,7 +365,8 @@ export function startTelegramTypingLoop(
365
365
  state: TelegramBridgeRuntimeState,
366
366
  deps: TelegramTypingLoopDeps,
367
367
  ): boolean {
368
- if (state.typingInterval || deps.chatId === undefined) return false;
368
+ if (state.typingInterval || deps.chatId === undefined || deps.chatId === 0)
369
+ return false;
369
370
  const sendTyping = (): void => {
370
371
  void deps.sendTypingAction(deps.chatId as number);
371
372
  };
package/lib/updates.ts CHANGED
@@ -130,10 +130,19 @@ export interface TelegramCallbackQuery {
130
130
  message?: TelegramUpdateMessage;
131
131
  }
132
132
 
133
+ export interface TelegramGuestMessage {
134
+ guest_query_id: string;
135
+ chat: TelegramChat;
136
+ from?: TelegramUser;
137
+ message_id?: number;
138
+ text?: string;
139
+ }
140
+
133
141
  export interface TelegramUpdateRouting {
134
142
  message?: TelegramUpdateMessage;
135
143
  edited_message?: TelegramUpdateMessage;
136
144
  callback_query?: TelegramCallbackQuery;
145
+ guest_message?: TelegramGuestMessage;
137
146
  }
138
147
 
139
148
  export function getAuthorizedTelegramCallbackQuery(
@@ -178,6 +187,16 @@ export function getAuthorizedTelegramEditedMessage(
178
187
  return message;
179
188
  }
180
189
 
190
+ export function getAuthorizedTelegramGuestMessage(
191
+ update: TelegramUpdateRouting,
192
+ ): TelegramGuestMessage | undefined {
193
+ const guestMessage = update.guest_message;
194
+ if (!guestMessage || !guestMessage.from || guestMessage.from.is_bot) {
195
+ return undefined;
196
+ }
197
+ return guestMessage;
198
+ }
199
+
181
200
  // --- Flow ---
182
201
 
183
202
  export interface TelegramMessageReactionUpdated {
@@ -198,6 +217,7 @@ export type TelegramUpdateFlowAction<
198
217
  TelegramMessageReactionUpdated,
199
218
  TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
200
219
  TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
220
+ TGuestMessage extends TelegramGuestMessage = TelegramGuestMessage,
201
221
  > =
202
222
  | { kind: "ignore" }
203
223
  | { kind: "deleted"; messageIds: number[] }
@@ -216,6 +236,11 @@ export type TelegramUpdateFlowAction<
216
236
  kind: "edited-message";
217
237
  message: TMessage & { from: TelegramUser };
218
238
  authorization: TelegramAuthorizationState;
239
+ }
240
+ | {
241
+ kind: "guest";
242
+ guestMessage: TGuestMessage & { from: TelegramUser };
243
+ authorization: TelegramAuthorizationState;
219
244
  };
220
245
 
221
246
  export function buildTelegramUpdateFlowAction<
@@ -226,7 +251,8 @@ export function buildTelegramUpdateFlowAction<
226
251
  ): TelegramUpdateFlowAction<
227
252
  NonNullable<TUpdate["message_reaction"]>,
228
253
  NonNullable<TUpdate["callback_query"]>,
229
- NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
254
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>,
255
+ NonNullable<TUpdate["guest_message"]>
230
256
  > {
231
257
  const deletedMessageIds = extractDeletedTelegramMessageIds(update);
232
258
  if (deletedMessageIds.length > 0) {
@@ -272,6 +298,19 @@ export function buildTelegramUpdateFlowAction<
272
298
  ),
273
299
  };
274
300
  }
301
+ const guestMessage = getAuthorizedTelegramGuestMessage(update);
302
+ if (guestMessage?.from) {
303
+ return {
304
+ kind: "guest",
305
+ guestMessage: guestMessage as NonNullable<TUpdate["guest_message"]> & {
306
+ from: TelegramUser;
307
+ },
308
+ authorization: getTelegramAuthorizationState(
309
+ guestMessage.from.id,
310
+ allowedUserId,
311
+ ),
312
+ };
313
+ }
275
314
  return { kind: "ignore" };
276
315
  }
277
316
 
@@ -282,6 +321,7 @@ export type TelegramUpdateExecutionPlan<
282
321
  TelegramMessageReactionUpdated,
283
322
  TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
284
323
  TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
324
+ TGuestMessage extends TelegramGuestMessage = TelegramGuestMessage,
285
325
  > =
286
326
  | { kind: "ignore" }
287
327
  | { kind: "deleted"; messageIds: number[] }
@@ -307,15 +347,31 @@ export type TelegramUpdateExecutionPlan<
307
347
  message: TMessage & { from: TelegramUser };
308
348
  shouldPair: boolean;
309
349
  shouldDeny: boolean;
350
+ }
351
+ | {
352
+ kind: "guest";
353
+ guestMessage: TGuestMessage & { from: TelegramUser };
354
+ shouldDeny: boolean;
310
355
  };
311
356
 
312
357
  export function buildTelegramUpdateExecutionPlan<
313
358
  TReactionUpdate extends TelegramMessageReactionUpdated,
314
359
  TCallbackQuery extends TelegramCallbackQuery,
315
360
  TMessage extends TelegramUpdateMessage,
361
+ TGuestMessage extends TelegramGuestMessage,
316
362
  >(
317
- action: TelegramUpdateFlowAction<TReactionUpdate, TCallbackQuery, TMessage>,
318
- ): TelegramUpdateExecutionPlan<TReactionUpdate, TCallbackQuery, TMessage> {
363
+ action: TelegramUpdateFlowAction<
364
+ TReactionUpdate,
365
+ TCallbackQuery,
366
+ TMessage,
367
+ TGuestMessage
368
+ >,
369
+ ): TelegramUpdateExecutionPlan<
370
+ TReactionUpdate,
371
+ TCallbackQuery,
372
+ TMessage,
373
+ TGuestMessage
374
+ > {
319
375
  switch (action.kind) {
320
376
  case "ignore":
321
377
  return { kind: "ignore" };
@@ -345,6 +401,12 @@ export function buildTelegramUpdateExecutionPlan<
345
401
  shouldPair: action.authorization.kind === "pair",
346
402
  shouldDeny: action.authorization.kind === "deny",
347
403
  };
404
+ case "guest":
405
+ return {
406
+ kind: "guest",
407
+ guestMessage: action.guestMessage,
408
+ shouldDeny: action.authorization.kind === "deny",
409
+ };
348
410
  }
349
411
  }
350
412
 
@@ -387,6 +449,7 @@ export interface TelegramUpdateRuntimeDeps<
387
449
  callbackQueryId: string,
388
450
  text?: string,
389
451
  ) => Promise<void>;
452
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
390
453
  handleAuthorizedTelegramCallbackQuery: (
391
454
  query: TCallbackQuery,
392
455
  ctx: TContext,
@@ -404,6 +467,10 @@ export interface TelegramUpdateRuntimeDeps<
404
467
  message: TMessage,
405
468
  ctx: TContext,
406
469
  ) => unknown;
470
+ handleAuthorizedTelegramGuestMessage?: (
471
+ guestMessage: TelegramGuestMessage & { from: TelegramUser },
472
+ ctx: TContext,
473
+ ) => Promise<void>;
407
474
  }
408
475
 
409
476
  export interface TelegramUpdateRuntimeControllerDeps<
@@ -431,6 +498,7 @@ export interface TelegramUpdateRuntimeControllerDeps<
431
498
  callbackQueryId: string,
432
499
  text?: string,
433
500
  ) => Promise<void>;
501
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
434
502
  handleAuthorizedTelegramCallbackQuery: (
435
503
  query: TCallbackQuery,
436
504
  ctx: TContext,
@@ -448,6 +516,10 @@ export interface TelegramUpdateRuntimeControllerDeps<
448
516
  message: TMessage,
449
517
  ctx: TContext,
450
518
  ) => unknown;
519
+ handleAuthorizedTelegramGuestMessage?: (
520
+ guestMessage: TelegramGuestMessage & { from: TelegramUser },
521
+ ctx: TContext,
522
+ ) => Promise<void>;
451
523
  }
452
524
 
453
525
  export interface TelegramUpdateRuntimeController<
@@ -536,12 +608,15 @@ export function createTelegramPairedUpdateRuntime<
536
608
  updateStatus: deps.updateStatus,
537
609
  }).pairIfNeeded,
538
610
  answerCallbackQuery: deps.answerCallbackQuery,
611
+ answerGuestQuery: deps.answerGuestQuery,
539
612
  handleAuthorizedTelegramCallbackQuery:
540
613
  deps.handleAuthorizedTelegramCallbackQuery,
541
614
  sendTextReply: deps.sendTextReply,
542
615
  handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
543
616
  handleAuthorizedTelegramEditedMessage:
544
617
  deps.handleAuthorizedTelegramEditedMessage,
618
+ handleAuthorizedTelegramGuestMessage:
619
+ deps.handleAuthorizedTelegramGuestMessage,
545
620
  });
546
621
  }
547
622
 
@@ -582,12 +657,15 @@ export function createTelegramUpdateRuntime<
582
657
  handleAuthorizedTelegramReactionUpdate: handleAuthorizedReactionUpdate,
583
658
  pairTelegramUserIfNeeded: deps.pairTelegramUserIfNeeded,
584
659
  answerCallbackQuery: deps.answerCallbackQuery,
660
+ answerGuestQuery: deps.answerGuestQuery,
585
661
  handleAuthorizedTelegramCallbackQuery:
586
662
  deps.handleAuthorizedTelegramCallbackQuery,
587
663
  sendTextReply: deps.sendTextReply,
588
664
  handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
589
665
  handleAuthorizedTelegramEditedMessage:
590
666
  deps.handleAuthorizedTelegramEditedMessage,
667
+ handleAuthorizedTelegramGuestMessage:
668
+ deps.handleAuthorizedTelegramGuestMessage,
591
669
  }),
592
670
  };
593
671
  }
@@ -712,6 +790,22 @@ export async function executeTelegramUpdatePlan<
712
790
  await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
713
791
  return;
714
792
  }
793
+ if (plan.kind === "guest") {
794
+ if (plan.shouldDeny) {
795
+ await deps.answerGuestQuery(
796
+ plan.guestMessage.guest_query_id,
797
+ "Access denied.",
798
+ );
799
+ return;
800
+ }
801
+ if (deps.handleAuthorizedTelegramGuestMessage) {
802
+ await deps.handleAuthorizedTelegramGuestMessage(
803
+ plan.guestMessage,
804
+ deps.ctx,
805
+ );
806
+ }
807
+ return;
808
+ }
715
809
  const pairedNow = plan.shouldPair
716
810
  ? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
717
811
  : false;
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "private": false,
5
- "description": "Better Telegram DM bridge extension for π",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "Telegram Runtime Adapter for π",
6
9
  "type": "module",
7
10
  "keywords": [
8
11
  "pi-package",
@@ -20,6 +23,9 @@
20
23
  "bugs": {
21
24
  "url": "https://github.com/llblab/pi-telegram/issues"
22
25
  },
26
+ "engines": {
27
+ "node": ">=22.0.0"
28
+ },
23
29
  "scripts": {
24
30
  "test": "node --experimental-strip-types --test tests/*.test.ts",
25
31
  "typecheck": "tsc --noEmit",
@@ -37,9 +43,6 @@
37
43
  "docs/",
38
44
  "screenshot.png"
39
45
  ],
40
- "publishConfig": {
41
- "access": "public"
42
- },
43
46
  "pi": {
44
47
  "extensions": [
45
48
  "./index.ts"
@@ -47,9 +50,9 @@
47
50
  "image": "https://github.com/llblab/pi-telegram/raw/main/screenshot.png"
48
51
  },
49
52
  "peerDependencies": {
50
- "@mariozechner/pi-agent-core": "*",
51
- "@mariozechner/pi-ai": "*",
52
- "@mariozechner/pi-coding-agent": "*",
53
+ "@earendil-works/pi-agent-core": "*",
54
+ "@earendil-works/pi-ai": "*",
55
+ "@earendil-works/pi-coding-agent": "*",
53
56
  "@sinclair/typebox": "*"
54
57
  },
55
58
  "devDependencies": {