@llblab/pi-telegram 0.15.0 → 0.16.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/CHANGELOG.md +7 -1
- package/README.md +2 -2
- package/api/commands.ts +11 -0
- package/docs/README.md +2 -0
- package/docs/architecture.md +2 -1
- package/docs/public-api.md +33 -1
- package/index.ts +1 -1
- package/lib/commands.ts +199 -4
- package/lib/prompt-templates.ts +8 -2
- package/lib/routing.ts +40 -4
- package/lib/runtime.ts +2 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 0.16.0: Telegram Extension Commands
|
|
6
|
+
|
|
7
|
+
- `[API]` Added `registerTelegramCommand()` on the public `/commands` subpath so companion extensions can explicitly provide Telegram-native slash commands without adding workflow-specific commands to core. Built-in bridge commands stay reserved, extension command names must be Bot API safe, duplicate extension names are rejected, commands stay hidden unless `showInMenu` is enabled, visible commands must provide an emoji used in `/start` help and Bot API descriptions, extension-command descriptions are shown in `/start`, visible extension commands are inserted after `/compact` before queue-control commands, prompt-template commands remain separated in `/start`, handler failures are isolated with runtime diagnostics, and routing precedence is built-ins → extension commands → prompt-template aliases. Impact: commands such as fresh-session controls can live in companion extensions while `pi-telegram` remains a lightweight Telegram shell.
|
|
8
|
+
|
|
9
|
+
## 0.15.1: Typing Keepalive Cadence
|
|
10
|
+
|
|
11
|
+
- `[Typing Status]` Pinned the default native Telegram typing keepalive interval at 2500 ms while preserving the 250 ms idle-drain cap. Impact: runtime behavior matches the intended conservative chat-action cadence.
|
|
6
12
|
|
|
7
13
|
## 0.15.0: Companion Status Lines
|
|
8
14
|
|
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ If you ask π for a generated file, π can call `telegram_attach`: during a Tele
|
|
|
114
114
|
|
|
115
115
|
### Operator menu and controls
|
|
116
116
|
|
|
117
|
-
The inline application menu is the primary operator surface. It exposes status, prompt-template commands, model selection, thinking level selection, settings, and queue inspection/mutation: a Telegram-shaped subset of the important handles normally available from the CLI. A typical control loop stays inside Telegram: open `/start`, inspect status, jump into Queue, delete stale work, switch model, return to the main menu, and keep the π session running without touching the terminal.
|
|
117
|
+
The inline application menu is the primary operator surface. It exposes status, prompt-template commands, companion-extension Telegram commands, model selection, thinking level selection, settings, and queue inspection/mutation: a Telegram-shaped subset of the important handles normally available from the CLI. A typical control loop stays inside Telegram: open `/start`, inspect status, jump into Queue, delete stale work, switch model, return to the main menu, and keep the π session running without touching the terminal.
|
|
118
118
|
|
|
119
119
|
### Queue runtime
|
|
120
120
|
|
|
@@ -209,7 +209,7 @@ Unknown inline-button callbacks are forwarded to π as `[callback] <data>` when
|
|
|
209
209
|
|
|
210
210
|
### Extension Sections
|
|
211
211
|
|
|
212
|
-
Ordinary pi extensions can register structured UI sections
|
|
212
|
+
Ordinary pi extensions can register Telegram-native slash commands, structured UI sections, and compact status lines without owning a second polling loop. Slash commands use explicit opt-in registration from `@llblab/pi-telegram/commands`, so workflow-specific commands such as a fresh-session control can live in companion extensions instead of expanding the core bridge command set. UI sections appear in the main Telegram menu and Settings submenu, while status lines allow widgets such as quota indicators to appear beside Status, Usage, Cost, and Context only when relevant to the active model. Each section gets a narrow typed context with `edit`, `open`, `enqueuePrompt`, `answerCallback`, and `callbackData()` — enough to build interactive Telegram-native surfaces while `pi-telegram` owns transport, callback routing, navigation hierarchy, and diagnostics.
|
|
213
213
|
|
|
214
214
|
Import `registerTelegramSection()` from `@llblab/pi-telegram/sections` and return a disposer on shutdown. Sections can send interactive messages directly into the chat via `ctx.open()` — confirmation dialogs, approve/deny gates, and multi-step forms live outside the menu hierarchy while callbacks route through the same typed handler. See [`@llblab/pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) for a working reference and the [Extension Sections Standard](./docs/sections.md) for the full contract.
|
|
215
215
|
|
package/api/commands.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Telegram commands API
|
|
3
|
+
* Zones: package boundary, companion extension interop
|
|
4
|
+
* Exposes the stable Telegram slash-command registration surface while keeping registry internals package-private
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
registerTelegramCommand,
|
|
9
|
+
type TelegramExtensionCommandContext,
|
|
10
|
+
type TelegramExtensionCommandRegistration,
|
|
11
|
+
} from "../lib/commands.ts";
|
package/docs/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Living index of project documentation in `/docs`.
|
|
4
4
|
|
|
5
|
+
`public-api.md` is the canonical entrypoint for stable extension surfaces. Focused documents exist only when a surface has enough runtime rules to need a dedicated standard.
|
|
6
|
+
|
|
5
7
|
## Documents
|
|
6
8
|
|
|
7
9
|
- [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, queueing model, rendering pipeline, and interactive controls
|
package/docs/architecture.md
CHANGED
|
@@ -207,12 +207,13 @@ Unknown callback data outside owned prefixes is forwarded as `[callback] <data>`
|
|
|
207
207
|
`pi-telegram` intentionally owns one `getUpdates` loop per bot. `polling` owns that internal loop; `updates` owns classification/default-routing plans plus the public handler registry layered extensions use to observe or consume updates without opening a competing polling connection. Layered extensions should integrate through extension surfaces instead of polling the same bot independently.
|
|
208
208
|
|
|
209
209
|
- Raw update observation/consumption: [Updates](./updates.md).
|
|
210
|
+
- Telegram-native slash commands: `registerTelegramCommand()` from [Public API](./public-api.md#commands).
|
|
210
211
|
- Structured inline UI sections: [Sections](./sections.md).
|
|
211
212
|
- Callback namespace discipline: [Callback Namespaces](./callback-namespaces.md).
|
|
212
213
|
- Voice/STT/TTS providers: [Voice Integration](./voice.md).
|
|
213
214
|
- Inbound/outbound command-template handlers: [Command Templates](./command-templates.md).
|
|
214
215
|
|
|
215
|
-
Extension callbacks must avoid `pi-telegram` owned prefixes such as `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
|
|
216
|
+
Extension callbacks must avoid `pi-telegram` owned prefixes such as `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`. Workflow-specific Telegram slash commands should use the public command registry instead of becoming new core built-ins unless they are bridge lifecycle, transport ownership, queue safety, or essential operator controls.
|
|
216
217
|
|
|
217
218
|
## Diagnostics And Operational Behavior
|
|
218
219
|
|
package/docs/public-api.md
CHANGED
|
@@ -18,6 +18,7 @@ import telegram from "@llblab/pi-telegram";
|
|
|
18
18
|
import { registerTelegramSection } from "@llblab/pi-telegram/sections";
|
|
19
19
|
import { registerTelegramStatusLineProvider } from "@llblab/pi-telegram/status";
|
|
20
20
|
import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
|
|
21
|
+
import { registerTelegramCommand } from "@llblab/pi-telegram/commands";
|
|
21
22
|
import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
|
|
22
23
|
import { registerTelegramOutboundHandler } from "@llblab/pi-telegram/outbound";
|
|
23
24
|
import {
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
} from "@llblab/pi-telegram/voice";
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
`0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private. See [Public API Smoke Examples](#public-api-smoke-examples) below for minimal companion-extension patterns that avoid implementation imports.
|
|
30
|
+
`0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private. Telegram command extensions use `/commands` as an explicit opt-in surface instead of automatically exposing arbitrary π slash commands to Telegram. See [Public API Smoke Examples](#public-api-smoke-examples) below for minimal companion-extension patterns that avoid implementation imports.
|
|
30
31
|
|
|
31
32
|
## User-Facing API
|
|
32
33
|
|
|
@@ -103,6 +104,9 @@ High-level stable APIs:
|
|
|
103
104
|
- `registerTelegramSection()`
|
|
104
105
|
- Identity: required `id`.
|
|
105
106
|
- Purpose: managed menu/settings UI surfaces.
|
|
107
|
+
- `registerTelegramCommand()`
|
|
108
|
+
- Identity: command name.
|
|
109
|
+
- Purpose: explicit opt-in Telegram-native slash commands for companion workflows.
|
|
106
110
|
- `registerTelegramStatusLineProvider()`
|
|
107
111
|
- Identity: required `id`.
|
|
108
112
|
- Purpose: compact companion status rows in the `/start` menu status text.
|
|
@@ -133,6 +137,34 @@ Advanced stable diagnostics:
|
|
|
133
137
|
|
|
134
138
|
All registration APIs return a disposer. Companion extensions should call disposers on shutdown and re-register on session start when they recreate runtime state. Low-level bus APIs intentionally avoid ids and run in registration order. High-level provider/UI APIs require stable identity in their public contract so diagnostics, replacement, and cleanup are understandable. Generated voice-provider ids remain a temporary compatibility path where documented.
|
|
135
139
|
|
|
140
|
+
## Commands
|
|
141
|
+
|
|
142
|
+
Import from `@llblab/pi-telegram/commands`. This registers Telegram slash commands only; it does not expose π slash commands and is unrelated to command-template handlers.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const off = registerTelegramCommand({
|
|
146
|
+
name: "new",
|
|
147
|
+
description: "Start fresh",
|
|
148
|
+
showInMenu: true,
|
|
149
|
+
emoji: "🆕",
|
|
150
|
+
handler: async (ctx) => {
|
|
151
|
+
await ctx.reply("Starting a fresh session is handled by my extension.");
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Contract:
|
|
157
|
+
|
|
158
|
+
- Command names are Telegram Bot API names: lowercase `a-z`, digits, and `_`, up to 32 characters. Hyphenated names are rejected.
|
|
159
|
+
- Built-in bridge commands such as `/start`, `/compact`, `/next`, `/abort`, and `/stop` are reserved and cannot be claimed by extensions.
|
|
160
|
+
- Duplicate extension command names are rejected. The disposer removes only its own command registration.
|
|
161
|
+
- Routing precedence is built-in bridge commands first, registered extension commands second, and prompt-template aliases after that. This lets an extension intentionally claim a command name; prompt-template owners can resolve collisions by renaming the template alias.
|
|
162
|
+
- `showInMenu` defaults to `false`. When `true`, `emoji` is required and the command appears in `/start` help with that marker; it also joins Bot API command sync only when `description` is provided, because Telegram command-list entries require descriptions. The emoji is prefixed to the Bot API description as well. Workflow/product commands should opt in deliberately instead of expanding the core command row by default.
|
|
163
|
+
- The command context currently provides `name`, `args`, `reply(text)`, and `enqueuePrompt(prompt)`. Use `enqueuePrompt()` when a command should create normal queued π work rather than perform immediate Telegram-side handling.
|
|
164
|
+
- Handler failures are isolated: the bridge records a `telegram-command` runtime diagnostic, sends a compact failure reply, and keeps Telegram polling/routing alive.
|
|
165
|
+
|
|
166
|
+
Core commands stay reserved for bridge lifecycle, transport ownership, queue safety, and essential operator controls. Opinionated workflow commands should live in companion extensions through this registry.
|
|
167
|
+
|
|
136
168
|
## Sections
|
|
137
169
|
|
|
138
170
|
Import from `@llblab/pi-telegram/sections`.
|
package/index.ts
CHANGED
|
@@ -256,7 +256,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
256
256
|
const getPromptTemplateCommands =
|
|
257
257
|
PromptTemplates.createTelegramPromptTemplateCommandGetter({
|
|
258
258
|
getCommands,
|
|
259
|
-
|
|
259
|
+
getReservedCommandNames: Commands.getTelegramReservedCommandNames,
|
|
260
260
|
});
|
|
261
261
|
const menuActions = Menu.createTelegramMenuActionRuntimeWithStateBuilder({
|
|
262
262
|
runtime: modelMenuRuntime,
|
package/lib/commands.ts
CHANGED
|
@@ -27,6 +27,130 @@ export interface TelegramPromptTemplateMenuCommand {
|
|
|
27
27
|
description?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY = "__piTelegramCommandRegistry__";
|
|
31
|
+
const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
|
32
|
+
|
|
33
|
+
export interface TelegramExtensionCommandContext {
|
|
34
|
+
name: string;
|
|
35
|
+
args: string;
|
|
36
|
+
reply: (text: string) => Promise<void>;
|
|
37
|
+
enqueuePrompt: (prompt: string) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TelegramExtensionCommandRegistration {
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
order?: number;
|
|
44
|
+
showInMenu?: boolean;
|
|
45
|
+
emoji?: string;
|
|
46
|
+
handler: (ctx: TelegramExtensionCommandContext) => Promise<void> | void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface RegisteredTelegramExtensionCommand {
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
order: number;
|
|
53
|
+
showInMenu: boolean;
|
|
54
|
+
emoji?: string;
|
|
55
|
+
handler: TelegramExtensionCommandRegistration["handler"];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface TelegramExtensionCommandRegistry {
|
|
59
|
+
commands: Map<string, RegisteredTelegramExtensionCommand>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getOrCreateTelegramCommandRegistry(): TelegramExtensionCommandRegistry {
|
|
63
|
+
const existing = (globalThis as Record<string, unknown>)[
|
|
64
|
+
TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY
|
|
65
|
+
];
|
|
66
|
+
if (
|
|
67
|
+
existing &&
|
|
68
|
+
typeof existing === "object" &&
|
|
69
|
+
existing !== null &&
|
|
70
|
+
"commands" in existing &&
|
|
71
|
+
existing.commands instanceof Map
|
|
72
|
+
) {
|
|
73
|
+
return existing as TelegramExtensionCommandRegistry;
|
|
74
|
+
}
|
|
75
|
+
const registry: TelegramExtensionCommandRegistry = { commands: new Map() };
|
|
76
|
+
(globalThis as Record<string, unknown>)[
|
|
77
|
+
TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY
|
|
78
|
+
] = registry;
|
|
79
|
+
return registry;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeTelegramExtensionCommandName(name: string): string {
|
|
83
|
+
return name.trim().replace(/^\/+/, "").toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isTelegramExtensionCommandName(name: string): boolean {
|
|
87
|
+
return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeTelegramExtensionCommandEmoji(
|
|
91
|
+
emoji: string | undefined,
|
|
92
|
+
): string | undefined {
|
|
93
|
+
const normalized = emoji?.trim();
|
|
94
|
+
return normalized ? normalized : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function registerTelegramCommand(
|
|
98
|
+
registration: TelegramExtensionCommandRegistration,
|
|
99
|
+
): () => void {
|
|
100
|
+
const name = normalizeTelegramExtensionCommandName(registration.name);
|
|
101
|
+
const showInMenu = registration.showInMenu ?? false;
|
|
102
|
+
const emoji = normalizeTelegramExtensionCommandEmoji(registration.emoji);
|
|
103
|
+
if (!isTelegramExtensionCommandName(name)) {
|
|
104
|
+
throw new Error(`Invalid Telegram command name: ${registration.name}`);
|
|
105
|
+
}
|
|
106
|
+
if (showInMenu && !emoji) {
|
|
107
|
+
throw new Error(`Visible Telegram command requires emoji: ${name}`);
|
|
108
|
+
}
|
|
109
|
+
if (emoji && emoji.length > 8) {
|
|
110
|
+
throw new Error(`Telegram command emoji is too long: ${name}`);
|
|
111
|
+
}
|
|
112
|
+
if (isTelegramReservedCommandName(name)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Telegram command conflicts with built-in command: ${name}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const registry = getOrCreateTelegramCommandRegistry();
|
|
118
|
+
if (registry.commands.has(name)) {
|
|
119
|
+
throw new Error(`Telegram command is already registered: ${name}`);
|
|
120
|
+
}
|
|
121
|
+
const command: RegisteredTelegramExtensionCommand = {
|
|
122
|
+
name,
|
|
123
|
+
description: registration.description,
|
|
124
|
+
order: registration.order ?? 0,
|
|
125
|
+
showInMenu,
|
|
126
|
+
emoji,
|
|
127
|
+
handler: registration.handler,
|
|
128
|
+
};
|
|
129
|
+
registry.commands.set(name, command);
|
|
130
|
+
return () => {
|
|
131
|
+
if (registry.commands.get(name) === command) registry.commands.delete(name);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getTelegramExtensionCommands(): RegisteredTelegramExtensionCommand[] {
|
|
136
|
+
return Array.from(
|
|
137
|
+
getOrCreateTelegramCommandRegistry().commands.values(),
|
|
138
|
+
).sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function findTelegramExtensionCommand(
|
|
142
|
+
name: string | undefined,
|
|
143
|
+
): RegisteredTelegramExtensionCommand | undefined {
|
|
144
|
+
if (!name) return undefined;
|
|
145
|
+
return getOrCreateTelegramCommandRegistry().commands.get(
|
|
146
|
+
normalizeTelegramExtensionCommandName(name),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function clearTelegramExtensionCommands(): void {
|
|
151
|
+
getOrCreateTelegramCommandRegistry().commands.clear();
|
|
152
|
+
}
|
|
153
|
+
|
|
30
154
|
export const TELEGRAM_COMMAND_EMOJI = {
|
|
31
155
|
start: "🟢",
|
|
32
156
|
status: "📊",
|
|
@@ -106,6 +230,22 @@ export const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinitio
|
|
|
106
230
|
|
|
107
231
|
export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS;
|
|
108
232
|
|
|
233
|
+
function getVisibleTelegramExtensionBotCommands(): TelegramBotCommandDefinition[] {
|
|
234
|
+
return getTelegramExtensionCommands()
|
|
235
|
+
.filter((command) => command.showInMenu && command.description)
|
|
236
|
+
.map((command) => ({
|
|
237
|
+
command: command.name,
|
|
238
|
+
description: `${command.emoji} ${command.description ?? command.name}`,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getTelegramReservedCommandNames(): string[] {
|
|
243
|
+
return [
|
|
244
|
+
...TELEGRAM_RESERVED_COMMAND_NAMES,
|
|
245
|
+
...getTelegramExtensionCommands().map((command) => command.name),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
109
249
|
export interface TelegramBotCommandRegistrationDeps {
|
|
110
250
|
setMyCommands: (
|
|
111
251
|
commands: readonly TelegramBotCommandDefinition[],
|
|
@@ -115,7 +255,23 @@ export interface TelegramBotCommandRegistrationDeps {
|
|
|
115
255
|
export async function registerTelegramBotCommands(
|
|
116
256
|
deps: TelegramBotCommandRegistrationDeps,
|
|
117
257
|
): Promise<void> {
|
|
118
|
-
|
|
258
|
+
const extensionCommands = getVisibleTelegramExtensionBotCommands();
|
|
259
|
+
if (extensionCommands.length === 0) {
|
|
260
|
+
await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const compactCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex(
|
|
264
|
+
(command) => command.command === "compact",
|
|
265
|
+
);
|
|
266
|
+
if (compactCommandIndex === -1) {
|
|
267
|
+
await deps.setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await deps.setMyCommands([
|
|
271
|
+
...TELEGRAM_BOT_COMMANDS.slice(0, compactCommandIndex + 1),
|
|
272
|
+
...extensionCommands,
|
|
273
|
+
...TELEGRAM_BOT_COMMANDS.slice(compactCommandIndex + 1),
|
|
274
|
+
]);
|
|
119
275
|
}
|
|
120
276
|
|
|
121
277
|
export function createTelegramBotCommandRegistrar(
|
|
@@ -566,6 +722,11 @@ export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
|
|
|
566
722
|
message: TMessage,
|
|
567
723
|
ctx: TContext,
|
|
568
724
|
) => Promise<boolean>;
|
|
725
|
+
executeExtensionCommand?: (
|
|
726
|
+
command: ParsedTelegramCommand,
|
|
727
|
+
message: TMessage,
|
|
728
|
+
ctx: TContext,
|
|
729
|
+
) => Promise<boolean>;
|
|
569
730
|
expandPromptTemplateCommand?: (
|
|
570
731
|
commandName: string,
|
|
571
732
|
args: string,
|
|
@@ -650,15 +811,41 @@ function buildTelegramPromptTemplateMenuHtml(
|
|
|
650
811
|
.join("\n");
|
|
651
812
|
}
|
|
652
813
|
|
|
814
|
+
function buildTelegramExtensionCommandMenuLines(): string[] {
|
|
815
|
+
return getTelegramExtensionCommands()
|
|
816
|
+
.filter((command) => command.showInMenu)
|
|
817
|
+
.map((command) => {
|
|
818
|
+
const prefix = `${escapeTelegramCommandMenuHtml(command.emoji ?? "")} /${escapeTelegramCommandMenuHtml(command.name)}`;
|
|
819
|
+
if (!command.description) return prefix;
|
|
820
|
+
return `${prefix} — ${escapeTelegramCommandMenuHtml(command.description)}`;
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function buildTelegramAppMenuIntroHtml(): string {
|
|
825
|
+
const extensionLines = buildTelegramExtensionCommandMenuLines();
|
|
826
|
+
if (extensionLines.length === 0) return TELEGRAM_APP_MENU_INTRO_HTML;
|
|
827
|
+
return [
|
|
828
|
+
"<b>π Telegram</b>",
|
|
829
|
+
"",
|
|
830
|
+
`${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
|
|
831
|
+
`${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
|
|
832
|
+
...extensionLines,
|
|
833
|
+
`${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
|
|
834
|
+
`${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
|
|
835
|
+
`${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort π`,
|
|
836
|
+
`${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort π & Clear queue`,
|
|
837
|
+
].join("\n");
|
|
838
|
+
}
|
|
839
|
+
|
|
653
840
|
export function buildTelegramAppMenuHtml(
|
|
654
841
|
statusHtml: string,
|
|
655
842
|
promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
|
|
656
843
|
): string {
|
|
844
|
+
const introHtml = buildTelegramAppMenuIntroHtml();
|
|
657
845
|
const promptTemplateHtml =
|
|
658
846
|
buildTelegramPromptTemplateMenuHtml(promptTemplates);
|
|
659
|
-
if (!promptTemplateHtml)
|
|
660
|
-
|
|
661
|
-
return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
|
|
847
|
+
if (!promptTemplateHtml) return `${introHtml}\n\n${statusHtml}`;
|
|
848
|
+
return `${introHtml}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
|
|
662
849
|
}
|
|
663
850
|
|
|
664
851
|
export function createTelegramAppMenuHtmlBuilder<TContext>(deps: {
|
|
@@ -1133,6 +1320,14 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
|
|
|
1133
1320
|
ctx,
|
|
1134
1321
|
);
|
|
1135
1322
|
if (handled) return;
|
|
1323
|
+
if (command && deps.executeExtensionCommand) {
|
|
1324
|
+
const handledByExtension = await deps.executeExtensionCommand(
|
|
1325
|
+
command,
|
|
1326
|
+
messages[0]!,
|
|
1327
|
+
ctx,
|
|
1328
|
+
);
|
|
1329
|
+
if (handledByExtension) return;
|
|
1330
|
+
}
|
|
1136
1331
|
if (command?.name && deps.expandPromptTemplateCommand) {
|
|
1137
1332
|
const expanded = deps.expandPromptTemplateCommand(
|
|
1138
1333
|
command.name,
|
package/lib/prompt-templates.ts
CHANGED
|
@@ -99,6 +99,7 @@ export function mapPiPromptTemplateNameToTelegramCommandName(
|
|
|
99
99
|
export interface TelegramPromptTemplateCommandGetterDeps {
|
|
100
100
|
getCommands: () => readonly PiSlashCommandInfo[];
|
|
101
101
|
reservedCommandNames?: readonly string[];
|
|
102
|
+
getReservedCommandNames?: () => readonly string[];
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export function getTelegramPromptTemplateCommands(
|
|
@@ -128,9 +129,14 @@ export function getTelegramPromptTemplateCommands(
|
|
|
128
129
|
export function createTelegramPromptTemplateCommandGetter(
|
|
129
130
|
deps: TelegramPromptTemplateCommandGetterDeps,
|
|
130
131
|
): () => TelegramPromptTemplateCommand[] {
|
|
131
|
-
const reservedNames = new Set(deps.reservedCommandNames);
|
|
132
132
|
return function getPromptTemplateCommands() {
|
|
133
|
-
return getTelegramPromptTemplateCommands(
|
|
133
|
+
return getTelegramPromptTemplateCommands(
|
|
134
|
+
deps.getCommands(),
|
|
135
|
+
new Set([
|
|
136
|
+
...(deps.reservedCommandNames ?? []),
|
|
137
|
+
...(deps.getReservedCommandNames?.() ?? []),
|
|
138
|
+
]),
|
|
139
|
+
);
|
|
134
140
|
};
|
|
135
141
|
}
|
|
136
142
|
|
package/lib/routing.ts
CHANGED
|
@@ -358,13 +358,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
358
358
|
deps.queueMutationRuntime.append(continueTurn, ctx);
|
|
359
359
|
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
360
360
|
};
|
|
361
|
-
const reservedCommandNames =
|
|
362
|
-
Commands.
|
|
363
|
-
);
|
|
361
|
+
const reservedCommandNames = () =>
|
|
362
|
+
new Set(Commands.getTelegramReservedCommandNames());
|
|
364
363
|
const getPromptTemplateCommands = () =>
|
|
365
364
|
PromptTemplates.getTelegramPromptTemplateCommands(
|
|
366
365
|
deps.getCommands(),
|
|
367
|
-
reservedCommandNames,
|
|
366
|
+
reservedCommandNames(),
|
|
368
367
|
);
|
|
369
368
|
const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
|
|
370
369
|
TMessage,
|
|
@@ -434,6 +433,43 @@ export function createTelegramInboundRouteRuntime<
|
|
|
434
433
|
>({
|
|
435
434
|
extractRawText: Media.extractFirstTelegramMessageText,
|
|
436
435
|
handleCommand: commandHandler,
|
|
436
|
+
executeExtensionCommand: async (command, message, ctx) => {
|
|
437
|
+
const extensionCommand = Commands.findTelegramExtensionCommand(
|
|
438
|
+
command.name,
|
|
439
|
+
);
|
|
440
|
+
if (!extensionCommand) return false;
|
|
441
|
+
try {
|
|
442
|
+
await extensionCommand.handler({
|
|
443
|
+
name: command.name,
|
|
444
|
+
args: command.args,
|
|
445
|
+
reply: (text) =>
|
|
446
|
+
deps
|
|
447
|
+
.sendTextReply(message.chat.id, message.message_id, text)
|
|
448
|
+
.then(() => {}),
|
|
449
|
+
enqueuePrompt: (prompt) =>
|
|
450
|
+
promptEnqueue(
|
|
451
|
+
[
|
|
452
|
+
{
|
|
453
|
+
...message,
|
|
454
|
+
text: prompt,
|
|
455
|
+
caption: undefined,
|
|
456
|
+
} as TMessage,
|
|
457
|
+
],
|
|
458
|
+
ctx,
|
|
459
|
+
),
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
deps.recordRuntimeEvent?.("telegram-command", error, {
|
|
463
|
+
command: command.name,
|
|
464
|
+
});
|
|
465
|
+
await deps.sendTextReply(
|
|
466
|
+
message.chat.id,
|
|
467
|
+
message.message_id,
|
|
468
|
+
"Command failed.",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return true;
|
|
472
|
+
},
|
|
437
473
|
expandPromptTemplateCommand: (commandName, args) =>
|
|
438
474
|
PromptTemplates.expandTelegramPromptTemplateCommand(
|
|
439
475
|
commandName,
|
package/lib/runtime.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const TELEGRAM_TYPING_ACTION_INTERVAL_MS =
|
|
8
|
-
const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS =
|
|
7
|
+
const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 2500;
|
|
8
|
+
const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
|
|
9
9
|
|
|
10
10
|
export interface TelegramRuntimeQueueCounters {
|
|
11
11
|
nextQueuedTelegramItemOrder: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"./inbound": "./api/inbound.ts",
|
|
50
50
|
"./outbound": "./api/outbound.ts",
|
|
51
51
|
"./updates": "./api/updates.ts",
|
|
52
|
+
"./commands": "./api/commands.ts",
|
|
52
53
|
"./sections": "./api/sections.ts",
|
|
53
54
|
"./status": "./api/status.ts",
|
|
54
55
|
"./voice": "./api/voice.ts",
|