feat(phase1): add project context and session tracking
Phase 1 Complete: Multi-project session management with context display Session Tracking: - Auto-generate unique session IDs for each Claude instance - Track project name and path from working directory - Register sessions automatically on MCP server startup - Expire inactive sessions after 30 minutes - Session heartbeat to track activity Project Context in Messages: - All notifications now show: 📁 ProjectName [#shortId] - Session ID automatically included in all notify calls - Project context helps identify which Claude sent the message - Short session ID (first 7 chars) for easy reference New API Endpoints: - POST /session/register - Register/update a Claude session - POST /session/heartbeat - Update session activity - GET /sessions - List all active sessions with idle times Telegram Bot Commands: - /sessions - View all active Claude sessions - Shows project name, session ID, and idle time - Status indicators: 🟢 active, 🟡 idle - Updated /help with new multi-project features MCP Server Changes: - getSessionId() - Generate/retrieve session ID - getProjectInfo() - Extract project name from cwd - registerSession() - Auto-register on startup - Include sessionId in all notify API calls This enables working with multiple Claude instances across different projects simultaneously, with clear visual indication of which project each message comes from. Next: Phase 2 (message queuing) and Phase 3 (remote spawner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
140
src/index.ts
140
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<string, ClaudeSession>();
|
||||
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<string, { resolve: (answer: string) => void; timeout: NodeJS.Timeout }>();
|
||||
const pendingQuestions = new Map<string, {
|
||||
resolve: (answer: string) => 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 }
|
||||
);
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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<boolean> {
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user