@vantageos/vantage-crm-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +260 -0
  2. package/dist/convex/crm/_helpers.js +24 -0
  3. package/dist/convex/crm/activities.js +220 -0
  4. package/dist/convex/crm/briefing.js +198 -0
  5. package/dist/convex/crm/calendarCron.js +92 -0
  6. package/dist/convex/crm/calendarCronDispatch.js +83 -0
  7. package/dist/convex/crm/calendarSync.js +294 -0
  8. package/dist/convex/crm/companies.js +323 -0
  9. package/dist/convex/crm/contacts.js +346 -0
  10. package/dist/convex/crm/deals.js +481 -0
  11. package/dist/convex/crm/emailActions.js +158 -0
  12. package/dist/convex/crm/emailCron.js +210 -0
  13. package/dist/convex/crm/emailCronDispatch.js +76 -0
  14. package/dist/convex/crm/emailSync.js +260 -0
  15. package/dist/convex/crm/onboarding.js +185 -0
  16. package/dist/convex/crm/stats.js +75 -0
  17. package/dist/convex/crm/tasks.js +109 -0
  18. package/dist/convex/crons.js +25 -0
  19. package/dist/convex/integrations.js +183 -0
  20. package/dist/convex/lib/auditLog.js +109 -0
  21. package/dist/convex/lib/auth.js +372 -0
  22. package/dist/convex/lib/rbac.js +123 -0
  23. package/dist/convex/lib/workspace.js +171 -0
  24. package/dist/convex/organizations.js +192 -0
  25. package/dist/convex/schema.js +690 -0
  26. package/dist/convex/users.js +217 -0
  27. package/dist/convex/workspaces.js +603 -0
  28. package/dist/mcp-server/lib/convexClient.js +50 -0
  29. package/dist/mcp-server/lib/scopeEnforcement.js +76 -0
  30. package/dist/mcp-server/registry.js +116 -0
  31. package/dist/mcp-server/server.js +97 -0
  32. package/dist/mcp-server/tests/registry.test.js +163 -0
  33. package/dist/mcp-server/tests/scopeEnforcement.test.js +137 -0
  34. package/dist/mcp-server/tests/security.test.js +257 -0
  35. package/dist/mcp-server/tests/tools.test.js +272 -0
  36. package/dist/mcp-server/tools/activities.js +207 -0
  37. package/dist/mcp-server/tools/admin.js +190 -0
  38. package/dist/mcp-server/tools/companies.js +233 -0
  39. package/dist/mcp-server/tools/contacts.js +306 -0
  40. package/dist/mcp-server/tools/customFields.js +222 -0
  41. package/dist/mcp-server/tools/customObjects.js +235 -0
  42. package/dist/mcp-server/tools/deals.js +297 -0
  43. package/dist/mcp-server/tools/rbac.js +177 -0
  44. package/dist/mcp-server/tools/search.js +155 -0
  45. package/dist/mcp-server/tools/workflows.js +234 -0
  46. package/dist/mcp-server/transport/http.js +257 -0
  47. package/dist/mcp-server/transport/stdio.js +90 -0
  48. package/package.json +45 -0
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDailyBrief = void 0;
4
+ const server_1 = require("../_generated/server");
5
+ const values_1 = require("convex/values");
6
+ const workspace_1 = require("../lib/workspace");
7
+ // =============================================================================
8
+ // DAILY BRIEFING
9
+ // =============================================================================
10
+ /**
11
+ * Returns a daily briefing snapshot for the CRM workspace.
12
+ * Each section includes a count + top 5 items for display.
13
+ */
14
+ exports.getDailyBrief = (0, server_1.query)({
15
+ args: {
16
+ workspaceId: values_1.v.id('workspaces'),
17
+ },
18
+ handler: async (ctx, args) => {
19
+ await (0, workspace_1.validateWorkspaceAccess)(ctx, args.workspaceId);
20
+ const now = Date.now();
21
+ const startOfToday = new Date();
22
+ startOfToday.setHours(0, 0, 0, 0);
23
+ const todayStart = startOfToday.getTime();
24
+ const endOfToday = new Date();
25
+ endOfToday.setHours(23, 59, 59, 999);
26
+ const todayEnd = endOfToday.getTime();
27
+ // End of this week (Sunday)
28
+ const endOfWeek = new Date();
29
+ endOfWeek.setDate(endOfWeek.getDate() + (7 - endOfWeek.getDay()));
30
+ endOfWeek.setHours(23, 59, 59, 999);
31
+ const weekEnd = endOfWeek.getTime();
32
+ // 7 days ago for stale contacts
33
+ const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
34
+ // -----------------------------------------------------------------------
35
+ // 1. Deals closing this week
36
+ // -----------------------------------------------------------------------
37
+ const allDeals = await ctx.db
38
+ .query('deals')
39
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', args.workspaceId))
40
+ .take(1000);
41
+ const activeDeals = allDeals.filter((d) => d.isArchived !== true &&
42
+ !d.stage.toLowerCase().includes('lost') &&
43
+ !d.stage.toLowerCase().includes('closed'));
44
+ const dealsClosingThisWeek = activeDeals
45
+ .filter((d) => d.expectedCloseDate !== undefined &&
46
+ d.expectedCloseDate >= todayStart &&
47
+ d.expectedCloseDate <= weekEnd)
48
+ .sort((a, b) => (a.expectedCloseDate ?? 0) - (b.expectedCloseDate ?? 0));
49
+ // Enrich with contact/company names
50
+ const enrichedDeals = await Promise.all(dealsClosingThisWeek.slice(0, 5).map(async (deal) => {
51
+ let contactName;
52
+ let companyName;
53
+ if (deal.contactId) {
54
+ const contact = await ctx.db.get(deal.contactId);
55
+ if (contact)
56
+ contactName = `${contact.firstName} ${contact.lastName}`;
57
+ }
58
+ if (deal.companyId) {
59
+ const company = await ctx.db.get(deal.companyId);
60
+ if (company)
61
+ companyName = company.name;
62
+ }
63
+ return {
64
+ _id: deal._id,
65
+ title: deal.title,
66
+ stage: deal.stage,
67
+ value: deal.value,
68
+ currency: deal.currency,
69
+ expectedCloseDate: deal.expectedCloseDate,
70
+ probability: deal.probability,
71
+ contactName,
72
+ companyName,
73
+ };
74
+ }));
75
+ // -----------------------------------------------------------------------
76
+ // 2. Overdue tasks
77
+ // -----------------------------------------------------------------------
78
+ const allTasks = await ctx.db
79
+ .query('activities')
80
+ .withIndex('by_workspace_type', (q) => q.eq('workspaceId', args.workspaceId).eq('type', 'task'))
81
+ .take(1000);
82
+ const overdueTasks = allTasks
83
+ .filter((t) => t.completedAt === undefined &&
84
+ t.dueAt !== undefined &&
85
+ t.dueAt < now)
86
+ .sort((a, b) => (a.dueAt ?? 0) - (b.dueAt ?? 0));
87
+ const enrichedOverdueTasks = await Promise.all(overdueTasks.slice(0, 5).map(async (task) => {
88
+ let contactName;
89
+ let dealTitle;
90
+ if (task.contactId) {
91
+ const contact = await ctx.db.get(task.contactId);
92
+ if (contact)
93
+ contactName = `${contact.firstName} ${contact.lastName}`;
94
+ }
95
+ if (task.dealId) {
96
+ const deal = await ctx.db.get(task.dealId);
97
+ if (deal)
98
+ dealTitle = deal.title;
99
+ }
100
+ return {
101
+ _id: task._id,
102
+ subject: task.subject,
103
+ dueAt: task.dueAt,
104
+ contactName,
105
+ dealTitle,
106
+ };
107
+ }));
108
+ // -----------------------------------------------------------------------
109
+ // 3. Stale contacts (not contacted in 7+ days)
110
+ // -----------------------------------------------------------------------
111
+ const allContacts = await ctx.db
112
+ .query('contacts')
113
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', args.workspaceId))
114
+ .take(1000);
115
+ const staleContacts = allContacts
116
+ .filter((c) => c.isArchived !== true &&
117
+ (c.type === 'lead' || c.type === 'prospect') &&
118
+ (c.lastContactedAt === undefined || c.lastContactedAt < sevenDaysAgo))
119
+ .sort((a, b) => (a.lastContactedAt ?? 0) - (b.lastContactedAt ?? 0));
120
+ const enrichedStaleContacts = await Promise.all(staleContacts.slice(0, 5).map(async (contact) => {
121
+ let companyName;
122
+ if (contact.companyId) {
123
+ const company = await ctx.db.get(contact.companyId);
124
+ if (company)
125
+ companyName = company.name;
126
+ }
127
+ return {
128
+ _id: contact._id,
129
+ firstName: contact.firstName,
130
+ lastName: contact.lastName,
131
+ email: contact.email,
132
+ type: contact.type,
133
+ lastContactedAt: contact.lastContactedAt,
134
+ companyName,
135
+ };
136
+ }));
137
+ // -----------------------------------------------------------------------
138
+ // 4. Today's meetings (from calendarEvents)
139
+ // -----------------------------------------------------------------------
140
+ const calendarEvents = await ctx.db
141
+ .query('calendarEvents')
142
+ .withIndex('by_workspace_start', (q) => q
143
+ .eq('workspaceId', args.workspaceId)
144
+ .gte('startTime', todayStart)
145
+ .lte('startTime', todayEnd))
146
+ .take(50);
147
+ const todaysMeetings = calendarEvents
148
+ .sort((a, b) => a.startTime - b.startTime)
149
+ .slice(0, 5)
150
+ .map((event) => ({
151
+ _id: event._id,
152
+ title: event.title,
153
+ startTime: event.startTime,
154
+ endTime: event.endTime,
155
+ description: event.description,
156
+ attendeeCount: event.attendees?.length ?? 0,
157
+ }));
158
+ // -----------------------------------------------------------------------
159
+ // 5. Pipeline stats
160
+ // -----------------------------------------------------------------------
161
+ const stageMap = new Map();
162
+ for (const deal of activeDeals) {
163
+ const existing = stageMap.get(deal.stage) ?? { count: 0, totalValue: 0 };
164
+ existing.count += 1;
165
+ existing.totalValue += deal.value ?? 0;
166
+ stageMap.set(deal.stage, existing);
167
+ }
168
+ const pipelineStats = {
169
+ totalDeals: activeDeals.length,
170
+ totalValue: activeDeals.reduce((sum, d) => sum + (d.value ?? 0), 0),
171
+ weightedValue: activeDeals.reduce((sum, d) => sum + ((d.value ?? 0) * (d.probability ?? 0)) / 100, 0),
172
+ byStage: Array.from(stageMap.entries()).map(([stage, data]) => ({
173
+ stage,
174
+ count: data.count,
175
+ totalValue: data.totalValue,
176
+ })),
177
+ };
178
+ return {
179
+ dealsClosingThisWeek: {
180
+ count: dealsClosingThisWeek.length,
181
+ items: enrichedDeals,
182
+ },
183
+ overdueTasks: {
184
+ count: overdueTasks.length,
185
+ items: enrichedOverdueTasks,
186
+ },
187
+ staleContacts: {
188
+ count: staleContacts.length,
189
+ items: enrichedStaleContacts,
190
+ },
191
+ todaysMeetings: {
192
+ count: calendarEvents.length,
193
+ items: todaysMeetings,
194
+ },
195
+ pipelineStats,
196
+ };
197
+ },
198
+ });
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * Calendar Cron — Action that syncs Google Calendar events via Composio.
4
+ *
5
+ * Runs every 5 minutes via Convex cron scheduler.
6
+ * Flow: fetch events from Composio → transform → upsert via calendarSync.syncEvents
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.triggerSync = exports.syncCalendarEvents = void 0;
10
+ const server_1 = require("../_generated/server");
11
+ const api_1 = require("../_generated/api");
12
+ const values_1 = require("convex/values");
13
+ // =============================================================================
14
+ // SYNC CALENDAR EVENTS — internal action called by cron
15
+ // =============================================================================
16
+ exports.syncCalendarEvents = (0, server_1.internalAction)({
17
+ args: {
18
+ workspaceId: values_1.v.id('workspaces'),
19
+ entityId: values_1.v.string(),
20
+ },
21
+ handler: async (ctx, args) => {
22
+ console.log('[CalendarCron] Starting sync for workspace:', args.workspaceId, 'entity:', args.entityId);
23
+ try {
24
+ // Call the Next.js API route that wraps Composio
25
+ // Convex actions can't import Node.js-only Composio SDK directly,
26
+ // so we call the integration endpoint via fetch.
27
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL;
28
+ if (!appUrl) {
29
+ console.error('[CalendarCron] NEXT_PUBLIC_APP_URL not configured');
30
+ return { success: false, error: 'App URL not configured' };
31
+ }
32
+ const cronSecret = process.env.CRON_SECRET;
33
+ if (!cronSecret) {
34
+ console.error('[CalendarCron] CRON_SECRET not configured');
35
+ return { success: false, error: 'Cron secret not configured' };
36
+ }
37
+ // Fetch events from the calendar sync API endpoint
38
+ const response = await fetch(`${appUrl}/api/integrations/calendar-sync`, {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ Authorization: `Bearer ${cronSecret}`,
43
+ },
44
+ body: JSON.stringify({
45
+ entityId: args.entityId,
46
+ workspaceId: args.workspaceId,
47
+ }),
48
+ });
49
+ if (!response.ok) {
50
+ const errorText = await response.text();
51
+ console.error('[CalendarCron] API call failed:', response.status, errorText);
52
+ return { success: false, error: `API error: ${response.status}` };
53
+ }
54
+ const data = (await response.json());
55
+ if (!data.events || data.events.length === 0) {
56
+ console.log('[CalendarCron] No events to sync');
57
+ return { success: true, upserted: 0, linked: 0 };
58
+ }
59
+ console.log('[CalendarCron] Fetched', data.events.length, 'events from Composio');
60
+ // Upsert events into Convex via internal mutation
61
+ const result = await ctx.runMutation(api_1.internal.crm.calendarSync.syncEvents, {
62
+ workspaceId: args.workspaceId,
63
+ events: data.events,
64
+ });
65
+ console.log('[CalendarCron] Sync complete:', result.upserted, 'upserted,', result.linked, 'linked');
66
+ return { success: true, upserted: result.upserted, linked: result.linked };
67
+ }
68
+ catch (error) {
69
+ console.error('[CalendarCron] Sync failed:', error);
70
+ return {
71
+ success: false,
72
+ error: error instanceof Error ? error.message : 'Unknown error',
73
+ };
74
+ }
75
+ },
76
+ });
77
+ // =============================================================================
78
+ // TRIGGER SYNC — internal action to manually trigger calendar sync
79
+ // =============================================================================
80
+ exports.triggerSync = (0, server_1.internalAction)({
81
+ args: {
82
+ workspaceId: values_1.v.id('workspaces'),
83
+ entityId: values_1.v.string(),
84
+ },
85
+ handler: async (ctx, args) => {
86
+ const result = await ctx.runAction(api_1.internal.crm.calendarCron.syncCalendarEvents, {
87
+ workspaceId: args.workspaceId,
88
+ entityId: args.entityId,
89
+ });
90
+ return result;
91
+ },
92
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * Calendar Cron Dispatcher
4
+ *
5
+ * Finds all workspaces with Google Calendar sync enabled
6
+ * and triggers sync for each one.
7
+ *
8
+ * The cron calls this dispatcher every 5 minutes.
9
+ * The dispatcher then fans out to per-workspace sync actions.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getCalendarSyncWorkspaces = exports.dispatchCalendarSync = void 0;
13
+ const server_1 = require("../_generated/server");
14
+ const api_1 = require("../_generated/api");
15
+ // =============================================================================
16
+ // DISPATCH — find workspaces and fan out sync actions
17
+ // =============================================================================
18
+ exports.dispatchCalendarSync = (0, server_1.internalAction)({
19
+ args: {},
20
+ handler: async (ctx) => {
21
+ console.log('[CalendarCronDispatch] Starting dispatch...');
22
+ // Get all workspaces that have calendar sync configured
23
+ const workspaces = await ctx.runQuery(api_1.internal.crm.calendarCronDispatch.getCalendarSyncWorkspaces);
24
+ if (workspaces.length === 0) {
25
+ console.log('[CalendarCronDispatch] No workspaces with calendar sync enabled');
26
+ return { dispatched: 0 };
27
+ }
28
+ console.log('[CalendarCronDispatch] Dispatching sync for', workspaces.length, 'workspaces');
29
+ // Fan out sync actions for each workspace
30
+ let dispatched = 0;
31
+ for (const ws of workspaces) {
32
+ try {
33
+ await ctx.runAction(api_1.internal.crm.calendarCron.syncCalendarEvents, {
34
+ workspaceId: ws.workspaceId,
35
+ entityId: ws.entityId,
36
+ });
37
+ dispatched++;
38
+ }
39
+ catch (error) {
40
+ console.error('[CalendarCronDispatch] Failed for workspace:', ws.workspaceId, error);
41
+ }
42
+ }
43
+ console.log('[CalendarCronDispatch] Dispatched', dispatched, 'sync jobs');
44
+ return { dispatched };
45
+ },
46
+ });
47
+ // =============================================================================
48
+ // QUERY — get workspaces with calendar sync enabled
49
+ // =============================================================================
50
+ exports.getCalendarSyncWorkspaces = (0, server_1.internalQuery)({
51
+ args: {},
52
+ handler: async (ctx) => {
53
+ // Get all workspaces and filter to those with calendarSyncEnabled setting
54
+ const workspaces = await ctx.db.query('workspaces').collect();
55
+ const results = [];
56
+ for (const ws of workspaces) {
57
+ // Only sync workspaces that have calendar sync enabled
58
+ if (!ws.settings?.calendarSyncEnabled)
59
+ continue;
60
+ // Determine entityId: for personal workspaces use the owner's clerkId,
61
+ // for org workspaces use the orgId
62
+ let entityId;
63
+ if (ws.ownerType === 'user' && ws.ownerId) {
64
+ // Get the user's clerkId from the users table
65
+ const user = await ctx.db.get(ws.ownerId);
66
+ if (user) {
67
+ entityId = user.clerkId;
68
+ }
69
+ }
70
+ else if (ws.ownerType === 'organization' && ws.orgId) {
71
+ // For org workspaces, get the Clerk orgId from organizations table
72
+ const org = await ctx.db.get(ws.orgId);
73
+ if (org) {
74
+ entityId = org.clerkId;
75
+ }
76
+ }
77
+ if (entityId) {
78
+ results.push({ workspaceId: ws._id, entityId });
79
+ }
80
+ }
81
+ return results;
82
+ },
83
+ });
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ /**
3
+ * Calendar Sync — Mutations for upserting Google Calendar events
4
+ * and auto-linking attendees to contacts/companies.
5
+ *
6
+ * Sync flow: cron → Composio API → this module upserts into calendarEvents table.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.getMeetingBrief = exports.listByContact = exports.listUpcoming = exports.linkToContact = exports.syncEvents = void 0;
10
+ const server_1 = require("../_generated/server");
11
+ const values_1 = require("convex/values");
12
+ // =============================================================================
13
+ // VALIDATORS
14
+ // =============================================================================
15
+ const attendeeValidator = values_1.v.object({
16
+ email: values_1.v.string(),
17
+ name: values_1.v.optional(values_1.v.string()),
18
+ status: values_1.v.optional(values_1.v.string()),
19
+ });
20
+ const calendarEventValidator = {
21
+ googleEventId: values_1.v.string(),
22
+ calendarId: values_1.v.string(),
23
+ title: values_1.v.string(),
24
+ description: values_1.v.optional(values_1.v.string()),
25
+ location: values_1.v.optional(values_1.v.string()),
26
+ startTime: values_1.v.number(),
27
+ endTime: values_1.v.number(),
28
+ isAllDay: values_1.v.optional(values_1.v.boolean()),
29
+ attendees: values_1.v.optional(values_1.v.array(attendeeValidator)),
30
+ };
31
+ // =============================================================================
32
+ // SYNC EVENTS — upsert by googleEventId (internal, called by cron action)
33
+ // =============================================================================
34
+ exports.syncEvents = (0, server_1.internalMutation)({
35
+ args: {
36
+ workspaceId: values_1.v.id('workspaces'),
37
+ events: values_1.v.array(values_1.v.object(calendarEventValidator)),
38
+ },
39
+ handler: async (ctx, args) => {
40
+ const now = Date.now();
41
+ const results = {
42
+ upserted: 0,
43
+ linked: 0,
44
+ };
45
+ for (const event of args.events) {
46
+ // Check if event already exists by googleEventId
47
+ const existing = await ctx.db
48
+ .query('calendarEvents')
49
+ .withIndex('by_google_event', (q) => q.eq('googleEventId', event.googleEventId))
50
+ .first();
51
+ // Auto-link: match attendee emails to contacts and companies
52
+ let contactId;
53
+ let companyId;
54
+ if (event.attendees && event.attendees.length > 0) {
55
+ for (const attendee of event.attendees) {
56
+ if (!attendee.email)
57
+ continue;
58
+ // Match contact by email
59
+ if (!contactId) {
60
+ const contact = await ctx.db
61
+ .query('contacts')
62
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', args.workspaceId))
63
+ .filter((q) => q.eq(q.field('email'), attendee.email))
64
+ .first();
65
+ if (contact) {
66
+ contactId = contact._id;
67
+ // If contact has a company, use that too
68
+ if (contact.companyId) {
69
+ companyId = contact.companyId;
70
+ }
71
+ }
72
+ }
73
+ // Match company by domain (extract domain from email)
74
+ if (!companyId) {
75
+ const emailDomain = attendee.email.split('@')[1]?.toLowerCase();
76
+ if (emailDomain &&
77
+ !['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'icloud.com', 'protonmail.com'].includes(emailDomain)) {
78
+ const company = await ctx.db
79
+ .query('companies')
80
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', args.workspaceId))
81
+ .filter((q) => q.eq(q.field('domain'), emailDomain))
82
+ .first();
83
+ if (company) {
84
+ companyId = company._id;
85
+ }
86
+ }
87
+ }
88
+ // Stop early if both found
89
+ if (contactId && companyId)
90
+ break;
91
+ }
92
+ if (contactId || companyId)
93
+ results.linked++;
94
+ }
95
+ if (existing) {
96
+ // Update existing event
97
+ await ctx.db.patch(existing._id, {
98
+ title: event.title,
99
+ description: event.description,
100
+ startTime: event.startTime,
101
+ endTime: event.endTime,
102
+ attendees: event.attendees,
103
+ contactId,
104
+ companyId,
105
+ syncedAt: now,
106
+ });
107
+ }
108
+ else {
109
+ // Insert new event
110
+ await ctx.db.insert('calendarEvents', {
111
+ googleEventId: event.googleEventId,
112
+ title: event.title,
113
+ description: event.description,
114
+ startTime: event.startTime,
115
+ endTime: event.endTime,
116
+ attendees: event.attendees,
117
+ contactId,
118
+ companyId,
119
+ workspaceId: args.workspaceId,
120
+ syncedAt: now,
121
+ });
122
+ // Auto-create meeting activity if linked to a contact
123
+ if (contactId) {
124
+ await ctx.db.insert('activities', {
125
+ type: 'calendar-event',
126
+ subject: event.title,
127
+ description: event.description,
128
+ contactId,
129
+ companyId,
130
+ workspaceId: args.workspaceId,
131
+ ownerId: 'system',
132
+ actorType: 'agent',
133
+ actorId: 'agent:composio',
134
+ occurredAt: event.startTime,
135
+ createdAt: now,
136
+ updatedAt: now,
137
+ });
138
+ // Update lastContactedAt on contact
139
+ await ctx.db.patch(contactId, {
140
+ lastContactedAt: event.startTime,
141
+ updatedAt: now,
142
+ });
143
+ }
144
+ }
145
+ results.upserted++;
146
+ }
147
+ return results;
148
+ },
149
+ });
150
+ // =============================================================================
151
+ // LINK TO CONTACT — manually link a calendar event to a contact
152
+ // =============================================================================
153
+ exports.linkToContact = (0, server_1.mutation)({
154
+ args: {
155
+ eventId: values_1.v.id('calendarEvents'),
156
+ contactId: values_1.v.optional(values_1.v.id('contacts')),
157
+ companyId: values_1.v.optional(values_1.v.id('companies')),
158
+ dealId: values_1.v.optional(values_1.v.id('deals')),
159
+ },
160
+ handler: async (ctx, args) => {
161
+ const event = await ctx.db.get(args.eventId);
162
+ if (!event)
163
+ throw new Error('Calendar event not found');
164
+ const now = Date.now();
165
+ await ctx.db.patch(args.eventId, {
166
+ contactId: args.contactId,
167
+ companyId: args.companyId,
168
+ dealId: args.dealId,
169
+ });
170
+ return { success: true };
171
+ },
172
+ });
173
+ // =============================================================================
174
+ // QUERIES
175
+ // =============================================================================
176
+ /** List upcoming events for a workspace */
177
+ exports.listUpcoming = (0, server_1.query)({
178
+ args: {
179
+ workspaceId: values_1.v.id('workspaces'),
180
+ fromTime: values_1.v.optional(values_1.v.number()),
181
+ toTime: values_1.v.optional(values_1.v.number()),
182
+ limit: values_1.v.optional(values_1.v.number()),
183
+ },
184
+ handler: async (ctx, args) => {
185
+ const from = args.fromTime ?? Date.now();
186
+ const to = args.toTime ?? from + 7 * 24 * 60 * 60 * 1000; // default 7 days ahead
187
+ const events = await ctx.db
188
+ .query('calendarEvents')
189
+ .withIndex('by_workspace_start', (q) => q
190
+ .eq('workspaceId', args.workspaceId)
191
+ .gte('startTime', from)
192
+ .lte('startTime', to))
193
+ .order('asc')
194
+ .take(args.limit ?? 50);
195
+ return events;
196
+ },
197
+ });
198
+ /** List events linked to a specific contact */
199
+ exports.listByContact = (0, server_1.query)({
200
+ args: {
201
+ contactId: values_1.v.id('contacts'),
202
+ limit: values_1.v.optional(values_1.v.number()),
203
+ },
204
+ handler: async (ctx, args) => {
205
+ return await ctx.db
206
+ .query('calendarEvents')
207
+ .withIndex('by_contact', (q) => q.eq('contactId', args.contactId))
208
+ .order('desc')
209
+ .take(args.limit ?? 20);
210
+ },
211
+ });
212
+ /** Get pre-meeting brief: contact + deal context for next meeting */
213
+ exports.getMeetingBrief = (0, server_1.query)({
214
+ args: {
215
+ eventId: values_1.v.id('calendarEvents'),
216
+ },
217
+ handler: async (ctx, args) => {
218
+ const event = await ctx.db.get(args.eventId);
219
+ if (!event)
220
+ return null;
221
+ let contact = null;
222
+ let company = null;
223
+ let deal = null;
224
+ const recentActivities = [];
225
+ if (event.contactId) {
226
+ contact = await ctx.db.get(event.contactId);
227
+ // Get recent activities for this contact
228
+ const activities = await ctx.db
229
+ .query('activities')
230
+ .withIndex('by_contact', (q) => q.eq('contactId', event.contactId))
231
+ .order('desc')
232
+ .take(10);
233
+ recentActivities.push(...activities.map((a) => ({
234
+ type: a.type,
235
+ subject: a.subject,
236
+ occurredAt: a.occurredAt,
237
+ })));
238
+ }
239
+ if (event.companyId) {
240
+ company = await ctx.db.get(event.companyId);
241
+ }
242
+ if (event.dealId) {
243
+ deal = await ctx.db.get(event.dealId);
244
+ }
245
+ else if (event.contactId) {
246
+ // Try to find an active deal for this contact
247
+ deal = await ctx.db
248
+ .query('deals')
249
+ .withIndex('by_contact', (q) => q.eq('contactId', event.contactId))
250
+ .filter((q) => q.neq(q.field('isArchived'), true))
251
+ .first();
252
+ }
253
+ return {
254
+ event: {
255
+ title: event.title,
256
+ startTime: event.startTime,
257
+ endTime: event.endTime,
258
+ description: event.description,
259
+ attendees: event.attendees,
260
+ },
261
+ contact: contact
262
+ ? {
263
+ id: contact._id,
264
+ name: `${contact.firstName} ${contact.lastName}`,
265
+ email: contact.email,
266
+ phone: contact.phone,
267
+ type: contact.type,
268
+ jobTitle: contact.jobTitle,
269
+ notes: contact.notes,
270
+ lastContactedAt: contact.lastContactedAt,
271
+ }
272
+ : null,
273
+ company: company
274
+ ? {
275
+ id: company._id,
276
+ name: company.name,
277
+ domain: company.domain,
278
+ industry: company.industry,
279
+ size: company.size,
280
+ }
281
+ : null,
282
+ deal: deal
283
+ ? {
284
+ id: deal._id,
285
+ title: deal.title,
286
+ stage: deal.stage,
287
+ value: deal.value,
288
+ probability: deal.probability,
289
+ }
290
+ : null,
291
+ recentActivities,
292
+ };
293
+ },
294
+ });