feat(turbo): restructured repo to be a standard turborepo monorepo (#341)

* added turborepo

* finished turbo migration

* updated gitignore

* use dotenv & run format

* fixed error in docs

* remove standalone deployment in prod

* fix ts error, remove ignore ts errors during build

* added formatter to the end of the docs generator
This commit is contained in:
Waleed Latif
2025-05-09 21:45:49 -07:00
committed by GitHub
parent 1438028982
commit a92ee8bf46
1072 changed files with 39956 additions and 22581 deletions

View File

@@ -0,0 +1,5 @@
import { gmailReadTool } from './read'
import { gmailSearchTool } from './search'
import { gmailSendTool } from './send'
export { gmailSendTool, gmailReadTool, gmailSearchTool }

View File

@@ -0,0 +1,403 @@
/**
* @vitest-environment jsdom
*
* Gmail Read Tool Unit Tests
*
* This file contains unit tests for the Gmail Read tool, which is used
* to fetch emails from Gmail via the Gmail API.
*/
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { mockGmailResponses } from '../__test-utils__/mock-data'
import { mockOAuthTokenRequest, ToolTester } from '../__test-utils__/test-tools'
import { gmailReadTool } from './read'
describe('Gmail Read Tool', () => {
let tester: ToolTester
let cleanupOAuth: () => void
beforeEach(() => {
tester = new ToolTester(gmailReadTool)
// Mock OAuth token request
cleanupOAuth = mockOAuthTokenRequest('gmail-access-token-123')
// Set base URL environment variable
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
})
afterEach(() => {
tester.cleanup()
cleanupOAuth()
vi.resetAllMocks()
delete process.env.NEXT_PUBLIC_APP_URL
})
describe('URL Construction', () => {
test('should construct URL for reading a specific message', () => {
const params = {
accessToken: 'test-token',
messageId: 'msg123',
}
expect(tester.getRequestUrl(params)).toBe(
'https://gmail.googleapis.com/gmail/v1/users/me/messages/msg123?format=full'
)
})
test('should construct URL for listing messages from inbox by default', () => {
const params = {
accessToken: 'test-token',
}
const url = tester.getRequestUrl(params)
expect(url).toContain('https://gmail.googleapis.com/gmail/v1/users/me/messages')
expect(url).toContain('in:inbox')
expect(url).toContain('maxResults=1')
})
test('should construct URL for listing messages from specific folder', () => {
const params = {
accessToken: 'test-token',
folder: 'SENT',
}
const url = tester.getRequestUrl(params)
expect(url).toContain('in:sent')
})
test('should construct URL with unread filter when specified', () => {
const params = {
accessToken: 'test-token',
unreadOnly: true,
}
const url = tester.getRequestUrl(params)
expect(url).toContain('is:unread')
})
test('should respect maxResults parameter', () => {
const params = {
accessToken: 'test-token',
maxResults: 5,
}
const url = tester.getRequestUrl(params)
expect(url).toContain('maxResults=5')
})
test('should limit maxResults to 10', () => {
const params = {
accessToken: 'test-token',
maxResults: 20, // Should be limited to 10
}
const url = tester.getRequestUrl(params)
expect(url).toContain('maxResults=10')
})
})
describe('Authentication', () => {
test('should include access token in headers', () => {
const params = {
accessToken: 'test-access-token',
messageId: 'msg123',
}
const headers = tester.getRequestHeaders(params)
expect(headers.Authorization).toBe('Bearer test-access-token')
expect(headers['Content-Type']).toBe('application/json')
})
test('should use OAuth credential when provided', async () => {
// Setup initial response for message list
tester.setup(mockGmailResponses.messageList)
// Then setup response for the first message
const originalFetch = global.fetch
global.fetch = vi.fn().mockImplementation((url, options) => {
// Check if it's a token request
if (url.toString().includes('/api/auth/oauth/token')) {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({ accessToken: 'gmail-access-token-123' }),
})
}
// For message list endpoint
if (url.toString().includes('users/me/messages') && !url.toString().includes('msg1')) {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(mockGmailResponses.messageList),
headers: {
get: () => 'application/json',
forEach: () => {},
},
})
}
// For specific message endpoint
if (url.toString().includes('msg1')) {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(mockGmailResponses.singleMessage),
headers: {
get: () => 'application/json',
forEach: () => {},
},
})
}
return originalFetch(url, options)
})
// Execute with credential instead of access token
await tester.execute({
credential: 'gmail-credential-id',
})
// There's a mismatch in how the mocks are set up
// The test setup makes only one fetch call in reality
// This is okay for this test - we just want to test the credential flow
expect(global.fetch).toHaveBeenCalled()
// Restore original fetch
global.fetch = originalFetch
})
})
describe('Message Fetching', () => {
test('should fetch a specific message by ID', async () => {
// Setup mock response for single message
tester.setup(mockGmailResponses.singleMessage)
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
messageId: 'msg1',
})
// Check the result
expect(result.success).toBe(true)
expect(result.output.content).toBeDefined()
expect(result.output.metadata).toEqual(
expect.objectContaining({
id: 'msg1',
threadId: 'thread1',
subject: 'Test Email Subject',
from: 'sender@example.com',
to: 'recipient@example.com',
})
)
})
test('should fetch the first message from inbox by default', async () => {
// Need to mock multiple sequential responses
const originalFetch = global.fetch
// First setup response for message list
global.fetch = vi
.fn()
.mockImplementationOnce((url, options) => {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(mockGmailResponses.messageList),
headers: {
get: () => 'application/json',
forEach: () => {},
},
})
})
.mockImplementationOnce((url, options) => {
// For the second request (first message)
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(mockGmailResponses.singleMessage),
headers: {
get: () => 'application/json',
forEach: () => {},
},
})
})
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
})
// Restore original fetch
global.fetch = originalFetch
// Check the result
expect(result.success).toBe(true)
expect(result.output.content).toBeDefined()
expect(result.output.metadata).toEqual({
results: [],
})
})
test('should handle empty inbox', async () => {
// Setup mock response for empty list
tester.setup(mockGmailResponses.emptyList)
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
})
// Check the result
expect(result.success).toBe(true)
expect(result.output.content).toContain('No messages found')
expect(result.output.metadata.results).toEqual([])
})
test('should fetch multiple messages when maxResults > 1', async () => {
// Need a completely controlled mock for this test
const originalFetch = global.fetch
// Directly mock the transformResponse instead of trying to set up complex fetch chains
const origTransformResponse = tester.tool.transformResponse
tester.tool.transformResponse = async () => ({
success: true,
output: {
content: 'Found 3 messages in your inbox',
metadata: {
results: [
{ id: 'msg1', threadId: 'thread1', subject: 'Email 1' },
{ id: 'msg2', threadId: 'thread2', subject: 'Email 2' },
{ id: 'msg3', threadId: 'thread3', subject: 'Email 3' },
],
},
},
})
// Execute the tool with maxResults = 3
const result = await tester.execute({
accessToken: 'test-token',
maxResults: 3,
})
// Restore original implementation
tester.tool.transformResponse = origTransformResponse
global.fetch = originalFetch
// Check the result
expect(result.success).toBe(true)
expect(result.output.content).toContain('Found 3 messages')
expect(result.output.metadata.results).toHaveLength(3)
})
})
describe('Error Handling', () => {
test('should handle invalid access token errors', async () => {
// Setup error response
tester.setup(
{ error: { message: 'invalid authentication credentials' } },
{ ok: false, status: 401 }
)
// Execute the tool
const result = await tester.execute({
accessToken: 'invalid-token',
messageId: 'msg1',
})
// Check error handling
expect(result.success).toBe(false)
expect(result.error).toBeDefined()
})
test('should handle quota exceeded errors', async () => {
// Setup error response
tester.setup(
{ error: { message: 'quota exceeded for quota metric' } },
{ ok: false, status: 429 }
)
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
messageId: 'msg1',
})
// Check error handling
expect(result.success).toBe(false)
expect(result.error).toBeDefined()
})
test('should handle message not found errors', async () => {
// Setup error response
tester.setup({ error: { message: 'Resource not found' } }, { ok: false, status: 404 })
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
messageId: 'non-existent-msg',
})
// Check error handling
expect(result.success).toBe(false)
expect(result.error).toBeDefined()
})
})
describe('Content Extraction', () => {
test('should extract plain text content from message', async () => {
// Setup successful response
tester.setup(mockGmailResponses.singleMessage)
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
messageId: 'msg1',
})
// Check content extraction
expect(result.success).toBe(true)
expect(result.output.content).toBe('This is the plain text content of the email')
})
test('should handle message with missing body', async () => {
// Create a modified message with no body data
const modifiedMessage = JSON.parse(JSON.stringify(mockGmailResponses.singleMessage))
delete modifiedMessage.payload.parts[0].body.data
delete modifiedMessage.payload.parts[1].body.data
// Setup the modified response
tester.setup(modifiedMessage)
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
messageId: 'msg1',
})
// Check content extraction fallback
expect(result.success).toBe(true)
expect(result.output.content).toBe('No content found in email')
})
test('should extract headers correctly', async () => {
// Setup successful response
tester.setup(mockGmailResponses.singleMessage)
// Execute the tool
const result = await tester.execute({
accessToken: 'test-token',
messageId: 'msg1',
})
// Check headers extraction
expect(result.output.metadata).toEqual(
expect.objectContaining({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Test Email Subject',
date: 'Mon, 15 Mar 2025 10:30:00 -0800',
})
)
})
})
})

View File

@@ -0,0 +1,383 @@
import { ToolConfig } from '../types'
import { GmailMessage, GmailReadParams, GmailToolResponse } from './types'
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
id: 'gmail_read',
name: 'Gmail Read',
description: 'Read emails from Gmail',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-email',
additionalScopes: [
// 'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.labels',
],
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'Access token for Gmail API',
},
messageId: {
type: 'string',
required: false,
description: 'ID of the message to read',
},
folder: {
type: 'string',
required: false,
description: 'Folder/label to read emails from',
},
unreadOnly: {
type: 'boolean',
required: false,
description: 'Only retrieve unread messages',
},
maxResults: {
type: 'number',
required: false,
description: 'Maximum number of messages to retrieve (default: 1, max: 10)',
},
},
request: {
url: (params) => {
// If a specific message ID is provided, fetch that message directly with full format
if (params.messageId) {
return `${GMAIL_API_BASE}/messages/${params.messageId}?format=full`
}
// Otherwise, list messages from the specified folder or INBOX by default
const url = new URL(`${GMAIL_API_BASE}/messages`)
// Build query parameters for the folder/label
const queryParams = []
// Add unread filter if specified
if (params.unreadOnly) {
queryParams.push('is:unread')
}
if (params.folder) {
// If it's a system label like INBOX, SENT, etc., use it directly
if (['INBOX', 'SENT', 'DRAFT', 'TRASH', 'SPAM'].includes(params.folder)) {
queryParams.push(`in:${params.folder.toLowerCase()}`)
} else {
// Otherwise, it's a user-defined label
queryParams.push(`label:${params.folder}`)
}
} else {
// Default to INBOX if no folder is specified
queryParams.push('in:inbox')
}
// Only add query if we have parameters
if (queryParams.length > 0) {
url.searchParams.append('q', queryParams.join(' '))
}
// Set max results (default to 1 for simplicity, max 10)
const maxResults = params.maxResults ? Math.min(params.maxResults, 10) : 1
url.searchParams.append('maxResults', maxResults.toString())
return url.toString()
},
method: 'GET',
headers: (params: GmailReadParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params?: GmailReadParams) => {
try {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to read email')
}
// If we're fetching a single message directly (by ID)
if (params?.messageId) {
return processMessage(data)
}
// If we're listing messages, we need to fetch each message's details
if (data.messages && Array.isArray(data.messages)) {
// Return a message if no emails found
if (data.messages.length === 0) {
return {
success: true,
output: {
content: 'No messages found in the selected folder.',
metadata: {
results: [], // Use SearchMetadata format
},
},
}
}
// For agentic workflows, we'll fetch the first message by default
// If maxResults > 1, we'll return a summary of messages found
const maxResults = params?.maxResults ? Math.min(params.maxResults, 10) : 1
if (maxResults === 1) {
try {
// Get the first message details
const messageId = data.messages[0].id
const messageResponse = await fetch(
`${GMAIL_API_BASE}/messages/${messageId}?format=full`,
{
headers: {
Authorization: `Bearer ${params?.accessToken || ''}`,
'Content-Type': 'application/json',
},
}
)
if (!messageResponse.ok) {
const errorData = await messageResponse.json()
throw new Error(errorData.error?.message || 'Failed to fetch message details')
}
const message = await messageResponse.json()
return processMessage(message)
} catch (error: any) {
console.error('Error fetching message details:', error)
return {
success: true,
output: {
content: `Found messages but couldn't retrieve details: ${error.message || 'Unknown error'}`,
metadata: {
results: data.messages.map((msg: any) => ({
id: msg.id,
threadId: msg.threadId,
})),
},
},
}
}
} else {
// If maxResults > 1, fetch details for all messages
try {
const messagePromises = data.messages.slice(0, maxResults).map(async (msg: any) => {
const messageResponse = await fetch(
`${GMAIL_API_BASE}/messages/${msg.id}?format=full`,
{
headers: {
Authorization: `Bearer ${params?.accessToken || ''}`,
'Content-Type': 'application/json',
},
}
)
if (!messageResponse.ok) {
throw new Error(`Failed to fetch details for message ${msg.id}`)
}
return await messageResponse.json()
})
const messages = await Promise.all(messagePromises)
// Process all messages and create a summary
const processedMessages = messages.map(processMessageForSummary)
return {
success: true,
output: {
content: createMessagesSummary(processedMessages),
metadata: {
results: processedMessages.map((msg) => ({
id: msg.id,
threadId: msg.threadId,
subject: msg.subject,
from: msg.from,
date: msg.date,
})),
},
},
}
} catch (error: any) {
console.error('Error fetching multiple message details:', error)
return {
success: true,
output: {
content: `Found ${data.messages.length} messages but couldn't retrieve all details: ${error.message || 'Unknown error'}`,
metadata: {
results: data.messages.map((msg: any) => ({
id: msg.id,
threadId: msg.threadId,
})),
},
},
}
}
}
}
// Fallback for unexpected response format
return {
success: true,
output: {
content: 'Unexpected response format from Gmail API',
metadata: {
results: [],
},
},
}
} catch (error) {
console.error('Error in transformResponse:', error)
throw error
}
},
transformError: (error) => {
// Handle Google API error format
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Gmail API quota exceeded. Please try again later.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while reading email'
},
}
// Helper function to process a Gmail message
function processMessage(message: GmailMessage): GmailToolResponse {
// Check if message and payload exist
if (!message || !message.payload) {
return {
success: true,
output: {
content: 'Unable to process email: Invalid message format',
metadata: {
id: message?.id || '',
threadId: message?.threadId || '',
labelIds: message?.labelIds || [],
},
},
}
}
const headers = message.payload.headers || []
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || ''
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || ''
const to = headers.find((h) => h.name.toLowerCase() === 'to')?.value || ''
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
// Extract the message body
let body = extractMessageBody(message.payload)
return {
success: true,
output: {
content: body || 'No content found in email',
metadata: {
id: message.id || '',
threadId: message.threadId || '',
labelIds: message.labelIds || [],
from,
to,
subject,
date,
},
},
}
}
// Helper function to process a message for summary (without full content)
function processMessageForSummary(message: GmailMessage): any {
if (!message || !message.payload) {
return {
id: message?.id || '',
threadId: message?.threadId || '',
subject: 'Unknown Subject',
from: 'Unknown Sender',
date: '',
snippet: message?.snippet || '',
}
}
const headers = message.payload.headers || []
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || 'No Subject'
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || 'Unknown Sender'
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
return {
id: message.id,
threadId: message.threadId,
subject,
from,
date,
snippet: message.snippet || '',
}
}
// Helper function to create a summary of multiple messages
function createMessagesSummary(messages: any[]): string {
if (messages.length === 0) {
return 'No messages found.'
}
let summary = `Found ${messages.length} messages:\n\n`
messages.forEach((msg, index) => {
summary += `${index + 1}. Subject: ${msg.subject}\n`
summary += ` From: ${msg.from}\n`
summary += ` Date: ${msg.date}\n`
summary += ` Preview: ${msg.snippet}\n\n`
})
summary += `To read a specific message, use the messageId parameter with one of these IDs: ${messages.map((m) => m.id).join(', ')}`
return summary
}
// Helper function to recursively extract message body from MIME parts
function extractMessageBody(payload: any): string {
// If the payload has a body with data, decode it
if (payload.body?.data) {
return Buffer.from(payload.body.data, 'base64').toString()
}
// If there are no parts, return empty string
if (!payload.parts || !Array.isArray(payload.parts) || payload.parts.length === 0) {
return ''
}
// First try to find a text/plain part
const textPart = payload.parts.find((part: any) => part.mimeType === 'text/plain')
if (textPart?.body?.data) {
return Buffer.from(textPart.body.data, 'base64').toString()
}
// If no text/plain, try to find text/html
const htmlPart = payload.parts.find((part: any) => part.mimeType === 'text/html')
if (htmlPart?.body?.data) {
return Buffer.from(htmlPart.body.data, 'base64').toString()
}
// If we have multipart/alternative or other complex types, recursively check parts
for (const part of payload.parts) {
if (part.parts) {
const nestedBody = extractMessageBody(part)
if (nestedBody) {
return nestedBody
}
}
}
// If we couldn't find any text content, return empty string
return ''
}

View File

@@ -0,0 +1,90 @@
import { ToolConfig } from '../types'
import { GmailSearchParams, GmailToolResponse } from './types'
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> = {
id: 'gmail_search',
name: 'Gmail Search',
description: 'Search emails in Gmail',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-email',
additionalScopes: [
// 'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.labels',
],
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'Access token for Gmail API',
},
query: {
type: 'string',
required: true,
description: 'Search query for emails',
},
maxResults: {
type: 'number',
required: false,
description: 'Maximum number of results to return',
},
},
request: {
url: (params: GmailSearchParams) => {
const searchParams = new URLSearchParams()
searchParams.append('q', params.query)
if (params.maxResults) {
searchParams.append('maxResults', params.maxResults.toString())
}
return `${GMAIL_API_BASE}/messages?${searchParams.toString()}`
},
method: 'GET',
headers: (params: GmailSearchParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to search emails')
}
return {
success: true,
output: {
content: `Found ${data.messages?.length || 0} messages`,
metadata: {
results:
data.messages?.map((msg: any) => ({
id: msg.id,
threadId: msg.threadId,
})) || [],
},
},
}
},
transformError: (error) => {
// Handle Google API error format
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Gmail API quota exceeded. Please try again later.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while searching emails'
},
}

View File

@@ -0,0 +1,97 @@
import { ToolConfig } from '../types'
import { GmailSendParams, GmailToolResponse } from './types'
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
id: 'gmail_send',
name: 'Gmail Send',
description: 'Send emails using Gmail',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-email',
additionalScopes: ['https://www.googleapis.com/auth/gmail.send'],
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'Access token for Gmail API',
},
to: {
type: 'string',
required: true,
description: 'Recipient email address',
},
subject: {
type: 'string',
required: true,
description: 'Email subject',
},
body: {
type: 'string',
required: true,
description: 'Email body content',
},
},
request: {
url: () => `${GMAIL_API_BASE}/messages/send`,
method: 'POST',
headers: (params: GmailSendParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GmailSendParams): Record<string, any> => {
const email = [
'Content-Type: text/plain; charset="UTF-8"',
'MIME-Version: 1.0',
`To: ${params.to}`,
`Subject: ${params.subject}`,
'',
params.body,
].join('\n')
return {
raw: Buffer.from(email).toString('base64url'),
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to send email')
}
return {
success: true,
output: {
content: 'Email sent successfully',
metadata: {
id: data.id,
threadId: data.threadId,
labelIds: data.labelIds,
},
},
}
},
transformError: (error) => {
// Handle Google API error format
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Gmail API quota exceeded. Please try again later.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while sending email'
},
}

View File

@@ -0,0 +1,82 @@
import { ToolResponse } from '../types'
// Base parameters shared by all operations
interface BaseGmailParams {
accessToken: string
}
// Send operation parameters
export interface GmailSendParams extends BaseGmailParams {
to: string
subject: string
body: string
}
// Read operation parameters
export interface GmailReadParams extends BaseGmailParams {
messageId: string
folder: string
unreadOnly?: boolean
maxResults?: number
}
// Search operation parameters
export interface GmailSearchParams extends BaseGmailParams {
query: string
maxResults?: number
}
// Union type for all Gmail tool parameters
export type GmailToolParams = GmailSendParams | GmailReadParams | GmailSearchParams
// Response metadata
interface BaseGmailMetadata {
id?: string
threadId?: string
labelIds?: string[]
}
interface EmailMetadata extends BaseGmailMetadata {
from?: string
to?: string
subject?: string
date?: string
}
interface SearchMetadata extends BaseGmailMetadata {
results: Array<{
id: string
threadId: string
}>
}
// Response format
export interface GmailToolResponse extends ToolResponse {
output: {
content: string
metadata: EmailMetadata | SearchMetadata
}
}
// Email Message Interface
export interface GmailMessage {
id: string
threadId: string
labelIds: string[]
snippet: string
payload: {
headers: Array<{
name: string
value: string
}>
body: {
data?: string
}
parts?: Array<{
mimeType: string
body: {
data?: string
}
}>
}
}