telegram-claude-mcp 1.0.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/bin/run.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const srcPath = join(__dirname, '..', 'src', 'index.ts');
8
+ const tsxPath = join(__dirname, '..', 'node_modules', '.bin', 'tsx');
9
+
10
+ const child = spawn(tsxPath, [srcPath], {
11
+ stdio: 'inherit',
12
+ env: process.env,
13
+ });
14
+
15
+ child.on('exit', (code) => {
16
+ process.exit(code ?? 0);
17
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "telegram-claude-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that lets Claude message you on Telegram",
5
+ "author": "Geravant",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Geravant/telegram-claude"
10
+ },
11
+ "keywords": [
12
+ "claude",
13
+ "telegram",
14
+ "mcp",
15
+ "claude-code",
16
+ "bot",
17
+ "messaging"
18
+ ],
19
+ "type": "module",
20
+ "main": "src/index.ts",
21
+ "bin": {
22
+ "telegram-claude-mcp": "./bin/run.js"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "bin"
27
+ ],
28
+ "scripts": {
29
+ "start": "node --import tsx src/index.ts",
30
+ "dev": "node --watch --import tsx src/index.ts"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.4",
34
+ "node-telegram-bot-api": "^0.66.0",
35
+ "openai": "^4.77.3",
36
+ "tsx": "^4.21.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.10.5",
40
+ "@types/node-telegram-bot-api": "^0.64.7",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Telegram Claude MCP Server
5
+ *
6
+ * A stdio-based MCP server that lets Claude message you on Telegram.
7
+ * Supports multiple Claude Code sessions with message tagging.
8
+ */
9
+
10
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
13
+ import { TelegramManager } from './telegram.js';
14
+ import { loadAppConfig, validateAppConfig } from './providers/index.js';
15
+
16
+ async function main() {
17
+ // Load configuration
18
+ const config = loadAppConfig();
19
+
20
+ // Validate configuration
21
+ const errors = validateAppConfig(config);
22
+ if (errors.length > 0) {
23
+ console.error('Configuration errors:');
24
+ errors.forEach((e) => console.error(` - ${e}`));
25
+ process.exit(1);
26
+ }
27
+
28
+ // Create Telegram manager
29
+ const telegram = new TelegramManager({
30
+ botToken: config.telegramBotToken,
31
+ chatId: config.telegramChatId,
32
+ sessionName: config.sessionName,
33
+ responseTimeoutMs: config.responseTimeoutMs,
34
+ });
35
+
36
+ telegram.start();
37
+
38
+ // Track active chat sessions
39
+ let activeSessionId: string | null = null;
40
+
41
+ // Create stdio MCP server
42
+ const mcpServer = new Server(
43
+ { name: 'telegram-claude', version: '1.0.0' },
44
+ { capabilities: { tools: {} } }
45
+ );
46
+
47
+ // List available tools
48
+ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
49
+ return {
50
+ tools: [
51
+ {
52
+ name: 'send_message',
53
+ description:
54
+ 'Send a message to the user via Telegram and wait for their response. Use when you need user input, want to report completed work, or need discussion.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ message: {
59
+ type: 'string',
60
+ description: 'What you want to say to the user. Be clear and concise.',
61
+ },
62
+ },
63
+ required: ['message'],
64
+ },
65
+ },
66
+ {
67
+ name: 'continue_chat',
68
+ description: 'Continue an active chat with a follow-up message and wait for response.',
69
+ inputSchema: {
70
+ type: 'object',
71
+ properties: {
72
+ chat_id: { type: 'string', description: 'The chat ID from send_message' },
73
+ message: { type: 'string', description: 'Your follow-up message' },
74
+ },
75
+ required: ['chat_id', 'message'],
76
+ },
77
+ },
78
+ {
79
+ name: 'notify_user',
80
+ description:
81
+ 'Send a notification to the user without waiting for a response. Use for status updates or acknowledgments.',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ chat_id: { type: 'string', description: 'The chat ID from send_message' },
86
+ message: { type: 'string', description: 'The notification message' },
87
+ },
88
+ required: ['chat_id', 'message'],
89
+ },
90
+ },
91
+ {
92
+ name: 'end_chat',
93
+ description: 'End an active chat session with an optional closing message.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ chat_id: { type: 'string', description: 'The chat ID from send_message' },
98
+ message: {
99
+ type: 'string',
100
+ description: 'Optional closing message',
101
+ },
102
+ },
103
+ required: ['chat_id'],
104
+ },
105
+ },
106
+ ],
107
+ };
108
+ });
109
+
110
+ // Handle tool calls
111
+ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
112
+ try {
113
+ if (request.params.name === 'send_message') {
114
+ const { message } = request.params.arguments as { message: string };
115
+ const result = await telegram.sendMessageAndWait(message);
116
+ activeSessionId = result.chatId;
117
+
118
+ return {
119
+ content: [
120
+ {
121
+ type: 'text',
122
+ text: `Message sent to Telegram.\n\nChat ID: ${result.chatId}\n\nUser's response:\n${result.response}\n\nUse continue_chat for follow-ups or end_chat to close the session.`,
123
+ },
124
+ ],
125
+ };
126
+ }
127
+
128
+ if (request.params.name === 'continue_chat') {
129
+ const { chat_id, message } = request.params.arguments as {
130
+ chat_id: string;
131
+ message: string;
132
+ };
133
+
134
+ // Validate chat_id matches active session
135
+ if (activeSessionId && chat_id !== activeSessionId) {
136
+ console.error(`Warning: chat_id mismatch. Expected ${activeSessionId}, got ${chat_id}`);
137
+ }
138
+
139
+ const response = await telegram.continueChat(message);
140
+
141
+ return {
142
+ content: [{ type: 'text', text: `User's response:\n${response}` }],
143
+ };
144
+ }
145
+
146
+ if (request.params.name === 'notify_user') {
147
+ const { message } = request.params.arguments as {
148
+ chat_id: string;
149
+ message: string;
150
+ };
151
+ await telegram.notify(message);
152
+
153
+ return {
154
+ content: [{ type: 'text', text: `Notification sent: "${message}"` }],
155
+ };
156
+ }
157
+
158
+ if (request.params.name === 'end_chat') {
159
+ const { chat_id, message } = request.params.arguments as {
160
+ chat_id: string;
161
+ message?: string;
162
+ };
163
+ await telegram.endChat(message);
164
+ activeSessionId = null;
165
+
166
+ return {
167
+ content: [{ type: 'text', text: 'Chat session ended.' }],
168
+ };
169
+ }
170
+
171
+ throw new Error(`Unknown tool: ${request.params.name}`);
172
+ } catch (error) {
173
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
174
+ return {
175
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
176
+ isError: true,
177
+ };
178
+ }
179
+ });
180
+
181
+ // Connect MCP server via stdio
182
+ const transport = new StdioServerTransport();
183
+ await mcpServer.connect(transport);
184
+
185
+ console.error('');
186
+ console.error('Telegram Claude MCP server ready');
187
+ console.error(`Session: ${config.sessionName}`);
188
+ console.error(`Chat ID: ${config.telegramChatId}`);
189
+ console.error('');
190
+
191
+ // Graceful shutdown
192
+ const shutdown = async () => {
193
+ console.error('\nShutting down...');
194
+ telegram.stop();
195
+ process.exit(0);
196
+ };
197
+
198
+ process.on('SIGINT', shutdown);
199
+ process.on('SIGTERM', shutdown);
200
+ }
201
+
202
+ main().catch((error) => {
203
+ console.error('Fatal error:', error);
204
+ process.exit(1);
205
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Provider Factory
3
+ *
4
+ * Creates and configures providers based on environment variables.
5
+ * Supports Telegram for messaging and OpenRouter for LLM.
6
+ */
7
+
8
+ import { OpenRouterProvider } from './llm-openrouter.js';
9
+
10
+ export { OpenRouterProvider } from './llm-openrouter.js';
11
+ export type { OpenRouterConfig, ChatMessage, CompletionResult } from './llm-openrouter.js';
12
+
13
+ export interface AppConfig {
14
+ // Telegram settings
15
+ telegramBotToken: string;
16
+ telegramChatId: number;
17
+
18
+ // Session settings
19
+ sessionName: string;
20
+ sessionPort: number;
21
+
22
+ // OpenRouter settings (optional)
23
+ openrouterApiKey?: string;
24
+ openrouterModel?: string;
25
+
26
+ // Chat settings
27
+ responseTimeoutMs: number;
28
+ }
29
+
30
+ export function loadAppConfig(): AppConfig {
31
+ const chatId = process.env.TELEGRAM_CHAT_ID;
32
+ const sessionPort = process.env.SESSION_PORT || '3333';
33
+
34
+ return {
35
+ telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || '',
36
+ telegramChatId: chatId ? parseInt(chatId, 10) : 0,
37
+ sessionName: process.env.SESSION_NAME || 'default',
38
+ sessionPort: parseInt(sessionPort, 10),
39
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
40
+ openrouterModel: process.env.OPENROUTER_MODEL || 'openai/gpt-oss-120b',
41
+ responseTimeoutMs: parseInt(process.env.CHAT_RESPONSE_TIMEOUT_MS || '180000', 10),
42
+ };
43
+ }
44
+
45
+ export function validateAppConfig(config: AppConfig): string[] {
46
+ const errors: string[] = [];
47
+
48
+ if (!config.telegramBotToken) {
49
+ errors.push('Missing TELEGRAM_BOT_TOKEN (get from @BotFather)');
50
+ }
51
+ if (!config.telegramChatId) {
52
+ errors.push('Missing TELEGRAM_CHAT_ID (your Telegram user/chat ID)');
53
+ }
54
+ if (!config.sessionName) {
55
+ errors.push('Missing SESSION_NAME (unique identifier for this session)');
56
+ }
57
+
58
+ return errors;
59
+ }
60
+
61
+ export function createOpenRouterProvider(config: AppConfig): OpenRouterProvider | null {
62
+ if (!config.openrouterApiKey) {
63
+ return null;
64
+ }
65
+
66
+ const provider = new OpenRouterProvider();
67
+ provider.initialize({
68
+ apiKey: config.openrouterApiKey,
69
+ model: config.openrouterModel,
70
+ });
71
+
72
+ return provider;
73
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * OpenRouter LLM Provider
3
+ *
4
+ * Uses the OpenAI-compatible API provided by OpenRouter to access
5
+ * various LLM models including Claude, GPT-4, Gemini, etc.
6
+ */
7
+
8
+ import OpenAI from 'openai';
9
+
10
+ export interface OpenRouterConfig {
11
+ apiKey: string;
12
+ model?: string;
13
+ baseUrl?: string;
14
+ }
15
+
16
+ export interface ChatMessage {
17
+ role: 'system' | 'user' | 'assistant';
18
+ content: string;
19
+ }
20
+
21
+ export interface CompletionResult {
22
+ content: string;
23
+ model: string;
24
+ usage?: {
25
+ promptTokens: number;
26
+ completionTokens: number;
27
+ totalTokens: number;
28
+ };
29
+ }
30
+
31
+ export class OpenRouterProvider {
32
+ readonly name = 'openrouter';
33
+ private client: OpenAI | null = null;
34
+ private model: string = 'openai/gpt-oss-120b';
35
+
36
+ /**
37
+ * Initialize the OpenRouter client
38
+ */
39
+ initialize(config: OpenRouterConfig): void {
40
+ this.client = new OpenAI({
41
+ apiKey: config.apiKey,
42
+ baseURL: config.baseUrl || 'https://openrouter.ai/api/v1',
43
+ defaultHeaders: {
44
+ 'HTTP-Referer': 'https://github.com/anthropics/claude-code',
45
+ 'X-Title': 'Claude Code Telegram Bridge',
46
+ },
47
+ });
48
+
49
+ this.model = config.model || 'anthropic/claude-3.5-sonnet';
50
+ }
51
+
52
+ /**
53
+ * Check if the provider is initialized
54
+ */
55
+ isInitialized(): boolean {
56
+ return this.client !== null;
57
+ }
58
+
59
+ /**
60
+ * Generate a chat completion
61
+ */
62
+ async complete(messages: ChatMessage[]): Promise<CompletionResult> {
63
+ if (!this.client) {
64
+ throw new Error('OpenRouter provider not initialized');
65
+ }
66
+
67
+ const response = await this.client.chat.completions.create({
68
+ model: this.model,
69
+ messages: messages.map((m) => ({
70
+ role: m.role,
71
+ content: m.content,
72
+ })),
73
+ });
74
+
75
+ const choice = response.choices[0];
76
+ if (!choice || !choice.message.content) {
77
+ throw new Error('No response from OpenRouter');
78
+ }
79
+
80
+ return {
81
+ content: choice.message.content,
82
+ model: response.model,
83
+ usage: response.usage
84
+ ? {
85
+ promptTokens: response.usage.prompt_tokens,
86
+ completionTokens: response.usage.completion_tokens,
87
+ totalTokens: response.usage.total_tokens,
88
+ }
89
+ : undefined,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Simple text completion with optional system prompt
95
+ */
96
+ async simpleComplete(prompt: string, systemPrompt?: string): Promise<string> {
97
+ const messages: ChatMessage[] = [];
98
+
99
+ if (systemPrompt) {
100
+ messages.push({ role: 'system', content: systemPrompt });
101
+ }
102
+
103
+ messages.push({ role: 'user', content: prompt });
104
+
105
+ const result = await this.complete(messages);
106
+ return result.content;
107
+ }
108
+
109
+ /**
110
+ * Summarize text
111
+ */
112
+ async summarize(text: string, maxLength?: number): Promise<string> {
113
+ const systemPrompt = maxLength
114
+ ? `Summarize the following text in ${maxLength} characters or less. Be concise.`
115
+ : 'Summarize the following text concisely.';
116
+
117
+ return this.simpleComplete(text, systemPrompt);
118
+ }
119
+ }
120
+
121
+ // Singleton instance for optional use
122
+ let instance: OpenRouterProvider | null = null;
123
+
124
+ export function getOpenRouterProvider(): OpenRouterProvider {
125
+ if (!instance) {
126
+ instance = new OpenRouterProvider();
127
+ }
128
+ return instance;
129
+ }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Telegram Bot Manager
3
+ *
4
+ * Handles Telegram messaging for Claude Code with multi-session support.
5
+ * Multiple Claude Code instances can share one Telegram chat, with messages
6
+ * tagged by session name and replies routed to the correct session.
7
+ */
8
+
9
+ import TelegramBot from 'node-telegram-bot-api';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+
13
+ // Session state file location
14
+ const SESSION_DIR = '/tmp/telegram-claude-sessions';
15
+
16
+ interface SessionState {
17
+ sessionName: string;
18
+ chatId: number;
19
+ messageIds: number[]; // Telegram message IDs sent by this session
20
+ waitingForResponse: boolean;
21
+ lastActivity: number;
22
+ }
23
+
24
+ interface PendingResponse {
25
+ resolve: (response: string) => void;
26
+ reject: (error: Error) => void;
27
+ messageId: number;
28
+ timestamp: number;
29
+ }
30
+
31
+ export interface TelegramConfig {
32
+ botToken: string;
33
+ chatId: number;
34
+ sessionName: string;
35
+ responseTimeoutMs?: number;
36
+ }
37
+
38
+ export class TelegramManager {
39
+ private bot: TelegramBot;
40
+ private config: TelegramConfig;
41
+ private pendingResponses: Map<string, PendingResponse> = new Map();
42
+ private sessionStateFile: string;
43
+ private isRunning = false;
44
+
45
+ constructor(config: TelegramConfig) {
46
+ this.config = {
47
+ responseTimeoutMs: 180000, // 3 minutes default
48
+ ...config,
49
+ };
50
+
51
+ this.bot = new TelegramBot(config.botToken, { polling: true });
52
+ this.sessionStateFile = path.join(SESSION_DIR, `${config.sessionName}.json`);
53
+
54
+ // Ensure session directory exists
55
+ if (!fs.existsSync(SESSION_DIR)) {
56
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
57
+ }
58
+
59
+ this.setupMessageHandler();
60
+ this.updateSessionState({ waitingForResponse: false, messageIds: [] });
61
+ }
62
+
63
+ /**
64
+ * Start the bot (called after initialization)
65
+ */
66
+ start(): void {
67
+ this.isRunning = true;
68
+ console.error(`[${this.config.sessionName}] Telegram bot started`);
69
+ }
70
+
71
+ /**
72
+ * Stop the bot
73
+ */
74
+ stop(): void {
75
+ this.isRunning = false;
76
+ this.bot.stopPolling();
77
+ this.cleanupSessionState();
78
+ console.error(`[${this.config.sessionName}] Telegram bot stopped`);
79
+ }
80
+
81
+ /**
82
+ * Send a message and wait for user response
83
+ */
84
+ async sendMessageAndWait(message: string): Promise<{ chatId: string; response: string }> {
85
+ const taggedMessage = `[${this.config.sessionName}] ${message}`;
86
+
87
+ const sent = await this.bot.sendMessage(this.config.chatId, taggedMessage);
88
+ const messageId = sent.message_id;
89
+
90
+ // Update session state
91
+ this.updateSessionState({
92
+ waitingForResponse: true,
93
+ messageIds: [...this.getSessionState().messageIds, messageId],
94
+ });
95
+
96
+ // Wait for response
97
+ const response = await this.waitForResponse(messageId);
98
+
99
+ return {
100
+ chatId: `${this.config.sessionName}:${this.config.chatId}`,
101
+ response,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Continue a chat - send follow-up and wait for response
107
+ */
108
+ async continueChat(message: string): Promise<string> {
109
+ const taggedMessage = `[${this.config.sessionName}] ${message}`;
110
+
111
+ const sent = await this.bot.sendMessage(this.config.chatId, taggedMessage);
112
+ const messageId = sent.message_id;
113
+
114
+ // Update session state
115
+ this.updateSessionState({
116
+ waitingForResponse: true,
117
+ messageIds: [...this.getSessionState().messageIds, messageId],
118
+ });
119
+
120
+ return this.waitForResponse(messageId);
121
+ }
122
+
123
+ /**
124
+ * Send a notification without waiting for response
125
+ */
126
+ async notify(message: string): Promise<void> {
127
+ const taggedMessage = `[${this.config.sessionName}] ${message}`;
128
+ const sent = await this.bot.sendMessage(this.config.chatId, taggedMessage);
129
+
130
+ // Track the message but don't wait
131
+ this.updateSessionState({
132
+ messageIds: [...this.getSessionState().messageIds, sent.message_id],
133
+ });
134
+ }
135
+
136
+ /**
137
+ * End the chat session
138
+ */
139
+ async endChat(message?: string): Promise<void> {
140
+ if (message) {
141
+ const taggedMessage = `[${this.config.sessionName}] ${message}`;
142
+ await this.bot.sendMessage(this.config.chatId, taggedMessage);
143
+ }
144
+
145
+ // Clear pending responses
146
+ for (const [key, pending] of this.pendingResponses) {
147
+ pending.reject(new Error('Chat ended'));
148
+ this.pendingResponses.delete(key);
149
+ }
150
+
151
+ this.updateSessionState({ waitingForResponse: false, messageIds: [] });
152
+ }
153
+
154
+ /**
155
+ * Set up message handler for incoming messages
156
+ */
157
+ private setupMessageHandler(): void {
158
+ this.bot.on('message', (msg) => {
159
+ // Ignore messages not from our target chat
160
+ if (msg.chat.id !== this.config.chatId) {
161
+ return;
162
+ }
163
+
164
+ // Ignore our own messages
165
+ if (msg.from?.is_bot) {
166
+ return;
167
+ }
168
+
169
+ const text = msg.text || '';
170
+
171
+ // Check if this is a reply to one of our messages
172
+ if (msg.reply_to_message) {
173
+ const replyToId = msg.reply_to_message.message_id;
174
+ if (this.isOurMessage(replyToId)) {
175
+ this.resolveResponse(text);
176
+ return;
177
+ }
178
+ }
179
+
180
+ // Check if message is prefixed with our session name
181
+ const sessionPrefix = `@${this.config.sessionName}`;
182
+ if (text.toLowerCase().startsWith(sessionPrefix.toLowerCase())) {
183
+ const response = text.slice(sessionPrefix.length).trim();
184
+ this.resolveResponse(response);
185
+ return;
186
+ }
187
+
188
+ // Check if we're the most recent waiting session
189
+ if (this.isWaitingAndMostRecent()) {
190
+ this.resolveResponse(text);
191
+ }
192
+ });
193
+
194
+ this.bot.on('polling_error', (error) => {
195
+ console.error(`[${this.config.sessionName}] Telegram polling error:`, error.message);
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Wait for a response with timeout
201
+ */
202
+ private waitForResponse(messageId: number): Promise<string> {
203
+ return new Promise((resolve, reject) => {
204
+ const key = `${messageId}`;
205
+
206
+ // Set timeout
207
+ const timeout = setTimeout(() => {
208
+ this.pendingResponses.delete(key);
209
+ this.updateSessionState({ waitingForResponse: false });
210
+ reject(new Error(`Response timeout after ${this.config.responseTimeoutMs}ms`));
211
+ }, this.config.responseTimeoutMs);
212
+
213
+ this.pendingResponses.set(key, {
214
+ resolve: (response: string) => {
215
+ clearTimeout(timeout);
216
+ this.pendingResponses.delete(key);
217
+ this.updateSessionState({ waitingForResponse: false });
218
+ resolve(response);
219
+ },
220
+ reject: (error: Error) => {
221
+ clearTimeout(timeout);
222
+ this.pendingResponses.delete(key);
223
+ reject(error);
224
+ },
225
+ messageId,
226
+ timestamp: Date.now(),
227
+ });
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Resolve a pending response
233
+ */
234
+ private resolveResponse(text: string): void {
235
+ // Get the most recent pending response
236
+ let mostRecent: PendingResponse | null = null;
237
+ let mostRecentKey: string | null = null;
238
+
239
+ for (const [key, pending] of this.pendingResponses) {
240
+ if (!mostRecent || pending.timestamp > mostRecent.timestamp) {
241
+ mostRecent = pending;
242
+ mostRecentKey = key;
243
+ }
244
+ }
245
+
246
+ if (mostRecent && mostRecentKey) {
247
+ mostRecent.resolve(text);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Check if a message ID belongs to this session
253
+ */
254
+ private isOurMessage(messageId: number): boolean {
255
+ const state = this.getSessionState();
256
+ return state.messageIds.includes(messageId);
257
+ }
258
+
259
+ /**
260
+ * Check if this session is waiting and is the most recently active waiting session
261
+ */
262
+ private isWaitingAndMostRecent(): boolean {
263
+ const ourState = this.getSessionState();
264
+ if (!ourState.waitingForResponse) {
265
+ return false;
266
+ }
267
+
268
+ // Check all other sessions
269
+ const sessionFiles = fs.readdirSync(SESSION_DIR).filter((f) => f.endsWith('.json'));
270
+
271
+ for (const file of sessionFiles) {
272
+ if (file === `${this.config.sessionName}.json`) continue;
273
+
274
+ try {
275
+ const otherState = JSON.parse(
276
+ fs.readFileSync(path.join(SESSION_DIR, file), 'utf-8')
277
+ ) as SessionState;
278
+
279
+ // If another session is waiting and more recent, we're not the target
280
+ if (otherState.waitingForResponse && otherState.lastActivity > ourState.lastActivity) {
281
+ return false;
282
+ }
283
+ } catch {
284
+ // Ignore invalid files
285
+ }
286
+ }
287
+
288
+ return true;
289
+ }
290
+
291
+ /**
292
+ * Get current session state from file
293
+ */
294
+ private getSessionState(): SessionState {
295
+ try {
296
+ if (fs.existsSync(this.sessionStateFile)) {
297
+ return JSON.parse(fs.readFileSync(this.sessionStateFile, 'utf-8'));
298
+ }
299
+ } catch {
300
+ // Return default state on error
301
+ }
302
+
303
+ return {
304
+ sessionName: this.config.sessionName,
305
+ chatId: this.config.chatId,
306
+ messageIds: [],
307
+ waitingForResponse: false,
308
+ lastActivity: Date.now(),
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Update session state file
314
+ */
315
+ private updateSessionState(updates: Partial<SessionState>): void {
316
+ const current = this.getSessionState();
317
+ const updated: SessionState = {
318
+ ...current,
319
+ ...updates,
320
+ lastActivity: Date.now(),
321
+ };
322
+
323
+ try {
324
+ fs.writeFileSync(this.sessionStateFile, JSON.stringify(updated, null, 2));
325
+ } catch (error) {
326
+ console.error(`[${this.config.sessionName}] Failed to update session state:`, error);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Clean up session state file on shutdown
332
+ */
333
+ private cleanupSessionState(): void {
334
+ try {
335
+ if (fs.existsSync(this.sessionStateFile)) {
336
+ fs.unlinkSync(this.sessionStateFile);
337
+ }
338
+ } catch {
339
+ // Ignore cleanup errors
340
+ }
341
+ }
342
+ }