@llblab/pi-telegram 0.9.6 → 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/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,17 @@
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
+
3
15
  ## 0.9.6: Runtime Adapter Positioning
4
16
 
5
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.
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,25 @@ 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
+ }
165
+
154
166
  export interface TelegramUpdate {
155
167
  update_id: number;
156
168
  message?: TelegramMessage;
157
169
  edited_message?: TelegramMessage;
158
170
  callback_query?: TelegramCallbackQuery;
159
171
  message_reaction?: TelegramMessageReactionUpdated;
172
+ guest_message?: TelegramGuestMessage;
160
173
  deleted_business_messages?: { message_ids?: unknown };
161
174
  }
162
175
 
@@ -184,6 +197,15 @@ export type TelegramEditMessageTextBody = Record<string, unknown> & {
184
197
  parse_mode?: "HTML";
185
198
  };
186
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
+
187
209
  interface TelegramApiResponse<T> {
188
210
  ok: boolean;
189
211
  result?: T;
@@ -233,6 +255,7 @@ export interface TelegramApiClient {
233
255
  callbackQueryId: string,
234
256
  text?: string,
235
257
  ) => Promise<void>;
258
+ answerGuestQuery?: (guestQueryId: string, text?: string) => Promise<void>;
236
259
  }
237
260
 
238
261
  export interface TelegramBridgeApiRuntimeDeps {
@@ -275,7 +298,12 @@ export interface TelegramBridgeApiRuntime {
275
298
  sendMessageDraft: (
276
299
  chatId: number,
277
300
  draftId: number,
278
- text: string,
301
+ text?: string,
302
+ options?: {
303
+ parse_mode?: string;
304
+ entities?: unknown[];
305
+ message_thread_id?: number;
306
+ },
279
307
  ) => Promise<boolean>;
280
308
  sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
281
309
  editMessageText: (
@@ -285,6 +313,7 @@ export interface TelegramBridgeApiRuntime {
285
313
  callbackQueryId: string,
286
314
  text?: string,
287
315
  ) => Promise<void>;
316
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
288
317
  prepareTempDir: () => Promise<number>;
289
318
  }
290
319
 
@@ -534,9 +563,7 @@ export async function fetchTelegramBotIdentity(
534
563
  botToken: string,
535
564
  fetchImpl: typeof fetch = fetch,
536
565
  ): Promise<TelegramBotIdentityResponse> {
537
- const response = await fetchImpl(
538
- `${TELEGRAM_API_BASE}/bot${botToken}/getMe`,
539
- );
566
+ const response = await fetchImpl(`${TELEGRAM_API_BASE}/bot${botToken}/getMe`);
540
567
  return response.json() as Promise<TelegramBotIdentityResponse>;
541
568
  }
542
569
 
@@ -559,14 +586,11 @@ export async function callTelegramMultipart<TResponse>(
559
586
  form.set(key, value);
560
587
  }
561
588
  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
- );
589
+ return fetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
590
+ method: "POST",
591
+ body: form,
592
+ signal: options?.signal,
593
+ });
570
594
  },
571
595
  options,
572
596
  );
@@ -732,13 +756,18 @@ export function createTelegramBridgeApiRuntime(
732
756
  }),
733
757
  "typing",
734
758
  ),
735
- sendMessageDraft: (chatId, draftId, text) => {
736
- if (text.length === 0) return Promise.resolve(false);
737
- return callRecorded<boolean>("sendMessageDraft", {
759
+ sendMessageDraft: (chatId, draftId, text, options) => {
760
+ const body: Record<string, unknown> = {
738
761
  chat_id: chatId,
739
762
  draft_id: draftId,
740
- text,
741
- });
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);
742
771
  },
743
772
  sendMessage: (body) =>
744
773
  callRecorded<TelegramSentMessage>("sendMessage", body),
@@ -755,6 +784,18 @@ export function createTelegramBridgeApiRuntime(
755
784
  answerCallbackQuery: (callbackQueryId, text) => {
756
785
  return deps.client.answerCallbackQuery(callbackQueryId, text);
757
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
+ },
758
799
  prepareTempDir: () =>
759
800
  prepareTelegramTempDir(deps.tempDir, deps.tempFileMaxAgeMs),
760
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,7 +1,10 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
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": {