this repo has no description
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}