@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.
@@ -1,346 +0,0 @@
1
- /**
2
- * Telegram extension registration helpers
3
- * Owns tool, command, and lifecycle-hook registration so index.ts can stay focused on runtime orchestration state and side effects
4
- */
5
-
6
- import { Type } from "@sinclair/typebox";
7
-
8
- import {
9
- queueTelegramAttachments,
10
- TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
11
- type TelegramAttachmentQueueTargetView,
12
- } from "./attachments.ts";
13
- import type {
14
- AgentEndEvent,
15
- AgentStartEvent,
16
- BeforeAgentStartEvent,
17
- ExtensionAPI,
18
- ExtensionCommandContext,
19
- ExtensionContext,
20
- SessionShutdownEvent,
21
- SessionStartEvent,
22
- } from "./pi.ts";
23
- import { TELEGRAM_PREFIX } from "./turns.ts";
24
-
25
- const MAX_ATTACHMENTS_PER_TURN = 10;
26
-
27
- const SYSTEM_PROMPT_SUFFIX = `
28
-
29
- Telegram bridge extension is active.
30
- - Messages forwarded from Telegram are prefixed with "[telegram]".
31
- - [telegram] messages may include [attachments] sections with a base directory plus relative local file entries. Resolve and read those files as needed.
32
- - Telegram is often read on narrow phone screens, so prefer narrow table columns when presenting tabular data; wide monospace tables can become unreadable.
33
- - If a [telegram] user asked for a file or generated artifact, use the telegram_attach tool with the local file path so the extension can send it with your next final reply.
34
- - Do not assume mentioning a local file path in plain text will send it to Telegram. Use telegram_attach.`;
35
-
36
- // --- Tool Registration ---
37
-
38
- export interface TelegramRuntimeEventRecorderPort {
39
- recordRuntimeEvent?: (
40
- category: string,
41
- error: unknown,
42
- details?: Record<string, unknown>,
43
- ) => void;
44
- }
45
-
46
- export interface TelegramAttachmentToolRegistrationDeps
47
- extends TelegramRuntimeEventRecorderPort {
48
- maxAttachmentsPerTurn?: number;
49
- maxAttachmentSizeBytes?: number;
50
- getActiveTurn: () => TelegramAttachmentQueueTargetView | undefined;
51
- statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
52
- }
53
-
54
- export function registerTelegramAttachmentTool(
55
- pi: ExtensionAPI,
56
- deps: TelegramAttachmentToolRegistrationDeps,
57
- ): void {
58
- const maxAttachmentsPerTurn =
59
- deps.maxAttachmentsPerTurn ?? MAX_ATTACHMENTS_PER_TURN;
60
- const maxAttachmentSizeBytes =
61
- deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES;
62
- pi.registerTool({
63
- name: "telegram_attach",
64
- label: "Telegram Attach",
65
- description:
66
- "Queue one or more local files to be sent with the next Telegram reply.",
67
- promptSnippet: "Queue local files to be sent with the next Telegram reply.",
68
- promptGuidelines: [
69
- "When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.",
70
- ],
71
- parameters: Type.Object({
72
- paths: Type.Array(
73
- Type.String({ description: "Local file path to attach" }),
74
- { minItems: 1, maxItems: maxAttachmentsPerTurn },
75
- ),
76
- }),
77
- async execute(_toolCallId, params) {
78
- try {
79
- return await queueTelegramAttachments({
80
- activeTurn: deps.getActiveTurn(),
81
- paths: params.paths,
82
- maxAttachmentsPerTurn,
83
- maxAttachmentSizeBytes,
84
- statPath: deps.statPath,
85
- });
86
- } catch (error) {
87
- deps.recordRuntimeEvent?.("attachment", error, {
88
- phase: "queue",
89
- count: params.paths.length,
90
- });
91
- throw error;
92
- }
93
- },
94
- });
95
- }
96
-
97
- // --- Command Registration ---
98
-
99
- export interface TelegramCommandStartPollingOptions {
100
- force?: boolean;
101
- }
102
-
103
- export interface TelegramCommandStartPollingResult {
104
- ok: boolean;
105
- message?: string;
106
- canTakeover?: boolean;
107
- owner?: string;
108
- }
109
-
110
- export interface TelegramCommandRegistrationDeps {
111
- promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
112
- getStatusLines: () => string[];
113
- reloadConfig: () => Promise<void>;
114
- hasBotToken: () => boolean;
115
- startPolling: (
116
- ctx: ExtensionCommandContext,
117
- options?: TelegramCommandStartPollingOptions,
118
- ) =>
119
- | void
120
- | Promise<void | TelegramCommandStartPollingResult>
121
- | TelegramCommandStartPollingResult;
122
- stopPolling: () => Promise<void | string>;
123
- updateStatus: (ctx: ExtensionCommandContext) => void;
124
- }
125
-
126
- function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
127
- return ctx.ui.theme.fg("accent", "pi-telegram");
128
- }
129
-
130
- function formatTelegramTakeoverPrompt(
131
- ctx: ExtensionCommandContext,
132
- owner?: string,
133
- ): string {
134
- const theme = ctx.ui.theme;
135
- const action = theme.fg("warning", "move singleton lock here?");
136
- const from = theme.fg("muted", "from:");
137
- const to = theme.fg("muted", "to:");
138
- const source = owner ?? "another pi instance";
139
- return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
140
- }
141
-
142
- export function registerTelegramCommands(
143
- pi: ExtensionAPI,
144
- deps: TelegramCommandRegistrationDeps,
145
- ): void {
146
- pi.registerCommand("telegram-setup", {
147
- description: "Configure Telegram bot token",
148
- handler: async (_args, ctx) => {
149
- await deps.promptForConfig(ctx);
150
- },
151
- });
152
- pi.registerCommand("telegram-status", {
153
- description: "Show Telegram bridge status",
154
- handler: async (_args, ctx) => {
155
- ctx.ui.notify(deps.getStatusLines().join("\n"), "info");
156
- },
157
- });
158
- pi.registerCommand("telegram-connect", {
159
- description: "Start the Telegram bridge in this pi session",
160
- handler: async (_args, ctx) => {
161
- await deps.reloadConfig();
162
- if (!deps.hasBotToken()) {
163
- await deps.promptForConfig(ctx);
164
- return;
165
- }
166
- let result = await deps.startPolling(ctx);
167
- if (result && !result.ok && result.canTakeover) {
168
- const confirmed = await ctx.ui.confirm(
169
- formatTelegramTakeoverTitle(ctx),
170
- formatTelegramTakeoverPrompt(ctx, result.owner),
171
- );
172
- if (!confirmed) {
173
- ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
174
- deps.updateStatus(ctx);
175
- return;
176
- }
177
- result = await deps.startPolling(ctx, { force: true });
178
- }
179
- if (result?.message) {
180
- ctx.ui.notify(result.message, result.ok ? "info" : "warning");
181
- }
182
- deps.updateStatus(ctx);
183
- },
184
- });
185
- pi.registerCommand("telegram-disconnect", {
186
- description: "Stop the Telegram bridge in this pi session",
187
- handler: async (_args, ctx) => {
188
- const message = await deps.stopPolling();
189
- if (message) ctx.ui.notify(message, "info");
190
- deps.updateStatus(ctx);
191
- },
192
- });
193
- }
194
-
195
- // --- Lifecycle Hook Registration ---
196
-
197
- export function buildTelegramBridgeSystemPrompt(options: {
198
- prompt: string;
199
- systemPrompt: string;
200
- telegramPrefix?: string;
201
- systemPromptSuffix: string;
202
- }): { systemPrompt: string } {
203
- const telegramPrefix = options.telegramPrefix ?? TELEGRAM_PREFIX;
204
- const suffix = options.prompt.trimStart().startsWith(telegramPrefix)
205
- ? `${options.systemPromptSuffix}\n- The current user message came from Telegram.`
206
- : options.systemPromptSuffix;
207
- return { systemPrompt: options.systemPrompt + suffix };
208
- }
209
-
210
- export function createTelegramBeforeAgentStartHook(
211
- options: {
212
- telegramPrefix?: string;
213
- systemPromptSuffix?: string;
214
- } = {},
215
- ): (event: BeforeAgentStartEvent) => { systemPrompt: string } {
216
- return (event) =>
217
- buildTelegramBridgeSystemPrompt({
218
- prompt: event.prompt,
219
- systemPrompt: event.systemPrompt,
220
- telegramPrefix: options.telegramPrefix,
221
- systemPromptSuffix: options.systemPromptSuffix ?? SYSTEM_PROMPT_SUFFIX,
222
- });
223
- }
224
-
225
- export interface TelegramBeforeAgentStartResult {
226
- systemPrompt?: string;
227
- }
228
-
229
- type TelegramBeforeAgentStartReturn =
230
- | Promise<TelegramBeforeAgentStartResult | undefined>
231
- | TelegramBeforeAgentStartResult
232
- | undefined;
233
-
234
- type TelegramLifecycleModel = ExtensionContext["model"];
235
- type TelegramLifecycleMessage = AgentEndEvent["messages"][number];
236
-
237
- export interface TelegramLifecycleRegistrationDeps {
238
- onSessionStart: (
239
- event: SessionStartEvent,
240
- ctx: ExtensionContext,
241
- ) => Promise<void>;
242
- onSessionShutdown: (
243
- event: SessionShutdownEvent,
244
- ctx: ExtensionContext,
245
- ) => Promise<void>;
246
- onBeforeAgentStart: (
247
- event: BeforeAgentStartEvent,
248
- ctx: ExtensionContext,
249
- ) => TelegramBeforeAgentStartReturn;
250
- onModelSelect: (
251
- event: { model: TelegramLifecycleModel },
252
- ctx: ExtensionContext,
253
- ) => Promise<void> | void;
254
- onAgentStart: (
255
- event: AgentStartEvent,
256
- ctx: ExtensionContext,
257
- ) => Promise<void>;
258
- onToolExecutionStart: (
259
- event: unknown,
260
- ctx: ExtensionContext,
261
- ) => Promise<void> | void;
262
- onToolExecutionEnd: (
263
- event: unknown,
264
- ctx: ExtensionContext,
265
- ) => Promise<void> | void;
266
- onMessageStart: (
267
- event: { message: TelegramLifecycleMessage },
268
- ctx: ExtensionContext,
269
- ) => Promise<void>;
270
- onMessageUpdate: (
271
- event: { message: TelegramLifecycleMessage },
272
- ctx: ExtensionContext,
273
- ) => Promise<void>;
274
- onAgentEnd: (event: AgentEndEvent, ctx: ExtensionContext) => Promise<void>;
275
- }
276
-
277
- export interface TelegramSessionLifecycleHooks {
278
- onSessionStart: (event: SessionStartEvent, ctx: ExtensionContext) => Promise<void>;
279
- onSessionShutdown: (
280
- event: SessionShutdownEvent,
281
- ctx: ExtensionContext,
282
- ) => Promise<void>;
283
- }
284
-
285
- export interface TelegramExtraLifecycleHooks {
286
- onSessionStart?: (
287
- event: SessionStartEvent,
288
- ctx: ExtensionContext,
289
- ) => Promise<void>;
290
- onSessionShutdown?: (
291
- event: SessionShutdownEvent,
292
- ctx: ExtensionContext,
293
- ) => Promise<void>;
294
- }
295
-
296
- export function appendTelegramLifecycleHooks(
297
- base: TelegramSessionLifecycleHooks,
298
- extra: TelegramExtraLifecycleHooks,
299
- ): TelegramSessionLifecycleHooks {
300
- return {
301
- onSessionStart: async (event, ctx) => {
302
- await base.onSessionStart(event, ctx);
303
- await extra.onSessionStart?.(event, ctx);
304
- },
305
- onSessionShutdown: async (event, ctx) => {
306
- await base.onSessionShutdown(event, ctx);
307
- await extra.onSessionShutdown?.(event, ctx);
308
- },
309
- };
310
- }
311
-
312
- export function registerTelegramLifecycleHooks(
313
- pi: ExtensionAPI,
314
- deps: TelegramLifecycleRegistrationDeps,
315
- ): void {
316
- pi.on("session_start", async (event, ctx) => {
317
- await deps.onSessionStart(event, ctx);
318
- });
319
- pi.on("session_shutdown", async (event, ctx) => {
320
- await deps.onSessionShutdown(event, ctx);
321
- });
322
- pi.on("before_agent_start", async (event, ctx) => {
323
- return deps.onBeforeAgentStart(event, ctx);
324
- });
325
- pi.on("model_select", async (event, ctx) => {
326
- await deps.onModelSelect(event, ctx);
327
- });
328
- pi.on("agent_start", async (event, ctx) => {
329
- await deps.onAgentStart(event, ctx);
330
- });
331
- pi.on("tool_execution_start", async (event, ctx) => {
332
- await deps.onToolExecutionStart(event, ctx);
333
- });
334
- pi.on("tool_execution_end", async (event, ctx) => {
335
- await deps.onToolExecutionEnd(event, ctx);
336
- });
337
- pi.on("message_start", async (event, ctx) => {
338
- await deps.onMessageStart(event, ctx);
339
- });
340
- pi.on("message_update", async (event, ctx) => {
341
- await deps.onMessageUpdate(event, ctx);
342
- });
343
- pi.on("agent_end", async (event, ctx) => {
344
- await deps.onAgentEnd(event, ctx);
345
- });
346
- }