diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 91748946d4..c400df1147 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1235,6 +1235,27 @@ export function GoogleSlidesIcon(props: SVGProps) { ) } +export function GoogleContactsIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function GoogleCalendarIcon(props: SVGProps) { return ( = { gong: GongIcon, google_books: GoogleBooksIcon, google_calendar_v2: GoogleCalendarIcon, + google_contacts: GoogleContactsIcon, google_docs: GoogleDocsIcon, google_drive: GoogleDriveIcon, google_forms: GoogleFormsIcon, diff --git a/apps/docs/content/docs/en/tools/google_contacts.mdx b/apps/docs/content/docs/en/tools/google_contacts.mdx new file mode 100644 index 0000000000..b68c303a57 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_contacts.mdx @@ -0,0 +1,144 @@ +--- +title: Google Contacts +description: Manage Google Contacts +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Google Contacts into the workflow. Can create, read, update, delete, list, and search contacts. + + + +## Tools + +### `google_contacts_create` + +Create a new contact in Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `givenName` | string | Yes | First name of the contact | +| `familyName` | string | No | Last name of the contact | +| `email` | string | No | Email address of the contact | +| `emailType` | string | No | Email type: home, work, or other | +| `phone` | string | No | Phone number of the contact | +| `phoneType` | string | No | Phone type: mobile, home, work, or other | +| `organization` | string | No | Organization/company name | +| `jobTitle` | string | No | Job title at the organization | +| `notes` | string | No | Notes or biography for the contact | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact creation confirmation message | +| `metadata` | json | Created contact metadata including resource name and details | + +### `google_contacts_get` + +Get a specific contact from Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resourceName` | string | Yes | Resource name of the contact \(e.g., people/c1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact retrieval confirmation message | +| `metadata` | json | Contact details including name, email, phone, and organization | + +### `google_contacts_list` + +List contacts from Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `pageSize` | number | No | Number of contacts to return \(1-1000, default 100\) | +| `pageToken` | string | No | Page token from a previous list request for pagination | +| `sortOrder` | string | No | Sort order for contacts | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Summary of found contacts count | +| `metadata` | json | List of contacts with pagination tokens | + +### `google_contacts_search` + +Search contacts in Google Contacts by name, email, phone, or organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search query to match against contact names, emails, phones, and organizations | +| `pageSize` | number | No | Number of results to return \(default 10, max 30\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Summary of search results count | +| `metadata` | json | Search results with matching contacts | + +### `google_contacts_update` + +Update an existing contact in Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resourceName` | string | Yes | Resource name of the contact \(e.g., people/c1234567890\) | +| `etag` | string | Yes | ETag from a previous get request \(required for concurrency control\) | +| `givenName` | string | No | Updated first name | +| `familyName` | string | No | Updated last name | +| `email` | string | No | Updated email address | +| `emailType` | string | No | Email type: home, work, or other | +| `phone` | string | No | Updated phone number | +| `phoneType` | string | No | Phone type: mobile, home, work, or other | +| `organization` | string | No | Updated organization/company name | +| `jobTitle` | string | No | Updated job title | +| `notes` | string | No | Updated notes or biography | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact update confirmation message | +| `metadata` | json | Updated contact metadata | + +### `google_contacts_delete` + +Delete a contact from Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resourceName` | string | Yes | Resource name of the contact to delete \(e.g., people/c1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact deletion confirmation message | +| `metadata` | json | Deletion details including resource name | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index cd896c4457..521a4f5b3e 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -39,6 +39,7 @@ "gong", "google_books", "google_calendar", + "google_contacts", "google_docs", "google_drive", "google_forms", diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 6cac32e626..f74cffa205 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -40,6 +40,7 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files', 'https://www.googleapis.com/auth/drive': 'Access all Google Drive files', 'https://www.googleapis.com/auth/calendar': 'View and manage calendar', + 'https://www.googleapis.com/auth/contacts': 'View and manage Google Contacts', 'https://www.googleapis.com/auth/userinfo.email': 'View email address', 'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info', 'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms', diff --git a/apps/sim/blocks/blocks/google_contacts.ts b/apps/sim/blocks/blocks/google_contacts.ts new file mode 100644 index 0000000000..8504187ba5 --- /dev/null +++ b/apps/sim/blocks/blocks/google_contacts.ts @@ -0,0 +1,265 @@ +import { GoogleContactsIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { GoogleContactsResponse } from '@/tools/google_contacts/types' + +export const GoogleContactsBlock: BlockConfig = { + type: 'google_contacts', + name: 'Google Contacts', + description: 'Manage Google Contacts', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate Google Contacts into the workflow. Can create, read, update, delete, list, and search contacts.', + docsLink: 'https://docs.sim.ai/tools/google_contacts', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleContactsIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Contact', id: 'create' }, + { label: 'Get Contact', id: 'get' }, + { label: 'List Contacts', id: 'list' }, + { label: 'Search Contacts', id: 'search' }, + { label: 'Update Contact', id: 'update' }, + { label: 'Delete Contact', id: 'delete' }, + ], + value: () => 'create', + }, + { + id: 'credential', + title: 'Google Contacts Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + required: true, + serviceId: 'google-contacts', + requiredScopes: ['https://www.googleapis.com/auth/contacts'], + placeholder: 'Select Google account', + }, + { + id: 'manualCredential', + title: 'Google Contacts Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + + // Create Contact Fields + { + id: 'givenName', + title: 'First Name', + type: 'short-input', + placeholder: 'John', + condition: { field: 'operation', value: ['create', 'update'] }, + required: { field: 'operation', value: 'create' }, + }, + { + id: 'familyName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'john@example.com', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'emailType', + title: 'Email Type', + type: 'dropdown', + condition: { field: 'operation', value: ['create', 'update'] }, + options: [ + { label: 'Work', id: 'work' }, + { label: 'Home', id: 'home' }, + { label: 'Other', id: 'other' }, + ], + value: () => 'work', + }, + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+1234567890', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'phoneType', + title: 'Phone Type', + type: 'dropdown', + condition: { field: 'operation', value: ['create', 'update'] }, + options: [ + { label: 'Mobile', id: 'mobile' }, + { label: 'Home', id: 'home' }, + { label: 'Work', id: 'work' }, + { label: 'Other', id: 'other' }, + ], + value: () => 'mobile', + }, + { + id: 'organization', + title: 'Organization', + type: 'short-input', + placeholder: 'Acme Corp', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'jobTitle', + title: 'Job Title', + type: 'short-input', + placeholder: 'Software Engineer', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'notes', + title: 'Notes', + type: 'long-input', + placeholder: 'Additional notes about the contact', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + + // Get / Update / Delete Fields + { + id: 'resourceName', + title: 'Resource Name', + type: 'short-input', + placeholder: 'people/c1234567890', + condition: { field: 'operation', value: ['get', 'update', 'delete'] }, + required: { field: 'operation', value: ['get', 'update', 'delete'] }, + }, + + // Update requires etag + { + id: 'etag', + title: 'ETag', + type: 'short-input', + placeholder: 'ETag from a previous get request', + condition: { field: 'operation', value: 'update' }, + required: { field: 'operation', value: 'update' }, + }, + + // Search Fields + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Search by name, email, phone, or organization', + condition: { field: 'operation', value: 'search' }, + required: { field: 'operation', value: 'search' }, + }, + + // List/Search Fields + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: ['list', 'search'] }, + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token from previous list request', + condition: { field: 'operation', value: 'list' }, + }, + { + id: 'sortOrder', + title: 'Sort Order', + type: 'dropdown', + condition: { field: 'operation', value: 'list' }, + options: [ + { label: 'Last Modified (Descending)', id: 'LAST_MODIFIED_DESCENDING' }, + { label: 'Last Modified (Ascending)', id: 'LAST_MODIFIED_ASCENDING' }, + { label: 'First Name (Ascending)', id: 'FIRST_NAME_ASCENDING' }, + { label: 'Last Name (Ascending)', id: 'LAST_NAME_ASCENDING' }, + ], + value: () => 'LAST_MODIFIED_DESCENDING', + }, + ], + tools: { + access: [ + 'google_contacts_create', + 'google_contacts_get', + 'google_contacts_list', + 'google_contacts_search', + 'google_contacts_update', + 'google_contacts_delete', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'create': + return 'google_contacts_create' + case 'get': + return 'google_contacts_get' + case 'list': + return 'google_contacts_list' + case 'search': + return 'google_contacts_search' + case 'update': + return 'google_contacts_update' + case 'delete': + return 'google_contacts_delete' + default: + throw new Error(`Invalid Google Contacts operation: ${params.operation}`) + } + }, + params: (params) => { + const { oauthCredential, operation, ...rest } = params + + const processedParams: Record = { ...rest } + + // Convert pageSize to number if provided + if (processedParams.pageSize && typeof processedParams.pageSize === 'string') { + processedParams.pageSize = Number.parseInt(processedParams.pageSize, 10) + } + + return { + oauthCredential, + ...processedParams, + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'Google Contacts access token' }, + + // Create/Update inputs + givenName: { type: 'string', description: 'First name' }, + familyName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + emailType: { type: 'string', description: 'Email type' }, + phone: { type: 'string', description: 'Phone number' }, + phoneType: { type: 'string', description: 'Phone type' }, + organization: { type: 'string', description: 'Organization name' }, + jobTitle: { type: 'string', description: 'Job title' }, + notes: { type: 'string', description: 'Notes' }, + + // Get/Update/Delete inputs + resourceName: { type: 'string', description: 'Contact resource name' }, + etag: { type: 'string', description: 'Contact ETag for updates' }, + + // Search inputs + query: { type: 'string', description: 'Search query' }, + + // List inputs + pageSize: { type: 'string', description: 'Number of results' }, + pageToken: { type: 'string', description: 'Pagination token' }, + sortOrder: { type: 'string', description: 'Sort order' }, + }, + outputs: { + content: { type: 'string', description: 'Operation response content' }, + metadata: { type: 'json', description: 'Contact or contacts metadata' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index a6d3ef3652..82e966c15e 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -45,6 +45,7 @@ import { GongBlock } from '@/blocks/blocks/gong' import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' +import { GoogleContactsBlock } from '@/blocks/blocks/google_contacts' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_forms' @@ -230,6 +231,7 @@ export const registry: Record = { google_calendar: GoogleCalendarBlock, google_calendar_v2: GoogleCalendarV2Block, google_books: GoogleBooksBlock, + google_contacts: GoogleContactsBlock, google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, google_forms: GoogleFormsBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 91748946d4..c400df1147 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1235,6 +1235,27 @@ export function GoogleSlidesIcon(props: SVGProps) { ) } +export function GoogleContactsIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function GoogleCalendarIcon(props: SVGProps) { return ( { + try { + const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }) + if (!response.ok) { + logger.error('Failed to fetch Google user info', { status: response.status }) + throw new Error(`Failed to fetch Google user info: ${response.statusText}`) + } + const profile = await response.json() + const now = new Date() + return { + id: `${profile.sub}-${crypto.randomUUID()}`, + name: profile.name || 'Google User', + email: profile.email, + image: profile.picture || undefined, + emailVerified: profile.email_verified || false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Google getUserInfo', { error }) + throw error + } + }, + }, { providerId: 'google-forms', clientId: env.GOOGLE_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index b890566334..c9cfa07307 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -9,6 +9,7 @@ import { GithubIcon, GmailIcon, GoogleCalendarIcon, + GoogleContactsIcon, GoogleDocsIcon, GoogleDriveIcon, GoogleFormsIcon, @@ -119,6 +120,14 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: GoogleIcon, scopes: ['https://www.googleapis.com/auth/calendar'], }, + 'google-contacts': { + name: 'Google Contacts', + description: 'Create, read, update, and search contacts with Google Contacts.', + providerId: 'google-contacts', + icon: GoogleContactsIcon, + baseProviderIcon: GoogleIcon, + scopes: ['https://www.googleapis.com/auth/contacts'], + }, 'google-vault': { name: 'Google Vault', description: 'Search, export, and manage matters/holds via Google Vault.', diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index d5114a38bc..2a988671f6 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -7,6 +7,7 @@ export type OAuthProvider = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-contacts' | 'google-vault' | 'google-forms' | 'google-groups' @@ -52,6 +53,7 @@ export type OAuthService = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-contacts' | 'google-vault' | 'google-forms' | 'google-groups' diff --git a/apps/sim/tools/google_contacts/create.ts b/apps/sim/tools/google_contacts/create.ts new file mode 100644 index 0000000000..939fc739a0 --- /dev/null +++ b/apps/sim/tools/google_contacts/create.ts @@ -0,0 +1,146 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsCreateParams, + type GoogleContactsCreateResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const createTool: ToolConfig = { + id: 'google_contacts_create', + name: 'Google Contacts Create', + description: 'Create a new contact in Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + givenName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'First name of the contact', + }, + familyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name of the contact', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address of the contact', + }, + emailType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email type: home, work, or other', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number of the contact', + }, + phoneType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone type: mobile, home, work, or other', + }, + organization: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization/company name', + }, + jobTitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Job title at the organization', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Notes or biography for the contact', + }, + }, + + request: { + url: () => `${PEOPLE_API_BASE}/people:createContact?personFields=${DEFAULT_PERSON_FIELDS}`, + method: 'POST', + headers: (params: GoogleContactsCreateParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleContactsCreateParams) => { + const person: Record = { + names: [ + { + givenName: params.givenName, + ...(params.familyName ? { familyName: params.familyName } : {}), + }, + ], + } + + if (params.email) { + person.emailAddresses = [{ value: params.email, type: params.emailType || 'other' }] + } + + if (params.phone) { + person.phoneNumbers = [{ value: params.phone, type: params.phoneType || 'mobile' }] + } + + if (params.organization || params.jobTitle) { + person.organizations = [ + { + ...(params.organization ? { name: params.organization } : {}), + ...(params.jobTitle ? { title: params.jobTitle } : {}), + }, + ] + } + + if (params.notes) { + person.biographies = [{ value: params.notes, contentType: 'TEXT_PLAIN' }] + } + + return person + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const contact = transformPerson(data) + + return { + success: true, + output: { + content: `Contact "${contact.displayName || contact.givenName}" created successfully`, + metadata: contact, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Contact creation confirmation message' }, + metadata: { + type: 'json', + description: 'Created contact metadata including resource name and details', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/delete.ts b/apps/sim/tools/google_contacts/delete.ts new file mode 100644 index 0000000000..ab4bcaf935 --- /dev/null +++ b/apps/sim/tools/google_contacts/delete.ts @@ -0,0 +1,69 @@ +import { + type GoogleContactsDeleteParams, + type GoogleContactsDeleteResponse, + PEOPLE_API_BASE, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteTool: ToolConfig = { + id: 'google_contacts_delete', + name: 'Google Contacts Delete', + description: 'Delete a contact from Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + resourceName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Resource name of the contact to delete (e.g., people/c1234567890)', + }, + }, + + request: { + url: (params: GoogleContactsDeleteParams) => + `${PEOPLE_API_BASE}/${params.resourceName}:deleteContact`, + method: 'DELETE', + headers: (params: GoogleContactsDeleteParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (response.status === 200 || response.status === 204 || response.ok) { + return { + success: true, + output: { + content: 'Contact successfully deleted', + metadata: { + resourceName: params?.resourceName || '', + deleted: true, + }, + }, + } + } + + const errorData = await response.json() + throw new Error(errorData.error?.message || 'Failed to delete contact') + }, + + outputs: { + content: { type: 'string', description: 'Contact deletion confirmation message' }, + metadata: { + type: 'json', + description: 'Deletion details including resource name', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/get.ts b/apps/sim/tools/google_contacts/get.ts new file mode 100644 index 0000000000..ae44178ad2 --- /dev/null +++ b/apps/sim/tools/google_contacts/get.ts @@ -0,0 +1,66 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsGetParams, + type GoogleContactsGetResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const getTool: ToolConfig = { + id: 'google_contacts_get', + name: 'Google Contacts Get', + description: 'Get a specific contact from Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + resourceName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Resource name of the contact (e.g., people/c1234567890)', + }, + }, + + request: { + url: (params: GoogleContactsGetParams) => + `${PEOPLE_API_BASE}/${params.resourceName}?personFields=${DEFAULT_PERSON_FIELDS}`, + method: 'GET', + headers: (params: GoogleContactsGetParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const contact = transformPerson(data) + + return { + success: true, + output: { + content: `Retrieved contact "${contact.displayName || contact.resourceName}"`, + metadata: contact, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Contact retrieval confirmation message' }, + metadata: { + type: 'json', + description: 'Contact details including name, email, phone, and organization', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/index.ts b/apps/sim/tools/google_contacts/index.ts new file mode 100644 index 0000000000..1eb3c84669 --- /dev/null +++ b/apps/sim/tools/google_contacts/index.ts @@ -0,0 +1,13 @@ +import { createTool } from '@/tools/google_contacts/create' +import { deleteTool } from '@/tools/google_contacts/delete' +import { getTool } from '@/tools/google_contacts/get' +import { listTool } from '@/tools/google_contacts/list' +import { searchTool } from '@/tools/google_contacts/search' +import { updateTool } from '@/tools/google_contacts/update' + +export const googleContactsCreateTool = createTool +export const googleContactsDeleteTool = deleteTool +export const googleContactsGetTool = getTool +export const googleContactsListTool = listTool +export const googleContactsSearchTool = searchTool +export const googleContactsUpdateTool = updateTool diff --git a/apps/sim/tools/google_contacts/list.ts b/apps/sim/tools/google_contacts/list.ts new file mode 100644 index 0000000000..05b32e80e0 --- /dev/null +++ b/apps/sim/tools/google_contacts/list.ts @@ -0,0 +1,91 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsListParams, + type GoogleContactsListResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const listTool: ToolConfig = { + id: 'google_contacts_list', + name: 'Google Contacts List', + description: 'List contacts from Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of contacts to return (1-1000, default 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token from a previous list request for pagination', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order for contacts', + }, + }, + + request: { + url: (params: GoogleContactsListParams) => { + const queryParams = new URLSearchParams() + queryParams.append('personFields', DEFAULT_PERSON_FIELDS) + + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.pageToken) queryParams.append('pageToken', params.pageToken) + if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder) + + return `${PEOPLE_API_BASE}/people/me/connections?${queryParams.toString()}` + }, + method: 'GET', + headers: (params: GoogleContactsListParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const connections = data.connections || [] + const contacts = connections.map((person: Record) => transformPerson(person)) + + return { + success: true, + output: { + content: `Found ${contacts.length} contact${contacts.length !== 1 ? 's' : ''}`, + metadata: { + totalItems: data.totalItems ?? null, + nextPageToken: data.nextPageToken ?? null, + contacts, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of found contacts count' }, + metadata: { + type: 'json', + description: 'List of contacts with pagination tokens', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/search.ts b/apps/sim/tools/google_contacts/search.ts new file mode 100644 index 0000000000..226426c3e2 --- /dev/null +++ b/apps/sim/tools/google_contacts/search.ts @@ -0,0 +1,84 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsSearchParams, + type GoogleContactsSearchResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'google_contacts_search', + name: 'Google Contacts Search', + description: 'Search contacts in Google Contacts by name, email, phone, or organization', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query to match against contact names, emails, phones, and organizations', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 30)', + }, + }, + + request: { + url: (params: GoogleContactsSearchParams) => { + const queryParams = new URLSearchParams() + queryParams.append('query', params.query) + queryParams.append('readMask', DEFAULT_PERSON_FIELDS) + + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + + return `${PEOPLE_API_BASE}/people:searchContacts?${queryParams.toString()}` + }, + method: 'GET', + headers: (params: GoogleContactsSearchParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const results = data.results || [] + const contacts = results.map((result: Record) => + transformPerson(result.person || result) + ) + + return { + success: true, + output: { + content: `Found ${contacts.length} contact${contacts.length !== 1 ? 's' : ''} matching query`, + metadata: { + contacts, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of search results count' }, + metadata: { + type: 'json', + description: 'Search results with matching contacts', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/types.ts b/apps/sim/tools/google_contacts/types.ts new file mode 100644 index 0000000000..fc4dfe75b5 --- /dev/null +++ b/apps/sim/tools/google_contacts/types.ts @@ -0,0 +1,185 @@ +import type { ToolResponse } from '@/tools/types' + +export const PEOPLE_API_BASE = 'https://people.googleapis.com/v1' + +export const DEFAULT_PERSON_FIELDS = + 'names,emailAddresses,phoneNumbers,organizations,addresses,biographies,urls,photos,metadata' + +interface BaseGoogleContactsParams { + accessToken: string +} + +export interface GoogleContactsCreateParams extends BaseGoogleContactsParams { + givenName: string + familyName?: string + email?: string + emailType?: 'home' | 'work' | 'other' + phone?: string + phoneType?: 'mobile' | 'home' | 'work' | 'other' + organization?: string + jobTitle?: string + notes?: string +} + +export interface GoogleContactsGetParams extends BaseGoogleContactsParams { + resourceName: string +} + +export interface GoogleContactsListParams extends BaseGoogleContactsParams { + pageSize?: number + pageToken?: string + sortOrder?: + | 'LAST_MODIFIED_ASCENDING' + | 'LAST_MODIFIED_DESCENDING' + | 'FIRST_NAME_ASCENDING' + | 'LAST_NAME_ASCENDING' +} + +export interface GoogleContactsUpdateParams extends BaseGoogleContactsParams { + resourceName: string + etag: string + givenName?: string + familyName?: string + email?: string + emailType?: 'home' | 'work' | 'other' + phone?: string + phoneType?: 'mobile' | 'home' | 'work' | 'other' + organization?: string + jobTitle?: string + notes?: string +} + +export interface GoogleContactsDeleteParams extends BaseGoogleContactsParams { + resourceName: string +} + +export interface GoogleContactsSearchParams extends BaseGoogleContactsParams { + query: string + pageSize?: number +} + +export type GoogleContactsToolParams = + | GoogleContactsCreateParams + | GoogleContactsGetParams + | GoogleContactsListParams + | GoogleContactsUpdateParams + | GoogleContactsDeleteParams + | GoogleContactsSearchParams + +interface ContactMetadata { + resourceName: string + etag: string + displayName: string | null + givenName: string | null + familyName: string | null + emails: Array<{ value: string; type: string }> | null + phones: Array<{ value: string; type: string }> | null + organizations: Array<{ name: string; title: string }> | null + addresses: Array<{ formattedValue: string; type: string }> | null + biographies: Array<{ value: string }> | null + urls: Array<{ value: string; type: string }> | null + photos: Array<{ url: string }> | null +} + +export interface GoogleContactsCreateResponse extends ToolResponse { + output: { + content: string + metadata: ContactMetadata + } +} + +export interface GoogleContactsGetResponse extends ToolResponse { + output: { + content: string + metadata: ContactMetadata + } +} + +export interface GoogleContactsListResponse extends ToolResponse { + output: { + content: string + metadata: { + totalItems: number | null + nextPageToken: string | null + contacts: ContactMetadata[] + } + } +} + +export interface GoogleContactsUpdateResponse extends ToolResponse { + output: { + content: string + metadata: ContactMetadata + } +} + +export interface GoogleContactsDeleteResponse extends ToolResponse { + output: { + content: string + metadata: { + resourceName: string + deleted: boolean + } + } +} + +export interface GoogleContactsSearchResponse extends ToolResponse { + output: { + content: string + metadata: { + contacts: ContactMetadata[] + } + } +} + +export type GoogleContactsResponse = + | GoogleContactsCreateResponse + | GoogleContactsGetResponse + | GoogleContactsListResponse + | GoogleContactsUpdateResponse + | GoogleContactsDeleteResponse + | GoogleContactsSearchResponse + +/** Transforms a raw Google People API person object into a ContactMetadata */ +export function transformPerson(person: Record): ContactMetadata { + return { + resourceName: person.resourceName ?? '', + etag: person.etag ?? '', + displayName: person.names?.[0]?.displayName ?? null, + givenName: person.names?.[0]?.givenName ?? null, + familyName: person.names?.[0]?.familyName ?? null, + emails: + person.emailAddresses?.map((e: Record) => ({ + value: e.value ?? '', + type: e.type ?? 'other', + })) ?? null, + phones: + person.phoneNumbers?.map((p: Record) => ({ + value: p.value ?? '', + type: p.type ?? 'other', + })) ?? null, + organizations: + person.organizations?.map((o: Record) => ({ + name: o.name ?? '', + title: o.title ?? '', + })) ?? null, + addresses: + person.addresses?.map((a: Record) => ({ + formattedValue: a.formattedValue ?? '', + type: a.type ?? 'other', + })) ?? null, + biographies: + person.biographies?.map((b: Record) => ({ + value: b.value ?? '', + })) ?? null, + urls: + person.urls?.map((u: Record) => ({ + value: u.value ?? '', + type: u.type ?? 'other', + })) ?? null, + photos: + person.photos?.map((p: Record) => ({ + url: p.url ?? '', + })) ?? null, + } +} diff --git a/apps/sim/tools/google_contacts/update.ts b/apps/sim/tools/google_contacts/update.ts new file mode 100644 index 0000000000..33fed59f50 --- /dev/null +++ b/apps/sim/tools/google_contacts/update.ts @@ -0,0 +1,177 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsUpdateParams, + type GoogleContactsUpdateResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const updateTool: ToolConfig = { + id: 'google_contacts_update', + name: 'Google Contacts Update', + description: 'Update an existing contact in Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + resourceName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Resource name of the contact (e.g., people/c1234567890)', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ETag from a previous get request (required for concurrency control)', + }, + givenName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated first name', + }, + familyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated last name', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated email address', + }, + emailType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email type: home, work, or other', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated phone number', + }, + phoneType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone type: mobile, home, work, or other', + }, + organization: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated organization/company name', + }, + jobTitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated job title', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated notes or biography', + }, + }, + + request: { + url: (params: GoogleContactsUpdateParams) => { + const updateFields: string[] = [] + if (params.givenName || params.familyName) updateFields.push('names') + if (params.email) updateFields.push('emailAddresses') + if (params.phone) updateFields.push('phoneNumbers') + if (params.organization || params.jobTitle) updateFields.push('organizations') + if (params.notes) updateFields.push('biographies') + + if (updateFields.length === 0) { + throw new Error('At least one field to update must be provided') + } + + const updatePersonFields = updateFields.join(',') + + return `${PEOPLE_API_BASE}/${params.resourceName}:updateContact?updatePersonFields=${updatePersonFields}&personFields=${DEFAULT_PERSON_FIELDS}` + }, + method: 'PATCH', + headers: (params: GoogleContactsUpdateParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleContactsUpdateParams) => { + const person: Record = { + etag: params.etag, + } + + if (params.givenName || params.familyName) { + person.names = [ + { + ...(params.givenName ? { givenName: params.givenName } : {}), + ...(params.familyName ? { familyName: params.familyName } : {}), + }, + ] + } + + if (params.email) { + person.emailAddresses = [{ value: params.email, type: params.emailType || 'other' }] + } + + if (params.phone) { + person.phoneNumbers = [{ value: params.phone, type: params.phoneType || 'mobile' }] + } + + if (params.organization || params.jobTitle) { + person.organizations = [ + { + ...(params.organization ? { name: params.organization } : {}), + ...(params.jobTitle ? { title: params.jobTitle } : {}), + }, + ] + } + + if (params.notes) { + person.biographies = [{ value: params.notes, contentType: 'TEXT_PLAIN' }] + } + + return person + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const contact = transformPerson(data) + + return { + success: true, + output: { + content: `Contact "${contact.displayName || contact.resourceName}" updated successfully`, + metadata: contact, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Contact update confirmation message' }, + metadata: { + type: 'json', + description: 'Updated contact metadata', + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2d1b0e24be..ea47e6be1b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -655,6 +655,14 @@ import { googleCalendarUpdateTool, googleCalendarUpdateV2Tool, } from '@/tools/google_calendar' +import { + googleContactsCreateTool, + googleContactsDeleteTool, + googleContactsGetTool, + googleContactsListTool, + googleContactsSearchTool, + googleContactsUpdateTool, +} from '@/tools/google_contacts' import { googleDocsCreateTool, googleDocsReadTool, googleDocsWriteTool } from '@/tools/google_docs' import { googleDriveCopyTool, @@ -3540,6 +3548,12 @@ export const tools: Record = { google_calendar_quick_add_v2: googleCalendarQuickAddV2Tool, google_calendar_update: googleCalendarUpdateTool, google_calendar_update_v2: googleCalendarUpdateV2Tool, + google_contacts_create: googleContactsCreateTool, + google_contacts_delete: googleContactsDeleteTool, + google_contacts_get: googleContactsGetTool, + google_contacts_list: googleContactsListTool, + google_contacts_search: googleContactsSearchTool, + google_contacts_update: googleContactsUpdateTool, google_calendar_freebusy: googleCalendarFreeBusyTool, google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool, google_forms_get_responses: googleFormsGetResponsesTool,