11import { logger } from './logger' ;
22import 'dotenv/config' ;
3- import { Bot , Context } from 'grammy' ;
4- import { Database } from './database' ;
5- import CursorOfficialApi from './cursor-official-api' ;
3+ import { Context } from 'grammy' ;
64import Agent from './agent' ;
7- import { transcribeAudio , convertTelegramImageToCursorFormat } from './utils' ;
5+ import { transcribeAudio , convertTelegramImageToCursorFormat , safeSendMessage , safeReply , isUserAllowed } from './utils' ;
86import { saveImagesToCache , getImagesFromCache } from './image-cache' ;
9-
10- const bot = new Bot ( process . env . BOT_TOKEN || '' ) ;
11- const db = new Database ( ) ;
12- const apiKey = process . env . CURSOR_API_KEY || '' ;
13- if ( ! apiKey ) {
14- throw new Error ( 'No CURSOR_API_KEY configured for monitoring' ) ;
15- }
16- const cursorApi = new CursorOfficialApi ( { apiKey } ) ;
17- // Official API uses CURSOR_API_KEY; no cookie initialization required
18-
19- // Safe message sending with retry and fallback
20- async function safeSendMessage (
21- chatId : number ,
22- text : string ,
23- options : any = { } ,
24- maxRetries : number = 3
25- ) : Promise < void > {
26- for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
27- try {
28- await bot . api . sendMessage ( chatId , text , options ) ;
29- return ; // Success
30- } catch ( error : any ) {
31- logger . error ( `Attempt ${ attempt } /${ maxRetries } failed:` , error ) ;
32-
33- // If it's a parsing error, try fallback formats
34- if ( error . description ?. includes ( "can't parse entities" ) ) {
35- // First try HTML if we were using Markdown
36- if ( options . parse_mode === 'Markdown' ) {
37- logger . info ( 'Markdown parsing failed, trying HTML...' ) ;
38- try {
39- const htmlOptions = { ...options , parse_mode : 'HTML' } ;
40- // Convert basic Markdown to HTML
41- const htmlText = text
42- . replace ( / \* \* ( .* ?) \* \* / g, '<b>$1</b>' )
43- . replace ( / \* ( .* ?) \* / g, '<i>$1</i>' )
44- . replace ( / ` ( .* ?) ` / g, '<code>$1</code>' ) ;
45- await bot . api . sendMessage ( chatId , htmlText , htmlOptions ) ;
46- return ; // Success with HTML
47- } catch ( htmlError ) {
48- logger . error ( 'HTML also failed:' , htmlError ) ;
49- }
50- }
51-
52- // Finally try plain text
53- logger . info ( 'Trying plain text...' ) ;
54- try {
55- const plainOptions = { ...options } ;
56- delete plainOptions . parse_mode ;
57- await bot . api . sendMessage ( chatId , text , plainOptions ) ;
58- return ; // Success with plain text
59- } catch ( plainError ) {
60- logger . error ( 'Plain text also failed:' , plainError ) ;
61- }
62- }
63-
64- // If this is the last attempt, throw the error
65- if ( attempt === maxRetries ) {
66- throw error ;
67- }
68-
69- // Wait before retry (exponential backoff)
70- await new Promise ( resolve => setTimeout ( resolve , 1000 * attempt ) ) ;
71- }
72- }
73- }
74-
75- // Safe context reply with retry and fallback
76- async function safeReply (
77- ctx : Context ,
78- text : string ,
79- options : any = { } ,
80- maxRetries : number = 3
81- ) : Promise < void > {
82- for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
83- try {
84- await ctx . reply ( text , options ) ;
85- return ; // Success
86- } catch ( error : any ) {
87- logger . error ( `Reply attempt ${ attempt } /${ maxRetries } failed:` , error ) ;
88-
89- // If it's a parsing error, try fallback formats
90- if ( error . description ?. includes ( "can't parse entities" ) ) {
91- // First try HTML if we were using Markdown
92- if ( options . parse_mode === 'Markdown' ) {
93- logger . info ( 'Markdown parsing failed, trying HTML reply...' ) ;
94- try {
95- const htmlOptions = { ...options , parse_mode : 'HTML' } ;
96- // Convert basic Markdown to HTML
97- const htmlText = text
98- . replace ( / \* \* ( .* ?) \* \* / g, '<b>$1</b>' )
99- . replace ( / \* ( .* ?) \* / g, '<i>$1</i>' )
100- . replace ( / ` ( .* ?) ` / g, '<code>$1</code>' ) ;
101- await ctx . reply ( htmlText , htmlOptions ) ;
102- return ; // Success with HTML
103- } catch ( htmlError ) {
104- logger . error ( 'HTML reply also failed:' , htmlError ) ;
105- }
106- }
107-
108- // Finally try plain text
109- logger . info ( 'Trying plain text reply...' ) ;
110- try {
111- const plainOptions = { ...options } ;
112- delete plainOptions . parse_mode ;
113- await ctx . reply ( text , plainOptions ) ;
114- return ; // Success with plain text
115- } catch ( plainError ) {
116- logger . error ( 'Plain text reply also failed:' , plainError ) ;
117- }
118- }
119-
120- // If this is the last attempt, throw the error
121- if ( attempt === maxRetries ) {
122- throw error ;
123- }
124-
125- // Wait before retry (exponential backoff)
126- await new Promise ( resolve => setTimeout ( resolve , 1000 * attempt ) ) ;
127- }
128- }
129- }
130-
131- // Access control via ENV
132- function isUserAllowed ( userId : number ) : boolean {
133- const allowed = process . env . ALLOWED_USERS ;
134- if ( ! allowed ) return true ;
135- return allowed . split ( ',' ) . map ( x => x . trim ( ) ) . includes ( userId . toString ( ) ) ;
136- }
137-
138- // Background task monitoring
139- async function monitorTasks ( ) {
140- const tasks = await db . getActiveTasks ( ) ;
141-
142- for ( const task of tasks ) {
143- try {
144- const agent = await cursorApi . getAgent ( task . composer_id ) ;
145- const oldStatus = task . status ;
146- const newStatus = agent . status ;
147-
148- const keyboard = {
149- inline_keyboard : [
150- [
151- {
152- text : '🔍 Check Task Details' ,
153- url : `https://cursor.com/agents?selectedBcId=${ task . composer_id } `
154- } ,
155- {
156- text : '📁 Open Repository' ,
157- url : task . repo_url
158- }
159- ]
160- ]
161- } ;
162-
163- if ( oldStatus !== newStatus ) {
164- await db . updateTask ( task . id , newStatus ) ;
165-
166- // Notify user if task completed or failed
167- if ( newStatus === 'FINISHED' ) {
168-
169-
170- await safeSendMessage (
171- task . chat_id ,
172- `✅ *Task completed!*\n\n*${ task . task_description } *\n\nRepo: ${ task . repo_url } \nComposer: \`${ task . composer_id } \`` ,
173- {
174- parse_mode : 'Markdown' ,
175- reply_markup : keyboard
176- }
177- ) ;
178- } else if ( newStatus === 'ERROR' ) {
179- await safeSendMessage (
180- task . chat_id ,
181- `❌ *Task failed!*\n\n*${ task . task_description } *\n\nRepo: ${ task . repo_url } \nComposer: \`${ task . composer_id } \`` ,
182- {
183- parse_mode : 'Markdown' ,
184- reply_markup : keyboard
185- }
186- ) ;
187- } else if ( newStatus === 'EXPIRED' ) {
188- await safeSendMessage (
189- task . chat_id ,
190- `⏱️ *Task expired!*\n\n*${ task . task_description } *\n\nRepo: ${ task . repo_url } \nComposer: \`${ task . composer_id } \`` ,
191- {
192- parse_mode : 'Markdown' ,
193- reply_markup : keyboard
194- }
195- ) ;
196- } else if ( newStatus === 'RUNNING' ) {
197- await safeSendMessage (
198- task . chat_id ,
199- `🔄 *Task is now running*\n\n*${ task . task_description } *\n\nRepo: ${ task . repo_url } \nComposer: \`${ task . composer_id } \`` ,
200- {
201- parse_mode : 'Markdown'
202- }
203- ) ;
204- } else {
205- await safeSendMessage (
206- task . chat_id ,
207- `🔄 *Task status updated to ${ newStatus } *\n\n*${ task . task_description } *\n\nRepo: ${ task . repo_url } \nComposer: \`${ task . composer_id } \`` ,
208- {
209- parse_mode : 'Markdown'
210- }
211- ) ;
212- }
213- }
214- } catch ( error ) {
215- logger . error ( `Error monitoring task ${ task . id } :` , error ) ;
216- }
217- }
218- }
7+ import { bot , db , cursorApi } from './env' ;
8+ import { monitorTasks } from './monitor' ;
2199
22010// Common message processing function
22111async function processUserMessage (
@@ -283,8 +73,6 @@ let monitorInterval: NodeJS.Timeout | null = null;
28373
28474// Bot handlers
28575bot . use ( async ( ctx , next ) => {
286- logger . info ( 'ctx.from' , ctx . from ) ;
287- logger . info ( 'ctx.chat' , ctx . chat ) ;
28876 if ( ! ctx . from || ! ctx . chat ) return ;
28977
29078 // Middleware to check if user is allowed
@@ -294,6 +82,20 @@ bot.use(async (ctx, next) => {
29482 return ;
29583 }
29684
85+ const mentionOnlyMode = process . env . MENTION_ONLY_MODE === 'true' ;
86+ if ( mentionOnlyMode ) {
87+ const botInfo = await ctx . api . getMe ( ) ;
88+ const botMention = `@${ botInfo . username } ` ;
89+ const isDirectMessage = ctx . chat ?. type === 'private' ;
90+ const isMentioned = ctx . message ?. text ?. includes ( botMention ) ;
91+
92+ // Only respond if it's a direct message or bot is mentioned
93+ if ( ! isDirectMessage && ! isMentioned ) {
94+ logger . info ( 'Skipping message - mention-only mode enabled and bot not mentioned' ) ;
95+ return ;
96+ }
97+ }
98+
29799 // Save user and chat info (no allowed field)
298100 await db . createUser ( {
299101 id : ctx . from . id ,
@@ -347,22 +149,28 @@ bot.command('tasks', async (ctx) => {
347149} ) ;
348150
349151bot . command ( 'clear' , async ( ctx ) => {
350-
351152 try {
352153 await db . clearHistory ( ctx . from ! . id , ctx . chat ! . id ) ;
353- await safeReply ( ctx , 'History cleared successfully' ) ;
154+ await safeReply ( ctx , 'Chat history cleared successfully' ) ;
354155 } catch ( error ) {
355156 logger . error ( 'Error clearing history:' , error ) ;
356- await safeReply ( ctx , 'Failed to clear history' ) ;
157+ await safeReply ( ctx , 'Failed to clear chat history' ) ;
357158 }
358159} ) ;
359160
161+ bot . command ( 'models' , async ( ctx ) => {
162+ const models = await cursorApi . listModels ( ) ;
163+ await safeReply ( ctx , `*Available Models for Cursor Agent:*\n\n${ models . models . join ( '\n' ) } ` ) ;
164+ } ) ;
165+
360166bot . command ( 'help' , async ( ctx ) => {
361167 await safeReply ( ctx , `🤖 *Cursor AI Task Bot Help*
362168
363169*Available Commands:*
364170/start - Start the bot
365171/tasks - Show your active tasks
172+ /clear - Clear chat history
173+ /models - Show available models
366174/help - Show this help
367175
368176*Usage Examples:*
@@ -383,21 +191,6 @@ bot.on('message:text', async (ctx) => {
383191
384192 const message = ctx . message . text ;
385193
386- // Check if mention-only mode is enabled
387- const mentionOnlyMode = process . env . MENTION_ONLY_MODE === 'true' ;
388- if ( mentionOnlyMode ) {
389- const botInfo = await ctx . api . getMe ( ) ;
390- const botMention = `@${ botInfo . username } ` ;
391- const isDirectMessage = ctx . chat ?. type === 'private' ;
392- const isMentioned = message . includes ( botMention ) ;
393-
394- // Only respond if it's a direct message or bot is mentioned
395- if ( ! isDirectMessage && ! isMentioned ) {
396- logger . info ( 'Skipping message - mention-only mode enabled and bot not mentioned' ) ;
397- return ;
398- }
399- }
400-
401194 try {
402195 await processUserMessage ( ctx , message ) ;
403196 } catch ( error ) {
0 commit comments