Major Features: - Auto-start: MCP server now automatically starts Telegram bridge on demand - AFK Mode: Toggle notifications on/off with /afk slash command - New telegram_toggle_afk MCP tool for controlling notification state - Dynamic enable/disable via new /toggle and /status API endpoints MCP Server Improvements: - Auto-detects if bridge is running and starts it automatically - Monitors bridge process health and logs output - Clean shutdown handling for both MCP server and bridge - Process spawning with proper environment variable passing Telegram Bridge Updates: - Runtime toggle for ENABLED state (was previously static) - POST /toggle endpoint to toggle notifications with Telegram confirmation - GET /status endpoint to check current notification state - Telegram messages sent when state changes (🟢/🔴 indicators) Documentation: - Cross-platform setup instructions (Mac, Linux, Windows) - Claude Code CLI setup guide with claude mcp add commands - Global vs project-specific MCP configuration explained - Troubleshooting section for fixing configuration scope issues - Complete AFK mode usage documentation - All new API endpoints documented Slash Commands: - Created /afk command in .claude/commands/afk.md - Available in both InnerVoice and ESO-MCP projects Files Changed: - src/mcp-server.ts: Auto-start logic and telegram_toggle_afk tool - src/index.ts: Dynamic ENABLED toggle and new API endpoints - README.md: Comprehensive setup and troubleshooting guide - mcp-config.json: Updated with correct absolute path - .claude/commands/afk.md: New slash command for AFK mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
319 lines
8.5 KiB
TypeScript
319 lines
8.5 KiB
TypeScript
import { Telegraf } from 'telegraf';
|
||
import express from 'express';
|
||
import dotenv from 'dotenv';
|
||
import fs from 'fs/promises';
|
||
import path from 'path';
|
||
|
||
dotenv.config();
|
||
|
||
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
|
||
const app = express();
|
||
const PORT = parseInt(process.env.PORT || '3456');
|
||
const HOST = process.env.HOST || 'localhost';
|
||
let ENABLED = process.env.ENABLED !== 'false'; // Now mutable for runtime toggling
|
||
|
||
let chatId: string | null = process.env.TELEGRAM_CHAT_ID || null;
|
||
const envPath = path.join(process.cwd(), '.env');
|
||
|
||
// Message queue for two-way communication
|
||
interface QueuedMessage {
|
||
from: string;
|
||
message: string;
|
||
timestamp: Date;
|
||
read: boolean;
|
||
}
|
||
|
||
const messageQueue: QueuedMessage[] = [];
|
||
const pendingQuestions = new Map<string, { resolve: (answer: string) => void; timeout: NodeJS.Timeout }>();
|
||
|
||
app.use(express.json());
|
||
|
||
// Save chat ID to .env file
|
||
async function saveChatId(id: string) {
|
||
try {
|
||
const envContent = await fs.readFile(envPath, 'utf-8');
|
||
const updated = envContent.replace(
|
||
/TELEGRAM_CHAT_ID=.*/,
|
||
`TELEGRAM_CHAT_ID=${id}`
|
||
);
|
||
await fs.writeFile(envPath, updated);
|
||
console.log(`✅ Chat ID saved: ${id}`);
|
||
} catch (error) {
|
||
console.error('Failed to save chat ID:', error);
|
||
}
|
||
}
|
||
|
||
// Bot commands
|
||
bot.start(async (ctx) => {
|
||
chatId = ctx.chat.id.toString();
|
||
await saveChatId(chatId);
|
||
await ctx.reply(
|
||
'🤖 *Claude Telegram Bridge Active*\n\n' +
|
||
'I will now forward notifications from Claude Code and other apps.\n\n' +
|
||
'*Commands:*\n' +
|
||
'/status - Check bridge status\n' +
|
||
'/enable - Enable notifications\n' +
|
||
'/disable - Disable notifications\n' +
|
||
'/test - Send test notification',
|
||
{ parse_mode: 'Markdown' }
|
||
);
|
||
});
|
||
|
||
bot.command('status', async (ctx) => {
|
||
const status = ENABLED ? '✅ Enabled' : '⛔ Disabled';
|
||
await ctx.reply(
|
||
`*Bridge Status*\n\n` +
|
||
`Status: ${status}\n` +
|
||
`Chat ID: ${chatId}\n` +
|
||
`HTTP Server: http://${HOST}:${PORT}`,
|
||
{ parse_mode: 'Markdown' }
|
||
);
|
||
});
|
||
|
||
bot.command('help', async (ctx) => {
|
||
await ctx.reply(
|
||
'*Claude Telegram Bridge - Commands*\n\n' +
|
||
'*Bot Commands:*\n' +
|
||
'`/start` - Initialize and connect\n' +
|
||
'`/help` - Show this help message\n' +
|
||
'`/status` - Check bridge status\n' +
|
||
'`/test` - Send test notification\n\n' +
|
||
'*How it works:*\n' +
|
||
'• Send me any message - I forward it to Claude\n' +
|
||
'• Claude processes it and replies back\n' +
|
||
'• When Claude asks a question, your next message answers it\n\n' +
|
||
'*Features:*\n' +
|
||
'✅ Two-way communication\n' +
|
||
'✅ Question/Answer flow\n' +
|
||
'✅ Progress notifications\n' +
|
||
'✅ Error alerts\n\n' +
|
||
'More info: See README in bridge folder',
|
||
{ parse_mode: 'Markdown' }
|
||
);
|
||
});
|
||
|
||
bot.command('test', async (ctx) => {
|
||
await ctx.reply('✅ Test notification received! Bridge is working.');
|
||
});
|
||
|
||
// Listen for any text messages from user
|
||
bot.on('text', async (ctx) => {
|
||
const message = ctx.message.text;
|
||
const from = ctx.from.username || ctx.from.first_name;
|
||
|
||
console.log(`\n📨 Message from ${from}: "${message}"\n`);
|
||
|
||
// Check if this is an answer to a pending question
|
||
const questionId = Array.from(pendingQuestions.keys())[0];
|
||
if (questionId && pendingQuestions.has(questionId)) {
|
||
const { resolve, timeout } = pendingQuestions.get(questionId)!;
|
||
clearTimeout(timeout);
|
||
pendingQuestions.delete(questionId);
|
||
resolve(message);
|
||
await ctx.reply('✅ Answer received!');
|
||
return;
|
||
}
|
||
|
||
// Add to message queue for processing
|
||
messageQueue.push({
|
||
from,
|
||
message,
|
||
timestamp: new Date(),
|
||
read: false
|
||
});
|
||
|
||
// Acknowledge receipt - Claude will respond when available
|
||
await ctx.reply('💬 Message received - responding...');
|
||
|
||
console.log('📥 Queued for Claude to process');
|
||
});
|
||
|
||
// HTTP endpoint for sending notifications
|
||
app.post('/notify', async (req, res) => {
|
||
if (!ENABLED) {
|
||
return res.status(503).json({ error: 'Bridge is disabled' });
|
||
}
|
||
|
||
if (!chatId) {
|
||
return res.status(400).json({
|
||
error: 'No chat ID set. Please message the bot first with /start'
|
||
});
|
||
}
|
||
|
||
const { message, priority = 'info', parseMode = 'Markdown' } = req.body;
|
||
|
||
if (!message) {
|
||
return res.status(400).json({ error: 'Message is required' });
|
||
}
|
||
|
||
try {
|
||
const emojiMap: Record<string, string> = {
|
||
info: 'ℹ️',
|
||
success: '✅',
|
||
warning: '⚠️',
|
||
error: '❌',
|
||
question: '❓'
|
||
};
|
||
const emoji = emojiMap[priority] || 'ℹ️';
|
||
|
||
await bot.telegram.sendMessage(
|
||
chatId,
|
||
`${emoji} ${message}`,
|
||
{ parse_mode: parseMode as any }
|
||
);
|
||
|
||
res.json({ success: true, chatId });
|
||
} catch (error: any) {
|
||
console.error('Failed to send message:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// Get unread messages
|
||
app.get('/messages', (req, res) => {
|
||
const unread = messageQueue.filter(m => !m.read);
|
||
res.json({ messages: unread, count: unread.length });
|
||
});
|
||
|
||
// Mark messages as read
|
||
app.post('/messages/read', (req, res) => {
|
||
const { count } = req.body;
|
||
const toMark = count || messageQueue.filter(m => !m.read).length;
|
||
|
||
let marked = 0;
|
||
for (const msg of messageQueue) {
|
||
if (!msg.read && marked < toMark) {
|
||
msg.read = true;
|
||
marked++;
|
||
}
|
||
}
|
||
|
||
res.json({ markedAsRead: marked });
|
||
});
|
||
|
||
// Send reply to user message
|
||
app.post('/reply', async (req, res) => {
|
||
if (!chatId) {
|
||
return res.status(400).json({ error: 'No chat ID set' });
|
||
}
|
||
|
||
const { message } = req.body;
|
||
|
||
if (!message) {
|
||
return res.status(400).json({ error: 'Message is required' });
|
||
}
|
||
|
||
try {
|
||
await bot.telegram.sendMessage(chatId, message, { parse_mode: 'Markdown' });
|
||
res.json({ success: true });
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// Ask a question and wait for answer
|
||
app.post('/ask', async (req, res) => {
|
||
if (!chatId) {
|
||
return res.status(400).json({ error: 'No chat ID set' });
|
||
}
|
||
|
||
const { question, timeout = 300000 } = req.body; // 5 min default timeout
|
||
|
||
if (!question) {
|
||
return res.status(400).json({ error: 'Question is required' });
|
||
}
|
||
|
||
try {
|
||
const questionId = Date.now().toString();
|
||
|
||
// Send question to Telegram
|
||
await bot.telegram.sendMessage(chatId, `❓ ${question}`, { parse_mode: 'Markdown' });
|
||
|
||
// Wait for answer
|
||
const answer = await new Promise<string>((resolve, reject) => {
|
||
const timer = setTimeout(() => {
|
||
pendingQuestions.delete(questionId);
|
||
reject(new Error('Timeout waiting for answer'));
|
||
}, timeout);
|
||
|
||
pendingQuestions.set(questionId, { resolve, timeout: timer });
|
||
});
|
||
|
||
res.json({ answer });
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// Health check
|
||
app.get('/health', (req, res) => {
|
||
res.json({
|
||
status: 'running',
|
||
enabled: ENABLED,
|
||
chatId: chatId ? 'set' : 'not set',
|
||
unreadMessages: messageQueue.filter(m => !m.read).length,
|
||
pendingQuestions: pendingQuestions.size
|
||
});
|
||
});
|
||
|
||
// Toggle enabled state
|
||
app.post('/toggle', async (req, res) => {
|
||
const previousState = ENABLED;
|
||
ENABLED = !ENABLED;
|
||
|
||
const statusMessage = ENABLED
|
||
? '🟢 InnerVoice notifications ENABLED - You will receive messages'
|
||
: '🔴 InnerVoice notifications DISABLED - Messages paused';
|
||
|
||
// Notify via Telegram if chat ID is set
|
||
if (chatId) {
|
||
try {
|
||
await bot.telegram.sendMessage(chatId, statusMessage, { parse_mode: 'Markdown' });
|
||
} catch (error) {
|
||
console.error('Failed to send toggle notification:', error);
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
enabled: ENABLED,
|
||
previousState,
|
||
message: statusMessage
|
||
});
|
||
});
|
||
|
||
// Get current enabled state
|
||
app.get('/status', (req, res) => {
|
||
res.json({
|
||
enabled: ENABLED,
|
||
message: ENABLED ? 'Notifications are ON' : 'Notifications are OFF (AFK mode)'
|
||
});
|
||
});
|
||
|
||
// Start bot
|
||
bot.launch().then(() => {
|
||
console.log('🤖 Telegram bot started');
|
||
console.log('📱 Message your bot to get started');
|
||
});
|
||
|
||
// Start HTTP server
|
||
app.listen(PORT, HOST, () => {
|
||
console.log(`🌐 HTTP server running on http://${HOST}:${PORT}`);
|
||
console.log(`\n📋 Send notifications with:\n`);
|
||
console.log(`curl -X POST http://${HOST}:${PORT}/notify \\`);
|
||
console.log(` -H "Content-Type: application/json" \\`);
|
||
console.log(` -d '{"message": "Hello from Claude!", "priority": "info"}'`);
|
||
});
|
||
|
||
// Graceful shutdown
|
||
process.once('SIGINT', () => {
|
||
console.log('\n👋 Shutting down...');
|
||
bot.stop('SIGINT');
|
||
process.exit(0);
|
||
});
|
||
|
||
process.once('SIGTERM', () => {
|
||
bot.stop('SIGTERM');
|
||
process.exit(0);
|
||
});
|