this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 190 lines 6.6 kB view raw
1/** 2 * Main entry point for the ADHD Support Agent 3 * 4 * This Bun HTTP server provides: 5 * - Health check endpoints for monitoring 6 * - Telegram webhook endpoint for receiving messages 7 * - Bot initialization and message handling 8 * 9 * Uses Bun.serve() for high-performance HTTP handling. 10 */ 11 12import { config, isWebhookMode } from './config'; 13import { healthCheck, simpleHealthCheck } from './health'; 14import { initializeLetta } from './letta'; 15import { handleUpdate, startPolling, registerWebhook, registerCommands, getOrCreateAgent } from './bot'; 16import { dispatchTool } from './tools'; 17import type { Update } from 'telegraf/types'; 18 19/** 20 * Main server handler using Bun.serve() 21 */ 22async function main(): Promise<void> { 23 console.log('Starting ADHD Support Agent...'); 24 25 // Initialize Letta before starting the server 26 try { 27 await initializeLetta(); 28 } catch (error) { 29 console.error('Failed to initialize Letta:', error); 30 console.error('Server will start, but bot functionality may be limited.'); 31 } 32 33 // Initialize agent (syncs tools with correct webhook URLs) 34 try { 35 await getOrCreateAgent(); 36 } catch (error) { 37 console.error('Failed to initialize agent:', error); 38 } 39 40 // Register bot commands with Telegram (sets the command menu) 41 try { 42 await registerCommands(); 43 } catch (error) { 44 console.error('Failed to register commands:', error); 45 } 46 47 // Start the HTTP server 48 Bun.serve({ 49 port: config.PORT, 50 async fetch(req) { 51 const url = new URL(req.url); 52 const path = url.pathname; 53 54 // GET /health - Full health check 55 if (path === '/health' && req.method === 'GET') { 56 return await healthCheck(); 57 } 58 59 // GET /healthz - Simple health check (k8s liveness probe) 60 if (path === '/healthz' && req.method === 'GET') { 61 return simpleHealthCheck(); 62 } 63 64 // POST /webhook - Telegram webhook endpoint 65 if (path === '/webhook' && req.method === 'POST') { 66 // Verify the secret token 67 const token = req.headers.get('X-Telegram-Bot-Api-Secret-Token'); 68 if (token === null || token !== config.TELEGRAM_WEBHOOK_SECRET_TOKEN) { 69 console.warn('Webhook request with invalid or missing secret token'); 70 return new Response(JSON.stringify({ error: 'Unauthorized' }), { 71 status: 401, 72 headers: { 'Content-Type': 'application/json' }, 73 }); 74 } 75 76 // Parse the Telegram update 77 let update: Update; 78 try { 79 update = (await req.json()) as Update; 80 } catch (error) { 81 console.error('Failed to parse webhook body:', error); 82 return new Response(JSON.stringify({ error: 'Invalid JSON' }), { 83 status: 400, 84 headers: { 'Content-Type': 'application/json' }, 85 }); 86 } 87 88 // Handle the update (fire and forget - Telegram expects quick response) 89 handleUpdate(update).catch((error: unknown) => { 90 console.error('Error handling update:', error); 91 }); 92 93 // Return 200 OK immediately 94 return new Response(JSON.stringify({ ok: true }), { 95 status: 200, 96 headers: { 'Content-Type': 'application/json' }, 97 }); 98 } 99 100 // POST /tools/:name - Letta tool webhook endpoint 101 // Letta's Python tool stubs POST here to execute TypeScript handlers 102 if (path.startsWith('/tools/') && req.method === 'POST') { 103 const toolName = path.slice(7); // "/tools/save_item" → "save_item" 104 105 if (toolName.length === 0) { 106 return new Response(JSON.stringify({ error: 'Tool name required' }), { 107 status: 400, 108 headers: { 'Content-Type': 'application/json' }, 109 }); 110 } 111 112 let args: Record<string, unknown>; 113 try { 114 args = (await req.json()) as Record<string, unknown>; 115 } catch { 116 return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { 117 status: 400, 118 headers: { 'Content-Type': 'application/json' }, 119 }); 120 } 121 122 // Extract user_id from args (passed by Letta agent context) 123 const userId = typeof args['user_id'] === 'number' ? args['user_id'] : 0; 124 125 try { 126 console.log(`\n🔧 TOOL WEBHOOK RECEIVED: ${toolName}`); 127 console.log(` Args: ${JSON.stringify(args)}`); 128 const result = await dispatchTool(toolName, args, { userId }); 129 const resultStr = JSON.stringify(result); 130 const truncated = resultStr.length > 300 ? resultStr.slice(0, 300) + '...' : resultStr; 131 console.log(` Result: ${truncated}`); 132 return new Response(JSON.stringify(result), { 133 status: 200, 134 headers: { 'Content-Type': 'application/json' }, 135 }); 136 } catch (error: unknown) { 137 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 138 console.error(`❌ TOOL WEBHOOK ERROR (${toolName}):`, errorMessage); 139 return new Response(JSON.stringify({ error: errorMessage }), { 140 status: 500, 141 headers: { 'Content-Type': 'application/json' }, 142 }); 143 } 144 } 145 146 // 404 for unknown routes 147 return new Response(JSON.stringify({ error: 'Not Found' }), { 148 status: 404, 149 headers: { 'Content-Type': 'application/json' }, 150 }); 151 }, 152 }); 153 154 console.log(`Server listening on http://localhost:${config.PORT.toString()}`); 155 156 // Start bot in appropriate mode 157 if (isWebhookMode()) { 158 try { 159 await registerWebhook(); 160 } catch (error) { 161 console.error('Failed to register webhook:', error); 162 console.error('Bot will not receive messages until webhook is registered.'); 163 } 164 } else { 165 console.log('Webhook mode disabled, starting polling for development...'); 166 try { 167 await startPolling(); 168 } catch (error) { 169 console.error('Failed to start polling mode:', error); 170 console.error('Bot will not receive messages in polling mode.'); 171 } 172 } 173 174 console.log('ADHD Support Agent is ready!'); 175} 176 177// Start the server 178main().catch((error: unknown) => { 179 console.error('Fatal error starting server:', error); 180 process.exit(1); 181}); 182 183// Handle Bun hot reload - cleanup before module replacement 184// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions -- import.meta.hot is undefined when not in hot mode 185if (import.meta.hot) { 186 import.meta.hot.dispose(() => { 187 console.log('Hot reload: cleaning up main module...'); 188 // Bot cleanup is handled in bot.ts 189 }); 190}