BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

1144 lines
37 KiB
TypeScript

import { ToolDefinition, ToolResult, ToolContext, AppToolHandler } from './types';
import { getGHLClientForUser } from '@/lib/ghl/helpers';
/**
* Comprehensive GHL tools for Commercial Real Estate agents.
* These tools allow the AI assistant to help agents manage their CRM,
* communicate with clients, track deals, and automate workflows.
*/
export const appToolDefinitions: ToolDefinition[] = [
// ============================================
// DASHBOARD & REPORTING
// ============================================
{
name: 'get_dashboard_stats',
description: 'Get current CRM statistics including total contacts, conversations, and deal pipeline summary. Use this when the user asks about their overall CRM status, metrics, or wants a quick overview.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'get_pipeline_summary',
description: 'Get detailed summary of all deal pipelines including stage breakdown and total values. Use this when the user asks about their deals, pipeline status, or wants to see how many opportunities are in each stage.',
inputSchema: {
type: 'object',
properties: {
pipelineId: {
type: 'string',
description: 'Optional: specific pipeline ID to get stats for. Leave empty to get all pipelines.'
}
},
required: []
}
},
// ============================================
// CONTACT MANAGEMENT
// ============================================
{
name: 'search_contacts',
description: 'Search for contacts/leads in the CRM by name, email, phone, company, or any text. Use this to find specific investors, buyers, sellers, tenants, landlords, or property owners.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query - can be name, email, phone, company name, or any text'
},
limit: {
type: 'number',
description: 'Maximum results to return (default 20, max 100)'
}
},
required: ['query']
}
},
{
name: 'list_recent_contacts',
description: 'List recent contacts added to the CRM. Use this to see new leads or recently added prospects.',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of contacts to return (default 20)'
}
},
required: []
}
},
{
name: 'get_contact_details',
description: 'Get full details for a specific contact including all their information, tags, custom fields, and activity. Use this after finding a contact to see their complete profile.',
inputSchema: {
type: 'object',
properties: {
contactId: {
type: 'string',
description: 'The contact ID to look up'
}
},
required: ['contactId']
}
},
{
name: 'create_contact',
description: 'Create a new contact/lead in the CRM. Use this when the user wants to add a new investor, buyer, seller, tenant, landlord, or any prospect.',
inputSchema: {
type: 'object',
properties: {
firstName: { type: 'string', description: 'First name (required)' },
lastName: { type: 'string', description: 'Last name' },
email: { type: 'string', description: 'Email address' },
phone: { type: 'string', description: 'Phone number' },
companyName: { type: 'string', description: 'Company or business name' },
address1: { type: 'string', description: 'Street address' },
city: { type: 'string', description: 'City' },
state: { type: 'string', description: 'State' },
postalCode: { type: 'string', description: 'ZIP/Postal code' },
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags to add (e.g., "investor", "buyer", "seller", "tenant", "landlord", "hot-lead")'
},
source: { type: 'string', description: 'Lead source (e.g., "referral", "website", "cold-call")' }
},
required: ['firstName']
}
},
{
name: 'update_contact',
description: 'Update an existing contact\'s information. Use this to modify contact details, add notes, or update their profile.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID to update' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
companyName: { type: 'string' },
address1: { type: 'string' },
city: { type: 'string' },
state: { type: 'string' },
postalCode: { type: 'string' }
},
required: ['contactId']
}
},
{
name: 'add_tags_to_contact',
description: 'Add tags to categorize a contact. Common CRE tags: investor, buyer, seller, tenant, landlord, 1031-exchange, multifamily, retail, office, industrial, hot-lead, nurture.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID' },
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags to add'
}
},
required: ['contactId', 'tags']
}
},
{
name: 'remove_tags_from_contact',
description: 'Remove tags from a contact.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID' },
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags to remove'
}
},
required: ['contactId', 'tags']
}
},
// ============================================
// DEAL/OPPORTUNITY MANAGEMENT
// ============================================
{
name: 'list_deals',
description: 'List deals/opportunities in the pipeline. Use this to see active deals, deals in a specific stage, or filter by status. In CRE, deals represent property transactions being tracked.',
inputSchema: {
type: 'object',
properties: {
pipelineId: { type: 'string', description: 'Filter by specific pipeline' },
stageId: { type: 'string', description: 'Filter by specific stage' },
status: {
type: 'string',
enum: ['open', 'won', 'lost', 'abandoned', 'all'],
description: 'Filter by status (default: open)'
},
contactId: { type: 'string', description: 'Filter by contact/client' },
limit: { type: 'number', description: 'Max results (default 20)' }
},
required: []
}
},
{
name: 'get_deal_details',
description: 'Get full details for a specific deal/opportunity including value, stage, contact, and custom fields.',
inputSchema: {
type: 'object',
properties: {
dealId: { type: 'string', description: 'The opportunity/deal ID' }
},
required: ['dealId']
}
},
{
name: 'create_deal',
description: 'Create a new deal/opportunity in the pipeline. Use this when starting to track a new property transaction, listing, or investment opportunity.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Deal name (e.g., "123 Main St Office Building" or "Smith Industrial Acquisition")'
},
pipelineId: { type: 'string', description: 'Pipeline ID to add the deal to' },
stageId: { type: 'string', description: 'Starting stage ID' },
contactId: { type: 'string', description: 'Associated contact/client ID' },
monetaryValue: {
type: 'number',
description: 'Deal value in dollars (e.g., property price or commission)'
}
},
required: ['name', 'pipelineId', 'stageId', 'contactId']
}
},
{
name: 'update_deal',
description: 'Update a deal\'s information including name, value, or status.',
inputSchema: {
type: 'object',
properties: {
dealId: { type: 'string', description: 'The deal ID to update' },
name: { type: 'string', description: 'New deal name' },
monetaryValue: { type: 'number', description: 'Updated value' },
status: {
type: 'string',
enum: ['open', 'won', 'lost', 'abandoned'],
description: 'Update status'
}
},
required: ['dealId']
}
},
{
name: 'move_deal_to_stage',
description: 'Move a deal to a different pipeline stage. Use this to advance deals through your sales process (e.g., from "Prospect" to "Under Contract").',
inputSchema: {
type: 'object',
properties: {
dealId: { type: 'string', description: 'The deal ID' },
stageId: { type: 'string', description: 'Target stage ID to move to' }
},
required: ['dealId', 'stageId']
}
},
{
name: 'mark_deal_won',
description: 'Mark a deal as won/closed. Use this when a transaction closes successfully.',
inputSchema: {
type: 'object',
properties: {
dealId: { type: 'string', description: 'The deal ID to mark as won' }
},
required: ['dealId']
}
},
{
name: 'mark_deal_lost',
description: 'Mark a deal as lost. Use this when a transaction falls through or prospect goes elsewhere.',
inputSchema: {
type: 'object',
properties: {
dealId: { type: 'string', description: 'The deal ID to mark as lost' }
},
required: ['dealId']
}
},
// ============================================
// PIPELINE MANAGEMENT
// ============================================
{
name: 'list_pipelines',
description: 'List all deal pipelines and their stages. Use this to see available pipelines (e.g., "Buyer Pipeline", "Seller Pipeline", "Investment Deals") and their stages.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'get_pipeline_stages',
description: 'Get all stages for a specific pipeline. Use this to see the stages available when creating or moving deals.',
inputSchema: {
type: 'object',
properties: {
pipelineId: { type: 'string', description: 'The pipeline ID' }
},
required: ['pipelineId']
}
},
// ============================================
// COMMUNICATIONS
// ============================================
{
name: 'send_sms',
description: 'Send an SMS text message to a contact. Use this to quickly reach out to clients, follow up on showings, or send property updates.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID to message' },
message: {
type: 'string',
description: 'The message to send. Keep professional and concise.'
}
},
required: ['contactId', 'message']
}
},
{
name: 'send_email',
description: 'Send an email to a contact. Use this for longer communications, property details, or formal follow-ups.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID to email' },
subject: { type: 'string', description: 'Email subject line' },
body: {
type: 'string',
description: 'Email body content (can include HTML formatting)'
}
},
required: ['contactId', 'subject', 'body']
}
},
{
name: 'get_conversation_history',
description: 'Get message history with a contact including SMS, emails, and calls. Use this to review past communications before reaching out.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID' },
limit: { type: 'number', description: 'Number of messages to retrieve (default 20)' }
},
required: ['contactId']
}
},
{
name: 'get_unread_messages',
description: 'Get count and list of unread conversations. Use this to check what needs attention.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max conversations to list (default 10)' }
},
required: []
}
},
// ============================================
// WORKFLOW AUTOMATION
// ============================================
{
name: 'list_workflows',
description: 'List all available automation workflows. Workflows can automate follow-ups, drip campaigns, and nurture sequences.',
inputSchema: {
type: 'object',
properties: {
activeOnly: {
type: 'boolean',
description: 'Only show active workflows (default true)'
}
},
required: []
}
},
{
name: 'add_contact_to_workflow',
description: 'Add a contact to an automation workflow. Use this to enroll leads in nurture sequences, follow-up campaigns, or onboarding flows.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID' },
workflowId: { type: 'string', description: 'The workflow ID to enroll them in' }
},
required: ['contactId', 'workflowId']
}
},
{
name: 'remove_contact_from_workflow',
description: 'Remove a contact from an automation workflow. Use this to stop automated communications.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID' },
workflowId: { type: 'string', description: 'The workflow ID to remove them from' }
},
required: ['contactId', 'workflowId']
}
},
// ============================================
// CONTACT OPPORTUNITIES
// ============================================
{
name: 'get_contact_deals',
description: 'Get all deals/opportunities associated with a specific contact. Use this to see a client\'s transaction history.',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'The contact ID' }
},
required: ['contactId']
}
},
// ============================================
// FOLLOW-UP & ENGAGEMENT ANALYTICS
// ============================================
{
name: 'get_contacts_needing_followup',
description: 'Find contacts/leads who haven\'t responded to your messages in X days. Use this to identify leads that need follow-up, find cold leads, or see who hasn\'t replied to your outreach.',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days since last outbound message without response (default 7)'
},
limit: {
type: 'number',
description: 'Maximum contacts to return (default 20)'
}
},
required: []
}
},
{
name: 'get_stale_conversations',
description: 'Find conversations that have gone cold - no activity in X days. Use this to identify leads that need re-engagement or have dropped off.',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days of inactivity to consider stale (default 14)'
},
limit: {
type: 'number',
description: 'Maximum conversations to return (default 20)'
}
},
required: []
}
},
{
name: 'get_recent_engagement',
description: 'Get contacts who have recently engaged (responded to messages) in the last X days. Use this to find hot leads or active prospects.',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to look back (default 7)'
},
limit: {
type: 'number',
description: 'Maximum contacts to return (default 20)'
}
},
required: []
}
}
];
// ============================================
// TOOL HANDLERS
// ============================================
const toolHandlers: Record<string, AppToolHandler> = {
// Dashboard & Reporting
get_dashboard_stats: async (_input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured. Please connect your GoHighLevel account first.' };
const [contacts, conversations, pipelines] = await Promise.all([
ghl.contacts.getAll({ limit: 1 }),
ghl.conversations.getAll({ limit: 1 }),
ghl.pipelines.getAll()
]);
return {
totalContacts: contacts.meta?.total || 0,
totalConversations: conversations.meta?.total || 0,
totalPipelines: pipelines.length,
pipelineNames: pipelines.map(p => p.name)
};
},
get_pipeline_summary: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { pipelineId } = input as { pipelineId?: string };
const pipelines = await ghl.pipelines.getAll();
const targetPipelines = pipelineId
? pipelines.filter(p => p.id === pipelineId)
: pipelines;
const summaries = await Promise.all(
targetPipelines.map(async (pipeline) => {
const opportunities = await ghl.opportunities.getAll({
pipelineId: pipeline.id,
status: 'open'
});
const totalValue = (opportunities.data || []).reduce(
(sum, opp) => sum + (opp.monetaryValue || 0),
0
);
return {
pipelineId: pipeline.id,
pipelineName: pipeline.name,
stages: pipeline.stages?.map(s => s.name) || [],
openDeals: opportunities.data?.length || 0,
totalValue
};
})
);
return { pipelines: summaries };
},
// Contact Management
search_contacts: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { query, limit = 20 } = input as { query: string; limit?: number };
const contacts = await ghl.contacts.search(query, Math.min(limit, 100));
return {
count: contacts.data?.length || 0,
contacts: contacts.data?.map(c => ({
id: c.id,
name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'No name',
email: c.email,
phone: c.phone,
company: c.companyName,
tags: c.tags
})) || []
};
},
list_recent_contacts: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { limit = 20 } = input as { limit?: number };
const contacts = await ghl.contacts.getAll({ limit });
return {
count: contacts.data?.length || 0,
contacts: contacts.data?.map(c => ({
id: c.id,
name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'No name',
email: c.email,
phone: c.phone,
company: c.companyName,
tags: c.tags
})) || []
};
},
get_contact_details: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId } = input as { contactId: string };
const contact = await ghl.contacts.getById(contactId);
return {
id: contact.id,
firstName: contact.firstName,
lastName: contact.lastName,
email: contact.email,
phone: contact.phone,
company: contact.companyName,
address: contact.address1,
city: contact.city,
state: contact.state,
postalCode: contact.postalCode,
tags: contact.tags,
source: contact.source,
customFields: contact.customFields,
dateAdded: contact.dateAdded
};
},
create_contact: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const contact = await ghl.contacts.create(input as any);
return {
success: true,
contactId: contact.id,
message: `Contact "${contact.firstName} ${contact.lastName || ''}" created successfully.`
};
},
update_contact: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, ...updates } = input as { contactId: string } & Record<string, any>;
const contact = await ghl.contacts.update(contactId, updates);
return {
success: true,
message: `Contact updated successfully.`
};
},
add_tags_to_contact: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, tags } = input as { contactId: string; tags: string[] };
await ghl.contacts.addTags(contactId, tags);
return {
success: true,
message: `Tags [${tags.join(', ')}] added to contact.`
};
},
remove_tags_from_contact: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, tags } = input as { contactId: string; tags: string[] };
await ghl.contacts.removeTags(contactId, tags);
return {
success: true,
message: `Tags [${tags.join(', ')}] removed from contact.`
};
},
// Deal/Opportunity Management
list_deals: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { pipelineId, stageId, status = 'open', contactId, limit = 20 } = input as any;
const opportunities = await ghl.opportunities.getAll({
pipelineId,
pipelineStageId: stageId,
status,
contactId,
limit
});
return {
count: opportunities.data?.length || 0,
deals: opportunities.data?.map(o => ({
id: o.id,
name: o.name,
value: o.monetaryValue,
status: o.status,
stage: o.pipelineStageId,
contactId: o.contactId
})) || []
};
},
get_deal_details: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { dealId } = input as { dealId: string };
const deal = await ghl.opportunities.getById(dealId);
return {
id: deal.id,
name: deal.name,
value: deal.monetaryValue,
status: deal.status,
pipelineId: deal.pipelineId,
stageId: deal.pipelineStageId,
contactId: deal.contactId,
assignedTo: deal.assignedTo,
createdAt: deal.createdAt
};
},
create_deal: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { name, pipelineId, stageId, contactId, monetaryValue } = input as any;
const deal = await ghl.opportunities.create({
name,
pipelineId,
pipelineStageId: stageId,
contactId,
monetaryValue
});
return {
success: true,
dealId: deal.id,
message: `Deal "${name}" created successfully.`
};
},
update_deal: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { dealId, ...updates } = input as { dealId: string } & Record<string, any>;
await ghl.opportunities.update(dealId, updates);
return { success: true, message: 'Deal updated successfully.' };
},
move_deal_to_stage: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { dealId, stageId } = input as { dealId: string; stageId: string };
await ghl.opportunities.moveToStage(dealId, stageId);
return { success: true, message: 'Deal moved to new stage.' };
},
mark_deal_won: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { dealId } = input as { dealId: string };
await ghl.opportunities.markAsWon(dealId);
return { success: true, message: 'Deal marked as WON! Congratulations!' };
},
mark_deal_lost: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { dealId } = input as { dealId: string };
await ghl.opportunities.markAsLost(dealId);
return { success: true, message: 'Deal marked as lost.' };
},
// Pipeline Management
list_pipelines: async (_input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const pipelines = await ghl.pipelines.getAll();
return {
count: pipelines.length,
pipelines: pipelines.map(p => ({
id: p.id,
name: p.name,
stages: p.stages?.map(s => ({ id: s.id, name: s.name })) || []
}))
};
},
get_pipeline_stages: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { pipelineId } = input as { pipelineId: string };
const stages = await ghl.pipelines.getStages(pipelineId);
return {
stages: stages.map(s => ({ id: s.id, name: s.name, position: s.position }))
};
},
// Communications
send_sms: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, message } = input as { contactId: string; message: string };
const result = await ghl.conversations.sendSMS({ contactId, message });
return {
success: true,
messageId: result.id,
message: 'SMS sent successfully.'
};
},
send_email: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, subject, body } = input as { contactId: string; subject: string; body: string };
const result = await ghl.conversations.sendEmail({
contactId,
subject,
htmlBody: body
});
return {
success: true,
messageId: result.id,
message: 'Email sent successfully.'
};
},
get_conversation_history: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, limit = 20 } = input as { contactId: string; limit?: number };
// Get conversation for contact
const conversation = await ghl.conversations.getByContactId(contactId);
if (!conversation) {
return { messages: [], message: 'No conversation history found for this contact.' };
}
const messages = await ghl.conversations.getMessages(conversation.id, { limit });
return {
conversationId: conversation.id,
messageCount: messages.data?.length || 0,
messages: messages.data?.map(m => ({
id: m.id,
type: m.type,
direction: m.direction,
body: m.body,
dateAdded: m.dateAdded
})) || []
};
},
get_unread_messages: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { limit = 10 } = input as { limit?: number };
const conversations = await ghl.conversations.getAll({ status: 'unread', limit });
return {
unreadCount: conversations.meta?.total || 0,
conversations: conversations.data?.map(c => ({
id: c.id,
contactId: c.contactId,
lastMessageBody: c.lastMessageBody,
lastMessageDate: c.lastMessageDate
})) || []
};
},
// Workflow Automation
list_workflows: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { activeOnly = true } = input as { activeOnly?: boolean };
const workflows = activeOnly
? await ghl.workflows.getActive()
: await ghl.workflows.getAll();
return {
count: workflows.length,
workflows: workflows.map(w => ({
id: w.id,
name: w.name,
status: w.status
}))
};
},
add_contact_to_workflow: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, workflowId } = input as { contactId: string; workflowId: string };
await ghl.workflows.addContactToWorkflow(workflowId, contactId);
return { success: true, message: 'Contact added to workflow successfully.' };
},
remove_contact_from_workflow: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId, workflowId } = input as { contactId: string; workflowId: string };
await ghl.workflows.removeContactFromWorkflow(workflowId, contactId);
return { success: true, message: 'Contact removed from workflow.' };
},
// Contact Opportunities
get_contact_deals: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { contactId } = input as { contactId: string };
const deals = await ghl.opportunities.getByContact(contactId);
return {
count: deals.length,
deals: deals.map(d => ({
id: d.id,
name: d.name,
value: d.monetaryValue,
status: d.status,
stage: d.pipelineStageId
}))
};
},
// Follow-up & Engagement Analytics
get_contacts_needing_followup: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { days = 7, limit = 20 } = input as { days?: number; limit?: number };
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
// Get recent conversations
const conversations = await ghl.conversations.getAll({ limit: 100 });
const needsFollowup: Array<{
contactId: string;
lastOutboundDate: string;
daysSinceContact: number;
lastMessagePreview: string;
}> = [];
// Check each conversation for follow-up needs
for (const conv of conversations.data || []) {
if (needsFollowup.length >= limit) break;
try {
const messages = await ghl.conversations.getMessages(conv.id, { limit: 5 });
const messageList = messages.data || [];
if (messageList.length === 0) continue;
// Find the last outbound message
const lastOutbound = messageList.find(m => m.direction === 'outbound');
if (!lastOutbound) continue;
// Check if there's an inbound message after the last outbound
const lastOutboundIndex = messageList.indexOf(lastOutbound);
const hasResponse = messageList.slice(0, lastOutboundIndex).some(m => m.direction === 'inbound');
if (hasResponse) continue; // They responded, no follow-up needed
// Check if the outbound message is older than X days
if (!lastOutbound.dateAdded) continue;
const outboundDate = new Date(lastOutbound.dateAdded);
if (outboundDate < cutoffDate) {
const daysSince = Math.floor((Date.now() - outboundDate.getTime()) / (1000 * 60 * 60 * 24));
needsFollowup.push({
contactId: conv.contactId,
lastOutboundDate: outboundDate.toISOString(),
daysSinceContact: daysSince,
lastMessagePreview: (lastOutbound.body || '').substring(0, 100)
});
}
} catch (e) {
// Skip conversations we can't access
continue;
}
}
// Fetch contact details for the results
const results = await Promise.all(
needsFollowup.map(async (item) => {
try {
const contact = await ghl.contacts.getById(item.contactId);
return {
...item,
contactName: `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown',
contactEmail: contact.email,
contactPhone: contact.phone
};
} catch {
return { ...item, contactName: 'Unknown', contactEmail: null, contactPhone: null };
}
})
);
return {
count: results.length,
daysThreshold: days,
contacts: results.sort((a, b) => b.daysSinceContact - a.daysSinceContact)
};
},
get_stale_conversations: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { days = 14, limit = 20 } = input as { days?: number; limit?: number };
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const conversations = await ghl.conversations.getAll({ limit: 100 });
const stale: Array<{
conversationId: string;
contactId: string;
lastActivityDate: string;
daysSinceActivity: number;
}> = [];
for (const conv of conversations.data || []) {
if (stale.length >= limit) break;
const lastMsgDate = conv.lastMessageDate ? new Date(conv.lastMessageDate) : null;
if (!lastMsgDate) continue;
if (lastMsgDate < cutoffDate) {
const daysSince = Math.floor((Date.now() - lastMsgDate.getTime()) / (1000 * 60 * 60 * 24));
stale.push({
conversationId: conv.id,
contactId: conv.contactId,
lastActivityDate: lastMsgDate.toISOString(),
daysSinceActivity: daysSince
});
}
}
// Fetch contact details
const results = await Promise.all(
stale.map(async (item) => {
try {
const contact = await ghl.contacts.getById(item.contactId);
return {
...item,
contactName: `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown',
contactEmail: contact.email,
contactPhone: contact.phone
};
} catch {
return { ...item, contactName: 'Unknown', contactEmail: null, contactPhone: null };
}
})
);
return {
count: results.length,
daysThreshold: days,
conversations: results.sort((a, b) => b.daysSinceActivity - a.daysSinceActivity)
};
},
get_recent_engagement: async (input, context) => {
const ghl = await getGHLClientForUser(context.userId);
if (!ghl) return { error: 'GHL not configured' };
const { days = 7, limit = 20 } = input as { days?: number; limit?: number };
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const conversations = await ghl.conversations.getAll({ limit: 100 });
const engaged: Array<{
contactId: string;
lastEngagementDate: string;
daysSinceEngagement: number;
}> = [];
for (const conv of conversations.data || []) {
if (engaged.length >= limit) break;
try {
const messages = await ghl.conversations.getMessages(conv.id, { limit: 10 });
const messageList = messages.data || [];
// Find the most recent inbound message
const lastInbound = messageList.find(m => m.direction === 'inbound');
if (!lastInbound || !lastInbound.dateAdded) continue;
const inboundDate = new Date(lastInbound.dateAdded);
if (inboundDate >= cutoffDate) {
const daysSince = Math.floor((Date.now() - inboundDate.getTime()) / (1000 * 60 * 60 * 24));
engaged.push({
contactId: conv.contactId,
lastEngagementDate: inboundDate.toISOString(),
daysSinceEngagement: daysSince
});
}
} catch {
continue;
}
}
// Fetch contact details
const results = await Promise.all(
engaged.map(async (item) => {
try {
const contact = await ghl.contacts.getById(item.contactId);
return {
...item,
contactName: `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown',
contactEmail: contact.email,
contactPhone: contact.phone,
tags: contact.tags
};
} catch {
return { ...item, contactName: 'Unknown', contactEmail: null, contactPhone: null, tags: [] };
}
})
);
return {
count: results.length,
daysLookback: days,
contacts: results.sort((a, b) => a.daysSinceEngagement - b.daysSinceEngagement)
};
}
};
export class AppToolsRegistry {
private tools: Map<string, AppToolHandler>;
constructor() {
this.tools = new Map(Object.entries(toolHandlers));
}
has(toolName: string): boolean {
return this.tools.has(toolName);
}
getDefinitions(): ToolDefinition[] {
return appToolDefinitions;
}
async execute(toolName: string, input: Record<string, unknown>, context: ToolContext): Promise<ToolResult> {
const handler = this.tools.get(toolName);
if (!handler) {
return { toolCallId: '', success: false, error: `Unknown tool: ${toolName}` };
}
try {
const result = await handler(input, context);
return { toolCallId: '', success: true, result };
} catch (error) {
console.error(`Tool ${toolName} error:`, error);
return {
toolCallId: '',
success: false,
error: error instanceof Error ? error.message : 'Tool execution failed'
};
}
}
}
export const appToolsRegistry = new AppToolsRegistry();