@ebowwa/channel-telegram 1.12.5 → 1.13.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.
Files changed (49) hide show
  1. package/README.md +78 -44
  2. package/dist/commands/index.d.ts +1 -0
  3. package/dist/commands/index.d.ts.map +1 -1
  4. package/dist/commands/index.js +3 -0
  5. package/dist/commands/index.js.map +1 -1
  6. package/dist/commands/restart.d.ts +7 -0
  7. package/dist/commands/restart.d.ts.map +1 -0
  8. package/dist/commands/restart.js +29 -0
  9. package/dist/commands/restart.js.map +1 -0
  10. package/dist/commands/settings.d.ts +8 -0
  11. package/dist/commands/settings.d.ts.map +1 -0
  12. package/dist/commands/settings.js +16 -0
  13. package/dist/commands/settings.js.map +1 -0
  14. package/dist/index.d.ts +83 -29
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +350 -712
  17. package/dist/index.js.map +1 -1
  18. package/package.json +9 -13
  19. package/src/commands/index.ts +3 -0
  20. package/src/commands/restart.ts +41 -0
  21. package/src/commands/settings.ts +24 -0
  22. package/src/index.ts +415 -823
  23. package/dist/mcp/client.d.ts +0 -50
  24. package/dist/mcp/client.d.ts.map +0 -1
  25. package/dist/mcp/client.js +0 -150
  26. package/dist/mcp/client.js.map +0 -1
  27. package/dist/mcp/index.d.ts +0 -5
  28. package/dist/mcp/index.d.ts.map +0 -1
  29. package/dist/mcp/index.js +0 -5
  30. package/dist/mcp/index.js.map +0 -1
  31. package/src/api/fetch-retry.js +0 -96
  32. package/src/api/keys.js +0 -25
  33. package/src/commands/cancel.js +0 -120
  34. package/src/commands/clear.js +0 -59
  35. package/src/commands/doppler.js +0 -118
  36. package/src/commands/git.js +0 -126
  37. package/src/commands/help.js +0 -74
  38. package/src/commands/index.js +0 -65
  39. package/src/commands/logs.js +0 -81
  40. package/src/commands/pause.js +0 -133
  41. package/src/commands/resources.js +0 -87
  42. package/src/commands/resume.js +0 -95
  43. package/src/commands/start.js +0 -68
  44. package/src/commands/status.js +0 -62
  45. package/src/commands/tools.js +0 -67
  46. package/src/commands/toolsoutput.js +0 -85
  47. package/src/commands/types.js +0 -5
  48. package/src/mcp/client.ts +0 -188
  49. package/src/mcp/index.ts +0 -5
package/src/index.ts CHANGED
@@ -1,357 +1,241 @@
1
1
  /**
2
- * GLM Daemon Telegram Bot
2
+ * @ebowwa/channel-telegram
3
3
  *
4
- * Full-featured Telegram bot with GLM-4.7 AI, git status, Doppler integration
4
+ * Telegram channel adapter implementing ChannelConnector.
5
5
  *
6
- * Features:
7
- * - GLM-4.7 AI responses via Z.AI API
8
- * - 429 rate limit handling with exponential backoff
9
- * - /git command for git status + GitHub auth
10
- * - /doppler command for Doppler secrets status
11
- * - Rolling API key support
6
+ * This package handles:
7
+ * - Telegram protocol operations (polling, message delivery)
8
+ * - Built-in commands (/start, /help, /status, etc.)
9
+ * - Typing indicators, message chunking, access control
10
+ * - Message normalization to ChannelMessage format
11
+ *
12
+ * Intelligence (GLM, tools, memory) can be provided by:
13
+ * 1. This package (standalone mode with built-in tools)
14
+ * 2. External daemon/consumer (adapter mode via onMessage handler)
12
15
  */
13
16
 
14
- import TelegramBot from 'node-telegram-bot-api';
15
- import { execSync } from 'child_process';
16
- import { getStore, seedPrompts } from '@ebowwa/structured-prompts';
17
- import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
18
-
19
- import { ConversationMemory } from './conversation-memory';
20
- import { getZAIKey } from './api/keys';
21
- import { fetchWithRetry } from './api/fetch-retry';
22
- import { registerAllCommands, isQuiet } from './commands';
23
- import { MCPClient } from './mcp';
24
-
25
- const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
26
- const TELEGRAM_TEST_CHAT_ID = process.env.TELEGRAM_TEST_CHAT_ID || '';
27
-
28
- // MCP Client instance (initialized on startup)
29
- let mcpClient: MCPClient | null = null;
30
-
31
- // ====================================================================
32
- // Tool Definitions for LLM
33
- // ====================================================================
17
+ import TelegramBot from "node-telegram-bot-api";
18
+
19
+ import {
20
+ type ChannelId,
21
+ type ChannelMessage,
22
+ type ChannelResponse,
23
+ type ChannelCapabilities,
24
+ type MessageSender,
25
+ type MessageContext,
26
+ type MessageHandler,
27
+ createChannelId,
28
+ } from "@ebowwa/channel-types";
29
+
30
+ import { ConversationMemory } from "./conversation-memory.js";
31
+ import { registerAllCommands, isQuiet } from "./commands/index.js";
32
+ import type { Tool } from "./types.js";
33
+
34
+ // Re-export for external use
35
+ export { ConversationMemory } from "./conversation-memory.js";
36
+ export { registerAllCommands, isQuiet } from "./commands/index.js";
37
+ export type { Tool } from "./types.js";
38
+
39
+ // ============================================================
40
+ // CONFIG
41
+ // ============================================================
42
+
43
+ export interface TelegramConfig {
44
+ botToken: string;
45
+ testChatId?: string;
46
+ allowedUsers?: number[];
47
+ allowedChats?: number[];
48
+ /** Optional tools for command handlers (e.g., /tools command) */
49
+ tools?: Tool[];
50
+ /** Optional conversations file path */
51
+ conversationsFile?: string;
52
+ }
34
53
 
35
- export interface Tool {
36
- name: string;
37
- description: string;
38
- parameters: Record<string, unknown>;
39
- handler: (args: Record<string, unknown>) => Promise<string>;
54
+ export function createTelegramConfigFromEnv(): TelegramConfig {
55
+ const allowedUsers = process.env.TELEGRAM_ALLOWED_USERS
56
+ ?.split(",")
57
+ .map((s) => parseInt(s.trim(), 10))
58
+ .filter((n) => !isNaN(n));
59
+
60
+ const allowedChats = process.env.TELEGRAM_ALLOWED_CHATS
61
+ ?.split(",")
62
+ .map((s) => parseInt(s.trim(), 10))
63
+ .filter((n) => !isNaN(n));
64
+
65
+ return {
66
+ botToken: process.env.TELEGRAM_BOT_TOKEN || "",
67
+ testChatId: process.env.TELEGRAM_TEST_CHAT_ID,
68
+ allowedUsers,
69
+ allowedChats,
70
+ conversationsFile: process.env.CONVERSATIONS_FILE,
71
+ };
40
72
  }
41
73
 
42
- const TOOLS: Tool[] = [
43
- {
44
- name: 'read_file',
45
- description: 'Read a file from the filesystem. Use for checking configs, logs, code.',
46
- parameters: {
47
- type: 'object',
48
- properties: {
49
- path: { type: 'string', description: 'Absolute file path to read' }
50
- },
51
- required: ['path']
52
- },
53
- handler: async (args) => {
54
- const path = args.path as string;
55
- try {
56
- if (!existsSync(path)) return `File not found: ${path}`;
57
- const content = readFileSync(path, 'utf-8');
58
- return content.length > 4000 ? content.slice(0, 4000) + '\n...[truncated]' : content;
59
- } catch (e) {
60
- return `Error reading file: ${(e as Error).message}`;
61
- }
62
- }
63
- },
64
- {
65
- name: 'write_file',
66
- description: 'Write content to a file. Creates the file if it does not exist, overwrites if it does.',
67
- parameters: {
68
- type: 'object',
69
- properties: {
70
- path: { type: 'string', description: 'Absolute file path to write' },
71
- content: { type: 'string', description: 'Content to write to the file' }
72
- },
73
- required: ['path', 'content']
74
- },
75
- handler: async (args) => {
76
- const path = args.path as string;
77
- const content = args.content as string;
78
- try {
79
- writeFileSync(path, content, 'utf-8');
80
- return `Successfully wrote ${content.length} bytes to ${path}`;
81
- } catch (e) {
82
- return `Error writing file: ${(e as Error).message}`;
83
- }
84
- }
85
- },
86
- {
87
- name: 'edit_file',
88
- description: 'Edit a file by replacing a specific string with new content.',
89
- parameters: {
90
- type: 'object',
91
- properties: {
92
- path: { type: 'string', description: 'Absolute file path to edit' },
93
- old_string: { type: 'string', description: 'The exact text to find and replace' },
94
- new_string: { type: 'string', description: 'The text to replace it with' }
95
- },
96
- required: ['path', 'old_string', 'new_string']
97
- },
98
- handler: async (args) => {
99
- const path = args.path as string;
100
- const oldStr = args.old_string as string;
101
- const newStr = args.new_string as string;
102
- try {
103
- if (!existsSync(path)) return `File not found: ${path}`;
104
- const content = readFileSync(path, 'utf-8');
105
- if (!content.includes(oldStr)) {
106
- return `String not found in file. First 500 chars:\n${content.slice(0, 500)}`;
107
- }
108
- const newContent = content.replace(oldStr, newStr);
109
- writeFileSync(path, newContent, 'utf-8');
110
- return `Successfully edited ${path}`;
111
- } catch (e) {
112
- return `Error editing file: ${(e as Error).message}`;
113
- }
114
- }
115
- },
116
- {
117
- name: 'append_file',
118
- description: 'Append content to the end of a file. Creates the file if it does not exist.',
119
- parameters: {
120
- type: 'object',
121
- properties: {
122
- path: { type: 'string', description: 'Absolute file path to append to' },
123
- content: { type: 'string', description: 'Content to append to the file' }
124
- },
125
- required: ['path', 'content']
126
- },
127
- handler: async (args) => {
128
- const path = args.path as string;
129
- const content = args.content as string;
130
- try {
131
- appendFileSync(path, content, 'utf-8');
132
- return `Successfully appended ${content.length} bytes to ${path}`;
133
- } catch (e) {
134
- return `Error appending to file: ${(e as Error).message}`;
135
- }
136
- }
137
- },
138
- {
139
- name: 'list_dir',
140
- description: 'List contents of a directory. Shows files and subdirectories.',
141
- parameters: {
142
- type: 'object',
143
- properties: {
144
- path: { type: 'string', description: 'Directory path to list (default: current working directory)' }
145
- }
146
- },
147
- handler: async (args) => {
148
- const path = (args.path as string) || process.cwd();
149
- try {
150
- const items = execSync(`ls -la "${path}" 2>&1`).toString();
151
- return items;
152
- } catch (e) {
153
- return `Error listing directory: ${(e as Error).message}`;
154
- }
155
- }
156
- },
157
- {
158
- name: 'run_command',
159
- description: 'Execute a shell command. Use for git, system info, etc. BE CAREFUL with destructive commands.',
160
- parameters: {
161
- type: 'object',
162
- properties: {
163
- command: { type: 'string', description: 'Shell command to execute' },
164
- cwd: { type: 'string', description: 'Working directory (optional)' }
165
- },
166
- required: ['command']
74
+ // ============================================================
75
+ // TELEGRAM CHANNEL
76
+ // ============================================================
77
+
78
+ export class TelegramChannel {
79
+ readonly id: ChannelId;
80
+ readonly label = "Telegram";
81
+ readonly capabilities: ChannelCapabilities = {
82
+ supports: {
83
+ text: true,
84
+ media: true,
85
+ replies: true,
86
+ threads: false,
87
+ reactions: true,
88
+ editing: true,
89
+ streaming: false,
167
90
  },
168
- handler: async (args) => {
169
- const cmd = args.command as string;
170
- const cwd = (args.cwd as string) || process.cwd();
171
- // Safety check - block dangerous commands
172
- const blocked = ['rm -rf', 'mkfs', 'dd if=', '> /dev/', 'chmod 777'];
173
- if (blocked.some(b => cmd.includes(b))) {
174
- return `Blocked: command contains dangerous pattern`;
175
- }
176
- try {
177
- const result = execSync(cmd, { timeout: 10000, cwd }).toString();
178
- return result || '(no output)';
179
- } catch (e: any) {
180
- return e.stdout?.toString() || e.message;
181
- }
182
- }
183
- },
184
- {
185
- name: 'git_status',
186
- description: 'Check git repository status',
187
- parameters: {
188
- type: 'object',
189
- properties: {
190
- cwd: { type: 'string', description: 'Repository path (optional)' }
191
- }
91
+ media: {
92
+ maxFileSize: 50 * 1024 * 1024, // 50MB
93
+ supportedMimeTypes: ["image/*", "video/*", "audio/*", "application/pdf"],
192
94
  },
193
- handler: async (args) => {
194
- const cwd = (args.cwd as string) || process.cwd();
195
- try {
196
- const status = execSync('git status 2>&1', { cwd }).toString();
197
- const branch = execSync('git branch --show-current 2>&1', { cwd }).toString();
198
- return `Branch: ${branch}\n\n${status}`;
199
- } catch (e) {
200
- return `Error: ${(e as Error).message}`;
201
- }
202
- }
203
- },
204
- {
205
- name: 'get_prompt',
206
- description: 'Get a prompt template from the prompt store',
207
- parameters: {
208
- type: 'object',
209
- properties: {
210
- id: { type: 'string', description: 'Prompt ID to retrieve' }
211
- },
212
- required: ['id']
95
+ rateLimits: {
96
+ messagesPerMinute: 30,
97
+ charactersPerMessage: 4096,
213
98
  },
214
- handler: async (args) => {
215
- const store = getStore(process.env.PROMPTS_FILE);
216
- const prompt = store.get(args.id as string);
217
- return prompt ? JSON.stringify(prompt, null, 2) : `Prompt not found: ${args.id}`;
218
- }
219
- },
220
- {
221
- name: 'list_prompts',
222
- description: 'List all available prompt templates',
223
- parameters: { type: 'object', properties: {} },
224
- handler: async () => {
225
- const store = getStore(process.env.PROMPTS_FILE);
226
- const prompts = store.list();
227
- return prompts.map(p => `- ${p.id}: ${p.name}`).join('\n') || 'No prompts found';
228
- }
229
- },
230
- {
231
- name: 'system_info',
232
- description: 'Get system resource info (CPU, memory, disk, uptime)',
233
- parameters: { type: 'object', properties: {} },
234
- handler: async () => {
235
- try {
236
- const cpu = execSync('nproc 2>/dev/null || echo "unknown"').toString().trim();
237
- const mem = execSync('free -h 2>/dev/null | grep Mem || echo "unknown"').toString().trim();
238
- const disk = execSync('df -h / 2>/dev/null | tail -1 || echo "unknown"').toString().trim();
239
- const uptime = execSync('uptime -p 2>/dev/null || uptime 2>/dev/null || echo "unknown"').toString().trim();
240
- return `CPU cores: ${cpu}\nMemory: ${mem}\nDisk: ${disk}\nUptime: ${uptime}`;
241
- } catch (e) {
242
- return `Error: ${(e as Error).message}`;
243
- }
244
- }
99
+ };
100
+
101
+ private bot: TelegramBot;
102
+ private config: TelegramConfig;
103
+ private memory: ConversationMemory;
104
+ private tools: Tool[];
105
+ private messageHandler?: MessageHandler;
106
+ private typingIntervals: Map<number, NodeJS.Timeout> = new Map();
107
+ private connected = false;
108
+
109
+ constructor(config: TelegramConfig) {
110
+ this.config = config;
111
+ this.id = createChannelId("telegram", "default");
112
+ this.bot = new TelegramBot(config.botToken, { polling: false });
113
+ this.memory = new ConversationMemory(config.conversationsFile);
114
+ this.tools = config.tools || [];
245
115
  }
246
- ];
247
-
248
- // Get all tools (local + MCP) in GLM API format
249
- function getGLMTools(): Array<{ type: string; function: { name: string; description: string; parameters: Record<string, unknown> } }> {
250
- const allTools = [...TOOLS.map(t => ({
251
- type: 'function' as const,
252
- function: {
253
- name: t.name,
254
- description: t.description,
255
- parameters: t.parameters
256
- }
257
- }))];
258
-
259
- // Add MCP tools if connected
260
- if (mcpClient) {
261
- const mcpTools = mcpClient.getTools();
262
- for (const tool of mcpTools) {
263
- allTools.push({
264
- type: 'function',
265
- function: {
266
- name: tool.name,
267
- description: tool.description,
268
- parameters: tool.inputSchema
269
- }
270
- });
116
+
117
+ // ============================================================
118
+ // ChannelConnector Implementation
119
+ // ============================================================
120
+
121
+ async start(): Promise<void> {
122
+ console.log("[TelegramChannel] Starting...");
123
+
124
+ // Start polling
125
+ this.bot = new TelegramBot(this.config.botToken, {
126
+ polling: {
127
+ interval: 300,
128
+ autoStart: true,
129
+ params: {
130
+ allowed_updates: ["message", "edited_message", "message_reaction", "callback_query"],
131
+ },
132
+ },
133
+ });
134
+
135
+ // Register built-in commands
136
+ registerAllCommands(this.bot, this.memory, this.tools);
137
+
138
+ // Setup message routing
139
+ this.setupMessageHandlers();
140
+
141
+ // Register Telegram commands menu
142
+ await this.registerCommands();
143
+
144
+ this.connected = true;
145
+ console.log("[TelegramChannel] Connected and polling");
146
+
147
+ // Send startup notification if configured
148
+ if (this.config.testChatId) {
149
+ await this.sendMessage(Number(this.config.testChatId), "Telegram channel adapter online");
271
150
  }
272
151
  }
273
152
 
274
- return allTools;
275
- }
276
-
277
- // Convert tools to GLM API format (static fallback)
278
- const GLM_TOOLS = TOOLS.map(t => ({
279
- type: 'function',
280
- function: {
281
- name: t.name,
282
- description: t.description,
283
- parameters: t.parameters
153
+ async stop(): Promise<void> {
154
+ console.log("[TelegramChannel] Stopping...");
155
+ await this.bot.stopPolling();
156
+ this.connected = false;
284
157
  }
285
- }));
286
-
287
- // Execute a tool by name (checks local tools first, then MCP)
288
- async function executeTool(name: string, args: Record<string, unknown>): Promise<string> {
289
- // Check local tools first
290
- const tool = TOOLS.find(t => t.name === name);
291
- if (tool) {
292
- return tool.handler(args);
158
+
159
+ /**
160
+ * Set the message handler. The daemon/consumer provides intelligence.
161
+ */
162
+ onMessage(handler: MessageHandler): void {
163
+ this.messageHandler = handler;
293
164
  }
294
165
 
295
- // Check MCP tools
296
- if (mcpClient) {
297
- const mcpTools = mcpClient.getTools();
298
- const mcpTool = mcpTools.find(t => t.name === name);
299
- if (mcpTool) {
300
- try {
301
- return await mcpClient.callTool(name, args);
302
- } catch (error) {
303
- return `MCP tool error: ${(error as Error).message}`;
304
- }
166
+ /**
167
+ * Send a response back to Telegram.
168
+ */
169
+ async send(response: ChannelResponse): Promise<void> {
170
+ const chatId = this.extractChatId(response.replyTo);
171
+ if (!chatId) {
172
+ console.error("[TelegramChannel] Cannot send: no chat ID");
173
+ return;
174
+ }
175
+
176
+ const options: TelegramBot.SendMessageOptions = {};
177
+ if (response.content.replyToOriginal) {
178
+ options.reply_to_message_id = parseInt(response.replyTo.messageId, 10);
305
179
  }
180
+ if (response.content.options?.parseMode) {
181
+ options.parse_mode = response.content.options.parseMode as "Markdown" | "HTML";
182
+ }
183
+
184
+ await this.sendLongMessage(chatId, response.content.text, options);
306
185
  }
307
186
 
308
- return `Unknown tool: ${name}`;
309
- }
187
+ isConnected(): boolean {
188
+ return this.connected;
189
+ }
310
190
 
311
- const ZAI_API_ENDPOINT = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
191
+ // ============================================================
192
+ // Public Helpers
193
+ // ============================================================
312
194
 
313
- export class TelegramGLMBot {
314
- private bot: TelegramBot;
315
- private memory: ConversationMemory;
316
- private typingIntervals: Map<number, NodeJS.Timeout> = new Map();
195
+ /**
196
+ * Get the underlying TelegramBot instance for advanced operations.
197
+ */
198
+ getBot(): TelegramBot {
199
+ return this.bot;
200
+ }
317
201
 
318
- constructor(token: string) {
319
- // Enable polling with all required update types
320
- this.bot = new TelegramBot(token, {
321
- polling: {
322
- interval: 300,
323
- autoStart: true,
324
- params: {
325
- allowed_updates: ['message', 'edited_message', 'message_reaction', 'callback_query']
326
- }
327
- }
328
- });
329
- this.memory = new ConversationMemory(process.env.CONVERSATIONS_FILE);
202
+ /**
203
+ * Get the conversation memory.
204
+ */
205
+ getMemory(): ConversationMemory {
206
+ return this.memory;
330
207
  }
331
208
 
332
209
  /**
333
- * Start periodic typing indicator for a chat
334
- * Telegram typing indicator lasts ~5 seconds, so refresh every 3 seconds
210
+ * Send a simple text message.
335
211
  */
336
- private startTypingIndicator(chatId: number): void {
337
- // Clear any existing interval
338
- this.stopTypingIndicator(chatId);
212
+ async sendMessage(chatId: number, text: string, options?: TelegramBot.SendMessageOptions): Promise<void> {
213
+ await this.bot.sendMessage(chatId, text, options);
214
+ }
339
215
 
340
- // Send initial typing action
341
- this.bot.sendChatAction(chatId, 'typing').catch(() => {});
216
+ /**
217
+ * Send typing indicator.
218
+ */
219
+ async sendTyping(chatId: number): Promise<void> {
220
+ await this.bot.sendChatAction(chatId, "typing");
221
+ }
342
222
 
343
- // Set up periodic refresh every 3 seconds
223
+ /**
224
+ * Start continuous typing indicator (until response is ready).
225
+ */
226
+ startTypingIndicator(chatId: number): void {
227
+ this.stopTypingIndicator(chatId);
228
+ this.bot.sendChatAction(chatId, "typing").catch(() => {});
344
229
  const interval = setInterval(() => {
345
- this.bot.sendChatAction(chatId, 'typing').catch(() => {});
230
+ this.bot.sendChatAction(chatId, "typing").catch(() => {});
346
231
  }, 3000);
347
-
348
232
  this.typingIntervals.set(chatId, interval);
349
233
  }
350
234
 
351
235
  /**
352
- * Stop periodic typing indicator for a chat
236
+ * Stop typing indicator.
353
237
  */
354
- private stopTypingIndicator(chatId: number): void {
238
+ stopTypingIndicator(chatId: number): void {
355
239
  const interval = this.typingIntervals.get(chatId);
356
240
  if (interval) {
357
241
  clearInterval(interval);
@@ -360,572 +244,280 @@ export class TelegramGLMBot {
360
244
  }
361
245
 
362
246
  /**
363
- * Send message, splitting if too long for Telegram (4096 char limit)
247
+ * Check if user/chat is allowed.
364
248
  */
365
- private async sendLongMessage(chatId: number, text: string): Promise<void> {
366
- const MAX_LENGTH = 4000; // Leave buffer for safety
367
-
368
- if (text.length <= MAX_LENGTH) {
369
- await this.bot.sendMessage(chatId, text);
370
- return;
249
+ isAllowed(userId?: number, chatId?: number): boolean {
250
+ if (!this.config.allowedUsers?.length && !this.config.allowedChats?.length) {
251
+ return true;
371
252
  }
372
253
 
373
- // Split into chunks at word boundaries when possible
374
- const chunks: string[] = [];
375
- let remaining = text;
376
-
377
- while (remaining.length > 0) {
378
- if (remaining.length <= MAX_LENGTH) {
379
- chunks.push(remaining);
380
- break;
381
- }
382
-
383
- // Try to find a good break point (newline or space)
384
- let breakPoint = remaining.lastIndexOf('\n', MAX_LENGTH);
385
- if (breakPoint < 1000) {
386
- breakPoint = remaining.lastIndexOf(' ', MAX_LENGTH);
387
- }
388
- if (breakPoint < 1000) {
389
- breakPoint = MAX_LENGTH;
390
- }
391
-
392
- chunks.push(remaining.slice(0, breakPoint));
393
- remaining = remaining.slice(breakPoint).trim();
254
+ if (this.config.allowedUsers?.length && userId) {
255
+ if (this.config.allowedUsers.includes(userId)) return true;
394
256
  }
395
257
 
396
- // Send chunks with continuation indicator
397
- for (let i = 0; i < chunks.length; i++) {
398
- const prefix = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : '';
399
- await this.bot.sendMessage(chatId, prefix + chunks[i]);
258
+ if (this.config.allowedChats?.length && chatId) {
259
+ if (this.config.allowedChats.includes(chatId)) return true;
400
260
  }
401
- }
402
-
403
- async start(): Promise<void> {
404
- console.log('🤖 GLM Daemon Telegram Bot starting with GLM-4.7...');
405
-
406
- // Initialize MCP client for external tool access
407
- mcpClient = new MCPClient();
408
- try {
409
- await mcpClient.connect();
410
- } catch (error) {
411
- console.error('[MCP] Failed to initialize MCP client:', error);
412
- // Continue without MCP - local tools still work
413
- }
414
-
415
- // Register all commands from commands/ directory
416
- registerAllCommands(this.bot, this.memory, TOOLS);
417
-
418
- // Register commands with Telegram for autocomplete
419
- await this.bot.setMyCommands([
420
- { command: 'start', description: 'Start the bot' },
421
- { command: 'help', description: 'Show available commands' },
422
- { command: 'status', description: 'Check bot and API status' },
423
- { command: 'git', description: 'Check git status and GitHub auth' },
424
- { command: 'doppler', description: 'Check Doppler secrets status' },
425
- { command: 'logs', description: 'View recent bot logs' },
426
- { command: 'clear', description: 'Clear conversation history' },
427
- { command: 'tools', description: 'List available tools' },
428
- { command: 'cancel', description: 'Stop current task immediately' },
429
- { command: 'pause', description: 'Pause execution' },
430
- { command: 'resume', description: 'Resume after pause/cancel' },
431
- { command: 'quiet', description: 'Hide tool logging' },
432
- { command: 'verbose', description: 'Show tool logging' },
433
- ]);
434
-
435
- // Handle all text messages with GLM-4.7
436
- this.bot.on('message', async (msg: TelegramBot.Message) => {
437
- if (!msg.text) return;
438
- if (msg.text.startsWith('/')) return;
439
-
440
- const chatId = msg.chat.id;
441
- const userName = msg.from?.first_name || msg.from?.username || 'User';
442
- const messageId = msg.message_id;
443
-
444
- // Check if this is a reply to another message
445
- const replyContext = msg.reply_to_message ? {
446
- isReply: true,
447
- originalMessage: msg.reply_to_message.text || '[non-text message]',
448
- originalFrom: msg.reply_to_message.from?.first_name || 'User',
449
- originalMessageId: msg.reply_to_message.message_id
450
- } : null;
451
-
452
- if (replyContext) {
453
- console.log(`[Telegram] ${userName} (replying to ${replyContext.originalFrom}): ${msg.text}`);
454
- } else {
455
- console.log(`[Telegram] ${userName}: ${msg.text}`);
456
- }
457
261
 
458
- // Build message with reply context if present
459
- let messageWithContext = msg.text;
460
- if (replyContext) {
461
- messageWithContext = `[Replying to ${replyContext.originalFrom}'s message: "${replyContext.originalMessage.slice(0, 200)}${replyContext.originalMessage.length > 200 ? '...' : ''}"]\n\n${msg.text}`;
462
- }
262
+ return false;
263
+ }
463
264
 
464
- // Typing indicator is now handled by getGLMResponse with periodic refresh
465
-
466
- // Get response with conversation history
467
- const response = await this.getGLMResponse(chatId, messageWithContext, userName);
468
-
469
- // Save to memory with message ID and reply context
470
- this.memory.addWithReply(chatId, 'user', msg.text, {
471
- messageId,
472
- replyTo: replyContext ? {
473
- messageId: replyContext.originalMessageId,
474
- text: replyContext.originalMessage,
475
- from: replyContext.originalFrom
476
- } : undefined
477
- });
478
- this.memory.add(chatId, 'assistant', response);
479
-
480
- // Send response - if replying, use reply_to_message_id for threading
481
- if (replyContext) {
482
- await this.bot.sendMessage(chatId, response, {
483
- reply_to_message_id: messageId
484
- });
485
- } else {
486
- await this.sendLongMessage(chatId, response);
265
+ // ============================================================
266
+ // Message Routing (Internal)
267
+ // ============================================================
268
+
269
+ private setupMessageHandlers(): void {
270
+ // Text messages (skip commands - handled by onText handlers)
271
+ this.bot.on("message", async (msg) => {
272
+ if (!msg.text || msg.text.startsWith("/")) return;
273
+ if (!this.isAllowed(msg.from?.id, msg.chat.id)) return;
274
+
275
+ const message = this.createChannelMessage(msg);
276
+ console.log(`[TelegramChannel] Message from ${message.sender.displayName}: ${message.text.slice(0, 50)}...`);
277
+
278
+ // Route to handler if set
279
+ if (this.messageHandler) {
280
+ try {
281
+ const response = await this.messageHandler(message);
282
+ if (response) {
283
+ await this.send(response);
284
+ }
285
+ } catch (error) {
286
+ console.error("[TelegramChannel] Handler error:", error);
287
+ await this.sendMessage(msg.chat.id, `Error: ${(error as Error).message}`);
288
+ }
487
289
  }
488
290
  });
489
291
 
490
- // Polling errors
491
- this.bot.on('polling_error', (error: Error) => {
492
- console.error('[Telegram] Polling error:', error.message);
493
- });
494
-
495
- // ====================================================================
496
- // Message Edit Handler
497
- // ====================================================================
498
- this.bot.on('edited_message', async (msg: TelegramBot.Message) => {
292
+ // Edited messages
293
+ this.bot.on("edited_message", async (msg) => {
499
294
  if (!msg.text) return;
295
+ if (!this.isAllowed(msg.from?.id, msg.chat.id)) return;
500
296
 
501
- const chatId = msg.chat.id;
502
- const userName = msg.from?.first_name || msg.from?.username || 'User';
503
- const messageId = msg.message_id;
504
- const editDate = msg.edit_date || Date.now();
505
-
506
- console.log(`[Telegram] EDIT ${userName}: ${msg.text.slice(0, 50)}...`);
507
-
508
- // Update the message in memory
509
- const updated = this.memory.updateByMessageId(chatId, messageId, msg.text, editDate);
510
-
511
- if (updated) {
512
- // Message was found and updated - optionally re-process with AI
513
- const response = await this.getGLMResponse(
514
- chatId,
515
- `[User edited their previous message to: "${msg.text}"]`,
516
- userName
517
- );
518
- this.memory.add(chatId, 'assistant', response);
519
- await this.sendLongMessage(chatId, `📝 *Edit noted:*\n\n${response}`);
520
- } else {
521
- // Message not in memory (too old or new) - just acknowledge
522
- console.log(`[Telegram] Edit for unknown message ${messageId}`);
523
- }
524
- });
525
-
526
- // ====================================================================
527
- // Reaction Handler (Likes/Emojis)
528
- // ====================================================================
529
- // Note: message_reaction requires Telegram Bot API 5.0+
530
- // The type definitions may not include this yet, so we use any
531
- (this.bot as any).on('message_reaction', async (reaction: {
532
- chat: { id: number };
533
- message_id: number;
534
- user: { id: number; first_name?: string };
535
- old_reaction: Array<{ type: string; emoji?: string; custom_emoji_id?: string }>;
536
- new_reaction: Array<{ type: string; emoji?: string; custom_emoji_id?: string }>;
537
- }) => {
538
- const chatId = reaction.chat.id;
539
- const messageId = reaction.message_id;
540
- const userId = reaction.user.id;
541
- const userName = reaction.user.first_name || 'User';
542
-
543
- // Extract emojis from reactions
544
- const newEmojis = reaction.new_reaction
545
- .filter(r => r.type === 'emoji' && r.emoji)
546
- .map(r => r.emoji!);
547
-
548
- const oldEmojis = reaction.old_reaction
549
- .filter(r => r.type === 'emoji' && r.emoji)
550
- .map(r => r.emoji!);
551
-
552
- // Find added reactions
553
- const added = newEmojis.filter(e => !oldEmojis.includes(e));
554
- const removed = oldEmojis.filter(e => !newEmojis.includes(e));
555
-
556
- console.log(`[Telegram] REACTION by ${userName}: +${added.join(',')} -${removed.join(',')} on msg ${messageId}`);
557
-
558
- // Update memory for each added reaction
559
- for (const emoji of added) {
560
- this.memory.addReaction(chatId, messageId, emoji, userId);
561
- }
562
-
563
- // Update memory for each removed reaction
564
- for (const emoji of removed) {
565
- this.memory.removeReaction(chatId, messageId, emoji, userId);
566
- }
297
+ const message = this.createChannelMessage(msg);
298
+ // Mark as edited in text
299
+ (message as any).text = `[Edited] ${msg.text}`;
567
300
 
568
- // Optionally respond to specific reactions
569
- if (added.includes('👍') || added.includes('❤️')) {
570
- // Positive feedback - could track for learning
571
- console.log(`[Telegram] Positive feedback received on message ${messageId}`);
301
+ if (this.messageHandler) {
302
+ try {
303
+ const response = await this.messageHandler(message);
304
+ if (response) {
305
+ await this.send(response);
306
+ }
307
+ } catch (error) {
308
+ console.error("[TelegramChannel] Handler error:", error);
309
+ }
572
310
  }
311
+ });
573
312
 
574
- if (added.includes('👎')) {
575
- // Negative feedback - could ask for clarification
576
- await this.bot.sendMessage(chatId, `Thanks for the feedback, ${userName}. I'll try to do better!`);
313
+ // Reactions
314
+ (this.bot as any).on("message_reaction", async (reaction: any) => {
315
+ const message = {
316
+ messageId: reaction.message_id?.toString(),
317
+ channelId: this.id,
318
+ timestamp: new Date(),
319
+ sender: {
320
+ id: reaction.user?.id?.toString(),
321
+ displayName: reaction.user?.first_name || "User",
322
+ },
323
+ text: "",
324
+ context: { isDM: reaction.chat?.type === "private" },
325
+ metadata: { type: "reaction", reaction: reaction.new_reaction },
326
+ };
327
+
328
+ if (this.messageHandler) {
329
+ await this.messageHandler(message as any);
577
330
  }
578
331
  });
579
332
 
580
- // ====================================================================
581
- // Callback Query Handler (Inline Button Reactions)
582
- // ====================================================================
583
- this.bot.on('callback_query', async (query: TelegramBot.CallbackQuery) => {
333
+ // Callback queries (button clicks)
334
+ this.bot.on("callback_query", async (query) => {
584
335
  const chatId = query.message?.chat.id;
585
- const messageId = query.message?.message_id;
586
- const data = query.data;
587
- const userName = query.from.first_name || 'User';
588
-
589
- if (!chatId || !data) {
336
+ if (!chatId || !query.data) {
590
337
  await this.bot.answerCallbackQuery(query.id);
591
338
  return;
592
339
  }
593
340
 
594
- console.log(`[Telegram] CALLBACK ${userName}: ${data}`);
595
-
596
- // Parse callback data (format: "action:value")
597
- const [action, value] = data.split(':');
598
-
599
- switch (action) {
600
- case 'react': {
601
- // Toggle reaction via inline button
602
- const count = parseInt(value) || 0;
603
- const newCount = count + 1;
604
- await this.bot.editMessageReplyMarkup(
605
- {
606
- inline_keyboard: [[
607
- { text: `👍 ${newCount}`, callback_data: `react:${newCount}` }
608
- ]]
609
- },
610
- { chat_id: chatId, message_id: messageId }
611
- );
612
- break;
613
- }
614
-
615
- case 'approve': {
616
- // Approval action (useful for workflows)
617
- await this.bot.sendMessage(chatId, `✅ Approved by ${userName}`);
618
- // Could trigger follow-up actions here
619
- break;
620
- }
621
-
622
- case 'reject': {
623
- // Rejection action
624
- await this.bot.sendMessage(chatId, `❌ Rejected by ${userName}`);
625
- break;
626
- }
627
-
628
- case 'cancel': {
629
- // Cancel current task
630
- await this.bot.sendMessage(chatId, `🛑 Task cancelled by ${userName}`);
631
- break;
632
- }
633
-
634
- default: {
635
- // Unknown callback - let AI handle it
636
- const response = await this.getGLMResponse(
637
- chatId,
638
- `[User clicked button: "${data}". Respond appropriately.]`,
639
- userName
640
- );
641
- await this.bot.sendMessage(chatId, response);
341
+ const message = {
342
+ messageId: query.id,
343
+ channelId: this.id,
344
+ timestamp: new Date(),
345
+ sender: {
346
+ id: query.from?.id?.toString(),
347
+ displayName: query.from?.first_name || "User",
348
+ },
349
+ text: query.data,
350
+ context: { isDM: query.message?.chat.type === "private" },
351
+ metadata: { type: "callback" },
352
+ };
353
+
354
+ if (this.messageHandler) {
355
+ try {
356
+ const response = await this.messageHandler(message as any);
357
+ if (response) {
358
+ await this.send(response);
359
+ }
360
+ } catch (error) {
361
+ console.error("[TelegramChannel] Callback handler error:", error);
642
362
  }
643
363
  }
644
364
 
645
- // Always acknowledge the callback to remove loading state
646
365
  await this.bot.answerCallbackQuery(query.id);
647
366
  });
648
367
 
649
- console.log('✅ Telegram bot is running with GLM-4.7!');
368
+ // Polling errors
369
+ this.bot.on("polling_error", (error) => {
370
+ console.error("[TelegramChannel] Polling error:", error.message);
371
+ });
372
+ }
650
373
 
651
- if (TELEGRAM_TEST_CHAT_ID) {
652
- await this.sendTestMessage(Number(TELEGRAM_TEST_CHAT_ID));
653
- // Check for crash and report
654
- await this.checkCrashAndReport(Number(TELEGRAM_TEST_CHAT_ID));
655
- } else {
656
- console.log('\n💡 Tip: Get your chat ID from @userinfobot and set TELEGRAM_TEST_CHAT_ID');
657
- }
374
+ /**
375
+ * Register Telegram command menu
376
+ */
377
+ private async registerCommands(): Promise<void> {
378
+ await this.bot.setMyCommands([
379
+ { command: "start", description: "Start the bot" },
380
+ { command: "help", description: "Show available commands" },
381
+ { command: "status", description: "Check bot status" },
382
+ { command: "git", description: "Check git status" },
383
+ { command: "doppler", description: "Check Doppler secrets" },
384
+ { command: "logs", description: "View recent logs" },
385
+ { command: "clear", description: "Clear conversation history" },
386
+ { command: "tools", description: "List available tools" },
387
+ { command: "restart", description: "Restart the bot service" },
388
+ { command: "cancel", description: "Stop current task" },
389
+ { command: "quiet", description: "Hide tool logging" },
390
+ { command: "verbose", description: "Show tool logging" },
391
+ ]);
658
392
  }
659
393
 
660
- async getGLMResponse(chatId: number, userMessage: string, userName: string): Promise<string> {
661
- const apiKey = getZAIKey();
662
- if (!apiKey) {
663
- return '⚠️ Z_AI_API_KEY not configured in Doppler secrets.\n\nPlease set Z_AI_API_KEY to use GLM-4.7.';
394
+ /**
395
+ * Normalize Telegram message to ChannelMessage format.
396
+ */
397
+ private createChannelMessage(msg: TelegramBot.Message): ChannelMessage {
398
+ const sender: MessageSender = {
399
+ id: msg.from?.id?.toString() || msg.chat.id.toString(),
400
+ username: msg.from?.username,
401
+ displayName: msg.from?.first_name || msg.from?.username || "User",
402
+ isBot: msg.from?.is_bot || false,
403
+ };
404
+
405
+ const context: MessageContext = {
406
+ isDM: msg.chat.type === "private",
407
+ groupName: msg.chat.type !== "private" ? msg.chat.title : undefined,
408
+ };
409
+
410
+ // Build reply context
411
+ let replyText: string | undefined;
412
+ if (msg.reply_to_message?.text) {
413
+ const replyFrom = msg.reply_to_message.from?.first_name || "User";
414
+ replyText = `[Replying to ${replyFrom}: "${msg.reply_to_message.text.slice(0, 100)}"]`;
664
415
  }
665
416
 
666
- // Start typing indicator - will refresh every 3 seconds during processing
667
- this.startTypingIndicator(chatId);
668
-
669
- // Get system prompt from structured-prompts store
670
- const store = getStore(process.env.PROMPTS_FILE);
671
- const systemPrompt = store.get('glm-daemon-system');
672
- const systemContent = systemPrompt?.template ?? 'You are GLM Daemon, a helpful AI assistant.';
673
- const temperature = systemPrompt?.metadata?.temperature ?? 0.7;
674
- const maxTokens = systemPrompt?.metadata?.maxTokens ?? 2048;
675
-
676
- // Build messages with conversation history
677
- const history = this.memory.get(chatId);
678
- const messages: Array<{ role: string; content?: string; tool_calls?: unknown[]; name?: string }> = [
679
- { role: 'system', content: systemContent },
680
- ...history,
681
- { role: 'user', content: userMessage }
682
- ];
683
-
684
- try {
685
- console.log(`🔄 Calling GLM-4.7 API for: ${userMessage} (${history.length} history)`);
686
-
687
- // Loop to handle tool calls
688
- let iterations = 0;
689
- const maxIterations = 50; // Allow more tool calls for complex tasks
690
-
691
- while (iterations < maxIterations) {
692
- iterations++;
693
-
694
- const response = await fetchWithRetry(ZAI_API_ENDPOINT, {
695
- method: 'POST',
696
- headers: {
697
- 'Content-Type': 'application/json',
698
- 'Authorization': `Bearer ${apiKey}`
699
- },
700
- body: JSON.stringify({
701
- model: 'glm-4.7',
702
- messages,
703
- tools: getGLMTools(),
704
- temperature,
705
- max_tokens: maxTokens
706
- })
707
- }, 3, 1000);
708
-
709
- const data = await response.json() as {
710
- choices?: Array<{
711
- message?: {
712
- content?: string;
713
- tool_calls?: Array<{
714
- id: string;
715
- function: { name: string; arguments: string };
716
- }>;
717
- }
718
- }>
719
- };
720
-
721
- const message = data.choices?.[0]?.message;
722
- if (!message) {
723
- console.error('❌ Unexpected API response format:', data);
724
- return '❌ Unexpected response from AI API.';
725
- }
726
-
727
- // Check if LLM wants to call tools
728
- if (message.tool_calls && message.tool_calls.length > 0) {
729
- console.log(`🔧 LLM calling ${message.tool_calls.length} tool(s)`);
730
-
731
- // Add assistant message with tool calls to history
732
- messages.push({
733
- role: 'assistant',
734
- tool_calls: message.tool_calls
735
- });
736
-
737
- // Check quiet mode for Telegram messages
738
- const quiet = isQuiet();
739
-
740
- // Execute each tool and add results
741
- for (const toolCall of message.tool_calls) {
742
- const toolName = toolCall.function.name;
743
- const args = JSON.parse(toolCall.function.arguments);
744
-
745
- console.log(` → ${toolName}(${JSON.stringify(args)})`);
746
-
747
- // Send tool call notification to Telegram (skip if quiet)
748
- if (!quiet) {
749
- // Sanitize to prevent entity parsing errors - strip Markdown special chars
750
- const sanitizeForTelegram = (str: string): string => {
751
- return str
752
- .replace(/[_*[\]()~`>#+=|{}.!-]/g, '') // Remove Markdown special chars
753
- .replace(/\\/g, '') // Remove backslashes
754
- .slice(0, 100); // Truncate to prevent long messages
755
- };
756
- const argsPreview = Object.keys(args).length > 0
757
- ? sanitizeForTelegram(JSON.stringify(args))
758
- : '';
759
- await this.bot.sendMessage(chatId, `🔧 ${toolName}${argsPreview ? ' ' + argsPreview : ''}`);
760
- }
761
-
762
- // Execute tool locally
763
- const result = await executeTool(toolName, args);
764
-
765
- console.log(` ← ${result.slice(0, 100)}...`);
766
-
767
- // Send result to Telegram (skip if quiet)
768
- if (!quiet) {
769
- const sanitizedResult = result
770
- .replace(/[_*[\]()~`>#+=|{}.!-]/g, '')
771
- .replace(/\\/g, '');
772
- const resultPreview = sanitizedResult.length > 500 ? sanitizedResult.slice(0, 500) + ' [truncated]' : sanitizedResult;
773
- await this.bot.sendMessage(chatId, `📥 ${resultPreview}`);
774
- }
775
-
776
- // Add tool result to messages
777
- messages.push({
778
- role: 'tool',
779
- name: toolName,
780
- content: result
781
- });
417
+ return {
418
+ messageId: msg.message_id.toString(),
419
+ channelId: this.id,
420
+ timestamp: new Date(msg.date * 1000),
421
+ sender,
422
+ text: replyText ? `${replyText}\n\n${msg.text || ""}` : (msg.text || ""),
423
+ context,
424
+ replyTo: msg.reply_to_message
425
+ ? {
426
+ messageId: msg.reply_to_message.message_id.toString(),
427
+ channelId: this.id,
782
428
  }
429
+ : undefined,
430
+ };
431
+ }
783
432
 
784
- // Continue loop to get final response
785
- continue;
786
- }
433
+ private extractChatId(replyTo: { messageId: string; channelId: ChannelId }): number | null {
434
+ const accountId = replyTo.channelId.accountId;
435
+ const parsed = parseInt(accountId, 10);
436
+ if (!isNaN(parsed)) return parsed;
437
+ return null;
438
+ }
787
439
 
788
- // No tool calls - we have the final response
789
- if (message.content) {
790
- console.log(`✅ GLM-4.7 response: ${message.content.slice(0, 100)}...`);
791
- return message.content;
792
- }
440
+ /**
441
+ * Send long message by splitting into chunks.
442
+ */
443
+ private async sendLongMessage(
444
+ chatId: number,
445
+ text: string,
446
+ options?: TelegramBot.SendMessageOptions
447
+ ): Promise<void> {
448
+ const MAX_LENGTH = 4000;
793
449
 
794
- // No content and no tool calls - something weird
795
- return '❌ Empty response from AI.';
796
- }
450
+ if (text.length <= MAX_LENGTH) {
451
+ await this.bot.sendMessage(chatId, text, options);
452
+ return;
453
+ }
797
454
 
798
- return '⚠️ Reached tool limit (50). The AI was being very thorough! Try a more specific request.';
799
- } catch (error) {
800
- console.error('❌ Error calling GLM-4.7:', (error as Error).message);
455
+ const chunks: string[] = [];
456
+ let remaining = text;
801
457
 
802
- if ((error as Error).message.includes('429')) {
803
- return '⚠️ Rate limited by API. Please try again in a moment.';
458
+ while (remaining.length > 0) {
459
+ if (remaining.length <= MAX_LENGTH) {
460
+ chunks.push(remaining);
461
+ break;
804
462
  }
805
- return `❌ Error: ${(error as Error).message}`;
806
- } finally {
807
- // Always stop typing indicator when done
808
- this.stopTypingIndicator(chatId);
809
- }
810
- }
811
463
 
812
- async sendTestMessage(chatId: number): Promise<void> {
813
- const apiKey = getZAIKey();
814
- await this.bot.sendMessage(
815
- chatId,
816
- '✅ GLM Daemon Telegram Bot is NOW ONLINE!\n\n' +
817
- '🎉 Connected from seed-node-prod!\n\n' +
818
- `🧠 AI: ${apiKey ? 'GLM-4.7 via Z.AI' : 'Not configured'}\n\n` +
819
- 'Commands:\n' +
820
- '/start - Show welcome message\n' +
821
- '/status - Check API status\n' +
822
- '/git - Git & GitHub status\n' +
823
- '/doppler - Doppler config\n' +
824
- '/help - Show all commands\n' +
825
- 'Any message - Chat with GLM-4.7 AI'
826
- );
827
- console.log(`✅ Test message sent to chat ${chatId}`);
828
- }
464
+ let breakPoint = remaining.lastIndexOf("\n", MAX_LENGTH);
465
+ if (breakPoint < 1000) breakPoint = remaining.lastIndexOf(" ", MAX_LENGTH);
466
+ if (breakPoint < 1000) breakPoint = MAX_LENGTH;
829
467
 
830
- async stop(): Promise<void> {
831
- console.log('🛑 Stopping Telegram bot...');
832
- this.bot.stopPolling();
833
- }
468
+ chunks.push(remaining.slice(0, breakPoint));
469
+ remaining = remaining.slice(breakPoint).trim();
470
+ }
834
471
 
835
- /**
836
- * Check for crash recovery - analyze previous session and report
837
- * Always sends a startup message (crash or normal)
838
- */
839
- async checkCrashAndReport(chatId: number): Promise<void> {
840
- try {
841
- // Get previous boot/session logs to check for crash
842
- const crashLog = execSync(
843
- 'journalctl -u telegram-bot -b -1 -n 50 --no-pager 2>/dev/null || journalctl -u telegram-bot --since "5 minutes ago" -n 50 --no-pager 2>/dev/null',
844
- { encoding: 'utf-8', timeout: 5000 }
845
- ).trim();
846
-
847
- // Look for crash indicators
848
- const crashIndicators = ['Error:', 'error', 'crashed', 'killed', 'ETELEGRAM', '400 Bad Request', 'exception'];
849
- const hasCrash = crashLog && crashIndicators.some(ind => crashLog.toLowerCase().includes(ind.toLowerCase()));
850
-
851
- if (hasCrash) {
852
- // Crash detected - analyze and report
853
- console.log('🔍 Crash detected, analyzing...');
854
- await this.bot.sendMessage(chatId, '🔄 I detected a crash in my previous session. Analyzing...');
855
-
856
- // Extract error lines
857
- const errorLines = crashLog.split('\n')
858
- .filter(line => crashIndicators.some(ind => line.toLowerCase().includes(ind.toLowerCase())))
859
- .slice(-10)
860
- .join('\n');
861
-
862
- if (errorLines) {
863
- // Ask LLM to diagnose
864
- const diagnosis = await this.getGLMResponse(
865
- chatId,
866
- `Analyze this crash log and tell me what went wrong in one short paragraph, then suggest a fix:
867
-
868
- \`\`\`
869
- ${errorLines.slice(0, 1000)}
870
- \`\`\`
871
-
872
- Format: "Issue: [what happened]. Fix: [how to fix it]"`,
873
- 'CrashAnalyzer'
874
- );
875
-
876
- await this.sendLongMessage(chatId, `🏥 Crash Diagnosis:\n\n${diagnosis}`);
877
- }
878
- } else {
879
- // Normal start - no crash detected
880
- await this.bot.sendMessage(
881
- chatId,
882
- '✅ Bot started normally.\n\n' +
883
- 'Previous session ended cleanly. Ready to assist!'
884
- );
885
- }
886
- } catch (e) {
887
- // If we can't check logs, still notify of normal start
888
- console.log('Crash check skipped:', (e as Error).message);
889
- await this.bot.sendMessage(
890
- chatId,
891
- '✅ Bot started fresh.\n\n' +
892
- 'No previous session data available. Ready to assist!'
893
- );
472
+ for (let i = 0; i < chunks.length; i++) {
473
+ const prefix = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
474
+ await this.bot.sendMessage(chatId, prefix + chunks[i], options);
894
475
  }
895
476
  }
896
477
  }
897
478
 
898
- // Main entry point
479
+ // ============================================================
480
+ // FACTORY
481
+ // ============================================================
482
+
483
+ export function createTelegramChannel(config: TelegramConfig): TelegramChannel {
484
+ return new TelegramChannel(config);
485
+ }
486
+
487
+ // ============================================================
488
+ // MAIN ENTRY POINT (for standalone testing)
489
+ // ============================================================
490
+
899
491
  async function main() {
900
- if (!TELEGRAM_BOT_TOKEN) {
901
- console.error('❌ TELEGRAM_BOT_TOKEN not found in Doppler secrets');
902
- console.error('Make sure to set in your seed/prd Doppler project');
903
- process.exit(1);
904
- }
492
+ const config = createTelegramConfigFromEnv();
905
493
 
906
- // Ensure prompts are seeded
907
- const promptsFile = process.env.PROMPTS_FILE;
908
- const store = getStore(promptsFile);
909
- if (!store.get('glm-daemon-system')) {
910
- console.log('📝 Seeding prompts...');
911
- seedPrompts(promptsFile);
494
+ if (!config.botToken) {
495
+ console.error("TELEGRAM_BOT_TOKEN not found in environment");
496
+ process.exit(1);
912
497
  }
913
498
 
914
- console.log(`📱 Bot token loaded: ${TELEGRAM_BOT_TOKEN.substring(0, 10)}...`);
915
- console.log(`🤖 Bot name: @SimulationapiBot`);
499
+ const channel = createTelegramChannel(config);
916
500
 
917
- const bot = new TelegramGLMBot(TELEGRAM_BOT_TOKEN);
918
- await bot.start();
501
+ // Example: Echo handler for testing
502
+ channel.onMessage(async (msg) => {
503
+ console.log(`Received: ${msg.text}`);
504
+ return {
505
+ content: { text: `Echo: ${msg.text}` },
506
+ replyTo: { messageId: msg.messageId, channelId: msg.channelId },
507
+ };
508
+ });
919
509
 
920
- process.on('SIGINT', async () => {
921
- await bot.stop();
510
+ process.on("SIGINT", async () => {
511
+ await channel.stop();
922
512
  process.exit(0);
923
513
  });
924
514
 
925
- process.on('SIGTERM', async () => {
926
- await bot.stop();
515
+ process.on("SIGTERM", async () => {
516
+ await channel.stop();
927
517
  process.exit(0);
928
518
  });
519
+
520
+ await channel.start();
929
521
  }
930
522
 
931
523
  main().catch(console.error);