@llblab/pi-telegram 0.6.3 â 0.7.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.
- package/AGENTS.md +152 -0
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +142 -0
- package/README.md +39 -38
- package/docs/architecture.md +36 -27
- package/docs/attachment-handlers.md +4 -6
- package/docs/command-templates.md +53 -9
- package/docs/locks.md +4 -0
- package/docs/outbound-handlers.md +6 -5
- package/index.ts +59 -7
- package/lib/api.ts +1 -0
- package/lib/attachment-handlers.ts +16 -9
- package/lib/attachments.ts +1 -0
- package/lib/command-templates.ts +32 -3
- package/lib/commands.ts +367 -80
- 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 +608 -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 +26 -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 +1 -0
- package/lib/queue.ts +51 -15
- package/lib/rendering.ts +1 -0
- package/lib/replies.ts +86 -2
- package/lib/routing.ts +76 -14
- package/lib/runtime.ts +2 -0
- package/lib/setup.ts +1 -0
- package/lib/status.ts +15 -6
- package/lib/turns.ts +1 -0
- package/lib/updates.ts +36 -6
- package/package.json +4 -1
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,33 +22,123 @@ export interface TelegramBotCommandDefinition {
|
|
|
21
22
|
description: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export
|
|
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[] = [
|
|
25
65
|
{
|
|
26
66
|
command: "start",
|
|
27
|
-
description:
|
|
67
|
+
description: formatTelegramBotCommandDescription(
|
|
68
|
+
"start",
|
|
69
|
+
"Open menu / Pair bridge",
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
command: "compact",
|
|
74
|
+
description: formatTelegramBotCommandDescription(
|
|
75
|
+
"compact",
|
|
76
|
+
"Compact current session",
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
command: "next",
|
|
81
|
+
description: formatTelegramBotCommandDescription(
|
|
82
|
+
"next",
|
|
83
|
+
"Force next turn",
|
|
84
|
+
),
|
|
28
85
|
},
|
|
29
86
|
{
|
|
30
|
-
command: "
|
|
31
|
-
description:
|
|
87
|
+
command: "continue",
|
|
88
|
+
description: formatTelegramBotCommandDescription(
|
|
89
|
+
"continue",
|
|
90
|
+
"Queue continue prompt",
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
command: "abort",
|
|
95
|
+
description: formatTelegramBotCommandDescription("abort", "Abort Ī"),
|
|
32
96
|
},
|
|
33
|
-
{ command: "model", description: "Open the interactive model selector" },
|
|
34
|
-
{ command: "compact", description: "Compact the current pi session" },
|
|
35
97
|
{
|
|
36
98
|
command: "stop",
|
|
37
|
-
description:
|
|
99
|
+
description: formatTelegramBotCommandDescription(
|
|
100
|
+
"stop",
|
|
101
|
+
"Abort Ī & Clear queue",
|
|
102
|
+
),
|
|
38
103
|
},
|
|
39
104
|
];
|
|
40
105
|
|
|
106
|
+
export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS;
|
|
107
|
+
|
|
108
|
+
const TELEGRAM_MAX_BOT_COMMANDS = 100;
|
|
109
|
+
const TELEGRAM_BOT_COMMAND_DESCRIPTION_LIMIT = 256;
|
|
110
|
+
|
|
111
|
+
function truncateTelegramBotCommandDescription(description: string): string {
|
|
112
|
+
if (description.length <= TELEGRAM_BOT_COMMAND_DESCRIPTION_LIMIT) return description;
|
|
113
|
+
return `${description.slice(0, TELEGRAM_BOT_COMMAND_DESCRIPTION_LIMIT - 1)}âĻ`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildTelegramBotCommands(
|
|
117
|
+
promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
|
|
118
|
+
): TelegramBotCommandDefinition[] {
|
|
119
|
+
const remainingSlots = TELEGRAM_MAX_BOT_COMMANDS - TELEGRAM_BUILTIN_BOT_COMMANDS.length;
|
|
120
|
+
const templateCommands = promptTemplates.slice(0, remainingSlots).map((template) => ({
|
|
121
|
+
command: template.command,
|
|
122
|
+
description: truncateTelegramBotCommandDescription(
|
|
123
|
+
`đ§Š ${template.description?.trim() || "Prompt template"}`,
|
|
124
|
+
),
|
|
125
|
+
}));
|
|
126
|
+
return [...TELEGRAM_BUILTIN_BOT_COMMANDS, ...templateCommands];
|
|
127
|
+
}
|
|
128
|
+
|
|
41
129
|
export interface TelegramBotCommandRegistrationDeps {
|
|
42
130
|
setMyCommands: (
|
|
43
131
|
commands: readonly TelegramBotCommandDefinition[],
|
|
44
132
|
) => Promise<unknown>;
|
|
133
|
+
getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
|
|
45
134
|
}
|
|
46
135
|
|
|
47
136
|
export async function registerTelegramBotCommands(
|
|
48
137
|
deps: TelegramBotCommandRegistrationDeps,
|
|
49
138
|
): Promise<void> {
|
|
50
|
-
await deps.setMyCommands(
|
|
139
|
+
await deps.setMyCommands(
|
|
140
|
+
buildTelegramBotCommands(deps.getPromptTemplateCommands?.()),
|
|
141
|
+
);
|
|
51
142
|
}
|
|
52
143
|
|
|
53
144
|
export function createTelegramBotCommandRegistrar(
|
|
@@ -95,7 +186,7 @@ function formatTelegramTakeoverPrompt(
|
|
|
95
186
|
const action = theme.fg("warning", "move singleton lock here?");
|
|
96
187
|
const from = theme.fg("muted", "from:");
|
|
97
188
|
const to = theme.fg("muted", "to:");
|
|
98
|
-
const source = owner ?? "another
|
|
189
|
+
const source = owner ?? "another Ī instance";
|
|
99
190
|
return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
|
|
100
191
|
}
|
|
101
192
|
|
|
@@ -116,7 +207,7 @@ export function registerTelegramBridgeCommands(
|
|
|
116
207
|
},
|
|
117
208
|
});
|
|
118
209
|
pi.registerCommand("telegram-connect", {
|
|
119
|
-
description: "Start the Telegram bridge in this
|
|
210
|
+
description: "Start the Telegram bridge in this Ī session",
|
|
120
211
|
handler: async (_args, ctx) => {
|
|
121
212
|
await deps.reloadConfig();
|
|
122
213
|
if (!deps.hasBotToken()) {
|
|
@@ -143,7 +234,7 @@ export function registerTelegramBridgeCommands(
|
|
|
143
234
|
},
|
|
144
235
|
});
|
|
145
236
|
pi.registerCommand("telegram-disconnect", {
|
|
146
|
-
description: "Stop the Telegram bridge in this
|
|
237
|
+
description: "Stop the Telegram bridge in this Ī session",
|
|
147
238
|
handler: async (_args, ctx) => {
|
|
148
239
|
const message = await deps.stopPolling();
|
|
149
240
|
if (message) ctx.ui.notify(message, "info");
|
|
@@ -152,12 +243,44 @@ export function registerTelegramBridgeCommands(
|
|
|
152
243
|
});
|
|
153
244
|
}
|
|
154
245
|
|
|
246
|
+
export const TELEGRAM_RESERVED_COMMAND_NAMES = [
|
|
247
|
+
"stop",
|
|
248
|
+
"abort",
|
|
249
|
+
"next",
|
|
250
|
+
"continue",
|
|
251
|
+
"status",
|
|
252
|
+
"queue",
|
|
253
|
+
"compact",
|
|
254
|
+
"model",
|
|
255
|
+
"thinking",
|
|
256
|
+
"help",
|
|
257
|
+
"start",
|
|
258
|
+
] as const;
|
|
259
|
+
|
|
260
|
+
export type TelegramReservedCommandName =
|
|
261
|
+
(typeof TELEGRAM_RESERVED_COMMAND_NAMES)[number];
|
|
262
|
+
|
|
263
|
+
const TELEGRAM_RESERVED_COMMAND_NAME_SET = new Set<string>(
|
|
264
|
+
TELEGRAM_RESERVED_COMMAND_NAMES,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
export function isTelegramReservedCommandName(
|
|
268
|
+
commandName: string | undefined,
|
|
269
|
+
): commandName is TelegramReservedCommandName {
|
|
270
|
+
return commandName !== undefined && TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName);
|
|
271
|
+
}
|
|
272
|
+
|
|
155
273
|
export type TelegramCommandAction =
|
|
156
274
|
| { kind: "ignore"; executionMode: "ignored" }
|
|
157
275
|
| { kind: "stop"; executionMode: "immediate" }
|
|
276
|
+
| { kind: "abort"; executionMode: "immediate" }
|
|
277
|
+
| { kind: "next"; executionMode: "immediate" }
|
|
278
|
+
| { kind: "continue"; executionMode: "immediate" }
|
|
279
|
+
| { kind: "queue"; executionMode: "immediate" }
|
|
158
280
|
| { kind: "compact"; executionMode: "immediate" }
|
|
159
281
|
| { kind: "status"; executionMode: "immediate" }
|
|
160
282
|
| { kind: "model"; executionMode: "immediate" }
|
|
283
|
+
| { kind: "thinking"; executionMode: "immediate" }
|
|
161
284
|
| {
|
|
162
285
|
kind: "help";
|
|
163
286
|
commandName: "help" | "start";
|
|
@@ -168,9 +291,14 @@ export type TelegramCommandExecutionMode = "ignored" | "immediate";
|
|
|
168
291
|
|
|
169
292
|
export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
170
293
|
handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
294
|
+
handleAbort: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
295
|
+
handleNext: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
296
|
+
handleContinue: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
297
|
+
handleQueue: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
171
298
|
handleCompact: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
172
299
|
handleStatus: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
173
300
|
handleModel: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
301
|
+
handleThinking: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
174
302
|
handleHelp: (
|
|
175
303
|
message: TMessage,
|
|
176
304
|
commandName: "help" | "start",
|
|
@@ -213,16 +341,6 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
|
|
|
213
341
|
sendTextReply: (text: string) => Promise<void>;
|
|
214
342
|
}
|
|
215
343
|
|
|
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
344
|
export type TelegramControlCommandType =
|
|
227
345
|
PendingTelegramControlItem<unknown>["controlType"];
|
|
228
346
|
|
|
@@ -401,6 +519,11 @@ export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
|
|
|
401
519
|
message: TMessage,
|
|
402
520
|
ctx: TContext,
|
|
403
521
|
) => Promise<boolean>;
|
|
522
|
+
expandPromptTemplateCommand?: (
|
|
523
|
+
commandName: string,
|
|
524
|
+
args: string,
|
|
525
|
+
) => string | undefined;
|
|
526
|
+
replaceMessageText: (message: TMessage, text: string) => TMessage;
|
|
404
527
|
enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
405
528
|
}
|
|
406
529
|
|
|
@@ -422,6 +545,7 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
422
545
|
setCompactionInProgress: (inProgress: boolean) => void;
|
|
423
546
|
updateStatus: (ctx: TContext) => void;
|
|
424
547
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
548
|
+
enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
425
549
|
compact: (
|
|
426
550
|
ctx: TContext,
|
|
427
551
|
callbacks: { onComplete: () => void; onError: (error: unknown) => void },
|
|
@@ -435,15 +559,63 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
435
559
|
) => void;
|
|
436
560
|
showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
437
561
|
openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
562
|
+
openThinkingMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
563
|
+
openQueueMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
438
564
|
getAllowedUserId: () => number | undefined;
|
|
439
565
|
setAllowedUserId: (userId: number) => void;
|
|
440
566
|
registerBotCommands: () => Promise<void>;
|
|
567
|
+
getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
|
|
441
568
|
persistConfig: () => Promise<void>;
|
|
442
569
|
sendTextReply: (message: TMessage, text: string) => Promise<void>;
|
|
443
570
|
}
|
|
444
571
|
|
|
445
|
-
export const
|
|
446
|
-
"
|
|
572
|
+
export const TELEGRAM_APP_MENU_INTRO_HTML = [
|
|
573
|
+
"<b>Ī Telegram bridge</b>",
|
|
574
|
+
"",
|
|
575
|
+
`${formatTelegramCommandEmojiPrefix("start")}/start â Open menu / Pair bridge`,
|
|
576
|
+
`${formatTelegramCommandEmojiPrefix("compact")}/compact â Compact current session`,
|
|
577
|
+
`${formatTelegramCommandEmojiPrefix("next")}/next â Force next turn`,
|
|
578
|
+
`${formatTelegramCommandEmojiPrefix("continue")}/continue â Queue continue prompt`,
|
|
579
|
+
`${formatTelegramCommandEmojiPrefix("abort")}/abort â Abort Ī`,
|
|
580
|
+
`${formatTelegramCommandEmojiPrefix("stop")}/stop â Abort Ī & Clear queue`,
|
|
581
|
+
].join("\n");
|
|
582
|
+
|
|
583
|
+
function escapeTelegramCommandMenuHtml(text: string): string {
|
|
584
|
+
return text
|
|
585
|
+
.replace(/&/g, "&")
|
|
586
|
+
.replace(/</g, "<")
|
|
587
|
+
.replace(/>/g, ">");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function buildTelegramPromptTemplateMenuHtml(
|
|
591
|
+
promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
|
|
592
|
+
): string {
|
|
593
|
+
if (promptTemplates.length === 0) return "";
|
|
594
|
+
return promptTemplates
|
|
595
|
+
.map((template) => `đ§Š /${escapeTelegramCommandMenuHtml(template.command)}`)
|
|
596
|
+
.join("\n");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function buildTelegramAppMenuHtml(
|
|
600
|
+
statusHtml: string,
|
|
601
|
+
promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
|
|
602
|
+
): string {
|
|
603
|
+
const promptTemplateHtml = buildTelegramPromptTemplateMenuHtml(promptTemplates);
|
|
604
|
+
if (!promptTemplateHtml) return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${statusHtml}`;
|
|
605
|
+
return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function createTelegramAppMenuHtmlBuilder<TContext>(deps: {
|
|
609
|
+
buildStatusHtml: (ctx: TContext) => string;
|
|
610
|
+
getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
|
|
611
|
+
}): (ctx: TContext) => string {
|
|
612
|
+
return function buildTelegramAppMenuHtmlForContext(ctx) {
|
|
613
|
+
return buildTelegramAppMenuHtml(
|
|
614
|
+
deps.buildStatusHtml(ctx),
|
|
615
|
+
deps.getPromptTemplateCommands?.(),
|
|
616
|
+
);
|
|
617
|
+
};
|
|
618
|
+
}
|
|
447
619
|
|
|
448
620
|
function getTelegramCommandErrorMessage(error: unknown): string {
|
|
449
621
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -460,24 +632,27 @@ export function parseTelegramCommand(
|
|
|
460
632
|
return { name, args: tail.join(" ").trim() };
|
|
461
633
|
}
|
|
462
634
|
|
|
635
|
+
export const TELEGRAM_COMMAND_ACTIONS = {
|
|
636
|
+
stop: { kind: "stop", executionMode: "immediate" },
|
|
637
|
+
abort: { kind: "abort", executionMode: "immediate" },
|
|
638
|
+
next: { kind: "next", executionMode: "immediate" },
|
|
639
|
+
continue: { kind: "continue", executionMode: "immediate" },
|
|
640
|
+
status: { kind: "status", executionMode: "immediate" },
|
|
641
|
+
queue: { kind: "queue", executionMode: "immediate" },
|
|
642
|
+
compact: { kind: "compact", executionMode: "immediate" },
|
|
643
|
+
model: { kind: "model", executionMode: "immediate" },
|
|
644
|
+
thinking: { kind: "thinking", executionMode: "immediate" },
|
|
645
|
+
help: { kind: "help", commandName: "help", executionMode: "immediate" },
|
|
646
|
+
start: { kind: "help", commandName: "start", executionMode: "immediate" },
|
|
647
|
+
} as const satisfies Record<TelegramReservedCommandName, TelegramCommandAction>;
|
|
648
|
+
|
|
463
649
|
export function buildTelegramCommandAction(
|
|
464
650
|
commandName: string | undefined,
|
|
465
651
|
): 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" };
|
|
652
|
+
if (!isTelegramReservedCommandName(commandName)) {
|
|
653
|
+
return { kind: "ignore", executionMode: "ignored" };
|
|
480
654
|
}
|
|
655
|
+
return TELEGRAM_COMMAND_ACTIONS[commandName];
|
|
481
656
|
}
|
|
482
657
|
|
|
483
658
|
export function getTelegramCommandExecutionMode(
|
|
@@ -514,6 +689,69 @@ export async function handleTelegramStopCommand(
|
|
|
514
689
|
await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`);
|
|
515
690
|
}
|
|
516
691
|
|
|
692
|
+
export async function handleTelegramAbortCommand(deps: {
|
|
693
|
+
hasAbortHandler: () => boolean;
|
|
694
|
+
clearPendingModelSwitch: () => void;
|
|
695
|
+
abortCurrentTurn: () => void;
|
|
696
|
+
setPreserveForIdle: () => void;
|
|
697
|
+
updateStatus: () => void;
|
|
698
|
+
sendTextReply: (text: string) => Promise<void>;
|
|
699
|
+
}): Promise<void> {
|
|
700
|
+
deps.clearPendingModelSwitch();
|
|
701
|
+
if (!deps.hasAbortHandler()) {
|
|
702
|
+
await deps.sendTextReply("No active turn.");
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
deps.setPreserveForIdle();
|
|
706
|
+
deps.abortCurrentTurn();
|
|
707
|
+
deps.updateStatus();
|
|
708
|
+
await deps.sendTextReply("Aborted current turn.");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export async function handleTelegramNextCommand(deps: {
|
|
712
|
+
hasAbortHandler: () => boolean;
|
|
713
|
+
isIdle: () => boolean;
|
|
714
|
+
hasQueuedItems: () => boolean;
|
|
715
|
+
clearPendingModelSwitch: () => void;
|
|
716
|
+
abortCurrentTurn: () => void;
|
|
717
|
+
dispatchNextQueuedTurn: () => void;
|
|
718
|
+
setPreserveForDispatch: () => void;
|
|
719
|
+
updateStatus: () => void;
|
|
720
|
+
sendTextReply: (text: string) => Promise<void>;
|
|
721
|
+
}): Promise<void> {
|
|
722
|
+
deps.clearPendingModelSwitch();
|
|
723
|
+
if (!deps.hasQueuedItems()) {
|
|
724
|
+
await deps.sendTextReply("<b>Queue is empty.</b>");
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
728
|
+
deps.setPreserveForDispatch();
|
|
729
|
+
deps.abortCurrentTurn();
|
|
730
|
+
deps.updateStatus();
|
|
731
|
+
await deps.sendTextReply(
|
|
732
|
+
"Aborted current turn. Dispatching next queued turn.",
|
|
733
|
+
);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (!deps.isIdle()) {
|
|
737
|
+
await deps.sendTextReply("Ī is busy. Send /abort or /stop first.");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
deps.dispatchNextQueuedTurn();
|
|
741
|
+
deps.updateStatus();
|
|
742
|
+
await deps.sendTextReply("Dispatching next queued turn.");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export async function handleTelegramContinueCommand<TMessage, TContext>(
|
|
746
|
+
message: TMessage,
|
|
747
|
+
ctx: TContext,
|
|
748
|
+
deps: {
|
|
749
|
+
enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
750
|
+
},
|
|
751
|
+
): Promise<void> {
|
|
752
|
+
await deps.enqueueContinueTurn(message, ctx);
|
|
753
|
+
}
|
|
754
|
+
|
|
517
755
|
export async function handleTelegramCompactCommand(
|
|
518
756
|
deps: TelegramCompactCommandDeps,
|
|
519
757
|
): Promise<void> {
|
|
@@ -526,7 +764,7 @@ export async function handleTelegramCompactCommand(
|
|
|
526
764
|
deps.isCompactionInProgress()
|
|
527
765
|
) {
|
|
528
766
|
await deps.sendTextReply(
|
|
529
|
-
"Cannot compact while
|
|
767
|
+
"Cannot compact while Ī or the Telegram queue is busy. Wait for queued turns to finish or send /stop first.",
|
|
530
768
|
);
|
|
531
769
|
return;
|
|
532
770
|
}
|
|
@@ -560,30 +798,6 @@ export async function handleTelegramCompactCommand(
|
|
|
560
798
|
await deps.sendTextReply("Compaction started.");
|
|
561
799
|
}
|
|
562
800
|
|
|
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
801
|
export async function handleTelegramStatusCommand<TContext>(deps: {
|
|
588
802
|
ctx: TContext;
|
|
589
803
|
showStatus: (ctx: TContext) => Promise<void>;
|
|
@@ -610,6 +824,18 @@ export async function executeTelegramCommandAction<TMessage, TContext>(
|
|
|
610
824
|
case "stop":
|
|
611
825
|
await deps.handleStop(message, ctx);
|
|
612
826
|
return true;
|
|
827
|
+
case "abort":
|
|
828
|
+
await deps.handleAbort(message, ctx);
|
|
829
|
+
return true;
|
|
830
|
+
case "next":
|
|
831
|
+
await deps.handleNext(message, ctx);
|
|
832
|
+
return true;
|
|
833
|
+
case "continue":
|
|
834
|
+
await deps.handleContinue(message, ctx);
|
|
835
|
+
return true;
|
|
836
|
+
case "queue":
|
|
837
|
+
await deps.handleQueue(message, ctx);
|
|
838
|
+
return true;
|
|
613
839
|
case "compact":
|
|
614
840
|
await deps.handleCompact(message, ctx);
|
|
615
841
|
return true;
|
|
@@ -619,6 +845,9 @@ export async function executeTelegramCommandAction<TMessage, TContext>(
|
|
|
619
845
|
case "model":
|
|
620
846
|
await deps.handleModel(message, ctx);
|
|
621
847
|
return true;
|
|
848
|
+
case "thinking":
|
|
849
|
+
await deps.handleThinking(message, ctx);
|
|
850
|
+
return true;
|
|
622
851
|
case "help":
|
|
623
852
|
await deps.handleHelp(message, action.commandName, ctx);
|
|
624
853
|
return true;
|
|
@@ -683,14 +912,18 @@ export function createTelegramCommandHandlerTargetRuntime<
|
|
|
683
912
|
setCompactionInProgress: deps.setCompactionInProgress,
|
|
684
913
|
updateStatus: deps.updateStatus,
|
|
685
914
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
915
|
+
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
686
916
|
compact: deps.compact,
|
|
687
917
|
enqueueControlItem: commandTargetRuntime.enqueueControlItem,
|
|
688
918
|
showStatus: commandTargetRuntime.showStatus,
|
|
689
919
|
openModelMenu: commandTargetRuntime.openModelMenu,
|
|
920
|
+
openThinkingMenu: deps.openThinkingMenu,
|
|
921
|
+
openQueueMenu: deps.openQueueMenu,
|
|
690
922
|
getAllowedUserId: deps.getAllowedUserId,
|
|
691
923
|
setAllowedUserId: deps.setAllowedUserId,
|
|
692
924
|
registerBotCommands: createTelegramBotCommandRegistrar({
|
|
693
925
|
setMyCommands: deps.setMyCommands,
|
|
926
|
+
getPromptTemplateCommands: deps.getPromptTemplateCommands,
|
|
694
927
|
}),
|
|
695
928
|
persistConfig: deps.persistConfig,
|
|
696
929
|
sendTextReply: commandTargetRuntime.sendTextReply,
|
|
@@ -721,11 +954,22 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
|
|
|
721
954
|
): Promise<void> => {
|
|
722
955
|
const firstMessage = messages[0];
|
|
723
956
|
if (!firstMessage) return;
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
)?.name;
|
|
727
|
-
const handled = await deps.handleCommand(commandName, firstMessage, ctx);
|
|
957
|
+
const command = parseTelegramCommand(deps.extractRawText(messages));
|
|
958
|
+
const handled = await deps.handleCommand(command?.name, firstMessage, ctx);
|
|
728
959
|
if (handled) return;
|
|
960
|
+
if (command?.name && deps.expandPromptTemplateCommand) {
|
|
961
|
+
const expanded = deps.expandPromptTemplateCommand(
|
|
962
|
+
command.name,
|
|
963
|
+
command.args,
|
|
964
|
+
);
|
|
965
|
+
if (expanded !== undefined) {
|
|
966
|
+
await deps.enqueueTurn(
|
|
967
|
+
[deps.replaceMessageText(firstMessage, expanded), ...messages.slice(1)],
|
|
968
|
+
ctx,
|
|
969
|
+
);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
729
973
|
await deps.enqueueTurn(messages, ctx);
|
|
730
974
|
},
|
|
731
975
|
};
|
|
@@ -761,6 +1005,39 @@ async function handleTelegramCommandRuntime<
|
|
|
761
1005
|
sendTextReply: sendReplyFor(nextMessage),
|
|
762
1006
|
});
|
|
763
1007
|
},
|
|
1008
|
+
handleAbort: async (nextMessage, commandCtx) => {
|
|
1009
|
+
await handleTelegramAbortCommand({
|
|
1010
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
1011
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
1012
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
1013
|
+
setPreserveForIdle: () => deps.setPreserveQueuedTurnsAsHistory(true),
|
|
1014
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
1015
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
1016
|
+
});
|
|
1017
|
+
},
|
|
1018
|
+
handleNext: async (nextMessage, commandCtx) => {
|
|
1019
|
+
await handleTelegramNextCommand({
|
|
1020
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
1021
|
+
isIdle: () => deps.isIdle(commandCtx),
|
|
1022
|
+
hasQueuedItems: deps.hasQueuedTelegramItems,
|
|
1023
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
1024
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
1025
|
+
dispatchNextQueuedTurn: () =>
|
|
1026
|
+
deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
1027
|
+
setPreserveForDispatch: () =>
|
|
1028
|
+
deps.setPreserveQueuedTurnsAsHistory(false),
|
|
1029
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
1030
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
1031
|
+
});
|
|
1032
|
+
},
|
|
1033
|
+
handleContinue: async (nextMessage, commandCtx) => {
|
|
1034
|
+
await handleTelegramContinueCommand(nextMessage, commandCtx, {
|
|
1035
|
+
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
handleQueue: async (nextMessage, commandCtx) => {
|
|
1039
|
+
await deps.openQueueMenu(nextMessage, commandCtx);
|
|
1040
|
+
},
|
|
764
1041
|
handleCompact: async (nextMessage, commandCtx) => {
|
|
765
1042
|
await handleTelegramCompactCommand({
|
|
766
1043
|
isIdle: () => deps.isIdle(commandCtx),
|
|
@@ -779,10 +1056,7 @@ async function handleTelegramCommandRuntime<
|
|
|
779
1056
|
});
|
|
780
1057
|
},
|
|
781
1058
|
handleStatus: async (nextMessage, commandCtx) => {
|
|
782
|
-
await
|
|
783
|
-
ctx: commandCtx,
|
|
784
|
-
showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx),
|
|
785
|
-
});
|
|
1059
|
+
await deps.showStatus(nextMessage, commandCtx);
|
|
786
1060
|
},
|
|
787
1061
|
handleModel: async (nextMessage, commandCtx) => {
|
|
788
1062
|
await handleTelegramModelCommand<TContext>({
|
|
@@ -791,16 +1065,29 @@ async function handleTelegramCommandRuntime<
|
|
|
791
1065
|
deps.openModelMenu(nextMessage, controlCtx),
|
|
792
1066
|
});
|
|
793
1067
|
},
|
|
794
|
-
|
|
795
|
-
await
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
sendTextReply
|
|
803
|
-
|
|
1068
|
+
handleThinking: async (nextMessage, commandCtx) => {
|
|
1069
|
+
await deps.openThinkingMenu(nextMessage, commandCtx);
|
|
1070
|
+
},
|
|
1071
|
+
handleHelp: async (nextMessage, _nextCommandName, commandCtx) => {
|
|
1072
|
+
try {
|
|
1073
|
+
await deps.registerBotCommands();
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
const errorMessage = getTelegramCommandErrorMessage(error);
|
|
1076
|
+
await deps.sendTextReply(
|
|
1077
|
+
nextMessage,
|
|
1078
|
+
`Warning: failed to register bot commands menu: ${errorMessage}`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (nextMessage.from?.id !== undefined) {
|
|
1082
|
+
await pairTelegramUserIfNeeded(nextMessage.from.id, {
|
|
1083
|
+
allowedUserId: deps.getAllowedUserId(),
|
|
1084
|
+
ctx: undefined,
|
|
1085
|
+
setAllowedUserId: deps.setAllowedUserId,
|
|
1086
|
+
persistConfig: deps.persistConfig,
|
|
1087
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
await deps.showStatus(nextMessage, commandCtx);
|
|
804
1091
|
},
|
|
805
1092
|
},
|
|
806
1093
|
);
|
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
|
+
}
|