pi-telegram-plus 0.0.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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Split text for Telegram's 4096-byte message limit.
3
+ * Handles UTF-8 boundaries safely and prefers word/newline breaks.
4
+ */
5
+
6
+ const TELEGRAM_TEXT_LIMIT = 4096;
7
+ const SAFE_TEXT_CHUNK = 3600;
8
+
9
+ export function stripHtml(text: string): string {
10
+ return text
11
+ .replace(/<br\s*\/?>/gi, "\n")
12
+ .replace(/<\/p>/gi, "\n")
13
+ .replace(/<[^>]+>/g, "")
14
+ .replace(/&lt;/g, "<")
15
+ .replace(/&gt;/g, ">")
16
+ .replace(/&amp;/g, "&");
17
+ }
18
+
19
+ function byteLength(text: string): number {
20
+ return Buffer.byteLength(text, "utf8");
21
+ }
22
+
23
+ export function takeUtf8Prefix(text: string, maxBytes: number): { head: string; tail: string } {
24
+ let bytes = 0;
25
+ let end = 0;
26
+ for (const char of text) {
27
+ const next = bytes + Buffer.byteLength(char, "utf8");
28
+ if (next > maxBytes) break;
29
+ bytes = next;
30
+ end += char.length;
31
+ }
32
+ return { head: text.slice(0, end), tail: text.slice(end) };
33
+ }
34
+
35
+ export function splitTelegramText(text: string): string[] {
36
+ if (byteLength(text) <= TELEGRAM_TEXT_LIMIT) return [text];
37
+ const chunks: string[] = [];
38
+ let rest = text;
39
+
40
+ while (byteLength(rest) > TELEGRAM_TEXT_LIMIT) {
41
+ const { head: safeHead } = takeUtf8Prefix(rest, SAFE_TEXT_CHUNK);
42
+ let cut = Math.max(safeHead.lastIndexOf("\n"), safeHead.lastIndexOf(" "));
43
+ if (cut < 500) cut = safeHead.length;
44
+
45
+ let head = rest.slice(0, cut);
46
+ let tail = rest.slice(cut);
47
+ // Final guard: even after choosing a delimiter, keep the chunk byte-safe.
48
+ if (byteLength(head) > TELEGRAM_TEXT_LIMIT) {
49
+ const split = takeUtf8Prefix(rest, SAFE_TEXT_CHUNK);
50
+ head = split.head;
51
+ tail = split.tail;
52
+ }
53
+ chunks.push(head);
54
+ // Preserve exact content across chunks; do not trim leading whitespace/newlines.
55
+ rest = tail;
56
+ }
57
+ if (rest) chunks.push(rest);
58
+ return chunks;
59
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,123 @@
1
+ import type { AgentSession, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export type TelegramRenderLevel = "hidden" | "brief" | "full";
4
+ export type TelegramMessageMode = "queue" | "steer";
5
+
6
+ export const RENDER_LEVELS: readonly TelegramRenderLevel[] = ["hidden", "brief", "full"] as const;
7
+ export const MODE_VALUES: readonly TelegramMessageMode[] = ["queue", "steer"] as const;
8
+
9
+ export type TelegramConfigStore = {
10
+ version: 2;
11
+ global?: TelegramConfig;
12
+ workspaces?: TelegramWorkspaceConfig[];
13
+ };
14
+
15
+ export type TelegramWorkspaceConfig = {
16
+ path: string;
17
+ config: TelegramConfig;
18
+ };
19
+
20
+ export type ResolvedTelegramConfig = {
21
+ store: TelegramConfigStore;
22
+ scope: "global" | "workspace";
23
+ workspacePath?: string;
24
+ config: TelegramConfig;
25
+ };
26
+
27
+ export type TelegramConfig = {
28
+ botToken?: string;
29
+ botUsername?: string;
30
+ telegramEnabled?: boolean;
31
+ allowedUserId?: number;
32
+ /** Last chat that interacted with the bot. */
33
+ activeChatId?: number;
34
+ lastUpdateId?: number;
35
+ /** How to render tool executions in Telegram. */
36
+ tool?: TelegramRenderLevel;
37
+ /** How to render thinking blocks in Telegram. */
38
+ thinking?: TelegramRenderLevel;
39
+ /** How to handle incoming messages while the agent is running.
40
+ * "steer" — messages inject into the current turn via streamingBehavior (default).
41
+ * "queue" — messages wait for the current turn to finish.
42
+ */
43
+ messageMode?: TelegramMessageMode;
44
+ /** Number of retries for failed Telegram API calls (0 = no retry, default 3). */
45
+ retryCount?: number;
46
+ };
47
+
48
+ export type TelegramPhotoSize = {
49
+ file_id: string;
50
+ file_size?: number;
51
+ };
52
+
53
+ export type TelegramDocument = {
54
+ file_id: string;
55
+ file_name?: string;
56
+ mime_type?: string;
57
+ };
58
+
59
+ export type TelegramMessage = {
60
+ message_id: number;
61
+ text?: string;
62
+ caption?: string;
63
+ photo?: TelegramPhotoSize[];
64
+ document?: TelegramDocument;
65
+ video?: TelegramDocument;
66
+ audio?: TelegramDocument;
67
+ voice?: TelegramDocument;
68
+ animation?: TelegramDocument;
69
+ sticker?: TelegramDocument;
70
+ chat?: { id?: number };
71
+ from?: { id?: number };
72
+ reply_to_message?: { message_id?: number };
73
+ };
74
+
75
+ export type TelegramCallbackQuery = {
76
+ id: string;
77
+ data?: string;
78
+ message?: TelegramMessage;
79
+ from?: { id?: number };
80
+ };
81
+
82
+ export type TelegramUpdate = {
83
+ update_id: number;
84
+ message?: TelegramMessage;
85
+ callback_query?: TelegramCallbackQuery;
86
+ };
87
+
88
+ export type TelegramButton = { text: string; value: string };
89
+ export type PendingInputResolver = (value: string | boolean | undefined) => void;
90
+
91
+ export type TelegramSentMessage = { message_id: number };
92
+
93
+ export type TelegramTurn = {
94
+ chatId: number;
95
+ /** Message to edit in-place for callback-button initiated turns. */
96
+ replaceMessageId?: number;
97
+ queuedAttachments: Array<{ path: string; fileName: string }>;
98
+ attachmentsSent?: boolean;
99
+ };
100
+
101
+ export type TelegramTransport = {
102
+ removeInlineKeyboard(chatId: number, messageId: number): Promise<void>;
103
+ sendText(chatId: number, text: string): Promise<TelegramSentMessage[]>;
104
+ sendButtons(
105
+ chatId: number,
106
+ text: string,
107
+ rows: TelegramButton[][],
108
+ ): Promise<TelegramSentMessage>;
109
+ editText(chatId: number, messageId: number, text: string): Promise<void>;
110
+ editButtons(chatId: number, messageId: number, text: string, rows: TelegramButton[][]): Promise<void>;
111
+ answerCallbackQuery(callbackQueryId: string, text?: string): Promise<void>;
112
+ deleteMessage(chatId: number, messageId: number): Promise<void>;
113
+ sendDocument(chatId: number, path: string, caption?: string, signal?: AbortSignal): Promise<void>;
114
+ sendPhoto(chatId: number, data: string, caption?: string, isPath?: boolean, signal?: AbortSignal): Promise<void>;
115
+ sendChatAction(chatId: number, action: string): Promise<void>;
116
+ };
117
+
118
+ export type CapturedAgentSession = AgentSession & {
119
+ extensionRunner: AgentSession["extensionRunner"] & {
120
+ getUIContext(): ExtensionUIContext;
121
+ setUIContext(ui?: ExtensionUIContext): void;
122
+ };
123
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package",
3
+ "name": "pi-telegram-plus",
4
+ "version": "0.0.1",
5
+ "description": "Full Telegram control of pi coding agent — commands, menus, interactive UI, model management, and more",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "telegram",
10
+ "coding-agent",
11
+ "remote-control"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "jalyfeng",
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=22.19.0"
18
+ },
19
+ "files": [
20
+ "index.ts",
21
+ "lib/**/*.ts",
22
+ "!lib/__tests__/",
23
+ "pi-host.d.ts"
24
+ ],
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "prepublishOnly": "npm test && npm run typecheck"
30
+ },
31
+ "pi": {
32
+ "extensions": [
33
+ "./index.ts"
34
+ ],
35
+ "image": "https://pi.dev/logo-auto.svg",
36
+ "video": ""
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/jalyfeng/pi-telegram-plus"
41
+ },
42
+ "peerDependencies": {
43
+ "@earendil-works/pi-coding-agent": "*",
44
+ "typebox": "*"
45
+ },
46
+ "dependencies": {
47
+ "marked": "^18.0.4"
48
+ },
49
+ "devDependencies": {
50
+ "typescript": "^6.0.3",
51
+ "vitest": "^4.1.8"
52
+ }
53
+ }
package/pi-host.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ declare module "@earendil-works/pi-ai" {
2
+ export type Model = {
3
+ id: string;
4
+ name?: string;
5
+ provider?: string;
6
+ [key: string]: unknown;
7
+ };
8
+ }