/**
* Person Tool
*
* Application layer tool for managing Person entities in Twenty CRM.
* Supports both GraphQL and REST operations.
*/
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition, ToolResult } from '../../domain/types.js';
import { Person } from '../../domain/twenty-types.js';
import { ITwentyGraphQLClient } from '../../infrastructure/clients/twenty-graphql-client.js';
import { ITwentyRESTClient } from '../../infrastructure/clients/twenty-rest-client.js';
import { ILogger } from '../../infrastructure/logging/logger.js';
/**
* Tool names
*/
export const CREATE_PERSON_TOOL_NAME = 'create-person';
export const GET_PERSON_TOOL_NAME = 'get-person';
export const UPDATE_PERSON_TOOL_NAME = 'update-person';
export const DELETE_PERSON_TOOL_NAME = 'delete-person';
export const LIST_PERSONS_TOOL_NAME = 'list-persons';
/**
* Zod schemas for input validation
*/
export const CreatePersonInputSchema = z.object({
firstName: z.string().describe('First name of the person'),
lastName: z.string().describe('Last name of the person'),
email: z.string().email().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
city: z.string().optional().describe('City'),
companyId: z.string().optional().describe('ID of the associated company'),
position: z.string().optional().describe('Job position'),
});
export const GetPersonInputSchema = z.object({
id: z.string().describe('ID of the person to retrieve'),
});
export const UpdatePersonInputSchema = z.object({
id: z.string().describe('ID of the person to update'),
firstName: z.string().optional().describe('First name'),
lastName: z.string().optional().describe('Last name'),
email: z.string().email().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
city: z.string().optional().describe('City'),
companyId: z.string().optional().describe('Company ID'),
position: z.string().optional().describe('Job position'),
});
export const DeletePersonInputSchema = z.object({
id: z.string().describe('ID of the person to delete'),
});
export const ListPersonsInputSchema = z.object({
limit: z.number().int().positive().optional().describe('Maximum number of results (default: 20)'),
offset: z.number().int().nonnegative().optional().describe('Number of results to skip (default: 0)'),
companyId: z.string().optional().describe('Filter by company ID'),
});
/**
* Type inference from schemas
*/
export type CreatePersonInput = z.infer<typeof CreatePersonInputSchema>;
export type GetPersonInput = z.infer<typeof GetPersonInputSchema>;
export type UpdatePersonInput = z.infer<typeof UpdatePersonInputSchema>;
export type DeletePersonInput = z.infer<typeof DeletePersonInputSchema>;
export type ListPersonsInput = z.infer<typeof ListPersonsInputSchema>;
/**
* GraphQL queries and mutations
*/
const CREATE_PERSON_MUTATION = `
mutation CreatePerson($firstName: String!, $lastName: String!, $email: String, $phone: String, $city: String, $companyId: ID, $position: String) {
createPerson(data: {
name: { firstName: $firstName, lastName: $lastName }
email: $email
phone: $phone
city: $city
companyId: $companyId
position: $position
}) {
id
name { firstName lastName }
email
phone
city
companyId
position
createdAt
updatedAt
}
}
`;
const GET_PERSON_QUERY = `
query GetPerson($id: ID!) {
person(id: $id) {
id
name { firstName lastName }
email
phone
city
companyId
position
createdAt
updatedAt
}
}
`;
const UPDATE_PERSON_MUTATION = `
mutation UpdatePerson($id: ID!, $firstName: String, $lastName: String, $email: String, $phone: String, $city: String, $companyId: ID, $position: String) {
updatePerson(id: $id, data: {
name: { firstName: $firstName, lastName: $lastName }
email: $email
phone: $phone
city: $city
companyId: $companyId
position: $position
}) {
id
name { firstName lastName }
email
phone
city
companyId
position
updatedAt
}
}
`;
const DELETE_PERSON_MUTATION = `
mutation DeletePerson($id: ID!) {
deletePerson(id: $id) {
id
}
}
`;
const LIST_PERSONS_QUERY = `
query ListPersons($limit: Int, $filter: PersonFilterInput) {
people(first: $limit, filter: $filter) {
edges {
node {
id
name { firstName lastName }
emails { primaryEmail }
phones { primaryPhoneNumber primaryPhoneCountryCode }
city
companyId
position
createdAt
updatedAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
/**
* Helper to convert Zod schema to tool definition
*/
function createToolDefinition(name: string, description: string, schema: z.ZodObject<any>): ToolDefinition {
const jsonSchema = zodToJsonSchema(schema, {
name: `${name}Input`,
$refStrategy: 'none',
});
const actualSchema = (jsonSchema as any).definitions?.[`${name}Input`] || jsonSchema;
return {
name,
description,
inputSchema: actualSchema as ToolDefinition['inputSchema'],
};
}
/**
* Person tool handler
*/
export class PersonTool {
private readonly graphqlClient: ITwentyGraphQLClient;
private readonly restClient: ITwentyRESTClient;
private readonly logger: ILogger;
constructor(
graphqlClient: ITwentyGraphQLClient,
restClient: ITwentyRESTClient,
logger: ILogger
) {
this.graphqlClient = graphqlClient;
this.restClient = restClient;
this.logger = logger;
}
/**
* Create a new person
*/
async createPerson(args: unknown): Promise<ToolResult> {
const parseResult = CreatePersonInputSchema.safeParse(args);
if (!parseResult.success) {
return this.createErrorResult('Invalid input: ' + parseResult.error.message);
}
const input = parseResult.data;
this.logger.info('Creating person', { input });
const result = await this.graphqlClient.mutate<{ createPerson: Person }>(
CREATE_PERSON_MUTATION,
input
);
if (!result || !result.createPerson) {
return this.createErrorResult('Failed to create person');
}
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, person: result.createPerson }, null, 2),
}],
};
}
/**
* Get a person by ID
*/
async getPerson(args: unknown): Promise<ToolResult> {
const parseResult = GetPersonInputSchema.safeParse(args);
if (!parseResult.success) {
return this.createErrorResult('Invalid input: ' + parseResult.error.message);
}
const { id } = parseResult.data;
this.logger.info('Getting person', { id });
const result = await this.graphqlClient.query<{ person: Person }>(GET_PERSON_QUERY, { id });
if (!result || !result.person) {
return this.createErrorResult('Person not found');
}
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, person: result.person }, null, 2),
}],
};
}
/**
* Update a person
*/
async updatePerson(args: unknown): Promise<ToolResult> {
const parseResult = UpdatePersonInputSchema.safeParse(args);
if (!parseResult.success) {
return this.createErrorResult('Invalid input: ' + parseResult.error.message);
}
const input = parseResult.data;
this.logger.info('Updating person', { input });
const result = await this.graphqlClient.mutate<{ updatePerson: Person }>(
UPDATE_PERSON_MUTATION,
input
);
if (!result || !result.updatePerson) {
return this.createErrorResult('Failed to update person');
}
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, person: result.updatePerson }, null, 2),
}],
};
}
/**
* Delete a person
*/
async deletePerson(args: unknown): Promise<ToolResult> {
const parseResult = DeletePersonInputSchema.safeParse(args);
if (!parseResult.success) {
return this.createErrorResult('Invalid input: ' + parseResult.error.message);
}
const { id } = parseResult.data;
this.logger.info('Deleting person', { id });
const result = await this.graphqlClient.mutate<{ deletePerson: { id: string } }>(
DELETE_PERSON_MUTATION,
{ id }
);
if (!result || !result.deletePerson) {
return this.createErrorResult('Failed to delete person');
}
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Person deleted', id }, null, 2),
}],
};
}
/**
* List persons with optional filtering
*/
async listPersons(args: unknown): Promise<ToolResult> {
const parseResult = ListPersonsInputSchema.safeParse(args);
if (!parseResult.success) {
return this.createErrorResult('Invalid input: ' + parseResult.error.message);
}
const { limit = 20, companyId } = parseResult.data;
const filter = companyId ? { companyId: { eq: companyId } } : undefined;
this.logger.info('Listing persons', { limit, filter });
const result = await this.graphqlClient.query<{
people: {
edges: Array<{ node: any }>;
pageInfo: { hasNextPage: boolean; endCursor: string };
};
}>(LIST_PERSONS_QUERY, { limit, filter });
if (!result || !result.people) {
return this.createErrorResult('Failed to list persons');
}
const persons = result.people.edges.map((edge) => ({
id: edge.node.id,
name: edge.node.name,
email: edge.node.emails?.primaryEmail || null,
phone: edge.node.phones?.primaryPhoneNumber || null,
city: edge.node.city,
companyId: edge.node.companyId,
position: edge.node.position,
createdAt: edge.node.createdAt,
updatedAt: edge.node.updatedAt,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
persons,
count: persons.length,
hasMore: result.people.pageInfo.hasNextPage,
}, null, 2),
}],
};
}
private createErrorResult(message: string): ToolResult {
return {
content: [{ type: 'text', text: message }],
};
}
}
/**
* Tool definition getters
*/
export function getCreatePersonToolDefinition(): ToolDefinition {
return createToolDefinition(
CREATE_PERSON_TOOL_NAME,
'Create a new person in Twenty CRM',
CreatePersonInputSchema
);
}
export function getGetPersonToolDefinition(): ToolDefinition {
return createToolDefinition(
GET_PERSON_TOOL_NAME,
'Get a person by ID from Twenty CRM',
GetPersonInputSchema
);
}
export function getUpdatePersonToolDefinition(): ToolDefinition {
return createToolDefinition(
UPDATE_PERSON_TOOL_NAME,
'Update a person in Twenty CRM',
UpdatePersonInputSchema
);
}
export function getDeletePersonToolDefinition(): ToolDefinition {
return createToolDefinition(
DELETE_PERSON_TOOL_NAME,
'Delete a person from Twenty CRM',
DeletePersonInputSchema
);
}
export function getListPersonsToolDefinition(): ToolDefinition {
return createToolDefinition(
LIST_PERSONS_TOOL_NAME,
'List persons from Twenty CRM with optional filtering',
ListPersonsInputSchema
);
}
/**
* Factory function to create person tool
*/
export function createPersonTool(
graphqlClient: ITwentyGraphQLClient,
restClient: ITwentyRESTClient,
logger: ILogger
): PersonTool {
return new PersonTool(graphqlClient, restClient, logger);
}