/**
* Opportunity Tool
*
* Application layer tool for managing Opportunity entities in Twenty CRM.
*/
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
export const CREATE_OPPORTUNITY_TOOL_NAME = 'create-opportunity';
export const GET_OPPORTUNITY_TOOL_NAME = 'get-opportunity';
export const LIST_OPPORTUNITIES_TOOL_NAME = 'list-opportunities';
export const CreateOpportunityInputSchema = z.object({
name: z.string().describe('Opportunity name'),
amountMicros: z.number().int().optional().describe('Amount in micros (e.g., 100000000 = $100)'),
currencyCode: z.string().optional().describe('Currency code (e.g., USD, EUR)'),
closeDate: z.string().optional().describe('Expected close date (ISO format)'),
stage: z.string().optional().describe('Opportunity stage'),
companyId: z.string().optional().describe('Associated company ID'),
pointOfContactId: z.string().optional().describe('Point of contact person ID'),
});
export const GetOpportunityInputSchema = z.object({
id: z.string().describe('ID of the opportunity to retrieve'),
});
export const ListOpportunitiesInputSchema = 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'),
});
const CREATE_OPPORTUNITY_MUTATION = `
mutation CreateOpportunity($name: String!, $amountMicros: Int, $currencyCode: String, $closeDate: String, $stage: String, $companyId: ID, $pointOfContactId: ID) {
createOpportunity(data: {
name: $name
amount: { amountMicros: $amountMicros, currencyCode: $currencyCode }
closeDate: $closeDate
stage: $stage
companyId: $companyId
pointOfContactId: $pointOfContactId
}) {
id
name
amount { amountMicros currencyCode }
closeDate
stage
companyId
pointOfContactId
createdAt
}
}
`;
const GET_OPPORTUNITY_QUERY = `
query GetOpportunity($id: ID!) {
opportunity(id: $id) {
id
name
amount { amountMicros currencyCode }
closeDate
stage
probability
companyId
pointOfContactId
createdAt
updatedAt
}
}
`;
const LIST_OPPORTUNITIES_QUERY = `
query ListOpportunities($limit: Int, $filter: OpportunityFilterInput) {
opportunities(first: $limit, filter: $filter) {
edges {
node {
id
name
amount { amountMicros currencyCode }
closeDate
stage
pointOfContactId
companyId
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function createToolDefinition(name, description, schema) {
const jsonSchema = zodToJsonSchema(schema, { name: `${name}Input`, $refStrategy: 'none' });
const actualSchema = jsonSchema.definitions?.[`${name}Input`] || jsonSchema;
return { name, description, inputSchema: actualSchema };
}
export class OpportunityTool {
graphqlClient;
restClient;
logger;
constructor(graphqlClient, restClient, logger) {
this.graphqlClient = graphqlClient;
this.restClient = restClient;
this.logger = logger;
}
async createOpportunity(args) {
const parseResult = CreateOpportunityInputSchema.safeParse(args);
if (!parseResult.success) {
return this.error('Invalid input: ' + parseResult.error.message);
}
const result = await this.graphqlClient.mutate(CREATE_OPPORTUNITY_MUTATION, parseResult.data);
if (!result?.createOpportunity) {
return this.error('Failed to create opportunity');
}
return {
content: [{ type: 'text', text: JSON.stringify({ success: true, opportunity: result.createOpportunity }, null, 2) }],
};
}
async getOpportunity(args) {
const parseResult = GetOpportunityInputSchema.safeParse(args);
if (!parseResult.success) {
return this.error('Invalid input: ' + parseResult.error.message);
}
const result = await this.graphqlClient.query(GET_OPPORTUNITY_QUERY, parseResult.data);
if (!result?.opportunity) {
return this.error('Opportunity not found');
}
return {
content: [{ type: 'text', text: JSON.stringify({ success: true, opportunity: result.opportunity }, null, 2) }],
};
}
async listOpportunities(args) {
const parseResult = ListOpportunitiesInputSchema.safeParse(args);
if (!parseResult.success) {
return this.error('Invalid input: ' + parseResult.error.message);
}
const { limit = 20, companyId } = parseResult.data;
const filter = companyId ? { companyId: { eq: companyId } } : undefined;
const result = await this.graphqlClient.query(LIST_OPPORTUNITIES_QUERY, { limit, filter });
if (!result?.opportunities) {
return this.error('Failed to list opportunities');
}
const opportunities = result.opportunities.edges.map((e) => ({
id: e.node.id,
name: e.node.name,
amount: e.node.amount,
closeDate: e.node.closeDate,
stage: e.node.stage,
pointOfContactId: e.node.pointOfContactId,
companyId: e.node.companyId,
createdAt: e.node.createdAt,
}));
return {
content: [{ type: 'text', text: JSON.stringify({
opportunities,
count: opportunities.length,
hasMore: result.opportunities.pageInfo.hasNextPage,
}, null, 2) }],
};
}
error(message) {
return { content: [{ type: 'text', text: message }] };
}
}
export function getCreateOpportunityToolDefinition() {
return createToolDefinition(CREATE_OPPORTUNITY_TOOL_NAME, 'Create a new opportunity in Twenty CRM', CreateOpportunityInputSchema);
}
export function getGetOpportunityToolDefinition() {
return createToolDefinition(GET_OPPORTUNITY_TOOL_NAME, 'Get an opportunity by ID from Twenty CRM', GetOpportunityInputSchema);
}
export function getListOpportunitiesToolDefinition() {
return createToolDefinition(LIST_OPPORTUNITIES_TOOL_NAME, 'List opportunities from Twenty CRM', ListOpportunitiesInputSchema);
}
export function createOpportunityTool(graphqlClient, restClient, logger) {
return new OpportunityTool(graphqlClient, restClient, logger);
}