@@ -30,7 +30,6 @@ const README_PATH = join(__dirname, '../../../README.md')
3030const SHOULD_SERVE_BUILT_WEB = existsSync ( WEB_DIST_DIR ) && ( __filename . endsWith ( '/dist/index.js' ) || process . env . NODE_ENV === 'production' )
3131const WEB_ORIGIN = process . env . WEB_ORIGIN || ( SHOULD_SERVE_BUILT_WEB ? `http://localhost:${ PORT } ` : 'http://localhost:5173' )
3232const execFileAsync = promisify ( execFile )
33- const gmailMonitors = new Map < string , { running : boolean } > ( )
3433
3534app . use ( cors ( ) )
3635app . use ( express . json ( { limit : '10mb' } ) )
@@ -72,29 +71,6 @@ function createSessionId(): string {
7271 return id
7372}
7473
75- function sleep ( ms : number ) : Promise < void > {
76- return new Promise ( resolve => setTimeout ( resolve , ms ) )
77- }
78-
79- function firstNonEmpty ( ...values : Array < string | undefined > ) : string {
80- for ( const value of values ) {
81- if ( value && value . trim ( ) . length > 0 ) return value . trim ( )
82- }
83- return ''
84- }
85-
86- function gmailCategoryFromLabels ( labels : string [ ] ) : string {
87- if ( labels . includes ( 'CATEGORY_PROMOTIONS' ) ) return 'Promotions'
88- if ( labels . includes ( 'CATEGORY_SOCIAL' ) ) return 'Social'
89- if ( labels . includes ( 'CATEGORY_UPDATES' ) ) return 'Updates'
90- if ( labels . includes ( 'CATEGORY_FORUMS' ) ) return 'Forums'
91- return 'Primary'
92- }
93-
94- function textPreview ( text : string ) : string {
95- return text . replace ( / \s + / g, ' ' ) . trim ( ) . slice ( 0 , 160 )
96- }
97-
9874function bodyToParagraphs ( text : string ) : Array < { id : string ; content : string } > {
9975 return text
10076 . split ( / \n { 2 , } / )
@@ -114,31 +90,6 @@ async function runGogJson<T>(args: string[]): Promise<T> {
11490 return JSON . parse ( stdout ) as T
11591}
11692
117- type GogSearchThread = {
118- id : string
119- subject ?: string
120- from ?: string
121- date ?: string
122- labels ?: string [ ]
123- messageCount ?: number
124- }
125-
126- type GogThreadMessage = {
127- id : string
128- internalDate ?: string
129- labelIds ?: string [ ]
130- }
131-
132- type GogThreadGet = {
133- body ?: string
134- headers ?: Record < string , string >
135- message ?: GogThreadMessage
136- thread ?: {
137- id : string
138- messages : GogThreadMessage [ ]
139- }
140- }
141-
14293type GmailReviewEmail = {
14394 id : string
14495 from : string
@@ -188,45 +139,6 @@ function buildReplyDraftFromEmail(email: GmailReviewEmail) {
188139 }
189140}
190141
191- async function fetchUnreadInboxEmails ( max : number ) : Promise < GmailReviewEmail [ ] > {
192- const threads = await runGogJson < GogSearchThread [ ] > ( [ 'gmail' , 'search' , 'is:unread in:inbox' , '--max' , String ( max ) , '--json' , '--results-only' , '--no-input' ] )
193- const emails = await Promise . all ( threads . map ( async thread => {
194- const detail = await runGogJson < GogThreadGet > ( [ 'gmail' , 'thread' , 'get' , thread . id , '--json' , '--full' , '--results-only' , '--no-input' ] )
195- const headers = detail . headers ?? { }
196- const body = firstNonEmpty ( detail . body , thread . subject ? `${ thread . subject } \n\n${ thread . from ?? '' } ` : 'No content available.' )
197- const messages = detail . thread ?. messages ?? ( detail . message ? [ detail . message ] : [ ] )
198- const latestMessage = messages [ messages . length - 1 ]
199- const labelIds = latestMessage ?. labelIds ?? thread . labels ?? [ ]
200- const subject = firstNonEmpty ( headers . subject , thread . subject , 'No subject' )
201- const from = firstNonEmpty ( headers . from , thread . from , 'Unknown sender' )
202- const to = firstNonEmpty ( headers . to , process . env . GOG_ACCOUNT , '' )
203- const cc = headers . cc ? headers . cc . split ( ',' ) . map ( value => value . trim ( ) ) . filter ( Boolean ) : [ ]
204- const bcc = headers . bcc ? headers . bcc . split ( ',' ) . map ( value => value . trim ( ) ) . filter ( Boolean ) : [ ]
205- const headerList = Object . entries ( headers )
206- . filter ( ( [ , value ] ) => typeof value === 'string' && value . trim ( ) . length > 0 )
207- . slice ( 0 , 8 )
208- . map ( ( [ label , value ] ) => ( { label : label [ 0 ] . toUpperCase ( ) + label . slice ( 1 ) , value } ) )
209- return {
210- id : thread . id ,
211- from,
212- to,
213- cc,
214- bcc,
215- subject,
216- preview : textPreview ( body ) ,
217- body,
218- headers : headerList ,
219- unread : labelIds . includes ( 'UNREAD' ) ,
220- category : gmailCategoryFromLabels ( labelIds ) ,
221- timestamp : Number ( latestMessage ?. internalDate ?? 0 ) ,
222- gmailThreadId : thread . id ,
223- gmailMessageId : latestMessage ?. id ?? thread . id ,
224- replyState : 'idle' as const ,
225- }
226- } ) )
227- return emails . sort ( ( a , b ) => b . timestamp - a . timestamp )
228- }
229-
230142async function createOrUpdateGmailDraft ( email : GmailReviewEmail , editedDraft : Record < string , unknown > ) : Promise < string > {
231143 const to = typeof editedDraft . to === 'string' && editedDraft . to . trim ( ) . length > 0 ? editedDraft . to : email . from
232144 const subject = typeof editedDraft . subject === 'string' && editedDraft . subject . trim ( ) . length > 0
@@ -279,109 +191,6 @@ async function sendGmailDraft(draftId: string): Promise<void> {
279191 await runGog ( [ 'gmail' , 'drafts' , 'send' , draftId , '--json' , '--results-only' , '--no-input' ] )
280192}
281193
282- async function startGmailMonitor ( sessionId : string ) : Promise < void > {
283- if ( gmailMonitors . get ( sessionId ) ?. running ) return
284- gmailMonitors . set ( sessionId , { running : true } )
285- try {
286- while ( true ) {
287- const session = getSession ( sessionId )
288- if ( ! session ) break
289- if ( session . pageStatus ?. stopMonitoring ) break
290- if ( session . status === 'completed' ) break
291- if ( session . status !== 'rewriting' ) {
292- await sleep ( 1000 )
293- continue
294- }
295-
296- const payload = session . payload as Record < string , unknown >
297- const result = ( session . result ?? { } ) as Record < string , unknown >
298- const inbox = Array . isArray ( payload . inbox ) ? payload . inbox as GmailReviewEmail [ ] : [ ]
299- const defaultDraft = payload . draft as Record < string , unknown > | undefined
300-
301- if ( result . requestReplyDraft ) {
302- const emailId = String ( result . emailId ?? '' )
303- const targetEmail = inbox . find ( email => email . id === emailId )
304- if ( ! targetEmail ) {
305- updateSessionPayload ( sessionId , payload )
306- continue
307- }
308- const replyDraft = buildReplyDraftFromEmail ( targetEmail )
309- const draftId = await createOrUpdateGmailDraft ( targetEmail , {
310- to : replyDraft . to ,
311- subject : replyDraft . subject ,
312- body : replyDraft . paragraphs . map ( p => p . content ) . join ( '\n\n' ) ,
313- cc : [ ] ,
314- bcc : [ ] ,
315- } )
316- updateSessionPayload ( sessionId , {
317- ...payload ,
318- inbox : inbox . map ( email => email . id === emailId ? {
319- ...email ,
320- gmailDraftId : draftId ,
321- replyState : 'ready' ,
322- replyUnread : true ,
323- replyDraft,
324- } : email ) ,
325- draft : defaultDraft ,
326- } )
327- continue
328- }
329-
330- if ( result . readMore ) {
331- const existingIds = new Set ( inbox . map ( email => email . id ) )
332- const moreEmails = ( await fetchUnreadInboxEmails ( Math . max ( inbox . length + 10 , 20 ) ) ) . filter ( email => ! existingIds . has ( email . id ) )
333- updateSessionPayload ( sessionId , {
334- ...payload ,
335- inbox : [ ...inbox , ...moreEmails ] ,
336- draft : defaultDraft ,
337- } )
338- continue
339- }
340-
341- const editedDraft = result . editedDraft && typeof result . editedDraft === 'object'
342- ? result . editedDraft as Record < string , unknown >
343- : null
344- const editedEmailId = typeof editedDraft ?. emailId === 'string' ? editedDraft . emailId : ''
345- if ( editedDraft && editedEmailId ) {
346- const targetEmail = inbox . find ( email => email . id === editedEmailId )
347- if ( ! targetEmail ) {
348- updateSessionPayload ( sessionId , payload )
349- continue
350- }
351- const draftId = await createOrUpdateGmailDraft ( targetEmail , editedDraft )
352- const body = typeof editedDraft . body === 'string' ? editedDraft . body : ''
353- const paragraphs = Array . isArray ( editedDraft . paragraphs )
354- ? editedDraft . paragraphs as Array < { id : string ; content : string } >
355- : bodyToParagraphs ( body )
356- updateSessionPayload ( sessionId , {
357- ...payload ,
358- inbox : inbox . map ( email => email . id === editedEmailId ? {
359- ...email ,
360- gmailDraftId : draftId ,
361- replyState : 'ready' ,
362- replyUnread : true ,
363- replyDraft : {
364- replyTo : email . from ,
365- to : typeof editedDraft . to === 'string' ? editedDraft . to : email . from ,
366- subject : typeof editedDraft . subject === 'string' ? editedDraft . subject : `Re: ${ email . subject } ` ,
367- paragraphs,
368- cc : Array . isArray ( editedDraft . cc ) ? editedDraft . cc as string [ ] : [ ] ,
369- bcc : Array . isArray ( editedDraft . bcc ) ? editedDraft . bcc as string [ ] : [ ] ,
370- } ,
371- } : email ) ,
372- draft : defaultDraft ,
373- } )
374- continue
375- }
376-
377- await sleep ( 1000 )
378- }
379- } catch ( err ) {
380- console . error ( '[agentclick] Gmail monitor failed:' , err )
381- } finally {
382- gmailMonitors . delete ( sessionId )
383- }
384- }
385194
386195// Lightweight identity endpoint so clients can verify the service is AgentClick.
387196app . get ( '/api/identity' , ( _req , res ) => {
@@ -566,42 +375,6 @@ app.post('/api/review', async (req, res) => {
566375 res . json ( { sessionId : id , url } )
567376} )
568377
569- app . post ( '/api/gmail/review/start' , async ( req , res ) => {
570- try {
571- const max = typeof req . body ?. max === 'number' ? Math . max ( 1 , Math . min ( 20 , req . body . max ) ) : 10
572- const inbox = await fetchUnreadInboxEmails ( max )
573- const now = Date . now ( )
574- const id = createSessionId ( )
575- createSession ( {
576- id,
577- type : 'email_review' ,
578- payload : {
579- inbox,
580- draft : {
581- replyTo : '' ,
582- to : '' ,
583- subject : '' ,
584- paragraphs : [ ] ,
585- } ,
586- } ,
587- sessionKey : typeof req . body ?. sessionKey === 'string' ? req . body . sessionKey : undefined ,
588- status : 'pending' ,
589- pageStatus : { state : 'created' , updatedAt : now } ,
590- createdAt : now ,
591- updatedAt : now ,
592- revision : 0 ,
593- } )
594- void startGmailMonitor ( id )
595- const url = `${ WEB_ORIGIN } /review/${ id } `
596- if ( req . body ?. noOpen !== true ) {
597- await open ( url )
598- }
599- res . json ( { ok : true , sessionId : id , url, count : inbox . length } )
600- } catch ( err ) {
601- res . status ( 500 ) . json ( { error : err instanceof Error ? err . message : String ( err ) } )
602- }
603- } )
604-
605378app . get ( '/api/memory/files' , ( req , res ) => {
606379 const projectRoot = join ( __dirname , '../../..' )
607380 const currentContextFiles = typeof req . query . currentContextFiles === 'string'
0 commit comments