@llblab/pi-telegram 0.2.10 → 0.4.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/README.md +52 -19
- package/docs/README.md +2 -3
- package/docs/architecture.md +62 -31
- package/docs/locks.md +136 -0
- package/index.ts +323 -1880
- package/lib/api.ts +396 -60
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +648 -14
- package/lib/config.ts +169 -0
- package/lib/handlers.ts +474 -0
- package/lib/locks.ts +306 -0
- package/lib/media.ts +196 -46
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +90 -0
- package/lib/polling.ts +240 -14
- package/lib/preview.ts +420 -25
- package/lib/queue.ts +1137 -110
- package/lib/registration.ts +214 -31
- package/lib/rendering.ts +560 -366
- package/lib/replies.ts +198 -8
- package/lib/routing.ts +217 -0
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +143 -1
- package/lib/status.ts +432 -13
- package/lib/turns.ts +217 -36
- package/lib/updates.ts +340 -109
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -34
- package/lib/model-switch.ts +0 -62
- package/lib/types.ts +0 -137
- package/tests/api.test.ts +0 -331
- package/tests/attachments.test.ts +0 -132
- package/tests/commands.test.ts +0 -85
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -166
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -202
- package/tests/preview.test.ts +0 -480
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -526
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -247
- package/tests/updates.test.ts +0 -416
package/lib/registration.ts
CHANGED
|
@@ -3,28 +3,62 @@
|
|
|
3
3
|
* Owns tool, command, and lifecycle-hook registration so index.ts can stay focused on runtime orchestration state and side effects
|
|
4
4
|
*/
|
|
5
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";
|
|
6
13
|
import type {
|
|
14
|
+
AgentEndEvent,
|
|
15
|
+
AgentStartEvent,
|
|
16
|
+
BeforeAgentStartEvent,
|
|
7
17
|
ExtensionAPI,
|
|
8
18
|
ExtensionCommandContext,
|
|
9
19
|
ExtensionContext,
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
SessionShutdownEvent,
|
|
21
|
+
SessionStartEvent,
|
|
22
|
+
} from "./pi.ts";
|
|
23
|
+
import { TELEGRAM_PREFIX } from "./turns.ts";
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
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.`;
|
|
15
35
|
|
|
16
36
|
// --- Tool Registration ---
|
|
17
37
|
|
|
18
|
-
export interface
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 }>;
|
|
22
52
|
}
|
|
23
53
|
|
|
24
54
|
export function registerTelegramAttachmentTool(
|
|
25
55
|
pi: ExtensionAPI,
|
|
26
56
|
deps: TelegramAttachmentToolRegistrationDeps,
|
|
27
57
|
): void {
|
|
58
|
+
const maxAttachmentsPerTurn =
|
|
59
|
+
deps.maxAttachmentsPerTurn ?? MAX_ATTACHMENTS_PER_TURN;
|
|
60
|
+
const maxAttachmentSizeBytes =
|
|
61
|
+
deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES;
|
|
28
62
|
pi.registerTool({
|
|
29
63
|
name: "telegram_attach",
|
|
30
64
|
label: "Telegram Attach",
|
|
@@ -37,32 +71,74 @@ export function registerTelegramAttachmentTool(
|
|
|
37
71
|
parameters: Type.Object({
|
|
38
72
|
paths: Type.Array(
|
|
39
73
|
Type.String({ description: "Local file path to attach" }),
|
|
40
|
-
{ minItems: 1, maxItems:
|
|
74
|
+
{ minItems: 1, maxItems: maxAttachmentsPerTurn },
|
|
41
75
|
),
|
|
42
76
|
}),
|
|
43
77
|
async execute(_toolCallId, params) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
93
|
},
|
|
51
94
|
});
|
|
52
95
|
}
|
|
53
96
|
|
|
54
97
|
// --- Command Registration ---
|
|
55
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
|
+
|
|
56
110
|
export interface TelegramCommandRegistrationDeps {
|
|
57
111
|
promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
58
112
|
getStatusLines: () => string[];
|
|
59
113
|
reloadConfig: () => Promise<void>;
|
|
60
114
|
hasBotToken: () => boolean;
|
|
61
|
-
startPolling: (
|
|
62
|
-
|
|
115
|
+
startPolling: (
|
|
116
|
+
ctx: ExtensionCommandContext,
|
|
117
|
+
options?: TelegramCommandStartPollingOptions,
|
|
118
|
+
) =>
|
|
119
|
+
| void
|
|
120
|
+
| Promise<void | TelegramCommandStartPollingResult>
|
|
121
|
+
| TelegramCommandStartPollingResult;
|
|
122
|
+
stopPolling: () => Promise<void | string>;
|
|
63
123
|
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
64
124
|
}
|
|
65
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
|
+
|
|
66
142
|
export function registerTelegramCommands(
|
|
67
143
|
pi: ExtensionAPI,
|
|
68
144
|
deps: TelegramCommandRegistrationDeps,
|
|
@@ -76,7 +152,7 @@ export function registerTelegramCommands(
|
|
|
76
152
|
pi.registerCommand("telegram-status", {
|
|
77
153
|
description: "Show Telegram bridge status",
|
|
78
154
|
handler: async (_args, ctx) => {
|
|
79
|
-
ctx.ui.notify(deps.getStatusLines().join("
|
|
155
|
+
ctx.ui.notify(deps.getStatusLines().join("\n"), "info");
|
|
80
156
|
},
|
|
81
157
|
});
|
|
82
158
|
pi.registerCommand("telegram-connect", {
|
|
@@ -87,14 +163,30 @@ export function registerTelegramCommands(
|
|
|
87
163
|
await deps.promptForConfig(ctx);
|
|
88
164
|
return;
|
|
89
165
|
}
|
|
90
|
-
await deps.startPolling(ctx);
|
|
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
|
+
}
|
|
91
182
|
deps.updateStatus(ctx);
|
|
92
183
|
},
|
|
93
184
|
});
|
|
94
185
|
pi.registerCommand("telegram-disconnect", {
|
|
95
186
|
description: "Stop the Telegram bridge in this pi session",
|
|
96
187
|
handler: async (_args, ctx) => {
|
|
97
|
-
await deps.stopPolling();
|
|
188
|
+
const message = await deps.stopPolling();
|
|
189
|
+
if (message) ctx.ui.notify(message, "info");
|
|
98
190
|
deps.updateStatus(ctx);
|
|
99
191
|
},
|
|
100
192
|
});
|
|
@@ -102,18 +194,67 @@ export function registerTelegramCommands(
|
|
|
102
194
|
|
|
103
195
|
// --- Lifecycle Hook Registration ---
|
|
104
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
|
+
|
|
105
237
|
export interface TelegramLifecycleRegistrationDeps {
|
|
106
|
-
onSessionStart: (
|
|
107
|
-
|
|
238
|
+
onSessionStart: (
|
|
239
|
+
event: SessionStartEvent,
|
|
240
|
+
ctx: ExtensionContext,
|
|
241
|
+
) => Promise<void>;
|
|
242
|
+
onSessionShutdown: (
|
|
243
|
+
event: SessionShutdownEvent,
|
|
244
|
+
ctx: ExtensionContext,
|
|
245
|
+
) => Promise<void>;
|
|
108
246
|
onBeforeAgentStart: (
|
|
109
|
-
event:
|
|
247
|
+
event: BeforeAgentStartEvent,
|
|
110
248
|
ctx: ExtensionContext,
|
|
111
|
-
) =>
|
|
249
|
+
) => TelegramBeforeAgentStartReturn;
|
|
112
250
|
onModelSelect: (
|
|
113
|
-
event:
|
|
251
|
+
event: { model: TelegramLifecycleModel },
|
|
114
252
|
ctx: ExtensionContext,
|
|
115
253
|
) => Promise<void> | void;
|
|
116
|
-
onAgentStart: (
|
|
254
|
+
onAgentStart: (
|
|
255
|
+
event: AgentStartEvent,
|
|
256
|
+
ctx: ExtensionContext,
|
|
257
|
+
) => Promise<void>;
|
|
117
258
|
onToolExecutionStart: (
|
|
118
259
|
event: unknown,
|
|
119
260
|
ctx: ExtensionContext,
|
|
@@ -122,9 +263,50 @@ export interface TelegramLifecycleRegistrationDeps {
|
|
|
122
263
|
event: unknown,
|
|
123
264
|
ctx: ExtensionContext,
|
|
124
265
|
) => Promise<void> | void;
|
|
125
|
-
onMessageStart: (
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
};
|
|
128
310
|
}
|
|
129
311
|
|
|
130
312
|
export function registerTelegramLifecycleHooks(
|
|
@@ -137,8 +319,9 @@ export function registerTelegramLifecycleHooks(
|
|
|
137
319
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
138
320
|
await deps.onSessionShutdown(event, ctx);
|
|
139
321
|
});
|
|
140
|
-
pi.on("before_agent_start",
|
|
141
|
-
deps.onBeforeAgentStart(event, ctx)
|
|
322
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
323
|
+
return deps.onBeforeAgentStart(event, ctx);
|
|
324
|
+
});
|
|
142
325
|
pi.on("model_select", async (event, ctx) => {
|
|
143
326
|
await deps.onModelSelect(event, ctx);
|
|
144
327
|
});
|