Skip to main content
Glama
MonicaClient.ts59.9 kB
import type { Logger } from 'pino'; import { MonicaPaginatedResponse, MonicaCountriesResponse, MonicaContact, MonicaNote, MonicaSingleResponse, MonicaTask, MonicaDeleteResponse, MonicaActivity, MonicaAddress, MonicaContactField, MonicaContactFieldType, MonicaActivityType, MonicaActivityTypeCategory, MonicaCountry, MonicaGender, MonicaGroup, MonicaConversation, MonicaConversationMessage, MonicaCall, MonicaRelationship, MonicaRelationshipType, MonicaRelationshipTypeGroup, MonicaReminder, MonicaTag, MonicaCurrency, MonicaOccupation, MonicaDocument, MonicaPhoto, MonicaGift, MonicaDebt } from '../types.js'; export type MonicaTokenType = 'bearer' | 'apiKey' | 'legacy'; export interface MonicaClientOptions { baseUrl: string; token: string; tokenType: MonicaTokenType; userToken?: string; logger: Logger; defaultTimeoutMs?: number; } export interface RequestOptions extends RequestInit { searchParams?: Record<string, string | number | boolean | undefined>; timeoutMs?: number; } export interface SearchContactsOptions { query: string; limit?: number; page?: number; includePartial?: boolean; } export interface ListTasksOptions { contactId?: number; limit?: number; page?: number; status?: 'open' | 'completed' | 'all'; } export interface ListContactsOptions { limit?: number; page?: number; includePartial?: boolean; includeContactFields?: boolean; includeTags?: boolean; includeAddresses?: boolean; } export interface ListRelationshipsOptions { contactId: number; limit?: number; page?: number; } export interface ListRelationshipTypesOptions { limit?: number; page?: number; } export interface ListRelationshipTypeGroupsOptions { limit?: number; page?: number; } export interface ListGroupsOptions { limit?: number; page?: number; } export interface ListRemindersOptions { contactId?: number; limit?: number; page?: number; } export interface ListConversationsOptions { contactId?: number; limit?: number; page?: number; } export interface ListCallsOptions { contactId?: number; limit?: number; page?: number; } export interface CreateNotePayload { contactId: number; body: string; isFavorited?: boolean; } export interface UpdateNotePayload { contactId?: number; body?: string; isFavorited?: boolean; } export interface ListActivitiesOptions { contactId?: number; limit?: number; page?: number; } export interface CreateActivityPayload { activityTypeId: number; summary: string; description?: string | null; happenedAt: string; contactIds: number[]; emotionIds?: number[]; } export type UpdateActivityPayload = CreateActivityPayload; export interface ListAddressesOptions { contactId: number; limit?: number; page?: number; } export interface CreateAddressPayload { contactId: number; name: string; street?: string | null; city?: string | null; province?: string | null; postalCode?: string | null; countryId?: string | null; } export type UpdateAddressPayload = CreateAddressPayload; export interface ListActivityTypesOptions { limit?: number; page?: number; } export interface CreateActivityTypePayload { name: string; categoryId: number; locationType?: string | null; } export type UpdateActivityTypePayload = CreateActivityTypePayload; export interface ListActivityTypeCategoriesOptions { limit?: number; page?: number; } export interface CreateActivityTypeCategoryPayload { name: string; } export type UpdateActivityTypeCategoryPayload = CreateActivityTypeCategoryPayload; export interface CreateRelationshipPayload { contactIsId: number; relationshipTypeId: number; ofContactId: number; } export interface UpdateRelationshipPayload { relationshipTypeId: number; } export interface CreateRelationshipTypePayload { name: string; reverseName: string; relationshipTypeGroupId?: number | null; delible?: boolean; } export type UpdateRelationshipTypePayload = CreateRelationshipTypePayload; export interface CreateRelationshipTypeGroupPayload { name: string; delible?: boolean; } export type UpdateRelationshipTypeGroupPayload = CreateRelationshipTypeGroupPayload; export interface CreateGroupPayload { name: string; } export type UpdateGroupPayload = CreateGroupPayload; export interface CreateReminderPayload { title: string; description?: string | null; nextExpectedDate: string; frequencyType: 'one_time' | 'day' | 'week' | 'month' | 'year'; frequencyNumber?: number | null; contactId: number; } export interface UpdateReminderPayload { title?: string; description?: string | null; nextExpectedDate?: string; frequencyType?: 'one_time' | 'day' | 'week' | 'month' | 'year'; frequencyNumber?: number | null; contactId?: number; } export interface CreateConversationPayload { happenedAt: string; contactFieldTypeId: number; contactId: number; } export interface UpdateConversationPayload { happenedAt: string; } export interface CreateConversationMessagePayload { contactId: number; writtenAt: string; writtenByMe: boolean; content: string; } export interface UpdateConversationMessagePayload { contactId: number; writtenAt: string; writtenByMe: boolean; content: string; } export interface CreateCallPayload { contactId: number; calledAt: string; content?: string | null; } export interface UpdateCallPayload { contactId?: number; calledAt?: string; content?: string | null; } export interface CreateTaskPayload { title: string; description?: string | null; status?: 'open' | 'completed'; completedAt?: string | null; contactId: number; } export interface UpdateTaskPayload { title?: string; description?: string | null; status?: 'open' | 'completed'; completedAt?: string | null; contactId?: number; } export interface ListContactFieldTypesOptions { limit?: number; page?: number; } export interface CreateContactFieldTypePayload { name: string; fontawesomeIcon?: string | null; protocol?: string | null; delible?: boolean; kind?: string | null; } export type UpdateContactFieldTypePayload = CreateContactFieldTypePayload; export interface ListContactFieldsOptions { contactId: number; limit?: number; page?: number; } export interface ListCurrenciesOptions { limit?: number; page?: number; } export interface ListOccupationsOptions { limit?: number; page?: number; } export interface CreateContactFieldPayload { contactId: number; contactFieldTypeId: number; data: string; } export type UpdateContactFieldPayload = CreateContactFieldPayload; export interface CreateGiftPayload { contactId: number; receivedOn: string; title: string; description?: string | null; wasGivenByMe?: boolean; amount?: number | null; currencyId?: number | null; } export type UpdateGiftPayload = Partial<CreateGiftPayload>; export interface CreateDebtPayload { contactId: number; description?: string | null; amount: number; currencyId?: number | null; happenedAt: string; isSettled?: boolean; settledAt?: string | null; } export type UpdateDebtPayload = Partial<CreateDebtPayload>; export interface UploadDocumentPayload { base64Data: string; fileName: string; mimeType?: string; contactId?: number; } export interface UploadPhotoPayload { base64Data: string; fileName: string; mimeType?: string; contactId?: number; } export interface CreateTagPayload { name: string; } export type UpdateTagPayload = CreateTagPayload; export type BirthdateInput = | { type: 'exact'; day: number; month: number; year: number; } | { type: 'age'; age: number; } | { type: 'unknown'; }; export type DeceasedDateInput = | { type: 'exact'; day: number; month: number; year: number; } | { type: 'age'; age: number; } | { type: 'unknown'; }; export interface ContactProfileInput { firstName: string; lastName?: string | null; nickname?: string | null; genderId?: number; description?: string | null; isPartial?: boolean; isDeceased?: boolean; birthdate?: BirthdateInput; deceasedDate?: DeceasedDateInput; remindOnDeceasedDate?: boolean; } export class MonicaApiError extends Error { constructor( message: string, public readonly status: number, public readonly data: unknown, public readonly requestId?: string | null ) { super(message); this.name = 'MonicaApiError'; } } export class MonicaClient { private readonly apiBaseUrl: string; private readonly logger: Logger; private readonly defaultTimeoutMs: number; constructor(private readonly options: MonicaClientOptions) { this.apiBaseUrl = `${options.baseUrl.replace(/\/$/, '')}/api/`; this.logger = options.logger; this.defaultTimeoutMs = options.defaultTimeoutMs ?? 15000; if (options.tokenType === 'legacy' && !options.userToken) { throw new Error('Legacy authentication requires both token and user token.'); } } async searchContacts(options: SearchContactsOptions): Promise<MonicaPaginatedResponse<MonicaContact>> { const response = await this.request<MonicaPaginatedResponse<MonicaContact>>('contacts', { searchParams: { query: options.query, limit: options.limit, page: options.page } }); if (options.includePartial === false) { response.data = response.data.filter((contact) => !contact.is_partial); } return response; } async listContacts(options: ListContactsOptions = {}): Promise<MonicaPaginatedResponse<MonicaContact>> { const withParts: string[] = []; if (options.includeContactFields) { withParts.push('contactfields'); } if (options.includeTags) { withParts.push('tags'); } if (options.includeAddresses) { withParts.push('addresses'); } const searchParams: Record<string, string | number | undefined> = { limit: options.limit, page: options.page, with: withParts.length ? withParts.join(',') : undefined, include_partial: options.includePartial ? 1 : undefined }; return this.request<MonicaPaginatedResponse<MonicaContact>>('contacts', { searchParams }); } async getContact(id: number, includeContactFields = false): Promise<MonicaSingleResponse<MonicaContact>> { return this.request<MonicaSingleResponse<MonicaContact>>(`contacts/${id}`, { searchParams: includeContactFields ? { with: 'contactfields' } : undefined }); } async listTasks(options: ListTasksOptions = {}): Promise<MonicaPaginatedResponse<MonicaTask>> { const status = options.status ?? 'open'; const sharedSearchParams: Record<string, string | number | boolean | undefined> = { limit: options.limit, page: options.page }; if (status === 'open') { sharedSearchParams.completed = 0; } else if (status === 'completed') { sharedSearchParams.completed = 1; } if (options.contactId) { return this.request<MonicaPaginatedResponse<MonicaTask>>(`contacts/${options.contactId}/tasks`, { searchParams: sharedSearchParams }); } return this.request<MonicaPaginatedResponse<MonicaTask>>('tasks', { searchParams: sharedSearchParams }); } async listRelationships( options: ListRelationshipsOptions ): Promise<MonicaPaginatedResponse<MonicaRelationship>> { return this.request<MonicaPaginatedResponse<MonicaRelationship>>( `contacts/${options.contactId}/relationships`, { searchParams: { limit: options.limit, page: options.page } } ); } async getRelationship(relationshipId: number): Promise<MonicaSingleResponse<MonicaRelationship>> { return this.request<MonicaSingleResponse<MonicaRelationship>>(`relationships/${relationshipId}`); } async createRelationship( payload: CreateRelationshipPayload ): Promise<MonicaSingleResponse<MonicaRelationship>> { const body = buildRelationshipCreateRequestBody(payload); return this.request<MonicaSingleResponse<MonicaRelationship>>('relationships', { method: 'POST', body: JSON.stringify(body) }); } async updateRelationship( relationshipId: number, payload: UpdateRelationshipPayload ): Promise<MonicaSingleResponse<MonicaRelationship>> { const body = buildRelationshipUpdateRequestBody(payload); return this.request<MonicaSingleResponse<MonicaRelationship>>(`relationships/${relationshipId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteRelationship(relationshipId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`relationships/${relationshipId}`, { method: 'DELETE' }); } async listRelationshipTypes( options: ListRelationshipTypesOptions = {} ): Promise<MonicaPaginatedResponse<MonicaRelationshipType>> { return this.request<MonicaPaginatedResponse<MonicaRelationshipType>>('relationshiptypes', { searchParams: { limit: options.limit, page: options.page } }); } async getRelationshipType( relationshipTypeId: number ): Promise<MonicaSingleResponse<MonicaRelationshipType>> { return this.request<MonicaSingleResponse<MonicaRelationshipType>>(`relationshiptypes/${relationshipTypeId}`); } async createRelationshipType( payload: CreateRelationshipTypePayload ): Promise<MonicaSingleResponse<MonicaRelationshipType>> { const body = buildRelationshipTypeRequestBody(payload); return this.request<MonicaSingleResponse<MonicaRelationshipType>>('relationshiptypes', { method: 'POST', body: JSON.stringify(body) }); } async updateRelationshipType( relationshipTypeId: number, payload: UpdateRelationshipTypePayload ): Promise<MonicaSingleResponse<MonicaRelationshipType>> { const body = buildRelationshipTypeRequestBody(payload); return this.request<MonicaSingleResponse<MonicaRelationshipType>>(`relationshiptypes/${relationshipTypeId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteRelationshipType(relationshipTypeId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`relationshiptypes/${relationshipTypeId}`, { method: 'DELETE' }); } async listRelationshipTypeGroups( options: ListRelationshipTypeGroupsOptions = {} ): Promise<MonicaPaginatedResponse<MonicaRelationshipTypeGroup>> { return this.request<MonicaPaginatedResponse<MonicaRelationshipTypeGroup>>('relationshiptypegroups', { searchParams: { limit: options.limit, page: options.page } }); } async getRelationshipTypeGroup( relationshipTypeGroupId: number ): Promise<MonicaSingleResponse<MonicaRelationshipTypeGroup>> { return this.request<MonicaSingleResponse<MonicaRelationshipTypeGroup>>( `relationshiptypegroups/${relationshipTypeGroupId}` ); } async createRelationshipTypeGroup( payload: CreateRelationshipTypeGroupPayload ): Promise<MonicaSingleResponse<MonicaRelationshipTypeGroup>> { const body = buildRelationshipTypeGroupRequestBody(payload); return this.request<MonicaSingleResponse<MonicaRelationshipTypeGroup>>('relationshiptypegroups', { method: 'POST', body: JSON.stringify(body) }); } async updateRelationshipTypeGroup( relationshipTypeGroupId: number, payload: UpdateRelationshipTypeGroupPayload ): Promise<MonicaSingleResponse<MonicaRelationshipTypeGroup>> { const body = buildRelationshipTypeGroupRequestBody(payload); return this.request<MonicaSingleResponse<MonicaRelationshipTypeGroup>>( `relationshiptypegroups/${relationshipTypeGroupId}`, { method: 'PUT', body: JSON.stringify(body) } ); } async deleteRelationshipTypeGroup( relationshipTypeGroupId: number ): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`relationshiptypegroups/${relationshipTypeGroupId}`, { method: 'DELETE' }); } async listGroups(options: ListGroupsOptions = {}): Promise<MonicaPaginatedResponse<MonicaGroup>> { return this.request<MonicaPaginatedResponse<MonicaGroup>>('groups', { searchParams: { limit: options.limit, page: options.page } }); } async getGroup(groupId: number): Promise<MonicaSingleResponse<MonicaGroup>> { return this.request<MonicaSingleResponse<MonicaGroup>>(`groups/${groupId}`); } async createGroup(payload: CreateGroupPayload): Promise<MonicaSingleResponse<MonicaGroup>> { const body = buildGroupRequestBody(payload); return this.request<MonicaSingleResponse<MonicaGroup>>('groups', { method: 'POST', body: JSON.stringify(body) }); } async updateGroup( groupId: number, payload: UpdateGroupPayload ): Promise<MonicaSingleResponse<MonicaGroup>> { const body = buildGroupRequestBody(payload); return this.request<MonicaSingleResponse<MonicaGroup>>(`groups/${groupId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteGroup(groupId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`groups/${groupId}`, { method: 'DELETE' }); } async listConversations( options: ListConversationsOptions = {} ): Promise<MonicaPaginatedResponse<MonicaConversation>> { const searchParams: Record<string, string | number | boolean | undefined> = { limit: options.limit, page: options.page }; if (options.contactId) { return this.request<MonicaPaginatedResponse<MonicaConversation>>( `contacts/${options.contactId}/conversations`, { searchParams } ); } return this.request<MonicaPaginatedResponse<MonicaConversation>>('conversations', { searchParams }); } async getConversation(conversationId: number): Promise<MonicaSingleResponse<MonicaConversation>> { return this.request<MonicaSingleResponse<MonicaConversation>>(`conversations/${conversationId}`); } async createConversation( payload: CreateConversationPayload ): Promise<MonicaSingleResponse<MonicaConversation>> { const body = buildConversationCreateRequestBody(payload); return this.request<MonicaSingleResponse<MonicaConversation>>('conversations', { method: 'POST', body: JSON.stringify(body) }); } async updateConversation( conversationId: number, payload: UpdateConversationPayload ): Promise<MonicaSingleResponse<MonicaConversation>> { const body = buildConversationUpdateRequestBody(payload); return this.request<MonicaSingleResponse<MonicaConversation>>(`conversations/${conversationId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteConversation(conversationId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`conversations/${conversationId}`, { method: 'DELETE' }); } async addConversationMessage( conversationId: number, payload: CreateConversationMessagePayload ): Promise<MonicaSingleResponse<MonicaConversation>> { const body = buildConversationMessageRequestBody(payload); return this.request<MonicaSingleResponse<MonicaConversation>>( `conversations/${conversationId}/messages`, { method: 'POST', body: JSON.stringify(body) } ); } async updateConversationMessage( conversationId: number, messageId: number, payload: UpdateConversationMessagePayload ): Promise<MonicaSingleResponse<MonicaConversation>> { const body = buildConversationMessageRequestBody(payload); return this.request<MonicaSingleResponse<MonicaConversation>>( `conversations/${conversationId}/messages/${messageId}`, { method: 'PUT', body: JSON.stringify(body) } ); } async deleteConversationMessage( conversationId: number, messageId: number ): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`conversations/${conversationId}/messages/${messageId}`, { method: 'DELETE' }); } async listCalls(options: ListCallsOptions = {}): Promise<MonicaPaginatedResponse<MonicaCall>> { const searchParams: Record<string, string | number | boolean | undefined> = { limit: options.limit, page: options.page }; if (options.contactId) { return this.request<MonicaPaginatedResponse<MonicaCall>>( `contacts/${options.contactId}/calls`, { searchParams } ); } return this.request<MonicaPaginatedResponse<MonicaCall>>('calls', { searchParams }); } async getCall(callId: number): Promise<MonicaSingleResponse<MonicaCall>> { return this.request<MonicaSingleResponse<MonicaCall>>(`calls/${callId}`); } async createCall(payload: CreateCallPayload): Promise<MonicaSingleResponse<MonicaCall>> { const body = buildCallRequestBody(payload); return this.request<MonicaSingleResponse<MonicaCall>>('calls', { method: 'POST', body: JSON.stringify(body) }); } async updateCall(callId: number, payload: UpdateCallPayload): Promise<MonicaSingleResponse<MonicaCall>> { const existing = await this.getCall(callId); const merged = mergeCallPayload(existing.data, payload); const body = buildCallRequestBody(merged); return this.request<MonicaSingleResponse<MonicaCall>>(`calls/${callId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteCall(callId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`calls/${callId}`, { method: 'DELETE' }); } async listReminders(options: ListRemindersOptions = {}): Promise<MonicaPaginatedResponse<MonicaReminder>> { const searchParams: Record<string, string | number | boolean | undefined> = { limit: options.limit, page: options.page }; if (options.contactId) { return this.request<MonicaPaginatedResponse<MonicaReminder>>( `contacts/${options.contactId}/reminders`, { searchParams } ); } return this.request<MonicaPaginatedResponse<MonicaReminder>>('reminders', { searchParams }); } async getReminder(reminderId: number): Promise<MonicaSingleResponse<MonicaReminder>> { return this.request<MonicaSingleResponse<MonicaReminder>>(`reminders/${reminderId}`); } async createReminder( payload: CreateReminderPayload ): Promise<MonicaSingleResponse<MonicaReminder>> { const body = buildReminderRequestBody(payload); return this.request<MonicaSingleResponse<MonicaReminder>>('reminders', { method: 'POST', body: JSON.stringify(body) }); } async updateReminder( reminderId: number, payload: UpdateReminderPayload ): Promise<MonicaSingleResponse<MonicaReminder>> { const existing = await this.getReminder(reminderId); const merged = mergeReminderPayload(existing.data, payload); const body = buildReminderRequestBody(merged); return this.request<MonicaSingleResponse<MonicaReminder>>(`reminders/${reminderId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteReminder(reminderId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`reminders/${reminderId}`, { method: 'DELETE' }); } async createNote(payload: CreateNotePayload): Promise<MonicaSingleResponse<MonicaNote>> { return this.request<MonicaSingleResponse<MonicaNote>>('notes', { method: 'POST', body: JSON.stringify({ contact_id: payload.contactId, body: payload.body, is_favorited: payload.isFavorited ? 1 : 0 }) }); } async getNote(noteId: number): Promise<MonicaSingleResponse<MonicaNote>> { return this.request<MonicaSingleResponse<MonicaNote>>(`notes/${noteId}`); } async updateNote(noteId: number, payload: UpdateNotePayload): Promise<MonicaSingleResponse<MonicaNote>> { const existing = await this.getNote(noteId); const body = buildNoteRequestBody(existing.data, payload); return this.request<MonicaSingleResponse<MonicaNote>>(`notes/${noteId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteNote(noteId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`notes/${noteId}`, { method: 'DELETE' }); } async listActivities(options: ListActivitiesOptions = {}): Promise<MonicaPaginatedResponse<MonicaActivity>> { const searchParams: Record<string, string | number | boolean | undefined> = { limit: options.limit, page: options.page }; const path = options.contactId ? `contacts/${options.contactId}/activities` : 'activities'; return this.request<MonicaPaginatedResponse<MonicaActivity>>(path, { searchParams }); } async getActivity(activityId: number): Promise<MonicaSingleResponse<MonicaActivity>> { return this.request<MonicaSingleResponse<MonicaActivity>>(`activities/${activityId}`); } async createActivity(payload: CreateActivityPayload): Promise<MonicaSingleResponse<MonicaActivity>> { const body = buildActivityRequestBody(payload); return this.request<MonicaSingleResponse<MonicaActivity>>('activities', { method: 'POST', body: JSON.stringify(body) }); } async updateActivity(activityId: number, payload: UpdateActivityPayload): Promise<MonicaSingleResponse<MonicaActivity>> { const body = buildActivityRequestBody(payload); return this.request<MonicaSingleResponse<MonicaActivity>>(`activities/${activityId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteActivity(activityId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`activities/${activityId}`, { method: 'DELETE' }); } async getTask(taskId: number): Promise<MonicaSingleResponse<MonicaTask>> { return this.request<MonicaSingleResponse<MonicaTask>>(`tasks/${taskId}`); } async createTask(payload: CreateTaskPayload): Promise<MonicaSingleResponse<MonicaTask>> { const body = buildTaskRequestBody(payload); return this.request<MonicaSingleResponse<MonicaTask>>('task/', { method: 'POST', body: JSON.stringify(body) }); } async updateTask(taskId: number, payload: UpdateTaskPayload): Promise<MonicaSingleResponse<MonicaTask>> { const existing = await this.getTask(taskId); const body = buildTaskRequestBody(mergeTaskPayload(existing.data, payload)); return this.request<MonicaSingleResponse<MonicaTask>>(`tasks/${taskId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteTask(taskId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`tasks/${taskId}`, { method: 'DELETE' }); } async listAddresses(options: ListAddressesOptions): Promise<MonicaPaginatedResponse<MonicaAddress>> { if (!options.contactId) { throw new Error('contactId is required when listing addresses.'); } return this.request<MonicaPaginatedResponse<MonicaAddress>>(`contacts/${options.contactId}/addresses`, { searchParams: { limit: options.limit, page: options.page } }); } async getAddress(addressId: number): Promise<MonicaSingleResponse<MonicaAddress>> { return this.request<MonicaSingleResponse<MonicaAddress>>(`addresses/${addressId}`); } async createAddress(payload: CreateAddressPayload): Promise<MonicaSingleResponse<MonicaAddress>> { const body = buildAddressRequestBody(payload); return this.request<MonicaSingleResponse<MonicaAddress>>('addresses', { method: 'POST', body: JSON.stringify(body) }); } async updateAddress(addressId: number, payload: UpdateAddressPayload): Promise<MonicaSingleResponse<MonicaAddress>> { const body = buildAddressRequestBody(payload); return this.request<MonicaSingleResponse<MonicaAddress>>(`addresses/${addressId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteAddress(addressId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`addresses/${addressId}`, { method: 'DELETE' }); } async listActivityTypes(options: ListActivityTypesOptions = {}): Promise<MonicaPaginatedResponse<MonicaActivityType>> { return this.request<MonicaPaginatedResponse<MonicaActivityType>>('activitytypes', { searchParams: { limit: options.limit, page: options.page } }); } async getActivityType(activityTypeId: number): Promise<MonicaSingleResponse<MonicaActivityType>> { return this.request<MonicaSingleResponse<MonicaActivityType>>(`activitytypes/${activityTypeId}`); } async createActivityType(payload: CreateActivityTypePayload): Promise<MonicaSingleResponse<MonicaActivityType>> { const body = buildActivityTypeRequestBody(payload); return this.request<MonicaSingleResponse<MonicaActivityType>>('activitytypes', { method: 'POST', body: JSON.stringify(body) }); } async updateActivityType( activityTypeId: number, payload: UpdateActivityTypePayload ): Promise<MonicaSingleResponse<MonicaActivityType>> { const body = buildActivityTypeRequestBody(payload); return this.request<MonicaSingleResponse<MonicaActivityType>>(`activitytypes/${activityTypeId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteActivityType(activityTypeId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`activitytypes/${activityTypeId}`, { method: 'DELETE' }); } async listActivityTypeCategories( options: ListActivityTypeCategoriesOptions = {} ): Promise<MonicaPaginatedResponse<MonicaActivityTypeCategory>> { return this.request<MonicaPaginatedResponse<MonicaActivityTypeCategory>>('activitytypecategories', { searchParams: { limit: options.limit, page: options.page } }); } async getActivityTypeCategory( activityTypeCategoryId: number ): Promise<MonicaSingleResponse<MonicaActivityTypeCategory>> { return this.request<MonicaSingleResponse<MonicaActivityTypeCategory>>( `activitytypecategories/${activityTypeCategoryId}` ); } async createActivityTypeCategory( payload: CreateActivityTypeCategoryPayload ): Promise<MonicaSingleResponse<MonicaActivityTypeCategory>> { const body = buildActivityTypeCategoryRequestBody(payload); return this.request<MonicaSingleResponse<MonicaActivityTypeCategory>>('activitytypecategories', { method: 'POST', body: JSON.stringify(body) }); } async updateActivityTypeCategory( activityTypeCategoryId: number, payload: UpdateActivityTypeCategoryPayload ): Promise<MonicaSingleResponse<MonicaActivityTypeCategory>> { const body = buildActivityTypeCategoryRequestBody(payload); return this.request<MonicaSingleResponse<MonicaActivityTypeCategory>>( `activitytypecategories/${activityTypeCategoryId}`, { method: 'PUT', body: JSON.stringify(body) } ); } async deleteActivityTypeCategory(activityTypeCategoryId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`activitytypecategories/${activityTypeCategoryId}`, { method: 'DELETE' }); } async listCountries(limit?: number, page?: number): Promise<MonicaCountriesResponse> { return this.request<MonicaCountriesResponse>('countries', { searchParams: { limit, page } }); } async listCurrencies(options: ListCurrenciesOptions = {}): Promise<MonicaPaginatedResponse<MonicaCurrency>> { const { limit, page } = options; return this.request<MonicaPaginatedResponse<MonicaCurrency>>('currencies', { searchParams: { limit, page } }); } async listGenders(limit?: number, page?: number): Promise<MonicaPaginatedResponse<MonicaGender>> { return this.request<MonicaPaginatedResponse<MonicaGender>>('genders', { searchParams: { limit, page } }); } async listOccupations(options: ListOccupationsOptions = {}): Promise<MonicaPaginatedResponse<MonicaOccupation>> { const { limit, page } = options; return this.request<MonicaPaginatedResponse<MonicaOccupation>>('occupations', { searchParams: { limit, page } }); } async getGender(genderId: number): Promise<MonicaSingleResponse<MonicaGender>> { return this.request<MonicaSingleResponse<MonicaGender>>(`genders/${genderId}`); } async listContactFieldTypes(options: ListContactFieldTypesOptions = {}): Promise<MonicaPaginatedResponse<MonicaContactFieldType>> { return this.request<MonicaPaginatedResponse<MonicaContactFieldType>>('contactfieldtypes', { searchParams: { limit: options.limit, page: options.page } }); } async getContactFieldType(contactFieldTypeId: number): Promise<MonicaSingleResponse<MonicaContactFieldType>> { return this.request<MonicaSingleResponse<MonicaContactFieldType>>(`contactfieldtypes/${contactFieldTypeId}`); } async createContactFieldType(payload: CreateContactFieldTypePayload): Promise<MonicaSingleResponse<MonicaContactFieldType>> { const body = buildContactFieldTypeRequestBody(payload); return this.request<MonicaSingleResponse<MonicaContactFieldType>>('contactfieldtypes', { method: 'POST', body: JSON.stringify(body) }); } async updateContactFieldType( contactFieldTypeId: number, payload: UpdateContactFieldTypePayload ): Promise<MonicaSingleResponse<MonicaContactFieldType>> { const body = buildContactFieldTypeRequestBody(payload); return this.request<MonicaSingleResponse<MonicaContactFieldType>>(`contactfieldtypes/${contactFieldTypeId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteContactFieldType(contactFieldTypeId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`contactfieldtypes/${contactFieldTypeId}`, { method: 'DELETE' }); } async listContactFields(options: ListContactFieldsOptions): Promise<MonicaPaginatedResponse<MonicaContactField>> { if (!options.contactId) { throw new Error('contactId is required when listing contact fields.'); } return this.request<MonicaPaginatedResponse<MonicaContactField>>(`contacts/${options.contactId}/contactfields`, { searchParams: { limit: options.limit, page: options.page } }); } async getContactField(contactFieldId: number): Promise<MonicaSingleResponse<MonicaContactField>> { return this.request<MonicaSingleResponse<MonicaContactField>>(`contactfields/${contactFieldId}`); } async createContactField(payload: CreateContactFieldPayload): Promise<MonicaSingleResponse<MonicaContactField>> { const body = buildContactFieldRequestBody(payload); return this.request<MonicaSingleResponse<MonicaContactField>>('contactfields', { method: 'POST', body: JSON.stringify(body) }); } async updateContactField( contactFieldId: number, payload: UpdateContactFieldPayload ): Promise<MonicaSingleResponse<MonicaContactField>> { const body = buildContactFieldRequestBody(payload); return this.request<MonicaSingleResponse<MonicaContactField>>(`contactfields/${contactFieldId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteContactField(contactFieldId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`contactfields/${contactFieldId}`, { method: 'DELETE' }); } async listGifts(options: { contactId?: number; limit?: number; page?: number } = {}): Promise<MonicaPaginatedResponse<MonicaGift>> { const { contactId, limit, page } = options; return this.request<MonicaPaginatedResponse<MonicaGift>>('gifts', { searchParams: { contact_id: contactId, limit, page } }); } async getGift(giftId: number): Promise<MonicaSingleResponse<MonicaGift>> { return this.request<MonicaSingleResponse<MonicaGift>>(`gifts/${giftId}`); } async createGift(payload: CreateGiftPayload): Promise<MonicaSingleResponse<MonicaGift>> { return this.request<MonicaSingleResponse<MonicaGift>>('gifts', { method: 'POST', body: JSON.stringify({ contact_id: payload.contactId, received_on: payload.receivedOn, title: payload.title, description: payload.description ?? null, was_given_by_me: payload.wasGivenByMe ?? false, amount: payload.amount ?? null, currency_id: payload.currencyId ?? null }) }); } async updateGift(giftId: number, payload: UpdateGiftPayload): Promise<MonicaSingleResponse<MonicaGift>> { return this.request<MonicaSingleResponse<MonicaGift>>(`gifts/${giftId}`, { method: 'PUT', body: JSON.stringify({ contact_id: payload.contactId, received_on: payload.receivedOn, title: payload.title, description: payload.description ?? null, was_given_by_me: payload.wasGivenByMe, amount: payload.amount ?? null, currency_id: payload.currencyId ?? null }) }); } async deleteGift(giftId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`gifts/${giftId}`, { method: 'DELETE' }); } async listDebts(options: { contactId?: number; limit?: number; page?: number } = {}): Promise<MonicaPaginatedResponse<MonicaDebt>> { const { contactId, limit, page } = options; return this.request<MonicaPaginatedResponse<MonicaDebt>>('debts', { searchParams: { contact_id: contactId, limit, page } }); } async getDebt(debtId: number): Promise<MonicaSingleResponse<MonicaDebt>> { return this.request<MonicaSingleResponse<MonicaDebt>>(`debts/${debtId}`); } async createDebt(payload: CreateDebtPayload): Promise<MonicaSingleResponse<MonicaDebt>> { return this.request<MonicaSingleResponse<MonicaDebt>>('debts', { method: 'POST', body: JSON.stringify({ contact_id: payload.contactId, description: payload.description ?? null, amount: payload.amount, currency_id: payload.currencyId ?? null, happened_at: payload.happenedAt, is_settled: payload.isSettled ?? false, settled_at: payload.settledAt ?? null }) }); } async updateDebt(debtId: number, payload: UpdateDebtPayload): Promise<MonicaSingleResponse<MonicaDebt>> { return this.request<MonicaSingleResponse<MonicaDebt>>(`debts/${debtId}`, { method: 'PUT', body: JSON.stringify({ contact_id: payload.contactId, description: payload.description ?? null, amount: payload.amount, currency_id: payload.currencyId ?? null, happened_at: payload.happenedAt, is_settled: payload.isSettled, settled_at: payload.settledAt ?? null }) }); } async deleteDebt(debtId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`debts/${debtId}`, { method: 'DELETE' }); } async listDocuments(options: { contactId?: number; limit?: number; page?: number } = {}): Promise<MonicaPaginatedResponse<MonicaDocument>> { const { contactId, limit, page } = options; if (contactId) { return this.request<MonicaPaginatedResponse<MonicaDocument>>(`contacts/${contactId}/documents`, { searchParams: { limit, page } }); } return this.request<MonicaPaginatedResponse<MonicaDocument>>('documents', { searchParams: { limit, page } }); } async getDocument(documentId: number): Promise<MonicaSingleResponse<MonicaDocument>> { return this.request<MonicaSingleResponse<MonicaDocument>>(`documents/${documentId}`); } async uploadDocument(payload: UploadDocumentPayload): Promise<MonicaSingleResponse<MonicaDocument>> { const formData = buildMediaFormData(payload, 'document'); return this.request<MonicaSingleResponse<MonicaDocument>>('documents', { method: 'POST', body: formData }); } async deleteDocument(documentId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`documents/${documentId}`, { method: 'DELETE' }); } async listPhotos(options: { contactId?: number; limit?: number; page?: number } = {}): Promise<MonicaPaginatedResponse<MonicaPhoto>> { const { contactId, limit, page } = options; if (contactId) { return this.request<MonicaPaginatedResponse<MonicaPhoto>>(`contacts/${contactId}/photos`, { searchParams: { limit, page } }); } return this.request<MonicaPaginatedResponse<MonicaPhoto>>('photos', { searchParams: { limit, page } }); } async getPhoto(photoId: number): Promise<MonicaSingleResponse<MonicaPhoto>> { return this.request<MonicaSingleResponse<MonicaPhoto>>(`photos/${photoId}`); } async uploadPhoto(payload: UploadPhotoPayload): Promise<MonicaSingleResponse<MonicaPhoto>> { const formData = buildMediaFormData(payload, 'photo'); return this.request<MonicaSingleResponse<MonicaPhoto>>('photos', { method: 'POST', body: formData }); } async deletePhoto(photoId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`photos/${photoId}`, { method: 'DELETE' }); } async listTags(limit?: number, page?: number): Promise<MonicaPaginatedResponse<MonicaTag>> { return this.request<MonicaPaginatedResponse<MonicaTag>>('tags', { searchParams: { limit, page } }); } async getTag(tagId: number): Promise<MonicaSingleResponse<MonicaTag>> { return this.request<MonicaSingleResponse<MonicaTag>>(`tags/${tagId}`); } async createTag(payload: CreateTagPayload): Promise<MonicaSingleResponse<MonicaTag>> { const body = buildTagRequestBody(payload); return this.request<MonicaSingleResponse<MonicaTag>>('tags', { method: 'POST', body: JSON.stringify(body) }); } async listContactTags(contactId: number): Promise<MonicaTag[]> { const response = await this.request<MonicaSingleResponse<MonicaContact>>(`contacts/${contactId}`, { searchParams: { with: 'tags' } }); return response.data.tags ?? []; } async updateTag(tagId: number, payload: UpdateTagPayload): Promise<MonicaSingleResponse<MonicaTag>> { const body = buildTagRequestBody(payload); return this.request<MonicaSingleResponse<MonicaTag>>(`tags/${tagId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteTag(tagId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`tags/${tagId}`, { method: 'DELETE' }); } async setContactTags(contactId: number, tags: string[]): Promise<MonicaSingleResponse<MonicaContact>> { if (!tags.length) { throw new Error('Provide at least one tag name when setting tags on a contact.'); } return this.request<MonicaSingleResponse<MonicaContact>>(`contacts/${contactId}/setTags`, { method: 'POST', body: JSON.stringify({ tags }) }); } async unsetContactTags(contactId: number, tagIds: number[]): Promise<MonicaSingleResponse<MonicaContact>> { if (!tagIds.length) { throw new Error('Provide at least one tagId when removing tags from a contact.'); } return this.request<MonicaSingleResponse<MonicaContact>>(`contacts/${contactId}/unsetTag`, { method: 'POST', body: JSON.stringify({ tags: tagIds }) }); } async createContact(profile: ContactProfileInput): Promise<MonicaSingleResponse<MonicaContact>> { const body = buildContactRequestBody(profile); return this.request<MonicaSingleResponse<MonicaContact>>('contacts', { method: 'POST', body: JSON.stringify(body) }); } async updateContact(contactId: number, profile: ContactProfileInput): Promise<MonicaSingleResponse<MonicaContact>> { const body = buildContactRequestBody(profile); return this.request<MonicaSingleResponse<MonicaContact>>(`contacts/${contactId}`, { method: 'PUT', body: JSON.stringify(body) }); } async deleteContact(contactId: number): Promise<MonicaDeleteResponse> { return this.request<MonicaDeleteResponse>(`contacts/${contactId}`, { method: 'DELETE' }); } async fetchContactNotes(contactId: number, limit?: number, page?: number): Promise<MonicaPaginatedResponse<MonicaNote>> { return this.request<MonicaPaginatedResponse<MonicaNote>>(`contacts/${contactId}/notes`, { searchParams: { limit, page } }); } async healthCheck(): Promise<void> { await this.request('contacts', { searchParams: { limit: 1 } }); } private buildHeaders(additional?: HeadersInit, skipDefaultContentType = false): Headers { const headers = new Headers(additional); headers.set('Accept', 'application/json'); if (!skipDefaultContentType && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } switch (this.options.tokenType) { case 'bearer': headers.set('Authorization', `Bearer ${this.options.token}`); break; case 'apiKey': headers.set('X-API-Key', this.options.token); break; case 'legacy': headers.set('X-Auth-Token', this.options.token); headers.set('X-User-Token', this.options.userToken ?? ''); break; } headers.set('User-Agent', 'monica-crm-mcp/0.1'); return headers; } private async request<T>(path: string, options: RequestOptions = {}): Promise<T> { const url = new URL(path.replace(/^\//, ''), this.apiBaseUrl); if (options.searchParams) { for (const [key, value] of Object.entries(options.searchParams)) { if (value === undefined || value === null) continue; url.searchParams.append(key, String(value)); } } const abortController = new AbortController(); const timeout = setTimeout(() => abortController.abort(), options.timeoutMs ?? this.defaultTimeoutMs); const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData; const headers = this.buildHeaders(options.headers, isFormData); const requestInit: RequestInit = { ...options, headers, signal: abortController.signal }; try { this.logger.debug( { method: (requestInit.method ?? 'GET').toUpperCase(), url: url.pathname, query: url.search ? url.search.slice(1) : undefined }, 'Monica API request started' ); const response = await fetch(url, requestInit); const requestId = response.headers.get('x-request-id'); const text = await response.text(); const data = text ? deserializeJson(text) : undefined; if (!response.ok) { this.logger.warn( { status: response.status, requestId, url: url.toString(), body: scrubSecrets(data) }, 'Monica API request failed' ); throw new MonicaApiError( `Monica API request to ${url.pathname} failed with status ${response.status}`, response.status, data, requestId ); } this.logger.info( { method: (requestInit.method ?? 'GET').toUpperCase(), url: url.pathname, status: response.status, requestId }, 'Monica API request completed' ); return data as T; } catch (error) { if (error instanceof MonicaApiError) { throw error; } if ((error as Error).name === 'AbortError') { this.logger.warn( { method: (requestInit.method ?? 'GET').toUpperCase(), url: url.pathname }, 'Monica API request timed out' ); throw new Error(`Monica API request to ${url.pathname} timed out.`); } this.logger.error({ err: error, url: url.toString() }, 'Unexpected Monica API error'); throw error; } finally { clearTimeout(timeout); } } } function deserializeJson(payload: string): unknown { try { return JSON.parse(payload); } catch (error) { // Monica occasionally returns empty bodies on 204s; surface the raw payload otherwise. return payload; } } function scrubSecrets(value: unknown): unknown { if (!value || typeof value !== 'object') { return value; } if (Array.isArray(value)) { return value.map((entry) => scrubSecrets(entry)); } const record: Record<string, unknown> = { ...(value as Record<string, unknown>) }; for (const [key, v] of Object.entries(record)) { if (key.toLowerCase().includes('token')) { record[key] = '[redacted]'; continue; } record[key] = scrubSecrets(v); } return record; } function buildMediaFormData( payload: UploadDocumentPayload | UploadPhotoPayload, fieldName: 'document' | 'photo' ): FormData { const formData = new FormData(); if (payload.contactId != null) { formData.append('contact_id', String(payload.contactId)); } const buffer = Buffer.from(payload.base64Data, 'base64'); const blob = new Blob([buffer], { type: payload.mimeType ?? 'application/octet-stream' }); formData.append(fieldName, blob, payload.fileName); return formData; } function buildActivityRequestBody(payload: CreateActivityPayload | UpdateActivityPayload): Record<string, unknown> { if (!payload.contactIds.length) { throw new Error('Provide at least one contact ID when creating or updating an activity.'); } const body: Record<string, unknown> = { activity_type_id: payload.activityTypeId, summary: payload.summary, description: payload.description ?? null, happened_at: payload.happenedAt, contacts: payload.contactIds }; if (payload.emotionIds && payload.emotionIds.length) { body.emotions = payload.emotionIds; } return body; } function buildAddressRequestBody(payload: CreateAddressPayload | UpdateAddressPayload): Record<string, unknown> { return { name: payload.name, street: toNull(payload.street), city: toNull(payload.city), province: toNull(payload.province), postal_code: toNull(payload.postalCode), country: payload.countryId ?? null, contact_id: payload.contactId }; } function buildContactFieldTypeRequestBody(payload: CreateContactFieldTypePayload | UpdateContactFieldTypePayload) { return { name: payload.name, fontawesome_icon: toNull(payload.fontawesomeIcon), protocol: toNull(payload.protocol), delible: payload.delible === undefined ? undefined : payload.delible ? 1 : 0, type: toNull(payload.kind) }; } function buildContactFieldRequestBody(payload: CreateContactFieldPayload | UpdateContactFieldPayload) { return { data: payload.data, contact_field_type_id: payload.contactFieldTypeId, contact_id: payload.contactId }; } function buildActivityTypeRequestBody(payload: CreateActivityTypePayload | UpdateActivityTypePayload) { return { name: payload.name, activity_type_category_id: payload.categoryId, location_type: toNull(payload.locationType) }; } function buildActivityTypeCategoryRequestBody( payload: CreateActivityTypeCategoryPayload | UpdateActivityTypeCategoryPayload ) { return { name: payload.name }; } function buildTaskRequestBody(payload: CreateTaskPayload): Record<string, unknown> { return { title: payload.title, description: toNull(payload.description), completed: payload.status === 'completed' ? 1 : 0, completed_at: toNull(payload.completedAt), contact_id: payload.contactId }; } function buildCallRequestBody(payload: CreateCallPayload): Record<string, unknown> { return { contact_id: payload.contactId, called_at: payload.calledAt, content: toNull(payload.content ?? null) }; } function buildConversationCreateRequestBody(payload: CreateConversationPayload) { return { happened_at: payload.happenedAt, contact_field_type_id: payload.contactFieldTypeId, contact_id: payload.contactId }; } function buildConversationUpdateRequestBody(payload: UpdateConversationPayload) { return { happened_at: payload.happenedAt }; } function buildConversationMessageRequestBody( payload: CreateConversationMessagePayload | UpdateConversationMessagePayload ) { return { contact_id: payload.contactId, written_at: payload.writtenAt, written_by_me: payload.writtenByMe, content: payload.content }; } function buildReminderRequestBody(payload: CreateReminderPayload): Record<string, unknown> { const nextExpectedDate = normalizeReminderDate(payload.nextExpectedDate); const body: Record<string, unknown> = { title: payload.title, description: toNull(payload.description ?? null), next_expected_date: nextExpectedDate, frequency_type: payload.frequencyType, contact_id: payload.contactId }; if (payload.frequencyNumber != null) { body.frequency_number = payload.frequencyNumber; } return body; } function normalizeReminderDate(date: string): string { if (!date) { return date; } if (/^\d{4}-\d{2}-\d{2}$/u.test(date)) { return `${date}T00:00:00`; } return date; } function buildRelationshipCreateRequestBody(payload: CreateRelationshipPayload) { return { contact_is: payload.contactIsId, relationship_type_id: payload.relationshipTypeId, of_contact: payload.ofContactId }; } function buildRelationshipUpdateRequestBody(payload: UpdateRelationshipPayload) { return { relationship_type_id: payload.relationshipTypeId }; } function buildRelationshipTypeRequestBody( payload: CreateRelationshipTypePayload | UpdateRelationshipTypePayload ) { return { name: payload.name, name_reverse_relationship: payload.reverseName, relationship_type_group_id: payload.relationshipTypeGroupId ?? null, delible: payload.delible === undefined ? undefined : payload.delible ? 1 : 0 }; } function buildRelationshipTypeGroupRequestBody( payload: CreateRelationshipTypeGroupPayload | UpdateRelationshipTypeGroupPayload ) { return { name: payload.name, delible: payload.delible === undefined ? undefined : payload.delible ? 1 : 0 }; } function buildGroupRequestBody(payload: CreateGroupPayload | UpdateGroupPayload) { return { name: payload.name }; } function mergeCallPayload(existing: MonicaCall, patch: UpdateCallPayload): CreateCallPayload { const calledAt = patch.calledAt ?? (existing.called_at ? existing.called_at.slice(0, 10) : undefined); if (!calledAt) { throw new Error('calledAt is required to update a call.'); } return { contactId: patch.contactId ?? existing.contact.id, calledAt, content: patch.content === undefined ? existing.content : patch.content }; } function mergeReminderPayload(existing: MonicaReminder, patch: UpdateReminderPayload): CreateReminderPayload { const nextExpectedDate = patch.nextExpectedDate ?? (existing.next_expected_date ? existing.next_expected_date.slice(0, 10) : undefined); if (!nextExpectedDate) { throw new Error('nextExpectedDate is required to update a reminder.'); } return { title: patch.title ?? existing.title, description: patch.description === undefined ? existing.description : patch.description, nextExpectedDate, frequencyType: patch.frequencyType ?? existing.frequency_type, frequencyNumber: patch.frequencyNumber ?? existing.frequency_number ?? null, contactId: patch.contactId ?? existing.contact.id }; } function buildTagRequestBody(payload: CreateTagPayload | UpdateTagPayload) { return { name: payload.name }; } function mergeTaskPayload(existing: MonicaTask, patch: UpdateTaskPayload): CreateTaskPayload { const contactId = patch.contactId ?? existing.contact?.id; if (!contactId) { throw new Error('contactId is required to update a task.'); } return { title: patch.title ?? existing.title, description: patch.description ?? existing.description ?? null, status: patch.status ?? (existing.completed ? 'completed' : 'open'), completedAt: patch.completedAt ?? existing.completed_at ?? null, contactId }; } function buildNoteRequestBody(existing: MonicaNote, patch: UpdateNotePayload) { const contactId = patch.contactId ?? existing.contact.id; if (!contactId) { throw new Error('contactId is required to update a note.'); } return { contact_id: contactId, body: patch.body ?? existing.body, is_favorited: (patch.isFavorited ?? existing.is_favorited) ? 1 : 0 }; } function buildContactRequestBody(profile: ContactProfileInput): Record<string, unknown> { const birthdate = resolveBirthdate(profile.birthdate); const deceased = resolveDeceasedDate(profile.deceasedDate); return { first_name: profile.firstName, last_name: toNull(profile.lastName), nickname: toNull(profile.nickname), description: toNull(profile.description), gender_id: profile.genderId, is_partial: profile.isPartial ?? false, is_deceased: profile.isDeceased ?? false, is_birthdate_known: birthdate.isKnown, birthdate_is_age_based: birthdate.isAgeBased, birthdate_day: birthdate.day, birthdate_month: birthdate.month, birthdate_year: birthdate.year, birthdate_age: birthdate.age, is_deceased_date_known: deceased.isKnown, deceased_date_is_age_based: deceased.isAgeBased, deceased_date_day: deceased.day, deceased_date_month: deceased.month, deceased_date_year: deceased.year, deceased_date_age: deceased.age, deceased_date_is_year_unknown: deceased.isYearUnknown, deceased_date_add_reminder: profile.remindOnDeceasedDate ?? false }; } interface ResolvedDateInfo { isKnown: boolean; isAgeBased: boolean; day: number | null; month: number | null; year: number | null; age: number | null; isYearUnknown: boolean; } function resolveBirthdate(input?: BirthdateInput): ResolvedDateInfo { if (!input || input.type === 'unknown') { return { isKnown: false, isAgeBased: false, day: null, month: null, year: null, age: null, isYearUnknown: false }; } if (input.type === 'exact') { return { isKnown: true, isAgeBased: false, day: input.day, month: input.month, year: input.year, age: null, isYearUnknown: false }; } return { isKnown: true, isAgeBased: true, day: null, month: null, year: null, age: input.age, isYearUnknown: false }; } function resolveDeceasedDate(input?: DeceasedDateInput): ResolvedDateInfo { if (!input || input.type === 'unknown') { return { isKnown: false, isAgeBased: false, day: null, month: null, year: null, age: null, isYearUnknown: false }; } if (input.type === 'exact') { return { isKnown: true, isAgeBased: false, day: input.day, month: input.month, year: input.year, age: null, isYearUnknown: false }; } return { isKnown: true, isAgeBased: true, day: null, month: null, year: null, age: input.age, isYearUnknown: false }; } function toNull<T>(value: T | null | undefined): T | null { return value ?? null; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jacob-Stokes/monica-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server