@llblab/pi-telegram 0.9.7 → 0.9.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.9: Guest Mode HTML Rendering
4
+
5
+ - `[Guest Mode]` Guest replies now render through the same `renderTelegramMessage` pipeline as direct messages: Markdown → HTML → `answerGuestQuery` with `parse_mode: "HTML"`. Bold, italic, code, links, lists, and tables render identically in guest and DM replies.
6
+ - `[Replies]` Added `createGuestMarkdownReplySender` in the replies domain — guest rendering stays encapsulated within `replies.ts` and `index.ts` no longer imports from `rendering.ts` directly.
7
+ - `[API]` Simplified `answerGuestQuery` title to a fixed `"Response"` string (the `InlineQueryResultArticle` title is hidden in guest mode and was previously generated by stripping HTML/Markdown from the message text).
8
+ - `[API]` Removed `replyMarkup` parameter from `answerGuestQuery` — inline keyboards are not supported in guest mode because `callback_query` from inline results carries `inline_message_id` instead of `chat_id`/`message_id`, which the existing callback routing cannot handle.
9
+
10
+ ## 0.9.8: Guest Mode Context
11
+
12
+ - `[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.
13
+ - `[Guest Mode]` Added |from:user to the [reply] block so the agent knows the original author of a replied-to message in guest mode.
14
+ - `[Guest Mode]` Formatted guest prompt text identically to regular DMs through buildTelegramTurnPrompt, including [attachments] and [outputs] sections with file downloads and inbound handler processing.
15
+ - `[Prompts]` Added compact agent guidance explaining guest-mode prefix suffixes (|from:, |guest:) and reply-from context.
16
+
3
17
  ## 0.9.7: Bot API 10.0 Alignment
4
18
 
5
19
  - `[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.
package/index.ts CHANGED
@@ -157,6 +157,11 @@ export default function (pi: Pi.ExtensionAPI) {
157
157
 
158
158
  // --- Message Delivery & Preview ---
159
159
 
160
+ const sendGuestReply = Replies.createGuestMarkdownReplySender({
161
+ renderTelegramMessage: Replies.renderTelegramMessage,
162
+ answerGuestQuery,
163
+ });
164
+
160
165
  const promptDispatchRuntime =
161
166
  Runtime.createTelegramPromptDispatchRuntime<Pi.ExtensionContext>({
162
167
  lifecycle,
@@ -497,6 +502,7 @@ export default function (pi: Pi.ExtensionAPI) {
497
502
  sendTextReply,
498
503
  sendQueuedAttachments: queuedAttachmentSender,
499
504
  answerGuestQuery,
505
+ sendGuestReply,
500
506
  planOutboundReply: outboundReplyPlanner,
501
507
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
502
508
  isCurrentOwner: lockOwnershipGuard.ownsContext,
package/lib/api.ts CHANGED
@@ -161,6 +161,7 @@ export interface TelegramGuestMessage {
161
161
  guest_query_id: string;
162
162
  guest_bot_caller_user?: TelegramUser;
163
163
  guest_bot_caller_chat?: TelegramChat;
164
+ reply_to_message?: TelegramMessage;
164
165
  }
165
166
 
166
167
  export interface TelegramUpdate {
@@ -255,7 +256,11 @@ export interface TelegramApiClient {
255
256
  callbackQueryId: string,
256
257
  text?: string,
257
258
  ) => Promise<void>;
258
- answerGuestQuery?: (guestQueryId: string, text?: string) => Promise<void>;
259
+ answerGuestQuery?: (
260
+ guestQueryId: string,
261
+ text?: string,
262
+ options?: { parseMode?: string },
263
+ ) => Promise<void>;
259
264
  }
260
265
 
261
266
  export interface TelegramBridgeApiRuntimeDeps {
@@ -313,7 +318,11 @@ export interface TelegramBridgeApiRuntime {
313
318
  callbackQueryId: string,
314
319
  text?: string,
315
320
  ) => Promise<void>;
316
- answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
321
+ answerGuestQuery: (
322
+ guestQueryId: string,
323
+ text?: string,
324
+ options?: { parseMode?: string },
325
+ ) => Promise<void>;
317
326
  prepareTempDir: () => Promise<number>;
318
327
  }
319
328
 
@@ -784,14 +793,24 @@ export function createTelegramBridgeApiRuntime(
784
793
  answerCallbackQuery: (callbackQueryId, text) => {
785
794
  return deps.client.answerCallbackQuery(callbackQueryId, text);
786
795
  },
787
- answerGuestQuery: (guestQueryId, text) => {
796
+ answerGuestQuery: (
797
+ guestQueryId: string,
798
+ text: string | undefined,
799
+ options: { parseMode?: string } | undefined,
800
+ ) => {
788
801
  const body: Record<string, unknown> = { guest_query_id: guestQueryId };
789
802
  if (text !== undefined) {
803
+ const inputContent: Record<string, unknown> = {
804
+ message_text: text,
805
+ };
806
+ if (options?.parseMode) {
807
+ inputContent.parse_mode = options.parseMode;
808
+ }
790
809
  body.result = {
791
810
  type: "article",
792
811
  id: "1",
793
- title: text.length > 64 ? text.slice(0, 61) + "..." : text,
794
- input_message_content: { message_text: text },
812
+ title: "Response",
813
+ input_message_content: inputContent,
795
814
  };
796
815
  }
797
816
  return callRecorded<void>("answerGuestQuery", body);
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
@@ -788,7 +788,8 @@ export interface TelegramAgentEndRuntimeDeps<
788
788
  text: string,
789
789
  ) => Promise<unknown>;
790
790
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
791
- answerGuestQuery?: (guestQueryId: string, text?: string) => Promise<void>;
791
+ answerGuestQuery?: (guestQueryId: string, text?: string, options?: { parseMode?: string }) => Promise<void>;
792
+ sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
792
793
  planOutboundReply?: (
793
794
  markdown: string,
794
795
  ) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
@@ -837,6 +838,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
837
838
  sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
838
839
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
839
840
  answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
841
+ sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
840
842
  planOutboundReply?: TelegramAgentEndRuntimeDeps<
841
843
  TTurn,
842
844
  TReplyMarkup
@@ -958,6 +960,7 @@ export function createTelegramAgentEndHook<
958
960
  sendTextReply: deps.sendTextReply,
959
961
  sendQueuedAttachments: deps.sendQueuedAttachments,
960
962
  answerGuestQuery: deps.answerGuestQuery,
963
+ sendGuestReply: deps.sendGuestReply,
961
964
  planOutboundReply: deps.planOutboundReply,
962
965
  sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
963
966
  getDefaultChatId: deps.getDefaultChatId,
@@ -1027,7 +1030,11 @@ export async function handleTelegramAgentEndRuntime<
1027
1030
  return;
1028
1031
  }
1029
1032
  if (finalText) {
1030
- await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1033
+ if (deps.sendGuestReply) {
1034
+ await deps.sendGuestReply(turn.guestQueryId, finalText);
1035
+ } else {
1036
+ await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1037
+ }
1031
1038
  }
1032
1039
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1033
1040
  return;
package/lib/replies.ts CHANGED
@@ -11,6 +11,12 @@ import {
11
11
  type TelegramRenderMode,
12
12
  } from "./rendering.ts";
13
13
 
14
+ export {
15
+ renderTelegramMessage,
16
+ type TelegramRenderedChunk,
17
+ type TelegramRenderMode,
18
+ };
19
+
14
20
  // --- Reply Dedup ---
15
21
 
16
22
  /** Non-persistent reply deduplication for a single agent turn.
@@ -420,3 +426,26 @@ export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
420
426
  return inner(chatId, effectiveReplyTo, markdown, options);
421
427
  };
422
428
  }
429
+
430
+ /**
431
+ * Guest reply sender: renders Markdown → HTML, sends via answerGuestQuery.
432
+ * Keeps guest rendering inside the replies domain so the orchestration layer
433
+ * (index.ts) does not import from rendering.ts directly.
434
+ */
435
+ export function createGuestMarkdownReplySender(deps: {
436
+ renderTelegramMessage: (
437
+ text: string,
438
+ options?: { mode?: TelegramRenderMode },
439
+ ) => TelegramRenderedChunk[];
440
+ answerGuestQuery: (
441
+ guestQueryId: string,
442
+ text?: string,
443
+ options?: { parseMode?: string },
444
+ ) => Promise<void>;
445
+ }) {
446
+ return async (guestQueryId: string, markdown: string) => {
447
+ const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
448
+ const html = chunks.length > 0 ? chunks[0].text : markdown;
449
+ await deps.answerGuestQuery(guestQueryId, html, { parseMode: "HTML" });
450
+ };
451
+ }
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";
@@ -371,7 +372,71 @@ export function createTelegramInboundRouteRuntime<
371
372
  ctx: TContext,
372
373
  ): Promise<void> => {
373
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
+ });
374
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
+ }
375
440
  const guestTurn: Queue.PendingTelegramTurn = {
376
441
  kind: "prompt",
377
442
  chatId: 0,
@@ -382,9 +447,15 @@ export function createTelegramInboundRouteRuntime<
382
447
  queueLane: "default",
383
448
  laneOrder: order,
384
449
  queuedAttachments: [],
385
- content: [{ type: "text", text }],
386
- historyText: text,
387
- statusSummary: Turns.truncateTelegramQueueSummary(text),
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
+ ),
388
459
  };
389
460
  const items = deps.telegramQueueStore.getQueuedItems();
390
461
  deps.telegramQueueStore.setQueuedItems(
package/lib/updates.ts CHANGED
@@ -136,6 +136,7 @@ export interface TelegramGuestMessage {
136
136
  from?: TelegramUser;
137
137
  message_id?: number;
138
138
  text?: string;
139
+ reply_to_message?: TelegramUpdateMessage;
139
140
  }
140
141
 
141
142
  export interface TelegramUpdateRouting {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"