import { execFileSync } from 'node:child_process'; import { existsSync } from 'node:fs'; type ToolResult = { content: Array<{ type: string; text: string }>; isError?: boolean; }; type Params = { action: string; sql?: string; search?: string; domain?: string; sender?: string; before?: string; after?: string; content_type?: string; limit?: number; }; const DEFAULT_LIMIT = 20; const MAX_LIMIT = 100; function getDbPath(): string { return process.env.OFFICER_EMAIL_DB ?? '/officer/emails.db'; } function sqlStr(v: string): string { return `'${v.replace(/'/g, "''")}'`; } function queryJson(sql: string): Record[] { const output = execFileSync('sqlite3', ['-json', getDbPath()], { input: sql, encoding: 'utf-8', timeout: 10000, }); const trimmed = output.trim(); if (!trimmed) return []; return JSON.parse(trimmed); } function execAndCount(sql: string): number { const output = execFileSync('sqlite3', [getDbPath()], { input: `${sql};\nSELECT changes();`, encoding: 'utf-8', timeout: 10000, }); return parseInt(output.trim(), 10) || 0; } function ok(text: string): ToolResult { return { content: [{ type: 'text', text }] }; } function err(text: string): ToolResult { return { content: [{ type: 'text', text }], isError: true }; } function formatRows(rows: Record[], limit: number): string { if (rows.length === 0) return 'No results.'; const cols = Object.keys(rows[0]!); const lines = rows.slice(0, limit).map((row, i) => { const fields = cols.map((c) => `${c}: ${row[c] ?? ''}`).join(' | '); return `${i + 1}. ${fields}`; }); const header = `${rows.length} result${rows.length !== 1 ? 's' : ''}${rows.length > limit ? ` (showing first ${limit})` : ''}:`; return [header, '', ...lines].join('\n'); } // ── Helpers ── function buildWhereClause(params: Params): string { const conditions: string[] = []; if (params.domain) { conditions.push(`from_domain = ${sqlStr(params.domain.toLowerCase())}`); } if (params.sender) { conditions.push(`from_address = ${sqlStr(params.sender.toLowerCase())}`); } if (params.before) { conditions.push(`date < ${sqlStr(params.before)}`); } if (params.after) { conditions.push(`date > ${sqlStr(params.after)}`); } if (params.search) { const escaped = sqlStr(`%${params.search}%`); conditions.push(`(subject LIKE ${escaped} OR from_address LIKE ${escaped} OR from_name LIKE ${escaped} OR snippet LIKE ${escaped})`); } return conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; } function withActive(where: string): string { return where ? `${where} AND deleted = 0` : 'WHERE deleted = 0'; } // ── Actions ── function runQuery(sql: string, limit: number): string { const trimmed = sql.trim().toLowerCase(); if (!trimmed.startsWith('select')) { throw new Error('Only SELECT statements are allowed in query action.'); } const rows = queryJson(sql); return formatRows(rows, limit); } function searchEmails(params: Params, limit: number): string { const where = withActive(buildWhereClause(params)); const rows = queryJson(`SELECT id, from_name, from_address, subject, date, snippet, attachment_count FROM emails ${where} ORDER BY date DESC LIMIT ${limit}`); return formatRows(rows, limit); } function getStats(): string { const totalRows = queryJson('SELECT COUNT(*) as count FROM emails WHERE deleted = 0'); const total = (totalRows[0]?.count as number) ?? 0; if (total === 0) return 'Email database is empty.'; const dateRange = queryJson('SELECT MIN(date) as oldest, MAX(date) as newest FROM emails WHERE deleted = 0'); const topDomains = queryJson('SELECT from_domain, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_domain ORDER BY count DESC LIMIT 10'); const topSenders = queryJson('SELECT from_address, from_name, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_address ORDER BY count DESC LIMIT 10'); const attRows = queryJson('SELECT COUNT(*) as count FROM emails WHERE deleted = 0 AND attachment_count > 0'); const withAttachments = (attRows[0]?.count as number) ?? 0; const dr = dateRange[0] ?? {}; const lines = [ `**Email Database Statistics**`, ``, `Total emails: ${total}`, `With attachments: ${withAttachments}`, `Date range: ${(dr.oldest as string)?.slice(0, 10)} to ${(dr.newest as string)?.slice(0, 10)}`, ``, `**Top 10 Domains:**`, ...topDomains.map((d, i) => `${i + 1}. ${d.from_domain} (${d.count})`), ``, `**Top 10 Senders:**`, ...topSenders.map((s, i) => `${i + 1}. ${s.from_name ? `${s.from_name} <${s.from_address}>` : s.from_address} (${s.count})`), ]; return lines.join('\n'); } function countEmails(params: Params): string { const where = withActive(buildWhereClause(params)); const rows = queryJson(`SELECT COUNT(*) as count FROM emails ${where}`); const count = (rows[0]?.count as number) ?? 0; const filters: string[] = []; if (params.domain) filters.push(`domain=${params.domain}`); if (params.sender) filters.push(`sender=${params.sender}`); if (params.before) filters.push(`before=${params.before}`); if (params.after) filters.push(`after=${params.after}`); if (params.search) filters.push(`search="${params.search}"`); const desc = filters.length > 0 ? ` matching ${filters.join(', ')}` : ''; return `${count} email${count !== 1 ? 's' : ''}${desc}.`; } function listDomains(limit: number): string { const rows = queryJson(`SELECT from_domain, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_domain ORDER BY count DESC LIMIT ${limit}`); if (rows.length === 0) return 'No emails in database.'; const lines = rows.map((d, i) => `${i + 1}. ${d.from_domain} — ${d.count} email${(d.count as number) !== 1 ? 's' : ''}`); return [`**Sender Domains** (${rows.length}):`, '', ...lines].join('\n'); } function listSenders(limit: number): string { const rows = queryJson(`SELECT from_address, from_name, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_address ORDER BY count DESC LIMIT ${limit}`); if (rows.length === 0) return 'No emails in database.'; const lines = rows.map((s, i) => { const display = s.from_name ? `${s.from_name} <${s.from_address}>` : s.from_address; return `${i + 1}. ${display} — ${s.count} email${(s.count as number) !== 1 ? 's' : ''}`; }); return [`**Senders** (${rows.length}):`, '', ...lines].join('\n'); } function listAttachmentTypes(limit: number): string { const rows = queryJson(`SELECT content_type, COUNT(*) as count FROM attachments a JOIN emails e ON a.email_id = e.id WHERE e.deleted = 0 GROUP BY content_type ORDER BY count DESC LIMIT ${limit}`); if (rows.length === 0) return 'No attachments in database.'; const totalRows = queryJson('SELECT COUNT(*) as count FROM attachments a JOIN emails e ON a.email_id = e.id WHERE e.deleted = 0'); const total = (totalRows[0]?.count as number) ?? 0; const lines = rows.map((r, i) => `${i + 1}. ${r.content_type} — ${r.count}`); return [`**Attachment Types** (${total} total):`, '', ...lines].join('\n'); } function searchAttachments(params: Params, limit: number): string { const conditions: string[] = ['e.deleted = 0']; if (params.content_type) { const ct = params.content_type as string; if (ct.includes('/')) { conditions.push(`a.content_type = ${sqlStr(ct)}`); } else { conditions.push(`a.content_type LIKE ${sqlStr(ct + '/%')}`); } } if (params.domain) conditions.push(`e.from_domain = ${sqlStr(params.domain.toLowerCase())}`); if (params.sender) conditions.push(`e.from_address = ${sqlStr(params.sender.toLowerCase())}`); if (params.before) conditions.push(`e.date < ${sqlStr(params.before)}`); if (params.after) conditions.push(`e.date > ${sqlStr(params.after)}`); if (params.search) { const escaped = sqlStr(`%${params.search}%`); conditions.push(`(a.filename LIKE ${escaped} OR e.subject LIKE ${escaped})`); } const where = `WHERE ${conditions.join(' AND ')}`; const rows = queryJson(`SELECT a.filename, a.size, a.content_type, e.id as email_id, e.from_address, e.subject, e.date FROM attachments a JOIN emails e ON a.email_id = e.id ${where} ORDER BY e.date DESC LIMIT ${limit}`); return formatRows(rows, limit); } function deleteEmails(params: Params): string { const where = buildWhereClause(params); if (!where) { throw new Error('Delete requires at least one filter (domain, sender, before, after, or search).'); } const activeWhere = withActive(where); const countRows = queryJson(`SELECT COUNT(*) as count FROM emails ${activeWhere}`); const count = (countRows[0]?.count as number) ?? 0; if (count === 0) return 'No emails match the given filters.'; const changes = execAndCount(`UPDATE emails SET deleted = 1 ${activeWhere}`); return `Deleted ${changes} email${changes !== 1 ? 's' : ''}.`; } // ── Main ── export async function execute(_toolCallId: string, params: Params): Promise { const limit = Math.min(params.limit ?? DEFAULT_LIMIT, MAX_LIMIT); const dbPath = getDbPath(); if (!existsSync(dbPath)) { return err(`Email database not found at ${dbPath}. Has Gmail been synced?`); } try { switch (params.action) { case 'query': if (!params.sql) return err('sql parameter is required for query action.'); return ok(runQuery(params.sql, limit)); case 'search': if (!params.search && !params.domain && !params.sender && !params.before && !params.after) { return err('At least one filter is required: search, domain, sender, before, or after.'); } return ok(searchEmails(params, limit)); case 'stats': return ok(getStats()); case 'count': return ok(countEmails(params)); case 'domains': return ok(listDomains(limit)); case 'senders': return ok(listSenders(limit)); case 'attachment-types': return ok(listAttachmentTypes(limit)); case 'attachments': if (!params.content_type && !params.search && !params.domain && !params.sender && !params.before && !params.after) { return err('At least one filter is required: content_type, search, domain, sender, before, or after.'); } return ok(searchAttachments(params, limit)); case 'delete': return ok(deleteEmails(params)); default: return err(`Unknown action: "${params.action}". Available: query, search, stats, count, domains, senders, attachments, attachment-types, delete.`); } } catch (e) { return err(`Email DB error: ${e instanceof Error ? e.message : String(e)}`); } }