Skip to content

Commit 53d0699

Browse files
committed
Remove server-side Gmail monitor
1 parent 754628e commit 53d0699

File tree

3 files changed

+0
-290
lines changed

3 files changed

+0
-290
lines changed

packages/server/src/index.ts

Lines changed: 0 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const README_PATH = join(__dirname, '../../../README.md')
3030
const SHOULD_SERVE_BUILT_WEB = existsSync(WEB_DIST_DIR) && (__filename.endsWith('/dist/index.js') || process.env.NODE_ENV === 'production')
3131
const WEB_ORIGIN = process.env.WEB_ORIGIN || (SHOULD_SERVE_BUILT_WEB ? `http://localhost:${PORT}` : 'http://localhost:5173')
3232
const execFileAsync = promisify(execFile)
33-
const gmailMonitors = new Map<string, { running: boolean }>()
3433

3534
app.use(cors())
3635
app.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-
9874
function 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-
14293
type 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-
230142
async 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.
387196
app.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-
605378
app.get('/api/memory/files', (req, res) => {
606379
const projectRoot = join(__dirname, '../../..')
607380
const currentContextFiles = typeof req.query.currentContextFiles === 'string'

packages/web/src/App.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from 'react'
22
import { BrowserRouter, Routes, Route } from 'react-router-dom'
33
import ReviewPage from './pages/ReviewPage'
44
import EmailTestPage from './pages/EmailTestPage'
5-
import EmailLivePage from './pages/EmailLivePage'
65
import ApprovalPage from './pages/ApprovalPage'
76
import CodeReviewPage from './pages/CodeReviewPage'
87
import HomePage from './pages/HomePage'
@@ -103,7 +102,6 @@ export default function App() {
103102
<Route path="/" element={<HomePage />} />
104103
<Route path="/review/:id" element={<ReviewPage />} />
105104
<Route path="/email-test" element={<EmailTestPage />} />
106-
<Route path="/email-live" element={<EmailLivePage />} />
107105
<Route path="/approval/:id" element={<ApprovalPage />} />
108106
<Route path="/code-review/:id" element={<CodeReviewPage />} />
109107
<Route path="/form-review/:id" element={<FormReviewPage />} />

packages/web/src/pages/EmailLivePage.tsx

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)