diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 36884cc4f0..900ee4e13b 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1265,6 +1265,20 @@ export function GoogleSlidesIcon(props: SVGProps) { ) } +export function GoogleContactsIcon(props: SVGProps) { + return ( + + + + + + + ) +} + export function GoogleCalendarIcon(props: SVGProps) { return ( = { google_bigquery: GoogleBigQueryIcon, 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/databricks.mdx b/apps/docs/content/docs/en/tools/databricks.mdx index c2dd7ddcf7..7286685fdc 100644 --- a/apps/docs/content/docs/en/tools/databricks.mdx +++ b/apps/docs/content/docs/en/tools/databricks.mdx @@ -5,7 +5,7 @@ description: Run SQL queries and manage jobs on Databricks import { BlockInfoCard } from "@/components/ui/block-info-card" - @@ -24,6 +24,7 @@ With the Databricks integration in Sim, you can: In Sim, the Databricks integration enables your agents to interact with your data lakehouse as part of automated workflows. Agents can query large-scale datasets, orchestrate ETL pipelines by triggering jobs, monitor job execution, and retrieve results—all without leaving the workflow canvas. This is ideal for automated reporting, data pipeline management, scheduled analytics, and building AI-driven data workflows that react to query results or job outcomes. {/* MANUAL-CONTENT-END */} + ## Usage Instructions Connect to Databricks to execute SQL queries against SQL warehouses, trigger and monitor job runs, manage clusters, and retrieve run outputs. Requires a Personal Access Token and workspace host URL. 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/greenhouse.mdx b/apps/docs/content/docs/en/tools/greenhouse.mdx index 854fa86ac0..61e712935f 100644 --- a/apps/docs/content/docs/en/tools/greenhouse.mdx +++ b/apps/docs/content/docs/en/tools/greenhouse.mdx @@ -5,7 +5,7 @@ description: Manage candidates, jobs, and applications in Greenhouse import { BlockInfoCard } from "@/components/ui/block-info-card" - @@ -24,6 +24,7 @@ With the Greenhouse integration in Sim, you can: In Sim, the Greenhouse integration enables your agents to interact with your recruiting data as part of automated workflows. Agents can pull candidate information, monitor application pipelines, track job openings, and cross-reference hiring team data—all programmatically. This is ideal for building automated recruiting reports, candidate pipeline monitoring, hiring analytics dashboards, and workflows that react to changes in your talent pipeline. {/* MANUAL-CONTENT-END */} + ## Usage Instructions Integrate Greenhouse into the workflow. List and retrieve candidates, jobs, applications, users, departments, offices, and job stages from your Greenhouse ATS account. diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 6f4e0593d9..3a1917fc19 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -44,6 +44,7 @@ "google_bigquery", "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 e173f043c2..fb340a877b 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/tasks': 'Create, read, update, and delete Google Tasks', 'https://www.googleapis.com/auth/userinfo.email': 'View email address', 'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info', diff --git a/apps/sim/blocks/blocks/google_contacts.ts b/apps/sim/blocks/blocks/google_contacts.ts new file mode 100644 index 0000000000..c2eb006df0 --- /dev/null +++ b/apps/sim/blocks/blocks/google_contacts.ts @@ -0,0 +1,271 @@ +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', + mode: 'advanced', + }, + { + 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', + mode: 'advanced', + }, + { + 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'] }, + mode: 'advanced', + }, + + // 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'] }, + mode: 'advanced', + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token from previous list request', + condition: { field: 'operation', value: 'list' }, + mode: 'advanced', + }, + { + 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', + mode: 'advanced', + }, + ], + 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 1a34d4f8fc..3b423aad70 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -50,6 +50,7 @@ import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleBigQueryBlock } from '@/blocks/blocks/google_bigquery' 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' @@ -243,6 +244,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 36884cc4f0..900ee4e13b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1265,6 +1265,20 @@ 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 67f44a7342..e1d3f862aa 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -10,6 +10,7 @@ import { GmailIcon, GoogleBigQueryIcon, GoogleCalendarIcon, + GoogleContactsIcon, GoogleDocsIcon, GoogleDriveIcon, GoogleFormsIcon, @@ -121,6 +122,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-bigquery': { name: 'Google BigQuery', description: 'Query, list, and insert data in Google BigQuery.', diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index 0da86f06fd..2297c5df30 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-bigquery' | 'google-tasks' | 'google-vault' @@ -54,6 +55,7 @@ export type OAuthService = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-contacts' | 'google-bigquery' | 'google-tasks' | 'google-vault' diff --git a/apps/sim/tools/google_contacts/create.ts b/apps/sim/tools/google_contacts/create.ts new file mode 100644 index 0000000000..7ff624626f --- /dev/null +++ b/apps/sim/tools/google_contacts/create.ts @@ -0,0 +1,156 @@ +import { createLogger } from '@sim/logger' +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsCreateParams, + type GoogleContactsCreateResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleContactsCreate') + +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() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to create contact' + logger.error('Failed to create contact', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + + 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..92f02cd339 --- /dev/null +++ b/apps/sim/tools/google_contacts/delete.ts @@ -0,0 +1,74 @@ +import { createLogger } from '@sim/logger' +import { + type GoogleContactsDeleteParams, + type GoogleContactsDeleteResponse, + PEOPLE_API_BASE, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleContactsDelete') + +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.trim()}: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() + const errorMessage = errorData.error?.message || 'Failed to delete contact' + logger.error('Failed to delete contact', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + }, + + 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..a9837d83e8 --- /dev/null +++ b/apps/sim/tools/google_contacts/get.ts @@ -0,0 +1,76 @@ +import { createLogger } from '@sim/logger' +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsGetParams, + type GoogleContactsGetResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleContactsGet') + +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.trim()}?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() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to get contact' + logger.error('Failed to get contact', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + + 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..0509b74599 --- /dev/null +++ b/apps/sim/tools/google_contacts/list.ts @@ -0,0 +1,101 @@ +import { createLogger } from '@sim/logger' +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsListParams, + type GoogleContactsListResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleContactsList') + +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() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to list contacts' + logger.error('Failed to list contacts', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + + 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..79a718f641 --- /dev/null +++ b/apps/sim/tools/google_contacts/search.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsSearchParams, + type GoogleContactsSearchResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleContactsSearch') + +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() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to search contacts' + logger.error('Failed to search contacts', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + + 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..51d50448d7 --- /dev/null +++ b/apps/sim/tools/google_contacts/update.ts @@ -0,0 +1,187 @@ +import { createLogger } from '@sim/logger' +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsUpdateParams, + type GoogleContactsUpdateResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleContactsUpdate') + +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.trim()}: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() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to update contact' + logger.error('Failed to update contact', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + + 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 05a5bc9de5..a0ac0eda4f 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -714,6 +714,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, @@ -3794,6 +3802,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,