@llblab/pi-telegram 0.4.0 → 0.5.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/README.md +22 -13
- package/docs/README.md +2 -0
- package/docs/architecture.md +42 -45
- package/docs/attachment-handlers.md +60 -0
- package/docs/command-templates.md +75 -0
- package/index.ts +9 -7
- package/lib/attachments.ts +70 -2
- package/lib/commands.ts +144 -58
- package/lib/handlers.ts +118 -192
- package/lib/lifecycle.ts +140 -0
- package/lib/locks.ts +43 -13
- package/lib/menu.ts +0 -4
- package/lib/prompts.ts +44 -0
- package/lib/queue.ts +21 -6
- package/lib/routing.ts +5 -2
- package/lib/runtime.ts +9 -6
- package/lib/setup.ts +5 -1
- package/lib/status.ts +29 -4
- package/lib/turns.ts +12 -7
- package/package.json +1 -1
- package/lib/registration.ts +0 -346
package/lib/commands.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram command routing helpers
|
|
3
|
-
* Owns slash-command normalization
|
|
3
|
+
* Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { pairTelegramUserIfNeeded } from "./config.ts";
|
|
7
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
|
|
7
8
|
import {
|
|
8
9
|
createTelegramControlItemBuilder,
|
|
9
10
|
createTelegramControlQueueController,
|
|
@@ -31,7 +32,10 @@ export const TELEGRAM_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [
|
|
|
31
32
|
},
|
|
32
33
|
{ command: "model", description: "Open the interactive model selector" },
|
|
33
34
|
{ command: "compact", description: "Compact the current pi session" },
|
|
34
|
-
{
|
|
35
|
+
{
|
|
36
|
+
command: "stop",
|
|
37
|
+
description: "Abort the current pi task and clear queued turns",
|
|
38
|
+
},
|
|
35
39
|
];
|
|
36
40
|
|
|
37
41
|
export interface TelegramBotCommandRegistrationDeps {
|
|
@@ -52,22 +56,115 @@ export function createTelegramBotCommandRegistrar(
|
|
|
52
56
|
return () => registerTelegramBotCommands(deps);
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
export interface TelegramBridgeCommandStartPollingOptions {
|
|
60
|
+
force?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TelegramBridgeCommandStartPollingResult {
|
|
64
|
+
ok: boolean;
|
|
65
|
+
message?: string;
|
|
66
|
+
canTakeover?: boolean;
|
|
67
|
+
owner?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TelegramBridgeCommandRegistrationDeps {
|
|
71
|
+
promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
72
|
+
getStatusLines: () => string[];
|
|
73
|
+
reloadConfig: () => Promise<void>;
|
|
74
|
+
hasBotToken: () => boolean;
|
|
75
|
+
startPolling: (
|
|
76
|
+
ctx: ExtensionCommandContext,
|
|
77
|
+
options?: TelegramBridgeCommandStartPollingOptions,
|
|
78
|
+
) =>
|
|
79
|
+
| void
|
|
80
|
+
| Promise<void | TelegramBridgeCommandStartPollingResult>
|
|
81
|
+
| TelegramBridgeCommandStartPollingResult;
|
|
82
|
+
stopPolling: () => Promise<void | string>;
|
|
83
|
+
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
|
|
87
|
+
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatTelegramTakeoverPrompt(
|
|
91
|
+
ctx: ExtensionCommandContext,
|
|
92
|
+
owner?: string,
|
|
93
|
+
): string {
|
|
94
|
+
const theme = ctx.ui.theme;
|
|
95
|
+
const action = theme.fg("warning", "move singleton lock here?");
|
|
96
|
+
const from = theme.fg("muted", "from:");
|
|
97
|
+
const to = theme.fg("muted", "to:");
|
|
98
|
+
const source = owner ?? "another pi instance";
|
|
99
|
+
return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function registerTelegramBridgeCommands(
|
|
103
|
+
pi: ExtensionAPI,
|
|
104
|
+
deps: TelegramBridgeCommandRegistrationDeps,
|
|
105
|
+
): void {
|
|
106
|
+
pi.registerCommand("telegram-setup", {
|
|
107
|
+
description: "Configure Telegram bot token",
|
|
108
|
+
handler: async (_args, ctx) => {
|
|
109
|
+
await deps.promptForConfig(ctx);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
pi.registerCommand("telegram-status", {
|
|
113
|
+
description: "Show Telegram bridge status",
|
|
114
|
+
handler: async (_args, ctx) => {
|
|
115
|
+
ctx.ui.notify(deps.getStatusLines().join("\n"), "info");
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
pi.registerCommand("telegram-connect", {
|
|
119
|
+
description: "Start the Telegram bridge in this pi session",
|
|
120
|
+
handler: async (_args, ctx) => {
|
|
121
|
+
await deps.reloadConfig();
|
|
122
|
+
if (!deps.hasBotToken()) {
|
|
123
|
+
await deps.promptForConfig(ctx);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let result = await deps.startPolling(ctx);
|
|
127
|
+
if (result && !result.ok && result.canTakeover) {
|
|
128
|
+
const confirmed = await ctx.ui.confirm(
|
|
129
|
+
formatTelegramTakeoverTitle(ctx),
|
|
130
|
+
formatTelegramTakeoverPrompt(ctx, result.owner),
|
|
131
|
+
);
|
|
132
|
+
if (!confirmed) {
|
|
133
|
+
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
134
|
+
deps.updateStatus(ctx);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
result = await deps.startPolling(ctx, { force: true });
|
|
138
|
+
}
|
|
139
|
+
if (result?.message) {
|
|
140
|
+
ctx.ui.notify(result.message, result.ok ? "info" : "warning");
|
|
141
|
+
}
|
|
142
|
+
deps.updateStatus(ctx);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
pi.registerCommand("telegram-disconnect", {
|
|
146
|
+
description: "Stop the Telegram bridge in this pi session",
|
|
147
|
+
handler: async (_args, ctx) => {
|
|
148
|
+
const message = await deps.stopPolling();
|
|
149
|
+
if (message) ctx.ui.notify(message, "info");
|
|
150
|
+
deps.updateStatus(ctx);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
55
155
|
export type TelegramCommandAction =
|
|
56
156
|
| { kind: "ignore"; executionMode: "ignored" }
|
|
57
157
|
| { kind: "stop"; executionMode: "immediate" }
|
|
58
158
|
| { kind: "compact"; executionMode: "immediate" }
|
|
59
|
-
| { kind: "status"; executionMode: "
|
|
60
|
-
| { kind: "model"; executionMode: "
|
|
159
|
+
| { kind: "status"; executionMode: "immediate" }
|
|
160
|
+
| { kind: "model"; executionMode: "immediate" }
|
|
61
161
|
| {
|
|
62
162
|
kind: "help";
|
|
63
163
|
commandName: "help" | "start";
|
|
64
164
|
executionMode: "immediate";
|
|
65
165
|
};
|
|
66
166
|
|
|
67
|
-
export type TelegramCommandExecutionMode =
|
|
68
|
-
| "ignored"
|
|
69
|
-
| "immediate"
|
|
70
|
-
| "control-queue";
|
|
167
|
+
export type TelegramCommandExecutionMode = "ignored" | "immediate";
|
|
71
168
|
|
|
72
169
|
export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
73
170
|
handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
@@ -84,7 +181,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
|
84
181
|
export interface TelegramStopCommandDeps {
|
|
85
182
|
hasAbortHandler: () => boolean;
|
|
86
183
|
clearPendingModelSwitch: () => void;
|
|
87
|
-
|
|
184
|
+
clearQueuedTelegramItems: () => number;
|
|
88
185
|
setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
|
|
89
186
|
abortCurrentTurn: () => void;
|
|
90
187
|
updateStatus: () => void;
|
|
@@ -99,8 +196,7 @@ export interface TelegramRuntimeEventRecorderPort {
|
|
|
99
196
|
) => void;
|
|
100
197
|
}
|
|
101
198
|
|
|
102
|
-
export interface TelegramCompactCommandDeps
|
|
103
|
-
extends TelegramRuntimeEventRecorderPort {
|
|
199
|
+
export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort {
|
|
104
200
|
isIdle: () => boolean;
|
|
105
201
|
hasPendingMessages: () => boolean;
|
|
106
202
|
hasActiveTelegramTurn: () => boolean;
|
|
@@ -130,14 +226,6 @@ export interface TelegramHelpCommandDeps {
|
|
|
130
226
|
export type TelegramControlCommandType =
|
|
131
227
|
PendingTelegramControlItem<unknown>["controlType"];
|
|
132
228
|
|
|
133
|
-
export interface TelegramQueuedControlCommandDeps<TContext> {
|
|
134
|
-
enqueueControlItem: (
|
|
135
|
-
controlType: TelegramControlCommandType,
|
|
136
|
-
statusSummary: string,
|
|
137
|
-
execute: (ctx: TContext) => Promise<void>,
|
|
138
|
-
) => void;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
229
|
export interface TelegramCommandRuntimeMessage {
|
|
142
230
|
chat: { id: number };
|
|
143
231
|
message_id: number;
|
|
@@ -323,6 +411,7 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
323
411
|
hasAbortHandler: () => boolean;
|
|
324
412
|
clearPendingModelSwitch: () => void;
|
|
325
413
|
hasQueuedTelegramItems: () => boolean;
|
|
414
|
+
clearQueuedTelegramItems: (ctx: TContext) => number;
|
|
326
415
|
setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
|
|
327
416
|
abortCurrentTurn: () => void;
|
|
328
417
|
isIdle: (ctx: TContext) => boolean;
|
|
@@ -354,7 +443,7 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
354
443
|
}
|
|
355
444
|
|
|
356
445
|
export const TELEGRAM_HELP_TEXT =
|
|
357
|
-
"Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop.";
|
|
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.";
|
|
358
447
|
|
|
359
448
|
function getTelegramCommandErrorMessage(error: unknown): string {
|
|
360
449
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -380,9 +469,9 @@ export function buildTelegramCommandAction(
|
|
|
380
469
|
case "compact":
|
|
381
470
|
return { kind: "compact", executionMode: "immediate" };
|
|
382
471
|
case "status":
|
|
383
|
-
return { kind: "status", executionMode: "
|
|
472
|
+
return { kind: "status", executionMode: "immediate" };
|
|
384
473
|
case "model":
|
|
385
|
-
return { kind: "model", executionMode: "
|
|
474
|
+
return { kind: "model", executionMode: "immediate" };
|
|
386
475
|
case "help":
|
|
387
476
|
case "start":
|
|
388
477
|
return { kind: "help", commandName, executionMode: "immediate" };
|
|
@@ -397,20 +486,32 @@ export function getTelegramCommandExecutionMode(
|
|
|
397
486
|
return action.executionMode;
|
|
398
487
|
}
|
|
399
488
|
|
|
489
|
+
function formatTelegramQueuedTurnCount(count: number): string {
|
|
490
|
+
return count === 1 ? "1 queued turn" : `${count} queued turns`;
|
|
491
|
+
}
|
|
492
|
+
|
|
400
493
|
export async function handleTelegramStopCommand(
|
|
401
494
|
deps: TelegramStopCommandDeps,
|
|
402
495
|
): Promise<void> {
|
|
496
|
+
deps.clearPendingModelSwitch();
|
|
497
|
+
const clearedCount = deps.clearQueuedTelegramItems();
|
|
498
|
+
deps.setPreserveQueuedTurnsAsHistory(false);
|
|
403
499
|
if (!deps.hasAbortHandler()) {
|
|
404
|
-
|
|
500
|
+
const clearedSuffix =
|
|
501
|
+
clearedCount > 0
|
|
502
|
+
? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
|
|
503
|
+
: "";
|
|
504
|
+
if (clearedCount > 0) deps.updateStatus();
|
|
505
|
+
await deps.sendTextReply(`No active turn.${clearedSuffix}`);
|
|
405
506
|
return;
|
|
406
507
|
}
|
|
407
|
-
deps.clearPendingModelSwitch();
|
|
408
|
-
if (deps.hasQueuedTelegramItems()) {
|
|
409
|
-
deps.setPreserveQueuedTurnsAsHistory(true);
|
|
410
|
-
}
|
|
411
508
|
deps.abortCurrentTurn();
|
|
412
509
|
deps.updateStatus();
|
|
413
|
-
|
|
510
|
+
const clearedSuffix =
|
|
511
|
+
clearedCount > 0
|
|
512
|
+
? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
|
|
513
|
+
: "";
|
|
514
|
+
await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`);
|
|
414
515
|
}
|
|
415
516
|
|
|
416
517
|
export async function handleTelegramCompactCommand(
|
|
@@ -483,20 +584,18 @@ export async function handleTelegramHelpCommand(
|
|
|
483
584
|
});
|
|
484
585
|
}
|
|
485
586
|
|
|
486
|
-
export async function handleTelegramStatusCommand<TContext>(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
491
|
-
deps.enqueueControlItem("status", "⚡ status", deps.showStatus);
|
|
587
|
+
export async function handleTelegramStatusCommand<TContext>(deps: {
|
|
588
|
+
ctx: TContext;
|
|
589
|
+
showStatus: (ctx: TContext) => Promise<void>;
|
|
590
|
+
}): Promise<void> {
|
|
591
|
+
await deps.showStatus(deps.ctx);
|
|
492
592
|
}
|
|
493
593
|
|
|
494
|
-
export async function handleTelegramModelCommand<TContext>(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
499
|
-
deps.enqueueControlItem("model", "⚡ model", deps.openModelMenu);
|
|
594
|
+
export async function handleTelegramModelCommand<TContext>(deps: {
|
|
595
|
+
ctx: TContext;
|
|
596
|
+
openModelMenu: (ctx: TContext) => Promise<void>;
|
|
597
|
+
}): Promise<void> {
|
|
598
|
+
await deps.openModelMenu(deps.ctx);
|
|
500
599
|
}
|
|
501
600
|
|
|
502
601
|
export async function executeTelegramCommandAction<TMessage, TContext>(
|
|
@@ -573,6 +672,7 @@ export function createTelegramCommandHandlerTargetRuntime<
|
|
|
573
672
|
hasAbortHandler: deps.hasAbortHandler,
|
|
574
673
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
575
674
|
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
675
|
+
clearQueuedTelegramItems: deps.clearQueuedTelegramItems,
|
|
576
676
|
setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
|
|
577
677
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
578
678
|
isIdle: deps.isIdle,
|
|
@@ -644,21 +744,6 @@ async function handleTelegramCommandRuntime<
|
|
|
644
744
|
deps.sendTextReply(nextMessage, text);
|
|
645
745
|
const updateStatusFor = (commandCtx: TContext) => () =>
|
|
646
746
|
deps.updateStatus(commandCtx);
|
|
647
|
-
const enqueueControlFor =
|
|
648
|
-
(nextMessage: TMessage, commandCtx: TContext) =>
|
|
649
|
-
(
|
|
650
|
-
controlType: TelegramControlCommandType,
|
|
651
|
-
statusSummary: string,
|
|
652
|
-
execute: (ctx: TContext) => Promise<void>,
|
|
653
|
-
) => {
|
|
654
|
-
deps.enqueueControlItem(
|
|
655
|
-
nextMessage,
|
|
656
|
-
commandCtx,
|
|
657
|
-
controlType,
|
|
658
|
-
statusSummary,
|
|
659
|
-
execute,
|
|
660
|
-
);
|
|
661
|
-
};
|
|
662
747
|
return executeTelegramCommandAction(
|
|
663
748
|
buildTelegramCommandAction(commandName),
|
|
664
749
|
message,
|
|
@@ -668,7 +753,8 @@ async function handleTelegramCommandRuntime<
|
|
|
668
753
|
await handleTelegramStopCommand({
|
|
669
754
|
hasAbortHandler: deps.hasAbortHandler,
|
|
670
755
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
671
|
-
|
|
756
|
+
clearQueuedTelegramItems: () =>
|
|
757
|
+
deps.clearQueuedTelegramItems(commandCtx),
|
|
672
758
|
setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
|
|
673
759
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
674
760
|
updateStatus: updateStatusFor(commandCtx),
|
|
@@ -694,13 +780,13 @@ async function handleTelegramCommandRuntime<
|
|
|
694
780
|
},
|
|
695
781
|
handleStatus: async (nextMessage, commandCtx) => {
|
|
696
782
|
await handleTelegramStatusCommand<TContext>({
|
|
697
|
-
|
|
783
|
+
ctx: commandCtx,
|
|
698
784
|
showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx),
|
|
699
785
|
});
|
|
700
786
|
},
|
|
701
787
|
handleModel: async (nextMessage, commandCtx) => {
|
|
702
788
|
await handleTelegramModelCommand<TContext>({
|
|
703
|
-
|
|
789
|
+
ctx: commandCtx,
|
|
704
790
|
openModelMenu: (controlCtx) =>
|
|
705
791
|
deps.openModelMenu(nextMessage, controlCtx),
|
|
706
792
|
});
|