diff --git a/apps/sim/tools/gmail/create_label.ts b/apps/sim/tools/gmail/create_label.ts new file mode 100644 index 0000000000..98d9c4e447 --- /dev/null +++ b/apps/sim/tools/gmail/create_label.ts @@ -0,0 +1,119 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailCreateLabelParams { + accessToken: string + name: string + messageListVisibility?: string + labelListVisibility?: string +} + +interface GmailCreateLabelResponse { + success: boolean + output: { + id: string + name: string + messageListVisibility?: string + labelListVisibility?: string + type?: string + } +} + +export const gmailCreateLabelV2Tool: ToolConfig = + { + id: 'gmail_create_label_v2', + name: 'Gmail Create Label', + description: 'Create a new label in Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Display name for the new label', + }, + messageListVisibility: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Visibility of messages with this label in the message list (show or hide)', + }, + labelListVisibility: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Visibility of the label in the label list (labelShow, labelShowIfUnread, or labelHide)', + }, + }, + + request: { + url: () => `${GMAIL_API_BASE}/labels`, + method: 'POST', + headers: (params: GmailCreateLabelParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GmailCreateLabelParams) => { + const body: Record = { name: params.name } + if (params.messageListVisibility) { + body.messageListVisibility = params.messageListVisibility + } + if (params.labelListVisibility) { + body.labelListVisibility = params.labelListVisibility + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', name: '' }, + error: data.error?.message || 'Failed to create label', + } + } + + return { + success: true, + output: { + id: data.id, + name: data.name, + messageListVisibility: data.messageListVisibility ?? null, + labelListVisibility: data.labelListVisibility ?? null, + type: data.type ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Label ID' }, + name: { type: 'string', description: 'Label display name' }, + messageListVisibility: { + type: 'string', + description: 'Visibility of messages with this label', + optional: true, + }, + labelListVisibility: { + type: 'string', + description: 'Visibility of the label in the label list', + optional: true, + }, + type: { type: 'string', description: 'Label type (system or user)', optional: true }, + }, + } diff --git a/apps/sim/tools/gmail/delete_draft.ts b/apps/sim/tools/gmail/delete_draft.ts new file mode 100644 index 0000000000..9597931542 --- /dev/null +++ b/apps/sim/tools/gmail/delete_draft.ts @@ -0,0 +1,76 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailDeleteDraftParams { + accessToken: string + draftId: string +} + +interface GmailDeleteDraftResponse { + success: boolean + output: { + deleted: boolean + draftId: string + } +} + +export const gmailDeleteDraftV2Tool: ToolConfig = + { + id: 'gmail_delete_draft_v2', + name: 'Gmail Delete Draft', + description: 'Delete a specific draft from Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + draftId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the draft to delete', + }, + }, + + request: { + url: (params: GmailDeleteDraftParams) => `${GMAIL_API_BASE}/drafts/${params.draftId}`, + method: 'DELETE', + headers: (params: GmailDeleteDraftParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params?: GmailDeleteDraftParams) => { + if (!response.ok) { + const data = await response.json() + return { + success: false, + output: { deleted: false, draftId: params?.draftId ?? '' }, + error: data.error?.message || 'Failed to delete draft', + } + } + + return { + success: true, + output: { + deleted: true, + draftId: params?.draftId ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the draft was successfully deleted' }, + draftId: { type: 'string', description: 'ID of the deleted draft' }, + }, + } diff --git a/apps/sim/tools/gmail/delete_label.ts b/apps/sim/tools/gmail/delete_label.ts new file mode 100644 index 0000000000..2ecf1e76ad --- /dev/null +++ b/apps/sim/tools/gmail/delete_label.ts @@ -0,0 +1,76 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailDeleteLabelParams { + accessToken: string + labelId: string +} + +interface GmailDeleteLabelResponse { + success: boolean + output: { + deleted: boolean + labelId: string + } +} + +export const gmailDeleteLabelV2Tool: ToolConfig = + { + id: 'gmail_delete_label_v2', + name: 'Gmail Delete Label', + description: 'Delete a label from Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the label to delete', + }, + }, + + request: { + url: (params: GmailDeleteLabelParams) => `${GMAIL_API_BASE}/labels/${params.labelId}`, + method: 'DELETE', + headers: (params: GmailDeleteLabelParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params?: GmailDeleteLabelParams) => { + if (!response.ok) { + const data = await response.json() + return { + success: false, + output: { deleted: false, labelId: params?.labelId ?? '' }, + error: data.error?.message || 'Failed to delete label', + } + } + + return { + success: true, + output: { + deleted: true, + labelId: params?.labelId ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the label was successfully deleted' }, + labelId: { type: 'string', description: 'ID of the deleted label' }, + }, + } diff --git a/apps/sim/tools/gmail/get_draft.ts b/apps/sim/tools/gmail/get_draft.ts new file mode 100644 index 0000000000..932f0733c7 --- /dev/null +++ b/apps/sim/tools/gmail/get_draft.ts @@ -0,0 +1,117 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailGetDraftParams { + accessToken: string + draftId: string +} + +interface GmailGetDraftResponse { + success: boolean + output: { + id: string + messageId?: string + threadId?: string + to?: string + from?: string + subject?: string + body?: string + labelIds?: string[] + } +} + +export const gmailGetDraftV2Tool: ToolConfig = { + id: 'gmail_get_draft_v2', + name: 'Gmail Get Draft', + description: 'Get a specific draft from Gmail by its ID', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + draftId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the draft to retrieve', + }, + }, + + request: { + url: (params: GmailGetDraftParams) => `${GMAIL_API_BASE}/drafts/${params.draftId}?format=full`, + method: 'GET', + headers: (params: GmailGetDraftParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '' }, + error: data.error?.message || 'Failed to get draft', + } + } + + const message = data.message || {} + const headers = message.payload?.headers || [] + const getHeader = (name: string): string | undefined => + headers.find((h: Record) => h.name.toLowerCase() === name.toLowerCase()) + ?.value + + let body = '' + if (message.payload?.body?.data) { + body = Buffer.from(message.payload.body.data, 'base64').toString() + } else if (message.payload?.parts) { + const textPart = message.payload.parts.find( + (part: Record) => part.mimeType === 'text/plain' + ) + if (textPart?.body?.data) { + body = Buffer.from(textPart.body.data, 'base64').toString() + } + } + + return { + success: true, + output: { + id: data.id, + messageId: message.id ?? undefined, + threadId: message.threadId ?? undefined, + to: getHeader('To'), + from: getHeader('From'), + subject: getHeader('Subject'), + body: body || undefined, + labelIds: message.labelIds ?? undefined, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Draft ID' }, + messageId: { type: 'string', description: 'Gmail message ID', optional: true }, + threadId: { type: 'string', description: 'Gmail thread ID', optional: true }, + to: { type: 'string', description: 'Recipient email address', optional: true }, + from: { type: 'string', description: 'Sender email address', optional: true }, + subject: { type: 'string', description: 'Draft subject', optional: true }, + body: { type: 'string', description: 'Draft body text', optional: true }, + labelIds: { + type: 'array', + items: { type: 'string' }, + description: 'Draft labels', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/gmail/get_thread.ts b/apps/sim/tools/gmail/get_thread.ts new file mode 100644 index 0000000000..33641298ce --- /dev/null +++ b/apps/sim/tools/gmail/get_thread.ts @@ -0,0 +1,136 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailGetThreadParams { + accessToken: string + threadId: string + format?: string +} + +interface GmailGetThreadResponse { + success: boolean + output: { + id: string + historyId?: string + messages: Array<{ + id: string + threadId: string + labelIds?: string[] + snippet?: string + from?: string + to?: string + subject?: string + date?: string + body?: string + }> + } +} + +export const gmailGetThreadV2Tool: ToolConfig = { + id: 'gmail_get_thread_v2', + name: 'Gmail Get Thread', + description: 'Get a specific email thread from Gmail, including all messages in the thread', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the thread to retrieve', + }, + format: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Format to return the messages in (full, metadata, or minimal). Defaults to full.', + }, + }, + + request: { + url: (params: GmailGetThreadParams) => { + const format = params.format || 'full' + return `${GMAIL_API_BASE}/threads/${params.threadId}?format=${format}` + }, + method: 'GET', + headers: (params: GmailGetThreadParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', messages: [] }, + error: data.error?.message || 'Failed to get thread', + } + } + + const messages = (data.messages || []).map((message: Record) => { + const payload = message.payload as Record | undefined + const headers = (payload?.headers as Array>) || [] + const getHeader = (name: string): string | undefined => + headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value + + let body = '' + const payloadBody = payload?.body as Record | undefined + if (payloadBody?.data) { + body = Buffer.from(payloadBody.data as string, 'base64').toString() + } else if (payload?.parts) { + const parts = payload.parts as Array> + const textPart = parts.find((part) => part.mimeType === 'text/plain') + const textBody = textPart?.body as Record | undefined + if (textBody?.data) { + body = Buffer.from(textBody.data as string, 'base64').toString() + } + } + + return { + id: message.id, + threadId: message.threadId, + labelIds: message.labelIds ?? null, + snippet: message.snippet ?? null, + from: getHeader('From') ?? null, + to: getHeader('To') ?? null, + subject: getHeader('Subject') ?? null, + date: getHeader('Date') ?? null, + body: body || null, + } + }) + + return { + success: true, + output: { + id: data.id, + historyId: data.historyId ?? null, + messages, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Thread ID' }, + historyId: { type: 'string', description: 'History ID', optional: true }, + messages: { + type: 'json', + description: + 'Array of messages in the thread with id, from, to, subject, date, body, and labels', + }, + }, +} diff --git a/apps/sim/tools/gmail/index.ts b/apps/sim/tools/gmail/index.ts index 3dfa89e4eb..6869a2e5f6 100644 --- a/apps/sim/tools/gmail/index.ts +++ b/apps/sim/tools/gmail/index.ts @@ -1,7 +1,15 @@ import { gmailAddLabelTool, gmailAddLabelV2Tool } from '@/tools/gmail/add_label' import { gmailArchiveTool, gmailArchiveV2Tool } from '@/tools/gmail/archive' +import { gmailCreateLabelV2Tool } from '@/tools/gmail/create_label' import { gmailDeleteTool, gmailDeleteV2Tool } from '@/tools/gmail/delete' +import { gmailDeleteDraftV2Tool } from '@/tools/gmail/delete_draft' +import { gmailDeleteLabelV2Tool } from '@/tools/gmail/delete_label' import { gmailDraftTool, gmailDraftV2Tool } from '@/tools/gmail/draft' +import { gmailGetDraftV2Tool } from '@/tools/gmail/get_draft' +import { gmailGetThreadV2Tool } from '@/tools/gmail/get_thread' +import { gmailListDraftsV2Tool } from '@/tools/gmail/list_drafts' +import { gmailListLabelsV2Tool } from '@/tools/gmail/list_labels' +import { gmailListThreadsV2Tool } from '@/tools/gmail/list_threads' import { gmailMarkReadTool, gmailMarkReadV2Tool } from '@/tools/gmail/mark_read' import { gmailMarkUnreadTool, gmailMarkUnreadV2Tool } from '@/tools/gmail/mark_unread' import { gmailMoveTool, gmailMoveV2Tool } from '@/tools/gmail/move' @@ -9,7 +17,9 @@ import { gmailReadTool, gmailReadV2Tool } from '@/tools/gmail/read' import { gmailRemoveLabelTool, gmailRemoveLabelV2Tool } from '@/tools/gmail/remove_label' import { gmailSearchTool, gmailSearchV2Tool } from '@/tools/gmail/search' import { gmailSendTool, gmailSendV2Tool } from '@/tools/gmail/send' +import { gmailTrashThreadV2Tool } from '@/tools/gmail/trash_thread' import { gmailUnarchiveTool, gmailUnarchiveV2Tool } from '@/tools/gmail/unarchive' +import { gmailUntrashThreadV2Tool } from '@/tools/gmail/untrash_thread' export { gmailSendTool, @@ -36,4 +46,14 @@ export { gmailAddLabelV2Tool, gmailRemoveLabelTool, gmailRemoveLabelV2Tool, + gmailListDraftsV2Tool, + gmailGetDraftV2Tool, + gmailDeleteDraftV2Tool, + gmailCreateLabelV2Tool, + gmailDeleteLabelV2Tool, + gmailListLabelsV2Tool, + gmailGetThreadV2Tool, + gmailListThreadsV2Tool, + gmailTrashThreadV2Tool, + gmailUntrashThreadV2Tool, } diff --git a/apps/sim/tools/gmail/list_drafts.ts b/apps/sim/tools/gmail/list_drafts.ts new file mode 100644 index 0000000000..5ba31e5a23 --- /dev/null +++ b/apps/sim/tools/gmail/list_drafts.ts @@ -0,0 +1,126 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailListDraftsParams { + accessToken: string + maxResults?: number + pageToken?: string + query?: string +} + +interface GmailListDraftsResponse { + success: boolean + output: { + drafts: Array<{ + id: string + messageId: string + threadId: string + }> + resultSizeEstimate: number + nextPageToken?: string + } +} + +export const gmailListDraftsV2Tool: ToolConfig = { + id: 'gmail_list_drafts_v2', + name: 'Gmail List Drafts', + description: 'List all drafts in a Gmail account', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of drafts to return (default: 100, max: 500)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token for paginated results', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter drafts (same syntax as Gmail search)', + }, + }, + + request: { + url: (params: GmailListDraftsParams) => { + const searchParams = new URLSearchParams() + if (params.maxResults) { + searchParams.append('maxResults', Number(params.maxResults).toString()) + } + if (params.pageToken) { + searchParams.append('pageToken', params.pageToken) + } + if (params.query) { + searchParams.append('q', params.query) + } + const qs = searchParams.toString() + return `${GMAIL_API_BASE}/drafts${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params: GmailListDraftsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { drafts: [], resultSizeEstimate: 0 }, + error: data.error?.message || 'Failed to list drafts', + } + } + + const drafts = (data.drafts || []).map((draft: Record) => ({ + id: draft.id, + messageId: (draft.message as Record)?.id ?? null, + threadId: (draft.message as Record)?.threadId ?? null, + })) + + return { + success: true, + output: { + drafts, + resultSizeEstimate: data.resultSizeEstimate ?? 0, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + drafts: { + type: 'json', + description: 'Array of draft objects with id, messageId, and threadId', + }, + resultSizeEstimate: { + type: 'number', + description: 'Estimated total number of drafts', + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/gmail/list_labels.ts b/apps/sim/tools/gmail/list_labels.ts new file mode 100644 index 0000000000..159af96f34 --- /dev/null +++ b/apps/sim/tools/gmail/list_labels.ts @@ -0,0 +1,81 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailListLabelsParams { + accessToken: string +} + +interface GmailListLabelsResponse { + success: boolean + output: { + labels: Array<{ + id: string + name: string + type: string + messageListVisibility?: string + labelListVisibility?: string + }> + } +} + +export const gmailListLabelsV2Tool: ToolConfig = { + id: 'gmail_list_labels_v2', + name: 'Gmail List Labels', + description: 'List all labels in a Gmail account', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + }, + + request: { + url: () => `${GMAIL_API_BASE}/labels`, + method: 'GET', + headers: (params: GmailListLabelsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { labels: [] }, + error: data.error?.message || 'Failed to list labels', + } + } + + const labels = (data.labels || []).map((label: Record) => ({ + id: label.id, + name: label.name, + type: label.type ?? null, + messageListVisibility: label.messageListVisibility ?? null, + labelListVisibility: label.labelListVisibility ?? null, + })) + + return { + success: true, + output: { labels }, + } + }, + + outputs: { + labels: { + type: 'json', + description: 'Array of label objects with id, name, type, and visibility settings', + }, + }, +} diff --git a/apps/sim/tools/gmail/list_threads.ts b/apps/sim/tools/gmail/list_threads.ts new file mode 100644 index 0000000000..a2617cf9c5 --- /dev/null +++ b/apps/sim/tools/gmail/list_threads.ts @@ -0,0 +1,140 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailListThreadsParams { + accessToken: string + maxResults?: number + pageToken?: string + query?: string + labelIds?: string +} + +interface GmailListThreadsResponse { + success: boolean + output: { + threads: Array<{ + id: string + snippet: string + historyId: string + }> + resultSizeEstimate: number + nextPageToken?: string + } +} + +export const gmailListThreadsV2Tool: ToolConfig = + { + id: 'gmail_list_threads_v2', + name: 'Gmail List Threads', + description: 'List email threads in a Gmail account', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of threads to return (default: 100, max: 500)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token for paginated results', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter threads (same syntax as Gmail search)', + }, + labelIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated label IDs to filter threads by', + }, + }, + + request: { + url: (params: GmailListThreadsParams) => { + const searchParams = new URLSearchParams() + if (params.maxResults) { + searchParams.append('maxResults', Number(params.maxResults).toString()) + } + if (params.pageToken) { + searchParams.append('pageToken', params.pageToken) + } + if (params.query) { + searchParams.append('q', params.query) + } + if (params.labelIds) { + const labels = params.labelIds.split(',').map((l) => l.trim()) + for (const label of labels) { + searchParams.append('labelIds', label) + } + } + const qs = searchParams.toString() + return `${GMAIL_API_BASE}/threads${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params: GmailListThreadsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { threads: [], resultSizeEstimate: 0 }, + error: data.error?.message || 'Failed to list threads', + } + } + + const threads = (data.threads || []).map((thread: Record) => ({ + id: thread.id, + snippet: thread.snippet ?? '', + historyId: thread.historyId ?? '', + })) + + return { + success: true, + output: { + threads, + resultSizeEstimate: data.resultSizeEstimate ?? 0, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + threads: { + type: 'json', + description: 'Array of thread objects with id, snippet, and historyId', + }, + resultSizeEstimate: { + type: 'number', + description: 'Estimated total number of threads', + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/gmail/trash_thread.ts b/apps/sim/tools/gmail/trash_thread.ts new file mode 100644 index 0000000000..4d13c32090 --- /dev/null +++ b/apps/sim/tools/gmail/trash_thread.ts @@ -0,0 +1,78 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailTrashThreadParams { + accessToken: string + threadId: string +} + +interface GmailTrashThreadResponse { + success: boolean + output: { + id: string + trashed: boolean + } +} + +export const gmailTrashThreadV2Tool: ToolConfig = + { + id: 'gmail_trash_thread_v2', + name: 'Gmail Trash Thread', + description: 'Move an email thread to trash in Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the thread to trash', + }, + }, + + request: { + url: (params: GmailTrashThreadParams) => `${GMAIL_API_BASE}/threads/${params.threadId}/trash`, + method: 'POST', + headers: (params: GmailTrashThreadParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', trashed: false }, + error: data.error?.message || 'Failed to trash thread', + } + } + + return { + success: true, + output: { + id: data.id, + trashed: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Thread ID' }, + trashed: { type: 'boolean', description: 'Whether the thread was successfully trashed' }, + }, + } diff --git a/apps/sim/tools/gmail/untrash_thread.ts b/apps/sim/tools/gmail/untrash_thread.ts new file mode 100644 index 0000000000..ea88aac7c4 --- /dev/null +++ b/apps/sim/tools/gmail/untrash_thread.ts @@ -0,0 +1,84 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailUntrashThreadParams { + accessToken: string + threadId: string +} + +interface GmailUntrashThreadResponse { + success: boolean + output: { + id: string + untrashed: boolean + } +} + +export const gmailUntrashThreadV2Tool: ToolConfig< + GmailUntrashThreadParams, + GmailUntrashThreadResponse +> = { + id: 'gmail_untrash_thread_v2', + name: 'Gmail Untrash Thread', + description: 'Remove an email thread from trash in Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the thread to untrash', + }, + }, + + request: { + url: (params: GmailUntrashThreadParams) => + `${GMAIL_API_BASE}/threads/${params.threadId}/untrash`, + method: 'POST', + headers: (params: GmailUntrashThreadParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', untrashed: false }, + error: data.error?.message || 'Failed to untrash thread', + } + } + + return { + success: true, + output: { + id: data.id, + untrashed: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Thread ID' }, + untrashed: { + type: 'boolean', + description: 'Whether the thread was successfully removed from trash', + }, + }, +} diff --git a/apps/sim/tools/google_calendar/freebusy.ts b/apps/sim/tools/google_calendar/freebusy.ts new file mode 100644 index 0000000000..00c68ec180 --- /dev/null +++ b/apps/sim/tools/google_calendar/freebusy.ts @@ -0,0 +1,155 @@ +import { + CALENDAR_API_BASE, + type GoogleCalendarApiFreeBusyResponse, + type GoogleCalendarFreeBusyParams, + type GoogleCalendarFreeBusyResponse, +} from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +export const freebusyTool: ToolConfig< + GoogleCalendarFreeBusyParams, + GoogleCalendarFreeBusyResponse +> = { + id: 'google_calendar_freebusy', + name: 'Google Calendar Free/Busy', + description: 'Query free/busy information for one or more Google Calendars', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated calendar IDs to query (e.g., "primary,other@example.com")', + }, + timeMin: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start of the time range (RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z)', + }, + timeMax: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End of the time range (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)', + }, + timeZone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'IANA time zone (e.g., "UTC", "America/New_York"). Defaults to UTC.', + }, + }, + + request: { + url: () => `${CALENDAR_API_BASE}/freeBusy`, + method: 'POST', + headers: (params: GoogleCalendarFreeBusyParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleCalendarFreeBusyParams) => { + const ids = params.calendarIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + + return { + timeMin: params.timeMin, + timeMax: params.timeMax, + timeZone: params.timeZone || 'UTC', + items: ids.map((id) => ({ id })), + } + }, + }, + + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiFreeBusyResponse = await response.json() + + const calendarIds = Object.keys(data.calendars || {}) + const totalBusy = calendarIds.reduce((sum, id) => { + return sum + (data.calendars[id]?.busy?.length || 0) + }, 0) + + return { + success: true, + output: { + content: `Found ${totalBusy} busy period${totalBusy !== 1 ? 's' : ''} across ${calendarIds.length} calendar${calendarIds.length !== 1 ? 's' : ''}`, + metadata: { + timeMin: data.timeMin, + timeMax: data.timeMax, + calendars: data.calendars, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of free/busy results' }, + metadata: { + type: 'json', + description: 'Free/busy data with time range and per-calendar busy periods', + }, + }, +} + +interface GoogleCalendarFreeBusyV2Response { + success: boolean + output: { + timeMin: string + timeMax: string + calendars: Record< + string, + { + busy: Array<{ start: string; end: string }> + errors?: Array<{ domain: string; reason: string }> + } + > + } +} + +export const freebusyV2Tool: ToolConfig< + GoogleCalendarFreeBusyParams, + GoogleCalendarFreeBusyV2Response +> = { + id: 'google_calendar_freebusy_v2', + name: 'Google Calendar Free/Busy', + description: + 'Query free/busy information for one or more Google Calendars. Returns API-aligned fields only.', + version: '2.0.0', + oauth: freebusyTool.oauth, + params: freebusyTool.params, + request: freebusyTool.request, + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiFreeBusyResponse = await response.json() + + return { + success: true, + output: { + timeMin: data.timeMin, + timeMax: data.timeMax, + calendars: data.calendars, + }, + } + }, + outputs: { + timeMin: { type: 'string', description: 'Start of the queried time range' }, + timeMax: { type: 'string', description: 'End of the queried time range' }, + calendars: { + type: 'json', + description: 'Per-calendar free/busy data with busy periods and any errors', + }, + }, +} diff --git a/apps/sim/tools/google_calendar/index.ts b/apps/sim/tools/google_calendar/index.ts index 2ddfcf55e5..4d55e57ad9 100644 --- a/apps/sim/tools/google_calendar/index.ts +++ b/apps/sim/tools/google_calendar/index.ts @@ -1,5 +1,6 @@ import { createTool, createV2Tool } from '@/tools/google_calendar/create' import { deleteTool, deleteV2Tool } from '@/tools/google_calendar/delete' +import { freebusyTool, freebusyV2Tool } from '@/tools/google_calendar/freebusy' import { getTool, getV2Tool } from '@/tools/google_calendar/get' import { instancesTool, instancesV2Tool } from '@/tools/google_calendar/instances' import { inviteTool, inviteV2Tool } from '@/tools/google_calendar/invite' @@ -11,6 +12,7 @@ import { updateTool, updateV2Tool } from '@/tools/google_calendar/update' export const googleCalendarCreateTool = createTool export const googleCalendarDeleteTool = deleteTool +export const googleCalendarFreeBusyTool = freebusyTool export const googleCalendarGetTool = getTool export const googleCalendarInstancesTool = instancesTool export const googleCalendarInviteTool = inviteTool @@ -22,6 +24,7 @@ export const googleCalendarUpdateTool = updateTool export const googleCalendarCreateV2Tool = createV2Tool export const googleCalendarDeleteV2Tool = deleteV2Tool +export const googleCalendarFreeBusyV2Tool = freebusyV2Tool export const googleCalendarGetV2Tool = getV2Tool export const googleCalendarInstancesV2Tool = instancesV2Tool export const googleCalendarInviteV2Tool = inviteV2Tool diff --git a/apps/sim/tools/google_calendar/types.ts b/apps/sim/tools/google_calendar/types.ts index 46b39f8a04..fc3cc83262 100644 --- a/apps/sim/tools/google_calendar/types.ts +++ b/apps/sim/tools/google_calendar/types.ts @@ -90,6 +90,14 @@ export interface GoogleCalendarInstancesParams extends BaseGoogleCalendarParams showDeleted?: boolean } +export interface GoogleCalendarFreeBusyParams { + accessToken: string + calendarIds: string // Comma-separated calendar IDs (e.g., "primary,other@example.com") + timeMin: string // RFC3339 timestamp (e.g., 2025-06-03T00:00:00Z) + timeMax: string // RFC3339 timestamp (e.g., 2025-06-04T00:00:00Z) + timeZone?: string // IANA time zone (e.g., "UTC", "America/New_York") +} + export interface GoogleCalendarListCalendarsParams { accessToken: string minAccessRole?: 'freeBusyReader' | 'reader' | 'writer' | 'owner' @@ -109,6 +117,7 @@ export type GoogleCalendarToolParams = | GoogleCalendarInviteParams | GoogleCalendarMoveParams | GoogleCalendarInstancesParams + | GoogleCalendarFreeBusyParams | GoogleCalendarListCalendarsParams interface EventMetadata { @@ -341,6 +350,36 @@ export interface GoogleCalendarInstancesResponse extends ToolResponse { } } +export interface GoogleCalendarFreeBusyResponse extends ToolResponse { + output: { + content: string + metadata: { + timeMin: string + timeMax: string + calendars: Record< + string, + { + busy: Array<{ start: string; end: string }> + errors?: Array<{ domain: string; reason: string }> + } + > + } + } +} + +export interface GoogleCalendarApiFreeBusyResponse { + kind: string + timeMin: string + timeMax: string + calendars: Record< + string, + { + busy: Array<{ start: string; end: string }> + errors?: Array<{ domain: string; reason: string }> + } + > +} + export interface GoogleCalendarListCalendarsResponse extends ToolResponse { output: { content: string @@ -373,4 +412,5 @@ export type GoogleCalendarResponse = | GoogleCalendarDeleteResponse | GoogleCalendarMoveResponse | GoogleCalendarInstancesResponse + | GoogleCalendarFreeBusyResponse | GoogleCalendarListCalendarsResponse diff --git a/apps/sim/tools/google_drive/index.ts b/apps/sim/tools/google_drive/index.ts index 58dbdb03e7..7c81c6c2fc 100644 --- a/apps/sim/tools/google_drive/index.ts +++ b/apps/sim/tools/google_drive/index.ts @@ -7,6 +7,8 @@ import { getContentTool } from '@/tools/google_drive/get_content' import { getFileTool } from '@/tools/google_drive/get_file' import { listTool } from '@/tools/google_drive/list' import { listPermissionsTool } from '@/tools/google_drive/list_permissions' +import { moveTool } from '@/tools/google_drive/move' +import { searchTool } from '@/tools/google_drive/search' import { shareTool } from '@/tools/google_drive/share' import { trashTool } from '@/tools/google_drive/trash' import { unshareTool } from '@/tools/google_drive/unshare' @@ -23,6 +25,8 @@ export const googleDriveGetContentTool = getContentTool export const googleDriveGetFileTool = getFileTool export const googleDriveListTool = listTool export const googleDriveListPermissionsTool = listPermissionsTool +export const googleDriveMoveTool = moveTool +export const googleDriveSearchTool = searchTool export const googleDriveShareTool = shareTool export const googleDriveTrashTool = trashTool export const googleDriveUnshareTool = unshareTool diff --git a/apps/sim/tools/google_drive/move.ts b/apps/sim/tools/google_drive/move.ts new file mode 100644 index 0000000000..4358b7d29b --- /dev/null +++ b/apps/sim/tools/google_drive/move.ts @@ -0,0 +1,145 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveMoveParams extends GoogleDriveToolParams { + fileId: string + destinationFolderId: string + removeFromCurrent?: boolean +} + +interface GoogleDriveMoveResponse extends ToolResponse { + output: { + file: GoogleDriveFile + } +} + +export const moveTool: ToolConfig = { + id: 'google_drive_move', + name: 'Move Google Drive File', + description: 'Move a file or folder to a different folder in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to move', + }, + destinationFolderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the destination folder', + }, + removeFromCurrent: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Whether to remove the file from its current parent folder (default: true). Set to false to add the file to the destination without removing it from the current location.', + }, + }, + + request: { + url: 'https://www.googleapis.com/drive/v3/files', + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + directExecution: async (params) => { + const fileId = params.fileId?.trim() + const destinationFolderId = params.destinationFolderId?.trim() + const removeFromCurrent = params.removeFromCurrent !== false + + if (!fileId) { + throw new Error('fileId is required') + } + if (!destinationFolderId) { + throw new Error('destinationFolderId is required') + } + + const headers = { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + + // Build the PATCH URL with addParents + const url = new URL(`https://www.googleapis.com/drive/v3/files/${fileId}`) + url.searchParams.append('addParents', destinationFolderId) + url.searchParams.append('fields', ALL_FILE_FIELDS) + url.searchParams.append('supportsAllDrives', 'true') + + if (removeFromCurrent) { + // Fetch current parents so we can remove them + const metadataUrl = new URL(`https://www.googleapis.com/drive/v3/files/${fileId}`) + metadataUrl.searchParams.append('fields', 'parents') + metadataUrl.searchParams.append('supportsAllDrives', 'true') + + const metadataResponse = await fetch(metadataUrl.toString(), { headers }) + + if (!metadataResponse.ok) { + const errorData = await metadataResponse.json() + throw new Error(errorData.error?.message || 'Failed to retrieve file metadata') + } + + const metadata = await metadataResponse.json() + if (metadata.parents && metadata.parents.length > 0) { + url.searchParams.append('removeParents', metadata.parents.join(',')) + } + } + + const response = await fetch(url.toString(), { + method: 'PATCH', + headers, + body: JSON.stringify({}), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to move Google Drive file') + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'json', + description: 'The moved file metadata', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + kind: { type: 'string', description: 'Resource type identifier' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + parents: { type: 'json', description: 'Parent folder IDs' }, + createdTime: { type: 'string', description: 'File creation time' }, + modifiedTime: { type: 'string', description: 'Last modification time' }, + owners: { type: 'json', description: 'List of file owners' }, + size: { type: 'string', description: 'File size in bytes' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/search.ts b/apps/sim/tools/google_drive/search.ts new file mode 100644 index 0000000000..52eccb7ef6 --- /dev/null +++ b/apps/sim/tools/google_drive/search.ts @@ -0,0 +1,145 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveSearchParams extends GoogleDriveToolParams { + query: string + pageSize?: number + pageToken?: string +} + +interface GoogleDriveSearchResponse extends ToolResponse { + output: { + files: GoogleDriveFile[] + nextPageToken?: string + } +} + +export const searchTool: ToolConfig = { + id: 'google_drive_search', + name: 'Search Google Drive Files', + description: + 'Search for files in Google Drive using advanced query syntax (e.g., fullText contains, mimeType, modifiedTime, etc.)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Google Drive query string using advanced search syntax (e.g., "fullText contains \'budget\'", "mimeType = \'application/pdf\'", "modifiedTime > \'2024-01-01\'")', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of files to return (default: 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Token for fetching the next page of results', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://www.googleapis.com/drive/v3/files') + url.searchParams.append('fields', `files(${ALL_FILE_FIELDS}),nextPageToken`) + url.searchParams.append('corpora', 'allDrives') + url.searchParams.append('supportsAllDrives', 'true') + url.searchParams.append('includeItemsFromAllDrives', 'true') + + // The query is passed directly as Google Drive query syntax + const conditions = ['trashed = false'] + if (params.query?.trim()) { + conditions.push(params.query.trim()) + } + url.searchParams.append('q', conditions.join(' and ')) + + if (params.pageSize) { + url.searchParams.append('pageSize', Number(params.pageSize).toString()) + } + if (params.pageToken) { + url.searchParams.append('pageToken', params.pageToken) + } + + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to search Google Drive files') + } + + return { + success: true, + output: { + files: data.files || [], + nextPageToken: data.nextPageToken, + }, + } + }, + + outputs: { + files: { + type: 'array', + description: 'Array of file metadata objects matching the search query', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + kind: { type: 'string', description: 'Resource type identifier' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + description: { type: 'string', description: 'File description' }, + originalFilename: { type: 'string', description: 'Original uploaded filename' }, + fullFileExtension: { type: 'string', description: 'Full file extension' }, + fileExtension: { type: 'string', description: 'File extension' }, + owners: { type: 'json', description: 'List of file owners' }, + permissions: { type: 'json', description: 'File permissions' }, + shared: { type: 'boolean', description: 'Whether file is shared' }, + ownedByMe: { type: 'boolean', description: 'Whether owned by current user' }, + starred: { type: 'boolean', description: 'Whether file is starred' }, + trashed: { type: 'boolean', description: 'Whether file is in trash' }, + createdTime: { type: 'string', description: 'File creation time' }, + modifiedTime: { type: 'string', description: 'Last modification time' }, + lastModifyingUser: { type: 'json', description: 'User who last modified the file' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + webContentLink: { type: 'string', description: 'Direct download URL' }, + iconLink: { type: 'string', description: 'URL to file icon' }, + thumbnailLink: { type: 'string', description: 'URL to thumbnail' }, + size: { type: 'string', description: 'File size in bytes' }, + parents: { type: 'json', description: 'Parent folder IDs' }, + driveId: { type: 'string', description: 'Shared drive ID' }, + capabilities: { type: 'json', description: 'User capabilities on file' }, + version: { type: 'string', description: 'Version number' }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + }, + }, +} diff --git a/apps/sim/tools/google_sheets/delete_rows.ts b/apps/sim/tools/google_sheets/delete_rows.ts new file mode 100644 index 0000000000..9e146d3ad3 --- /dev/null +++ b/apps/sim/tools/google_sheets/delete_rows.ts @@ -0,0 +1,156 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface GoogleSheetsV2DeleteRowsParams { + accessToken: string + spreadsheetId: string + sheetId: number + startIndex: number + endIndex: number +} + +export interface GoogleSheetsV2DeleteRowsResponse extends ToolResponse { + output: { + spreadsheetId: string + sheetId: number + deletedRowRange: string + metadata: { + spreadsheetId: string + spreadsheetUrl: string + } + } +} + +export const deleteRowsV2Tool: ToolConfig< + GoogleSheetsV2DeleteRowsParams, + GoogleSheetsV2DeleteRowsResponse +> = { + id: 'google_sheets_delete_rows_v2', + name: 'Delete Rows from Google Sheets V2', + description: 'Delete rows from a sheet in a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Sheets spreadsheet ID', + }, + sheetId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The numeric ID of the sheet/tab (not the sheet name). Use Get Spreadsheet to find sheet IDs.', + }, + startIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The start row index (0-based, inclusive) of the rows to delete', + }, + endIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The end row index (0-based, exclusive) of the rows to delete', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + if (params.sheetId === undefined || params.sheetId === null) { + throw new Error('Sheet ID is required') + } + if (params.startIndex === undefined || params.startIndex === null) { + throw new Error('Start index is required') + } + if (params.endIndex === undefined || params.endIndex === null) { + throw new Error('End index is required') + } + + return { + requests: [ + { + deleteDimension: { + range: { + sheetId: params.sheetId, + dimension: 'ROWS', + startIndex: params.startIndex, + endIndex: params.endIndex, + }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteRowsParams) => { + await response.json() + + const spreadsheetId = params?.spreadsheetId ?? '' + const startIndex = params?.startIndex ?? 0 + const endIndex = params?.endIndex ?? 0 + + return { + success: true, + output: { + spreadsheetId, + sheetId: params?.sheetId ?? 0, + deletedRowRange: `rows ${startIndex} to ${endIndex}`, + metadata: { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + }, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + sheetId: { type: 'number', description: 'The numeric ID of the sheet' }, + deletedRowRange: { + type: 'string', + description: 'Description of the deleted row range', + }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/delete_sheet.ts b/apps/sim/tools/google_sheets/delete_sheet.ts new file mode 100644 index 0000000000..b7a7246680 --- /dev/null +++ b/apps/sim/tools/google_sheets/delete_sheet.ts @@ -0,0 +1,123 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface GoogleSheetsV2DeleteSheetParams { + accessToken: string + spreadsheetId: string + sheetId: number +} + +export interface GoogleSheetsV2DeleteSheetResponse extends ToolResponse { + output: { + spreadsheetId: string + deletedSheetId: number + metadata: { + spreadsheetId: string + spreadsheetUrl: string + } + } +} + +export const deleteSheetV2Tool: ToolConfig< + GoogleSheetsV2DeleteSheetParams, + GoogleSheetsV2DeleteSheetResponse +> = { + id: 'google_sheets_delete_sheet_v2', + name: 'Delete Sheet V2', + description: 'Delete a sheet/tab from a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Sheets spreadsheet ID', + }, + sheetId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The numeric ID of the sheet/tab to delete (not the sheet name). Use Get Spreadsheet to find sheet IDs.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + if (params.sheetId === undefined || params.sheetId === null) { + throw new Error('Sheet ID is required') + } + + return { + requests: [ + { + deleteSheet: { + sheetId: params.sheetId, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteSheetParams) => { + await response.json() + + const spreadsheetId = params?.spreadsheetId ?? '' + + return { + success: true, + output: { + spreadsheetId, + deletedSheetId: params?.sheetId ?? 0, + metadata: { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + }, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + deletedSheetId: { type: 'number', description: 'The numeric ID of the deleted sheet' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/delete_spreadsheet.ts b/apps/sim/tools/google_sheets/delete_spreadsheet.ts new file mode 100644 index 0000000000..70b7138c90 --- /dev/null +++ b/apps/sim/tools/google_sheets/delete_spreadsheet.ts @@ -0,0 +1,86 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface GoogleSheetsV2DeleteSpreadsheetParams { + accessToken: string + spreadsheetId: string +} + +export interface GoogleSheetsV2DeleteSpreadsheetResponse extends ToolResponse { + output: { + spreadsheetId: string + deleted: boolean + } +} + +export const deleteSpreadsheetV2Tool: ToolConfig< + GoogleSheetsV2DeleteSpreadsheetParams, + GoogleSheetsV2DeleteSpreadsheetResponse +> = { + id: 'google_sheets_delete_spreadsheet_v2', + name: 'Delete Spreadsheet V2', + description: 'Permanently delete a Google Sheets spreadsheet using the Google Drive API', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the Google Sheets spreadsheet to delete', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://www.googleapis.com/drive/v3/files/${spreadsheetId}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteSpreadsheetParams) => { + const spreadsheetId = params?.spreadsheetId ?? '' + + if (response.status === 204 || response.ok) { + return { + success: true, + output: { + spreadsheetId, + deleted: true, + }, + } + } + + const data = await response.json() + throw new Error(data.error?.message ?? 'Failed to delete spreadsheet') + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'The ID of the deleted spreadsheet' }, + deleted: { type: 'boolean', description: 'Whether the spreadsheet was successfully deleted' }, + }, +} diff --git a/apps/sim/tools/google_sheets/index.ts b/apps/sim/tools/google_sheets/index.ts index 750e0a3b26..47422712ff 100644 --- a/apps/sim/tools/google_sheets/index.ts +++ b/apps/sim/tools/google_sheets/index.ts @@ -5,6 +5,9 @@ import { batchUpdateV2Tool } from '@/tools/google_sheets/batch_update' import { clearV2Tool } from '@/tools/google_sheets/clear' import { copySheetV2Tool } from '@/tools/google_sheets/copy_sheet' import { createSpreadsheetV2Tool } from '@/tools/google_sheets/create_spreadsheet' +import { deleteRowsV2Tool } from '@/tools/google_sheets/delete_rows' +import { deleteSheetV2Tool } from '@/tools/google_sheets/delete_sheet' +import { deleteSpreadsheetV2Tool } from '@/tools/google_sheets/delete_spreadsheet' import { getSpreadsheetV2Tool } from '@/tools/google_sheets/get_spreadsheet' import { readTool, readV2Tool } from '@/tools/google_sheets/read' import { updateTool, updateV2Tool } from '@/tools/google_sheets/update' @@ -28,3 +31,6 @@ export const googleSheetsBatchGetV2Tool = batchGetV2Tool export const googleSheetsBatchUpdateV2Tool = batchUpdateV2Tool export const googleSheetsBatchClearV2Tool = batchClearV2Tool export const googleSheetsCopySheetV2Tool = copySheetV2Tool +export const googleSheetsDeleteRowsV2Tool = deleteRowsV2Tool +export const googleSheetsDeleteSheetV2Tool = deleteSheetV2Tool +export const googleSheetsDeleteSpreadsheetV2Tool = deleteSpreadsheetV2Tool diff --git a/apps/sim/tools/google_sheets/types.ts b/apps/sim/tools/google_sheets/types.ts index 2c0c7aa5a4..66a080d9df 100644 --- a/apps/sim/tools/google_sheets/types.ts +++ b/apps/sim/tools/google_sheets/types.ts @@ -1,3 +1,6 @@ +import type { GoogleSheetsV2DeleteRowsResponse } from '@/tools/google_sheets/delete_rows' +import type { GoogleSheetsV2DeleteSheetResponse } from '@/tools/google_sheets/delete_sheet' +import type { GoogleSheetsV2DeleteSpreadsheetResponse } from '@/tools/google_sheets/delete_spreadsheet' import type { ToolResponse } from '@/tools/types' export interface GoogleSheetsRange { @@ -146,6 +149,9 @@ export type GoogleSheetsV2Response = | GoogleSheetsV2BatchUpdateResponse | GoogleSheetsV2BatchClearResponse | GoogleSheetsV2CopySheetResponse + | GoogleSheetsV2DeleteRowsResponse + | GoogleSheetsV2DeleteSheetResponse + | GoogleSheetsV2DeleteSpreadsheetResponse // V2 Clear Types export interface GoogleSheetsV2ClearParams { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5a2f5787c7..8be956c7d6 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -578,10 +578,18 @@ import { gmailAddLabelV2Tool, gmailArchiveTool, gmailArchiveV2Tool, + gmailCreateLabelV2Tool, + gmailDeleteDraftV2Tool, + gmailDeleteLabelV2Tool, gmailDeleteTool, gmailDeleteV2Tool, gmailDraftTool, gmailDraftV2Tool, + gmailGetDraftV2Tool, + gmailGetThreadV2Tool, + gmailListDraftsV2Tool, + gmailListLabelsV2Tool, + gmailListThreadsV2Tool, gmailMarkReadTool, gmailMarkReadV2Tool, gmailMarkUnreadTool, @@ -596,8 +604,10 @@ import { gmailSearchV2Tool, gmailSendTool, gmailSendV2Tool, + gmailTrashThreadV2Tool, gmailUnarchiveTool, gmailUnarchiveV2Tool, + gmailUntrashThreadV2Tool, } from '@/tools/gmail' import { gongAggregateActivityTool, @@ -626,6 +636,8 @@ import { googleCalendarCreateV2Tool, googleCalendarDeleteTool, googleCalendarDeleteV2Tool, + googleCalendarFreeBusyTool, + googleCalendarFreeBusyV2Tool, googleCalendarGetTool, googleCalendarGetV2Tool, googleCalendarInstancesTool, @@ -654,6 +666,8 @@ import { googleDriveGetFileTool, googleDriveListPermissionsTool, googleDriveListTool, + googleDriveMoveTool, + googleDriveSearchTool, googleDriveShareTool, googleDriveTrashTool, googleDriveUnshareTool, @@ -714,6 +728,9 @@ import { googleSheetsClearV2Tool, googleSheetsCopySheetV2Tool, googleSheetsCreateSpreadsheetV2Tool, + googleSheetsDeleteRowsV2Tool, + googleSheetsDeleteSheetV2Tool, + googleSheetsDeleteSpreadsheetV2Tool, googleSheetsGetSpreadsheetV2Tool, googleSheetsReadTool, googleSheetsReadV2Tool, @@ -2471,6 +2488,16 @@ export const tools: Record = { gmail_add_label_v2: gmailAddLabelV2Tool, gmail_remove_label: gmailRemoveLabelTool, gmail_remove_label_v2: gmailRemoveLabelV2Tool, + gmail_create_label_v2: gmailCreateLabelV2Tool, + gmail_delete_draft_v2: gmailDeleteDraftV2Tool, + gmail_delete_label_v2: gmailDeleteLabelV2Tool, + gmail_get_draft_v2: gmailGetDraftV2Tool, + gmail_get_thread_v2: gmailGetThreadV2Tool, + gmail_list_drafts_v2: gmailListDraftsV2Tool, + gmail_list_labels_v2: gmailListLabelsV2Tool, + gmail_list_threads_v2: gmailListThreadsV2Tool, + gmail_trash_thread_v2: gmailTrashThreadV2Tool, + gmail_untrash_thread_v2: gmailUntrashThreadV2Tool, whatsapp_send_message: whatsappSendMessageTool, x_write: xWriteTool, x_read: xReadTool, @@ -2867,6 +2894,8 @@ export const tools: Record = { google_drive_get_file: googleDriveGetFileTool, google_drive_list: googleDriveListTool, google_drive_list_permissions: googleDriveListPermissionsTool, + google_drive_move: googleDriveMoveTool, + google_drive_search: googleDriveSearchTool, google_drive_share: googleDriveShareTool, google_drive_trash: googleDriveTrashTool, google_drive_unshare: googleDriveUnshareTool, @@ -2906,6 +2935,9 @@ export const tools: Record = { google_sheets_batch_update_v2: googleSheetsBatchUpdateV2Tool, google_sheets_batch_clear_v2: googleSheetsBatchClearV2Tool, google_sheets_copy_sheet_v2: googleSheetsCopySheetV2Tool, + google_sheets_delete_rows_v2: googleSheetsDeleteRowsV2Tool, + google_sheets_delete_sheet_v2: googleSheetsDeleteSheetV2Tool, + google_sheets_delete_spreadsheet_v2: googleSheetsDeleteSpreadsheetV2Tool, google_slides_read: googleSlidesReadTool, google_slides_write: googleSlidesWriteTool, google_slides_create: googleSlidesCreateTool, @@ -3505,6 +3537,8 @@ export const tools: Record = { google_calendar_quick_add_v2: googleCalendarQuickAddV2Tool, google_calendar_update: googleCalendarUpdateTool, google_calendar_update_v2: googleCalendarUpdateV2Tool, + google_calendar_freebusy: googleCalendarFreeBusyTool, + google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool, google_forms_get_responses: googleFormsGetResponsesTool, google_forms_get_form: googleFormsGetFormTool, google_forms_create_form: googleFormsCreateFormTool,