@llblab/pi-telegram 0.7.2 → 0.8.0

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Telegram attachment domain helpers
2
+ * Telegram outbound attachment helpers
3
3
  * Zones: telegram outbound, pi agent tool, filesystem
4
- * Owns telegram_attach registration, attachment queueing, and attachment delivery so Telegram file output stays in one domain module
4
+ * Owns telegram_attach registration, outbound attachment queueing, and delivery so Telegram file output stays in one domain module
5
5
  */
6
6
 
7
7
  import { stat } from "node:fs/promises";
@@ -16,7 +16,7 @@ const MAX_ATTACHMENTS_PER_TURN = 10;
16
16
 
17
17
  export const TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
18
18
 
19
- export function getTelegramAttachmentByteLimitFromEnv(
19
+ export function getTelegramOutboundAttachmentByteLimitFromEnv(
20
20
  env: NodeJS.ProcessEnv,
21
21
  names: string[],
22
22
  defaultValue = TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES,
@@ -31,17 +31,17 @@ export function getTelegramAttachmentByteLimitFromEnv(
31
31
  }
32
32
 
33
33
  export const TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES =
34
- getTelegramAttachmentByteLimitFromEnv(process.env, [
34
+ getTelegramOutboundAttachmentByteLimitFromEnv(process.env, [
35
35
  "PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES",
36
36
  "TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES",
37
37
  ]);
38
38
 
39
- export interface TelegramAttachmentToolResult {
39
+ export interface TelegramOutboundAttachmentToolResult {
40
40
  content: Array<{ type: "text"; text: string }>;
41
41
  details: { paths: string[] };
42
42
  }
43
43
 
44
- export interface TelegramAttachmentRuntimeEventRecorderPort {
44
+ export interface TelegramOutboundAttachmentRuntimeEventRecorderPort {
45
45
  recordRuntimeEvent?: (
46
46
  category: string,
47
47
  error: unknown,
@@ -49,28 +49,28 @@ export interface TelegramAttachmentRuntimeEventRecorderPort {
49
49
  ) => void;
50
50
  }
51
51
 
52
- export interface TelegramAttachmentToolRegistrationDeps extends TelegramAttachmentRuntimeEventRecorderPort {
52
+ export interface TelegramOutboundAttachmentToolRegistrationDeps extends TelegramOutboundAttachmentRuntimeEventRecorderPort {
53
53
  maxAttachmentsPerTurn?: number;
54
54
  maxAttachmentSizeBytes?: number;
55
- getActiveTurn: () => TelegramAttachmentQueueTargetView | undefined;
55
+ getActiveTurn: () => TelegramOutboundAttachmentQueueTargetView | undefined;
56
56
  statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
57
57
  }
58
58
 
59
- export interface TelegramQueuedAttachmentView {
59
+ export interface TelegramQueuedOutboundAttachmentView {
60
60
  path: string;
61
61
  fileName: string;
62
62
  }
63
63
 
64
- export interface TelegramAttachmentQueueTargetView {
65
- queuedAttachments: TelegramQueuedAttachmentView[];
64
+ export interface TelegramOutboundAttachmentQueueTargetView {
65
+ queuedAttachments: TelegramQueuedOutboundAttachmentView[];
66
66
  }
67
67
 
68
- export interface TelegramQueuedAttachmentTurnView extends TelegramAttachmentQueueTargetView {
68
+ export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
69
69
  chatId: number;
70
70
  replyToMessageId: number;
71
71
  }
72
72
 
73
- function isTelegramPhotoAttachmentPath(path: string): boolean {
73
+ function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
74
74
  const normalized = path.toLowerCase();
75
75
  return (
76
76
  normalized.endsWith(".jpg") ||
@@ -81,7 +81,7 @@ function isTelegramPhotoAttachmentPath(path: string): boolean {
81
81
  );
82
82
  }
83
83
 
84
- function formatTelegramAttachmentSizeLimitError(
84
+ function formatTelegramOutboundAttachmentSizeLimitError(
85
85
  size: number,
86
86
  maxSize: number,
87
87
  path?: string,
@@ -90,14 +90,14 @@ function formatTelegramAttachmentSizeLimitError(
90
90
  return path ? `${message}: ${path}` : message;
91
91
  }
92
92
 
93
- function formatTelegramAttachmentToolResultText(count: number): string {
93
+ function formatTelegramOutboundAttachmentToolResultText(count: number): string {
94
94
  // Pi's compact tool rows need an empty first line to visually separate header and result
95
95
  return ["", `Queued ${count} Telegram attachment(s).`].join("\n");
96
96
  }
97
97
 
98
- export function registerTelegramAttachmentTool(
98
+ export function registerTelegramOutboundAttachmentTool(
99
99
  pi: ExtensionAPI,
100
- deps: TelegramAttachmentToolRegistrationDeps,
100
+ deps: TelegramOutboundAttachmentToolRegistrationDeps,
101
101
  ): void {
102
102
  const maxAttachmentsPerTurn =
103
103
  deps.maxAttachmentsPerTurn ?? MAX_ATTACHMENTS_PER_TURN;
@@ -120,7 +120,7 @@ export function registerTelegramAttachmentTool(
120
120
  }),
121
121
  async execute(_toolCallId, params) {
122
122
  try {
123
- return await queueTelegramAttachments({
123
+ return await queueTelegramOutboundAttachments({
124
124
  activeTurn: deps.getActiveTurn(),
125
125
  paths: params.paths,
126
126
  maxAttachmentsPerTurn,
@@ -138,7 +138,7 @@ export function registerTelegramAttachmentTool(
138
138
  });
139
139
  }
140
140
 
141
- export interface TelegramQueuedAttachmentDeliveryDeps {
141
+ export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
142
142
  sendMultipart: (
143
143
  method: string,
144
144
  fields: Record<string, string>,
@@ -160,13 +160,13 @@ export interface TelegramQueuedAttachmentDeliveryDeps {
160
160
  maxAttachmentSizeBytes?: number;
161
161
  }
162
162
 
163
- export async function queueTelegramAttachments(options: {
164
- activeTurn: TelegramAttachmentQueueTargetView | undefined;
163
+ export async function queueTelegramOutboundAttachments(options: {
164
+ activeTurn: TelegramOutboundAttachmentQueueTargetView | undefined;
165
165
  paths: string[];
166
166
  maxAttachmentsPerTurn: number;
167
167
  maxAttachmentSizeBytes?: number;
168
168
  statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
169
- }): Promise<TelegramAttachmentToolResult> {
169
+ }): Promise<TelegramOutboundAttachmentToolResult> {
170
170
  if (!options.activeTurn) {
171
171
  throw new Error(
172
172
  "telegram_attach can only be used while replying to an active Telegram turn",
@@ -180,7 +180,7 @@ export async function queueTelegramAttachments(options: {
180
180
  `Attachment limit reached (${options.maxAttachmentsPerTurn})`,
181
181
  );
182
182
  }
183
- const pendingAttachments: TelegramQueuedAttachmentView[] = [];
183
+ const pendingAttachments: TelegramQueuedOutboundAttachmentView[] = [];
184
184
  for (const inputPath of options.paths) {
185
185
  const stats = await (options.statPath ?? stat)(inputPath);
186
186
  if (!stats.isFile()) {
@@ -192,7 +192,7 @@ export async function queueTelegramAttachments(options: {
192
192
  stats.size > options.maxAttachmentSizeBytes
193
193
  ) {
194
194
  throw new Error(
195
- formatTelegramAttachmentSizeLimitError(
195
+ formatTelegramOutboundAttachmentSizeLimitError(
196
196
  stats.size,
197
197
  options.maxAttachmentSizeBytes,
198
198
  inputPath,
@@ -210,20 +210,20 @@ export async function queueTelegramAttachments(options: {
210
210
  content: [
211
211
  {
212
212
  type: "text",
213
- text: formatTelegramAttachmentToolResultText(added.length),
213
+ text: formatTelegramOutboundAttachmentToolResultText(added.length),
214
214
  },
215
215
  ],
216
216
  details: { paths: added },
217
217
  };
218
218
  }
219
219
 
220
- export function createTelegramQueuedAttachmentSender(
221
- deps: TelegramQueuedAttachmentDeliveryDeps,
220
+ export function createTelegramQueuedOutboundAttachmentSender(
221
+ deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
222
222
  ) {
223
223
  return async function sendQueuedAttachments(
224
- turn: TelegramQueuedAttachmentTurnView,
224
+ turn: TelegramQueuedOutboundAttachmentTurnView,
225
225
  ): Promise<void> {
226
- await sendQueuedTelegramAttachments(turn, {
226
+ await sendQueuedTelegramOutboundAttachments(turn, {
227
227
  ...deps,
228
228
  maxAttachmentSizeBytes:
229
229
  deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
@@ -231,9 +231,9 @@ export function createTelegramQueuedAttachmentSender(
231
231
  };
232
232
  }
233
233
 
234
- export async function sendQueuedTelegramAttachments(
235
- turn: TelegramQueuedAttachmentTurnView,
236
- deps: TelegramQueuedAttachmentDeliveryDeps,
234
+ export async function sendQueuedTelegramOutboundAttachments(
235
+ turn: TelegramQueuedOutboundAttachmentTurnView,
236
+ deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
237
237
  ): Promise<void> {
238
238
  for (const attachment of turn.queuedAttachments) {
239
239
  try {
@@ -241,14 +241,14 @@ export async function sendQueuedTelegramAttachments(
241
241
  const stats = await (deps.statPath ?? stat)(attachment.path);
242
242
  if (stats.size > deps.maxAttachmentSizeBytes) {
243
243
  throw new Error(
244
- formatTelegramAttachmentSizeLimitError(
244
+ formatTelegramOutboundAttachmentSizeLimitError(
245
245
  stats.size,
246
246
  deps.maxAttachmentSizeBytes,
247
247
  ),
248
248
  );
249
249
  }
250
250
  }
251
- const isPhoto = isTelegramPhotoAttachmentPath(attachment.path);
251
+ const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
252
252
  const method = isPhoto ? "sendPhoto" : "sendDocument";
253
253
  const fieldName = isPhoto ? "photo" : "document";
254
254
  const replyParameters = buildTelegramMultipartReplyParameters(
@@ -97,6 +97,55 @@ export interface TelegramVoiceReplySenderDeps {
97
97
  ) => void;
98
98
  }
99
99
 
100
+ export interface TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup = unknown> {
101
+ execCommand: TelegramVoiceReplySenderDeps["execCommand"];
102
+ getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
103
+ sendTextReply: (
104
+ chatId: number,
105
+ replyToMessageId: number | undefined,
106
+ text: string,
107
+ options?: { parseMode?: "HTML" },
108
+ ) => Promise<number | undefined>;
109
+ sendMarkdownReply: (
110
+ chatId: number,
111
+ replyToMessageId: number | undefined,
112
+ markdown: string,
113
+ options?: { replyMarkup?: TReplyMarkup },
114
+ ) => Promise<number | undefined>;
115
+ cwd?: string;
116
+ recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
117
+ }
118
+
119
+ export interface TelegramInlineKeyboardLike {
120
+ inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
121
+ }
122
+
123
+ export interface TelegramOutboundTextTransformOptions<TReplyMarkup = unknown> {
124
+ handlers?: TelegramOutboundHandlerConfig[];
125
+ cwd?: string;
126
+ execCommand: TelegramVoiceReplySenderDeps["execCommand"];
127
+ recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
128
+ replyMarkup?: TReplyMarkup;
129
+ }
130
+
131
+ export interface TelegramOutboundTextTransformResult<TReplyMarkup = unknown> {
132
+ text: string;
133
+ replyMarkup?: TReplyMarkup;
134
+ }
135
+
136
+ export interface TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup = unknown> {
137
+ execCommand: TelegramVoiceReplySenderDeps["execCommand"];
138
+ getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
139
+ finalizeMarkdownPreview: (
140
+ chatId: number,
141
+ markdown: string,
142
+ replyToMessageId: number,
143
+ options?: { replyMarkup?: TReplyMarkup },
144
+ ) => Promise<boolean>;
145
+ cwd?: string;
146
+ recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
147
+ }
148
+
100
149
  interface TelegramTopLevelHtmlComment {
101
150
  raw: string;
102
151
  content: string;
@@ -665,6 +714,202 @@ export async function generateTelegramVoiceReplyFile(
665
714
  });
666
715
  }
667
716
 
717
+ function getOutboundTextTemplateValues(text: string): Record<string, string> {
718
+ return { text, type: "text" };
719
+ }
720
+
721
+ async function transformTelegramOutboundTextWithHandler(
722
+ text: string,
723
+ options: {
724
+ handler: TelegramOutboundHandlerConfig;
725
+ cwd: string;
726
+ execCommand: TelegramVoiceReplySenderDeps["execCommand"];
727
+ },
728
+ ): Promise<string> {
729
+ const values = getOutboundTextTemplateValues(text);
730
+ const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
731
+ if (steps.length > 0) {
732
+ const startedAt = Date.now();
733
+ let stdout = text;
734
+ for (const [index, step] of steps.entries()) {
735
+ try {
736
+ const result = await runVoiceReplyCommand(
737
+ `Outbound text template step ${index + 1}`,
738
+ step,
739
+ values,
740
+ {
741
+ cwd: options.cwd,
742
+ timeout: getVoiceReplyCompositionStepTimeout(
743
+ getVoiceReplyTimeout(options.handler),
744
+ step,
745
+ startedAt,
746
+ ),
747
+ execCommand: options.execCommand,
748
+ stdin: stdout,
749
+ },
750
+ );
751
+ stdout = result.stdout;
752
+ } catch (error) {
753
+ if (typeof step === "object" && step.critical) throw error;
754
+ stdout = "";
755
+ }
756
+ if (!stdout) stdout = text;
757
+ }
758
+ return stdout.trim() || text;
759
+ }
760
+ const result = await runVoiceReplyCommand(
761
+ "Outbound text template",
762
+ options.handler,
763
+ values,
764
+ {
765
+ cwd: options.cwd,
766
+ timeout: getVoiceReplyTimeout(options.handler),
767
+ execCommand: options.execCommand,
768
+ stdin: text,
769
+ },
770
+ );
771
+ return result.stdout.trim() || text;
772
+ }
773
+
774
+ export async function transformTelegramOutboundText(
775
+ text: string,
776
+ options: {
777
+ handlers?: TelegramOutboundHandlerConfig[];
778
+ cwd?: string;
779
+ execCommand: TelegramVoiceReplySenderDeps["execCommand"];
780
+ recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
781
+ },
782
+ ): Promise<string> {
783
+ let transformed = text;
784
+ for (const handler of findTelegramOutboundHandlers(options.handlers, "text")) {
785
+ try {
786
+ transformed = await transformTelegramOutboundTextWithHandler(
787
+ transformed,
788
+ {
789
+ handler,
790
+ cwd: options.cwd ?? process.cwd(),
791
+ execCommand: options.execCommand,
792
+ },
793
+ );
794
+ } catch (error) {
795
+ options.recordRuntimeEvent?.("outbound-text-handler", error, {
796
+ handler: outboundHandlerMatchesType(handler, "text") ? "text" : "unknown",
797
+ });
798
+ }
799
+ }
800
+ return transformed;
801
+ }
802
+
803
+ function isTelegramInlineKeyboardLike(
804
+ replyMarkup: unknown,
805
+ ): replyMarkup is TelegramInlineKeyboardLike {
806
+ if (!replyMarkup || typeof replyMarkup !== "object") return false;
807
+ const keyboard = (replyMarkup as { inline_keyboard?: unknown }).inline_keyboard;
808
+ return Array.isArray(keyboard);
809
+ }
810
+
811
+ async function transformTelegramOutboundReplyMarkup<TReplyMarkup>(
812
+ replyMarkup: TReplyMarkup | undefined,
813
+ options: Omit<TelegramOutboundTextTransformOptions, "replyMarkup">,
814
+ ): Promise<TReplyMarkup | undefined> {
815
+ if (!isTelegramInlineKeyboardLike(replyMarkup)) return replyMarkup;
816
+ const translatedRows = [];
817
+ for (const row of replyMarkup.inline_keyboard) {
818
+ const translatedRow = [];
819
+ for (const button of row) {
820
+ const text = await transformTelegramOutboundText(button.text, options);
821
+ translatedRow.push({ ...button, text });
822
+ }
823
+ translatedRows.push(translatedRow);
824
+ }
825
+ return { ...replyMarkup, inline_keyboard: translatedRows } as TReplyMarkup;
826
+ }
827
+
828
+ export async function transformTelegramOutboundTextReply<TReplyMarkup = unknown>(
829
+ text: string,
830
+ options: TelegramOutboundTextTransformOptions<TReplyMarkup>,
831
+ ): Promise<TelegramOutboundTextTransformResult<TReplyMarkup>> {
832
+ const transformOptions = {
833
+ handlers: options.handlers,
834
+ cwd: options.cwd,
835
+ execCommand: options.execCommand,
836
+ recordRuntimeEvent: options.recordRuntimeEvent,
837
+ };
838
+ const transformedText = await transformTelegramOutboundText(
839
+ text,
840
+ transformOptions,
841
+ );
842
+ const replyMarkup = await transformTelegramOutboundReplyMarkup(
843
+ options.replyMarkup,
844
+ transformOptions,
845
+ );
846
+ return { text: transformedText, ...(replyMarkup ? { replyMarkup } : {}) };
847
+ }
848
+
849
+ export function createTelegramOutboundTextReplyRuntime<TReplyMarkup = unknown>(
850
+ deps: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>,
851
+ ): Pick<
852
+ TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>,
853
+ "sendTextReply" | "sendMarkdownReply"
854
+ > {
855
+ return {
856
+ sendTextReply: async (chatId, replyToMessageId, text, options) => {
857
+ const transformed = await transformTelegramOutboundText(text, {
858
+ handlers: deps.getHandlers?.(),
859
+ cwd: deps.cwd,
860
+ execCommand: deps.execCommand,
861
+ recordRuntimeEvent: deps.recordRuntimeEvent,
862
+ });
863
+ return deps.sendTextReply(chatId, replyToMessageId, transformed, options);
864
+ },
865
+ sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
866
+ const transformed = await transformTelegramOutboundTextReply(markdown, {
867
+ handlers: deps.getHandlers?.(),
868
+ cwd: deps.cwd,
869
+ execCommand: deps.execCommand,
870
+ recordRuntimeEvent: deps.recordRuntimeEvent,
871
+ replyMarkup: options?.replyMarkup,
872
+ });
873
+ return deps.sendMarkdownReply(chatId, replyToMessageId, transformed.text, {
874
+ ...options,
875
+ ...(transformed.replyMarkup
876
+ ? { replyMarkup: transformed.replyMarkup }
877
+ : {}),
878
+ });
879
+ },
880
+ };
881
+ }
882
+
883
+ export function createTelegramOutboundTextPreviewRuntime<TReplyMarkup = unknown>(
884
+ deps: TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
885
+ ): Pick<
886
+ TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
887
+ "finalizeMarkdownPreview"
888
+ > {
889
+ return {
890
+ finalizeMarkdownPreview: async (chatId, markdown, replyToMessageId, options) => {
891
+ const transformed = await transformTelegramOutboundTextReply(markdown, {
892
+ handlers: deps.getHandlers?.(),
893
+ cwd: deps.cwd,
894
+ execCommand: deps.execCommand,
895
+ recordRuntimeEvent: deps.recordRuntimeEvent,
896
+ replyMarkup: options?.replyMarkup,
897
+ });
898
+ return deps.finalizeMarkdownPreview(
899
+ chatId,
900
+ transformed.text,
901
+ replyToMessageId,
902
+ {
903
+ ...options,
904
+ ...(transformed.replyMarkup
905
+ ? { replyMarkup: transformed.replyMarkup }
906
+ : {}),
907
+ },
908
+ );
909
+ },
910
+ };
911
+ }
912
+
668
913
  export interface TelegramOutboundReplyPlan<TReplyMarkup = unknown> {
669
914
  markdown: string;
670
915
  replyMarkup?: TReplyMarkup;
package/lib/prompts.ts CHANGED
@@ -14,7 +14,7 @@ Telegram bridge extension is active.
14
14
  Inbound context:
15
15
  - \`[telegram]\` marks Telegram-originated messages.
16
16
  - \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
17
- - \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains attachment-handler stdout such as transcriptions or extracted text for those attachments.
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
 
20
20
  Telegram-visible output:
package/lib/routing.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import * as OutboundHandlers from "./outbound-handlers.ts";
8
8
  import * as Commands from "./commands.ts";
9
9
  import type { TelegramConfigStore } from "./config.ts";
10
- import type { TelegramAttachmentHandlerRuntime } from "./attachment-handlers.ts";
10
+ import type { TelegramInboundHandlerRuntime } from "./inbound-handlers.ts";
11
11
  import * as Media from "./media.ts";
12
12
  import * as Menu from "./menu.ts";
13
13
  import * as Model from "./model.ts";
@@ -60,7 +60,7 @@ export interface TelegramInboundRouteRuntimeDeps<
60
60
  ctx: TContext,
61
61
  ) => Promise<boolean>;
62
62
  buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
63
- attachmentHandlerRuntime: TelegramAttachmentHandlerRuntime<TContext>;
63
+ inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
64
64
  updateStatus: (ctx: TContext, error?: string) => void;
65
65
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
66
66
  answerCallbackQuery: (
@@ -205,7 +205,7 @@ export function createTelegramInboundRouteRuntime<
205
205
  >({
206
206
  allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
207
207
  downloadFile: deps.downloadFile,
208
- processAttachments: deps.attachmentHandlerRuntime.process,
208
+ processAttachments: deps.inboundHandlerRuntime.process,
209
209
  });
210
210
  const enqueueContinueTurn = async (
211
211
  message: TMessage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for π",
6
6
  "type": "module",
@@ -1,50 +0,0 @@
1
- # Attachment Handlers
2
-
3
- `pi-telegram` can run ordered inbound attachment handlers after downloading files and before the Telegram turn enters the π queue.
4
-
5
- This document is the local adaptation of the portable [Command Template Standard](./command-templates.md).
6
-
7
- ## Config Shape
8
-
9
- `telegram.json` may define `attachmentHandlers`:
10
-
11
- ```json
12
- {
13
- "attachmentHandlers": [
14
- {
15
- "type": "voice",
16
- "template": "/path/to/stt1 --file {file} --lang {lang=ru}"
17
- },
18
- {
19
- "mime": "audio/*",
20
- "template": "/path/to/stt2 --file {file} --lang {lang=ru}"
21
- }
22
- ]
23
- }
24
- ```
25
-
26
- Handlers match by `type`, `mime`, or `match`. Wildcards such as `audio/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically. Legacy configs may still use `pipe` as a local alias.
27
-
28
- ## Template Placeholders
29
-
30
- Attachment handlers support these built-in placeholders:
31
-
32
- | Placeholder | Value |
33
- | ----------- | ---------------------------------------------------------------- |
34
- | `{file}` | Full local path to the downloaded file |
35
- | `{mime}` | MIME type if known |
36
- | `{type}` | Attachment kind such as `voice`, `audio`, `document`, or `photo` |
37
-
38
- `defaults` may provide additional placeholder values such as `{lang}` or `{model}`. `args` is only a string-array declaration of supported placeholders; defaults belong in `defaults` or inline placeholders such as `{lang=ru}`. Examples prefer explicit flag-style CLIs for readability, but positional forms such as `/path/to/stt {file} {lang=ru} {model=voxtral-mini-latest}` are equally valid when the target script supports them.
39
-
40
- If a top-level one-step handler template has no `{file}` placeholder, the downloaded file path is appended as the last command arg as a one-step handler convenience. Composition steps are plain command templates and do not receive implicit file-path args; include `{file}` explicitly where needed.
41
-
42
- ## Ordered Fallbacks
43
-
44
- A handler list is ordered. For each attachment, matching handlers run in list order and stop after the first successful handler. A composed handler counts as one handler for fallback purposes: if any step fails, the next matching handler is tried.
45
-
46
- If a matching handler fails with a non-zero exit code, the runtime records diagnostics and tries the next matching handler. If every matching handler fails, the attachment remains visible in the prompt as a normal local file reference.
47
-
48
- ## Prompt Output
49
-
50
- Local attachments stay in the prompt under `[attachments] <directory>` with relative file entries. Successful handler stdout is added under `[outputs]`. For composed handlers, each step receives the previous step's stdout on stdin by default, and stdout from the last successful step is used as the handler output. Empty output and failed handler output are omitted from the prompt text.