Skip to content

Commit 28a28f8

Browse files
committed
update
1 parent fbf7ab8 commit 28a28f8

4 files changed

Lines changed: 147 additions & 13 deletions

File tree

apps/chat/lib/agent.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
cancelBooking,
88
confirmBooking,
99
createBooking,
10+
createBookingPublic,
1011
createEventType,
1112
createSchedule,
1213
declineBooking,
@@ -122,13 +123,13 @@ To book, you need these 4 pieces:
122123
4. ${bold}Slot is available${bold} — call check_availability ONCE
123124
124125
${bold}Option B — THEIR calendar (they host):${bold}
125-
The other person is the host. The requesting user is the attendee.
126+
The other person is the host. The requesting user (you) is the attendee.
126127
1. Ask for the other person's Cal.com username if not provided.
127128
2. Call \`list_event_types_by_username\` with their username.
128129
3. Show their event types and let the user pick. Note the \`slug\` from the result.
129130
4. Call \`check_availability_public\` with the event type \`slug\` and \`username\`. Do NOT use \`check_availability\` — that requires the host's auth token which you don't have.
130131
5. Present available slots and let the user pick.
131-
6. Call book_meeting with their event type ID. Use the requesting user's name + email (from Your Account above) as attendeeName/attendeeEmail.
132+
6. Call \`book_meeting_public\` (NOT book_meeting) with the event type slug + username. For attendeeName and attendeeEmail, use the ${bold}requesting user's${bold} name and email from "Your Account" above — the requesting user is the attendee in this flow. NEVER use the bot name "Cal.com" as attendeeName.
132133
133134
EVENT TYPE SELECTION:
134135
- If there is only 1 non-hidden event type, auto-select it. Tell the user which one you're using.
@@ -621,6 +622,85 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
621622
},
622623
}),
623624

625+
book_meeting_public: tool({
626+
description:
627+
"Book a meeting on ANOTHER user's Cal.com calendar using their public event type. Use this for Option B (they host). Does NOT require the host's auth token. Pass eventTypeSlug + username instead of eventTypeId. The attendee is the requesting user (you) — use YOUR name and email.",
628+
inputSchema: z.object({
629+
eventTypeSlug: z
630+
.string()
631+
.describe("The event type slug from list_event_types_by_username (e.g. 'meet')"),
632+
username: z
633+
.string()
634+
.describe("The Cal.com username of the host (e.g. 'peer')"),
635+
startTime: z
636+
.string()
637+
.describe("Start time in ISO 8601 UTC format (e.g. '2026-03-23T10:30:00Z')"),
638+
attendeeName: z
639+
.string()
640+
.describe("YOUR full name (the requesting user, not the host)"),
641+
attendeeEmail: z
642+
.string()
643+
.describe("YOUR email address (the requesting user, not the host)"),
644+
attendeeTimeZone: z
645+
.string()
646+
.nullable()
647+
.optional()
648+
.describe("Your timezone (e.g. 'Asia/Kolkata'). Defaults to your linked timezone."),
649+
guests: z
650+
.array(z.string())
651+
.nullable()
652+
.optional()
653+
.describe("Optional additional guest emails"),
654+
notes: z.string().nullable().optional().describe("Optional notes for the booking"),
655+
lengthInMinutes: z
656+
.number()
657+
.nullable()
658+
.optional()
659+
.describe("Duration in minutes. Only needed if the event type supports multiple durations."),
660+
}),
661+
execute: async ({
662+
eventTypeSlug,
663+
username,
664+
startTime,
665+
attendeeName,
666+
attendeeEmail,
667+
attendeeTimeZone,
668+
guests,
669+
notes,
670+
lengthInMinutes,
671+
}) => {
672+
const linked = await getLinkedUser(teamId, userId);
673+
674+
try {
675+
const booking = await createBookingPublic({
676+
eventTypeSlug,
677+
username,
678+
start: startTime,
679+
attendee: {
680+
name: attendeeName,
681+
email: attendeeEmail,
682+
timeZone: attendeeTimeZone ?? linked?.calcomTimeZone ?? "UTC",
683+
},
684+
guests: guests?.filter(Boolean) ?? undefined,
685+
notes: notes ?? undefined,
686+
lengthInMinutes: lengthInMinutes ?? undefined,
687+
});
688+
689+
return {
690+
success: true,
691+
bookingUid: booking.uid,
692+
title: booking.title,
693+
start: booking.start,
694+
end: booking.end,
695+
meetingUrl: booking.meetingUrl,
696+
attendees: booking.attendees.map((a) => ({ name: a.name, email: a.email })),
697+
};
698+
} catch (err) {
699+
return { error: err instanceof Error ? err.message : "Failed to create booking" };
700+
}
701+
},
702+
}),
703+
624704
add_booking_attendee: tool({
625705
description:
626706
"Add a full attendee record (name + timezone) to an existing booking. Use after book_meeting for additional attendees resolved via lookup_platform_user on Slack where you have full profile details.",
@@ -1155,6 +1235,7 @@ const CORE_TOOL_NAMES = new Set([
11551235
"check_availability",
11561236
"check_availability_public",
11571237
"book_meeting",
1238+
"book_meeting_public",
11581239
"add_booking_attendee",
11591240
"list_bookings",
11601241
"get_booking",

apps/chat/lib/bot.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -491,15 +491,6 @@ async function withBotErrorHandling(
491491
botLogger.warn("Feature not supported", err.feature);
492492
return;
493493
}
494-
if (isSlackAuthError(err)) {
495-
botLogger.error("Slack auth error — workspace may need reinstall", err);
496-
await options
497-
.postError(
498-
"The Slack app token has expired or been revoked. Please reinstall the app by visiting the Cal.com app settings."
499-
)
500-
.catch(() => {});
501-
return;
502-
}
503494
// AI/LLM rate limit (e.g. Groq tokens-per-day) — show friendly message
504495
if (isAIRateLimitError(err)) {
505496
botLogger.warn("AI rate limit", err);
@@ -518,10 +509,29 @@ async function withBotErrorHandling(
518509
.catch(() => {});
519510
return;
520511
}
521-
botLogger.error(options.logContext ? `Error in ${options.logContext}` : "Error", err);
512+
// Check for a custom error message from the handler (e.g. captured AI stream error)
513+
// before falling through to Slack auth — a Slack not_authed can be a secondary
514+
// failure caused by the stream crashing, not a real token issue.
522515
const customMsg = options.getCustomErrorMessage?.(err);
516+
if (customMsg) {
517+
botLogger.error(options.logContext ? `Error in ${options.logContext}` : "Error", err);
518+
await options.postError(customMsg).catch((postErr) => {
519+
botLogger.error("Failed to post error message to user", { postErr, originalErr: err });
520+
});
521+
return;
522+
}
523+
if (isSlackAuthError(err)) {
524+
botLogger.error("Slack auth error — workspace may need reinstall", err);
525+
await options
526+
.postError(
527+
"The Slack app token has expired or been revoked. Please reinstall the app by visiting the Cal.com app settings."
528+
)
529+
.catch(() => {});
530+
return;
531+
}
532+
botLogger.error(options.logContext ? `Error in ${options.logContext}` : "Error", err);
523533
await options
524-
.postError(customMsg ?? "Sorry, something went wrong. Please try again.")
534+
.postError("Sorry, something went wrong. Please try again.")
525535
.catch((postErr) => {
526536
botLogger.error("Failed to post error message to user", { postErr, originalErr: err });
527537
});

apps/chat/lib/calcom/client.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
CalcomSlot,
99
CalendarLink,
1010
CreateBookingInput,
11+
CreatePublicBookingInput,
1112
CreateEventTypeInput,
1213
CreateScheduleInput,
1314
SlotsResponse,
@@ -220,6 +221,34 @@ export async function createBooking(
220221
});
221222
}
222223

224+
export async function createBookingPublic(
225+
input: CreatePublicBookingInput
226+
): Promise<CalcomBooking> {
227+
const url = `${CALCOM_API_URL}/v2/bookings`;
228+
const res = await fetch(url, {
229+
method: "POST",
230+
headers: {
231+
"cal-api-version": "2024-08-13",
232+
"Content-Type": "application/json",
233+
},
234+
body: JSON.stringify(input),
235+
});
236+
if (!res.ok) {
237+
const body = await res.text();
238+
let message = `Booking failed (${res.status})`;
239+
try {
240+
const parsed = JSON.parse(body);
241+
message = parsed.error?.message ?? parsed.message ?? message;
242+
} catch { /* use default */ }
243+
throw new CalcomApiError(message, res.status);
244+
}
245+
const json = (await res.json()) as CalcomApiResponse<CalcomBooking>;
246+
if (json.status === "error") {
247+
throw new CalcomApiError(json.error?.message ?? "Booking failed", undefined, json.error?.code);
248+
}
249+
return json.data;
250+
}
251+
223252
export async function cancelBooking(
224253
accessToken: string,
225254
bookingUid: string,

apps/chat/lib/calcom/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ export interface CreateBookingInput {
8787
metadata?: Record<string, string>;
8888
}
8989

90+
export interface CreatePublicBookingInput {
91+
eventTypeSlug: string;
92+
username: string;
93+
start: string; // ISO 8601 UTC
94+
attendee: {
95+
name: string;
96+
email: string;
97+
timeZone: string;
98+
};
99+
guests?: string[];
100+
notes?: string;
101+
lengthInMinutes?: number;
102+
}
103+
90104
/** Metadata passed from Cal.com booking form for routing notifications. */
91105
export interface CalcomWebhookMetadata {
92106
/** Slack workspace/team ID for routing to Slack */

0 commit comments

Comments
 (0)