feat(google-contacts): add google contacts integration (#3340)

* feat(google-contacts): add google contacts integration

* fix(google-contacts): throw error when no update fields provided

* lint

* update icon

* improvement(google-contacts): add advanced mode, error handling, and input trimming

- Set mode: 'advanced' on optional fields (emailType, phoneType, notes, pageSize, pageToken, sortOrder)
- Add createLogger and response.ok error handling to all 6 tools
- Add .trim() on resourceName in get, update, delete URL builders
This commit is contained in:
Waleed
2026-02-27 10:55:51 -08:00
committed by GitHub
parent 49db3ca50b
commit 4cfe8be75a
22 changed files with 1404 additions and 2 deletions

View File

@@ -1265,6 +1265,20 @@ export function GoogleSlidesIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GoogleContactsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'>
<path fill='#86a9ff' d='M199 244c-89 0-161 71-161 160v67c0 16 13 29 29 29h77l77-256z' />
<path fill='#578cff' d='M462 349c0-58-48-105-106-105h-77v256h77c58 0 106-47 106-106' />
<path
fill='#0057cc'
d='M115 349c0-58 48-105 106-105h58c58 0 106 47 106 105v45c0 59-48 106-106 106H144c-16 0-29-13-29-29z'
/>
<circle cx='250' cy='99.4' r='99.4' fill='#0057cc' />
</svg>
)
}
export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -49,6 +49,7 @@ import {
GoogleBigQueryIcon,
GoogleBooksIcon,
GoogleCalendarIcon,
GoogleContactsIcon,
GoogleDocsIcon,
GoogleDriveIcon,
GoogleFormsIcon,
@@ -204,6 +205,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
google_bigquery: GoogleBigQueryIcon,
google_books: GoogleBooksIcon,
google_calendar_v2: GoogleCalendarIcon,
google_contacts: GoogleContactsIcon,
google_docs: GoogleDocsIcon,
google_drive: GoogleDriveIcon,
google_forms: GoogleFormsIcon,

View File

@@ -5,7 +5,7 @@ description: Run SQL queries and manage jobs on Databricks
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
<BlockInfoCard
type="databricks"
color="#FF3621"
/>
@@ -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.

View File

@@ -0,0 +1,144 @@
---
title: Google Contacts
description: Manage Google Contacts
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_contacts"
color="#E0E0E0"
/>
## 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 |

View File

@@ -5,7 +5,7 @@ description: Manage candidates, jobs, and applications in Greenhouse
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
<BlockInfoCard
type="greenhouse"
color="#469776"
/>
@@ -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.

View File

@@ -44,6 +44,7 @@
"google_bigquery",
"google_books",
"google_calendar",
"google_contacts",
"google_docs",
"google_drive",
"google_forms",

View File

@@ -40,6 +40,7 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'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',

View File

@@ -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<GoogleContactsResponse> = {
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<string, any> = { ...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' },
},
}

View File

@@ -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<string, BlockConfig> = {
google_calendar: GoogleCalendarBlock,
google_calendar_v2: GoogleCalendarV2Block,
google_books: GoogleBooksBlock,
google_contacts: GoogleContactsBlock,
google_docs: GoogleDocsBlock,
google_drive: GoogleDriveBlock,
google_forms: GoogleFormsBlock,

View File

@@ -1265,6 +1265,20 @@ export function GoogleSlidesIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GoogleContactsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'>
<path fill='#86a9ff' d='M199 244c-89 0-161 71-161 160v67c0 16 13 29 29 29h77l77-256z' />
<path fill='#578cff' d='M462 349c0-58-48-105-106-105h-77v256h77c58 0 106-47 106-106' />
<path
fill='#0057cc'
d='M115 349c0-58 48-105 106-105h58c58 0 106 47 106 105v45c0 59-48 106-106 106H144c-16 0-29-13-29-29z'
/>
<circle cx='250' cy='99.4' r='99.4' fill='#0057cc' />
</svg>
)
}
export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -480,6 +480,7 @@ export const auth = betterAuth({
'spotify',
'google-email',
'google-calendar',
'google-contacts',
'google-drive',
'google-docs',
'google-sheets',
@@ -1029,6 +1030,45 @@ export const auth = betterAuth({
},
},
{
providerId: 'google-contacts',
clientId: env.GOOGLE_CLIENT_ID as string,
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
accessType: 'offline',
scopes: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/contacts',
],
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-contacts`,
getUserInfo: async (tokens) => {
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,

View File

@@ -10,6 +10,7 @@ import {
GmailIcon,
GoogleBigQueryIcon,
GoogleCalendarIcon,
GoogleContactsIcon,
GoogleDocsIcon,
GoogleDriveIcon,
GoogleFormsIcon,
@@ -121,6 +122,14 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
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.',

View File

@@ -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'

View File

@@ -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<GoogleContactsCreateParams, GoogleContactsCreateResponse> = {
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<string, any> = {
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',
},
},
}

View File

@@ -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<GoogleContactsDeleteParams, GoogleContactsDeleteResponse> = {
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',
},
},
}

View File

@@ -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<GoogleContactsGetParams, GoogleContactsGetResponse> = {
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',
},
},
}

View File

@@ -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

View File

@@ -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<GoogleContactsListParams, GoogleContactsListResponse> = {
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<string, any>) => 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',
},
},
}

View File

@@ -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<GoogleContactsSearchParams, GoogleContactsSearchResponse> = {
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<string, any>) =>
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',
},
},
}

View File

@@ -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<string, any>): 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<string, any>) => ({
value: e.value ?? '',
type: e.type ?? 'other',
})) ?? null,
phones:
person.phoneNumbers?.map((p: Record<string, any>) => ({
value: p.value ?? '',
type: p.type ?? 'other',
})) ?? null,
organizations:
person.organizations?.map((o: Record<string, any>) => ({
name: o.name ?? '',
title: o.title ?? '',
})) ?? null,
addresses:
person.addresses?.map((a: Record<string, any>) => ({
formattedValue: a.formattedValue ?? '',
type: a.type ?? 'other',
})) ?? null,
biographies:
person.biographies?.map((b: Record<string, any>) => ({
value: b.value ?? '',
})) ?? null,
urls:
person.urls?.map((u: Record<string, any>) => ({
value: u.value ?? '',
type: u.type ?? 'other',
})) ?? null,
photos:
person.photos?.map((p: Record<string, any>) => ({
url: p.url ?? '',
})) ?? null,
}
}

View File

@@ -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<GoogleContactsUpdateParams, GoogleContactsUpdateResponse> = {
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<string, any> = {
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',
},
},
}

View File

@@ -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<string, ToolConfig> = {
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,