@llblab/pi-telegram 0.3.0 → 0.5.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 +41 -10
- package/docs/README.md +4 -3
- package/docs/architecture.md +43 -38
- package/docs/attachment-handlers.md +60 -0
- package/docs/command-templates.md +75 -0
- package/docs/locks.md +136 -0
- package/index.ts +80 -142
- package/lib/attachments.ts +70 -2
- package/lib/commands.ts +116 -48
- package/lib/config.ts +17 -5
- package/lib/handlers.ts +400 -0
- package/lib/lifecycle.ts +140 -0
- package/lib/locks.ts +336 -0
- package/lib/media.ts +50 -6
- package/lib/menu.ts +0 -4
- package/lib/pi.ts +11 -1
- package/lib/prompts.ts +44 -0
- package/lib/queue.ts +12 -6
- package/lib/routing.ts +219 -0
- package/lib/runtime.ts +9 -6
- package/lib/setup.ts +21 -3
- package/lib/status.ts +33 -4
- package/lib/turns.ts +103 -21
- package/package.json +1 -1
- package/lib/registration.ts +0 -262
package/index.ts
CHANGED
|
@@ -7,20 +7,22 @@ import * as Api from "./lib/api.ts";
|
|
|
7
7
|
import * as Attachments from "./lib/attachments.ts";
|
|
8
8
|
import * as Commands from "./lib/commands.ts";
|
|
9
9
|
import * as Config from "./lib/config.ts";
|
|
10
|
+
import * as Handlers from "./lib/handlers.ts";
|
|
11
|
+
import * as Lifecycle from "./lib/lifecycle.ts";
|
|
12
|
+
import * as Locks from "./lib/locks.ts";
|
|
10
13
|
import * as Media from "./lib/media.ts";
|
|
11
14
|
import * as Menu from "./lib/menu.ts";
|
|
12
15
|
import * as Model from "./lib/model.ts";
|
|
13
16
|
import * as Pi from "./lib/pi.ts";
|
|
14
17
|
import * as Polling from "./lib/polling.ts";
|
|
15
18
|
import * as Preview from "./lib/preview.ts";
|
|
19
|
+
import * as Prompts from "./lib/prompts.ts";
|
|
16
20
|
import * as Queue from "./lib/queue.ts";
|
|
17
|
-
import * as Registration from "./lib/registration.ts";
|
|
18
21
|
import * as Replies from "./lib/replies.ts";
|
|
19
22
|
import * as Runtime from "./lib/runtime.ts";
|
|
23
|
+
import * as Routing from "./lib/routing.ts";
|
|
20
24
|
import * as Setup from "./lib/setup.ts";
|
|
21
25
|
import * as Status from "./lib/status.ts";
|
|
22
|
-
import * as Turns from "./lib/turns.ts";
|
|
23
|
-
import * as Updates from "./lib/updates.ts";
|
|
24
26
|
|
|
25
27
|
type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
|
|
26
28
|
type RuntimeTelegramQueueItem = Queue.TelegramQueueItem<Pi.ExtensionContext>;
|
|
@@ -31,6 +33,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
31
33
|
const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
|
|
32
34
|
const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
|
|
33
35
|
const configStore = Config.createTelegramConfigStore();
|
|
36
|
+
const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
|
|
34
37
|
const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
|
|
35
38
|
const pendingModelSwitchStore =
|
|
36
39
|
Model.createPendingModelSwitchStore<
|
|
@@ -63,6 +66,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
63
66
|
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
64
67
|
formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
|
|
65
68
|
getRecentRuntimeEvents: runtimeEvents.getEvents,
|
|
69
|
+
getRuntimeLockState: lockRuntime.getStatusLabel,
|
|
66
70
|
});
|
|
67
71
|
const currentModelRuntime = Model.createCurrentModelRuntime<
|
|
68
72
|
Pi.ExtensionContext,
|
|
@@ -80,6 +84,13 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
80
84
|
bridgeRuntime.queue.incrementNextPriorityReactionOrder,
|
|
81
85
|
updateStatus,
|
|
82
86
|
});
|
|
87
|
+
const attachmentHandlerRuntime =
|
|
88
|
+
Handlers.createTelegramAttachmentHandlerRuntime<Pi.ExtensionContext>({
|
|
89
|
+
getHandlers: configStore.getAttachmentHandlers,
|
|
90
|
+
execCommand: piRuntime.exec,
|
|
91
|
+
getCwd: Pi.getExtensionContextCwd,
|
|
92
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
93
|
+
});
|
|
83
94
|
|
|
84
95
|
// --- Telegram API ---
|
|
85
96
|
|
|
@@ -202,158 +213,52 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
202
213
|
deleteWebhook,
|
|
203
214
|
getUpdates,
|
|
204
215
|
persistConfig: configStore.persist,
|
|
205
|
-
handleUpdate:
|
|
216
|
+
handleUpdate: Routing.createTelegramInboundRouteRuntime<
|
|
217
|
+
Api.TelegramUpdate,
|
|
218
|
+
Api.TelegramMessage,
|
|
219
|
+
Api.TelegramCallbackQuery,
|
|
206
220
|
Pi.ExtensionContext,
|
|
207
|
-
|
|
221
|
+
ActivePiModel
|
|
208
222
|
>({
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
configStore,
|
|
224
|
+
bridgeRuntime,
|
|
225
|
+
activeTurnRuntime,
|
|
226
|
+
mediaGroupRuntime,
|
|
227
|
+
telegramQueueStore,
|
|
228
|
+
queueMutationRuntime,
|
|
229
|
+
modelMenuRuntime,
|
|
230
|
+
currentModelRuntime,
|
|
231
|
+
modelSwitchController,
|
|
232
|
+
menuActions,
|
|
233
|
+
attachmentHandlerRuntime,
|
|
212
234
|
updateStatus,
|
|
213
|
-
|
|
214
|
-
removeQueuedTelegramTurnsByMessageIds:
|
|
215
|
-
queueMutationRuntime.removeByMessageIds,
|
|
216
|
-
clearQueuedTelegramTurnPriorityByMessageId:
|
|
217
|
-
queueMutationRuntime.clearPriorityByMessageId,
|
|
218
|
-
prioritizeQueuedTelegramTurnByMessageId:
|
|
219
|
-
queueMutationRuntime.prioritizeByMessageId,
|
|
235
|
+
dispatchNextQueuedTelegramTurn,
|
|
220
236
|
answerCallbackQuery,
|
|
221
|
-
handleAuthorizedTelegramCallbackQuery:
|
|
222
|
-
Menu.createTelegramMenuCallbackHandlerForContext<
|
|
223
|
-
Api.TelegramCallbackQuery,
|
|
224
|
-
Pi.ExtensionContext,
|
|
225
|
-
ActivePiModel
|
|
226
|
-
>({
|
|
227
|
-
getStoredModelMenuState: modelMenuRuntime.getState,
|
|
228
|
-
getActiveModel: currentModelRuntime.get,
|
|
229
|
-
getThinkingLevel: piRuntime.getThinkingLevel,
|
|
230
|
-
setThinkingLevel: piRuntime.setThinkingLevel,
|
|
231
|
-
updateStatus,
|
|
232
|
-
updateModelMenuMessage: menuActions.updateModelMenuMessage,
|
|
233
|
-
updateThinkingMenuMessage: menuActions.updateThinkingMenuMessage,
|
|
234
|
-
updateStatusMessage: menuActions.updateStatusMessage,
|
|
235
|
-
answerCallbackQuery,
|
|
236
|
-
isIdle: Pi.isExtensionContextIdle,
|
|
237
|
-
hasActiveTelegramTurn: activeTurnRuntime.has,
|
|
238
|
-
hasAbortHandler: bridgeRuntime.abort.hasHandler,
|
|
239
|
-
getActiveToolExecutions:
|
|
240
|
-
bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
241
|
-
setModel: piRuntime.setModel,
|
|
242
|
-
setCurrentModel: currentModelRuntime.setCurrentModel,
|
|
243
|
-
stagePendingModelSwitch: modelSwitchController.stagePendingSwitch,
|
|
244
|
-
restartInterruptedTelegramTurn:
|
|
245
|
-
modelSwitchController.restartInterruptedTurn,
|
|
246
|
-
}),
|
|
247
237
|
sendTextReply,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
>({
|
|
258
|
-
extractRawText: Media.extractFirstTelegramMessageText,
|
|
259
|
-
handleCommand: Commands.createTelegramCommandHandlerTargetRuntime<
|
|
260
|
-
Api.TelegramMessage,
|
|
261
|
-
Pi.ExtensionContext
|
|
262
|
-
>({
|
|
263
|
-
hasAbortHandler: bridgeRuntime.abort.hasHandler,
|
|
264
|
-
clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
265
|
-
hasQueuedTelegramItems: telegramQueueStore.hasQueuedItems,
|
|
266
|
-
setPreserveQueuedTurnsAsHistory:
|
|
267
|
-
bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
268
|
-
abortCurrentTurn: bridgeRuntime.abort.abortTurn,
|
|
269
|
-
isIdle: Pi.isExtensionContextIdle,
|
|
270
|
-
hasPendingMessages: Pi.hasExtensionContextPendingMessages,
|
|
271
|
-
hasActiveTelegramTurn: activeTurnRuntime.has,
|
|
272
|
-
hasDispatchPending: bridgeRuntime.lifecycle.hasDispatchPending,
|
|
273
|
-
isCompactionInProgress:
|
|
274
|
-
bridgeRuntime.lifecycle.isCompactionInProgress,
|
|
275
|
-
setCompactionInProgress:
|
|
276
|
-
bridgeRuntime.lifecycle.setCompactionInProgress,
|
|
277
|
-
updateStatus,
|
|
278
|
-
dispatchNextQueuedTelegramTurn,
|
|
279
|
-
compact: Pi.compactExtensionContext,
|
|
280
|
-
allocateItemOrder: bridgeRuntime.queue.allocateItemOrder,
|
|
281
|
-
allocateControlOrder: bridgeRuntime.queue.allocateControlOrder,
|
|
282
|
-
appendControlItem: queueMutationRuntime.append,
|
|
283
|
-
showStatus: menuActions.sendStatusMessage,
|
|
284
|
-
openModelMenu: menuActions.openModelMenu,
|
|
285
|
-
getAllowedUserId: configStore.getAllowedUserId,
|
|
286
|
-
setAllowedUserId: configStore.setAllowedUserId,
|
|
287
|
-
setMyCommands,
|
|
288
|
-
persistConfig: configStore.persist,
|
|
289
|
-
sendTextReply,
|
|
290
|
-
recordRuntimeEvent: runtimeEvents.record,
|
|
291
|
-
}),
|
|
292
|
-
enqueueTurn: Queue.createTelegramPromptEnqueueController<
|
|
293
|
-
Api.TelegramMessage,
|
|
294
|
-
Pi.ExtensionContext
|
|
295
|
-
>({
|
|
296
|
-
...telegramQueueStore,
|
|
297
|
-
getPreserveQueuedTurnsAsHistory:
|
|
298
|
-
bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
299
|
-
setPreserveQueuedTurnsAsHistory:
|
|
300
|
-
bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
301
|
-
createTurn:
|
|
302
|
-
Turns.createTelegramPromptTurnRuntimeBuilder<Api.TelegramMessage>(
|
|
303
|
-
{
|
|
304
|
-
allocateQueueOrder: bridgeRuntime.queue.allocateItemOrder,
|
|
305
|
-
downloadFile: downloadTelegramBridgeFile,
|
|
306
|
-
},
|
|
307
|
-
),
|
|
308
|
-
updateStatus,
|
|
309
|
-
dispatchNextQueuedTelegramTurn,
|
|
310
|
-
}).enqueue,
|
|
311
|
-
}).dispatchMessages,
|
|
312
|
-
}).handleMessage,
|
|
313
|
-
handleAuthorizedTelegramEditedMessage:
|
|
314
|
-
Turns.createTelegramQueuedPromptEditRuntime<
|
|
315
|
-
Api.TelegramMessage,
|
|
316
|
-
Pi.ExtensionContext
|
|
317
|
-
>({
|
|
318
|
-
...telegramQueueStore,
|
|
319
|
-
updateStatus,
|
|
320
|
-
}).updateFromEditedMessage,
|
|
238
|
+
setMyCommands,
|
|
239
|
+
downloadFile: downloadTelegramBridgeFile,
|
|
240
|
+
getThinkingLevel: piRuntime.getThinkingLevel,
|
|
241
|
+
setThinkingLevel: piRuntime.setThinkingLevel,
|
|
242
|
+
setModel: piRuntime.setModel,
|
|
243
|
+
isIdle: Pi.isExtensionContextIdle,
|
|
244
|
+
hasPendingMessages: Pi.hasExtensionContextPendingMessages,
|
|
245
|
+
compact: Pi.compactExtensionContext,
|
|
246
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
321
247
|
}).handleUpdate,
|
|
322
248
|
stopTypingLoop: bridgeRuntime.typing.stop,
|
|
323
249
|
updateStatus,
|
|
324
250
|
recordRuntimeEvent: runtimeEvents.record,
|
|
325
251
|
});
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
Registration.registerTelegramAttachmentTool(pi, {
|
|
330
|
-
getActiveTurn: activeTurnRuntime.get,
|
|
331
|
-
recordRuntimeEvent: runtimeEvents.record,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
Registration.registerTelegramCommands(pi, {
|
|
335
|
-
promptForConfig: Setup.createTelegramSetupPromptRuntime({
|
|
336
|
-
getConfig: configStore.get,
|
|
337
|
-
setConfig: configStore.set,
|
|
338
|
-
setupGuard: bridgeRuntime.setup,
|
|
339
|
-
getMe: Api.fetchTelegramBotIdentity,
|
|
340
|
-
persistConfig: configStore.persist,
|
|
341
|
-
startPolling: pollingRuntime.start,
|
|
342
|
-
updateStatus,
|
|
343
|
-
recordRuntimeEvent: runtimeEvents.record,
|
|
344
|
-
}),
|
|
345
|
-
getStatusLines,
|
|
346
|
-
reloadConfig: configStore.load,
|
|
252
|
+
const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
|
|
253
|
+
lock: lockRuntime,
|
|
347
254
|
hasBotToken: configStore.hasBotToken,
|
|
348
255
|
startPolling: pollingRuntime.start,
|
|
349
256
|
stopPolling: pollingRuntime.stop,
|
|
350
257
|
updateStatus,
|
|
258
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
351
259
|
});
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
Registration.registerTelegramLifecycleHooks(pi, {
|
|
356
|
-
...Queue.createTelegramSessionLifecycleRuntime<
|
|
260
|
+
const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
|
|
261
|
+
Queue.createTelegramSessionLifecycleRuntime<
|
|
357
262
|
Pi.ExtensionContext,
|
|
358
263
|
RuntimeTelegramQueueItem,
|
|
359
264
|
ActivePiModel
|
|
@@ -373,10 +278,43 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
373
278
|
clearPreview: previewRuntime.clear,
|
|
374
279
|
clearActiveTurn: activeTurnRuntime.clear,
|
|
375
280
|
clearAbort: bridgeRuntime.abort.clearHandler,
|
|
376
|
-
stopPolling:
|
|
281
|
+
stopPolling: lockedPollingRuntime.suspend,
|
|
282
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
283
|
+
}),
|
|
284
|
+
{ onSessionStart: lockedPollingRuntime.onSessionStart },
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// --- Extension API Bindings ---
|
|
288
|
+
|
|
289
|
+
Attachments.registerTelegramAttachmentTool(pi, {
|
|
290
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
291
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
Commands.registerTelegramBridgeCommands(pi, {
|
|
295
|
+
promptForConfig: Setup.createTelegramSetupPromptRuntime({
|
|
296
|
+
getConfig: configStore.get,
|
|
297
|
+
setConfig: configStore.set,
|
|
298
|
+
setupGuard: bridgeRuntime.setup,
|
|
299
|
+
getMe: Api.fetchTelegramBotIdentity,
|
|
300
|
+
persistConfig: configStore.persist,
|
|
301
|
+
startPolling: lockedPollingRuntime.start,
|
|
302
|
+
updateStatus,
|
|
377
303
|
recordRuntimeEvent: runtimeEvents.record,
|
|
378
304
|
}),
|
|
379
|
-
|
|
305
|
+
getStatusLines,
|
|
306
|
+
reloadConfig: configStore.load,
|
|
307
|
+
hasBotToken: configStore.hasBotToken,
|
|
308
|
+
startPolling: lockedPollingRuntime.start,
|
|
309
|
+
stopPolling: lockedPollingRuntime.stop,
|
|
310
|
+
updateStatus,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// --- Lifecycle Hooks ---
|
|
314
|
+
|
|
315
|
+
Lifecycle.registerTelegramLifecycleHooks(pi, {
|
|
316
|
+
...sessionLifecycleRuntime,
|
|
317
|
+
onBeforeAgentStart: Prompts.createTelegramBeforeAgentStartHook(),
|
|
380
318
|
onModelSelect: currentModelRuntime.onModelSelect,
|
|
381
319
|
...Queue.createTelegramAgentLifecycleHooks<
|
|
382
320
|
Queue.PendingTelegramTurn,
|
package/lib/attachments.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram attachment domain helpers
|
|
3
|
-
* Owns attachment queueing and attachment delivery so Telegram file output stays in one domain module
|
|
3
|
+
* Owns telegram_attach registration, attachment queueing, and attachment delivery so Telegram file output stays in one domain module
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { stat } from "node:fs/promises";
|
|
7
7
|
import { basename } from "node:path";
|
|
8
8
|
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "./pi.ts";
|
|
9
12
|
import { buildTelegramMultipartReplyParameters } from "./replies.ts";
|
|
10
13
|
|
|
14
|
+
const MAX_ATTACHMENTS_PER_TURN = 10;
|
|
15
|
+
|
|
11
16
|
export const TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
12
17
|
|
|
13
18
|
export function getTelegramAttachmentByteLimitFromEnv(
|
|
@@ -35,6 +40,21 @@ export interface TelegramAttachmentToolResult {
|
|
|
35
40
|
details: { paths: string[] };
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
export interface TelegramAttachmentRuntimeEventRecorderPort {
|
|
44
|
+
recordRuntimeEvent?: (
|
|
45
|
+
category: string,
|
|
46
|
+
error: unknown,
|
|
47
|
+
details?: Record<string, unknown>,
|
|
48
|
+
) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TelegramAttachmentToolRegistrationDeps extends TelegramAttachmentRuntimeEventRecorderPort {
|
|
52
|
+
maxAttachmentsPerTurn?: number;
|
|
53
|
+
maxAttachmentSizeBytes?: number;
|
|
54
|
+
getActiveTurn: () => TelegramAttachmentQueueTargetView | undefined;
|
|
55
|
+
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
export interface TelegramQueuedAttachmentView {
|
|
39
59
|
path: string;
|
|
40
60
|
fileName: string;
|
|
@@ -69,6 +89,54 @@ function formatTelegramAttachmentSizeLimitError(
|
|
|
69
89
|
return path ? `${message}: ${path}` : message;
|
|
70
90
|
}
|
|
71
91
|
|
|
92
|
+
function formatTelegramAttachmentToolResultText(count: number): string {
|
|
93
|
+
// Pi's compact tool rows need an empty first line to visually separate header and result
|
|
94
|
+
return ["", `Queued ${count} Telegram attachment(s).`].join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function registerTelegramAttachmentTool(
|
|
98
|
+
pi: ExtensionAPI,
|
|
99
|
+
deps: TelegramAttachmentToolRegistrationDeps,
|
|
100
|
+
): void {
|
|
101
|
+
const maxAttachmentsPerTurn =
|
|
102
|
+
deps.maxAttachmentsPerTurn ?? MAX_ATTACHMENTS_PER_TURN;
|
|
103
|
+
const maxAttachmentSizeBytes =
|
|
104
|
+
deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES;
|
|
105
|
+
pi.registerTool({
|
|
106
|
+
name: "telegram_attach",
|
|
107
|
+
label: "Telegram Attach",
|
|
108
|
+
description:
|
|
109
|
+
"Queue one or more local files to be sent with the next Telegram reply.",
|
|
110
|
+
promptSnippet: "Queue local files to be sent with the next Telegram reply.",
|
|
111
|
+
promptGuidelines: [
|
|
112
|
+
"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.",
|
|
113
|
+
],
|
|
114
|
+
parameters: Type.Object({
|
|
115
|
+
paths: Type.Array(
|
|
116
|
+
Type.String({ description: "Local file path to attach" }),
|
|
117
|
+
{ minItems: 1, maxItems: maxAttachmentsPerTurn },
|
|
118
|
+
),
|
|
119
|
+
}),
|
|
120
|
+
async execute(_toolCallId, params) {
|
|
121
|
+
try {
|
|
122
|
+
return await queueTelegramAttachments({
|
|
123
|
+
activeTurn: deps.getActiveTurn(),
|
|
124
|
+
paths: params.paths,
|
|
125
|
+
maxAttachmentsPerTurn,
|
|
126
|
+
maxAttachmentSizeBytes,
|
|
127
|
+
statPath: deps.statPath,
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
deps.recordRuntimeEvent?.("attachment", error, {
|
|
131
|
+
phase: "queue",
|
|
132
|
+
count: params.paths.length,
|
|
133
|
+
});
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
72
140
|
export interface TelegramQueuedAttachmentDeliveryDeps {
|
|
73
141
|
sendMultipart: (
|
|
74
142
|
method: string,
|
|
@@ -141,7 +209,7 @@ export async function queueTelegramAttachments(options: {
|
|
|
141
209
|
content: [
|
|
142
210
|
{
|
|
143
211
|
type: "text",
|
|
144
|
-
text:
|
|
212
|
+
text: formatTelegramAttachmentToolResultText(added.length),
|
|
145
213
|
},
|
|
146
214
|
],
|
|
147
215
|
details: { paths: added },
|
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,
|
|
@@ -52,22 +53,115 @@ export function createTelegramBotCommandRegistrar(
|
|
|
52
53
|
return () => registerTelegramBotCommands(deps);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export interface TelegramBridgeCommandStartPollingOptions {
|
|
57
|
+
force?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TelegramBridgeCommandStartPollingResult {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
message?: string;
|
|
63
|
+
canTakeover?: boolean;
|
|
64
|
+
owner?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TelegramBridgeCommandRegistrationDeps {
|
|
68
|
+
promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
69
|
+
getStatusLines: () => string[];
|
|
70
|
+
reloadConfig: () => Promise<void>;
|
|
71
|
+
hasBotToken: () => boolean;
|
|
72
|
+
startPolling: (
|
|
73
|
+
ctx: ExtensionCommandContext,
|
|
74
|
+
options?: TelegramBridgeCommandStartPollingOptions,
|
|
75
|
+
) =>
|
|
76
|
+
| void
|
|
77
|
+
| Promise<void | TelegramBridgeCommandStartPollingResult>
|
|
78
|
+
| TelegramBridgeCommandStartPollingResult;
|
|
79
|
+
stopPolling: () => Promise<void | string>;
|
|
80
|
+
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
|
|
84
|
+
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatTelegramTakeoverPrompt(
|
|
88
|
+
ctx: ExtensionCommandContext,
|
|
89
|
+
owner?: string,
|
|
90
|
+
): string {
|
|
91
|
+
const theme = ctx.ui.theme;
|
|
92
|
+
const action = theme.fg("warning", "move singleton lock here?");
|
|
93
|
+
const from = theme.fg("muted", "from:");
|
|
94
|
+
const to = theme.fg("muted", "to:");
|
|
95
|
+
const source = owner ?? "another pi instance";
|
|
96
|
+
return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function registerTelegramBridgeCommands(
|
|
100
|
+
pi: ExtensionAPI,
|
|
101
|
+
deps: TelegramBridgeCommandRegistrationDeps,
|
|
102
|
+
): void {
|
|
103
|
+
pi.registerCommand("telegram-setup", {
|
|
104
|
+
description: "Configure Telegram bot token",
|
|
105
|
+
handler: async (_args, ctx) => {
|
|
106
|
+
await deps.promptForConfig(ctx);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
pi.registerCommand("telegram-status", {
|
|
110
|
+
description: "Show Telegram bridge status",
|
|
111
|
+
handler: async (_args, ctx) => {
|
|
112
|
+
ctx.ui.notify(deps.getStatusLines().join("\n"), "info");
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
pi.registerCommand("telegram-connect", {
|
|
116
|
+
description: "Start the Telegram bridge in this pi session",
|
|
117
|
+
handler: async (_args, ctx) => {
|
|
118
|
+
await deps.reloadConfig();
|
|
119
|
+
if (!deps.hasBotToken()) {
|
|
120
|
+
await deps.promptForConfig(ctx);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let result = await deps.startPolling(ctx);
|
|
124
|
+
if (result && !result.ok && result.canTakeover) {
|
|
125
|
+
const confirmed = await ctx.ui.confirm(
|
|
126
|
+
formatTelegramTakeoverTitle(ctx),
|
|
127
|
+
formatTelegramTakeoverPrompt(ctx, result.owner),
|
|
128
|
+
);
|
|
129
|
+
if (!confirmed) {
|
|
130
|
+
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
131
|
+
deps.updateStatus(ctx);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
result = await deps.startPolling(ctx, { force: true });
|
|
135
|
+
}
|
|
136
|
+
if (result?.message) {
|
|
137
|
+
ctx.ui.notify(result.message, result.ok ? "info" : "warning");
|
|
138
|
+
}
|
|
139
|
+
deps.updateStatus(ctx);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
pi.registerCommand("telegram-disconnect", {
|
|
143
|
+
description: "Stop the Telegram bridge in this pi session",
|
|
144
|
+
handler: async (_args, ctx) => {
|
|
145
|
+
const message = await deps.stopPolling();
|
|
146
|
+
if (message) ctx.ui.notify(message, "info");
|
|
147
|
+
deps.updateStatus(ctx);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
55
152
|
export type TelegramCommandAction =
|
|
56
153
|
| { kind: "ignore"; executionMode: "ignored" }
|
|
57
154
|
| { kind: "stop"; executionMode: "immediate" }
|
|
58
155
|
| { kind: "compact"; executionMode: "immediate" }
|
|
59
|
-
| { kind: "status"; executionMode: "
|
|
60
|
-
| { kind: "model"; executionMode: "
|
|
156
|
+
| { kind: "status"; executionMode: "immediate" }
|
|
157
|
+
| { kind: "model"; executionMode: "immediate" }
|
|
61
158
|
| {
|
|
62
159
|
kind: "help";
|
|
63
160
|
commandName: "help" | "start";
|
|
64
161
|
executionMode: "immediate";
|
|
65
162
|
};
|
|
66
163
|
|
|
67
|
-
export type TelegramCommandExecutionMode =
|
|
68
|
-
| "ignored"
|
|
69
|
-
| "immediate"
|
|
70
|
-
| "control-queue";
|
|
164
|
+
export type TelegramCommandExecutionMode = "ignored" | "immediate";
|
|
71
165
|
|
|
72
166
|
export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
73
167
|
handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
@@ -99,8 +193,7 @@ export interface TelegramRuntimeEventRecorderPort {
|
|
|
99
193
|
) => void;
|
|
100
194
|
}
|
|
101
195
|
|
|
102
|
-
export interface TelegramCompactCommandDeps
|
|
103
|
-
extends TelegramRuntimeEventRecorderPort {
|
|
196
|
+
export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort {
|
|
104
197
|
isIdle: () => boolean;
|
|
105
198
|
hasPendingMessages: () => boolean;
|
|
106
199
|
hasActiveTelegramTurn: () => boolean;
|
|
@@ -130,14 +223,6 @@ export interface TelegramHelpCommandDeps {
|
|
|
130
223
|
export type TelegramControlCommandType =
|
|
131
224
|
PendingTelegramControlItem<unknown>["controlType"];
|
|
132
225
|
|
|
133
|
-
export interface TelegramQueuedControlCommandDeps<TContext> {
|
|
134
|
-
enqueueControlItem: (
|
|
135
|
-
controlType: TelegramControlCommandType,
|
|
136
|
-
statusSummary: string,
|
|
137
|
-
execute: (ctx: TContext) => Promise<void>,
|
|
138
|
-
) => void;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
226
|
export interface TelegramCommandRuntimeMessage {
|
|
142
227
|
chat: { id: number };
|
|
143
228
|
message_id: number;
|
|
@@ -380,9 +465,9 @@ export function buildTelegramCommandAction(
|
|
|
380
465
|
case "compact":
|
|
381
466
|
return { kind: "compact", executionMode: "immediate" };
|
|
382
467
|
case "status":
|
|
383
|
-
return { kind: "status", executionMode: "
|
|
468
|
+
return { kind: "status", executionMode: "immediate" };
|
|
384
469
|
case "model":
|
|
385
|
-
return { kind: "model", executionMode: "
|
|
470
|
+
return { kind: "model", executionMode: "immediate" };
|
|
386
471
|
case "help":
|
|
387
472
|
case "start":
|
|
388
473
|
return { kind: "help", commandName, executionMode: "immediate" };
|
|
@@ -483,20 +568,18 @@ export async function handleTelegramHelpCommand(
|
|
|
483
568
|
});
|
|
484
569
|
}
|
|
485
570
|
|
|
486
|
-
export async function handleTelegramStatusCommand<TContext>(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
491
|
-
deps.enqueueControlItem("status", "⚡ status", deps.showStatus);
|
|
571
|
+
export async function handleTelegramStatusCommand<TContext>(deps: {
|
|
572
|
+
ctx: TContext;
|
|
573
|
+
showStatus: (ctx: TContext) => Promise<void>;
|
|
574
|
+
}): Promise<void> {
|
|
575
|
+
await deps.showStatus(deps.ctx);
|
|
492
576
|
}
|
|
493
577
|
|
|
494
|
-
export async function handleTelegramModelCommand<TContext>(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
499
|
-
deps.enqueueControlItem("model", "⚡ model", deps.openModelMenu);
|
|
578
|
+
export async function handleTelegramModelCommand<TContext>(deps: {
|
|
579
|
+
ctx: TContext;
|
|
580
|
+
openModelMenu: (ctx: TContext) => Promise<void>;
|
|
581
|
+
}): Promise<void> {
|
|
582
|
+
await deps.openModelMenu(deps.ctx);
|
|
500
583
|
}
|
|
501
584
|
|
|
502
585
|
export async function executeTelegramCommandAction<TMessage, TContext>(
|
|
@@ -644,21 +727,6 @@ async function handleTelegramCommandRuntime<
|
|
|
644
727
|
deps.sendTextReply(nextMessage, text);
|
|
645
728
|
const updateStatusFor = (commandCtx: TContext) => () =>
|
|
646
729
|
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
730
|
return executeTelegramCommandAction(
|
|
663
731
|
buildTelegramCommandAction(commandName),
|
|
664
732
|
message,
|
|
@@ -694,13 +762,13 @@ async function handleTelegramCommandRuntime<
|
|
|
694
762
|
},
|
|
695
763
|
handleStatus: async (nextMessage, commandCtx) => {
|
|
696
764
|
await handleTelegramStatusCommand<TContext>({
|
|
697
|
-
|
|
765
|
+
ctx: commandCtx,
|
|
698
766
|
showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx),
|
|
699
767
|
});
|
|
700
768
|
},
|
|
701
769
|
handleModel: async (nextMessage, commandCtx) => {
|
|
702
770
|
await handleTelegramModelCommand<TContext>({
|
|
703
|
-
|
|
771
|
+
ctx: commandCtx,
|
|
704
772
|
openModelMenu: (controlCtx) =>
|
|
705
773
|
deps.openModelMenu(nextMessage, controlCtx),
|
|
706
774
|
});
|
package/lib/config.ts
CHANGED
|
@@ -5,10 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import type { TelegramAttachmentHandlerConfig } from "./handlers.ts";
|
|
11
|
+
|
|
12
|
+
function getAgentDir(): string {
|
|
13
|
+
return process.env.PI_CODING_AGENT_DIR
|
|
14
|
+
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
15
|
+
: join(homedir(), ".pi", "agent");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getConfigPath(): string {
|
|
19
|
+
return join(getAgentDir(), "telegram.json");
|
|
20
|
+
}
|
|
12
21
|
|
|
13
22
|
export interface TelegramConfig {
|
|
14
23
|
botToken?: string;
|
|
@@ -16,6 +25,7 @@ export interface TelegramConfig {
|
|
|
16
25
|
botId?: number;
|
|
17
26
|
allowedUserId?: number;
|
|
18
27
|
lastUpdateId?: number;
|
|
28
|
+
attachmentHandlers?: TelegramAttachmentHandlerConfig[];
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export interface TelegramConfigStore {
|
|
@@ -25,6 +35,7 @@ export interface TelegramConfigStore {
|
|
|
25
35
|
getBotToken: () => string | undefined;
|
|
26
36
|
hasBotToken: () => boolean;
|
|
27
37
|
getAllowedUserId: () => number | undefined;
|
|
38
|
+
getAttachmentHandlers: () => TelegramAttachmentHandlerConfig[] | undefined;
|
|
28
39
|
setAllowedUserId: (userId: number) => void;
|
|
29
40
|
load: () => Promise<void>;
|
|
30
41
|
persist: (config?: TelegramConfig) => Promise<void>;
|
|
@@ -64,8 +75,8 @@ export function createTelegramConfigStore(
|
|
|
64
75
|
options: TelegramConfigStoreOptions = {},
|
|
65
76
|
): TelegramConfigStore {
|
|
66
77
|
let config: TelegramConfig = options.initialConfig ?? {};
|
|
67
|
-
const agentDir = options.agentDir ??
|
|
68
|
-
const configPath = options.configPath ??
|
|
78
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
79
|
+
const configPath = options.configPath ?? getConfigPath();
|
|
69
80
|
return {
|
|
70
81
|
get: () => config,
|
|
71
82
|
set: (nextConfig) => {
|
|
@@ -77,6 +88,7 @@ export function createTelegramConfigStore(
|
|
|
77
88
|
getBotToken: () => config.botToken,
|
|
78
89
|
hasBotToken: () => !!config.botToken,
|
|
79
90
|
getAllowedUserId: () => config.allowedUserId,
|
|
91
|
+
getAttachmentHandlers: () => config.attachmentHandlers,
|
|
80
92
|
setAllowedUserId: (userId) => {
|
|
81
93
|
config.allowedUserId = userId;
|
|
82
94
|
},
|