@llblab/pi-telegram 0.9.6 → 0.9.8

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/BACKLOG.md CHANGED
@@ -2,13 +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.
11
- - Tighten dependency posture for reproducible extension development.
12
- - Priority: Medium.
13
- - Idea: Replace broad peer dependency `*` ranges and dev dependency `latest` ranges with explicit compatible ranges once the supported pi/Node/TypeScript matrix is clear.
14
- - Exit: `package.json` documents the supported Node expectation and compatible pi package ranges without over-constraining early-stage extension iteration.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.8: Guest Mode Context
4
+
5
+ - `[Guest Mode]` Extended the [telegram] prefix with |from:user (sender) and |guest:GroupName (source chat for group guest messages) so the agent sees who sent the message and where from. Private guest chats omit the guest: suffix.
6
+ - `[Guest Mode]` Added |from:user to the [reply] block so the agent knows the original author of a replied-to message in guest mode.
7
+ - `[Guest Mode]` Formatted guest prompt text identically to regular DMs through buildTelegramTurnPrompt, including [attachments] and [outputs] sections with file downloads and inbound handler processing.
8
+ - `[Prompts]` Added compact agent guidance explaining guest-mode prefix suffixes (|from:, |guest:) and reply-from context.
9
+
10
+ ## 0.9.7: Bot API 10.0 Alignment
11
+
12
+ - `[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.
13
+ - `[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.
14
+ - `[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.
15
+ - `[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`.
16
+ - `[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.
17
+ - `[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.
18
+ - `[Runtime]` Added typing-loop skip for guest turns (`chatId === 0`) to avoid spurious `sendChatAction` errors in the status bar.
19
+ - `[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.
20
+ - `[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.
21
+
3
22
  ## 0.9.6: Runtime Adapter Positioning
4
23
 
5
24
  - `[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.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
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
- 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.
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.
10
10
 
11
11
  ## Install
12
12
 
@@ -61,24 +61,11 @@ The first user to message the bot becomes the exclusive owner of the adapter. Me
61
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:
62
62
 
63
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`.
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
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
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
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.
68
68
 
69
- Proxy example:
70
-
71
- ```bash
72
- export HTTPS_PROXY="http://127.0.0.1:8083"
73
- export HTTP_PROXY="http://127.0.0.1:8083"
74
- export NO_PROXY="localhost,127.0.0.1"
75
- export NODE_USE_ENV_PROXY=1
76
-
77
- pi
78
- ```
79
-
80
- 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.
81
-
82
69
  ## Use
83
70
 
84
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.
@@ -134,7 +121,7 @@ The inline application menu is the primary operator surface. It exposes status,
134
121
 
135
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.
136
123
 
137
- 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 set intentionally includes common default reactions first; premium-only reactions such as `🗑` are optional convenience, not the core queue UI. The same rules apply to text, voice, files, images, and media groups.
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.
138
125
 
139
126
  ### Streaming and Telegram HTML rendering
140
127
 
@@ -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
@@ -151,12 +151,26 @@ export interface TelegramMessageReactionUpdated {
151
151
  date: number;
152
152
  }
153
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
+ reply_to_message?: TelegramMessage;
165
+ }
166
+
154
167
  export interface TelegramUpdate {
155
168
  update_id: number;
156
169
  message?: TelegramMessage;
157
170
  edited_message?: TelegramMessage;
158
171
  callback_query?: TelegramCallbackQuery;
159
172
  message_reaction?: TelegramMessageReactionUpdated;
173
+ guest_message?: TelegramGuestMessage;
160
174
  deleted_business_messages?: { message_ids?: unknown };
161
175
  }
162
176
 
@@ -184,6 +198,15 @@ export type TelegramEditMessageTextBody = Record<string, unknown> & {
184
198
  parse_mode?: "HTML";
185
199
  };
186
200
 
201
+ export type TelegramSendMessageDraftBody = Record<string, unknown> & {
202
+ chat_id: number;
203
+ draft_id: number;
204
+ text?: string;
205
+ parse_mode?: string;
206
+ entities?: unknown[];
207
+ message_thread_id?: number;
208
+ };
209
+
187
210
  interface TelegramApiResponse<T> {
188
211
  ok: boolean;
189
212
  result?: T;
@@ -233,6 +256,7 @@ export interface TelegramApiClient {
233
256
  callbackQueryId: string,
234
257
  text?: string,
235
258
  ) => Promise<void>;
259
+ answerGuestQuery?: (guestQueryId: string, text?: string) => Promise<void>;
236
260
  }
237
261
 
238
262
  export interface TelegramBridgeApiRuntimeDeps {
@@ -275,7 +299,12 @@ export interface TelegramBridgeApiRuntime {
275
299
  sendMessageDraft: (
276
300
  chatId: number,
277
301
  draftId: number,
278
- text: string,
302
+ text?: string,
303
+ options?: {
304
+ parse_mode?: string;
305
+ entities?: unknown[];
306
+ message_thread_id?: number;
307
+ },
279
308
  ) => Promise<boolean>;
280
309
  sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
281
310
  editMessageText: (
@@ -285,6 +314,7 @@ export interface TelegramBridgeApiRuntime {
285
314
  callbackQueryId: string,
286
315
  text?: string,
287
316
  ) => Promise<void>;
317
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
288
318
  prepareTempDir: () => Promise<number>;
289
319
  }
290
320
 
@@ -534,9 +564,7 @@ export async function fetchTelegramBotIdentity(
534
564
  botToken: string,
535
565
  fetchImpl: typeof fetch = fetch,
536
566
  ): Promise<TelegramBotIdentityResponse> {
537
- const response = await fetchImpl(
538
- `${TELEGRAM_API_BASE}/bot${botToken}/getMe`,
539
- );
567
+ const response = await fetchImpl(`${TELEGRAM_API_BASE}/bot${botToken}/getMe`);
540
568
  return response.json() as Promise<TelegramBotIdentityResponse>;
541
569
  }
542
570
 
@@ -559,14 +587,11 @@ export async function callTelegramMultipart<TResponse>(
559
587
  form.set(key, value);
560
588
  }
561
589
  form.set(fileField, fileBlob, fileName);
562
- return fetch(
563
- `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
564
- {
565
- method: "POST",
566
- body: form,
567
- signal: options?.signal,
568
- },
569
- );
590
+ return fetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
591
+ method: "POST",
592
+ body: form,
593
+ signal: options?.signal,
594
+ });
570
595
  },
571
596
  options,
572
597
  );
@@ -732,13 +757,18 @@ export function createTelegramBridgeApiRuntime(
732
757
  }),
733
758
  "typing",
734
759
  ),
735
- sendMessageDraft: (chatId, draftId, text) => {
736
- if (text.length === 0) return Promise.resolve(false);
737
- return callRecorded<boolean>("sendMessageDraft", {
760
+ sendMessageDraft: (chatId, draftId, text, options) => {
761
+ const body: Record<string, unknown> = {
738
762
  chat_id: chatId,
739
763
  draft_id: draftId,
740
- text,
741
- });
764
+ };
765
+ if (text !== undefined) body.text = text;
766
+ if (options?.parse_mode !== undefined)
767
+ body.parse_mode = options.parse_mode;
768
+ if (options?.entities !== undefined) body.entities = options.entities;
769
+ if (options?.message_thread_id !== undefined)
770
+ body.message_thread_id = options.message_thread_id;
771
+ return callRecorded<boolean>("sendMessageDraft", body);
742
772
  },
743
773
  sendMessage: (body) =>
744
774
  callRecorded<TelegramSentMessage>("sendMessage", body),
@@ -755,6 +785,18 @@ export function createTelegramBridgeApiRuntime(
755
785
  answerCallbackQuery: (callbackQueryId, text) => {
756
786
  return deps.client.answerCallbackQuery(callbackQueryId, text);
757
787
  },
788
+ answerGuestQuery: (guestQueryId, text) => {
789
+ const body: Record<string, unknown> = { guest_query_id: guestQueryId };
790
+ if (text !== undefined) {
791
+ body.result = {
792
+ type: "article",
793
+ id: "1",
794
+ title: text.length > 64 ? text.slice(0, 61) + "..." : text,
795
+ input_message_content: { message_text: text },
796
+ };
797
+ }
798
+ return callRecorded<void>("answerGuestQuery", body);
799
+ },
758
800
  prepareTempDir: () =>
759
801
  prepareTelegramTempDir(deps.tempDir, deps.tempFileMaxAgeMs),
760
802
  };
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/prompts.ts CHANGED
@@ -12,8 +12,8 @@ const SYSTEM_PROMPT_SUFFIX = `
12
12
  Telegram bridge extension is active.
13
13
 
14
14
  Inbound context:
15
- - \`[telegram]\` marks Telegram-originated messages.
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.
15
+ - \`[telegram]\` marks Telegram-originated messages. Suffixes \`|from:user\` (sender) and \`|guest:group\` (guest mode — message from another chat where the bot is not a member) may be present; the bot sees the message as if forwarded from that user/chat.
16
+ - \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Suffix \`|from:user\` identifies the original author in guest-mode replies. 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 inbound-handler stdout such as transcriptions or extracted text for those attachments.
18
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.
19
19
 
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
@@ -6,6 +6,7 @@
6
6
 
7
7
  import * as OutboundHandlers from "./outbound-handlers.ts";
8
8
  import * as Commands from "./commands.ts";
9
+ import { readFile } from "node:fs/promises";
9
10
  import type { TelegramConfigStore } from "./config.ts";
10
11
  import type { TelegramInboundHandlerRuntime } from "./inbound-handlers.ts";
11
12
  import * as Media from "./media.ts";
@@ -17,6 +18,7 @@ import type { TelegramBridgeRuntime } from "./runtime.ts";
17
18
  import * as TextGroups from "./text-groups.ts";
18
19
  import * as Turns from "./turns.ts";
19
20
  import * as Updates from "./updates.ts";
21
+ import type { TelegramUser } from "./updates.ts";
20
22
 
21
23
  export type TelegramRoutedMessage = Updates.TelegramUpdateMessage &
22
24
  Media.TelegramMediaMessage &
@@ -80,6 +82,7 @@ export interface TelegramInboundRouteRuntimeDeps<
80
82
  callbackQueryId: string,
81
83
  text?: string,
82
84
  ) => Promise<void>;
85
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
83
86
  sendTextReply: (
84
87
  chatId: number,
85
88
  replyToMessageId: number,
@@ -364,6 +367,103 @@ export function createTelegramInboundRouteRuntime<
364
367
  ...deps.telegramQueueStore,
365
368
  updateStatus: deps.updateStatus,
366
369
  });
370
+ const handleAuthorizedTelegramGuestMessage = async (
371
+ guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
372
+ ctx: TContext,
373
+ ): Promise<void> => {
374
+ const text = guestMessage.text ?? "";
375
+ const gm = guestMessage as unknown as Record<string, unknown>;
376
+ // Build telegram prefix with guest context
377
+ const fromRaw = gm.from as Record<string, unknown> | undefined;
378
+ const fromName =
379
+ (fromRaw?.username as string) ||
380
+ (fromRaw?.first_name as string) ||
381
+ "";
382
+ const chatRaw = gm.chat as Record<string, unknown>;
383
+ const chatTitle = chatRaw?.title as string | undefined;
384
+ const chatType = chatRaw?.type as string;
385
+ const prefixParts = ["telegram"];
386
+ if (fromName) prefixParts.push(`from:${fromName}`);
387
+ if (chatType !== "private" && chatTitle) {
388
+ prefixParts.push(`guest:${chatTitle}`);
389
+ }
390
+ const telegramPrefix = `[${prefixParts.join("|")}]`;
391
+ // Extract reply context
392
+ const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
393
+ const replyText =
394
+ replyMsg
395
+ ? ((replyMsg.text as string) || (replyMsg.caption as string) || "").trim()
396
+ : "";
397
+ const replyFrom =
398
+ replyMsg
399
+ ? (replyMsg.from as Record<string, unknown> | undefined)?.username as string | undefined
400
+ : undefined;
401
+ // Download files, run inbound handlers
402
+ const guestMsg = guestMessage as unknown as Media.TelegramMediaMessage;
403
+ const files = await Media.downloadTelegramMessageFiles([guestMsg], {
404
+ downloadFile: deps.downloadFile,
405
+ });
406
+ const processed = await deps.inboundHandlerRuntime.process(files, text, ctx);
407
+ let rawText = processed.rawText || text;
408
+ // Append reply context after handler processing
409
+ if (replyText) {
410
+ const replyBlock = replyFrom
411
+ ? `[reply|from:${replyFrom}] ${replyText}`
412
+ : `[reply] ${replyText}`;
413
+ rawText = `${rawText}\n\n${replyBlock}`;
414
+ }
415
+ const promptText = Turns.buildTelegramTurnPrompt({
416
+ telegramPrefix,
417
+ rawText,
418
+ files,
419
+ promptFiles: processed.promptFiles,
420
+ handlerOutputs: processed.handlerOutputs,
421
+ });
422
+ const order = deps.bridgeRuntime.queue.allocateItemOrder();
423
+ const content: Queue.TelegramPromptContent[] = [
424
+ { type: "text", text: promptText },
425
+ ];
426
+ for (const file of processed.promptFiles) {
427
+ if (file.isImage && file.mimeType) {
428
+ try {
429
+ const buffer = await readFile(file.path);
430
+ content.push({
431
+ type: "image",
432
+ data: Buffer.from(buffer).toString("base64"),
433
+ mimeType: file.mimeType,
434
+ });
435
+ } catch {
436
+ // skip unreadable files
437
+ }
438
+ }
439
+ }
440
+ const guestTurn: Queue.PendingTelegramTurn = {
441
+ kind: "prompt",
442
+ chatId: 0,
443
+ replyToMessageId: 0,
444
+ guestQueryId: guestMessage.guest_query_id,
445
+ sourceMessageIds: [],
446
+ queueOrder: order,
447
+ queueLane: "default",
448
+ laneOrder: order,
449
+ queuedAttachments: [],
450
+ content,
451
+ historyText: Turns.formatTelegramTurnStatusSummary(
452
+ processed.rawText || text,
453
+ processed.promptFiles,
454
+ processed.handlerOutputs,
455
+ ),
456
+ statusSummary: Turns.truncateTelegramQueueSummary(
457
+ processed.rawText || text,
458
+ ),
459
+ };
460
+ const items = deps.telegramQueueStore.getQueuedItems();
461
+ deps.telegramQueueStore.setQueuedItems(
462
+ Queue.appendTelegramQueueItem(items, guestTurn),
463
+ );
464
+ deps.updateStatus(ctx);
465
+ deps.dispatchNextQueuedTelegramTurn(ctx);
466
+ };
367
467
  return Updates.createTelegramPairedUpdateRuntime<TContext, TUpdate>({
368
468
  getAllowedUserId: deps.configStore.getAllowedUserId,
369
469
  setAllowedUserId: deps.configStore.setAllowedUserId,
@@ -377,9 +477,11 @@ export function createTelegramInboundRouteRuntime<
377
477
  prioritizeQueuedTelegramTurnByMessageId:
378
478
  deps.queueMutationRuntime.prioritizeByMessageId,
379
479
  answerCallbackQuery: deps.answerCallbackQuery,
480
+ answerGuestQuery: deps.answerGuestQuery,
380
481
  handleAuthorizedTelegramCallbackQuery: callbackHandler,
381
482
  sendTextReply: deps.sendTextReply,
382
483
  handleAuthorizedTelegramMessage: textDispatch.handleMessage,
383
484
  handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
485
+ handleAuthorizedTelegramGuestMessage,
384
486
  });
385
487
  }
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,20 @@ 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
+ reply_to_message?: TelegramUpdateMessage;
140
+ }
141
+
133
142
  export interface TelegramUpdateRouting {
134
143
  message?: TelegramUpdateMessage;
135
144
  edited_message?: TelegramUpdateMessage;
136
145
  callback_query?: TelegramCallbackQuery;
146
+ guest_message?: TelegramGuestMessage;
137
147
  }
138
148
 
139
149
  export function getAuthorizedTelegramCallbackQuery(
@@ -178,6 +188,16 @@ export function getAuthorizedTelegramEditedMessage(
178
188
  return message;
179
189
  }
180
190
 
191
+ export function getAuthorizedTelegramGuestMessage(
192
+ update: TelegramUpdateRouting,
193
+ ): TelegramGuestMessage | undefined {
194
+ const guestMessage = update.guest_message;
195
+ if (!guestMessage || !guestMessage.from || guestMessage.from.is_bot) {
196
+ return undefined;
197
+ }
198
+ return guestMessage;
199
+ }
200
+
181
201
  // --- Flow ---
182
202
 
183
203
  export interface TelegramMessageReactionUpdated {
@@ -198,6 +218,7 @@ export type TelegramUpdateFlowAction<
198
218
  TelegramMessageReactionUpdated,
199
219
  TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
200
220
  TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
221
+ TGuestMessage extends TelegramGuestMessage = TelegramGuestMessage,
201
222
  > =
202
223
  | { kind: "ignore" }
203
224
  | { kind: "deleted"; messageIds: number[] }
@@ -216,6 +237,11 @@ export type TelegramUpdateFlowAction<
216
237
  kind: "edited-message";
217
238
  message: TMessage & { from: TelegramUser };
218
239
  authorization: TelegramAuthorizationState;
240
+ }
241
+ | {
242
+ kind: "guest";
243
+ guestMessage: TGuestMessage & { from: TelegramUser };
244
+ authorization: TelegramAuthorizationState;
219
245
  };
220
246
 
221
247
  export function buildTelegramUpdateFlowAction<
@@ -226,7 +252,8 @@ export function buildTelegramUpdateFlowAction<
226
252
  ): TelegramUpdateFlowAction<
227
253
  NonNullable<TUpdate["message_reaction"]>,
228
254
  NonNullable<TUpdate["callback_query"]>,
229
- NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
255
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>,
256
+ NonNullable<TUpdate["guest_message"]>
230
257
  > {
231
258
  const deletedMessageIds = extractDeletedTelegramMessageIds(update);
232
259
  if (deletedMessageIds.length > 0) {
@@ -272,6 +299,19 @@ export function buildTelegramUpdateFlowAction<
272
299
  ),
273
300
  };
274
301
  }
302
+ const guestMessage = getAuthorizedTelegramGuestMessage(update);
303
+ if (guestMessage?.from) {
304
+ return {
305
+ kind: "guest",
306
+ guestMessage: guestMessage as NonNullable<TUpdate["guest_message"]> & {
307
+ from: TelegramUser;
308
+ },
309
+ authorization: getTelegramAuthorizationState(
310
+ guestMessage.from.id,
311
+ allowedUserId,
312
+ ),
313
+ };
314
+ }
275
315
  return { kind: "ignore" };
276
316
  }
277
317
 
@@ -282,6 +322,7 @@ export type TelegramUpdateExecutionPlan<
282
322
  TelegramMessageReactionUpdated,
283
323
  TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
284
324
  TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
325
+ TGuestMessage extends TelegramGuestMessage = TelegramGuestMessage,
285
326
  > =
286
327
  | { kind: "ignore" }
287
328
  | { kind: "deleted"; messageIds: number[] }
@@ -307,15 +348,31 @@ export type TelegramUpdateExecutionPlan<
307
348
  message: TMessage & { from: TelegramUser };
308
349
  shouldPair: boolean;
309
350
  shouldDeny: boolean;
351
+ }
352
+ | {
353
+ kind: "guest";
354
+ guestMessage: TGuestMessage & { from: TelegramUser };
355
+ shouldDeny: boolean;
310
356
  };
311
357
 
312
358
  export function buildTelegramUpdateExecutionPlan<
313
359
  TReactionUpdate extends TelegramMessageReactionUpdated,
314
360
  TCallbackQuery extends TelegramCallbackQuery,
315
361
  TMessage extends TelegramUpdateMessage,
362
+ TGuestMessage extends TelegramGuestMessage,
316
363
  >(
317
- action: TelegramUpdateFlowAction<TReactionUpdate, TCallbackQuery, TMessage>,
318
- ): TelegramUpdateExecutionPlan<TReactionUpdate, TCallbackQuery, TMessage> {
364
+ action: TelegramUpdateFlowAction<
365
+ TReactionUpdate,
366
+ TCallbackQuery,
367
+ TMessage,
368
+ TGuestMessage
369
+ >,
370
+ ): TelegramUpdateExecutionPlan<
371
+ TReactionUpdate,
372
+ TCallbackQuery,
373
+ TMessage,
374
+ TGuestMessage
375
+ > {
319
376
  switch (action.kind) {
320
377
  case "ignore":
321
378
  return { kind: "ignore" };
@@ -345,6 +402,12 @@ export function buildTelegramUpdateExecutionPlan<
345
402
  shouldPair: action.authorization.kind === "pair",
346
403
  shouldDeny: action.authorization.kind === "deny",
347
404
  };
405
+ case "guest":
406
+ return {
407
+ kind: "guest",
408
+ guestMessage: action.guestMessage,
409
+ shouldDeny: action.authorization.kind === "deny",
410
+ };
348
411
  }
349
412
  }
350
413
 
@@ -387,6 +450,7 @@ export interface TelegramUpdateRuntimeDeps<
387
450
  callbackQueryId: string,
388
451
  text?: string,
389
452
  ) => Promise<void>;
453
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
390
454
  handleAuthorizedTelegramCallbackQuery: (
391
455
  query: TCallbackQuery,
392
456
  ctx: TContext,
@@ -404,6 +468,10 @@ export interface TelegramUpdateRuntimeDeps<
404
468
  message: TMessage,
405
469
  ctx: TContext,
406
470
  ) => unknown;
471
+ handleAuthorizedTelegramGuestMessage?: (
472
+ guestMessage: TelegramGuestMessage & { from: TelegramUser },
473
+ ctx: TContext,
474
+ ) => Promise<void>;
407
475
  }
408
476
 
409
477
  export interface TelegramUpdateRuntimeControllerDeps<
@@ -431,6 +499,7 @@ export interface TelegramUpdateRuntimeControllerDeps<
431
499
  callbackQueryId: string,
432
500
  text?: string,
433
501
  ) => Promise<void>;
502
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
434
503
  handleAuthorizedTelegramCallbackQuery: (
435
504
  query: TCallbackQuery,
436
505
  ctx: TContext,
@@ -448,6 +517,10 @@ export interface TelegramUpdateRuntimeControllerDeps<
448
517
  message: TMessage,
449
518
  ctx: TContext,
450
519
  ) => unknown;
520
+ handleAuthorizedTelegramGuestMessage?: (
521
+ guestMessage: TelegramGuestMessage & { from: TelegramUser },
522
+ ctx: TContext,
523
+ ) => Promise<void>;
451
524
  }
452
525
 
453
526
  export interface TelegramUpdateRuntimeController<
@@ -536,12 +609,15 @@ export function createTelegramPairedUpdateRuntime<
536
609
  updateStatus: deps.updateStatus,
537
610
  }).pairIfNeeded,
538
611
  answerCallbackQuery: deps.answerCallbackQuery,
612
+ answerGuestQuery: deps.answerGuestQuery,
539
613
  handleAuthorizedTelegramCallbackQuery:
540
614
  deps.handleAuthorizedTelegramCallbackQuery,
541
615
  sendTextReply: deps.sendTextReply,
542
616
  handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
543
617
  handleAuthorizedTelegramEditedMessage:
544
618
  deps.handleAuthorizedTelegramEditedMessage,
619
+ handleAuthorizedTelegramGuestMessage:
620
+ deps.handleAuthorizedTelegramGuestMessage,
545
621
  });
546
622
  }
547
623
 
@@ -582,12 +658,15 @@ export function createTelegramUpdateRuntime<
582
658
  handleAuthorizedTelegramReactionUpdate: handleAuthorizedReactionUpdate,
583
659
  pairTelegramUserIfNeeded: deps.pairTelegramUserIfNeeded,
584
660
  answerCallbackQuery: deps.answerCallbackQuery,
661
+ answerGuestQuery: deps.answerGuestQuery,
585
662
  handleAuthorizedTelegramCallbackQuery:
586
663
  deps.handleAuthorizedTelegramCallbackQuery,
587
664
  sendTextReply: deps.sendTextReply,
588
665
  handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
589
666
  handleAuthorizedTelegramEditedMessage:
590
667
  deps.handleAuthorizedTelegramEditedMessage,
668
+ handleAuthorizedTelegramGuestMessage:
669
+ deps.handleAuthorizedTelegramGuestMessage,
591
670
  }),
592
671
  };
593
672
  }
@@ -712,6 +791,22 @@ export async function executeTelegramUpdatePlan<
712
791
  await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
713
792
  return;
714
793
  }
794
+ if (plan.kind === "guest") {
795
+ if (plan.shouldDeny) {
796
+ await deps.answerGuestQuery(
797
+ plan.guestMessage.guest_query_id,
798
+ "Access denied.",
799
+ );
800
+ return;
801
+ }
802
+ if (deps.handleAuthorizedTelegramGuestMessage) {
803
+ await deps.handleAuthorizedTelegramGuestMessage(
804
+ plan.guestMessage,
805
+ deps.ctx,
806
+ );
807
+ }
808
+ return;
809
+ }
715
810
  const pairedNow = plan.shouldPair
716
811
  ? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
717
812
  : false;
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
5
8
  "description": "Telegram Runtime Adapter for π",
6
9
  "type": "module",
7
10
  "keywords": [
@@ -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": {