@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/commands.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram command routing helpers
3
+ * Zones: telegram controls, pi agent commands, queue controls
3
4
  * Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports
4
5
  */
5
6
 
@@ -21,22 +22,89 @@ export interface TelegramBotCommandDefinition {
21
22
  description: string;
22
23
  }
23
24
 
24
- export const TELEGRAM_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [
25
- {
26
- command: "start",
27
- description: "Show help and pair the Telegram bridge",
28
- },
29
- {
30
- command: "status",
31
- description: "Show model, usage, cost, and context status",
32
- },
33
- { command: "model", description: "Open the interactive model selector" },
34
- { command: "compact", description: "Compact the current pi session" },
35
- {
36
- command: "stop",
37
- description: "Abort the current pi task and clear queued turns",
38
- },
39
- ];
25
+ export interface TelegramPromptTemplateMenuCommand {
26
+ command: string;
27
+ description?: string;
28
+ }
29
+
30
+ export const TELEGRAM_COMMAND_EMOJI = {
31
+ start: "đŸŸĸ",
32
+ status: "📊",
33
+ model: "🤖",
34
+ thinking: "🧠",
35
+ compact: "🗜",
36
+ queue: "đŸ”ĸ",
37
+ next: "⏊",
38
+ continue: "â–ļī¸",
39
+ abort: "âšī¸",
40
+ stop: "đŸŸĨ",
41
+ } as const;
42
+
43
+ export type TelegramCommandEmojiName = keyof typeof TELEGRAM_COMMAND_EMOJI;
44
+
45
+ export function getTelegramCommandEmoji(
46
+ command: TelegramCommandEmojiName,
47
+ ): string {
48
+ return TELEGRAM_COMMAND_EMOJI[command];
49
+ }
50
+
51
+ export function formatTelegramCommandEmojiPrefix(
52
+ command: TelegramCommandEmojiName,
53
+ ): string {
54
+ return `${getTelegramCommandEmoji(command)} `;
55
+ }
56
+
57
+ function formatTelegramBotCommandDescription(
58
+ command: TelegramCommandEmojiName,
59
+ description: string,
60
+ ): string {
61
+ return `${formatTelegramCommandEmojiPrefix(command)}${description}`;
62
+ }
63
+
64
+ export const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] =
65
+ [
66
+ {
67
+ command: "start",
68
+ description: formatTelegramBotCommandDescription(
69
+ "start",
70
+ "Open menu / Pair bridge",
71
+ ),
72
+ },
73
+ {
74
+ command: "compact",
75
+ description: formatTelegramBotCommandDescription(
76
+ "compact",
77
+ "Compact current session",
78
+ ),
79
+ },
80
+ {
81
+ command: "next",
82
+ description: formatTelegramBotCommandDescription(
83
+ "next",
84
+ "Force next turn",
85
+ ),
86
+ },
87
+ {
88
+ command: "continue",
89
+ description: formatTelegramBotCommandDescription(
90
+ "continue",
91
+ "Queue continue prompt",
92
+ ),
93
+ },
94
+ {
95
+ command: "abort",
96
+ description: formatTelegramBotCommandDescription("abort", "Abort ΀"),
97
+ },
98
+ {
99
+ command: "stop",
100
+ description: formatTelegramBotCommandDescription(
101
+ "stop",
102
+ "Abort ΀ & Clear queue",
103
+ ),
104
+ },
105
+ ];
106
+
107
+ export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS;
40
108
 
41
109
  export interface TelegramBotCommandRegistrationDeps {
42
110
  setMyCommands: (
@@ -95,7 +163,7 @@ function formatTelegramTakeoverPrompt(
95
163
  const action = theme.fg("warning", "move singleton lock here?");
96
164
  const from = theme.fg("muted", "from:");
97
165
  const to = theme.fg("muted", "to:");
98
- const source = owner ?? "another pi instance";
166
+ const source = owner ?? "another ΀ instance";
99
167
  return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
100
168
  }
101
169
 
@@ -116,7 +184,7 @@ export function registerTelegramBridgeCommands(
116
184
  },
117
185
  });
118
186
  pi.registerCommand("telegram-connect", {
119
- description: "Start the Telegram bridge in this pi session",
187
+ description: "Start the Telegram bridge in this ΀ session",
120
188
  handler: async (_args, ctx) => {
121
189
  await deps.reloadConfig();
122
190
  if (!deps.hasBotToken()) {
@@ -143,7 +211,7 @@ export function registerTelegramBridgeCommands(
143
211
  },
144
212
  });
145
213
  pi.registerCommand("telegram-disconnect", {
146
- description: "Stop the Telegram bridge in this pi session",
214
+ description: "Stop the Telegram bridge in this ΀ session",
147
215
  handler: async (_args, ctx) => {
148
216
  const message = await deps.stopPolling();
149
217
  if (message) ctx.ui.notify(message, "info");
@@ -152,12 +220,47 @@ export function registerTelegramBridgeCommands(
152
220
  });
153
221
  }
154
222
 
223
+ export const TELEGRAM_RESERVED_COMMAND_NAMES = [
224
+ "stop",
225
+ "abort",
226
+ "next",
227
+ "continue",
228
+ "status",
229
+ "queue",
230
+ "compact",
231
+ "model",
232
+ "thinking",
233
+ "help",
234
+ "start",
235
+ ] as const;
236
+
237
+ export type TelegramReservedCommandName =
238
+ (typeof TELEGRAM_RESERVED_COMMAND_NAMES)[number];
239
+
240
+ const TELEGRAM_RESERVED_COMMAND_NAME_SET = new Set<string>(
241
+ TELEGRAM_RESERVED_COMMAND_NAMES,
242
+ );
243
+
244
+ export function isTelegramReservedCommandName(
245
+ commandName: string | undefined,
246
+ ): commandName is TelegramReservedCommandName {
247
+ return (
248
+ commandName !== undefined &&
249
+ TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName)
250
+ );
251
+ }
252
+
155
253
  export type TelegramCommandAction =
156
254
  | { kind: "ignore"; executionMode: "ignored" }
157
255
  | { kind: "stop"; executionMode: "immediate" }
256
+ | { kind: "abort"; executionMode: "immediate" }
257
+ | { kind: "next"; executionMode: "immediate" }
258
+ | { kind: "continue"; executionMode: "immediate" }
259
+ | { kind: "queue"; executionMode: "immediate" }
158
260
  | { kind: "compact"; executionMode: "immediate" }
159
261
  | { kind: "status"; executionMode: "immediate" }
160
262
  | { kind: "model"; executionMode: "immediate" }
263
+ | { kind: "thinking"; executionMode: "immediate" }
161
264
  | {
162
265
  kind: "help";
163
266
  commandName: "help" | "start";
@@ -168,9 +271,14 @@ export type TelegramCommandExecutionMode = "ignored" | "immediate";
168
271
 
169
272
  export interface TelegramCommandActionDeps<TMessage, TContext> {
170
273
  handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
274
+ handleAbort: (message: TMessage, ctx: TContext) => Promise<void>;
275
+ handleNext: (message: TMessage, ctx: TContext) => Promise<void>;
276
+ handleContinue: (message: TMessage, ctx: TContext) => Promise<void>;
277
+ handleQueue: (message: TMessage, ctx: TContext) => Promise<void>;
171
278
  handleCompact: (message: TMessage, ctx: TContext) => Promise<void>;
172
279
  handleStatus: (message: TMessage, ctx: TContext) => Promise<void>;
173
280
  handleModel: (message: TMessage, ctx: TContext) => Promise<void>;
281
+ handleThinking: (message: TMessage, ctx: TContext) => Promise<void>;
174
282
  handleHelp: (
175
283
  message: TMessage,
176
284
  commandName: "help" | "start",
@@ -213,16 +321,6 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
213
321
  sendTextReply: (text: string) => Promise<void>;
214
322
  }
215
323
 
216
- export interface TelegramHelpCommandDeps {
217
- senderUserId?: number;
218
- getAllowedUserId: () => number | undefined;
219
- setAllowedUserId: (userId: number) => void;
220
- registerBotCommands: () => Promise<void>;
221
- persistConfig: () => Promise<void>;
222
- updateStatus: () => void;
223
- sendTextReply: (text: string) => Promise<void>;
224
- }
225
-
226
324
  export type TelegramControlCommandType =
227
325
  PendingTelegramControlItem<unknown>["controlType"];
228
326
 
@@ -401,6 +499,11 @@ export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
401
499
  message: TMessage,
402
500
  ctx: TContext,
403
501
  ) => Promise<boolean>;
502
+ expandPromptTemplateCommand?: (
503
+ commandName: string,
504
+ args: string,
505
+ ) => string | undefined;
506
+ replaceMessageText: (message: TMessage, text: string) => TMessage;
404
507
  enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise<void>;
405
508
  }
406
509
 
@@ -422,6 +525,7 @@ export interface TelegramCommandRuntimeDeps<
422
525
  setCompactionInProgress: (inProgress: boolean) => void;
423
526
  updateStatus: (ctx: TContext) => void;
424
527
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
528
+ enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
425
529
  compact: (
426
530
  ctx: TContext,
427
531
  callbacks: { onComplete: () => void; onError: (error: unknown) => void },
@@ -435,15 +539,65 @@ export interface TelegramCommandRuntimeDeps<
435
539
  ) => void;
436
540
  showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
437
541
  openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
542
+ openThinkingMenu: (message: TMessage, ctx: TContext) => Promise<void>;
543
+ openQueueMenu: (message: TMessage, ctx: TContext) => Promise<void>;
438
544
  getAllowedUserId: () => number | undefined;
439
545
  setAllowedUserId: (userId: number) => void;
440
546
  registerBotCommands: () => Promise<void>;
547
+ getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
441
548
  persistConfig: () => Promise<void>;
442
549
  sendTextReply: (message: TMessage, text: string) => Promise<void>;
443
550
  }
444
551
 
445
- export const TELEGRAM_HELP_TEXT =
446
- "Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop. /stop aborts the current run and clears queued Telegram turns.";
552
+ export const TELEGRAM_APP_MENU_INTRO_HTML = [
553
+ "<b>Ī€ Telegram bridge</b>",
554
+ "",
555
+ `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
556
+ `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
557
+ `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
558
+ `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
559
+ `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Ī€`,
560
+ `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Ī€ & Clear queue`,
561
+ ].join("\n");
562
+
563
+ function escapeTelegramCommandMenuHtml(text: string): string {
564
+ return text
565
+ .replace(/&/g, "&amp;")
566
+ .replace(/</g, "&lt;")
567
+ .replace(/>/g, "&gt;");
568
+ }
569
+
570
+ function buildTelegramPromptTemplateMenuHtml(
571
+ promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
572
+ ): string {
573
+ if (promptTemplates.length === 0) return "";
574
+ return promptTemplates
575
+ .map((template) => `🧩 /${escapeTelegramCommandMenuHtml(template.command)}`)
576
+ .join("\n");
577
+ }
578
+
579
+ export function buildTelegramAppMenuHtml(
580
+ statusHtml: string,
581
+ promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
582
+ ): string {
583
+ const promptTemplateHtml =
584
+ buildTelegramPromptTemplateMenuHtml(promptTemplates);
585
+ if (!promptTemplateHtml)
586
+ return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${statusHtml}`;
587
+ return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
588
+ }
589
+
590
+ export function createTelegramAppMenuHtmlBuilder<TContext>(deps: {
591
+ buildStatusHtml: (ctx: TContext) => string;
592
+ getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
593
+ }): (ctx: TContext) => string {
594
+ return function buildTelegramAppMenuHtmlForContext(ctx) {
595
+ return buildTelegramAppMenuHtml(
596
+ deps.buildStatusHtml(ctx),
597
+ deps.getPromptTemplateCommands?.(),
598
+ );
599
+ };
600
+ }
447
601
 
448
602
  function getTelegramCommandErrorMessage(error: unknown): string {
449
603
  return error instanceof Error ? error.message : String(error);
@@ -460,24 +614,27 @@ export function parseTelegramCommand(
460
614
  return { name, args: tail.join(" ").trim() };
461
615
  }
462
616
 
617
+ export const TELEGRAM_COMMAND_ACTIONS = {
618
+ stop: { kind: "stop", executionMode: "immediate" },
619
+ abort: { kind: "abort", executionMode: "immediate" },
620
+ next: { kind: "next", executionMode: "immediate" },
621
+ continue: { kind: "continue", executionMode: "immediate" },
622
+ status: { kind: "status", executionMode: "immediate" },
623
+ queue: { kind: "queue", executionMode: "immediate" },
624
+ compact: { kind: "compact", executionMode: "immediate" },
625
+ model: { kind: "model", executionMode: "immediate" },
626
+ thinking: { kind: "thinking", executionMode: "immediate" },
627
+ help: { kind: "help", commandName: "help", executionMode: "immediate" },
628
+ start: { kind: "help", commandName: "start", executionMode: "immediate" },
629
+ } as const satisfies Record<TelegramReservedCommandName, TelegramCommandAction>;
630
+
463
631
  export function buildTelegramCommandAction(
464
632
  commandName: string | undefined,
465
633
  ): TelegramCommandAction {
466
- switch (commandName) {
467
- case "stop":
468
- return { kind: "stop", executionMode: "immediate" };
469
- case "compact":
470
- return { kind: "compact", executionMode: "immediate" };
471
- case "status":
472
- return { kind: "status", executionMode: "immediate" };
473
- case "model":
474
- return { kind: "model", executionMode: "immediate" };
475
- case "help":
476
- case "start":
477
- return { kind: "help", commandName, executionMode: "immediate" };
478
- default:
479
- return { kind: "ignore", executionMode: "ignored" };
634
+ if (!isTelegramReservedCommandName(commandName)) {
635
+ return { kind: "ignore", executionMode: "ignored" };
480
636
  }
637
+ return TELEGRAM_COMMAND_ACTIONS[commandName];
481
638
  }
482
639
 
483
640
  export function getTelegramCommandExecutionMode(
@@ -514,6 +671,69 @@ export async function handleTelegramStopCommand(
514
671
  await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`);
515
672
  }
516
673
 
674
+ export async function handleTelegramAbortCommand(deps: {
675
+ hasAbortHandler: () => boolean;
676
+ clearPendingModelSwitch: () => void;
677
+ abortCurrentTurn: () => void;
678
+ setPreserveForIdle: () => void;
679
+ updateStatus: () => void;
680
+ sendTextReply: (text: string) => Promise<void>;
681
+ }): Promise<void> {
682
+ deps.clearPendingModelSwitch();
683
+ if (!deps.hasAbortHandler()) {
684
+ await deps.sendTextReply("No active turn.");
685
+ return;
686
+ }
687
+ deps.setPreserveForIdle();
688
+ deps.abortCurrentTurn();
689
+ deps.updateStatus();
690
+ await deps.sendTextReply("Aborted current turn.");
691
+ }
692
+
693
+ export async function handleTelegramNextCommand(deps: {
694
+ hasAbortHandler: () => boolean;
695
+ isIdle: () => boolean;
696
+ hasQueuedItems: () => boolean;
697
+ clearPendingModelSwitch: () => void;
698
+ abortCurrentTurn: () => void;
699
+ dispatchNextQueuedTurn: () => void;
700
+ setPreserveForDispatch: () => void;
701
+ updateStatus: () => void;
702
+ sendTextReply: (text: string) => Promise<void>;
703
+ }): Promise<void> {
704
+ deps.clearPendingModelSwitch();
705
+ if (!deps.hasQueuedItems()) {
706
+ await deps.sendTextReply("<b>Queue is empty.</b>");
707
+ return;
708
+ }
709
+ if (!deps.isIdle() && deps.hasAbortHandler()) {
710
+ deps.setPreserveForDispatch();
711
+ deps.abortCurrentTurn();
712
+ deps.updateStatus();
713
+ await deps.sendTextReply(
714
+ "Aborted current turn. Dispatching next queued turn.",
715
+ );
716
+ return;
717
+ }
718
+ if (!deps.isIdle()) {
719
+ await deps.sendTextReply("Ī€ is busy. Send /abort or /stop first.");
720
+ return;
721
+ }
722
+ deps.dispatchNextQueuedTurn();
723
+ deps.updateStatus();
724
+ await deps.sendTextReply("Dispatching next queued turn.");
725
+ }
726
+
727
+ export async function handleTelegramContinueCommand<TMessage, TContext>(
728
+ message: TMessage,
729
+ ctx: TContext,
730
+ deps: {
731
+ enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
732
+ },
733
+ ): Promise<void> {
734
+ await deps.enqueueContinueTurn(message, ctx);
735
+ }
736
+
517
737
  export async function handleTelegramCompactCommand(
518
738
  deps: TelegramCompactCommandDeps,
519
739
  ): Promise<void> {
@@ -526,7 +746,7 @@ export async function handleTelegramCompactCommand(
526
746
  deps.isCompactionInProgress()
527
747
  ) {
528
748
  await deps.sendTextReply(
529
- "Cannot compact while pi or the Telegram queue is busy. Wait for queued turns to finish or send /stop first.",
749
+ "Cannot compact while ΀ or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
530
750
  );
531
751
  return;
532
752
  }
@@ -560,30 +780,6 @@ export async function handleTelegramCompactCommand(
560
780
  await deps.sendTextReply("Compaction started.");
561
781
  }
562
782
 
563
- export async function handleTelegramHelpCommand(
564
- commandName: "help" | "start",
565
- deps: TelegramHelpCommandDeps,
566
- ): Promise<void> {
567
- let helpText = TELEGRAM_HELP_TEXT;
568
- if (commandName === "start") {
569
- try {
570
- await deps.registerBotCommands();
571
- } catch (error) {
572
- const errorMessage = getTelegramCommandErrorMessage(error);
573
- helpText += `\n\nWarning: failed to register bot commands menu: ${errorMessage}`;
574
- }
575
- }
576
- await deps.sendTextReply(helpText);
577
- if (deps.senderUserId === undefined) return;
578
- await pairTelegramUserIfNeeded(deps.senderUserId, {
579
- allowedUserId: deps.getAllowedUserId(),
580
- ctx: undefined,
581
- setAllowedUserId: deps.setAllowedUserId,
582
- persistConfig: deps.persistConfig,
583
- updateStatus: deps.updateStatus,
584
- });
585
- }
586
-
587
783
  export async function handleTelegramStatusCommand<TContext>(deps: {
588
784
  ctx: TContext;
589
785
  showStatus: (ctx: TContext) => Promise<void>;
@@ -610,6 +806,18 @@ export async function executeTelegramCommandAction<TMessage, TContext>(
610
806
  case "stop":
611
807
  await deps.handleStop(message, ctx);
612
808
  return true;
809
+ case "abort":
810
+ await deps.handleAbort(message, ctx);
811
+ return true;
812
+ case "next":
813
+ await deps.handleNext(message, ctx);
814
+ return true;
815
+ case "continue":
816
+ await deps.handleContinue(message, ctx);
817
+ return true;
818
+ case "queue":
819
+ await deps.handleQueue(message, ctx);
820
+ return true;
613
821
  case "compact":
614
822
  await deps.handleCompact(message, ctx);
615
823
  return true;
@@ -619,6 +827,9 @@ export async function executeTelegramCommandAction<TMessage, TContext>(
619
827
  case "model":
620
828
  await deps.handleModel(message, ctx);
621
829
  return true;
830
+ case "thinking":
831
+ await deps.handleThinking(message, ctx);
832
+ return true;
622
833
  case "help":
623
834
  await deps.handleHelp(message, action.commandName, ctx);
624
835
  return true;
@@ -683,10 +894,13 @@ export function createTelegramCommandHandlerTargetRuntime<
683
894
  setCompactionInProgress: deps.setCompactionInProgress,
684
895
  updateStatus: deps.updateStatus,
685
896
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
897
+ enqueueContinueTurn: deps.enqueueContinueTurn,
686
898
  compact: deps.compact,
687
899
  enqueueControlItem: commandTargetRuntime.enqueueControlItem,
688
900
  showStatus: commandTargetRuntime.showStatus,
689
901
  openModelMenu: commandTargetRuntime.openModelMenu,
902
+ openThinkingMenu: deps.openThinkingMenu,
903
+ openQueueMenu: deps.openQueueMenu,
690
904
  getAllowedUserId: deps.getAllowedUserId,
691
905
  setAllowedUserId: deps.setAllowedUserId,
692
906
  registerBotCommands: createTelegramBotCommandRegistrar({
@@ -721,11 +935,29 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
721
935
  ): Promise<void> => {
722
936
  const firstMessage = messages[0];
723
937
  if (!firstMessage) return;
724
- const commandName = parseTelegramCommand(
725
- deps.extractRawText(messages),
726
- )?.name;
727
- const handled = await deps.handleCommand(commandName, firstMessage, ctx);
938
+ const command = parseTelegramCommand(deps.extractRawText(messages));
939
+ const handled = await deps.handleCommand(
940
+ command?.name,
941
+ firstMessage,
942
+ ctx,
943
+ );
728
944
  if (handled) return;
945
+ if (command?.name && deps.expandPromptTemplateCommand) {
946
+ const expanded = deps.expandPromptTemplateCommand(
947
+ command.name,
948
+ command.args,
949
+ );
950
+ if (expanded !== undefined) {
951
+ await deps.enqueueTurn(
952
+ [
953
+ deps.replaceMessageText(firstMessage, expanded),
954
+ ...messages.slice(1),
955
+ ],
956
+ ctx,
957
+ );
958
+ return;
959
+ }
960
+ }
729
961
  await deps.enqueueTurn(messages, ctx);
730
962
  },
731
963
  };
@@ -761,6 +993,39 @@ async function handleTelegramCommandRuntime<
761
993
  sendTextReply: sendReplyFor(nextMessage),
762
994
  });
763
995
  },
996
+ handleAbort: async (nextMessage, commandCtx) => {
997
+ await handleTelegramAbortCommand({
998
+ hasAbortHandler: deps.hasAbortHandler,
999
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
1000
+ abortCurrentTurn: deps.abortCurrentTurn,
1001
+ setPreserveForIdle: () => deps.setPreserveQueuedTurnsAsHistory(true),
1002
+ updateStatus: updateStatusFor(commandCtx),
1003
+ sendTextReply: sendReplyFor(nextMessage),
1004
+ });
1005
+ },
1006
+ handleNext: async (nextMessage, commandCtx) => {
1007
+ await handleTelegramNextCommand({
1008
+ hasAbortHandler: deps.hasAbortHandler,
1009
+ isIdle: () => deps.isIdle(commandCtx),
1010
+ hasQueuedItems: deps.hasQueuedTelegramItems,
1011
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
1012
+ abortCurrentTurn: deps.abortCurrentTurn,
1013
+ dispatchNextQueuedTurn: () =>
1014
+ deps.dispatchNextQueuedTelegramTurn(commandCtx),
1015
+ setPreserveForDispatch: () =>
1016
+ deps.setPreserveQueuedTurnsAsHistory(false),
1017
+ updateStatus: updateStatusFor(commandCtx),
1018
+ sendTextReply: sendReplyFor(nextMessage),
1019
+ });
1020
+ },
1021
+ handleContinue: async (nextMessage, commandCtx) => {
1022
+ await handleTelegramContinueCommand(nextMessage, commandCtx, {
1023
+ enqueueContinueTurn: deps.enqueueContinueTurn,
1024
+ });
1025
+ },
1026
+ handleQueue: async (nextMessage, commandCtx) => {
1027
+ await deps.openQueueMenu(nextMessage, commandCtx);
1028
+ },
764
1029
  handleCompact: async (nextMessage, commandCtx) => {
765
1030
  await handleTelegramCompactCommand({
766
1031
  isIdle: () => deps.isIdle(commandCtx),
@@ -779,10 +1044,7 @@ async function handleTelegramCommandRuntime<
779
1044
  });
780
1045
  },
781
1046
  handleStatus: async (nextMessage, commandCtx) => {
782
- await handleTelegramStatusCommand<TContext>({
783
- ctx: commandCtx,
784
- showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx),
785
- });
1047
+ await deps.showStatus(nextMessage, commandCtx);
786
1048
  },
787
1049
  handleModel: async (nextMessage, commandCtx) => {
788
1050
  await handleTelegramModelCommand<TContext>({
@@ -791,16 +1053,29 @@ async function handleTelegramCommandRuntime<
791
1053
  deps.openModelMenu(nextMessage, controlCtx),
792
1054
  });
793
1055
  },
794
- handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
795
- await handleTelegramHelpCommand(nextCommandName, {
796
- senderUserId: nextMessage.from?.id,
797
- getAllowedUserId: deps.getAllowedUserId,
798
- setAllowedUserId: deps.setAllowedUserId,
799
- registerBotCommands: deps.registerBotCommands,
800
- persistConfig: deps.persistConfig,
801
- updateStatus: updateStatusFor(commandCtx),
802
- sendTextReply: sendReplyFor(nextMessage),
803
- });
1056
+ handleThinking: async (nextMessage, commandCtx) => {
1057
+ await deps.openThinkingMenu(nextMessage, commandCtx);
1058
+ },
1059
+ handleHelp: async (nextMessage, _nextCommandName, commandCtx) => {
1060
+ try {
1061
+ await deps.registerBotCommands();
1062
+ } catch (error) {
1063
+ const errorMessage = getTelegramCommandErrorMessage(error);
1064
+ await deps.sendTextReply(
1065
+ nextMessage,
1066
+ `Warning: failed to register bot commands menu: ${errorMessage}`,
1067
+ );
1068
+ }
1069
+ if (nextMessage.from?.id !== undefined) {
1070
+ await pairTelegramUserIfNeeded(nextMessage.from.id, {
1071
+ allowedUserId: deps.getAllowedUserId(),
1072
+ ctx: undefined,
1073
+ setAllowedUserId: deps.setAllowedUserId,
1074
+ persistConfig: deps.persistConfig,
1075
+ updateStatus: updateStatusFor(commandCtx),
1076
+ });
1077
+ }
1078
+ await deps.showStatus(nextMessage, commandCtx);
804
1079
  },
805
1080
  },
806
1081
  );
package/lib/config.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Telegram bridge config and pairing helpers
3
+ * Zones: telegram config, pairing, filesystem
3
4
  * Owns persisted bot/session pairing state, local config storage, authorization policy, and first-user pairing side effects
4
5
  */
5
6
 
6
7
  import { existsSync } from "node:fs";
7
- import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
8
9
  import { homedir } from "node:os";
9
10
  import { join, resolve } from "node:path";
10
11
 
@@ -76,10 +77,13 @@ export async function writeTelegramConfig(
76
77
  config: TelegramConfig,
77
78
  ): Promise<void> {
78
79
  await mkdir(agentDir, { recursive: true });
79
- await writeFile(configPath, JSON.stringify(config, null, "\t") + "\n", {
80
+ const tempConfigPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
81
+ await writeFile(tempConfigPath, JSON.stringify(config, null, "\t") + "\n", {
80
82
  encoding: "utf8",
81
83
  mode: 0o600,
82
84
  });
85
+ await chmod(tempConfigPath, 0o600);
86
+ await rename(tempConfigPath, configPath);
83
87
  await chmod(configPath, 0o600);
84
88
  }
85
89
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Telegram inline-keyboard structural contracts
3
+ * Zones: telegram ui, shared structure
4
+ * Owns the shared Bot API reply-markup shape while feature domains own their button semantics
5
+ */
6
+
7
+ export interface TelegramInlineKeyboardButton {
8
+ text: string;
9
+ callback_data: string;
10
+ }
11
+
12
+ export interface TelegramInlineKeyboardMarkup {
13
+ inline_keyboard: TelegramInlineKeyboardButton[][];
14
+ }