diff --git a/src/index.ts b/src/index.ts index c672105..b8ee25e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,16 +15,45 @@ let ENABLED = process.env.ENABLED !== 'false'; // Now mutable for runtime toggli let chatId: string | null = process.env.TELEGRAM_CHAT_ID || null; const envPath = path.join(process.cwd(), '.env'); +// Session tracking for multi-project support +interface ClaudeSession { + id: string; + projectName: string; + projectPath: string; + startTime: Date; + lastActivity: Date; + status: 'active' | 'idle'; +} + +const activeSessions = new Map(); +const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes of inactivity + // Message queue for two-way communication interface QueuedMessage { from: string; message: string; timestamp: Date; read: boolean; + sessionId?: string; // Target session for this message } const messageQueue: QueuedMessage[] = []; -const pendingQuestions = new Map void; timeout: NodeJS.Timeout }>(); +const pendingQuestions = new Map void; + timeout: NodeJS.Timeout; + sessionId?: string; +}>(); + +// Clean up expired sessions periodically +setInterval(() => { + const now = Date.now(); + for (const [sessionId, session] of activeSessions.entries()) { + if (now - session.lastActivity.getTime() > SESSION_TIMEOUT) { + console.log(`๐Ÿงน Removing expired session: ${sessionId} (${session.projectName})`); + activeSessions.delete(sessionId); + } + } +}, 5 * 60 * 1000); // Check every 5 minutes app.use(express.json()); @@ -77,12 +106,15 @@ bot.command('help', async (ctx) => { '`/start` - Initialize and connect\n' + '`/help` - Show this help message\n' + '`/status` - Check bridge status\n' + + '`/sessions` - List active Claude sessions\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' + + 'โ€ข When Claude asks a question, your next message answers it\n' + + 'โ€ข Messages show project context: ๐Ÿ“ ProjectName [#abc1234]\n\n' + '*Features:*\n' + + 'โœ… Multi-project session tracking\n' + 'โœ… Two-way communication\n' + 'โœ… Question/Answer flow\n' + 'โœ… Progress notifications\n' + @@ -96,6 +128,27 @@ bot.command('test', async (ctx) => { await ctx.reply('โœ… Test notification received! Bridge is working.'); }); +bot.command('sessions', async (ctx) => { + const sessions = Array.from(activeSessions.values()); + + if (sessions.length === 0) { + await ctx.reply('๐Ÿ“ญ No active Claude sessions'); + return; + } + + const sessionList = sessions.map((s, i) => { + const shortId = s.id.substring(0, 7); + const idleMinutes = Math.floor((Date.now() - s.lastActivity.getTime()) / 60000); + const statusEmoji = s.status === 'active' ? '๐ŸŸข' : '๐ŸŸก'; + return `${i + 1}. ${statusEmoji} *${s.projectName}* [#${shortId}]\n Last active: ${idleMinutes}m ago`; + }).join('\n\n'); + + await ctx.reply( + `*Active Claude Sessions* (${sessions.length})\n\n${sessionList}\n\n_Reply with #sessionId to send a message to a specific session_`, + { parse_mode: 'Markdown' } + ); +}); + // Listen for any text messages from user bot.on('text', async (ctx) => { const message = ctx.message.text; @@ -128,6 +181,70 @@ bot.on('text', async (ctx) => { console.log('๐Ÿ“ฅ Queued for Claude to process'); }); +// Register or update a Claude session +app.post('/session/register', (req, res) => { + const { sessionId, projectName, projectPath } = req.body; + + if (!sessionId || !projectName || !projectPath) { + return res.status(400).json({ error: 'sessionId, projectName, and projectPath are required' }); + } + + const now = new Date(); + const existing = activeSessions.get(sessionId); + + if (existing) { + // Update existing session + existing.lastActivity = now; + existing.status = 'active'; + } else { + // Create new session + activeSessions.set(sessionId, { + id: sessionId, + projectName, + projectPath, + startTime: now, + lastActivity: now, + status: 'active' + }); + console.log(`๐Ÿ“ Registered new session: ${sessionId} (${projectName})`); + } + + res.json({ success: true, sessionId, projectName }); +}); + +// Update session activity +app.post('/session/heartbeat', (req, res) => { + const { sessionId } = req.body; + + if (!sessionId) { + return res.status(400).json({ error: 'sessionId is required' }); + } + + const session = activeSessions.get(sessionId); + if (session) { + session.lastActivity = new Date(); + session.status = 'active'; + res.json({ success: true }); + } else { + res.status(404).json({ error: 'Session not found' }); + } +}); + +// List active sessions +app.get('/sessions', (req, res) => { + const sessions = Array.from(activeSessions.values()).map(s => ({ + id: s.id, + projectName: s.projectName, + projectPath: s.projectPath, + startTime: s.startTime, + lastActivity: s.lastActivity, + status: s.status, + idleMinutes: Math.floor((Date.now() - s.lastActivity.getTime()) / 60000) + })); + + res.json({ sessions, count: sessions.length }); +}); + // HTTP endpoint for sending notifications app.post('/notify', async (req, res) => { if (!ENABLED) { @@ -135,12 +252,12 @@ app.post('/notify', async (req, res) => { } if (!chatId) { - return res.status(400).json({ - error: 'No chat ID set. Please message the bot first with /start' + 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; + const { message, priority = 'info', parseMode = 'Markdown', sessionId } = req.body; if (!message) { return res.status(400).json({ error: 'Message is required' }); @@ -156,9 +273,20 @@ app.post('/notify', async (req, res) => { }; const emoji = emojiMap[priority] || 'โ„น๏ธ'; + // Add project context if session ID provided + let projectContext = ''; + if (sessionId) { + const session = activeSessions.get(sessionId); + if (session) { + session.lastActivity = new Date(); + const shortId = sessionId.substring(0, 7); + projectContext = `๐Ÿ“ *${session.projectName}* [#${shortId}]\n`; + } + } + await bot.telegram.sendMessage( chatId, - `${emoji} ${message}`, + `${projectContext}${emoji} ${message}`, { parse_mode: parseMode as any } ); diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 39fce8a..a024622 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -20,6 +20,50 @@ const BRIDGE_HOST = new URL(BRIDGE_URL).hostname || 'localhost'; let bridgeProcess: ChildProcess | null = null; +// Session management +let currentSessionId: string | null = null; +let currentProjectName: string | null = null; +let currentProjectPath: string | null = null; + +// Get or create session ID +function getSessionId(): string { + if (!currentSessionId) { + currentSessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + return currentSessionId; +} + +// Get project name from current working directory +function getProjectInfo(): { name: string; path: string } { + const cwd = process.cwd(); + const name = cwd.split('/').pop() || 'Unknown'; + return { name, path: cwd }; +} + +// Register this session with the bridge +async function registerSession(): Promise { + const sessionId = getSessionId(); + const { name, path } = getProjectInfo(); + + currentProjectName = name; + currentProjectPath = path; + + try { + await fetch(`${BRIDGE_URL}/session/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId, + projectName: name, + projectPath: path + }) + }); + console.error(`โœ… Session registered: ${name} [${sessionId.substring(0, 7)}]`); + } catch (error) { + console.error('โš ๏ธ Failed to register session:', error); + } +} + // Check if the bridge is running async function isBridgeRunning(): Promise { try { @@ -208,7 +252,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const response = await fetch(`${BRIDGE_URL}/notify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message, priority }), + body: JSON.stringify({ + message, + priority, + sessionId: getSessionId() + }), }); if (!response.ok) { @@ -395,6 +443,9 @@ async function main() { // Ensure the bridge is running before starting the MCP server await ensureBridge(); + // Register this Claude session + await registerSession(); + const transport = new StdioServerTransport(); await server.connect(transport); console.error('๐Ÿš€ Telegram MCP server running on stdio');