@llblab/pi-telegram 0.6.3 → 0.7.1

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/lib/model.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram model control domain helpers
3
+ * Zones: pi agent model control, telegram controls, queue continuation
3
4
  * Owns model identity, thinking levels, scoped resolution, current-model state, and in-flight model switching
4
5
  */
5
6
 
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram outbound handler helpers
3
+ * Zones: telegram outbound, assistant markup, command templates, callback routing
3
4
  * Owns assistant-authored outbound markup extraction, configured artifact generation, callback actions, and Telegram outbound delivery
4
5
  */
5
6
 
@@ -8,6 +9,7 @@ import { mkdir } from "node:fs/promises";
8
9
  import { homedir } from "node:os";
9
10
  import { basename, join, resolve } from "node:path";
10
11
 
12
+ import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
11
13
  import type { PendingTelegramTurn } from "./queue.ts";
12
14
  import { buildTelegramMultipartReplyParameters } from "./replies.ts";
13
15
  import { truncateTelegramQueueSummary } from "./turns.ts";
@@ -52,6 +54,7 @@ export interface TelegramVoiceExecOptions {
52
54
  timeout?: number;
53
55
  signal?: AbortSignal;
54
56
  stdin?: string;
57
+ retry?: number;
55
58
  }
56
59
 
57
60
  export interface TelegramVoiceExecResult {
@@ -473,6 +476,9 @@ async function runVoiceReplyCommand(
473
476
  {
474
477
  cwd: options.cwd,
475
478
  timeout: options.timeout,
479
+ ...(typeof config === "object" && config.retry !== undefined
480
+ ? { retry: config.retry }
481
+ : {}),
476
482
  ...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
477
483
  },
478
484
  );
@@ -597,22 +603,27 @@ async function generateTelegramVoiceReplyFileWithHandler(
597
603
  const startedAt = Date.now();
598
604
  let stdout = "";
599
605
  for (const [index, step] of steps.entries()) {
600
- const result = await runVoiceReplyCommand(
601
- `Outbound voice template step ${index + 1}`,
602
- step,
603
- values,
604
- {
605
- cwd: options.cwd,
606
- timeout: getVoiceReplyCompositionStepTimeout(
607
- options.timeout,
608
- step,
609
- startedAt,
610
- ),
611
- execCommand: options.execCommand,
612
- ...(index === 0 ? {} : { stdin: stdout }),
613
- },
614
- );
615
- stdout = result.stdout;
606
+ try {
607
+ const result = await runVoiceReplyCommand(
608
+ `Outbound voice template step ${index + 1}`,
609
+ step,
610
+ values,
611
+ {
612
+ cwd: options.cwd,
613
+ timeout: getVoiceReplyCompositionStepTimeout(
614
+ options.timeout,
615
+ step,
616
+ startedAt,
617
+ ),
618
+ execCommand: options.execCommand,
619
+ ...(index === 0 ? {} : { stdin: stdout }),
620
+ },
621
+ );
622
+ stdout = result.stdout;
623
+ } catch (error) {
624
+ if (typeof step === "object" && step.critical) throw error;
625
+ stdout = "";
626
+ }
616
627
  }
617
628
  return getVoiceReplyOutputPath(options.handler, values, stdout);
618
629
  }
@@ -722,9 +733,7 @@ export interface TelegramOutboundButtonStoredAction extends TelegramOutboundButt
722
733
  createdAt: number;
723
734
  }
724
735
 
725
- export interface TelegramOutboundButtonMarkup {
726
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
727
- }
736
+ export type TelegramOutboundButtonMarkup = TelegramInlineKeyboardMarkup;
728
737
 
729
738
  export interface TelegramButtonReplyPlan {
730
739
  markdown: string;
package/lib/pi.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * pi SDK adapter boundary
3
+ * Zones: pi agent sdk boundary, shared adapters
3
4
  * Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer
4
5
  */
5
6
 
@@ -12,6 +13,7 @@ import {
12
13
  type ExtensionContext,
13
14
  type SessionShutdownEvent,
14
15
  type SessionStartEvent,
16
+ type SlashCommandInfo,
15
17
  SettingsManager,
16
18
  } from "@mariozechner/pi-coding-agent";
17
19
 
@@ -24,6 +26,7 @@ export type {
24
26
  ExtensionContext,
25
27
  SessionShutdownEvent,
26
28
  SessionStartEvent,
29
+ SlashCommandInfo,
27
30
  };
28
31
 
29
32
  export interface PiSettingsManager {
@@ -31,9 +34,12 @@ export interface PiSettingsManager {
31
34
  getEnabledModels: () => string[] | undefined;
32
35
  }
33
36
 
37
+ export type PiSlashCommandInfo = SlashCommandInfo;
38
+
34
39
  export interface PiExtensionApiRuntimePorts {
35
40
  sendUserMessage: ExtensionAPI["sendUserMessage"];
36
41
  exec: ExtensionAPI["exec"];
42
+ getCommands: ExtensionAPI["getCommands"];
37
43
  getThinkingLevel: ExtensionAPI["getThinkingLevel"];
38
44
  setThinkingLevel: ExtensionAPI["setThinkingLevel"];
39
45
  setModel: ExtensionAPI["setModel"];
@@ -44,6 +50,7 @@ export function createExtensionApiRuntimePorts(
44
50
  ExtensionAPI,
45
51
  | "sendUserMessage"
46
52
  | "exec"
53
+ | "getCommands"
47
54
  | "getThinkingLevel"
48
55
  | "setThinkingLevel"
49
56
  | "setModel"
@@ -52,6 +59,7 @@ export function createExtensionApiRuntimePorts(
52
59
  return {
53
60
  sendUserMessage: (content) => api.sendUserMessage(content),
54
61
  exec: (command, args, options) => api.exec(command, args, options),
62
+ getCommands: () => api.getCommands(),
55
63
  getThinkingLevel: () => api.getThinkingLevel(),
56
64
  setThinkingLevel: (level) => api.setThinkingLevel(level),
57
65
  setModel: (model) => api.setModel(model),
package/lib/polling.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram polling domain helpers
3
+ * Zones: telegram transport, polling runtime
3
4
  * Owns polling request builders, stop conditions, and the long-poll loop runtime for Telegram updates
4
5
  */
5
6
 
package/lib/preview.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram preview streaming helpers
3
+ * Zones: telegram outbound, streaming preview, rendering
3
4
  * Owns preview transport selection, runtime updates, and preview finalization
4
5
  */
5
6
 
@@ -44,9 +45,11 @@ export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
44
45
  }
45
46
 
46
47
  export type TelegramSentPreviewMessage = TelegramSentMessage;
47
- export type TelegramPreviewReplyMarkup = any;
48
+ export type TelegramPreviewReplyMarkup = unknown;
48
49
 
49
- export interface TelegramPreviewRuntimeDeps {
50
+ export interface TelegramPreviewRuntimeDeps<
51
+ TReplyMarkup = TelegramPreviewReplyMarkup,
52
+ > {
50
53
  getState: () => TelegramPreviewRuntimeState | undefined;
51
54
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
52
55
  clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
@@ -78,13 +81,13 @@ export interface TelegramPreviewRuntimeDeps {
78
81
  sendRenderedChunks: (
79
82
  chatId: number,
80
83
  chunks: TelegramRenderedChunk[],
81
- options?: { replyMarkup?: TelegramPreviewReplyMarkup },
84
+ options?: { replyMarkup?: TReplyMarkup },
82
85
  ) => Promise<number | undefined>;
83
86
  editRenderedMessage: (
84
87
  chatId: number,
85
88
  messageId: number,
86
89
  chunks: TelegramRenderedChunk[],
87
- options?: { replyMarkup?: TelegramPreviewReplyMarkup },
90
+ options?: { replyMarkup?: TReplyMarkup },
88
91
  ) => Promise<number | undefined>;
89
92
  }
90
93
 
@@ -92,7 +95,10 @@ export interface TelegramPreviewActiveTurn {
92
95
  chatId: number;
93
96
  }
94
97
 
95
- export interface TelegramAssistantMessagePreviewStartDeps<TMessage> {
98
+ export interface TelegramAssistantMessagePreviewStartDeps<
99
+ TMessage,
100
+ TReplyMarkup = TelegramPreviewReplyMarkup,
101
+ > {
96
102
  getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
97
103
  isAssistantMessage: (message: TMessage) => boolean;
98
104
  getState: () => TelegramPreviewRuntimeState | undefined;
@@ -102,6 +108,8 @@ export interface TelegramAssistantMessagePreviewStartDeps<TMessage> {
102
108
  finalizeMarkdownPreview: (
103
109
  chatId: number,
104
110
  markdown: string,
111
+ replyToMessageId?: number,
112
+ options?: { replyMarkup?: TReplyMarkup },
105
113
  ) => Promise<boolean>;
106
114
  }
107
115
 
@@ -115,9 +123,11 @@ export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
115
123
  schedulePreviewFlush: (chatId: number) => void;
116
124
  }
117
125
 
118
- export type TelegramAssistantMessagePreviewHookDeps<TMessage> =
119
- TelegramAssistantMessagePreviewStartDeps<TMessage> &
120
- TelegramAssistantMessagePreviewUpdateDeps<TMessage>;
126
+ export type TelegramAssistantMessagePreviewHookDeps<
127
+ TMessage,
128
+ TReplyMarkup = TelegramPreviewReplyMarkup,
129
+ > = TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup> &
130
+ TelegramAssistantMessagePreviewUpdateDeps<TMessage>;
121
131
 
122
132
  export interface TelegramAssistantMessagePreviewHookEvent<TMessage> {
123
133
  message: TMessage;
@@ -132,7 +142,9 @@ export interface TelegramAssistantMessagePreviewHooks<TMessage> {
132
142
  ) => Promise<void>;
133
143
  }
134
144
 
135
- export interface TelegramPreviewControllerDeps {
145
+ export interface TelegramPreviewControllerDeps<
146
+ TReplyMarkup = TelegramPreviewReplyMarkup,
147
+ > {
136
148
  getDefaultReplyToMessageId?: () => number | undefined;
137
149
  maxMessageLength?: number;
138
150
  renderPreviewText?: (markdown: string) => string;
@@ -162,13 +174,13 @@ export interface TelegramPreviewControllerDeps {
162
174
  chatId: number,
163
175
  chunks: TelegramRenderedChunk[],
164
176
  replyToMessageId: number | undefined,
165
- options?: { replyMarkup?: TelegramPreviewReplyMarkup },
177
+ options?: { replyMarkup?: TReplyMarkup },
166
178
  ) => Promise<number | undefined>;
167
179
  editRenderedMessage: (
168
180
  chatId: number,
169
181
  messageId: number,
170
182
  chunks: TelegramRenderedChunk[],
171
- options?: { replyMarkup?: TelegramPreviewReplyMarkup },
183
+ options?: { replyMarkup?: TReplyMarkup },
172
184
  ) => Promise<number | undefined>;
173
185
  throttleMs?: number;
174
186
  maxDraftId?: number;
@@ -179,7 +191,9 @@ export interface TelegramPreviewControllerDeps {
179
191
  clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
180
192
  }
181
193
 
182
- export interface TelegramPreviewController {
194
+ export interface TelegramPreviewController<
195
+ TReplyMarkup = TelegramPreviewReplyMarkup,
196
+ > {
183
197
  getState: () => TelegramPreviewRuntimeState | undefined;
184
198
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
185
199
  setPendingText: (text: string) => void;
@@ -193,7 +207,7 @@ export interface TelegramPreviewController {
193
207
  chatId: number,
194
208
  markdown: string,
195
209
  replyToMessageId?: number,
196
- options?: { replyMarkup?: TelegramPreviewReplyMarkup },
210
+ options?: { replyMarkup?: TReplyMarkup },
197
211
  ) => Promise<boolean>;
198
212
  }
199
213
 
@@ -230,27 +244,31 @@ export function createTelegramPreviewMessageTransport(
230
244
  };
231
245
  }
232
246
 
233
- export interface TelegramPreviewRenderedChunkTransportDeps {
247
+ export interface TelegramPreviewRenderedChunkTransportDeps<
248
+ TReplyMarkup = TelegramPreviewReplyMarkup,
249
+ > {
234
250
  sendRenderedChunks: (
235
251
  chatId: number,
236
252
  chunks: TelegramRenderedChunk[],
237
253
  options?: {
238
254
  replyToMessageId?: number;
239
- replyMarkup?: TelegramPreviewReplyMarkup;
255
+ replyMarkup?: TReplyMarkup;
240
256
  },
241
257
  ) => Promise<number | undefined>;
242
258
  editRenderedMessage: (
243
259
  chatId: number,
244
260
  messageId: number,
245
261
  chunks: TelegramRenderedChunk[],
246
- options?: { replyMarkup?: TelegramPreviewReplyMarkup },
262
+ options?: { replyMarkup?: TReplyMarkup },
247
263
  ) => Promise<number | undefined>;
248
264
  }
249
265
 
250
- export function createTelegramPreviewRenderedChunkTransport(
251
- deps: TelegramPreviewRenderedChunkTransportDeps,
266
+ export function createTelegramPreviewRenderedChunkTransport<
267
+ TReplyMarkup = TelegramPreviewReplyMarkup,
268
+ >(
269
+ deps: TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>,
252
270
  ): Pick<
253
- TelegramPreviewControllerDeps,
271
+ TelegramPreviewControllerDeps<TReplyMarkup>,
254
272
  "sendRenderedChunks" | "editRenderedMessage"
255
273
  > {
256
274
  return {
@@ -264,19 +282,23 @@ export function createTelegramPreviewRenderedChunkTransport(
264
282
  };
265
283
  }
266
284
 
267
- export type TelegramPreviewControllerRuntimeDeps = Omit<
268
- TelegramPreviewControllerDeps,
285
+ export type TelegramPreviewControllerRuntimeDeps<
286
+ TReplyMarkup = TelegramPreviewReplyMarkup,
287
+ > = Omit<
288
+ TelegramPreviewControllerDeps<TReplyMarkup>,
269
289
  | "sendMessage"
270
290
  | "editMessageText"
271
291
  | "sendRenderedChunks"
272
292
  | "editRenderedMessage"
273
293
  > &
274
294
  TelegramPreviewMessageTransportDeps &
275
- TelegramPreviewRenderedChunkTransportDeps;
295
+ TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>;
276
296
 
277
- export function createTelegramPreviewControllerRuntime(
278
- deps: TelegramPreviewControllerRuntimeDeps,
279
- ): TelegramPreviewController {
297
+ export function createTelegramPreviewControllerRuntime<
298
+ TReplyMarkup = TelegramPreviewReplyMarkup,
299
+ >(
300
+ deps: TelegramPreviewControllerRuntimeDeps<TReplyMarkup>,
301
+ ): TelegramPreviewController<TReplyMarkup> {
280
302
  return createTelegramPreviewController({
281
303
  getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
282
304
  maxMessageLength: deps.maxMessageLength,
@@ -302,18 +324,25 @@ export function createTelegramPreviewControllerRuntime(
302
324
 
303
325
  export interface TelegramAssistantPreviewRuntimeDeps<
304
326
  TMessage,
305
- > extends TelegramPreviewControllerRuntimeDeps {
327
+ TReplyMarkup = TelegramPreviewReplyMarkup,
328
+ > extends TelegramPreviewControllerRuntimeDeps<TReplyMarkup> {
306
329
  getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
307
330
  isAssistantMessage: (message: TMessage) => boolean;
308
331
  getMessageText: (message: TMessage) => string;
309
332
  }
310
333
 
311
- export type TelegramAssistantPreviewRuntime<TMessage> =
312
- TelegramPreviewController & TelegramAssistantMessagePreviewHooks<TMessage>;
334
+ export type TelegramAssistantPreviewRuntime<
335
+ TMessage,
336
+ TReplyMarkup = TelegramPreviewReplyMarkup,
337
+ > = TelegramPreviewController<TReplyMarkup> &
338
+ TelegramAssistantMessagePreviewHooks<TMessage>;
313
339
 
314
- export function createTelegramAssistantPreviewRuntime<TMessage>(
315
- deps: TelegramAssistantPreviewRuntimeDeps<TMessage>,
316
- ): TelegramAssistantPreviewRuntime<TMessage> {
340
+ export function createTelegramAssistantPreviewRuntime<
341
+ TMessage,
342
+ TReplyMarkup = TelegramPreviewReplyMarkup,
343
+ >(
344
+ deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
345
+ ): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
317
346
  const controller = createTelegramPreviewControllerRuntime(deps);
318
347
  return {
319
348
  ...controller,
@@ -331,9 +360,11 @@ export function createTelegramAssistantPreviewRuntime<TMessage>(
331
360
  };
332
361
  }
333
362
 
334
- export function createTelegramPreviewController(
335
- deps: TelegramPreviewControllerDeps,
336
- ): TelegramPreviewController {
363
+ export function createTelegramPreviewController<
364
+ TReplyMarkup = TelegramPreviewReplyMarkup,
365
+ >(
366
+ deps: TelegramPreviewControllerDeps<TReplyMarkup>,
367
+ ): TelegramPreviewController<TReplyMarkup> {
337
368
  let state: TelegramPreviewRuntimeState | undefined;
338
369
  const clearTimer = deps.clearTimer ?? clearTimeout;
339
370
  const setTimer =
@@ -349,7 +380,7 @@ export function createTelegramPreviewController(
349
380
  let nextDraftId = 0;
350
381
  const getRuntimeDeps = (
351
382
  replyToMessageId?: number,
352
- ): TelegramPreviewRuntimeDeps => ({
383
+ ): TelegramPreviewRuntimeDeps<TReplyMarkup> => ({
353
384
  getState: () => state,
354
385
  setState: (nextState) => {
355
386
  state = nextState;
@@ -420,8 +451,11 @@ export function createTelegramPreviewController(
420
451
  };
421
452
  }
422
453
 
423
- export function createTelegramAssistantMessagePreviewHooks<TMessage>(
424
- deps: TelegramAssistantMessagePreviewHookDeps<TMessage>,
454
+ export function createTelegramAssistantMessagePreviewHooks<
455
+ TMessage,
456
+ TReplyMarkup = TelegramPreviewReplyMarkup,
457
+ >(
458
+ deps: TelegramAssistantMessagePreviewHookDeps<TMessage, TReplyMarkup>,
425
459
  ): TelegramAssistantMessagePreviewHooks<TMessage> {
426
460
  return {
427
461
  onMessageStart: async (
@@ -437,9 +471,12 @@ export function createTelegramAssistantMessagePreviewHooks<TMessage>(
437
471
  };
438
472
  }
439
473
 
440
- export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
474
+ export async function handleTelegramAssistantMessagePreviewStart<
475
+ TMessage,
476
+ TReplyMarkup = TelegramPreviewReplyMarkup,
477
+ >(
441
478
  message: TMessage,
442
- deps: TelegramAssistantMessagePreviewStartDeps<TMessage>,
479
+ deps: TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup>,
443
480
  ): Promise<void> {
444
481
  const turn = deps.getActiveTurn();
445
482
  if (!turn || !deps.isAssistantMessage(message)) return;
@@ -517,9 +554,11 @@ export function shouldUseTelegramDraftPreview(options: {
517
554
  );
518
555
  }
519
556
 
520
- export async function clearTelegramPreview(
557
+ export async function clearTelegramPreview<
558
+ TReplyMarkup = TelegramPreviewReplyMarkup,
559
+ >(
521
560
  chatId: number,
522
- deps: TelegramPreviewRuntimeDeps,
561
+ deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
523
562
  ): Promise<void> {
524
563
  void chatId;
525
564
  const state = deps.getState();
@@ -528,10 +567,12 @@ export async function clearTelegramPreview(
528
567
  deps.setState(undefined);
529
568
  }
530
569
 
531
- async function performTelegramPreviewFlush(
570
+ async function performTelegramPreviewFlush<
571
+ TReplyMarkup = TelegramPreviewReplyMarkup,
572
+ >(
532
573
  chatId: number,
533
574
  state: TelegramPreviewRuntimeState,
534
- deps: TelegramPreviewRuntimeDeps,
575
+ deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
535
576
  ): Promise<void> {
536
577
  const snapshot = buildTelegramPreviewSnapshot({
537
578
  state,
@@ -580,9 +621,11 @@ async function performTelegramPreviewFlush(
580
621
  state.lastSentStrategy = snapshot.strategy;
581
622
  }
582
623
 
583
- export async function flushTelegramPreview(
624
+ export async function flushTelegramPreview<
625
+ TReplyMarkup = TelegramPreviewReplyMarkup,
626
+ >(
584
627
  chatId: number,
585
- deps: TelegramPreviewRuntimeDeps,
628
+ deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
586
629
  ): Promise<void> {
587
630
  const state = deps.getState();
588
631
  if (!state) return;
@@ -607,9 +650,11 @@ export async function flushTelegramPreview(
607
650
  }
608
651
  }
609
652
 
610
- export async function finalizeTelegramPreview(
653
+ export async function finalizeTelegramPreview<
654
+ TReplyMarkup = TelegramPreviewReplyMarkup,
655
+ >(
611
656
  chatId: number,
612
- deps: TelegramPreviewRuntimeDeps,
657
+ deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
613
658
  ): Promise<boolean> {
614
659
  const state = deps.getState();
615
660
  if (!state) return false;
@@ -628,11 +673,13 @@ export async function finalizeTelegramPreview(
628
673
  return state.messageId !== undefined;
629
674
  }
630
675
 
631
- export async function finalizeTelegramMarkdownPreview(
676
+ export async function finalizeTelegramMarkdownPreview<
677
+ TReplyMarkup = TelegramPreviewReplyMarkup,
678
+ >(
632
679
  chatId: number,
633
680
  markdown: string,
634
- deps: TelegramPreviewRuntimeDeps,
635
- options?: { replyMarkup?: TelegramPreviewReplyMarkup },
681
+ deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
682
+ options?: { replyMarkup?: TReplyMarkup },
636
683
  ): Promise<boolean> {
637
684
  const state = deps.getState();
638
685
  if (!state) return false;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * π prompt-template bridge helpers
3
+ * Zones: pi agent prompts, telegram controls, filesystem
4
+ * Discovers π prompt-template slash commands and expands them before Telegram queue dispatch
5
+ */
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import type { PiSlashCommandInfo } from "./pi.ts";
9
+
10
+ export interface TelegramPromptTemplateCommand {
11
+ command: string;
12
+ description?: string;
13
+ path: string;
14
+ }
15
+
16
+ export type TelegramPromptTemplateReader = (path: string) => string;
17
+
18
+ const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
19
+
20
+ function stripPromptTemplateFrontmatter(content: string): string {
21
+ if (!content.startsWith("---")) return content;
22
+ const lines = content.split("\n");
23
+ if (lines[0]?.trim() !== "---") return content;
24
+ for (let index = 1; index < lines.length; index += 1) {
25
+ if (lines[index]?.trim() === "---")
26
+ return lines.slice(index + 1).join("\n");
27
+ }
28
+ return content;
29
+ }
30
+
31
+ export function parsePromptTemplateArgs(argsString: string): string[] {
32
+ const args: string[] = [];
33
+ let current = "";
34
+ let quote: string | undefined;
35
+ for (const char of argsString) {
36
+ if (quote) {
37
+ if (char === quote) {
38
+ quote = undefined;
39
+ } else {
40
+ current += char;
41
+ }
42
+ continue;
43
+ }
44
+ if (char === '"' || char === "'") {
45
+ quote = char;
46
+ continue;
47
+ }
48
+ if (char === " " || char === "\t") {
49
+ if (current) args.push(current);
50
+ current = "";
51
+ continue;
52
+ }
53
+ current += char;
54
+ }
55
+ if (current) args.push(current);
56
+ return args;
57
+ }
58
+
59
+ export function substitutePromptTemplateArgs(
60
+ content: string,
61
+ args: readonly string[],
62
+ ): string {
63
+ let result = content.replace(/\$(\d+)/g, (_, num: string) => {
64
+ const index = Number.parseInt(num, 10) - 1;
65
+ return args[index] ?? "";
66
+ });
67
+ result = result.replace(
68
+ /\$\{@:(\d+)(?::(\d+))?\}/g,
69
+ (_, startValue: string, lengthValue: string | undefined) => {
70
+ const start = Math.max(Number.parseInt(startValue, 10) - 1, 0);
71
+ if (lengthValue) {
72
+ const length = Number.parseInt(lengthValue, 10);
73
+ return args.slice(start, start + length).join(" ");
74
+ }
75
+ return args.slice(start).join(" ");
76
+ },
77
+ );
78
+ const allArgs = args.join(" ");
79
+ return result.replace(/\$ARGUMENTS/g, allArgs).replace(/\$@/g, allArgs);
80
+ }
81
+
82
+ export function isTelegramPromptTemplateCommandName(name: string): boolean {
83
+ return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
84
+ }
85
+
86
+ export function mapPiPromptTemplateNameToTelegramCommandName(
87
+ name: string,
88
+ ): string | undefined {
89
+ const command = name
90
+ .toLowerCase()
91
+ .replace(/[^a-z0-9_]+/g, "_")
92
+ .replace(/_+/g, "_")
93
+ .replace(/^_+|_+$/g, "")
94
+ .slice(0, 32)
95
+ .replace(/_+$/g, "");
96
+ return isTelegramPromptTemplateCommandName(command) ? command : undefined;
97
+ }
98
+
99
+ export interface TelegramPromptTemplateCommandGetterDeps {
100
+ getCommands: () => readonly PiSlashCommandInfo[];
101
+ reservedCommandNames?: readonly string[];
102
+ }
103
+
104
+ export function getTelegramPromptTemplateCommands(
105
+ commands: readonly PiSlashCommandInfo[],
106
+ reservedNames: ReadonlySet<string> = new Set(),
107
+ ): TelegramPromptTemplateCommand[] {
108
+ const seen = new Set<string>();
109
+ const promptCommands: TelegramPromptTemplateCommand[] = [];
110
+ for (const command of commands) {
111
+ if (command.source !== "prompt") continue;
112
+ const telegramCommand = mapPiPromptTemplateNameToTelegramCommandName(
113
+ command.name,
114
+ );
115
+ if (!telegramCommand) continue;
116
+ if (reservedNames.has(telegramCommand)) continue;
117
+ if (seen.has(telegramCommand)) continue;
118
+ seen.add(telegramCommand);
119
+ promptCommands.push({
120
+ command: telegramCommand,
121
+ description: command.description,
122
+ path: command.sourceInfo.path,
123
+ });
124
+ }
125
+ return promptCommands.sort((a, b) => a.command.localeCompare(b.command));
126
+ }
127
+
128
+ export function createTelegramPromptTemplateCommandGetter(
129
+ deps: TelegramPromptTemplateCommandGetterDeps,
130
+ ): () => TelegramPromptTemplateCommand[] {
131
+ const reservedNames = new Set(deps.reservedCommandNames);
132
+ return function getPromptTemplateCommands() {
133
+ return getTelegramPromptTemplateCommands(deps.getCommands(), reservedNames);
134
+ };
135
+ }
136
+
137
+ export function expandTelegramPromptTemplateCommand(
138
+ commandName: string,
139
+ args: string,
140
+ commands: readonly TelegramPromptTemplateCommand[],
141
+ readTemplate: TelegramPromptTemplateReader = (path) =>
142
+ readFileSync(path, "utf-8"),
143
+ ): string | undefined {
144
+ const command = commands.find(
145
+ (candidate) => candidate.command === commandName,
146
+ );
147
+ if (!command) return undefined;
148
+ const content = stripPromptTemplateFrontmatter(readTemplate(command.path));
149
+ return substitutePromptTemplateArgs(content, parsePromptTemplateArgs(args));
150
+ }
package/lib/prompts.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram prompt injection helpers
3
+ * Zones: pi agent prompts, telegram guidance
3
4
  * Owns Telegram-specific system prompt suffixes injected into pi agent turns
4
5
  */
5
6
 
@@ -14,6 +15,7 @@ Inbound context:
14
15
  - \`[telegram]\` marks Telegram-originated messages.
15
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.
16
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.
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.
17
19
 
18
20
  Telegram-visible output:
19
21
  - Telegram is often phone-width; prefer narrow table columns because wide monospace tables can become unreadable.