@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/AGENTS.md +153 -0
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +148 -0
- package/README.md +40 -39
- package/docs/README.md +1 -0
- package/docs/architecture.md +36 -27
- package/docs/attachment-handlers.md +4 -6
- package/docs/callback-namespaces.md +36 -0
- package/docs/command-templates.md +53 -9
- package/docs/locks.md +4 -0
- package/docs/outbound-handlers.md +6 -5
- package/index.ts +60 -7
- package/lib/api.ts +1 -0
- package/lib/attachment-handlers.ts +21 -10
- package/lib/attachments.ts +1 -0
- package/lib/command-templates.ts +37 -3
- package/lib/commands.ts +363 -88
- package/lib/config.ts +6 -2
- package/lib/keyboard.ts +14 -0
- package/lib/lifecycle.ts +26 -0
- package/lib/locks.ts +3 -2
- package/lib/media.ts +1 -0
- package/lib/menu-model.ts +881 -0
- package/lib/menu-queue.ts +610 -0
- package/lib/menu-status.ts +226 -0
- package/lib/menu-thinking.ts +171 -0
- package/lib/menu.ts +143 -1019
- package/lib/model.ts +1 -0
- package/lib/outbound-handlers.ts +28 -19
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +1 -0
- package/lib/preview.ts +97 -50
- package/lib/prompt-templates.ts +150 -0
- package/lib/prompts.ts +2 -0
- package/lib/queue.ts +60 -15
- package/lib/rendering.ts +1 -0
- package/lib/replies.ts +90 -2
- package/lib/routing.ts +106 -14
- package/lib/runtime.ts +2 -0
- package/lib/setup.ts +1 -0
- package/lib/status.ts +18 -6
- package/lib/turns.ts +1 -0
- package/lib/updates.ts +55 -6
- package/package.json +5 -2
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
446
|
-
"
|
|
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, "&")
|
|
566
|
+
.replace(/</g, "<")
|
|
567
|
+
.replace(/>/g, ">");
|
|
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
|
-
|
|
467
|
-
|
|
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
|
|
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
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
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
|
-
|
|
795
|
-
await
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
sendTextReply
|
|
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
|
-
|
|
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
|
|
package/lib/keyboard.ts
ADDED
|
@@ -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
|
+
}
|